Skip to content

Merge Namespace pins in GemPins.combine, not just Method - #1348

Open
apiology wants to merge 7 commits into
castwide:masterfrom
apiology:fix-gempins-combine-namespace-generics
Open

apiology wants to merge 7 commits into
castwide:masterfrom
apiology:fix-gempins-combine-namespace-generics

Conversation

@apiology

@apiology apiology commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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

GemPins.combine merges a gem's YARD and RBS pins for the same path, but only for Pin::Method - a Pin::Namespace hits the same early exit before RBS is checked, and the leftover RBS pin is then dropped entirely:

# ActiveSupport::HashWithIndifferentAccess has plain Ruby source
# (no generics syntax) and a separate RBS signature:
#   class HashWithIndifferentAccess[T, U] < Hash[T, U]
Solargraph::GemPins.combine(yard_pins, rbs_pins)
  .find { |p| p.path == 'ActiveSupport::HashWithIndifferentAccess' }
  .generics
# => [] (RBS's ["T", "U"] silently dropped)

Routes Namespace pins through the same combine_with merge Method pins already use. Namespace needs its own combine_with, since the base implementation resets unset attributes (type, visibility, gates) to constructor defaults instead of carrying them over; Closure#combine_with gains generic_defaults, which had the same drop.

Merging Namespace pins this way surfaced a real cross-source disagreement: ruby/gem_rbs_collection's nokogiri signature still says class Nokogiri::XML::XPath, unchanged since nokogiri 1.11, though nokogiri redeclared it a module at 1.12. combine_with now prefers the YARD-derived pin's :type when the two disagree, since it comes from parsing the real keyword rather than a signature that can go stale.

Pin::Reference::Superclass has an analogous symptom but a different mechanism (both pins survive, ApiMap::Store#get_superclass just picks the first by insertion order) - left for separate follow-up.

GemPins.combine only merged YARD and RBS pins for Pin::Method. A
Pin::Namespace hit the same code path's early exit, so the RBS side
of a class or module (e.g. its generics) was silently dropped, and
the leftover RBS pin was excluded from the output entirely because
its path already appeared in the YARD set.

Route Namespace pins through the same combine_with-based merge as
Method pins, matching Pin::Closure and Pin::Namespace generics
handling. Namespace#combine_with is a new override, needed because
combine_with builds a fresh pin from only the attrs it sets, and
type/visibility/gates would otherwise silently reset to the
Pin::Namespace constructor defaults. Closure#combine_with also
gains generic_defaults, which had the identical drop bug.

Verified against ActiveSupport::HashWithIndifferentAccess (real
YARD class + RBS generic class declaration): source changes from
:yardoc to :combined and generics from [] to [T, U].
Two comments this PR added ran well past the repo's per-method
1-3 line budget for restating context available elsewhere:

- COMBINABLE_PIN_TYPES's docstring (5 lines) repeated what
  Pin::Namespace#combine_with already documents about which
  attributes it preserves; kept only the merge-vs-one-side-wins
  fact.
- The comment above gem_pins_combine_spec.rb's yard_pin fixture
  (6 lines) narrated the Pin::Base#== mechanics in full; kept only
  the fact needed to explain the location: fixture attribute.

No code changes; rubocop and the edited spec still pass.
GemPins.combine now merges Pin::Namespace pins in addition to
Pin::Method pins, calling assert_same on :type. Nokogiri::XML::XPath
was a class through nokogiri 1.11 but became a module at 1.12;
ruby/gem_rbs_collection's nokogiri signature was never updated past
1.11, so every RBS-derived pin for later nokogiri versions disagrees
with the real source on this attribute. Under SOLARGRAPH_ASSERTS=on
this crashed both the standalone Typecheck job and Overcommit's
Solargraph pre-commit hook.

An RBS signature's class/module keyword is a hand-maintained
annotation that can go stale independent of how accurate the rest of
the signature is, while the YARD-derived pin comes directly from
parsing the gem's actual class/module keyword. Namespace#combine_with
now prefers whichever side has source == :yardoc when :type
disagrees, and :combine_with_type joins the existing
:combine_with_visibility exemption in assert_or_log so a real
cross-source disagreement logs instead of raising.
undercover flagged lib/solargraph/pin/namespace.rb's new combine_type
at 80% coverage: no spec exercised the branch where neither
disagreeing pin is YARD-sourced.
assert_or_log's "not ready for prime time, ignored" exemption idiom
(shared with :combine_with_visibility) says a case is a known,
unsolved problem awaiting real handling. That doesn't describe
combine_type: the RBS-vs-real-source disagreement is a decided,
permanent trade-off (prefer the real-source pin), not a TODO.
Routing it through assert_or_log also meant it produced zero output
under SOLARGRAPH_ASSERTS=on, since assert_or_log's non-raising
exempted path returns before ever reaching its own logger.info call.

Log the disagreement directly instead, unconditionally, and drop
:combine_with_type from assert_or_log's exemption list along with
its now-inapplicable @todo.
warn overstated it: this is an expected, handled case, not an
operational concern.
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