solargraph 0.59.0.dev.2 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linting.yml +3 -1
  3. data/.github/workflows/plugins.yml +8 -2
  4. data/.github/workflows/rspec.yml +6 -40
  5. data/.github/workflows/typecheck.yml +2 -1
  6. data/.rubocop.yml +6 -1
  7. data/.rubocop_todo.yml +3 -0
  8. data/CHANGELOG.md +15 -0
  9. data/lib/solargraph/api_map/constants.rb +0 -1
  10. data/lib/solargraph/api_map/index.rb +6 -0
  11. data/lib/solargraph/api_map/store.rb +6 -0
  12. data/lib/solargraph/api_map.rb +20 -4
  13. data/lib/solargraph/complex_type/type_methods.rb +2 -1
  14. data/lib/solargraph/complex_type/unique_type.rb +2 -4
  15. data/lib/solargraph/complex_type.rb +1 -1
  16. data/lib/solargraph/doc_map.rb +370 -131
  17. data/lib/solargraph/gem_pins.rb +16 -17
  18. data/lib/solargraph/library.rb +44 -66
  19. data/lib/solargraph/logging.rb +0 -2
  20. data/lib/solargraph/parser/flow_sensitive_typing.rb +0 -2
  21. data/lib/solargraph/parser/parser_gem/class_methods.rb +0 -2
  22. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +0 -1
  23. data/lib/solargraph/pin/base.rb +0 -2
  24. data/lib/solargraph/pin/method.rb +3 -0
  25. data/lib/solargraph/pin/reference/type_alias.rb +16 -0
  26. data/lib/solargraph/pin/reference.rb +1 -0
  27. data/lib/solargraph/pin_cache.rb +66 -480
  28. data/lib/solargraph/position.rb +7 -4
  29. data/lib/solargraph/rbs_map/conversions.rb +18 -18
  30. data/lib/solargraph/rbs_map.rb +2 -3
  31. data/lib/solargraph/shell.rb +163 -15
  32. data/lib/solargraph/source/chain.rb +3 -1
  33. data/lib/solargraph/source_map/mapper.rb +0 -2
  34. data/lib/solargraph/type_checker.rb +1 -2
  35. data/lib/solargraph/version.rb +1 -1
  36. data/lib/solargraph/workspace/config.rb +1 -1
  37. data/lib/solargraph/workspace/gemspecs.rb +2 -2
  38. data/lib/solargraph/workspace.rb +32 -129
  39. data/lib/solargraph/yard_map.rb +17 -18
  40. data/lib/solargraph/yardoc.rb +26 -33
  41. data/lib/solargraph.rb +2 -0
  42. data/solargraph.gemspec +2 -2
  43. metadata +6 -11
@@ -5,15 +5,41 @@ require 'benchmark'
5
5
  require 'open3'
6
6
 
7
7
  module Solargraph
8
- # A collection of pins generated from specific 'require' statements
9
- # in code. Multiple can be created per workspace, to represent the
10
- # pins available in different files based on their particular
11
- # 'require' lines.
8
+ # A collection of pins generated from required gems.
12
9
  #
13
10
  class DocMap
14
11
  include Logging
15
12
 
16
- # @return [Workspace]
13
+ # @return [Array<String>]
14
+ attr_reader :requires
15
+ alias required requires
16
+
17
+ # @return [Array<Gem::Specification>]
18
+ attr_reader :preferences
19
+
20
+ # @return [Array<Pin::Base>]
21
+ attr_reader :pins
22
+
23
+ # @return [Array<Gem::Specification>]
24
+ def uncached_gemspecs
25
+ uncached_yard_gemspecs.concat(uncached_rbs_collection_gemspecs)
26
+ .sort
27
+ .uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" }
28
+ end
29
+
30
+ # @return [Array<Gem::Specification>]
31
+ attr_reader :uncached_yard_gemspecs
32
+
33
+ # @return [Array<Gem::Specification>]
34
+ attr_reader :uncached_rbs_collection_gemspecs
35
+
36
+ # @return [String, nil]
37
+ attr_reader :rbs_collection_path
38
+
39
+ # @return [String, nil]
40
+ attr_reader :rbs_collection_config_path
41
+
42
+ # @return [Workspace, nil]
17
43
  attr_reader :workspace
18
44
 
19
45
  # @return [Environ]
@@ -21,70 +47,81 @@ module Solargraph
21
47
 
