Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ruby 2.7 **nil syntax #3330

Closed
wants to merge 4 commits into from

Conversation

Morriar
Copy link
Collaborator

@Morriar Morriar commented Jul 29, 2020

Motivation

Ruby 2.7 introduces the no-keyword-arguments syntax **nil.

You can use **nil in a method definition to explicitly mark that the method accepts no keyword arguments. Calling such methods with keyword arguments will result in an ArgumentError.

def foo(*args, **nil)
end


foo(k: 1)
  #=> Ruby 2.7 or later: no keywords accepted (ArgumentError)

This pull request makes Sorbet support the **nil syntax and raise errors when keyword arguments are passed to methods that use it. See the many examples in test/testdata/infer/kwnil.rb.

Approach

I wanted to avoid touching the way arguments were handled just for **nil, so I figured the easiest way was to just remove it completely from the args list.

  1. During parsing, a node is created for **nil and added to the arguments list
  2. During desugar, the node is removed from the args list and the method definition is tagged as using **nil
  3. During namer, we move that information from the method definition to the symbol so it can be accessed during typing
  4. Finally, during typing, that information is used to raise an error for any call with keyword arguments to a method symbol tagged **nil (I reused the MethodArgumentCountMismatch error)

**nil in procs and lambda

With Ruby 2.7, you can also use **nil in procs and lambdas:

m = ->(a, **nil) {}
m.call(10, b: 20) # ArgumentError (no keywords accepted)

While this PR changes the parser so it does not create a syntax error, the **nil is still not checked in procs and lambdas.
We can store the information in the block definition but then the call is a special case with Proc.call and I'm not sure how to get back the **nil tag from the block at this point.

Test plan

See included automated tests.

This PR can be read commit by commit.

@Morriar Morriar requested a review from a team as a code owner July 29, 2020 21:05
@Morriar Morriar requested review from jez and removed request for a team July 29, 2020 21:05
@Morriar Morriar mentioned this pull request Jul 30, 2020
This commit tracks upstream commit ruby/ruby@6a9ce1f.

Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
When found, we save the state in the MethodDef.

Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
e.addErrorLine(method.data(gs)->loc(), "`{}` defined with `{}` here", data->show(gs), "**nil");
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks to me like this test case is allowed in Ruby 2.7:

def foo(x, **nil)
  p x
end

x = {hello: :world}
foo(x)

but I think after the changes in this PR Sorbet will reject this code, right?

Copy link
Collaborator

@jez jez left a comment

Choose a reason for hiding this comment

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

@Morriar Are you down to land the changes to expand the parser and the changes to make use of the new **nil syntax downstream in infer separately? I have a couple of reasons:

  • We don't like adding more bits to ast nodes (like the isKwnil bit that was added here)
  • We don't like adding new flags to the symbol table (like the isMethodWitKwnil flag here)
  • I don't think the change as implemented current models Ruby particularly well (it emits too many errors than it should)

The **nil seems to be me to be a property of a method's arity / arguments, but we're modeling it as a bit on a method symbol, not stored anywhere near the arguments. It might be that's the best place for it, but I'm not sure at a glance, and I think the parser changes should be enough to be permissive with this new syntax, and we can take more time to think about what the semantics should be.

@Morriar
Copy link
Collaborator Author

Morriar commented Sep 10, 2020

@Morriar Are you down to land the changes to expand the parser and the changes to make use of the new **nil syntax downstream in infer separately?

Sure, I opened #3412 with only the parser bits.

To be honest, Sorbet already handles those errors through the signature validation. The infer part of this PR is mostly to catch those errors earlier and display a different error message.

The **nil seems to be me to be a property of a method's arity / arguments, but we're modeling it as a bit on a method symbol, not stored anywhere near the arguments.

For me it felt more like a property of the method symbol itself than something related to a particular argument. The syntax ties it to an arg but it if we had to write a Sorbet signature builder for it we would most likely have added it the same way we did for abstract or override.

@Morriar
Copy link
Collaborator Author

Morriar commented Sep 10, 2020

Keeping this PR open until we figure out what to do with the infer part.

@jez
Copy link
Collaborator

jez commented Sep 10, 2020

Keeping this PR open until we figure out what to do with the infer part.

Sounds good to me. Just chatted with @elliottt about this, and it's starting to sound like we'll have to significantly revamp how Sorbet treats keyword args, at which point supporting **nil would get a lot easier.

So I think we're good to keep this open until then.

Another thing so I don't forget it:

Trevor and I were discussing an idea where instead of having the isKwnil flag on the ast::MethodDef, we desugar a parser::Kwnilarg node to ast::RestArg { ast::KeywordArg { ast::Local { name = <nil> } } }, i.e., use a special variable name, and keep it as a parameter in the tree. (just for future reference)

Thanks again for getting the parser support broken out into a separate PR.

@Morriar Morriar closed this Nov 27, 2020
@Morriar Morriar deleted the at-ruby27-kwnil branch July 26, 2022 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants