Skip to content

Check keyword arguments passed through a ** splat - #63

Draft
apiology wants to merge 2 commits into
fix-1229-intersection-typesfrom
kwsplat-record-types
Draft

Check keyword arguments passed through a ** splat#63
apiology wants to merge 2 commits into
fix-1229-intersection-typesfrom
kwsplat-record-types

Conversation

@apiology

Copy link
Copy Markdown
Owner

Passing a hash through ** currently produces one false missing keyword argument finding per declared keyword, because the splatted hash's keys are invisible to the checker. This makes those calls checkable instead: a record-typed splat has its required keywords, value types and unrecognised keys verified for real, and an opaque splat produces a single diagnostic in place of N false ones — only where a required keyword is supplied by neither a literal key nor a record type. An opaque splat into a method with only optional keywords stays silent.

The enabling change is in ComplexType::UniqueType#qualify, which widened every literal, so Hash{:a => Integer} reached the checker as Hash{Symbol => Integer} and record types were erased before anything could use them. Preserving literals wholesale broke 11 specs, so this narrows it to keys only. Intersection gets its own qualify, since it holds conjuncts rather than key types and subtypes.

Stacked on castwide#1231 and cannot merge before it.

This PR was written by Claude Code on behalf of @apiology.

apiology and others added 2 commits August 20, 2026 09:12
ComplexType::UniqueType#qualify widened every literal type to its class,
so a record type written as Hash{:a => Integer} & Hash{:b => String}
arrived at any consumer as Hash{Symbol => Integer} & Hash{Symbol =>
String} - the keys, which are the whole content of a record type, were
gone by the time inference finished.

Literal keys are now qualified by hand and kept; literals in any other
position still widen, which is what castwide#1201 disabled literal inference
for. Intersection holds conjuncts rather than key_types/subtypes, so it
qualifies those instead of inheriting the walk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AsvDi68YqsKoBtS2kg9ch
--level strong reported every required keyword of a call as missing when
the keywords came through a ** splat of a hash whose type records no
keys, e.g. foo(**args) where args is a Hash{Symbol => Integer}. The keys
are not in the type, so the checker could not see whether they were
supplied; reporting them as absent was a claim it had no basis for.

Two behaviours replace that one. When the splatted value has a record
type - Hash{:a => Integer} & Hash{:b => String} - its keys are checked
like literal keywords: required keywords must be present, values must
conform to the parameter types, and a key the method does not accept is
reported as unrecognized. When the type records no keys, the call gets

  Cannot verify keyword arguments to #foo: the ** splat is
  Hash{Symbol => Integer}, which does not record its keys, so required
  keyword arguments a, b cannot be checked - give the splatted value a
  record type (e.g. Hash{:a => Object}) to check it

once per call rather than one "missing keyword" per parameter.

convert_hash also discarded literal keys whenever a kwsplat shared the
hash node, so foo(a: 1, **args) lost a, and it turned the splatted
variable's own name into a key, which is what produced "Unrecognized
keyword argument kwargs" on super(name: name, **kwargs) - the two
@sg-ignore comments that suppressed it are gone. hash_is_splatted? now
looks at every child rather than only the last.

Two @sg-ignore comments on the calls that destructure
keyword_splat_types record a separate Solargraph defect: multiple
assignment from an Array(A, B) return type gives every variable the type
of the first element. Declaring the type of par cleared eight existing
findings in kwarg_problems_for and left one about the declaration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AsvDi68YqsKoBtS2kg9ch
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.

1 participant