22
48
  # @param requires [Array<String>]
23
49
  # @param workspace [Workspace, nil]
24
- # @param out [IO, nil] output stream for logging
50
+ # @param [Object] out
25
51
  def initialize requires, workspace, out: $stderr
26
- @provided_requires = requires.compact
52
+ @requires = requires.compact
27
53
  @workspace = workspace
54
+ @rbs_collection_path = workspace&.rbs_collection_path
55
+ @rbs_collection_config_path = workspace&.rbs_collection_config_path
56
+ @environ = Convention.for_global(self)
57
+ @requires.concat @environ.requires if @environ
58
+ load_serialized_gem_pins
59
+ pins.concat @environ.pins
28
60
  @out = out
29
61
  end
30
62
 
31
- # @return [Array<String>]
32
- def requires
33
- @requires ||= @provided_requires + (workspace.global_environ&.requires || [])
34
- end
35
- alias required requires
36
-
37
- # @sg-ignore flow sensitive typing needs to understand reassignment
38
- # @return [Array<Gem::Specification>]
39
- def uncached_gemspecs
40
- if @uncached_gemspecs.nil?
41
- @uncached_gemspecs = []
42
- pins # force lazy-loaded pin lookup
63
+ # @param out [IO, StringIO, nil]
64
+ # @return [void]
65
+ # @param [Boolean] rebuild
66
+ def cache_all! out, rebuild: false
67
+ # if we log at debug level:
68
+ if logger.info?
69
+ gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
70
+ logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty?
43
71
  end
44
- @uncached_gemspecs
45
- end
46
-
47
- # @return [Array<Pin::Base>]
48
- def pins
49
- @pins ||= load_serialized_gem_pins + (workspace.global_environ&.pins || [])
72
+ logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" }
73
+ logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" }
74
+ load_serialized_gem_pins
75
+ uncached_gemspecs.each do |gemspec|
76
+ cache(gemspec, rebuild: rebuild, out: out)
77
+ end
78
+ load_serialized_gem_pins
79
+ @uncached_rbs_collection_gemspecs = []
80
+ @uncached_yard_gemspecs = []
50
81
  end
51
82
 
83
+ # @param gemspec [Gem::Specification]
84
+ # @param out [IO, StringIO, nil]
52
85
  # @return [void]
53
- def reset_pins!
54
- @uncached_gemspecs = nil
55
- @pins = nil
56
- end
57
-
58
- # @return [Solargraph::PinCache]
59
- def pin_cache
60
- @pin_cache ||= workspace.fresh_pincache
86
+ def cache_yard_pins gemspec, out
87
+ pins = GemPins.build_yard_pins(yard_plugins, gemspec)
88
+ PinCache.serialize_yard_gem(gemspec, pins)
89
+ logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?
61
90
  end
62
91
 
63
- def any_uncached?
64
- uncached_gemspecs.any?
92
+ # @param gemspec [Gem::Specification]
93
+ # @param out [IO, StringIO, nil]
94
+ # @return [void]
95
+ def cache_rbs_collection_pins gemspec, out
96
+ rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
97
+ pins = rbs_map.pins
98
+ rbs_version_cache_key = rbs_map.cache_key
99
+ # cache pins even if result is zero, so we don't retry building pins
100
+ pins ||= []
101
+ PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins)
102
+ logger.info { "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? }
65
103
  end
66
104
 
67
- # Cache all pins needed for the sources in this doc_map
68
- # @param out [StringIO, IO, nil] output stream for logging
105
+ # @param gemspec [Gem::Specification]
69
106
  # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
107
+ # @param out [IO, StringIO, nil] output stream for logging
70
108
  # @return [void]
71
- def cache_doc_map_gems! out, rebuild: false
72
- unless uncached_gemspecs.empty?
73
- logger.info do
74
- gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
75
- "Caching pins for gems: #{gem_desc}"
76
- end
77
- end
78
- time = Benchmark.measure do
79
- uncached_gemspecs.each do |gemspec|
80
- cache(gemspec, rebuild: rebuild, out: out)
81
- end
82
- end
83
- milliseconds = (time.real * 1000).round
84
- if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any?
85
- out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms"
109
+ def cache gemspec, rebuild: false, out: nil
110
+ build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild
111
+ build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild
112
+ if build_yard || build_rbs_collection
113
+ type = []
114
+ type << 'YARD' if build_yard
115
+ type << 'RBS collection' if build_rbs_collection
116
+ out&.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}")
86
117
  end
