solargraph 0.56.0 → 0.58.2

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -0
  3. data/.github/workflows/linting.yml +127 -0
  4. data/.github/workflows/plugins.yml +183 -7
  5. data/.github/workflows/rspec.yml +55 -5
  6. data/.github/workflows/typecheck.yml +6 -3
  7. data/.gitignore +6 -0
  8. data/.overcommit.yml +72 -0
  9. data/.rspec +1 -0
  10. data/.rubocop.yml +66 -0
  11. data/.rubocop_todo.yml +1279 -0
  12. data/.yardopts +1 -0
  13. data/CHANGELOG.md +92 -1
  14. data/README.md +8 -4
  15. data/Rakefile +125 -13
  16. data/bin/solargraph +3 -0
  17. data/lib/solargraph/api_map/cache.rb +110 -109
  18. data/lib/solargraph/api_map/constants.rb +279 -0
  19. data/lib/solargraph/api_map/index.rb +193 -175
  20. data/lib/solargraph/api_map/source_to_yard.rb +97 -88
  21. data/lib/solargraph/api_map/store.rb +384 -266
  22. data/lib/solargraph/api_map.rb +945 -973
  23. data/lib/solargraph/bench.rb +1 -0
  24. data/lib/solargraph/complex_type/type_methods.rb +228 -222
  25. data/lib/solargraph/complex_type/unique_type.rb +482 -475
  26. data/lib/solargraph/complex_type.rb +444 -423
  27. data/lib/solargraph/convention/active_support_concern.rb +111 -0
  28. data/lib/solargraph/convention/base.rb +17 -0
  29. data/lib/solargraph/convention/data_definition/data_assignment_node.rb +61 -0
  30. data/lib/solargraph/convention/data_definition/data_definition_node.rb +91 -0
  31. data/lib/solargraph/convention/data_definition.rb +105 -0
  32. data/lib/solargraph/convention/gemspec.rb +3 -2
  33. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +61 -60
  34. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +102 -100
  35. data/lib/solargraph/convention/struct_definition.rb +164 -101
  36. data/lib/solargraph/convention.rb +32 -2
  37. data/lib/solargraph/diagnostics/require_not_found.rb +53 -53
  38. data/lib/solargraph/diagnostics/rubocop.rb +118 -113
  39. data/lib/solargraph/diagnostics/rubocop_helpers.rb +68 -66
  40. data/lib/solargraph/diagnostics/type_check.rb +55 -55
  41. data/lib/solargraph/doc_map.rb +439 -405
  42. data/lib/solargraph/environ.rb +9 -2
  43. data/lib/solargraph/equality.rb +34 -33
  44. data/lib/solargraph/gem_pins.rb +98 -88
  45. data/lib/solargraph/language_server/host/diagnoser.rb +89 -89
  46. data/lib/solargraph/language_server/host/dispatch.rb +130 -128
  47. data/lib/solargraph/language_server/host/message_worker.rb +112 -109
  48. data/lib/solargraph/language_server/host/sources.rb +99 -99
  49. data/lib/solargraph/language_server/host.rb +878 -871
  50. data/lib/solargraph/language_server/message/base.rb +2 -1
  51. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +114 -114
  52. data/lib/solargraph/language_server/message/extended/document.rb +23 -23
  53. data/lib/solargraph/language_server/message/text_document/completion.rb +56 -56
  54. data/lib/solargraph/language_server/message/text_document/definition.rb +40 -38
  55. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +26 -26
  56. data/lib/solargraph/language_server/message/text_document/formatting.rb +148 -131
  57. data/lib/solargraph/language_server/message/text_document/hover.rb +58 -58
  58. data/lib/solargraph/language_server/message/text_document/signature_help.rb +24 -24
  59. data/lib/solargraph/language_server/message/text_document/type_definition.rb +25 -24
  60. data/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +2 -0
  61. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +23 -23
  62. data/lib/solargraph/language_server/progress.rb +8 -0
  63. data/lib/solargraph/language_server/request.rb +4 -1
  64. data/lib/solargraph/library.rb +683 -666
  65. data/lib/solargraph/location.rb +82 -79
  66. data/lib/solargraph/logging.rb +37 -28
  67. data/lib/solargraph/page.rb +3 -0
  68. data/lib/solargraph/parser/comment_ripper.rb +69 -62
  69. data/lib/solargraph/parser/flow_sensitive_typing.rb +255 -227
  70. data/lib/solargraph/parser/node_processor/base.rb +92 -87
  71. data/lib/solargraph/parser/node_processor.rb +62 -46
  72. data/lib/solargraph/parser/parser_gem/class_methods.rb +149 -159
  73. data/lib/solargraph/parser/parser_gem/flawed_builder.rb +1 -0
  74. data/lib/solargraph/parser/parser_gem/node_chainer.rb +166 -164
  75. data/lib/solargraph/parser/parser_gem/node_methods.rb +486 -497
  76. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +22 -21
  77. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +59 -59
  78. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +15 -15
  79. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +46 -45
  80. data/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +1 -21
  81. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +53 -53
  82. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +23 -21
  83. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +40 -40
  84. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +29 -29
  85. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +59 -53
  86. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +0 -22
  87. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +98 -41
  88. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +17 -16
  89. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +38 -37
  90. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +52 -43
  91. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +291 -271
  92. data/lib/solargraph/parser/parser_gem/node_processors/sym_node.rb +1 -0
  93. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +29 -29
  94. data/lib/solargraph/parser/parser_gem/node_processors.rb +70 -66
  95. data/lib/solargraph/parser/region.rb +69 -66
  96. data/lib/solargraph/parser/snippet.rb +17 -15
  97. data/lib/solargraph/pin/base.rb +729 -651
  98. data/lib/solargraph/pin/base_variable.rb +126 -125
  99. data/lib/solargraph/pin/block.rb +104 -103
  100. data/lib/solargraph/pin/breakable.rb +9 -9
  101. data/lib/solargraph/pin/callable.rb +231 -218
  102. data/lib/solargraph/pin/closure.rb +72 -74
  103. data/lib/solargraph/pin/common.rb +79 -75
  104. data/lib/solargraph/pin/constant.rb +2 -0
  105. data/lib/solargraph/pin/conversions.rb +123 -123
  106. data/lib/solargraph/pin/delegated_method.rb +120 -120
  107. data/lib/solargraph/pin/documenting.rb +114 -114
  108. data/lib/solargraph/pin/instance_variable.rb +34 -34
  109. data/lib/solargraph/pin/keyword.rb +20 -20
  110. data/lib/solargraph/pin/local_variable.rb +75 -76
  111. data/lib/solargraph/pin/method.rb +672 -651
  112. data/lib/solargraph/pin/method_alias.rb +34 -31
  113. data/lib/solargraph/pin/namespace.rb +115 -115
  114. data/lib/solargraph/pin/parameter.rb +275 -261
  115. data/lib/solargraph/pin/proxy_type.rb +39 -35
  116. data/lib/solargraph/pin/reference/override.rb +47 -33
  117. data/lib/solargraph/pin/reference/superclass.rb +15 -10
  118. data/lib/solargraph/pin/reference.rb +39 -22
  119. data/lib/solargraph/pin/search.rb +61 -56
  120. data/lib/solargraph/pin/signature.rb +61 -59
  121. data/lib/solargraph/pin/symbol.rb +53 -48
  122. data/lib/solargraph/pin/until.rb +18 -18
  123. data/lib/solargraph/pin/while.rb +18 -18
  124. data/lib/solargraph/pin.rb +44 -44
  125. data/lib/solargraph/pin_cache.rb +245 -185
  126. data/lib/solargraph/position.rb +132 -116
  127. data/lib/solargraph/range.rb +112 -107
  128. data/lib/solargraph/rbs_map/conversions.rb +823 -773
  129. data/lib/solargraph/rbs_map/core_fills.rb +18 -0
  130. data/lib/solargraph/rbs_map/core_map.rb +58 -51
  131. data/lib/solargraph/rbs_map/stdlib_map.rb +43 -43
  132. data/lib/solargraph/rbs_map.rb +163 -150
  133. data/lib/solargraph/shell.rb +352 -268
  134. data/lib/solargraph/source/chain/call.rb +337 -333
  135. data/lib/solargraph/source/chain/constant.rb +26 -89
  136. data/lib/solargraph/source/chain/hash.rb +34 -34
  137. data/lib/solargraph/source/chain/if.rb +28 -28
  138. data/lib/solargraph/source/chain/instance_variable.rb +13 -13
  139. data/lib/solargraph/source/chain/link.rb +11 -2
  140. data/lib/solargraph/source/chain/literal.rb +48 -48
  141. data/lib/solargraph/source/chain/or.rb +23 -23
  142. data/lib/solargraph/source/chain.rb +291 -282
  143. data/lib/solargraph/source/change.rb +82 -82
  144. data/lib/solargraph/source/cursor.rb +166 -167
  145. data/lib/solargraph/source/encoding_fixes.rb +23 -23
  146. data/lib/solargraph/source/source_chainer.rb +194 -194
  147. data/lib/solargraph/source/updater.rb +55 -55
  148. data/lib/solargraph/source.rb +498 -495
  149. data/lib/solargraph/source_map/clip.rb +226 -234
  150. data/lib/solargraph/source_map/data.rb +34 -30
  151. data/lib/solargraph/source_map/mapper.rb +259 -259
  152. data/lib/solargraph/source_map.rb +212 -200
  153. data/lib/solargraph/type_checker/checks.rb +124 -124
  154. data/lib/solargraph/type_checker/param_def.rb +37 -35
  155. data/lib/solargraph/type_checker/problem.rb +32 -32
  156. data/lib/solargraph/type_checker/rules.rb +84 -62
  157. data/lib/solargraph/type_checker.rb +814 -699
  158. data/lib/solargraph/version.rb +5 -5
  159. data/lib/solargraph/workspace/config.rb +255 -239
  160. data/lib/solargraph/workspace/require_paths.rb +97 -0
  161. data/lib/solargraph/workspace.rb +220 -249
  162. data/lib/solargraph/yard_map/helpers.rb +44 -16
  163. data/lib/solargraph/yard_map/mapper/to_constant.rb +5 -5
  164. data/lib/solargraph/yard_map/mapper/to_method.rb +130 -134
  165. data/lib/solargraph/yard_map/mapper/to_namespace.rb +31 -30
  166. data/lib/solargraph/yard_map/mapper.rb +79 -79
  167. data/lib/solargraph/yard_map/to_method.rb +89 -88
  168. data/lib/solargraph/yardoc.rb +87 -49
  169. data/lib/solargraph.rb +105 -90
  170. data/rbs/fills/bundler/0/bundler.rbs +4271 -0
  171. data/rbs/fills/open3/0/open3.rbs +172 -0
  172. data/rbs/fills/rubygems/0/basic_specification.rbs +326 -0
  173. data/rbs/fills/rubygems/0/errors.rbs +364 -0
  174. data/rbs/fills/rubygems/0/spec_fetcher.rbs +107 -0
  175. data/rbs/fills/rubygems/0/specification.rbs +1753 -0
  176. data/rbs/fills/{tuple.rbs → tuple/tuple.rbs} +2 -3
  177. data/rbs/shims/ast/0/node.rbs +5 -0
  178. data/rbs/shims/ast/2.4/.rbs_meta.yaml +9 -0
  179. data/rbs/shims/ast/2.4/ast.rbs +73 -0
  180. data/rbs/shims/parser/3.2.0.1/builders/default.rbs +195 -0
  181. data/rbs/shims/parser/3.2.0.1/manifest.yaml +7 -0
  182. data/rbs/shims/parser/3.2.0.1/parser.rbs +201 -0
  183. data/rbs/shims/parser/3.2.0.1/polyfill.rbs +4 -0
  184. data/rbs/shims/thor/1.2.0.1/.rbs_meta.yaml +9 -0
  185. data/rbs/shims/thor/1.2.0.1/manifest.yaml +7 -0
  186. data/rbs/shims/thor/1.2.0.1/thor.rbs +17 -0
  187. data/rbs_collection.yaml +4 -4
  188. data/solargraph.gemspec +26 -5
  189. metadata +187 -15
  190. data/lib/.rubocop.yml +0 -22
  191. data/lib/solargraph/parser/node_methods.rb +0 -97
