Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/solargraph/pin_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ def serialize_stdlib_require require, pins
save(stdlib_require_path(require), pins)
end

# Every entry in the running Ruby installation's standard library
# directory, as a name that might be requirable. Not all of them
# resolve to a gemspec or to RBS; callers discard the misses.
#
# @return [Array<String>] possible standard library names
def possible_stdlibs
Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir|
basename = File.basename(file_or_dir)
basename.end_with?('.rb') ? basename[0..-4] : basename
end.sort.uniq
rescue StandardError => e
logger.info { "Failed to list possible stdlibs: #{e.message}" }
[]
end

# @return [String]
def core_path
File.join(work_dir, 'core.ser')
Expand Down
5 changes: 3 additions & 2 deletions lib/solargraph/shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ def gems *names
workspace = Solargraph::Workspace.new('.')

if names.empty?
Gem::Specification.to_a.each { |spec| do_cache spec, rebuild: options[:rebuild] }
$stderr.puts "Documentation cached for all #{Gem::Specification.count} gems."
gemspecs = workspace.gemspecs_to_cache
gemspecs.each { |gemspec| do_cache gemspec, rebuild: options[:rebuild] }
$stderr.puts "Documentation cached for #{gemspecs.length} gems."
else
warn("Caching these gems: #{names}")
names.each do |name|
Expand Down
30 changes: 30 additions & 0 deletions lib/solargraph/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ def find_gem name, version = nil, out: nil
Gem::Specification.find_by_name(name, version)
end

# Gemspecs the bundle depends on directly. Empty when the
# workspace has no resolvable bundle.
#
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def all_gemspecs_from_bundle
gemspecs.all_gemspecs_from_bundle
end

# Every gemspec whose pins this workspace could need: the bundle,
# plus any standard library that resolves to a gemspec in this Ruby
# installation. Candidate stdlib names resolving to nothing are dropped.
#
# @return [Array<Gem::Specification>]
def gemspecs_to_cache
# @sg-ignore Wrong argument type for Solargraph::Workspace::Gemspecs#find_gem: out expected IO, nil, received NilClass
stdlib_gemspecs = PinCache.possible_stdlibs.map { |name| gemspecs.find_gem(name, out: nil) }.compact

# Outside a bundle there is nothing to scope to, so every installed gem
# is a candidate - the same set master always cached.
bundled_gemspecs = all_gemspecs_from_bundle
bundled_gemspecs = Gem::Specification.to_a if bundled_gemspecs.empty?

(bundled_gemspecs + stdlib_gemspecs).uniq { |gemspec| [gemspec.name, gemspec.version] }
end

# Synchronize the workspace from the provided updater.
#
# @param updater [Source::Updater]
Expand Down Expand Up @@ -197,6 +222,11 @@ def gemspec_files

private

# @return [Solargraph::Workspace::Gemspecs]
def gemspecs
@gemspecs ||= Gemspecs.new(directory_or_nil)
end

# The language server configuration (or an empty hash if the workspace was
# not initialized from a server).
#
Expand Down
17 changes: 17 additions & 0 deletions spec/pin_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

describe Solargraph::PinCache do
describe '.possible_stdlibs' do
it 'lists names from the stdlib directory without the .rb suffix' do
allow(Dir).to receive(:glob).and_return(['/ruby/3.2.0/set.rb', '/ruby/3.2.0/json'])

expect(described_class.possible_stdlibs).to eq(%w[json set])
end

it 'is tolerant of less usual Ruby installations' do
stub_const('Gem::RUBYGEMS_DIR', nil)

expect(described_class.possible_stdlibs).to eq([])
end
end
end
40 changes: 40 additions & 0 deletions spec/shell_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@ def bundle_exec(*cmd)
expect(output).to include("Gem 'solargraph123' not found")
end
end

context 'with mocked Workspace' do
let(:workspace) { instance_double(Solargraph::Workspace) }
let(:gemspec) { instance_double(Gem::Specification, name: 'backport', version: '1.2.0') }

before do
allow(Solargraph::Workspace).to receive(:new).and_return(workspace)
allow(workspace).to receive(:gemspecs_to_cache).and_return([gemspec])
allow(shell).to receive(:do_cache)
end

it 'caches only what the workspace selects' do
capture_both { shell.gems }

expect(shell).to have_received(:do_cache).with(gemspec, any_args)
end

it 'reports the number of gems it cached' do
output = capture_both { shell.gems }

expect(output).to include('Documentation cached for 1 gems.')
end
end

context 'with the core pseudo-gem' do
let(:core_map) { instance_double(Solargraph::RbsMap::CoreMap) }

before do
allow(Solargraph::RbsMap::CoreMap).to receive(:new).and_return(core_map)
allow(core_map).to receive(:cache_core).and_return([])
end

it 'caches core pins' do
pending 'Shell#gems calls PinCache.cache_core, which is defined nowhere'

capture_both { shell.gems('core') }

expect(core_map).to have_received(:cache_core)
end
end
end

describe 'cache' do
Expand Down
47 changes: 47 additions & 0 deletions spec/workspace/gemspecs_to_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

describe Solargraph::Workspace, '#gemspecs_to_cache' do
subject(:selected) { workspace.gemspecs_to_cache }

let(:workspace) { described_class.new('spec/fixtures/workspace') }
let(:gemspecs) { instance_double(Solargraph::Workspace::Gemspecs) }
let(:bundled) { instance_double(Gem::Specification, name: 'rspec', version: '3.13.0') }
let(:stdlib) { instance_double(Gem::Specification, name: 'json', version: '2.7.0') }

before do
allow(Solargraph::Workspace::Gemspecs).to receive(:new).and_return(gemspecs)
allow(gemspecs).to receive(:all_gemspecs_from_bundle).and_return([bundled])
allow(Solargraph::PinCache).to receive(:possible_stdlibs).and_return(%w[json notagem])
allow(gemspecs).to receive(:find_gem).with('json', out: nil).and_return(stdlib)
allow(gemspecs).to receive(:find_gem).with('notagem', out: nil).and_return(nil)
end

it 'combines the bundle with the standard libraries that resolve to a gemspec' do
expect(selected).to eq([bundled, stdlib])
end

it 'does not fall back to every gem installed on the machine' do
allow(Gem::Specification).to receive(:to_a)

selected

expect(Gem::Specification).not_to have_received(:to_a)
end

it 'keeps one entry when the bundle and a standard library name resolve to the same gem' do
allow(gemspecs).to receive(:all_gemspecs_from_bundle).and_return([bundled, stdlib])

expect(selected).to eq([bundled, stdlib])
end

context 'when there is no bundle to scope to' do
before do
allow(gemspecs).to receive(:all_gemspecs_from_bundle).and_return([])
allow(Solargraph::PinCache).to receive(:possible_stdlibs).and_return([])
end

it 'falls back to the gems installed on the machine' do
expect(selected.map(&:name)).to include('yard')
end
end
end
12 changes: 12 additions & 0 deletions spec/workspace_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,16 @@
described_class.new('./path', config)
end.not_to raise_error
end

describe '#gemfile?' do
it 'returns true when the workspace directory has a Gemfile' do
File.write(File.join(dir_path, 'Gemfile'), "source 'https://rubygems.org'")

expect(workspace.gemfile?).to be(true)
end

it 'returns false when the workspace directory has no Gemfile' do
expect(workspace.gemfile?).to be(false)
end
end
end
Loading