From cc11a609ee997007a51cc86c0a9174e78a29c6c2 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 6e943e5d6d..0e7f616936 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -1232,6 +1232,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)} @@ -1346,8 +1351,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 + return unless names = call_node_name_arguments(call_node) + @scanner.add_attributes(names, rw, call_node.location.start_line) end class MethodSignatureVisitor < Prism::Visitor # :nodoc: diff --git a/test/rdoc/parser/ruby_test.rb b/test/rdoc/parser/ruby_test.rb index 3ebfd5891b..b5ead1eb2e 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -1580,14 +1580,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 @@ -1614,16 +1614,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 f4607d26d8db4ca8d96b819028bf083e8ad5f5c4 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 0e7f616936..c7a59a7cde 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -413,7 +413,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 b5ead1eb2e..a57f77d398 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -1755,19 +1755,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: