solargraph 0.60.3 → 0.61.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/plugins.yml +1 -5
  3. data/.github/workflows/rspec.yml +3 -1
  4. data/CHANGELOG.md +26 -0
  5. data/Gemfile +0 -3
  6. data/lib/solargraph/api_map/index.rb +15 -2
  7. data/lib/solargraph/api_map/store.rb +13 -0
  8. data/lib/solargraph/api_map.rb +60 -81
  9. data/lib/solargraph/cache_dir.rb +58 -0
  10. data/lib/solargraph/collection/base.rb +50 -0
  11. data/lib/solargraph/collection/core.rb +17 -0
  12. data/lib/solargraph/collection/gem.rb +57 -0
  13. data/lib/solargraph/collection/rbs.rb +49 -0
  14. data/lib/solargraph/collection/stdlib.rb +25 -0
  15. data/lib/solargraph/collection.rb +31 -0
  16. data/lib/solargraph/complex_type.rb +18 -5
  17. data/lib/solargraph/convention.rb +5 -3
  18. data/lib/solargraph/diagnostics/require_not_found.rb +0 -4
  19. data/lib/solargraph/external.rb +159 -0
  20. data/lib/solargraph/language_server/message/text_document/definition.rb +4 -1
  21. data/lib/solargraph/language_server/message/text_document/hover.rb +1 -2
  22. data/lib/solargraph/library.rb +11 -24
  23. data/lib/solargraph/metagem.rb +80 -0
  24. data/lib/solargraph/parser/parser_gem/node_chainer.rb +22 -3
  25. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +14 -5
  26. data/lib/solargraph/pin/closure.rb +17 -0
  27. data/lib/solargraph/pin/factory_parameter.rb +84 -0
  28. data/lib/solargraph/pin/method.rb +7 -4
  29. data/lib/solargraph/pin.rb +1 -0
  30. data/lib/solargraph/rbs_map/base.rb +31 -0
  31. data/lib/solargraph/rbs_map/conversions.rb +3 -1
  32. data/lib/solargraph/rbs_map/{core_map.rb → core.rb} +18 -39
  33. data/lib/solargraph/rbs_map/core_fills.rb +1 -1
  34. data/lib/solargraph/rbs_map/gem.rb +13 -0
  35. data/lib/solargraph/rbs_map/helpers.rb +60 -0
  36. data/lib/solargraph/rbs_map/path.rb +19 -0
  37. data/lib/solargraph/rbs_map/stdlib.rb +24 -0
  38. data/lib/solargraph/rbs_map.rb +7 -198
  39. data/lib/solargraph/repo.rb +177 -0
  40. data/lib/solargraph/shell.rb +49 -99
  41. data/lib/solargraph/source/chain/call.rb +190 -67
  42. data/lib/solargraph/source/chain/literal.rb +17 -17
  43. data/lib/solargraph/source/chain/parameter.rb +79 -0
  44. data/lib/solargraph/source/chain.rb +44 -5
  45. data/lib/solargraph/source/source_chainer.rb +40 -44
  46. data/lib/solargraph/source_map/clip.rb +5 -3
  47. data/lib/solargraph/source_map/mapper.rb +6 -2
  48. data/lib/solargraph/source_map.rb +10 -5
  49. data/lib/solargraph/type_checker/rules.rb +2 -1
  50. data/lib/solargraph/version.rb +1 -1
  51. data/lib/solargraph/workspace.rb +0 -1
  52. data/lib/solargraph/yard_map/cache.rb +1 -1
  53. data/lib/solargraph/yard_map/directives/attribute_directive.rb +1 -1
  54. data/lib/solargraph/yard_map/directives/domain_directive.rb +1 -1
  55. data/lib/solargraph/yard_map/directives/method_directive.rb +1 -1
  56. data/lib/solargraph/yard_map/directives/override_directive.rb +1 -1
  57. data/lib/solargraph/yard_map/directives/parse_directive.rb +1 -1
  58. data/lib/solargraph/yard_map/directives/scope_directive.rb +178 -0
  59. data/lib/solargraph/yard_map/directives/visibility_directive.rb +1 -1
  60. data/lib/solargraph/yard_map/directives.rb +5 -2
  61. data/lib/solargraph/yard_map/helpers.rb +1 -1
  62. data/lib/solargraph/yard_map/macro.rb +34 -5
  63. data/lib/solargraph/yard_map/mapper/to_constant.rb +1 -1
  64. data/lib/solargraph/yard_map/mapper/to_method.rb +1 -1
  65. data/lib/solargraph/yard_map/mapper/to_namespace.rb +1 -1
  66. data/lib/solargraph/yard_map/mapper.rb +37 -4
  67. data/lib/solargraph/yard_map.rb +1 -1
  68. data/lib/solargraph/yardoc.rb +43 -60
  69. data/lib/solargraph.rb +6 -3
  70. data/solargraph.gemspec +1 -1
  71. metadata +22 -9
  72. data/lib/solargraph/doc_map.rb +0 -439
  73. data/lib/solargraph/gem_pins.rb +0 -87
  74. data/lib/solargraph/pin_cache.rb +0 -248
  75. data/lib/solargraph/rbs_map/stdlib_map.rb +0 -71
  76. data/lib/solargraph/workspace/gemspecs.rb +0 -373