87
- reset_pins!
118
+ cache_yard_pins(gemspec, out) if build_yard
119
+ cache_rbs_collection_pins(gemspec, out) if build_rbs_collection
120
+ end
121
+
122
+ # @return [Array<Gem::Specification>]
123
+ def gemspecs
124
+ @gemspecs ||= required_gems_map.values.compact.flatten
88
125
  end
89
126
 
90
127
  # @return [Array<String>]
@@ -92,108 +129,310 @@ module Solargraph
92
129
  @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
93
130
  end
94
131
 
95
- # @return [Array<Gem::Specification>]
96
- # @param out [IO, nil]
97
- def dependencies out: $stderr
98
- @dependencies ||=
99
- begin
100
- gem_deps = gemspecs
101
- .flat_map { |spec| workspace.fetch_dependencies(spec, out: out) }
102
- .uniq(&:name)
103
- stdlib_deps = gemspecs
104
- .flat_map { |spec| workspace.stdlib_dependencies(spec.name) }
105
- .flat_map { |dep_name| workspace.resolve_require(dep_name) }
106
- .compact
107
- existing_gems = gemspecs.map(&:name)
108
- (gem_deps + stdlib_deps).reject { |gemspec| existing_gems.include? gemspec.name }
109
- end
132
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
133
+ def self.all_yard_gems_in_memory
134
+ @all_yard_gems_in_memory ||= {}
110
135
  end
111
136
 
112
- # Cache gem documentation if needed for this doc_map
113
- #
114
- # @param gemspec [Gem::Specification]
115
- # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
116
- # @param out [StringIO, IO, nil] output stream for logging
117
- #
118
- # @return [void]
119
- def cache gemspec, rebuild: false, out: nil
120
- pin_cache.cache_gem(gemspec: gemspec,
121
- rebuild: rebuild,
122
- out: out)
137
+ # @return [Hash{String => Hash{Array(String, String) => Array<Pin::Base>}}] stored by RBS collection path
138
+ def self.all_rbs_collection_gems_in_memory
139
+ @all_rbs_collection_gems_in_memory ||= {}
123
140
  end
124
141
 
125
- private
142
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
143
+ def yard_pins_in_memory
144
+ self.class.all_yard_gems_in_memory
145
+ end
126
146
 
127
- # @return [Array<Gem::Specification>]
128
- def gemspecs
129
- @gemspecs ||= required_gems_map.values.compact.flatten
147
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
148
+ def rbs_collection_pins_in_memory
149
+ # @sg-ignore rbs_collection_path is String | nil but used as hash key
150
+ self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {}
130
151
  end
131
152
 
132
- # @param out [IO, nil]
133
- # @return [Array<Pin::Base>]
134
- def load_serialized_gem_pins out: @out
135
- serialized_pins = []
153
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
154
+ def self.all_combined_pins_in_memory
155
+ @all_combined_pins_in_memory ||= {}
156
+ end
157
+
158
+ # @todo this should also include an index by the hash of the RBS collection
159
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
160
+ def combined_pins_in_memory
161
+ self.class.all_combined_pins_in_memory
162
+ end
163
+
164
+ # @return [Array<String>]
165
+ def yard_plugins
166
+ @environ.yard_plugins
167
+ end
168
+
169
+ # @return [Set<Gem::Specification>]
170
+ def dependencies
171
+ @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set
172
+ end
173
+
174
+ private
175
+
176
+ # @return [void]
177
+ def load_serialized_gem_pins
178
+ @pins = []
179
+ @uncached_yard_gemspecs = []
180
+ @uncached_rbs_collection_gemspecs = []
136
181
  with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
137
182
  # @type [Array<String>]
138
- missing_paths = without_gemspecs.to_h.keys
183
+ paths = without_gemspecs.to_h.keys
139
184
  # @type [Array<Gem::Specification>]
