solargraph 0.58.2 → 0.59.0.dev.1

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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +3 -0
  3. data/.github/workflows/linting.yml +4 -5
  4. data/.github/workflows/plugins.yml +40 -36
  5. data/.github/workflows/rspec.yml +45 -13
  6. data/.github/workflows/typecheck.yml +2 -2
  7. data/.gitignore +0 -1
  8. data/.rubocop_todo.yml +27 -49
  9. data/CHANGELOG.md +1 -7
  10. data/README.md +3 -3
  11. data/Rakefile +1 -0
  12. data/lib/solargraph/api_map/cache.rb +3 -3
  13. data/lib/solargraph/api_map/constants.rb +13 -3
  14. data/lib/solargraph/api_map/index.rb +22 -11
  15. data/lib/solargraph/api_map/source_to_yard.rb +13 -1
  16. data/lib/solargraph/api_map/store.rb +11 -8
  17. data/lib/solargraph/api_map.rb +105 -50
  18. data/lib/solargraph/complex_type/conformance.rb +176 -0
  19. data/lib/solargraph/complex_type/type_methods.rb +16 -2
  20. data/lib/solargraph/complex_type/unique_type.rb +170 -20
  21. data/lib/solargraph/complex_type.rb +119 -14
  22. data/lib/solargraph/convention/data_definition/data_definition_node.rb +3 -1
  23. data/lib/solargraph/convention/data_definition.rb +4 -1
  24. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +1 -0
  25. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +1 -0
  26. data/lib/solargraph/convention/struct_definition.rb +5 -1
  27. data/lib/solargraph/diagnostics/require_not_found.rb +1 -0
  28. data/lib/solargraph/diagnostics/rubocop.rb +1 -0
  29. data/lib/solargraph/diagnostics/rubocop_helpers.rb +2 -0
  30. data/lib/solargraph/diagnostics/type_check.rb +1 -0
  31. data/lib/solargraph/doc_map.rb +134 -373
  32. data/lib/solargraph/equality.rb +1 -1
  33. data/lib/solargraph/gem_pins.rb +14 -15
  34. data/lib/solargraph/language_server/host/diagnoser.rb +89 -89
  35. data/lib/solargraph/language_server/host/dispatch.rb +1 -0
  36. data/lib/solargraph/language_server/host/message_worker.rb +2 -1
  37. data/lib/solargraph/language_server/host/sources.rb +1 -0
  38. data/lib/solargraph/language_server/host.rb +6 -1
  39. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +2 -7
  40. data/lib/solargraph/language_server/message/extended/document.rb +1 -0
  41. data/lib/solargraph/language_server/message/text_document/completion.rb +2 -0
  42. data/lib/solargraph/language_server/message/text_document/definition.rb +2 -0
  43. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +2 -0
  44. data/lib/solargraph/language_server/message/text_document/formatting.rb +2 -0
  45. data/lib/solargraph/language_server/message/text_document/hover.rb +2 -0
  46. data/lib/solargraph/language_server/message/text_document/signature_help.rb +1 -0
  47. data/lib/solargraph/language_server/message/text_document/type_definition.rb +2 -0
  48. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +2 -0
  49. data/lib/solargraph/library.rb +59 -13
  50. data/lib/solargraph/location.rb +9 -4
  51. data/lib/solargraph/logging.rb +21 -1
  52. data/lib/solargraph/parser/comment_ripper.rb +7 -0
  53. data/lib/solargraph/parser/flow_sensitive_typing.rb +330 -102
  54. data/lib/solargraph/parser/node_processor/base.rb +32 -2
  55. data/lib/solargraph/parser/node_processor.rb +7 -6
  56. data/lib/solargraph/parser/parser_gem/class_methods.rb +28 -10
  57. data/lib/solargraph/parser/parser_gem/node_chainer.rb +31 -6
  58. data/lib/solargraph/parser/parser_gem/node_methods.rb +27 -7
  59. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +4 -4
  60. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +2 -0
  61. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +9 -0
  62. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +11 -11
  63. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +7 -0
  64. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +36 -6
  65. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +3 -2
  66. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +1 -0
  67. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +3 -1
  68. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +2 -2
  69. data/lib/solargraph/parser/parser_gem/node_processors/or_node.rb +22 -0
  70. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +1 -1
  71. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +2 -1
  72. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +1 -0
  73. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +12 -7
  74. data/lib/solargraph/parser/parser_gem/node_processors/when_node.rb +23 -0
  75. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +5 -1
  76. data/lib/solargraph/parser/parser_gem/node_processors.rb +4 -0
  77. data/lib/solargraph/parser/region.rb +9 -3
  78. data/lib/solargraph/parser/snippet.rb +1 -1
  79. data/lib/solargraph/pin/base.rb +53 -21
  80. data/lib/solargraph/pin/base_variable.rb +312 -20
  81. data/lib/solargraph/pin/block.rb +26 -4
  82. data/lib/solargraph/pin/breakable.rb +5 -1
  83. data/lib/solargraph/pin/callable.rb +50 -3
  84. data/lib/solargraph/pin/closure.rb +2 -6
  85. data/lib/solargraph/pin/common.rb +20 -5
  86. data/lib/solargraph/pin/compound_statement.rb +55 -0
  87. data/lib/solargraph/pin/conversions.rb +2 -1
  88. data/lib/solargraph/pin/delegated_method.rb +15 -4
  89. data/lib/solargraph/pin/documenting.rb +1 -0
  90. data/lib/solargraph/pin/instance_variable.rb +5 -1
  91. data/lib/solargraph/pin/keyword.rb +0 -4
  92. data/lib/solargraph/pin/local_variable.rb +13 -57
  93. data/lib/solargraph/pin/method.rb +90 -42
  94. data/lib/solargraph/pin/method_alias.rb +8 -0
  95. data/lib/solargraph/pin/namespace.rb +7 -1
  96. data/lib/solargraph/pin/parameter.rb +76 -13
  97. data/lib/solargraph/pin/proxy_type.rb +2 -1
  98. data/lib/solargraph/pin/reference/override.rb +1 -1
  99. data/lib/solargraph/pin/reference/superclass.rb +2 -0
  100. data/lib/solargraph/pin/reference.rb +2 -0
  101. data/lib/solargraph/pin/search.rb +1 -0
  102. data/lib/solargraph/pin/signature.rb +8 -0
  103. data/lib/solargraph/pin/symbol.rb +1 -1
  104. data/lib/solargraph/pin/until.rb +1 -1
  105. data/lib/solargraph/pin/while.rb +1 -1
  106. data/lib/solargraph/pin.rb +2 -0
  107. data/lib/solargraph/pin_cache.rb +477 -57
  108. data/lib/solargraph/position.rb +12 -26
  109. data/lib/solargraph/range.rb +6 -6
  110. data/lib/solargraph/rbs_map/conversions.rb +33 -10
  111. data/lib/solargraph/rbs_map/core_map.rb +24 -17
  112. data/lib/solargraph/rbs_map/stdlib_map.rb +34 -5
  113. data/lib/solargraph/rbs_map.rb +74 -20
  114. data/lib/solargraph/shell.rb +73 -28
  115. data/lib/solargraph/source/chain/call.rb +52 -17
  116. data/lib/solargraph/source/chain/constant.rb +2 -0
  117. data/lib/solargraph/source/chain/hash.rb +1 -0
  118. data/lib/solargraph/source/chain/if.rb +1 -0
  119. data/lib/solargraph/source/chain/instance_variable.rb +22 -1
  120. data/lib/solargraph/source/chain/literal.rb +5 -0
  121. data/lib/solargraph/source/chain/or.rb +9 -1
  122. data/lib/solargraph/source/chain.rb +25 -22
  123. data/lib/solargraph/source/change.rb +9 -2
  124. data/lib/solargraph/source/cursor.rb +7 -1
  125. data/lib/solargraph/source/source_chainer.rb +13 -3
  126. data/lib/solargraph/source/updater.rb +4 -0
  127. data/lib/solargraph/source.rb +33 -7
  128. data/lib/solargraph/source_map/clip.rb +13 -2
  129. data/lib/solargraph/source_map/data.rb +4 -1
  130. data/lib/solargraph/source_map/mapper.rb +24 -1
  131. data/lib/solargraph/source_map.rb +14 -6
  132. data/lib/solargraph/type_checker/problem.rb +3 -1
  133. data/lib/solargraph/type_checker/rules.rb +75 -2
  134. data/lib/solargraph/type_checker.rb +111 -30
  135. data/lib/solargraph/version.rb +1 -1
  136. data/lib/solargraph/workspace/config.rb +3 -1
  137. data/lib/solargraph/workspace/gemspecs.rb +367 -0
  138. data/lib/solargraph/workspace/require_paths.rb +1 -0
  139. data/lib/solargraph/workspace.rb +158 -16
  140. data/lib/solargraph/yard_map/helpers.rb +2 -1
  141. data/lib/solargraph/yard_map/mapper/to_method.rb +5 -1
  142. data/lib/solargraph/yard_map/mapper/to_namespace.rb +1 -0
  143. data/lib/solargraph/yard_map/mapper.rb +5 -0
  144. data/lib/solargraph/yardoc.rb +33 -23
  145. data/lib/solargraph.rb +24 -3
  146. data/rbs/fills/rubygems/0/dependency.rbs +193 -0
  147. data/rbs/fills/tuple/tuple.rbs +28 -0
  148. data/rbs/shims/ast/0/node.rbs +1 -1
  149. data/rbs/shims/diff-lcs/1.5/diff-lcs.rbs +11 -0
  150. data/solargraph.gemspec +2 -1
  151. metadata +12 -7
  152. data/lib/solargraph/type_checker/checks.rb +0 -124
  153. data/lib/solargraph/type_checker/param_def.rb +0 -37
  154. data/lib/solargraph/yard_map/to_method.rb +0 -89
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ module Solargraph
7
+ class Workspace
8
+ # Manages determining which gemspecs are available in a workspace
9
+ class Gemspecs
10
+ include Logging
11
+
12
+ attr_reader :directory, :preferences
13
+
14
+ # @param directory [String, nil] If nil, assume no bundle is present
15
+ # @param preferences [Array<Gem::Specification>]
16
+ def initialize directory, preferences: []
17
+ # @todo an issue with both external bundles and the potential
18
+ # preferences feature is that bundler gives you a 'clean'
19
+ # rubygems environment with only the specified versions
20
+ # installed. Possible alternatives:
21
+ #
22
+ # *) prompt the user to run solargraph outside of bundler
23
+ # and treat all bundles as external
24
+ # *) reinstall the needed gems dynamically each time
25
+ # *) manipulate the rubygems/bundler environment
26
+ @directory = directory && File.absolute_path(directory)
27
+ # @todo implement preferences as a config-exposed feature
28
+ @preferences = preferences
29
+ end
30
+
31
+ # Take the path given to a 'require' statement in a source file
32
+ # and return the Gem::Specifications which will be brought into
33
+ # scope with it, so we can load pins for them.
34
+ #
35
+ # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require'
36
+ # @return [::Array<Gem::Specification>, nil]
37
+ def resolve_require require
38
+ return nil if require.empty?
39
+
40
+ # This is added in the parser when it sees 'Bundler.require' -
41
+ # see https://bundler.io/guides/bundler_setup.html '
42
+ #
43
+ # @todo handle different arguments to Bundler.require
44
+ return auto_required_gemspecs_from_bundler if require == 'bundler/require'
45
+
46
+ # Determine gem name based on the require path
47
+ file = "lib/#{require}.rb"
48
+ spec_with_path = Gem::Specification.find_by_path(file)
49
+
50
+ all_gemspecs = all_gemspecs_from_bundle
51
+
52
+ gem_names_to_try = [
53
+ spec_with_path&.name,
54
+ require.tr('/', '-'),
55
+ require.split('/').first
56
+ ].compact.uniq
57
+ # @param gem_name [String]
58
+ gem_names_to_try.each do |gem_name|
59
+ # @sg-ignore Unresolved call to == on Boolean
60
+ gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name }
61
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
62
+ return [gemspec_or_preference(gemspec)] if gemspec
63
+
64
+ begin
65
+ gemspec = Gem::Specification.find_by_name(gem_name)
66
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
67
+ return [gemspec_or_preference(gemspec)] if gemspec
68
+ rescue Gem::MissingSpecError
69
+ logger.debug do
70
+ "Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name}"
71
+ end
72
+ end
73
+
74
+ # look ourselves just in case this is hanging out somewhere
75
+ # that find_by_path doesn't index
76
+ gemspec = all_gemspecs.find do |spec|
77
+ spec = to_gem_specification(spec) unless spec.respond_to?(:files)
78
+
79
+ # @sg-ignore Translate to something flow sensitive typing understands
80
+ spec&.files&.any? { |gemspec_file| file == gemspec_file }
81
+ end
82
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
83
+ return [gemspec_or_preference(gemspec)] if gemspec
84
+ end
85
+
86
+ nil
87
+ end
88
+
89
+ # @param stdlib_name [String]
90
+ #
91
+ # @return [Array<String>]
92
+ def stdlib_dependencies stdlib_name
93
+ deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || []
94
+ deps.map { |dep| dep['name'] }.compact
95
+ end
96
+
97
+ # @param name [String]
98
+ # @param version [String, nil]
99
+ # @param out [IO, nil] output stream for logging
100
+ #
101
+ # @return [Gem::Specification, nil]
102
+ def find_gem name, version = nil, out: $stderr
103
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
104
+ specish = all_gemspecs_from_bundle.find { |specish| specish.name == name && specish.version == version }
105
+ return to_gem_specification specish if specish
106
+
107
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
108
+ specish = all_gemspecs_from_bundle.find { |specish| specish.name == name }
109
+ # @sg-ignore flow sensitive typing needs to create separate ranges for postfix if
110
+ return to_gem_specification specish if specish
111
+
112
+ resolve_gem_ignoring_local_bundle name, version, out: out
113
+ end
114
+
115
+ # @param gemspec [Gem::Specification]
116
+ # @param out[IO, nil] output stream for logging
117
+ #
118
+ # @return [Array<Gem::Specification>]
119
+ def fetch_dependencies gemspec, out: $stderr
120
+ gemspecs = all_gemspecs_from_bundle
121
+
122
+ # @type [Hash{String => Gem::Specification}]
123
+ deps_so_far = {}
124
+
125
+ # @param runtime_dep [Gem::Dependency]
126
+ # @param deps [Hash{String => Gem::Specification}]
127
+ gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps|
128
+ dep = find_gem(runtime_dep.name, runtime_dep.requirement)
129
+ next unless dep
130
+
131
+ fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep }
132
+
133
+ deps[dep.name] ||= dep
134
+ end
135
+
136
+ # RBS tracks implicit dependencies, like how the YAML standard
137
+ # library implies pulling in the psych library.
138
+ stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || []
139
+ stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact
140
+ (gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name)
141
+ end
142
+
143
+ # Returns all gemspecs directly depended on by this workspace's
144
+ # bundle (does not include transitive dependencies).
145
+ #
146
+ # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
147
+ def all_gemspecs_from_bundle
148
+ return [] unless directory
149
+
150
+ @all_gemspecs_from_bundle ||=
151
+ if in_this_bundle?
152
+ all_gemspecs_from_this_bundle
153
+ else
154
+ all_gemspecs_from_external_bundle
155
+ end
156
+ end
157
+
158
+ # @return [Hash{Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification => Gem::Specification}]
159
+ def self.gem_specification_cache
160
+ @gem_specification_cache ||= {}
161
+ end
162
+
163
+ private
164
+
165
+ # @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification]
166
+ #
167
+ # @return [Gem::Specification, nil]
168
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
169
+ def to_gem_specification specish
170
+ # print time including milliseconds
171
+ self.class.gem_specification_cache[specish] ||= case specish
172
+ when Gem::Specification
173
+ specish
174
+ when Bundler::LazySpecification
175
+ # materializing didn't work. Let's look in the local
176
+ # rubygems without bundler's help
177
+ resolve_gem_ignoring_local_bundle specish.name,
178
+ specish.version
179
+ when Bundler::StubSpecification
180
+ # turns a Bundler::StubSpecification into a
181
+ # Gem::StubSpecification if we can
182
+ if specish.respond_to?(:stub)
183
+ # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName'
184
+ to_gem_specification specish.stub
185
+ else
186
+ # A Bundler::StubSpecification is a Bundler::
187
+ # RemoteSpecification which ought to proxy a Gem::
188
+ # Specification
189
+ specish
190
+ end
191
+ # @sg-ignore Unresolved constant Gem::StubSpecification
192
+ when Gem::StubSpecification
193
+ # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName'
194
+ specish.to_spec
195
+ else
196
+ raise "Unexpected type while resolving gem: #{specish.class}"
197
+ end
198
+ end
199
+
200
+ # @param command [String] The expression to evaluate in the external bundle
201
+ # @sg-ignore Need a JSON type
202
+ # @yield [undefined, nil]
203
+ def query_external_bundle command
204
+ Solargraph.with_clean_env do
205
+ cmd = [
206
+ 'ruby', '-e',
207
+ "require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }"
208
+ ]
209
+ o, e, s = Open3.capture3(*cmd)
210
+ if s.success?
211
+ Solargraph.logger.debug "External bundle: #{o}"
212
+ o && !o.empty? ? JSON.parse(o.split("\n").last) : nil
213
+ else
214
+ Solargraph.logger.warn e
215
+ raise BundleNotFoundError, "Failed to load gems from bundle at #{directory}"
216
+ end
217
+ end
218
+ end
219
+
220
+ # @sg-ignore need boolish support for ? methods
221
+ def in_this_bundle?
222
+ Bundler.definition&.lockfile&.to_s&.start_with?(directory)
223
+ end
224
+
225
+ # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
226
+ def all_gemspecs_from_this_bundle
227
+ # Find only the gems bundler is now using
228
+ specish_objects = Bundler.definition.locked_gems.specs
229
+ if specish_objects.first.respond_to?(:materialize_for_installation)
230
+ specish_objects = specish_objects.map(&:materialize_for_installation)
231
+ end
232
+ specish_objects.map do |specish|
233
+ if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir)
234
+ # duck type is good enough for outside uses!
235
+ specish
236
+ else
237
+ to_gem_specification(specish)
238
+ end
239
+ end.compact
240
+ end
241
+
242
+ # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
243
+ def auto_required_gemspecs_from_bundler
244
+ return [] unless directory
245
+
246
+ logger.info 'Fetching gemspecs autorequired from Bundler (bundler/require)'
247
+ @auto_required_gemspecs_from_bundler ||=
248
+ if in_this_bundle?
249
+ auto_required_gemspecs_from_this_bundle
250
+ else
251
+ auto_required_gemspecs_from_external_bundle
252
+ end
253
+ end
254
+
255
+ # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
256
+ def auto_required_gemspecs_from_this_bundle
257
+ # Adapted from require() in lib/bundler/runtime.rb
258
+ dep_names = Bundler.definition.dependencies.select do |dep|
259
+ dep.groups.include?(:default) && dep.should_include?
260
+ end.map(&:name)
261
+
262
+ all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) }
263
+ end
264
+
265
+ # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
266
+ def auto_required_gemspecs_from_external_bundle
267
+ @auto_required_gemspecs_from_external_bundle ||=
268
+ begin
269
+ logger.info 'Fetching auto-required gemspecs from Bundler (bundler/require)'
270
+ command =
271
+ 'Bundler.definition.dependencies' \
272
+ '.select { |dep| dep.groups.include?(:default) && dep.should_include? }' \
273
+ '.map(&:name)'
274
+ # @sg-ignore
275
+ # @type [Array<String>]
276
+ dep_names = query_external_bundle command
277
+
278
+ all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) }
279
+ end
280
+ end
281
+
282
+ # @param gemspec [Gem::Specification]
283
+ # @return [Array<Gem::Dependency>]
284
+ def only_runtime_dependencies gemspec
285
+ unless gemspec.respond_to?(:dependencies) && gemspec.respond_to?(:development_dependencies)
286
+ gemspec = to_gem_specification(gemspec)
287
+ end
288
+ return [] if gemspec.nil?
289
+
290
+ gemspec.dependencies - gemspec.development_dependencies
291
+ end
292
+
293
+ # @todo Should this be using Gem::SpecFetcher and pull them automatically?
294
+ #
295
+ # @param name [String]
296
+ # @param version_or_requirement [String, nil]
297
+ # @param out [IO, nil] output stream for logging
298
+ #
299
+ # @return [Gem::Specification, nil]
300
+ def resolve_gem_ignoring_local_bundle name, version_or_requirement = nil, out: $stderr
301
+ Gem::Specification.find_by_name(name, version_or_requirement)
302
+ rescue Gem::MissingSpecError
303
+ begin
304
+ Gem::Specification.find_by_name(name)
305
+ rescue Gem::MissingSpecError
306
+ stdlibmap = RbsMap::StdlibMap.new(name)
307
+ unless stdlibmap.resolved?
308
+ gem_desc = name
309
+ gem_desc += ":#{version_or_requirement}" if version_or_requirement
310
+ out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment"
311
+ end
312
+ nil # either not here or in stdlib
313
+ end
314
+ end
315
+
316
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
317
+ # @return [Array<Gem::Specification>]
318
+ def all_gemspecs_from_external_bundle
319
+ @all_gemspecs_from_external_bundle ||=
320
+ begin
321
+ logger.info 'Fetching gemspecs required from external bundle'
322
+
323
+ command = 'specish_objects = Bundler.definition.locked_gems&.specs; ' \
324
+ 'if specish_objects.first.respond_to?(:materialize_for_installation);' \
325
+ 'specish_objects = specish_objects.map(&:materialize_for_installation);' \
326
+ 'end;' \
327
+ 'specish_objects.map { |specish| [specish.name, specish.version] }'
328
+ # @type [Array<Gem::Specification>]
329
+ query_external_bundle(command).map do |name, version|
330
+ resolve_gem_ignoring_local_bundle(name, version)
331
+ end.compact
332
+ rescue Solargraph::BundleNotFoundError => e
333
+ Solargraph.logger.info e.message
334
+ # @sg-ignore Need to add nil check here
335
+ Solargraph.logger.debug e.backtrace.join("\n")
336
+ []
337
+ end
338
+ end
339
+
340
+ # @return [Hash{String => Gem::Specification}]
341
+ def preference_map
342
+ @preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] }
343
+ end
344
+
345
+ # @param gemspec [Gem::Specification]
346
+ #
347
+ # @return [Gem::Specification]
348
+ def gemspec_or_preference gemspec
349
+ return gemspec unless preference_map.key?(gemspec.name)
350
+ return gemspec if gemspec.version == preference_map[gemspec.name].version
351
+
352
+ change_gemspec_version gemspec, preference_map[gemspec.name].version
353
+ end
354
+
355
+ # @param gemspec [Gem::Specification]
356
+ # @param version [String]
357
+ # @return [Gem::Specification]
358
+ def change_gemspec_version gemspec, version
359
+ Gem::Specification.find_by_name(gemspec.name, "= #{version}")
360
+ rescue Gem::MissingSpecError
361
+ Solargraph.logger.info "Gem #{gemspec.name} version #{version.inspect} not found. " \
362
+ "Using #{gemspec.version} instead"
363
+ gemspec
364
+ end
365
+ end
366
+ end
367
+ end
@@ -83,6 +83,7 @@ module Solargraph
83
83
  return [] if hash.empty?