@@ -3,7 +3,7 @@
3
3
  require 'rbs'
4
4
 
5
5
  module Solargraph
6
- class RbsMap
6
+ module RbsMap
7
7
  # Functions for converting RBS declarations to Solargraph pins
8
8
  #
9
9
  class Conversions
@@ -49,6 +49,8 @@ module Solargraph
49
49
  return
50
50
  end
51
51
  environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) }
52
+ rescue RBS::DuplicatedDeclarationError => e
53
+ Solargraph.logger.warn "RBS conversion failed due to duplicate declarations: [#{e.class}] #{e.message}"
52
54
  end
53
55
 
54
56
  # @param decl [RBS::AST::Declarations::Base]
@@ -1,39 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class RbsMap
5
- # Ruby core pins
6
- #
7
- class CoreMap
8
- include Logging
4
+ module RbsMap
5
+ class Core < Base
6
+ FILLS_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills'))
9
7
 
10
- def resolved?
11
- true
8
+ def pins
9
+ @pins ||= generate_pins
12
10
  end
13
11
 
14
- FILLS_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'rbs', 'fills'))
15
-
16
- def initialize; end
12
+ # @return [RBS::EnvironmentLoader]
13
+ def loader
14
+ @loader ||= RBS::EnvironmentLoader.new(repository: repository)
15
+ end
17
16
 
18
- # @param out [IO, nil] output stream for logging
19
- # @return [Enumerable<Pin::Base>]
20
- def pins out: $stderr
21
- return @pins if @pins
22
- @pins = cache_core(out: out)
17
+ def repository
18
+ @repository ||= RBS::Repository.new(no_stdlib: false)
23
19
  end
24
20
 
25
- # @param out [StringIO, IO, nil] output stream for logging
21
+ private
22
+
26
23
  # @return [Array<Pin::Base>]
27
- def cache_core out: $stderr
28
- new_pins = []
29
- cache = PinCache.deserialize_core
30
- return cache if cache
31
- new_pins.concat conversions.pins
24
+ def generate_pins
25
+ new_pins = RbsMap::Conversions.new(loader: loader).pins
32
26
 
33
27
  # Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader
34
28
  fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false))
35
29
  fill_loader.add(path: Pathname(FILLS_DIRECTORY))
36
- out&.puts 'Caching RBS pins for Ruby core'
37
30
  fill_conversions = Conversions.new(loader: fill_loader)
38
31
  new_pins.concat fill_conversions.pins
39
32
 
@@ -42,23 +35,9 @@ module Solargraph
42
35
 
43
36
  # process overrides, then remove any which couldn't be resolved
44
37
  processed = ApiMap::Store.new(new_pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) }
45
- new_pins.replace processed
46
-
47
- PinCache.serialize_core new_pins
48
-
49
- new_pins
50
- end
51
-
52
- private
53
-
54
- # @return [RBS::EnvironmentLoader]
55
- def loader
56
- @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false))
57
- end
58
-
59
- # @return [Conversions]
60
- def conversions
61
- @conversions ||= Conversions.new(loader: loader)
38
+ # serial = Marshal.dump(processed)
39
+ # File.write cache_file, serial, mode: 'wb'
40
+ processed
62
41
  end
63
42
  end
64
43
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- class RbsMap
4
+ module RbsMap
5
5
  # Override pins to fill gaps in RbsMap::CoreMap coverage
6
6
  #