@@ -1,405 +1,439 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
- require 'benchmark'
5
-
6
- module Solargraph
7
- # A collection of pins generated from required gems.
8
- #
9
- class DocMap
10
- include Logging
11
-
12
- # @return [Array<String>]
13
- attr_reader :requires
14
- alias required requires
15
-
16
- # @return [Array<Gem::Specification>]
17
- attr_reader :preferences
18
-
19
- # @return [Array<Pin::Base>]
20
- attr_reader :pins
21
-
22
- # @return [Array<Gem::Specification>]
23
- def uncached_gemspecs
24
- (uncached_yard_gemspecs + uncached_rbs_collection_gemspecs).sort.
25
- uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" }
26
- end
27
-
28
- # @return [Array<Gem::Specification>]
29
- attr_reader :uncached_yard_gemspecs
30
-
31
- # @return [Array<Gem::Specification>]
32
- attr_reader :uncached_rbs_collection_gemspecs
33
-
34
- attr_reader :rbs_collection_path
35
-
36
- attr_reader :rbs_collection_config_path
37
-
38
- # @return [Workspace, nil]
39
- attr_reader :workspace
40
-
41
- # @return [Environ]
42
- attr_reader :environ
43
-
44
- # @param requires [Array<String>]
45
- # @param preferences [Array<Gem::Specification>]
46
- # @param workspace [Workspace, nil]
47
- def initialize(requires, preferences, workspace = nil)
48
- @requires = requires.compact
49
- @preferences = preferences.compact
50
- @workspace = workspace
51
- @rbs_collection_path = workspace&.rbs_collection_path
52
- @rbs_collection_config_path = workspace&.rbs_collection_config_path
53
- @environ = Convention.for_global(self)
54
- load_serialized_gem_pins
55
- pins.concat @environ.pins
56
- end
57
-
58
- def cache_all!(out)
59
- # if we log at debug level:
60
- if logger.info?
61
- gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
62
- logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty?
63
- end
64
- logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" }
65
- logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" }
66
- load_serialized_gem_pins
67
- uncached_gemspecs.each do |gemspec|
68
- cache(gemspec, out: out)
69
- end
70
- load_serialized_gem_pins
71
- @uncached_rbs_collection_gemspecs = []
72
- @uncached_yard_gemspecs = []
73
- end
74
-
75
- def cache_yard_pins(gemspec, out)
76
- pins = GemPins.build_yard_pins(gemspec)
77
- PinCache.serialize_yard_gem(gemspec, pins)
78
- logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?
79
- end
80
-
81
- def cache_rbs_collection_pins(gemspec, out)
82
- rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
83
- pins = rbs_map.pins
84
- rbs_version_cache_key = rbs_map.cache_key
85
- # cache pins even if result is zero, so we don't retry building pins
86
- pins ||= []
87
- PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins)
88
- 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? }
89
- end
90
-
91
- # @param gemspec [Gem::Specification]
92
- def cache(gemspec, rebuild: false, out: nil)
93
- build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild
94
- build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild
95
- if build_yard || build_rbs_collection
96
- type = []
97
- type << 'YARD' if build_yard
98
- type << 'RBS collection' if build_rbs_collection
99
- out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out
100
- end
101
- cache_yard_pins(gemspec, out) if build_yard
102
- cache_rbs_collection_pins(gemspec, out) if build_rbs_collection
103
- end
104
-
105
- # @return [Array<Gem::Specification>]
106
- def gemspecs
107
- @gemspecs ||= required_gems_map.values.compact.flatten
108
- end
109
-
110
- # @return [Array<String>]
111
- def unresolved_requires
112
- @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
113
- end
114
-
115
- def self.all_yard_gems_in_memory
116
- @yard_gems_in_memory ||= {}
117
- end
118
-
119
- def self.all_rbs_collection_gems_in_memory
120
- @rbs_collection_gems_in_memory ||= {}
121
- end
122
-
123
- def yard_pins_in_memory
124
- self.class.all_yard_gems_in_memory
125
- end
126
-
127
- def rbs_collection_pins_in_memory
128
- self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {}
129
- end
130
-
131
- def self.all_combined_pins_in_memory
132
- @combined_pins_in_memory ||= {}
133
- end
134
-
135
- def combined_pins_in_memory
136
- self.class.all_combined_pins_in_memory
137
- end
138
-
139
- # @return [Set<Gem::Specification>]
140
- def dependencies
141
- @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set
142
- end
143
-
144
- private
145
-
146
- # @return [void]
147
- def load_serialized_gem_pins
148
- @pins = []
149
- @uncached_yard_gemspecs = []
150
- @uncached_rbs_collection_gemspecs = []
151
- with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
152
- paths = Hash[without_gemspecs].keys
153
- gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a
154
-
155
- paths.each do |path|
156
- rbs_pins = deserialize_stdlib_rbs_map path
157
- end
158
-
159
- logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." }
160
- time = Benchmark.measure do
161
- gemspecs.each do |gemspec|
162
- pins = deserialize_combined_pin_cache gemspec
163
- @pins.concat pins if pins
164
- end
165
- end
166
- logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" }
167
- @uncached_yard_gemspecs.uniq!
168
- @uncached_rbs_collection_gemspecs.uniq!
169
- nil
170
- end
171
-
172
- # @return [Hash{String => Array<Gem::Specification>}]
173
- def required_gems_map
174
- @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] }
175
- end
176
-
177
- # @return [Hash{String => Gem::Specification}]
178
- def preference_map
179
- @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] }
180
- end
181
-
182
- # @param gemspec [Gem::Specification]
183
- # @return [Array<Pin::Base>]
184
- def deserialize_yard_pin_cache gemspec
185
- if yard_pins_in_memory.key?([gemspec.name, gemspec.version])
186
- return yard_pins_in_memory[[gemspec.name, gemspec.version]]
187
- end
188
-
189
- cached = PinCache.deserialize_yard_gem(gemspec)
190
- if cached
191
- logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
192
- yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached
193
- cached
194
- else
195
- logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}"
196
- @uncached_yard_gemspecs.push gemspec
197
- nil
198
- end
199
- end
200
-
201
- # @param gemspec [Gem::Specification]
202
- # @return [void]
203
- def deserialize_combined_pin_cache(gemspec)
204
- unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil?
205
- return combined_pins_in_memory[[gemspec.name, gemspec.version]]
206
- end
207
-
208
- rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
209
- rbs_version_cache_key = rbs_map.cache_key
210
-
211
- cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key)
212
- if cached
213
- logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
214
- combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached
215
- return combined_pins_in_memory[[gemspec.name, gemspec.version]]
216
- end
217
-
218
- rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
219
-
220
- yard_pins = deserialize_yard_pin_cache gemspec
221
-
222
- if !rbs_collection_pins.nil? && !yard_pins.nil?
223
- logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" }
224
- combined_pins = GemPins.combine(yard_pins, rbs_collection_pins)
225
- PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins)
226
- combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins
227
- logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" }
228
- return combined_pins
229
- end
230
-
231
- if !yard_pins.nil?
232
- logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" }
233
- combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins
234
- return combined_pins_in_memory[[gemspec.name, gemspec.version]]
235
- elsif !rbs_collection_pins.nil?
236
- logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" }
237
- combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins
238
- return combined_pins_in_memory[[gemspec.name, gemspec.version]]
239
- else
240
- logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" }
241
- return nil
242
- end
243
- end
244
-
245
- # @param path [String] require path that might be in the RBS stdlib collection
246
- # @return [void]
247
- def deserialize_stdlib_rbs_map path
248
- map = RbsMap::StdlibMap.load(path)
249
- if map.resolved?
250
- logger.debug { "Loading stdlib pins for #{path}" }
251
- @pins.concat map.pins
252
- logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" }
253
- map.pins
254
- else
255
- # @todo Temporarily ignoring unresolved `require 'set'`
256
- logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set'
257
- nil
258
- end
259
- end
260
-
261
- # @return [Array<Pin::Base>, nil]
262
- def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
263
- return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key])
264
- cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key)
265
- if cached
266
- logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty?
267
- rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached
268
- cached
269
- else
270
- logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}"
271
- @uncached_rbs_collection_gemspecs.push gemspec
272
- nil
273
- end
274
- end
275
-
276
- # @param gemspec [Gem::Specification]
277
- # @return [Boolean]
278
- def try_gem_in_memory gemspec
279
- gempins = DocMap.gems_in_memory[gemspec]
280
- return false unless gempins
281
- Solargraph.logger.debug "Found #{gemspec.name} #{gemspec.version} in memory"
282
- @pins.concat gempins
283
- true
284
- end
285
-
286
- # @param path [String]
287
- # @return [::Array<Gem::Specification>, nil]
288
- def resolve_path_to_gemspecs path
289
- return nil if path.empty?
290
- return gemspecs_required_from_bundler if path == 'bundler/require'
291
-
292
- gemspec = Gem::Specification.find_by_path(path)
293
- if gemspec.nil?
294
- gem_name_guess = path.split('/').first
295
- begin
296
- # this can happen when the gem is included via a local path in
297
- # a Gemfile; Gem doesn't try to index the paths in that case.
298
- #
299
- # See if we can make a good guess:
300
- potential_gemspec = Gem::Specification.find_by_name(gem_name_guess)
301
- file = "lib/#{path}.rb"
302
- gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file }
303
- rescue Gem::MissingSpecError
304
- logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" }
305
- []
306
- end
307
- end
308
- return nil if gemspec.nil?
309
- [gemspec_or_preference(gemspec)]
310
- end
311
-
312
- # @param gemspec [Gem::Specification]
313
- # @return [Gem::Specification]
314
- def gemspec_or_preference gemspec
315
- return gemspec unless preference_map.key?(gemspec.name)
316
- return gemspec if gemspec.version == preference_map[gemspec.name].version
317
-
318
- change_gemspec_version gemspec, preference_map[by_path.name].version
319
- end
320
-
321
- # @param gemspec [Gem::Specification]
322
- # @param version [Gem::Version]
323
- # @return [Gem::Specification]
324
- def change_gemspec_version gemspec, version
325
- Gem::Specification.find_by_name(gemspec.name, "= #{version}")
326
- rescue Gem::MissingSpecError
327
- Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead"
328
- gemspec
329
- end
330
-
331
- # @param gemspec [Gem::Specification]
332
- # @return [Array<Gem::Specification>]
333
- def fetch_dependencies gemspec
334
- # @param spec [Gem::Dependency]
335
- only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps|
336
- Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}"
337
- dep = Gem.loaded_specs[spec.name]
338
- # @todo is next line necessary?
339
- dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement)
340
- deps.merge fetch_dependencies(dep) if deps.add?(dep)
341
- rescue Gem::MissingSpecError
342
- Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems."
343
- end.to_a
344
- end
345
-
346
- # @param gemspec [Gem::Specification]
347
- # @return [Array<Gem::Dependency>]
348
- def only_runtime_dependencies gemspec
349
- gemspec.dependencies - gemspec.development_dependencies
350
- end
351
-
352
-
353
- def inspect
354
- self.class.inspect
355
- end
356
-
357
- def gemspecs_required_from_bundler
358
- if workspace&.directory && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory)
359
- # Find only the gems bundler is now using
360
- Bundler.definition.locked_gems.specs.flat_map do |lazy_spec|
361
- logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}"
362
- [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)]
363
- rescue Gem::MissingSpecError => e
364
- logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess")
365
- # can happen in local filesystem references
366
- specs = resolve_path_to_gemspecs lazy_spec.name
367
- logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil?
368
- next specs
369
- end.compact
370
- else
371
- logger.info 'Fetching gemspecs required from Bundler (bundler/require)'
372
- gemspecs_required_from_external_bundle
373
- end
374
- end
375
-
376
- def gemspecs_required_from_external_bundle
377
- logger.info 'Fetching gemspecs required from external bundle'
378
- return [] unless workspace&.directory
379
-
380
- Solargraph.with_clean_env do
381
- cmd = [
382
- 'ruby', '-e',
383
- "require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }"
384
- ]
385
- o, e, s = Open3.capture3(*cmd)
386
- if s.success?
387
- Solargraph.logger.debug "External bundle: #{o}"
388
- hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
389
- hash.flat_map do |name, version|
390
- Gem::Specification.find_by_name(name, version)
391
- rescue Gem::MissingSpecError => e
392
- logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess")
393
- # can happen in local filesystem references
394
- specs = resolve_path_to_gemspecs name
395
- logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil?
396
- next specs
397
- end.compact
398
- else
399
- Solargraph.logger.warn e
400
- raise BundleNotFoundError, "Failed to load gems from bundle at #{workspace&.directory}"
401
- end
402
- end
403
- end
404
- end
405
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'benchmark'
5
+ require 'open3'
6
+
7
+ module Solargraph
8
+ # A collection of pins generated from required gems.
9
+ #
10
+ class DocMap
11
+ include Logging
12
+
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]
43
+ attr_reader :workspace
44
+
45
+ # @return [Environ]
46
+ attr_reader :environ
47
+
48
+ # @param requires [Array<String>]
49
+ # @param preferences [Array<Gem::Specification>]
50
+ # @param workspace [Workspace, nil]
51
+ def initialize(requires, preferences, workspace = nil)
52
+ @requires = requires.compact
53
+ @preferences = preferences.compact
54
+ @workspace = workspace
55
+ @rbs_collection_path = workspace&.rbs_collection_path
56
+ @rbs_collection_config_path = workspace&.rbs_collection_config_path
57
+ @environ = Convention.for_global(self)
58
+ @requires.concat @environ.requires if @environ
59
+ load_serialized_gem_pins
60
+ pins.concat @environ.pins
61
+ end
62
+
63
+ # @param out [IO]
64
+ # @return [void]
65
+ def cache_all!(out)
66
+ # if we log at debug level:
67
+ if logger.info?
68
+ gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
69
+ logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty?
70
+ end
71
+ logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" }
72
+ logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" }
73
+ load_serialized_gem_pins
74
+ uncached_gemspecs.each do |gemspec|
75
+ cache(gemspec, out: out)
76
+ end
77
+ load_serialized_gem_pins
78
+ @uncached_rbs_collection_gemspecs = []
79
+ @uncached_yard_gemspecs = []
80
+ end
81
+
82
+ # @param gemspec [Gem::Specification]
83
+ # @param out [IO]
84
+ # @return [void]
85
+ def cache_yard_pins(gemspec, out)
86
+ pins = GemPins.build_yard_pins(yard_plugins, gemspec)
87
+ PinCache.serialize_yard_gem(gemspec, pins)
88
+ logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?
89
+ end
90
+
91
+ # @param gemspec [Gem::Specification]
92
+ # @param out [IO]
93
+ # @return [void]
94
+ def cache_rbs_collection_pins(gemspec, out)
95
+ rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
96
+ pins = rbs_map.pins
97
+ rbs_version_cache_key = rbs_map.cache_key
98
+ # cache pins even if result is zero, so we don't retry building pins
99
+ pins ||= []
100
+ PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins)
101
+ 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? }
102
+ end
103
+
104
+ # @param gemspec [Gem::Specification]
105
+ # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
106
+ # @param out [IO, nil] output stream for logging
107
+ # @return [void]
108
+ def cache(gemspec, rebuild: false, out: nil)
109
+ build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild
110
+ build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild
111
+ if build_yard || build_rbs_collection
112
+ type = []
113
+ type << 'YARD' if build_yard
114
+ type << 'RBS collection' if build_rbs_collection
115
+ out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out
116
+ end
117
+ cache_yard_pins(gemspec, out) if build_yard
118
+ cache_rbs_collection_pins(gemspec, out) if build_rbs_collection
119
+ end
120
+
121
+ # @return [Array<Gem::Specification>]
122
+ def gemspecs
123
+ @gemspecs ||= required_gems_map.values.compact.flatten
124
+ end
125
+
126
+ # @return [Array<String>]
127
+ def unresolved_requires
128
+ @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
129
+ end
130
+
131
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
132
+ def self.all_yard_gems_in_memory
133
+ @yard_gems_in_memory ||= {}
134
+ end
135
+
136
+ # @return [Hash{String => Hash{Array(String, String) => Array<Pin::Base>}}] stored by RBS collection path
137
+ def self.all_rbs_collection_gems_in_memory
138
+ @rbs_collection_gems_in_memory ||= {}
139
+ end
140
+
141
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
142
+ def yard_pins_in_memory
143
+ self.class.all_yard_gems_in_memory
144
+ end
145
+
146
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
147
+ def rbs_collection_pins_in_memory
148
+ self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {}
149
+ end
150
+
151
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
152
+ def self.all_combined_pins_in_memory
153
+ @combined_pins_in_memory ||= {}
154
+ end
155
+
156
+ # @todo this should also include an index by the hash of the RBS collection
157
+ # @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
158
+ def combined_pins_in_memory
159
+ self.class.all_combined_pins_in_memory
160
+ end
161
+
162
+ # @return [Array<String>]
163
+ def yard_plugins
164
+ @environ.yard_plugins
165
+ end
166
+
167
+ # @return [Set<Gem::Specification>]
168
+ def dependencies
169
+ @dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set
170
+ end
171
+
172
+ private
173
+
174
+ # @return [void]
175
+ def load_serialized_gem_pins
176
+ @pins = []
177
+ @uncached_yard_gemspecs = []
178
+ @uncached_rbs_collection_gemspecs = []
179
+ with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
180
+ # @sg-ignore Need support for RBS duck interfaces like _ToHash
181
+ # @type [Array<String>]
182
+ paths = Hash[without_gemspecs].keys
183
+ # @sg-ignore Need support for RBS duck interfaces like _ToHash
184
+ # @type [Array<Gem::Specification>]
185
+ gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a
186
+
187
+ paths.each do |path|
188
+ rbs_pins = deserialize_stdlib_rbs_map path
189
+ end
190
+
191
+ logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." }
192
+ time = Benchmark.measure do
193
+ gemspecs.each do |gemspec|
194
+ pins = deserialize_combined_pin_cache gemspec
195
+ @pins.concat pins if pins
196
+ end
197
+ end
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
202
+ end
203
+
204
+ # @return [Hash{String => Array<Gem::Specification>}]
205
+ def required_gems_map
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
+ return 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
+ return combined_pins_in_memory[[gemspec.name, gemspec.version]]
271
+ else
272
+ logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" }
273
+ return 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
+ potential_gemspec = Gem::Specification.find_by_name(gem_name_guess)
326
+ file = "lib/#{path}.rb"
327
+ gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file }
328
+ rescue Gem::MissingSpecError
329
+ logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" }
330
+ []
331
+ end
332
+ end
333
+ return nil if gemspec.nil?
334
+ [gemspec_or_preference(gemspec)]
335
+ end
336
+
337
+ # @param gemspec [Gem::Specification]
338
+ # @return [Gem::Specification]
339
+ def gemspec_or_preference gemspec
340
+ # :nocov: dormant feature
341
+ return gemspec unless preference_map.key?(gemspec.name)
342
+ return gemspec if gemspec.version == preference_map[gemspec.name].version
343
+
344
+ change_gemspec_version gemspec, preference_map[gemspec.name].version
345
+ # :nocov:
346
+ end
347
+
348
+ # @param gemspec [Gem::Specification]
349
+ # @param version [Gem::Version]
350
+ # @return [Gem::Specification]
351
+ def change_gemspec_version gemspec, version
352
+ Gem::Specification.find_by_name(gemspec.name, "= #{version}")
353
+ rescue Gem::MissingSpecError
354
+ Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead"
355
+ gemspec
356
+ end
357
+
358
+ # @param gemspec [Gem::Specification]
359
+ # @return [Array<Gem::Specification>]
360
+ def fetch_dependencies gemspec
361
+ # @param spec [Gem::Dependency]
362
+ # @param deps [Set<Gem::Specification>]
363
+ only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps|
364
+ Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}"
365
+ dep = Gem.loaded_specs[spec.name]
366
+ # @todo is next line necessary?
367
+ # @sg-ignore Unresolved call to requirement on Gem::Dependency
368
+ dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement)
369
+ deps.merge fetch_dependencies(dep) if deps.add?(dep)
370
+ rescue Gem::MissingSpecError
371
+ # @sg-ignore Unresolved call to requirement on Gem::Dependency
372
+ Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems."
373
+ end.to_a
374
+ end
375
+
376
+ # @param gemspec [Gem::Specification]
377
+ # @return [Array<Gem::Dependency>]
378
+ def only_runtime_dependencies gemspec
379
+ gemspec.dependencies - gemspec.development_dependencies
380
+ end
381
+
382
+
383
+ def inspect
384
+ self.class.inspect
385
+ end
386
+
387
+ # @return [Array<Gem::Specification>, nil]
388
+ def gemspecs_required_from_bundler
389
+ # @todo Handle projects with custom Bundler/Gemfile setups
390
+ return unless workspace.gemfile?
391
+
392
+ if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory)
393
+ # Find only the gems bundler is now using
394
+ Bundler.definition.locked_gems.specs.flat_map do |lazy_spec|
395
+ logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}"
396
+ [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)]
397
+ rescue Gem::MissingSpecError => e
398
+ logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess")
399
+ # can happen in local filesystem references
400
+ specs = resolve_path_to_gemspecs lazy_spec.name
401
+ logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil?
402
+ next specs
403
+ end.compact
404
+ else
405
+ logger.info 'Fetching gemspecs required from Bundler (bundler/require)'
406
+ gemspecs_required_from_external_bundle
407
+ end
408
+ end
409
+
410
+ # @return [Array<Gem::Specification>, nil]
411
+ def gemspecs_required_from_external_bundle
412
+ logger.info 'Fetching gemspecs required from external bundle'
413
+ return [] unless workspace&.directory
414
+
415
+ Solargraph.with_clean_env do
416
+ cmd = [
417
+ 'ruby', '-e',
418
+ "require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }"
419
+ ]
420
+ o, e, s = Open3.capture3(*cmd)
421
+ if s.success?
422
+ Solargraph.logger.debug "External bundle: #{o}"
423
+ hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
424
+ hash.flat_map do |name, version|
425
+ Gem::Specification.find_by_name(name, version)
426
+ rescue Gem::MissingSpecError => e
427
+ logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess")
428
+ # can happen in local filesystem references
429
+ specs = resolve_path_to_gemspecs name
430
+ logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil?
431
+ next specs
432
+ end.compact
433
+ else
434
+ Solargraph.logger.warn "Failed to load gems from bundle at #{workspace&.directory}: #{e}"
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end