Match trailing keyword arguments to keyword/kwrest parameters, not by position - #1292
Match trailing keyword arguments to keyword/kwrest parameters, not by position#1292apiology wants to merge 3 commits into
Conversation
… position Call#inferred_pins matched call-site arguments to a signature's parameters purely by array index. A trailing keyword-arguments hash (e.g. use_ssl: true) is just another element in that array, so it was checked against whatever positional parameter happened to sit at that index instead of the method's keyword or kwrest parameter. The type mismatch rejected the whole overload, so a generic block-form overload lost its block-param typing whenever the call also passed a keyword argument. Confirmed independent of PR castwide#1289: reproducible on master before that fix, and with a plain YARD **kwrest method (no RBS involved). Split the keyword-arguments hash out of the positional list and match it against the signature's keyword?/kwrestarg? parameters instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vue2FLfnJ3M7pM1oGdYiZy
|
Claude: This regresses overload resolution for any method whose overloads differ only in which keyword(s) they accept, when an earlier overload also has a require 'rbs'
class Foo
# @return [void]
def bar
loader = RBS::EnvironmentLoader.new
loader.add(path: Pathname.new('.'))
end
end
|
keyword_argument_matches? inferred a single type for the entire trailing keyword-arguments hash and asked whether ANY of an overload's keyword/kwrest parameters accepted that type. A parameter whose type didn't resolve (e.g. an RBS `untyped` keyword) trivially "matched" via Parameter#compatible_arg?'s undefined-type bypass, regardless of which keys the call actually passed. This let an earlier overload's unrelated keyword steal the match from a later, correct overload whenever it had such a parameter, e.g. RBS::EnvironmentLoader#add(path:) getting matched against add(library:, ?resolve_dependencies: boolish) because resolve_dependencies has no resolvable type. Fix it by walking the hash literal's actual keys and checking each one against the identically-named parameter (or the kwrest parameter, if present), and requiring all non-optional keyword parameters to be present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyGKiHX7YHkwAoX4fHe3nU
…st parameters, not by position A trailing keyword-arguments hash doesn't line up positionally with the method's declared parameters, so Chain::Call now splits it off and matches it separately against the overload's keyword/kwrest parameters instead of checking it positionally by index. Follow-up commit fixes a regression this introduced: matching by inferring a single type for the whole trailing hash let a parameter with an unresolvable type (e.g. an RBS `untyped` keyword) trivially "match" via Parameter#compatible_arg?'s undefined-type bypass, regardless of which keys the call actually passed - stealing the match from a later, correct overload (e.g. RBS::EnvironmentLoader#add(path:) matching against add(library:, ?resolve_dependencies: boolish) instead). Now walks the hash literal's actual keys and checks each one against the identically-named parameter (or kwrest, if present), requiring all non-optional keyword parameters to be present. Removes 2 @sg-ignore comments the fix made unneeded. One residual, narrower issue remains and is marked `pending` in spec/type_checker/levels/strong_spec.rb's "understands Open3 methods" test with a citation to castwide#1292's PR comment: a pre-existing bypass in positional_arguments_match? (param.restarg? accepts any argument type against a *rest param) lets an earlier, positionally-wrong overload "match" and win via first-match-wins before a later, correct overload is tried. This predates castwide#1292 (masked previously by less precise position-index matching) and doesn't surface anywhere in solargraph's own codebase - confirmed via a clean full-project strong typecheck - only in this one synthetic Open3.capture2e regression test.
| api_map = Solargraph::ApiMap.new | ||
| source = Solargraph::Source.load_string(code, 'test.rb') | ||
| source_map = Solargraph::SourceMap.map(source) | ||
| source_map.send(:convention_pins=, conversions.pins) |
There was a problem hiding this comment.
Let's not introduce use of private methods here.
There was a problem hiding this comment.
Done in 0beed30. Both call sites are gone ��� this one and a second one added later in the "differ only in which keyword(s) they accept" context.
The pins now arrive through the public Solargraph::Convention extension point: RbsPinConvention < Solargraph::Convention::Base returns them from #local, and each example registers and unregisters it. That is the path a real gem's pins already travel, via Convention.for_local in SourceMap#initialize, so the setup exercises the supported mechanism instead of reaching past it.
ApiMap#index was the other public candidate, and it is what spec/convention/activesupport_concern_spec.rb uses, but it does not fit here: ApiMap#catalog rebuilds the store from the core map, doc map, conventions environ, and the bench's iced and live pins, so pins supplied through #index are dropped as soon as a source is cataloged. These contexts need both the pins and a mapped source.
The two contexts duplicated their tmpdir, loader, and infer_at helper, so that setup moved into a shared context.
This comment was written by Claude Code on behalf of @apiology.
…iter The RBS-backed contexts in call_spec.rb reached the pins they needed with source_map.send(:convention_pins=, conversions.pins). That writer is private on SourceMap, so the setup depended on an implementation detail rather than a supported entry point. Serve the same pins from a Convention instead. RbsPinConvention returns them from #local, and each example registers and unregisters it, so the pins arrive through Convention.for_local in SourceMap#initialize - the path a real gem's pins already travel. ApiMap#index is the other public option and is what activesupport_concern_spec.rb uses, but it does not fit here: ApiMap#catalog rebuilds the store from the core map, doc map, conventions environ, and the bench's iced and live pins, so pins supplied through #index are dropped once a source is cataloged. These contexts need both the pins and a mapped source. The two contexts shared their tmpdir, loader, and infer_at helper, so that setup moves into a shared context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXmnT5gSB1PheL9UbiGEVA
|
Measured with the repro above, under
The mechanism was not the one described here. After What actually happened was in I also checked the shape described here directly ��� an earlier overload carrying both a class Box
def self.pick: (*Integer vals, alpha: String) -> Integer
| (beta: String) -> String
end
The Regression coverage for the This comment was written by Claude Code on behalf of @apiology. |
One conflict in spec/source/chain/call_spec.rb: this branch added a standalone example just above the block the PR converts from a context into a shared_context served through the public Convention API. Kept the example and took the PR shared_context opener; both include_context call sites come from the PR side.
Summary
Call#inferred_pinsmatched call-site arguments to a signature's parameters purely by array index. A trailing keyword-arguments hash (e.g.use_ssl: true) is just another element in that array, so it was checked against whatever positional parameter happened to sit at that index instead of the method's keyword or kwrest parameter. The type mismatch rejected the whole overload, so a generic block-form overload lost its block-param typing whenever the call also passed a keyword argument.Found while following up on #1290, which reported this against
Net::HTTP.start, an RBS-declared generic block-form overload:Dropping
use_ssl: trueresolveshttpcorrectly, matching the original report.Confirmed this is independent of #1290/#1289: it reproduces on
masterbefore that PR's commit, and with a plain YARD**kwrestmethod that has no RBS involvement at all — the gap is in call-site argument matching (Call#inferred_pins,lib/solargraph/source/chain/call.rb), not in RBS translation.Fix
Split the trailing keyword-arguments hash out of the positional argument list and match it against the signature's
keyword?/kwrestarg?parameters instead of by position.Test plan
spec/source/chain/call_spec.rbcases using an inline RBS signature (Box.start) shaped likeNet::HTTP.start(multiple optional positional params before a generic block-form overload with**kwrest), covering: kwarg matches the kwrest param, no-kwarg baseline still works.bundle exec rspec— 1626 examples, 0 failuresbundle exec rubocopon changed files — no new offensesbundle exec solargraph typecheck --level strongon changed files — 0 problems