7
7
  module CoreFills
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module RbsMap
5
+ class Gem < Base
6
+ # @param metagem [Metagem]
7
+ def initialize metagem
8
+ super()
9
+ loader.add path: Pathname.new(metagem.full_path)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module RbsMap
5
+ module Helpers
6
+ module_function
7
+
8
+ # @param orig_pins [Array<Pin::Base>]
9
+ # @param rbs_pins [Array<Pin::Base>]
10
+ # @return [Array<Pin::Base>]
11
+ def combine orig_pins, rbs_pins
12
+ in_orig = Set.new
13
+ # @todo There's gotta be a better way!
14
+ rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins)
15
+ combined = orig_pins.map do |orig_pin|
16
+ in_orig.add orig_pin.path
17
+ rbs_pin = rbs_api_map.get_path_pins(orig_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
18
+ next orig_pin unless rbs_pin && orig_pin.instance_of?(Pin::Method)
19
+
20
+ unless rbs_pin
21
+ # @sg-ignore https://github.com/castwide/solargraph/pull/1114
22
+ Solargraph.logger.debug { "GemPins.combine: No rbs pin for #{orig_pin.path} - using YARD's '#{orig_pin.inspect} (return_type=#{orig_pin.return_type}; signatures=#{orig_pin.signatures})" }
23
+ next orig_pin
24
+ end
25
+
26
+ out = combine_method_pins(rbs_pin, orig_pin)
27
+ Solargraph.logger.debug { "GemPins.combine: Combining yard.path=#{orig_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{orig_pin.inspect} into #{out}" }
28
+ out
29
+ end
30
+ in_rbs_only = rbs_pins.select do |pin|
31
+ pin.path.nil? || !in_orig.include?(pin.path)
32
+ end
33
+ out = combined + in_rbs_only
34
+ Solargraph.logger.debug { "GemPins#combine: Returning #{out.length} combined pins" }
35
+ out
36
+ end
37
+
38
+ # @param pins [Array<Pin::Method>]
39
+ # @return [Pin::Method, nil]
40
+ def combine_method_pins(*pins)
41
+ # @type [Pin::Method, nil]
42
+ combined_pin = nil
43
+ # @param memo [Pin::Method, nil]
44
+ # @param pin [Pin::Method]
45
+ out = pins.reduce(combined_pin) do |memo, pin|
46
+ next pin if memo.nil?
47
+ if memo == pin && memo.source != :combined
48
+ # @todo we should track down situations where we are handled
49
+ # the same pin from the same source here and eliminate them -
50
+ # this is an efficiency workaround for now
51
+ next memo
52
+ end
53
+ memo.combine_with(pin)
54
+ end
55
+ Solargraph.logger.debug { "GemPins.combine_method_pins(pins.length=#{pins.length}, pins=#{pins}) => #{out.inspect}" }
56
+ out
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module RbsMap
5
+ class Path < Base
6
+ # @param path [String]
7
+ # @return [Array<Pin::Base>]
8
+ def self.pins path
9
+ new(path).pins
10
+ end
11
+
12
+ # @param path [String]
13
+ def initialize path
14
+ super()
15
+ loader.add path: Pathname.new(path)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbs'
4
+
5
+ module Solargraph
6
+ module RbsMap
7
+ # Ruby stdlib pins
8
+ #
9
+ class Stdlib < Base
10
+ def initialize library
11
+ super()
12
+ loader.add(library: library.gsub('/', '-')) if self.class.has?(library)
13
+ end
14
+
15
+ def repository
16
+ @repository ||= RBS::Repository.new(no_stdlib: false)
17
+ end
18
+
19
+ def self.has? library
20
+ !!RBS::Collection::Sources::Stdlib.instance.has?(library.gsub('/', '-'), nil)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,207 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
4
- require 'pathname'
5
3
  require 'rbs'
6
4
 
7
5
  module Solargraph
8
- class RbsMap
6
+ module RbsMap
7
+ autoload :Base, 'solargraph/rbs_map/base'
9
8
  autoload :Conversions, 'solargraph/rbs_map/conversions'
10
- autoload :CoreMap, 'solargraph/rbs_map/core_map'
9
+ autoload :Core, 'solargraph/rbs_map/core'
11
10
  autoload :CoreFills, 'solargraph/rbs_map/core_fills'
12
- autoload :StdlibMap, 'solargraph/rbs_map/stdlib_map'
13
-
14
- include Logging
15
-
16
- # @type [Hash{String => RbsMap}]
17
- @@rbs_maps_hash = {}
18
-
19
- attr_reader :library, :rbs_collection_paths, :rbs_collection_config_path
20
-
21
- # @param library [String]
22
- # @param version [String, nil]
23
- # @param rbs_collection_config_path [String, Pathname, nil]
24
- # @param rbs_collection_paths [Array<Pathname, String>]
25
- # @param out [StringIO, IO, nil] where to log messages
26
- def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [], out: $stderr
27
- if rbs_collection_config_path.nil? && !rbs_collection_paths.empty?
28
- raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths'
29
- end
30
- @library = library
31
- @version = version
32
- @rbs_collection_config_path = rbs_collection_config_path
33
- @rbs_collection_paths = rbs_collection_paths
34
- add_library loader, library, version
35
- end
36
-
37
- CACHE_KEY_GEM_EXPORT = 'gem-export'
38
- CACHE_KEY_UNRESOLVED = 'unresolved'
39
- CACHE_KEY_STDLIB = 'stdlib'
40
- CACHE_KEY_LOCAL = 'local'
41
-
42
- # @param cache_key [String, nil]
43
- # @return [String, nil] a description of the source of the RBS info
44
- def self.rbs_source_desc cache_key
45
- case cache_key
46
- when CACHE_KEY_GEM_EXPORT
47
- 'RBS gem export'
48
- when CACHE_KEY_UNRESOLVED
49
- nil
50
- when CACHE_KEY_STDLIB
51
- 'RBS standard library'
52
- when CACHE_KEY_LOCAL
53
- 'local RBS shims'
54
- else
55
- 'RBS collection'
56
- end
57
- end
58
-
59
- # @return [RBS::EnvironmentLoader]
60
- def loader
61
- @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository)
62
- end
63
-
64
- # @return [String] representing the version of the RBS info fetched
65
- # for the given library. Must change when the RBS info is
66
- # updated upstream for the same library and version. May change
67
- # if the config for where information comes form changes.
68
- def cache_key
69
- return CACHE_KEY_UNRESOLVED unless resolved?
70
-
71
- @cache_key ||= begin
72
- # @type [String, nil]
73
- data = nil
74
- # @type gem_config [nil, Hash{String => Hash{String => String}}]
75
- gem_config = nil
76
- if rbs_collection_config_path
77
- # @sg-ignore rbs_collection_config_path is not nil here
78
- lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path))
79
- if lockfile_path.exist?
80
- collection_config = RBS::Collection::Config.from_path lockfile_path
81
- gem_config = collection_config.gem(library)
82
- data = gem_config&.to_s
83
- end
84
- end
85
- if gem_config.nil?
86
- CACHE_KEY_STDLIB
87
- else
88
- # @type [String]
89
- source = gem_config.dig('source', 'type')
90
- case source
91
- when 'rubygems'
92
- CACHE_KEY_GEM_EXPORT
93
- when 'local'
94
- CACHE_KEY_LOCAL
95
- when 'stdlib'
96
- CACHE_KEY_STDLIB
97
- else
98
- Digest::SHA1.hexdigest(data || '')
99
- end
100
- end
101
- end
102
- end
103
-
104
- # @param gemspec [Gem::Specification, Bundler::LazySpecification]
105
- # @param rbs_collection_path [String, Pathname, nil]
106
- # @param rbs_collection_config_path [String, Pathname, nil]
107
- # @return [RbsMap]
108
- def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path
109
- # prefers stdlib RBS if available
110
- rbs_map = RbsMap::StdlibMap.new(gemspec.name)
111
- return rbs_map if rbs_map.resolved?
112
-
113
- rbs_map = RbsMap.new(gemspec.name, gemspec.version,
114
- rbs_collection_paths: [rbs_collection_path].compact,
115
- rbs_collection_config_path: rbs_collection_config_path)
116
- return rbs_map if rbs_map.resolved?
117
-
118
- # try any version of the gem in the collection
119
- RbsMap.new(gemspec.name, nil,
120
- rbs_collection_paths: [rbs_collection_path].compact,
121
- rbs_collection_config_path: rbs_collection_config_path)
122
- end
123
-
124
- # @param out [IO, nil] where to log messages
125
- # @return [Array<Pin::Base>]
126
- def pins out: $stderr
127
- @pins ||= if resolved?
128
- conversions.pins
129
- else
130
- []
131
- end
132
- end
133
-
134
- # @generic T
135
- # @param path [String]
136
- # @param klass [Class<generic<T>>]
137
- #
138
- # @sg-ignore Need to be able to resolve generics based on a
139
- # Class<generic<T>> param
140
- # @return [generic<T>, nil]
141
- def path_pin path, klass = Pin::Base
142
- pin = pins.find { |p| p.path == path }
143
- pin if pin.is_a?(klass)
144
- end
145
-
146
- # @param path [String]
147
- # @return [Array<Pin::Base>]
148
- def path_pins path
149
- pins.select { |p| p.path == path }
150
- end
151
-
152
- def resolved?
153
- @resolved
154
- end
155
-
156
- # @return [RBS::Repository]
157
- def repository
158
- @repository ||= RBS::Repository.new(no_stdlib: false).tap do |repo|
159
- @rbs_collection_paths.each do |dir|
160
- dir_path = Pathname.new(dir)
161
- repo.add(dir_path) if dir_path.exist? && dir_path.directory?
162
- end
163
- end
164
- end
165
-
166
- # @param library [String]
167
- # @return [RbsMap]
168
- def self.load library
169
- @@rbs_maps_hash[library] ||= RbsMap.new(library)
170
- end
171
-
172
- private
173
-
174
- # @return [Conversions]
175
- def conversions
176
- @conversions ||= Conversions.new(loader: loader)
177
- end
178
-
179
- def resolve_dependencies?
180
- # we need to resolve dependencies via gemfile.lock manually for
181
- # YARD regardless, so use same mechanism here so we don't
182
- # duplicate work generating pins from dependencies
183
- false
184
- end
185
-
186
- # @param loader [RBS::EnvironmentLoader]
187
- # @param library [String]
188
- # @param version [String, nil] the version of the library to load, or nil for any
189
- # @param out [StringIO, IO, nil] where to log messages
190
- # @return [Boolean] true if adding the library succeeded
191
- def add_library loader, library, version, out: $stderr
192
- @resolved = if loader.has_library?(library: library, version: version)
193
- loader.add library: library, version: version, resolve_dependencies: resolve_dependencies?
194
- logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" }
195
- true
196
- else
197
- logger.info { "#{short_name} did not find data for library #{library}:#{version}" }
198
- false
199
- end
200
- end
201
-
202
- # @return [String]
203
- def short_name
204
- self.class.name.split('::').last
205
- end
11
+ autoload :Gem, 'solargraph/rbs_map/gem'
12
+ autoload :Helpers, 'solargraph/rbs_map/helpers'
13
+ autoload :Path, 'solargraph/rbs_map/path'
14
+ autoload :Stdlib, 'solargraph/rbs_map/stdlib'
206
15
  end