84
84
  hash['paths'].map { |path| File.join(base, path) }
85
85
  rescue StandardError => e
86
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
86
87
  Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}"
87
88
  []
88
89
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'open3'
4
4
  require 'json'
5
+ require 'yaml'
5
6
 
6
7
  module Solargraph
7
8
  # A workspace consists of the files in a project's directory and the
@@ -9,7 +10,10 @@ module Solargraph
9
10
  # in an associated Library or ApiMap.
10
11
  #
11
12
  class Workspace
13
+ include Logging
14
+
12
15
  autoload :Config, 'solargraph/workspace/config'
16
+ autoload :Gemspecs, 'solargraph/workspace/gemspecs'
13
17
  autoload :RequirePaths, 'solargraph/workspace/require_paths'
14
18
 
15
19
  # @return [String]
@@ -19,7 +23,8 @@ module Solargraph
19
23
  attr_reader :gemnames
20
24
  alias source_gems gemnames
21
25
 
22
- # @param directory [String] TODO: Remove '' and '*' special cases
26
+ # @todo Remove '' and '*' special cases
27
+ # @param directory [String]
23
28
  # @param config [Config, nil]
24
29
  # @param server [Hash]
25
30
  def initialize directory = '', config = nil, server = {}
@@ -50,6 +55,76 @@ module Solargraph
50
55
  @config ||= Solargraph::Workspace::Config.new(directory)
