Skip to content

[regression fix] Load Solargraph plugin gems when caching gems - #1407

Draft
apiology wants to merge 36 commits into
castwide:masterfrom
apiology:load-plugins-when-caching-gems
Draft

apiology wants to merge 36 commits into
castwide:masterfrom
apiology:load-plugins-when-caching-gems

Conversation

@apiology

Copy link
Copy Markdown
Contributor

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

Builds on #1378.

Problem: In the language server, gem pins never get a plugin's contribution:

# .solargraph.yml lists `plugins: [solargraph-rails]`, but the server
# caches each gem by running this in a subprocess that never requires it
solargraph cache responders

With #1378 gem pins are mapped from gem source and serialized, so whatever a plugin adds while mapping a gem is missing from the cache the server builds, while an in-process typecheck includes it. The cache key also ignores plugins, so adding or upgrading one keeps serving the old pins.

Solution: solargraph cache and uncache require the plugins configured for --directory (via a new Workspace::Config#require_plugins, which Workspace now delegates to), and CacheDir.gem_dir includes each loaded plugin's name and gem version, so pins built with different plugin sets never share a directory.

With no plugin loaded, gem_dir is unchanged. Core, stdlib and RBS collection pins come from RBS rather than parsed source, so their directories stay plugin-independent.

The @sg-ignore in CacheDir.add_plugin records a typecheck defect: h.each_value.find { |v| v.even? } reports Unresolved call to even? on Integer, Hash{String => Integer}.

🤖 Generated with Claude Code

castwide and others added 24 commits September 28, 2026 23:57
…de#1379)

* RbsCollection

* Gem caching in External spec

* RbsCollection specs

* Stale code

* Linting
The no-YARD gem mapper lost pins that YARD used to produce:

* Mixins called on an explicit receiver. Only receiverless include/extend/prepend
  were mapped, so ActiveSupport's `Object.prepend(self)` (which is how
  `require_dependency` reaches every class) and
  `Integer.prepend ActiveSupport::NumericWithFormat` made all the methods those
  modules add to core classes invisible.

* ActiveSupport's `class_attribute`, whose accessors are created at run time.
  Rails documents them with `## :singleton-method:` comment directives that YARD
  turned into methods. `process_class_attribute` generates the same accessors and
  honors instance_accessor/instance_reader/instance_writer/instance_predicate.

