Skip to content

[DO NOT MERGE] Rewrite PinCache as an instance-based engine, with wiring and CLI update - #97

Draft
apiology wants to merge 22 commits into
masterfrom
pin-caching-3-pincache-core
Draft

[DO NOT MERGE] Rewrite PinCache as an instance-based engine, with wiring and CLI update#97
apiology wants to merge 22 commits into
masterfrom
pin-caching-3-pincache-core

Conversation

@apiology

@apiology apiology commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Claude: This PR was opened by Claude Code on behalf of @apiology. It carries over the body of castwide#1252 unchanged; that PR is being closed in favour of this one.

Rewrites PinCache from a class-method-based module into an instance-based gem-caching engine, and updates every consumer to match.

Why

Fixes a stale-type bug: after adding, updating, or removing an rbs_collection.yaml entry for a gem, hover/completion/type-checking could keep showing the previous RBS source's results, with no indication anything was stale short of a manual solargraph cache --rebuild.

How

The old cache key conflated a gem's possible RBS sources (bundled types, rbs_collection, local override, unresolved) into one entry. Replaced with explicit CACHE_KEY_GEM_EXPORT/_UNRESOLVED/_STDLIB/_LOCAL classification plus a content hash of the collection's per-gem lock data. PinCache becomes an instance scoped to that config, since the key logic now needs to know which config produced it.

What's in it

  • PinCache: instantiable class taking rbs_collection_path/rbs_collection_config_path/directory/yard_plugins; Yardoc, GemPins, RbsMap updated to match.
  • Wiring: DocMap, ApiMap, Workspace, Library delegate to it; CLI cache/uncache/gems are thin wrappers.
  • Also: Gemspecs#in_this_bundle? no longer raises with no Gemfile (#1225).

An unrelated RbsMap::Conversions dead-code fix, in a file this PR already touches, is split out to #41.

Testing: bundle exec rspec — 1595 examples, 0 failures, 64 pending.

apiology and others added 22 commits August 21, 2026 16:20
Introduce Solargraph::PinCache, replacing the old class-method-based
PinCache module with an instance-based engine that owns YARD and RBS
collection caching, plus combining them into a single cached pin set
per gem. Yardoc, GemPins, and RbsMap are updated to support it:

* Yardoc splits doc-building (build_docs/build_pins) out of its old
  do-everything cache method, so PinCache can drive the build and
  caching steps separately.
* GemPins drops build_yard_pins (now owned by PinCache) and adds
  combine_method_pins_by_path for deduping method pins by path.
* RbsMap falls back to StdlibMap resolution when a gemspec isn't
  found in the RBS collection.

Also fixes a real bug in RbsMap::Conversions surfaced while extracting
this: two pairs of duplicate method definitions (parts_of_function,
build_type) where an old implementation was left in place, shadowed
and made unreachable by a newer one added elsewhere in the file. The
dead code referenced two helper methods (other_type_to_type,
method_type_to_type) that don't exist anywhere in lib/, so it would
have raised NoMethodError had it ever been called - removing it drops
this file's strong-typecheck problem count from 32 to 21 (all
pre-existing, unrelated to this change).

Extracted from castwide#1006 (Improve pin caching) as the
foundational piece of that PR: the new caching engine and its direct
collaborators, without yet wiring it into DocMap/Workspace/ApiMap or
the CLI (those follow in stacked PRs on top of this one).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace DocMap's ad hoc gem-caching logic with delegation to PinCache
(introduced in the prior stacked PR), simplifying DocMap substantially.
Workspace gains a pin_cache accessor plus cache_gem/uncache_gem/
cache_all_for_workspace! entry points that drive PinCache for a given
workspace's gemspecs. ApiMap follows the renamed DocMap API
(cache_all! -> cache_doc_map_gems!, uncached_gemspecs.any? ->
any_uncached?) and dedupes resolved method aliases via
GemPins.combine_method_pins_by_path. Library exposes pin_cache
(delegating to workspace), uses it to check whether a gem's cache
build is already in progress, and fixes a subprocess chdir bug in its
background gem-caching thread.

Extracted from castwide#1006 (Improve pin caching) as the
second piece of that PR, stacked on top of the PinCache engine PR.
This depends on PinCache existing; the CLI updates that depend on this
wiring follow in a further stacked PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reimplement `solargraph cache`, `uncache`, and `gems` as thin wrappers
over the Workspace#cache_gem/uncache_gem/cache_all_for_workspace!
entry points added in the prior stacked PR, removing the CLI's own
duplicated build/cache logic.

