Skip to content

Fix 1286 generic cross file parse - #76

Open
apiology wants to merge 14 commits into
masterfrom
fix-1286-generic-cross-file-parse
Open

Fix 1286 generic cross file parse#76
apiology wants to merge 14 commits into
masterfrom
fix-1286-generic-cross-file-parse

Conversation

@apiology

@apiology apiology commented Sep 1, 2026

Copy link
Copy Markdown
Owner

No description provided.

apiology and others added 14 commits August 11, 2026 21:03
A class defined in a gem and re-opened elsewhere via @!parse to add
@Generic tags produced two pins for the same namespace/method path.
ApiMap picked whichever pin loaded first - the gem's own pins load
before workspace pins, so the annotation's @Generic declaration and
overridden return types were silently ignored.

- ApiMap#namespace_pin_for_generics prefers the pin that actually
  declares generics over an arbitrary .first.
- ApiMap::Store#get_methods combines same-path method pins (skipping
  aliases, since merging an alias pin with a non-alias pin at the same
  path produces a pin #resolve_method_alias can't trace back to its
  target, which raises under SOLARGRAPH_ASSERTS=on).

Fixes castwide#1286

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtM8dkYTeQEiyhFu1NLZCB
Confirms combining many pins for the same method path completes
quickly rather than hanging - related to the concern that prompted
castwide#1186 and castwide#1195 (see comment in spec for why this
doesn't reproduce that specific bug, and where the precise regression
guard for it already lives).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtM8dkYTeQEiyhFu1NLZCB
Pin::DuckMethod pins (created for `#method_name` duck-type tags, e.g.
`@param x [#to_s]`) are constructed without a closure. Method#typify
called `closure.gates` unconditionally once see_reference or
typify_from_super resolved a type, raising `NoMethodError: undefined
method 'gates' for nil` whenever that path was hit on such a pin.

Guard it the same way other call sites in this file already do:
`closure&.gates || ['']`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj8BgHwzHKFsD51H9TfPD5
Cut docstrings and spec comments to the review's per-comment budget
(2-6 lines): the combine_duplicate_method_pins and
namespace_pin_for_generics docstrings, the alias-combination spec
comment, and the "combines many same-path pins" spec comment, which
had narrated the history of issues castwide#1186, castwide#1195, and castwide#1238 instead
of stating the current constraint. Also replaces "plain" in the
cross-file generics spec with what actually makes that fixture
plain: no @Generic tag, no @!parse stub.
solargraph typecheck against any project using Forwardable dies before
emitting a single diagnostic:

    lib/solargraph/pin/delegated_method.rb:25:in 'initialize':
    either :method or :receiver is required (ArgumentError)

from ApiMap#load_with_cache -> catalog -> Store#update ->
combine_duplicate_method_pins -> Pin::Method#combine_with ->
Pin::Base#combine_with.

Pin::Base#combine_with rebuilds the merged pin with
self.class.new(**new_attrs), and new_attrs carries only generic pin
attributes (location, name, closure, comments, visibility, signatures).
Pin::DelegatedMethod#initialize requires exactly one of :method /
:receiver and receives neither, so combining two same-path
DelegatedMethod pins is structurally impossible. This went live when
castwide#1311 started minting DelegatedMethod pins for
def_delegators, which makes duplicate-path groups routine.

combine_duplicate_method_pins already skips groups containing a
Pin::MethodAlias for the same class of reason (a merged pin can't
represent the alias target); DelegatedMethod was never added to that
guard. Extend it rather than teaching DelegatedMethod to merge: a pin
constructed from a :receiver that has since resolved holds both
@receiver_chain and @resolved_method, while initialize forbids passing
both, so any combine_with override would have to discard one pin's
delegation target. When the two pins delegate to different receivers
(reopened class, source-vs-RBS duplicate) that loses information
silently. Keeping both pins preserves it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FEjW6nMpZrWPmeWX9miT
Method docstring was 6 prose lines over the 1-3 line budget; two spec
comments restated the example name or the assertion in different
words. Neither adds anything the code or the it-title doesn't already
say.
The cross-file @!parse spec called its gem-side source plain_impl and
then spent two comment lines explaining what "plain" meant. Rename it
to gem_source so the name carries that, and shorten the comment.

"pin" as a verb in store_spec collides with Solargraph::Pin, the
domain object the surrounding spec is about; say "assert" instead.

The combine_duplicate_method_pins docstring ran to seven lines counting
its @PARAM and @return tags. Fold the fourth prose line into the third.
namespace_pin_for_generics carried a @type on candidates. Dropping it
shows what it was covering:

  lib/solargraph/api_map.rb:822: Unresolved call to generics on
  Solargraph::Pin::Base

select with an is_a? block does not narrow the returned element type,
so the Array stays Array<Pin::Base>. That is a real inference gap and
distinct from the flow-sensitive-local narrowing tracked in 1241, 1251,
1254 and 1296; nothing upstream covers it. Replace the cast with an
@sg-ignore naming it, so the finding survives rather than being hidden.

The DuckMethod example asserted only that typify did not raise. It now
asserts the pin has no closure and still resolves to ::String through
the core Object#to_s, which is the property that would actually regress.

combine_duplicate_method_pins named neither situation that produces
duplicate same-path pins; say what they are. The alias and
DelegatedMethod reasoning it carried is already stated by the specs that
cover those two cases.
The two Timeout.timeout(5) wrappers in store_spec dated from when
Pin::Method#combine_same_type_arity_signatures could blow up
exponentially. That is fixed and merged, so the wrappers guard nothing
and the example is renamed for what it actually asserts: 30 same-path
pins combine into one. The timeout require goes with them.

namespace_pin_for_generics gains a @todo weighing merging duplicate
namespace pins against picking one of them.

The DuckMethod example said "core method return type", which names
nothing in particular. It resolves through Object#to_s to String; say
that.
A gem's plain &block signature and a workspace @!parse stub's
yield-typed one only differ in the block's own type_arity, so
combine_signatures_by_type_arity bucketed them as separate overloads
and left picking between them to Chain::Call at every call site.

Merge them into one signature before that bucketing instead, and
make Callable#combine_blocks prefer the block that actually declares
parameters rather than choosing arbitrarily. The merged pin is now
correct on its own - hover and completion benefit too, not just
inference at a call that happens to trigger dispatch_order.
The merge only fired when one side's block declared zero yielded
parameters. A block documenting one parameter and a sibling
documenting two hit the identical type_arity mismatch and still
failed to combine. Compare declared parameter counts instead of
emptiness, so any side with fewer yielded parameters loses to one
with more, not just the zero case.
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