140
- gemspecs = with_gemspecs.to_h.values.flatten.compact + dependencies(out: out).to_a
141
-
142
- # if we are type checking a gem project, we should not include
143
- # pins from rbs or yard from that gem here - we use our own
144
- # parser for those pins
145
-
146
- # @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification]
147
- gemspecs.reject! do |gemspec|
148
- gemspec.respond_to?(:source) &&
149
- gemspec.source.instance_of?(Bundler::Source::Gemspec) &&
150
- gemspec.source.respond_to?(:path) &&
151
- gemspec.source.path == Pathname.new('.')
152
- end
153
-
154
- missing_paths.each do |path|
155
- # this will load from disk if needed; no need to manage
156
- # uncached_gemspecs to trigger that later
157
- stdlib_name_guess = path.split('/').first
158
-
159
- # try to resolve the stdlib name
160
- # @type [Array<String>]
161
- deps = workspace.stdlib_dependencies(stdlib_name_guess) || []
162
- [stdlib_name_guess, *deps].compact.each do |potential_stdlib_name|
163
- # @sg-ignore Need to support splatting in literal array
164
- rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name
165
- serialized_pins.concat rbs_pins if rbs_pins
166
- end
185
+ gemspecs = with_gemspecs.to_h.values.flatten.compact + dependencies.to_a
186
+
187
+ paths.each do |path|
188
+ deserialize_stdlib_rbs_map path
167
189
  end
168
190
 
169
- serialized_pins.length
191
+ logger.debug { 'DocMap#load_serialized_gem_pins: Combining pins...' }
170
192
  time = Benchmark.measure do
171
193
  gemspecs.each do |gemspec|
172
- # only deserializes already-cached gems
173
- gemspec_pins = pin_cache.deserialize_combined_pin_cache gemspec
174
- if gemspec_pins
175
- serialized_pins.concat gemspec_pins
176
- else
177
- uncached_gemspecs << gemspec
178
- end
194
+ pins = deserialize_combined_pin_cache gemspec
195
+ @pins.concat pins if pins
179
196
  end
180
197
  end
181
- serialized_pins.length
182
- milliseconds = (time.real * 1000).round
183
- if (milliseconds > 500) && out && gemspecs.any?
184
- out.puts "Deserialized #{serialized_pins.length} gem pins from #{PinCache.base_dir} in #{milliseconds} ms"
185
- end
186
- uncached_gemspecs.uniq! { |gemspec| "#{gemspec.name}:#{gemspec.version}" }
187
- serialized_pins
198
+ logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" }
199
+ @uncached_yard_gemspecs.uniq!
200
+ @uncached_rbs_collection_gemspecs.uniq!
201
+ nil
188
202
  end
189
203
 
190
204
  # @return [Hash{String => Array<Gem::Specification>}]
191
205
  def required_gems_map