51
56
  end
52
57
 
58
+ # @param stdlib_name [String]
59
+ #
60
+ # @return [Array<String>]
61
+ def stdlib_dependencies stdlib_name
62
+ gemspecs.stdlib_dependencies(stdlib_name)
63
+ end
64
+
65
+ # @param out [IO, nil] output stream for logging
66
+ # @param gemspec [Gem::Specification]
67
+ # @return [Array<Gem::Specification>]
68
+ def fetch_dependencies gemspec, out: $stderr
69
+ gemspecs.fetch_dependencies(gemspec, out: out)
70
+ end
71
+
72
+ # @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require'
73
+ # @return [Array<Gem::Specification>, nil]
74
+ def resolve_require require
75
+ gemspecs.resolve_require(require)
76
+ end
77
+
78
+ # @return [Solargraph::PinCache]
79
+ def pin_cache
80
+ @pin_cache ||= fresh_pincache
81
+ end
82
+
83
+ # @param stdlib_name [String]
84
+ #
85
+ # @return [Array<String>]
86
+ def stdlib_dependencies stdlib_name
87
+ deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || []
88
+ deps.map { |dep| dep['name'] }.compact
89
+ end
90
+
91
+ # @return [Environ]
92
+ def global_environ
93
+ # empty docmap, since the result needs to work in any possible
94
+ # context here
95
+ @global_environ ||= Convention.for_global(DocMap.new([], self, out: nil))
96
+ end
97
+
98
+ # @param gemspec [Gem::Specification]
99
+ # @param out [StringIO, IO, nil] output stream for logging
100
+ # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
101
+ #
102
+ # @return [void]
103
+ def cache_gem gemspec, out: nil, rebuild: false
104
+ pin_cache.cache_gem(gemspec: gemspec, out: out, rebuild: rebuild)
105
+ end
106
+
107
+ # @param gemspec [Gem::Specification, Bundler::LazySpecification]
108
+ # @param out [StringIO, IO, nil] output stream for logging
109
+ #
110
+ # @return [void]
111
+ def uncache_gem gemspec, out: nil
112
+ pin_cache.uncache_gem(gemspec, out: out)
113
+ end
114
+
115
+ # @return [Solargraph::PinCache]
116
+ def fresh_pincache
117
+ PinCache.new(rbs_collection_path: rbs_collection_path,
118
+ rbs_collection_config_path: rbs_collection_config_path,
119
+ yard_plugins: yard_plugins,
120
+ directory: directory)
121
+ end
122
+
123
+ # @return [Array<String>]
124
+ def yard_plugins
125
+ @yard_plugins ||= global_environ.yard_plugins.sort.uniq
126
+ end
127
+
53
128
  # @param level [Symbol]
54
129
  # @return [TypeChecker::Rules]
55
130
  def rules(level)
@@ -63,6 +138,7 @@ module Solargraph
63
138
  # @param sources [Array<Solargraph::Source>]
64
139
  # @return [Boolean] True if the source was added to the workspace
65
140
  def merge *sources
141
+ # @sg-ignore Need to add nil check here
66
142
  unless directory == '*' || sources.all? { |source| source_hash.key?(source.filename) }
67
143
  # Reload the config to determine if a new source should be included
68
144
  @config = Solargraph::Workspace::Config.new(directory)
@@ -70,10 +146,12 @@ module Solargraph
70
146
 
71
147
  includes_any = false
72
148
  sources.each do |source|
73
- if directory == "*" || config.calculated.include?(source.filename)
74
- source_hash[source.filename] = source
75
- includes_any = true
76
- end
149
+ # @sg-ignore Need to add nil check here
150
+ next unless directory == "*" || config.calculated.include?(source.filename)
151
+
152
+ # @sg-ignore Need to add nil check here
153
+ source_hash[source.filename] = source
154
+ includes_any = true
77
155
  end
78
156
 
79
157
  includes_any
@@ -126,6 +204,23 @@ module Solargraph
126
204
  false
127
205
  end
128
206
 
207
+ # True if the workspace contains at least one gemspec file.
208
+ #
209
+ # @return [Boolean]
210
+ def gemspec?
211
+ !gemspec_files.empty?
212
+ end
213
+
214
+ # Get an array of all gemspec files in the workspace.
215
+ #
216
+ # @return [Array<String>]
217
+ def gemspec_files
218
+ return [] if directory.empty? || directory == '*'
219
+ @gemspec_files ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs|
220
+ config.allow? gs
221
+ end
222
+ end
223
+
129
224
  # @return [String, nil]
130
225
  def rbs_collection_path
131
226
  @gem_rbs_collection ||= read_rbs_collection_path
@@ -133,12 +228,57 @@ module Solargraph
133
228
 
134
229
  # @return [String, nil]
135
230
  def rbs_collection_config_path
136
- @rbs_collection_config_path ||= begin
137
- unless directory.empty? || directory == '*'
138
- yaml_file = File.join(directory, 'rbs_collection.yaml')
139
- yaml_file if File.file?(yaml_file)
231
+ @rbs_collection_config_path ||=
232
+ begin
233
+ unless directory.empty? || directory == '*'
234
+ yaml_file = File.join(directory, 'rbs_collection.yaml')
235
+ yaml_file if File.file?(yaml_file)
236
+ end
140
237
  end
238
+ end
239
+
240
+ # @param name [String]
241
+ # @param version [String, nil]
242
+ # @param out [IO, nil]
243
+ #
244
+ # @return [Gem::Specification, nil]
245
+ def find_gem name, version = nil, out: nil
246
+ gemspecs.find_gem(name, version, out: out)
247
+ end
248
+
249
+ # @return [Array<Gem::Specification>]
250
+ def all_gemspecs_from_bundle
251
+ gemspecs.all_gemspecs_from_bundle
252
+ end
253
+
254
+ # @todo make this actually work against bundle instead of pulling
255
+ # all installed gemspecs -
256
+ # https://github.com/apiology/solargraph/pull/10
257
+ # @return [Array<Gem::Specification>]
258
+ def all_gemspecs_from_bundle
259
+ Gem::Specification.to_a
260
+ end
261
+
262
+ # @param out [StringIO, IO, nil] output stream for logging
263
+ # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
264
+ # @return [void]
265
+ def cache_all_for_workspace! out, rebuild: false
266
+ PinCache.cache_core(out: out) unless PinCache.core? && !rebuild
267
+
268
+ gem_specs = all_gemspecs_from_bundle
269
+ # try any possible standard libraries, but be quiet about it
270
+ stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact
271
+ specs = (gem_specs + stdlib_specs)
272
+ specs.each do |spec|
273
+ pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec)
141
274
  end