Note: 2 specs in this PR ("with unbundled environments #cache
succeeds" / "#gems succeeds") will fail until
castwide#1225 merges - they exercise
Workspace::Gemspecs#find_gem in an environment with no discoverable
Gemfile, which currently raises Bundler::GemfileNotFound instead of
falling back gracefully. Verified locally that applying castwide#1225's fix
makes both pass with no other changes needed here.

Extracted from castwide#1006 (Improve pin caching) as the
final piece of that PR, stacked on top of the DocMap/Workspace wiring
PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The YAML/Psych stdlib resolution spec timed out at 240.33s against a
240s limit in CI - a genuine near-miss (0.14% over), not contention
from running multiple PRs' CI concurrently (each job gets its own
runner, so concurrent jobs affect queue time, not execution time).
Widen that limit and the other 120s limits proportionally to give
real headroom against normal run-to-run variance on GitHub Actions'
shared runners, rather than re-running and hoping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Earlier, while this PR's PinCache-core piece was still a standalone
branch (before combining the engine, wiring, and CLI update into one
PR), this spec was pointed at DocMap#cache_all! since that was the
only name that existed on master at the time. Now that this PR also
includes the DocMap wiring that renames cache_all! to
cache_doc_map_gems!, the spec needs to follow that rename too - CI
caught the drift (undefined method 'cache_all!' for an instance of
Solargraph::DocMap).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both pre-existing on master, unrelated to any currently open PR:
Pin::Method#== (super && other.node == node) is from castwide#930
(2025-05-11) and never compared signatures. Pin::Parameter#type_arity_decl
(arity_decl + return_type.items.count.to_s) is from castwide#1177
(2026-05-12), the same commit that added the spec/pin/method_spec.rb
"combines signatures by type" test this fix makes pass.

Both bugs are dormant on plain master: GemPins.combine_method_pins_by_path,
the only caller that exercises this combining logic, was itself removed by
castwide#1195 ("Limit pin combination to doc maps"), so this
fix has no observable effect and no test to point to on this base until
that function and its call site are restored. See PR description for
context on where that currently stands.

Traced from a CI-only failure on an unrelated integration-testing branch,
where a different, in-progress PR stack (apiology/solargraph
pin-caching-3/4) happens to re-add GemPins.combine_method_pins_by_path and
its caller, waking up both of these bugs:
Integer#+ inferred a return type of "Integer, BigDecimal" instead of
"Integer" for `x = 0; x += 1; x`, because Pin::Method#== treated two
RBS declarations of Integer#+ with different signatures (core Ruby's and
the bigdecimal gem's reopening) as equal - both have nil location and
identical rdoc-derived comments - so GemPins.combine_method_pins'
skip-if-already-identical shortcut fired and one declaration was silently
dropped instead of merged. Separately, type_arity_decl grouped signatures
for merging by how many types are in each parameter's union rather than
the types themselves, so distinct single-type overloads (Integer, Float,
Rational, Complex, BigDecimal) bucketed together and had their return
types incorrectly unioned.

Fixed by comparing actual type tags in type_arity_decl and by including
signatures in Pin::Method#==.
Gem::Specification.find_by_path already resolves relative to a gem's
own require_paths, so passing "lib/#{require}.rb" instead of the bare
require path always missed. This silently dropped any gem whose
conventional require path differs from its RubyGems package name
(e.g. activesupport/active_support), along with its transitive
dependencies, with no error or warning.

Reported in castwide#1252 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yvznkiHNc5iXyycEj8tmR
Porting PR 1311's fallback_pins mechanism from the pre-1252
DocMap#deserialize_combined_pin_cache onto this branch's
PinCache#deserialize_combined_pin_cache, which 1252 rewrote without
carrying it forward.

Known issue: since deserialize_combined_pin_cache now returns
non-nil via the fallback, the caller's uncached_gemspecs queue never
gets populated for a gem that hits this path, so no real combined
cache ever gets built for it. Still being investigated.
deserialize_combined_pin_cache can now return RbsMap#fallback_pins
before the real combined cache exists (see the prior commit). This
call site inferred "is this gem cached" from "did I get pins back",
which was true before that fallback existed but stopped being true
once a fallback answer could also be non-nil. Left as-is, a gem that
hits the fallback path would never get queued into uncached_gemspecs,
so no real combined cache (with YARD pins merged in) would ever get
built for it.

Check pin_cache.cached? separately instead, only when pins came back
at all - when nothing came back, it's unconditionally uncached
already and a second check is redundant.
Retag the sg-ignore in ApiMap#clip with the matching rules.rb
entry (flow sensitive typing needs to handle attrs) rather than
the generic nil-check placeholder; confirmed unneeded and dropped
entirely once removed and re-checked.

