Skip to content

Synthesize a Struct.new constructor pin for gem-sourced constants - #1328

Open
apiology wants to merge 5 commits into
castwide:masterfrom
apiology:fix-gem-struct-new-constructor-pin-deliverable
Open

Synthesize a Struct.new constructor pin for gem-sourced constants#1328
apiology wants to merge 5 commits into
castwide:masterfrom
apiology:fix-gem-struct-new-constructor-pin-deliverable

Conversation

@apiology

@apiology apiology commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Foo = Struct.new(:bar, :baz) (and class Foo < Struct.new(...)) sourced from a gem, not workspace code -- YARD's Ruby handler registers Foo as a real ClassObject with accessor pins for each member, but never documents an initialize. Solargraph::YardMap::Mapper has no constructor pin to combine, so Foo.new(...) falls back to resolving against Struct.new's own RBS signature -- the nearest ancestor method it can find -- and reports a wrong-argument-type error for ordinary positional construction.

Convention::StructDefinition already covers this for workspace source, building an initialize pin from the parsed Struct.new node. This adds the gem/yardoc equivalent (ToStructInitializer), where there's no such node -- only the member names YARD kept in ClassObject#attributes.

keyword_init: true is detected by rereading the source line YARD points at (YARD itself discards the flag), first detecting the file's declared magic-comment encoding the way Ruby's own parser does, so a gem source line declared e.g. ISO-8859-1 decodes correctly rather than being misread as the default external encoding.

Test plan

  • 8 specs in spec/yard_map/mapper/to_struct_initializer_spec.rb, including the ISO-8859-1 encoding case and one genuinely-unrecoverable case (declared encoding doesn't match actual bytes)
  • bundle exec rspec (full suite): 1633 examples, 1 pre-existing failure (confirmed unrelated via stash A/B)
  • bundle exec rubocop: no offenses

Claude: filed on behalf of the repo owner while auditing @sg-ignore suppressions in a downstream project.

YARD's Ruby handler for `Foo = Struct.new(:bar, :baz)` (and the
inheritance form `class Foo < Struct.new(...)`) registers the struct
as a real ClassObject with accessor methods for each member, but never
documents an initialize method. Solargraph::YardMap::Mapper then has
no constructor pin to combine for the resulting class, so
`Foo.new(...)` falls back to resolving against Struct.new's own RBS
signature -- the nearest ancestor method it can find -- and reports a
wrong-argument-type error for ordinary positional Struct construction.

Convention::StructDefinition already covers this for workspace source
by building its own initialize pin from the parsed Struct.new node.
ToStructInitializer covers the equivalent gem/yardoc case, where there
is no such node -- only the member names YARD kept in
ClassObject#attributes.

keyword_init: true is detected by rereading the source line YARD
points at, since YARD itself discards that flag. File.readlines
defaults to Encoding.default_external and does not honor a Ruby magic
encoding comment the way compiling the file would, so the reread first
detects a declared encoding the same way Ruby's own parser does
(shebang-aware, first one or two lines, ASCII-safe) and rereads with
it -- correctly decoding, for example, a gem source line declared
ISO-8859-1. keyword_init?'s rescue is narrowed to ArgumentError, for
the case that's still genuinely unrecoverable: a magic comment whose
declared encoding doesn't match the file's actual bytes. What was
caught is logged via Solargraph.logger.info, matching
to_class_definition.rb's sibling rescue.

The Fixnum/Integer RBS union-shadowing finding this surfaced (a
diff-lcs-shipped Fixnum shim gets YARD-indexed as a near-empty class
that shadows Integer in a union type) is recorded in
type_checker/rules.rb's existing "pending code fixes" catalog rather
than as an inline @sg-ignore, per this repo's own convention for
tracking known-but-unfixed typechecker gaps.
"See ToStructInitializer for why a Struct.new definition needs
this" adds nothing at the call site a reader would not already get
from the method call itself - ToStructInitializer.make already says
what it does, and its own module docstring already has the why.
undercover flagged lib/solargraph/yard_map/mapper/to_struct_initializer.rb:104-118
as uncovered against castwide/solargraph:master. The rescue that
falls back to UTF-8 when a magic comment names an encoding
Ruby doesn't recognize had no test.
Fold the two encoding-fallback specs' "why" out of buried
mid-test comments and into their `it` descriptions, per this
repo's spec-comment convention. Back the `Encoding.find`
@sg-ignore's claim about its nil-return case with a real Ruby
docs link instead of asserting it without a citation.
@apiology
apiology marked this pull request as ready for review September 6, 2026 16:10
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