275
+ out&.puts "Documentation cached for all #{specs.length} gems."
276
+
277
+ # do this after so that we prefer stdlib requires from gems,
278
+ # which are likely to be newer and have more pins
279
+ pin_cache.cache_all_stdlibs(out: out, rebuild: rebuild)
280
+
281
+ out&.puts "Documentation cached for core, standard library and gems."
142
282
  end
143
283
 
144
284
  # Synchronize the workspace from the provided updater.
@@ -149,7 +289,9 @@ module Solargraph
149
289
  source_hash[updater.filename] = source_hash[updater.filename].synchronize(updater)
150
290
  end
151
291
 
292
+ # @sg-ignore Need to validate config
152
293
  # @return [String]
294
+ # @sg-ignore Need to validate config
153
295
  def command_path
154
296
  server['commandPath'] || 'solargraph'
155
297
  end
@@ -160,12 +302,9 @@ module Solargraph
160
302
  directory
161
303
  end
162
304
 
163
- # True if the workspace has a root Gemfile.
164
- #
165
- # @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler)
166
- #
167
- def gemfile?
168
- directory && File.file?(File.join(directory, 'Gemfile'))
305
+ # @return [Solargraph::Workspace::Gemspecs]
306
+ def gemspecs
307
+ @gemspecs ||= Solargraph::Workspace::Gemspecs.new(directory_or_nil)
169
308
  end