Revert the 7 time_limit_seconds: RSpec metadata additions across
5 spec files, per review request to split them into their own
PR.
undercover flagged lib/solargraph/yardoc.rb:18-41 as uncovered
against castwide/solargraph:master. Lines 26-27 (the missing-gem_dir
branch) were genuinely untested: the existing "bails quietly" spec
stubbed File.exist? but build_docs checks Dir.exist?, so the branch
it claimed to cover never actually ran.

Stub Dir.exist? instead, and assert Open3.capture2e is never called
so the test fails if the early return stops firing.
Workspace#gemfile? had no coverage for either branch. The cache
CLI command had no coverage for the core-caching branch, which
calls PinCache.cache_core.
undercover flagged the names.each loop in Shell#gems (66.67%,
lib/solargraph/shell.rb:181 on origin/master) for missing coverage
of both rescue clauses: no spec triggered Gem::MissingSpecError or
Gem::Requirement::BadRequirementError from find_gem.

Add specs stubbing workspace.find_gem to raise each rescued error,
split out of the combined cover-shell-gaps branch (the other half,
covering Shell#scan, belongs on pin-cli-ux instead).
…nto pin-caching-3-pincache-core

# Conflicts:
#	spec/shell_spec.rb
Brings PR 1252 (Rewrite PinCache as an instance-based engine) up to
date with master, so its own CI can pass again. Two failures on the
stale branch:

1. spec/yard_map/mapper_spec.rb called the old Yardoc.load!(gemspec)
   API. Master added this spec (castwide#1279) after this
   branch forked, exercising a namespace-conflict fix in
   yard_map/mapper.rb that this branch didn't have. This branch's own
   Yardoc.load! now takes a path string, not a Gem::Specification, so
   the merged tree hit:

     TypeError: no implicit conversion of Gem::Specification into
     String

   The auto-merge carried master's version of the test through with
   no conflict, so the break landed silently. Fixed by adapting the
   spec to build the yardoc into a temp path first (build_docs) and
   loading that path, matching the pattern yardoc_spec.rb already
   uses. The merge itself supplies the missing namespace-conflict fix
   (Mapper#namespace_with_bug_fix), so the adapted spec now passes.

2. .github/workflows/plugins.yml's run_solargraph_rspec_specs job
   cloned a stale branch of lekemula/solargraph-rspec. Already fixed
   on master by castwide#1291 ("Use the original repo"),
   merged 2026-08-30. No local conflict; this branch never touched
   the file, so the merge carries master's fix through untouched.

No other file needed manual resolution. Every other changed file
auto-merged without conflict markers, verified file-by-file against
each side's own commits touching it.

Verified after merge: full spec suite 1680 examples/0 failures/67
pending; solargraph typecheck --level strong 527 problems in 89 of
250 files, with every problem not already present on this branch's
old tip confirmed pre-existing on plain master (531 problems
standalone) rather than merge-introduced; rubocop 48 offenses vs 51
on this branch's old tip, the improvement coming entirely from
master's own rewrite of source/chain/call.rb.
Both class-level methods were entirely untested: uncache_by_prefix's
glob-delete-and-log loop (including that it skips directories and
leaves non-matching files alone), and exist?'s file-vs-missing check.

Split from the 2026-08-04 integration branch's bundled undercover
coverage commit 4aab8b2.
GemPins.combine guards its loop body with

    next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method)

so rbs_pin is truthy on every line below it. The `unless rbs_pin`
block that followed could therefore never run - neither its
logger.debug call nor its `next yard_pin`.

undercover over a full 1684-example run reported
lib/solargraph/gem_pins.rb:60:62 at 0.0% coverage, 0 hits on both
executable lines. Nothing else moves: 1684 examples, 0 failures,
67 pending before and after, and `solargraph typecheck --level
strong` holds at 527 problems in 89 of 250 files.
extract_method_type_return_type only ever truth-tests implicit_nil, and
its sole caller parts_of_function already documents it as Boolean. The
YARD tag here declared it Object and sat below @return, so the signature
read as taking an arbitrary value.

Declare it Boolean, put the tag back in parameter order, and pass it by
name at the one call site so the meaning is visible there.
resolve_require built the lib-relative filename at the top of the
method, twenty lines above the manual spec.files scan that is the only
thing reading it. Gem::Specification.find_by_path takes a requirable
path rather than a filename, so nothing between the two points needs
the value.
type_arity_decl fed unrooted tags into the signature grouping key, so
two same-named types in different namespaces shared a bucket and their
signatures combined. Rooted tags cannot collide that way. The cost is
splitting a bucket that mixed rooted and unrooted spellings of one
type, which leaves signatures separate rather than merging them wrongly.

The type_arity example still showed the type-count form it replaced, so
update it alongside.
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