207
16
  end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+
6
+ module Solargraph
7
+ # A repository for the gems available in the specified directory. If the
8
+ # directory has a Gemfile, Repo will use its gem definitions. Otherwise it
9
+ # uses the system gems.
10
+ #
11
+ class Repo
12
+ # @return [String, nil]
13
+ attr_reader :directory
14
+
15
+ # @param directory [String, nil]
16
+ def initialize directory
17
+ @directory = File.expand_path(directory) if directory
18
+ build_from_directory
19
+ end
20
+
21
+ # Find a metagem by path from the directory's bundle definition or the
22
+ # system gems.
23
+ #
24
+ # @param path [String]
25
+ # @return [Metagem, nil]
26
+ def find_by_path path
27
+ return system_find_by_path(path) unless bundled?
28
+
29
+ bundled_metagem_path_map[path]
30
+ end
31
+
32
+ # Find a metagem by name from the directory's bundle definition or the
33
+ # system gems.
34
+ #
35
+ # @param name [String]
36
+ # @return [Metagem, nil]
37
+ def find_by_name name
38
+ return system_find_by_name(name) unless bundled?
39
+
40
+ bundled_metagem_name_map[name]
41
+ end
42
+
43
+ # Get an array of metagems by its bundle group name. Returns an empty array
44
+ # if the repo isn't bundled or the group doesn't exist.
45
+ #
46
+ # @param group [Symbol]
47
+ # @return [Array<Metagem>]
48
+ def find_by_group group
49
+ return system_find_by_group(group) unless bundled?
50
+
51
+ bundled_group_map[group] || []
52
+ end
53
+
54
+ # True if the directory has a bundle definition.
55
+ #
56
+ # @note If Solargraph itself is running in this directory's bundled
57
+ # environment, `bundled?` is false.
58
+ #
59
+ def bundled?
60
+ !!@metagems
61
+ end
62
+ alias bundle? bundled?
63
+
64
+ # Metagems from the directory's bundle definition, or an empty array if a
65
+ # bundle definition does not exist.
66
+ #
67
+ # @return [Array<Solargraph::Metagem>]
68
+ def bundled
69
+ @metagems || []
70
+ end
71
+
72
+ private
73
+
74
+ # @return [String, nil]
75
+ def gemfile
76
+ @gemfile ||= File.expand_path('Gemfile', directory) if directory
77
+ end
78
+
79
+ # @return [String, nil]
80
+ def lockfile
81
+ @lockfile ||= File.expand_path('Gemfile.lock', directory) if directory
82
+ end
83
+
84
+ def bundled_directory?
85
+ directory && File.file?(gemfile) && File.file?(lockfile)
86
+ end
87
+
88
+ # Load metagems from the directory's bundle definition if available.
89
+ #
90
+ # This method performs gem collection in a separate process to suppress
91
+ # output from the `Bundler.definition.specs` call.
92
+ #
93
+ # @return [Array<Metagem>, nil]
94
+ def build_from_directory
95
+ return unless bundled_directory? && ENV['BUNDLE_GEMFILE'] != gemfile
96
+
97
+ Solargraph.with_clean_env do
98
+ cmd = ['ruby', '-e', bundle_script]
99
+ o, e, s = Open3.capture3(*cmd, chdir: directory)
100
+ if s.success?
101
+ json = o && !o.empty? ? JSON.parse(o.strip.split("\n").last, symbolize_names: true) : []
102
+ @metagems = json[:metagems].map { |data| Metagem.new(**data) }
103
+ @bundled_group_map = json[:groups].transform_values { |names| names.map { |name| bundled_metagem_name_map[name] } }
104
+ else
105
+ Solargraph.logger.warn "Failed to load gems from bundle at #{directory}: #{e}"
106
+ nil
107
+ end
108
+ end
109
+ end
110
+
111
+ def bundle_script
112
+ "
113
+ require 'bundler/setup'
114
+ require 'json'
115
+
116
+ puts Bundler.definition.specs.length
117
+ metagems = Bundler.definition.specs.map do |spec|
118
+ {
119
+ name: spec.name,
120
+ full_path: spec.full_gem_path,
121
+ spec_file: spec.spec_file,
122
+ source: spec.source.to_s,
123
+ version: spec.version,
124
+ require_paths: spec.require_paths,
125
+ dependencies: spec.dependencies.map(&:name)
126
+ }
127
+ end
128
+ groups = Bundler.definition.groups.to_h do |group|
129
+ [group, Bundler.definition.specs_for([group]).map(&:name)]
130
+ end
131
+ puts({ groups: groups, metagems: metagems }.to_json)
132
+ "
133
+ end
134
+
135
+ # @param path [String]
136
+ # @return [Metagem, nil]
137
+ def system_find_by_path path
138
+ gem = Gem::Specification.find_by_path(path)
139
+ gem && Metagem.from_specification(gem)
140
+ end
141
+
142
+ # @param name [String]
143
+ # @return [Metagem, nil]
144
+ def system_find_by_name name
145
+ gem = Gem::Specification.find_by_name(name)
146
+ gem && Metagem.from_specification(gem)
147
+ rescue Gem::MissingSpecError => _e
148
+ nil
149
+ end
150
+
151
+ # @param group [Symbol]
152
+ # @return [Array<Metagem>]
153
+ def system_find_by_group group
154
+ return [] unless ENV['BUNDLE_GEMFILE']
155
+
156
+ Bundler.definition
157
+ .specs_for([group])
158
+ .map { |spec| Metagem.from_specification(spec) }
159
+ end
160
+
161
+ def bundled_metagem_name_map
162
+ @bundled_metagem_name_map ||= bundled.to_set
163
+ .classify(&:name)
164
+ .transform_values(&:first)
165
+ end
166
+
167
+ def bundled_metagem_path_map
168
+ @bundled_metagem_path_map ||= Hash.new do |hash, path|
169
+ hash[path] = bundled.find { |mg| mg.require?(path) }
170
+ end
171
+ end
172
+
173
+ def bundled_group_map
174
+ @bundled_group_map ||= {}
175
+ end
176
+ end
177
+ end