170
309
 
171
310
  private
@@ -186,7 +325,10 @@ module Solargraph
186
325
  source_hash.clear
187
326
  unless directory.empty? || directory == '*'
188
327
  size = config.calculated.length
189
- raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files
328
+ if config.max_files > 0 and size > config.max_files
329
+ raise WorkspaceTooLargeError,
330
+ "The workspace is too large to index (#{size} files, #{config.max_files} max)"
331
+ end
190
332
  config.calculated.each do |filename|
191
333
  begin
192
334
  source_hash[filename] = Solargraph::Source.load(filename)
@@ -5,7 +5,7 @@ module Solargraph
5
5
 
6
6
  # @param code_object [YARD::CodeObjects::Base]
7
7
  # @param spec [Gem::Specification, nil]
8
- # @return [Solargraph::Location, nil]
8
+ # @return [Solargraph::Location]
9
9
  def object_location code_object, spec
10
10
  if spec.nil? || code_object.nil? || code_object.file.nil? || code_object.line.nil?
11
11
  if code_object.namespace.is_a?(YARD::CodeObjects::NamespaceObject)
@@ -14,6 +14,7 @@ module Solargraph
14
14
  end
15
15
  return Solargraph::Location.new(__FILE__, Solargraph::Range.from_to(__LINE__ - 1, 0, __LINE__ - 1, 0))
16
16
  end
17
+ # @sg-ignore flow sensitive typing should be able to identify more blocks that always return
17
18
  file = File.join(spec.full_gem_path, code_object.file)
18
19
  Solargraph::Location.new(file, Solargraph::Range.from_to(code_object.line - 1, 0, code_object.line - 1, 0))
19
20
  end
@@ -6,6 +6,7 @@ module Solargraph
6
6
  module ToMethod
7
7
  extend YardMap::Helpers
8
8
 
9
+ # @type [Hash{Array<String, Symbol, String> => Symbol}]
9
10
  VISIBILITY_OVERRIDE = {
10
11
  # YARD pays attention to 'private' statements prior to class methods but shouldn't
11
12
  ["Rails::Engine", :class, "find_root_with_flag"] => :public
@@ -25,9 +26,12 @@ module Solargraph
25
26
  return_type = ComplexType::SELF if name == 'new'
26
27
  comments = code_object.docstring ? code_object.docstring.all.to_s : ''
27
28
  final_scope = scope || code_object.scope
29
+ # @sg-ignore Need to add nil check here
28
30
  override_key = [closure.path, final_scope, name]
29
31
  final_visibility = VISIBILITY_OVERRIDE[override_key]
32
+ # @sg-ignore Need to add nil check here
30
33
  final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]]
34
+ # @sg-ignore Need to add nil check here
31
35
  final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym)
32
36
  final_visibility ||= visibility
33
37
  final_visibility ||= :private if code_object.module_function? && final_scope == :instance
@@ -49,6 +53,7 @@ module Solargraph
49
53
  source: :yardoc,
50
54
  )
51
55
  else
56
+ # @sg-ignore Need to add nil check here
52
57
  pin = Pin::Method.new(
53
58
  location: location,
54
59
  closure: closure,
@@ -85,7 +90,6 @@ module Solargraph
85
90
  # HACK: Skip `nil` and `self` parameters that are sometimes emitted
86
91
  # for methods defined in C
87
92
  # See https://github.com/castwide/solargraph/issues/345
88
- # @sg-ignore https://github.com/castwide/solargraph/pull/1114
89
93
  code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a|
90
94
  Solargraph::Pin::Parameter.new(
91
95
  location: location,
@@ -21,6 +21,7 @@ module Solargraph
21
21
  type: code_object.is_a?(YARD::CodeObjects::ClassObject) ? :class : :module,
22
22
  visibility: code_object.visibility,
23
23
  closure: closure,
24
+ # @sg-ignore need to add a nil check here
24
25
  gates: closure.gates,
25
26
  source: :yardoc,
26
27
  )