Fixes several solargraph-rails definitions (require_dependency on ActiveJob,
Rails::Application, ActionDispatch::Routing::Mapper and ActiveRecord; ActionController
callbacks; ActiveRecord::Base.enumerate_columns_in_select_statements;
ActiveRecord::Base#signed_id_verifier_secret; Integer#to_formatted_s).
External now selects collection entries for the gems it loads, but the lookup
expected a collection directory whose version matches the installed gem. Two
kinds of signature sets were silently dropped:

* Gems whose signatures are published under a different version than the gem.
  gem_rbs_collection ships `actionpack/6.0` for every Rails 6 and 7 release and
  `devise/4.9` for Devise 4 and 5, so actionpack 7.0.10 and devise 5.0.4 got no
  signatures at all (this is what hid the ActionController callback DSL).
  RbsCollection#best_key now falls back to the closest version the collection
  provides for that gem, preferring the highest one that does not exceed it.

* Stdlib entries in the lockfile (`date/0`, `json/0`, ...). They are not
  associated with a gem in the bundle, and `rbs collection install` resolves
  them to the signatures that ship with the rbs gem instead of copying them into
  the collection directory, so they can never be matched by a gem lookup.
  RbsCollection#stdlib_names exposes them and External loads them with
  Collection::Stdlib, which restores e.g. `Date#xmlschema -> String`.

Also guards External#rbs_collection_lockfile against a nil directory, which
previously short-circuited before the collection was touched.
Gem pins are mapped from gem source and serialized, so a plugin's
conventions shape what lands in the gem cache. The language server
caches gems by running `solargraph cache` in a subprocess, which never
loaded the workspace's plugins, so the cache it wrote could differ from
the one an in-process typecheck builds.

`cache` and `uncache` now require the plugins configured for the
directory, through a Workspace::Config#require_plugins that Workspace
also delegates to. Each loaded plugin and its gem version are recorded
in CacheDir, and gem_dir includes them, so loading or upgrading a
plugin rebuilds the gem cache instead of reusing pins made without it.

The @sg-ignore in CacheDir.add_plugin records a typecheck defect:
Hash#each_value.find types the block parameter as the value type
unioned with the hash itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhUSLUimEYJyCZv8NdQGKb
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
@apiology
apiology force-pushed the load-plugins-when-caching-gems branch from fd049d2 to 2a0c289 Compare October 21, 2026 16:35
@apiology apiology changed the title Give gem caches the workspace's plugins [regression fix] Give gem caches the workspace's plugins Oct 21, 2026
apiology and others added 2 commits October 21, 2026 14:16
A plugin was recorded in the gem cache key only when the workspace
config required it. A process that requires the plugin directly, such
as a plugin spec suite using a workspace without config, mapped gems
with the plugin loaded but read and wrote the plain gems directory,
so it missed caches built by `solargraph gems` with the plugin
configured.

Record the gem that defines each registered convention, and key
config-required plugins by gem name so both paths give the same key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
Keep the natural Hash iteration and mark the Solargraph gap: the
each_value block param is typed as the Hash pair, not the value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
The parent keys gem_dir on the plugins it has loaded, while
`solargraph cache` re-read the --directory config. When the two sets
differ (a workspace built with another config, a plugin loaded by
plain require, or several workspaces in one process), the subprocess
writes its pins to a directory the parent never reads, and
Library#cache_next_gemspec re-caches the gem forever.

Record what to require for each plugin and pass that list with
--plugins, so the subprocess loads the same set and writes to the
same gem_dir.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
apiology and others added 5 commits October 21, 2026 15:44
Undercover flagged the block in Shell#require_plugins that requires
each plugin named by --plugins: the only spec passed an empty string,
so the require, CacheDir.add_plugin and LoadError branches never ran.
Add specs for a loadable plugin and a missing one, and clear the
plugin from CacheDir.plugin_requires after each example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
Specs that build a workspace at the repo root load the plugins in its
.solargraph.yml, as the plugin CI jobs configure, and require_plugins
then records them with CacheDir.add_plugin. That switched
CacheDir.gem_dir to a cold plugin-keyed directory for the rest of the
spec process. Save, clear and restore CacheDir.plugins and
plugin_requires around every example, which also makes the per-spec
CacheDir cleanup in shell_spec and config_spec redundant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
A before(:all) workspace load, such as the repo-root ApiMap that
conversions_spec builds, runs outside the per-example around hook, so
the plugins it records stayed in CacheDir for every later example.
Save, clear and restore them around each context as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
Brings in castwide#1436 so CI builds rugged on Ubuntu 26.04 runners.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
apiology added a commit to apiology/solargraph that referenced this pull request Oct 22, 2026
Brings in castwide master's castwide#1436 rugged build fix via castwide#1407.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEntZKsZwQHhzqcwG7MTqn
The five sg-ignores on Gem.loaded_specs.each_value.find in add_plugin
and add_convention come from UniqueType#resolve_generics collapsing
Enumerator parameters into one union for generic index 0, so the
Enumerable#find block parameter becomes Elem | Return. castwide
PR 1415 resolves every generic parameter and makes all five Unneeded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@apiology
apiology marked this pull request as draft October 29, 2026 15:00
@apiology apiology changed the title [regression fix] Give gem caches the workspace's plugins [regression fix] Load Solargraph plugin gems when caching gems Oct 29, 2026
Config#require_plugins and the private Shell helper have the side
effect of requiring plugins and recording them in the gem cache key;
the bang marks that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTpguHqALFufF4QdNagq1C
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.

2 participants