Skip to content

Allow less strict attribute argument parsing - #1793

Open
nevans wants to merge 2 commits into
ruby:masterfrom
nevans:attribute-initial_symbol_arguments
Open

Allow less strict attribute argument parsing#1793
nevans wants to merge 2 commits into
ruby:masterfrom
nevans:attribute-initial_symbol_arguments

Conversation

@nevans

@nevans nevans commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The prism parser is strict about attribute arguments: it only parses as an attribute when all arguments are symbols. rdoc 7.2's parser simply ignored any arguments after the initial symbol arguments.

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:

      # 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.

@nevans
nevans requested a deployment to fork-preview-protection August 28, 2026 14:29 — with GitHub Actions Waiting
@st0012 st0012 added the bug label Aug 29, 2026
Comment thread lib/rdoc/parser/ruby.rb Outdated
def _visit_call_attr_reader_writer_accessor(call_node, rw)
return if @scanner.in_proc_block
names = symbol_arguments(call_node)
names = initial_symbol_arguments(call_node)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel symbol_arguments should already work like initial_symbol_arguments.

@tompng why was it designed to return nil when there's any non-symbol arguments? What's the case we're guarding against?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To handle alias_method.
We need to prevent alias_method :foo, bar, :baz, blah: true treated as alias_method :foo, :baz.
In all other case, we can just use the non-strict version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like maybe expanding the scope to everything other than attributes either deserves separate PRs (automatically calling out in separate line items in generated release notes), or (at the very least) renaming this PR. :)

Comment thread lib/rdoc/parser/ruby.rb Outdated

def initial_symbol_arguments(call_node)
return unless arguments = call_node.arguments&.arguments
symbol_args = arguments.take_while {|arg| arg.is_a?(Prism::SymbolNode) }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using arguments.grep(Prism::SymbolNode) instead of take_while and renaming the method?
I think it's worth adding a test like this, which is possible in the original attr_accessor spec

# document :foo, :bar and :baz
attr_accessor :foo, *ignored1, :bar, (ignored2), :baz

@nevans nevans Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was trying to be a little bit conservative, since I wasn't sure if the change was intentional. But I'm even happier with that.

I feel like maybe both methods should be renamed, to avoid ambiguity confusion...

  • this grep-based variation => any_symbol_arguments or every_symbol_argument
    (or grep_symbol_arguments? I prefer "any".)
  • symbol_arguments => exclusively_symbol_arguments?

I (originally) added "node" to the names to emphasize that this is only matching symbol literal nodes.
(edited to add) But I dropped it in the version I pushed, both to be consistent with the original naming, and also because the methods are returning symbols and only working with literal SymbolNode is an implementation detail that can be inferred from context.

@nevans nevans Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to preserve any form of test_undocumentable_attributes? That is, what if the arguments aren't variables or expressions that're too dynamic or too complex to extract names from, but are instead integers or other literals which suggest an entirely different API is being used?

I'm updating the PR to just repurpose that test for your grep suggestion. But that test case (added in fde99f1) makes me think that you maybe had a specific scenario you wanted to prohibit?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, there were two possible design:

  1. Only accept attr_accessor :a, :b and reject attr_accessor :a, opt:
  2. Accept attr_accessor :a, 1, expr, :b, true and also attr_accessor :a, opt:

And I selected the first one. test_undocumentable_attributes is testing this design decision.
IMO, if were going to be conservative, reject a usage that might be a completely different API, attr_accessor :a, opt: is one of the case that would be rejected. It's not ruby core attr_accessor usage. It's not a bug.
I think it's OK to change the design from 1 to 2, and allow any dynamic expression mixed with symbols.

@nevans
nevans force-pushed the attribute-initial_symbol_arguments branch from b176b3a to 4285ba7 Compare August 30, 2026 16:12
@nevans
nevans requested a deployment to fork-preview-protection August 30, 2026 16:12 — with GitHub Actions Waiting
nevans added 2 commits August 30, 2026 12:16
The prism parser is strict about attribute arguments: it only parses as
an attribute when _all_ arguments are symbols.  rdoc 7.2's parser simply
ignored any non-symbol arguments.

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 ruby#1790.
This makes the distinction between this method and
`any_symbol_arguments` more explicit.
@nevans
nevans force-pushed the attribute-initial_symbol_arguments branch from 4285ba7 to c805f78 Compare August 30, 2026 16:17
@nevans
nevans requested a deployment to fork-preview-protection August 30, 2026 16:17 — with GitHub Actions Waiting
@nevans

nevans commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

@st0012 @tompng As a separate question, do we want to also allow string literal arguments? Because I'm pretty sure that all of the Module methods in question (attr, attr_reader, attr_writer, attr_accessor, alias_method, public, private, protected, module_function, private_class_function, public_class_function, private_constant, public_constant) also support string arguments. The only ones I was unsure about were the const visibility methods, and I just tested those (they work with strings).

BUT it's pretty weird to use string literals rather than symbol literals. Sure, for convenience, maybe we sometimes pass some variables or expressions into them which might evaluate to strings, to avoid the extra .to_sym calls. But using string literals just isn't done (at least, I haven't seen it in long time)!

So if it is done, maybe there's a reason, and maybe (for some of these) that reason could be that it's a simple (if somewhat hacky) way to avoid rdoc from picking up on them?

But I dunno... I can only really see that argument working for the attribute methods and alias_method. For the visibility methods, I think we should err on the side of reading every visibility change we can reasonably parse. So I'd want the visibility methods to support StringNode in addition to SymbolNode.

Anyway, after talking myself through it (and writing it down above), I'm leaning towards allowing both SymbolNode and StringNode for all of these. What do you think?

@nevans

nevans commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Related to that, I feel like maybe the following should not be ignored, but should issue a warning instead?

private def self.foo = bar
# => WARNING: "private def self.foo" makes "M#foo" private, not "M.foo"
private_class_method def foo = bar
# => WARNING: "private_class_method def foo" makes "M.foo" private, not "M#foo"
private def to_s.foo = bar
# => WARNING: "private def (expr).foo" makes "M#foo' private, not "(expr).foo"

Because def (expr).foo always returns :foo, all of those will make a(n existing) method private. Even if that isn't what the user expected, it's what they've done.

That's more scope than I want to push into this PR, but I'm curious about your thoughts?

@tompng

tompng commented Aug 30, 2026

Copy link
Copy Markdown
Member

Accepting StringNode is the original behavior in the old Ripper based parser. The Prism based parser just ported that behavior.

I think there is no strong reason to keep that behavior because string isn't used.
Actual code found on GitHub:
https://github.com/search?q=%22attr_accessor%28%5C%22%22+language%3ARuby&type=code&l=Ruby
Other: Dynamic string, RuboCop BAD example comment, test code.

I'm fine either way - keeping or dropping the behavior accepting StringNode.

private def self.foo = bar

I think it's OK to do that: Warn and make instance method foo private if it exist.

@nevans

nevans commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Accepting StringNode is the original behavior in the old Ripper based parser. The Prism based parser just ported that behavior.

@tompng Just to be clear: the Prism based parser does not currently accept string arguments for these methods. I think you're saying the old parser did support string arguments. So you're okay with me adding support for them to the Prism parser?

@nevans

nevans commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

It looks like the current prism parser supports either StringNode or SymbolNode as the first argument for meta method comments (call_node_name_arguments) (edited to add:) or for meta programmed attributes (with an attribute directive).

@st0012

st0012 commented Aug 30, 2026

Copy link
Copy Markdown
Member

I think we should accept string arguments in those calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser should be less strict about attribute arguments

3 participants