192
- @required_gems_map ||= requires.to_h { |path| [path, workspace.resolve_require(path)] }
206
+ @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] }
207
+ end
208
+
209
+ # @return [Hash{String => Gem::Specification}]
210
+ def preference_map
211
+ @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] }
212
+ end
213
+
214
+ # @param gemspec [Gem::Specification]
215
+ # @return [Array<Pin::Base>, nil]
216
+ def deserialize_yard_pin_cache gemspec
217
+ if yard_pins_in_memory.key?([gemspec.name, gemspec.version])
218
+ return yard_pins_in_memory[[gemspec.name, gemspec.version]]
219
+ end
220
+
221
+ cached = PinCache.deserialize_yard_gem(gemspec)
222
+ if cached
223
+ logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
224
+ yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached
225
+ cached
226
+ else
227
+ logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}"
228
+ @uncached_yard_gemspecs.push gemspec
229
+ nil
230
+ end
231
+ end
232
+
233
+ # @param gemspec [Gem::Specification]
234
+ # @return [void]
235
+ def deserialize_combined_pin_cache gemspec
236
+ unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil?
237
+ return combined_pins_in_memory[[gemspec.name, gemspec.version]]
238
+ end
239
+
240
+ rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
241
+ rbs_version_cache_key = rbs_map.cache_key
242
+
243
+ cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key)
244
+ if cached
245
+ logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
246
+ combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached
247
+ return combined_pins_in_memory[[gemspec.name, gemspec.version]]
248
+ end
249
+
250
+ rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
251
+
252
+ yard_pins = deserialize_yard_pin_cache gemspec
253
+
254
+ if !rbs_collection_pins.nil? && !yard_pins.nil?
255
+ logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" }
256
+ combined_pins = GemPins.combine(yard_pins, rbs_collection_pins)
257
+ PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins)
258
+ combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins
259
+ logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" }
260
+ return combined_pins
261
+ end
262
+
263
+ if !yard_pins.nil?
264
+ logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" }
265
+ combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins
266
+ combined_pins_in_memory[[gemspec.name, gemspec.version]]
267
+ elsif !rbs_collection_pins.nil?
268
+ logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" }
269
+ combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins
270
+ combined_pins_in_memory[[gemspec.name, gemspec.version]]
271
+ else
272
+ logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" }
273
+ nil
274
+ end
275
+ end
276
+
277
+ # @param path [String] require path that might be in the RBS stdlib collection
278
+ # @return [void]
279
+ def deserialize_stdlib_rbs_map path
280
+ map = RbsMap::StdlibMap.load(path)
281
+ if map.resolved?
282
+ logger.debug { "Loading stdlib pins for #{path}" }
283
+ @pins.concat map.pins
284
+ logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" }
285
+ map.pins
286
+ else
287
+ # @todo Temporarily ignoring unresolved `require 'set'`
288
+ logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set'
289
+ nil
290
+ end
291
+ end
292
+
293
+ # @param gemspec [Gem::Specification]
294
+ # @param rbs_version_cache_key [String]
295
+ # @return [Array<Pin::Base>, nil]
296
+ def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
297
+ return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key])
298
+ cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key)
299
+ if cached
300
+ logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty?
301
+ rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached
302
+ cached
303
+ else
304
+ logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}"
305
+ @uncached_rbs_collection_gemspecs.push gemspec
306
+ nil
307
+ end
308
+ end
309
+
310
+ # @param path [String]
311
+ # @return [::Array<Gem::Specification>, nil]
312
+ def resolve_path_to_gemspecs path
313
+ return nil if path.empty?
314
+ return gemspecs_required_from_bundler if path == 'bundler/require'
315
+
316
+ # @type [Gem::Specification, nil]
317
+ gemspec = Gem::Specification.find_by_path(path)
318
+ if gemspec.nil?
319
+ gem_name_guess = path.split('/').first
320
+ begin
321
+ # this can happen when the gem is included via a local path in
322
+ # a Gemfile; Gem doesn't try to index the paths in that case.
323
+ #
324
+ # See if we can make a good guess:
325
+ gemspec = Gem::Specification.find_by_name(gem_name_guess)
326
+ rescue Gem::MissingSpecError
327
+ logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" }
328
+ []
329
+ end
330
+ end
331
+ return nil if gemspec.nil?
332
+ [gemspec_or_preference(gemspec)]
333
+ end
334
+
335
+ # @param gemspec [Gem::Specification]
336
+ # @return [Gem::Specification]
337
+ def gemspec_or_preference gemspec
338
+ # :nocov: dormant feature
339
+ return gemspec unless preference_map.key?(gemspec.name)
340
+ return gemspec if gemspec.version == preference_map[gemspec.name].version
341
+
342
+ change_gemspec_version gemspec, preference_map[gemspec.name].version
343
+ # :nocov:
344
+ end
345
+
346
+ # @param gemspec [Gem::Specification]
347
+ # @param version [Gem::Version, String]
348
+ # @return [Gem::Specification]
349
+ def change_gemspec_version gemspec, version
350
+ Gem::Specification.find_by_name(gemspec.name, "= #{version}")
351
+ rescue Gem::MissingSpecError
352
+ Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead"
353
+ gemspec
354
+ end
355
+
356
+ # @param gemspec [Gem::Specification]
357
+ # @return [Array<Gem::Specification>]
358
+ def fetch_dependencies gemspec
359
+ # @param spec [Gem::Dependency]
360
+ # @param deps [Set<Gem::Specification>]
361
+ only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps|
362
+ Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}"
363
+ dep = Gem.loaded_specs[spec.name]
364
+ # @todo is next line necessary?
365
+ dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement)
366
+ deps.merge fetch_dependencies(dep) if deps.add?(dep)
367
+ rescue Gem::MissingSpecError
368
+ Solargraph.logger.warn "Gem dependency #{spec.name} for #{gemspec.name} not found in RubyGems."
369
+ end.to_a
370
+ end
371
+
372
+ # @param gemspec [Gem::Specification]
373
+ # @return [Array<Gem::Dependency>]
374
+ def only_runtime_dependencies gemspec
375
+ gemspec.dependencies - gemspec.development_dependencies
193
376
  end
194
377
 
195
378
  def inspect
196
379
  self.class.inspect
197
380
  end
381
+
382
+ # @return [Array<Gem::Specification>, nil]
383
+ def gemspecs_required_from_bundler
384
+ # @todo Handle projects with custom Bundler/Gemfile setups
385
+ return unless workspace&.gemfile?
386
+
387
+ # @sg-ignore workspace is checked for nil above
388
+ if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory) # rubocop:disable Style/SafeNavigationChainLength
389
+ # Find only the gems bundler is now using
390
+ Bundler.definition.locked_gems.specs.flat_map do |lazy_spec|
391
+ logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}"
392
+ [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)]
393
+ rescue Gem::MissingSpecError => e
394
+ logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess")
395
+ # can happen in local filesystem references
396
+ specs = resolve_path_to_gemspecs lazy_spec.name
397
+ logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil?
398
+ next specs
399
+ end.compact
400
+ else
401
+ logger.info 'Fetching gemspecs required from Bundler (bundler/require)'
402
+ gemspecs_required_from_external_bundle
403
+ end
404
+ end
405
+
406
+ # @return [Array<Gem::Specification>]
407
+ def gemspecs_required_from_external_bundle
408
+ logger.info 'Fetching gemspecs required from external bundle'
409
+ return [] unless workspace&.directory
410
+
411
+ Solargraph.with_clean_env do
412
+ cmd = [
413
+ 'ruby', '-e',
414
+ # @sg-ignore return above ensures workspace.directory is not nil
415
+ "require 'bundler'; require 'json'; Dir.chdir('#{workspace.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }"
416
+ ]
417
+ o, e, s = Open3.capture3(*cmd)
418
+ if s.success?
419
+ Solargraph.logger.debug "External bundle: #{o}"
420
+ hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
421
+ hash.flat_map do |name, version|
422
+ Gem::Specification.find_by_name(name, version)
423
+ rescue Gem::MissingSpecError => e
424
+ logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess")
425
+ # can happen in local filesystem references
426
+ specs = resolve_path_to_gemspecs name
427
+ logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil?
428
+ next specs
429
+ end.compact
430
+ else
431
+ # @sg-ignore return above ensures workspace.directory is not nil
432
+ Solargraph.logger.warn "Failed to load gems from bundle at #{workspace.directory}: #{e}"
433
+ []
434
+ end
435
+ end
436
+ end
198
437
  end
199
438
  end
@@ -43,37 +43,36 @@ module Solargraph
43
43
  out
44
44
  end
45
45
 
46
+ # @param yard_plugins [Array<String>] The names of YARD plugins to use.
47
+ # @param gemspec [Gem::Specification]
48
+ # @return [Array<Pin::Base>]
49
+ def self.build_yard_pins yard_plugins, gemspec
50
+ Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec)
51
+ return [] unless Yardoc.cached?(gemspec)
52
+ yardoc = Yardoc.load!(gemspec)
53
+ YardMap::Mapper.new(yardoc, gemspec).map
54
+ end
55
+
46
56
  # @param yard_pins [Array<Pin::Base>]
47
57
  # @param rbs_pins [Array<Pin::Base>]
48
58
  #
49
59
  # @return [Array<Pin::Base>]
50
60
  def self.combine yard_pins, rbs_pins
51
61
  in_yard = Set.new
52
- rbs_store = Solargraph::ApiMap::Store.new(rbs_pins)
62
+ rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins)
53
63
  combined = yard_pins.map do |yard_pin|
54
64
  in_yard.add yard_pin.path
55
- rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
56
-
57
- next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method)
65
+ rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
66
+ next yard_pin unless rbs_pin && yard_pin.instance_of?(Pin::Method)
58
67
 
59
68
  unless rbs_pin
60
- logger.debug do
61
- "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})"
62
- end
69
+ # @sg-ignore https://github.com/castwide/solargraph/pull/1114
70
+ logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" }
63
71
  next yard_pin
64
72
  end
65
73
 
66
- # at this point both yard_pins and rbs_pins are methods or
67
- # method aliases. if not plain methods, prefer the YARD one
68
- next yard_pin if rbs_pin.class != Pin::Method
69
-
70
- next rbs_pin if yard_pin.class != Pin::Method
71
-
72
- # both are method pins
73
74
  out = combine_method_pins(rbs_pin, yard_pin)
74
- logger.debug do
75
- "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}"
76
- end
75
+ logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" }
77
76
  out
78
77
  end
79
78
  in_rbs_only = rbs_pins.select do |pin|