From bdadf5c5f2645e02ab1ded59290369597a076a58 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 7 Aug 2026 07:56:26 -0400 Subject: [PATCH] WIP: Bust pin cache when solargraph's own lib code changes, not just VERSION PinCache.work_dir keys the on-disk cache directory on Solargraph::VERSION alone. Marshal silently tolerates loading pins serialized by an older Pin::Callable definition with fewer ivars (missing ones just read back as nil), so a gem source swap that changes Pin behavior without bumping VERSION -- e.g. a downstream Gemfile switching a git-sourced solargraph dependency to a different commit, common in fork-based development -- can leave stale pins in place after upgrading. This was flagged via https://github.com/castwide/solargraph/pull/1274#issuecomment-5211750475: after PR #1274 added Pin::Callable#block_required?, a consumer bisecting by swapping gem revisions without clearing ~/.cache/solargraph got block_required? == false (nil from the old-schema cached pin) for an RBS signature whose block is genuinely required, letting a generic-returning block overload spuriously match a call with no block and leak an unresolved generic into the inferred return type. Reproduced by populating the cache on the pre-#1274 commit and then switching to the post-#1274 commit under the same forced VERSION, without clearing the cache. Fold a digest of solargraph's own lib/ file mtimes+sizes into work_dir so any code change -- not just a VERSION bump -- busts the cache. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01QYGSCei2o8gmaghfjc15Uo --- lib/solargraph/pin_cache.rb | 26 +++++++++++++++++++++++++- spec/pin_cache_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 spec/pin_cache_spec.rb diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 803170764..007d551a6 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -1,4 +1,5 @@ require 'yard-activesupport-concern' +require 'digest' require 'fileutils' require 'pathname' # @todo Required by RBS but not loaded in some use cases require 'rbs' @@ -19,13 +20,36 @@ def base_dir File.join(Dir.home, '.cache', 'solargraph') end + # A digest of solargraph's own lib/ contents. Marshal silently + # tolerates loading pins serialized by a class definition with + # fewer ivars than the one currently loaded (missing ivars just + # read back as nil), so a `Solargraph::VERSION`-only cache key + # isn't enough to protect against a gem source swap that changes + # Pin behavior without bumping VERSION -- e.g. switching a + # bundler `path:`/`git:` install to a different commit, common + # in fork-based development. Folding this digest into work_dir + # busts the cache whenever the library code actually changes. + # + # @sg-ignore flow sensitive typing doesn't narrow @lib_digest past the nil guard below + # @return [String] + def lib_digest + return @lib_digest unless @lib_digest.nil? + lib_dir = File.expand_path('..', __dir__) + digest = Digest::SHA256.new + Dir.glob(File.join(lib_dir, '**', '*.rb')).each do |file| + stat = File.stat(file) + digest << file << stat.mtime.to_i.to_s << stat.size.to_s + end + @lib_digest = digest.hexdigest[0, 12] + end + # The working directory for the current Ruby, RBS, and Solargraph versions. # # @return [String] def work_dir # The directory is not stored in a variable so it can be overridden # in specs. - File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}") + File.join(base_dir, "ruby-#{RUBY_VERSION}", "rbs-#{RBS::VERSION}", "solargraph-#{Solargraph::VERSION}-#{lib_digest}") end # @param gemspec [Gem::Specification] diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb new file mode 100644 index 000000000..6b75a3712 --- /dev/null +++ b/spec/pin_cache_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +describe Solargraph::PinCache do + describe '.lib_digest' do + it 'changes when a lib file is edited, even though Solargraph::VERSION does not' do + original_digest = described_class.lib_digest + + target = File.expand_path('../lib/solargraph/pin_cache.rb', __dir__) + original_mtime = File.mtime(target) + begin + # bump mtime without touching the file's actual content, the + # same signal a real code change (or a fresh git checkout of a + # different commit) would produce + File.utime(Time.now + 1, Time.now + 1, target) + described_class.instance_variable_set(:@lib_digest, nil) + + expect(described_class.lib_digest).not_to eq(original_digest) + ensure + File.utime(original_mtime, original_mtime, target) + described_class.instance_variable_set(:@lib_digest, nil) + end + end + + it 'is folded into work_dir so a code-only change (no VERSION bump) still busts the pin cache' do + expect(described_class.work_dir).to include(described_class.lib_digest) + end + end +end