From ce0a4e48ec1d2ed1882ac610ef8a560365b81ed4 Mon Sep 17 00:00:00 2001 From: nick evans Date: Fri, 28 Aug 2026 10:23:52 -0400 Subject: [PATCH 1/2] Allow less strict attribute argument parsing This updates argument parsing for the `attr`, `attr_reader`, `attr_writer`, and `attr_accessor` methods, so they behave more like rdoc 7.2's parser. The prism parser is strict about attribute arguments: it only parses as an attribute when _all_ arguments are symbols. rdoc 7.2's parser allowed all symbol or string arguments and ignored the rest. As an example, the rdoc for `Net::IMAP::Config` intentionally took advantage of the looser parsing done by rdoc 7.2. That class redefines `attr_reader`, `attr_writer` and `attr_accessor` to add keyword arguments for type validation/coercion and defaults: ```ruby # Seconds to wait until a connection is opened. # # Applied separately for establishing TCP connection and starting a TLS # connection. # # If the IMAP object cannot open a connection within this time, # it raises a Net::OpenTimeout exception. # # See Net::IMAP.new and Net::IMAP#starttls. # # The default value is +30+ seconds. attr_accessor :open_timeout, type: Integer, default: 30 ``` rdoc 7.2 simply ignored the unknown keyword args, and parses this no differently from `attr_accessor :open_timeout.` Fixes #1790. --- lib/rdoc/parser/ruby.rb | 9 +++++-- test/rdoc/parser/ruby_test.rb | 44 ++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index 10a8cf840e..3d5b1ba80f 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -1215,6 +1215,11 @@ def constant_arguments_names(call_node) names.all? ? names : nil end + def call_node_name_arguments(call_node) + names = @scanner.call_node_name_arguments(call_node).compact + names unless names.empty? + end + def symbol_arguments(call_node) arguments_node = call_node.arguments return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)} @@ -1329,8 +1334,8 @@ def _visit_call_private_constant(call_node) def _visit_call_attr_reader_writer_accessor(call_node, rw) return if @scanner.in_proc_block - names = symbol_arguments(call_node) - @scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names + names = call_node_name_arguments(call_node) + @scanner.add_attributes(names, rw, call_node.location.start_line) if names end class MethodSignatureVisitor < Prism::Visitor # :nodoc: diff --git a/test/rdoc/parser/ruby_test.rb b/test/rdoc/parser/ruby_test.rb index 40cc6f4893..8b20fbd9eb 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -1605,14 +1605,14 @@ class Foo # attrs attr :attr1, :attr2 # readers - attr_reader :reader1, :reader2 + attr_reader :reader1, "reader2" # writers - attr_writer :writer1, :writer2 + attr_writer "writer1", :writer2 # accessors attr_accessor :accessor1, :accessor2 # :stopdoc: attr :attr3, :attr4 - attr_reader :reader3, :reader4 + attr_reader :reader3, "reader4" attr_writer :write3, :writer4 attr_accessor :accessor3, :accessor4 end @@ -1639,16 +1639,44 @@ class Foo assert_equal [@top_level] * 8, [a1, a2, r1, r2, w1, w2, rw1, rw2].map(&:file) end - def test_undocumentable_attributes + def test_ignored_undocumentable_attributes util_parser <<~RUBY class Foo - attr - attr 42, :foo + # attrs + attr :attr1, *ignored1, :attr2, (ignored2), kwarg: :ignored3 + # readers + attr_reader ignored3, :reader1, ignored4, :reader2, kw: ignored5 + # writers + attr_writer :writer1, *%i[ignored6], :writer2, kwarg: :ignored7 + # accessors + attr_accessor ignored8, :accessor1, (:ignored9), :accessor2, kw: :ignored10 + # ignored + attr ignored11 + attr_reader ignored12 + attr_writer ignored13 + attr_accessor ignored14 end RUBY klass = @store.find_class_named 'Foo' - assert_empty klass.method_list - assert_empty klass.attributes + assert_equal 8, klass.attributes.size + a1, a2, r1, r2, w1, w2, rw1, rw2 = klass.attributes + assert_equal ['attr1', 'attr2'], [a1.name, a2.name] + assert_equal ['reader1', 'reader2'], [r1.name, r2.name] + assert_equal ['writer1', 'writer2'], [w1.name, w2.name] + assert_equal ['accessor1', 'accessor2'], [rw1.name, rw2.name] + assert_equal ['R', 'R'], [a1.rw, a2.rw] + assert_equal ['R', 'R'], [r1.rw, r2.rw] + assert_equal ['W', 'W'], [w1.rw, w2.rw] + assert_equal ['RW', 'RW'], [rw1.rw, rw2.rw] + assert_equal ['attrs', 'attrs'], [a1.comment.text, a2.comment.text] + assert_equal ['readers', 'readers'], [r1.comment.text, r2.comment.text] + assert_equal ['writers', 'writers'], [w1.comment.text, w2.comment.text] + assert_equal ['accessors', 'accessors'], [rw1.comment.text, rw2.comment.text] + assert_equal [3, 3], [a1.line, a2.line] + assert_equal [5, 5], [r1.line, r2.line] + assert_equal [7, 7], [w1.line, w2.line] + assert_equal [9, 9], [rw1.line, rw2.line] + assert_equal [@top_level] * 8, [a1, a2, r1, r2, w1, w2, rw1, rw2].map(&:file) end def test_singleton_class_attributes From c72644446cce8b1c95859d4fa57ab0a310edc1f0 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 30 Aug 2026 14:32:16 -0400 Subject: [PATCH 2/2] Don't create meta attributes with no name This updates inferred attribute name parsing for the `:attr:`, `:attr_reader:`, `:attr_writer:`, and `:attr_accessor:` directives, to avoid creating unnamed attributes from unparsable arguments. Previously, an unnamed (nil) attribute would be created. Now, any non-string/non-symbol arguments are simply ignored. --- lib/rdoc/parser/ruby.rb | 2 +- test/rdoc/parser/ruby_test.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index 3d5b1ba80f..a4d363082c 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -412,7 +412,7 @@ def handle_meta_method_comment(comment, directives, node) case directive when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor' attributes = [param] if param - attributes ||= call_node_name_arguments(node) if is_call_node + attributes ||= call_node_name_arguments(node).compact if is_call_node rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R' when 'method' method_name = param if param diff --git a/test/rdoc/parser/ruby_test.rb b/test/rdoc/parser/ruby_test.rb index 8b20fbd9eb..e3e7fd712f 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -1780,19 +1780,19 @@ class Foo ## # :attr: # attrs - add_my_method :attr1, :attr2 + add_my_method :attr1, "attr2", (ignored) ## # :attr_reader: # readers - add_my_method :reader1, :reader2 + add_my_method :reader1, ignored, "reader2" ## # :attr_writer: # writers - add_my_method :writer1, :writer2 + add_my_method :writer1, :writer2, kwarg: ignored ## # :attr_accessor: # accessors - add_my_method :accessor1, :accessor2 + add_my_method ignored, :accessor1, :accessor2 # :stopdoc: