solargraph 0.58.1 → 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 (162) 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/.rubocop_todo.yml +27 -49
  8. data/README.md +3 -3
  9. data/Rakefile +1 -0
  10. data/lib/solargraph/api_map/cache.rb +110 -110
  11. data/lib/solargraph/api_map/constants.rb +289 -279
  12. data/lib/solargraph/api_map/index.rb +204 -193
  13. data/lib/solargraph/api_map/source_to_yard.rb +109 -97
  14. data/lib/solargraph/api_map/store.rb +387 -384
  15. data/lib/solargraph/api_map.rb +1000 -945
  16. data/lib/solargraph/complex_type/conformance.rb +176 -0
  17. data/lib/solargraph/complex_type/type_methods.rb +242 -228
  18. data/lib/solargraph/complex_type/unique_type.rb +632 -482
  19. data/lib/solargraph/complex_type.rb +549 -444
  20. data/lib/solargraph/convention/data_definition/data_definition_node.rb +93 -91
  21. data/lib/solargraph/convention/data_definition.rb +108 -105
  22. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +62 -61
  23. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +103 -102
  24. data/lib/solargraph/convention/struct_definition.rb +168 -164
  25. data/lib/solargraph/diagnostics/require_not_found.rb +54 -53
  26. data/lib/solargraph/diagnostics/rubocop.rb +119 -118
  27. data/lib/solargraph/diagnostics/rubocop_helpers.rb +70 -68
  28. data/lib/solargraph/diagnostics/type_check.rb +56 -55
  29. data/lib/solargraph/doc_map.rb +200 -439
  30. data/lib/solargraph/equality.rb +34 -34
  31. data/lib/solargraph/gem_pins.rb +97 -98
  32. data/lib/solargraph/language_server/host/dispatch.rb +131 -130
  33. data/lib/solargraph/language_server/host/message_worker.rb +113 -112
  34. data/lib/solargraph/language_server/host/sources.rb +100 -99
  35. data/lib/solargraph/language_server/host.rb +883 -878
  36. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +109 -114
  37. data/lib/solargraph/language_server/message/extended/document.rb +24 -23
  38. data/lib/solargraph/language_server/message/text_document/completion.rb +58 -56
  39. data/lib/solargraph/language_server/message/text_document/definition.rb +42 -40
  40. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +28 -26
  41. data/lib/solargraph/language_server/message/text_document/formatting.rb +150 -148
  42. data/lib/solargraph/language_server/message/text_document/hover.rb +60 -58
  43. data/lib/solargraph/language_server/message/text_document/signature_help.rb +25 -24
  44. data/lib/solargraph/language_server/message/text_document/type_definition.rb +27 -25
  45. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +25 -23
  46. data/lib/solargraph/library.rb +729 -683
  47. data/lib/solargraph/location.rb +87 -82
  48. data/lib/solargraph/logging.rb +57 -37
  49. data/lib/solargraph/parser/comment_ripper.rb +76 -69
  50. data/lib/solargraph/parser/flow_sensitive_typing.rb +483 -255
  51. data/lib/solargraph/parser/node_processor/base.rb +122 -92
  52. data/lib/solargraph/parser/node_processor.rb +63 -62
  53. data/lib/solargraph/parser/parser_gem/class_methods.rb +167 -149
  54. data/lib/solargraph/parser/parser_gem/node_chainer.rb +191 -166
  55. data/lib/solargraph/parser/parser_gem/node_methods.rb +506 -486
  56. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +22 -22
  57. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +61 -59
  58. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +24 -15
  59. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +46 -46
  60. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +60 -53
  61. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +53 -23
  62. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +41 -40
  63. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +30 -29
  64. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +61 -59
  65. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +98 -98
  66. data/lib/solargraph/parser/parser_gem/node_processors/or_node.rb +22 -0
  67. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +17 -17
  68. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +39 -38
  69. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +53 -52
  70. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +296 -291
  71. data/lib/solargraph/parser/parser_gem/node_processors/when_node.rb +23 -0
  72. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +33 -29
  73. data/lib/solargraph/parser/parser_gem/node_processors.rb +74 -70
  74. data/lib/solargraph/parser/region.rb +75 -69
  75. data/lib/solargraph/parser/snippet.rb +17 -17
  76. data/lib/solargraph/pin/base.rb +761 -729
  77. data/lib/solargraph/pin/base_variable.rb +418 -126
  78. data/lib/solargraph/pin/block.rb +126 -104
  79. data/lib/solargraph/pin/breakable.rb +13 -9
  80. data/lib/solargraph/pin/callable.rb +278 -231
  81. data/lib/solargraph/pin/closure.rb +68 -72
  82. data/lib/solargraph/pin/common.rb +94 -79
  83. data/lib/solargraph/pin/compound_statement.rb +55 -0
  84. data/lib/solargraph/pin/conversions.rb +124 -123
  85. data/lib/solargraph/pin/delegated_method.rb +131 -120
  86. data/lib/solargraph/pin/documenting.rb +115 -114
  87. data/lib/solargraph/pin/instance_variable.rb +38 -34
  88. data/lib/solargraph/pin/keyword.rb +16 -20
  89. data/lib/solargraph/pin/local_variable.rb +31 -75
  90. data/lib/solargraph/pin/method.rb +720 -672
  91. data/lib/solargraph/pin/method_alias.rb +42 -34
  92. data/lib/solargraph/pin/namespace.rb +121 -115
  93. data/lib/solargraph/pin/parameter.rb +338 -275
  94. data/lib/solargraph/pin/proxy_type.rb +40 -39
  95. data/lib/solargraph/pin/reference/override.rb +47 -47
  96. data/lib/solargraph/pin/reference/superclass.rb +17 -15
  97. data/lib/solargraph/pin/reference.rb +41 -39
  98. data/lib/solargraph/pin/search.rb +62 -61
  99. data/lib/solargraph/pin/signature.rb +69 -61
  100. data/lib/solargraph/pin/symbol.rb +53 -53
  101. data/lib/solargraph/pin/until.rb +18 -18
  102. data/lib/solargraph/pin/while.rb +18 -18
  103. data/lib/solargraph/pin.rb +46 -44
  104. data/lib/solargraph/pin_cache.rb +665 -245
  105. data/lib/solargraph/position.rb +118 -119
  106. data/lib/solargraph/range.rb +112 -112
  107. data/lib/solargraph/rbs_map/conversions.rb +846 -823
  108. data/lib/solargraph/rbs_map/core_map.rb +65 -58
  109. data/lib/solargraph/rbs_map/stdlib_map.rb +72 -43
  110. data/lib/solargraph/rbs_map.rb +217 -163
  111. data/lib/solargraph/shell.rb +397 -352
  112. data/lib/solargraph/source/chain/call.rb +372 -337
  113. data/lib/solargraph/source/chain/constant.rb +28 -26
  114. data/lib/solargraph/source/chain/hash.rb +35 -34
  115. data/lib/solargraph/source/chain/if.rb +29 -28
  116. data/lib/solargraph/source/chain/instance_variable.rb +34 -13
  117. data/lib/solargraph/source/chain/literal.rb +53 -48
  118. data/lib/solargraph/source/chain/or.rb +31 -23
  119. data/lib/solargraph/source/chain.rb +294 -291
  120. data/lib/solargraph/source/change.rb +89 -82
  121. data/lib/solargraph/source/cursor.rb +172 -166
  122. data/lib/solargraph/source/source_chainer.rb +204 -194
  123. data/lib/solargraph/source/updater.rb +59 -55
  124. data/lib/solargraph/source.rb +524 -498
  125. data/lib/solargraph/source_map/clip.rb +237 -226
  126. data/lib/solargraph/source_map/data.rb +37 -34
  127. data/lib/solargraph/source_map/mapper.rb +282 -259
  128. data/lib/solargraph/source_map.rb +220 -212
  129. data/lib/solargraph/type_checker/problem.rb +34 -32
  130. data/lib/solargraph/type_checker/rules.rb +157 -84
  131. data/lib/solargraph/type_checker.rb +895 -814
  132. data/lib/solargraph/version.rb +1 -1
  133. data/lib/solargraph/workspace/config.rb +257 -255
  134. data/lib/solargraph/workspace/gemspecs.rb +367 -0
  135. data/lib/solargraph/workspace/require_paths.rb +98 -97
  136. data/lib/solargraph/workspace.rb +362 -220
  137. data/lib/solargraph/yard_map/helpers.rb +45 -44
  138. data/lib/solargraph/yard_map/mapper/to_method.rb +134 -130
  139. data/lib/solargraph/yard_map/mapper/to_namespace.rb +32 -31
  140. data/lib/solargraph/yard_map/mapper.rb +84 -79
  141. data/lib/solargraph/yardoc.rb +97 -87
  142. data/lib/solargraph.rb +126 -105
  143. data/rbs/fills/rubygems/0/dependency.rbs +193 -0
  144. data/rbs/fills/tuple/tuple.rbs +28 -0
  145. data/rbs/shims/ast/0/node.rbs +5 -0
  146. data/rbs/shims/diff-lcs/1.5/diff-lcs.rbs +11 -0
  147. data/rbs_collection.yaml +1 -1
  148. data/solargraph.gemspec +2 -1
  149. metadata +22 -17
  150. data/lib/solargraph/type_checker/checks.rb +0 -124
  151. data/lib/solargraph/type_checker/param_def.rb +0 -37
  152. data/lib/solargraph/yard_map/to_method.rb +0 -89
  153. data/sig/shims/ast/0/node.rbs +0 -5
  154. /data/{sig → rbs}/shims/ast/2.4/.rbs_meta.yaml +0 -0
  155. /data/{sig → rbs}/shims/ast/2.4/ast.rbs +0 -0
  156. /data/{sig → rbs}/shims/parser/3.2.0.1/builders/default.rbs +0 -0
  157. /data/{sig → rbs}/shims/parser/3.2.0.1/manifest.yaml +0 -0
  158. /data/{sig → rbs}/shims/parser/3.2.0.1/parser.rbs +0 -0
  159. /data/{sig → rbs}/shims/parser/3.2.0.1/polyfill.rbs +0 -0
  160. /data/{sig → rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml +0 -0
  161. /data/{sig → rbs}/shims/thor/1.2.0.1/manifest.yaml +0 -0
  162. /data/{sig → rbs}/shims/thor/1.2.0.1/thor.rbs +0 -0
@@ -1,945 +1,1000 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
- require 'yard'
5
- require 'solargraph/yard_tags'
6
-
7
- module Solargraph
8
- # An aggregate provider for information about Workspaces, Sources, gems, and
9
- # the Ruby core.
10
- #
11
- class ApiMap
12
- autoload :Cache, 'solargraph/api_map/cache'
13
- autoload :SourceToYard, 'solargraph/api_map/source_to_yard'
14
- autoload :Store, 'solargraph/api_map/store'
15
- autoload :Index, 'solargraph/api_map/index'
16
- autoload :Constants, 'solargraph/api_map/constants'
17
-
18
- # @return [Array<String>]
19
- attr_reader :unresolved_requires
20
-
21
- @@core_map = RbsMap::CoreMap.new
22
-
23
- # @return [Array<String>]
24
- attr_reader :missing_docs
25
-
26
- # @param pins [Array<Solargraph::Pin::Base>]
27
- def initialize pins: []
28
- @source_map_hash = {}
29
- @cache = Cache.new
30
- index pins
31
- end
32
-
33
- #
34
- # This is a mutable object, which is cached in the Chain class -
35
- # if you add any fields which change the results of calls (not
36
- # just caches), please also change `equality_fields` below.
37
- #
38
-
39
- # @param other [Object]
40
- def eql?(other)
41
- self.class == other.class &&
42
- # @sg-ignore Flow sensitive typing needs to handle self.class == other.class
43
- equality_fields == other.equality_fields
44
- end
45
-
46
- # @param other [Object]
47
- def ==(other)
48
- self.eql?(other)
49
- end
50
-
51
- def hash
52
- equality_fields.hash
53
- end
54
-
55
- def to_s
56
- self.class.to_s
57
- end
58
-
59
- # avoid enormous dump
60
- def inspect
61
- to_s
62
- end
63
-
64
- # @param pins [Array<Pin::Base>]
65
- # @return [self]
66
- def index pins
67
- # @todo This implementation is incomplete. It should probably create a
68
- # Bench.
69
- @source_map_hash = {}
70
- conventions_environ.clear
71
- cache.clear
72
- store.update @@core_map.pins, pins
73
- self
74
- end
75
-
76
- # Map a single source.
77
- #
78
- # @param source [Source]
79
- # @param live [Boolean] True for live source map (active editor file)
80
- # @return [self]
81
- def map source, live: false
82
- map = Solargraph::SourceMap.map(source)
83
- catalog Bench.new(source_maps: [map], live_map: live ? map : nil)
84
- self
85
- end
86
-
87
- # Catalog a bench.
88
- #
89
- # @param bench [Bench]
90
- # @return [self]
91
- def catalog bench
92
- @source_map_hash = bench.source_map_hash
93
- iced_pins = bench.icebox.flat_map(&:pins)
94
- live_pins = bench.live_map&.all_pins || []
95
- conventions_environ.clear
96
- source_map_hash.each_value do |map|
97
- conventions_environ.merge map.conventions_environ
98
- end
99
- unresolved_requires = (bench.external_requires + conventions_environ.requires + bench.workspace.config.required).to_a.compact.uniq
100
- recreate_docmap = @unresolved_requires != unresolved_requires ||
101
- @doc_map&.uncached_yard_gemspecs&.any? ||
102
- @doc_map&.uncached_rbs_collection_gemspecs&.any? ||
103
- @doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path
104
- if recreate_docmap
105
- @doc_map = DocMap.new(unresolved_requires, [], bench.workspace) # @todo Implement gem preferences
106
- @unresolved_requires = @doc_map.unresolved_requires
107
- end
108
- @cache.clear if store.update(@@core_map.pins, @doc_map.pins, conventions_environ.pins, iced_pins, live_pins)
109
- @missing_docs = [] # @todo Implement missing docs
110
- self
111
- end
112
-
113
- # @todo need to model type def statement in chains as a symbol so
114
- # that this overload of 'protected' will typecheck @sg-ignore
115
- # @sg-ignore
116
- protected def equality_fields
117
- [self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires]
118
- end
119
-
120
- # @return [DocMap]
121
- def doc_map
122
- @doc_map ||= DocMap.new([], [])
123
- end
124
-
125
- # @return [::Array<Gem::Specification>]
126
- def uncached_gemspecs
127
- @doc_map&.uncached_gemspecs || []
128
- end
129
-
130
- # @return [::Array<Gem::Specification>]
131
- def uncached_rbs_collection_gemspecs
132
- @doc_map.uncached_rbs_collection_gemspecs
133
- end
134
-
135
- # @return [::Array<Gem::Specification>]
136
- def uncached_yard_gemspecs
137
- @doc_map.uncached_yard_gemspecs
138
- end
139
-
140
- # @return [Enumerable<Pin::Base>]
141
- def core_pins
142
- @@core_map.pins
143
- end
144
-
145
- # @param name [String]
146
- # @return [YARD::Tags::MacroDirective, nil]
147
- def named_macro name
148
- store.named_macros[name]
149
- end
150
-
151
- # @return [Set<String>]
152
- def required
153
- @required ||= Set.new
154
- end
155
-
156
- # @return [Environ]
157
- def conventions_environ
158
- @conventions_environ ||= Environ.new
159
- end
160
-
161
- # @param filename [String]
162
- # @param position [Position, Array(Integer, Integer)]
163
- # @return [Source::Cursor]
164
- def cursor_at filename, position
165
- position = Position.normalize(position)
166
- raise FileNotFoundError, "File not found: #{filename}" unless source_map_hash.key?(filename)
167
- source_map_hash[filename].cursor_at(position)
168
- end
169
-
170
- # Get a clip by filename and position.
171
- #
172
- # @param filename [String]
173
- # @param position [Position, Array(Integer, Integer)]
174
- # @return [SourceMap::Clip]
175
- def clip_at filename, position
176
- position = Position.normalize(position)
177
- clip(cursor_at(filename, position))
178
- end
179
-
180
- # Create an ApiMap with a workspace in the specified directory.
181
- #
182
- # @param directory [String]
183
- #
184
- # @return [ApiMap]
185
- def self.load directory
186
- api_map = new
187
- workspace = Solargraph::Workspace.new(directory)
188
- # api_map.catalog Bench.new(workspace: workspace)
189
- library = Library.new(workspace)
190
- library.map!
191
- api_map.catalog library.bench
192
- api_map
193
- end
194
-
195
- # @param out [IO, nil]
196
- # @return [void]
197
- def cache_all!(out)
198
- @doc_map.cache_all!(out)
199
- end
200
-
201
- # @param gemspec [Gem::Specification]
202
- # @param rebuild [Boolean]
203
- # @param out [IO, nil]
204
- # @return [void]
205
- def cache_gem(gemspec, rebuild: false, out: nil)
206
- @doc_map.cache(gemspec, rebuild: rebuild, out: out)
207
- end
208
-
209
- class << self
210
- include Logging
211
- end
212
-
213
- # Create an ApiMap with a workspace in the specified directory and cache
214
- # any missing gems.
215
- #
216
- #
217
- # @param directory [String]
218
- # @param out [IO] The output stream for messages
219
- #
220
- # @return [ApiMap]
221
- def self.load_with_cache directory, out
222
- api_map = load(directory)
223
- if api_map.uncached_gemspecs.empty?
224
- logger.info { "All gems cached for #{directory}" }
225
- return api_map
226
- end
227
-
228
- api_map.cache_all!(out)
229
- load(directory)
230
- end
231
-
232
- # @return [Array<Solargraph::Pin::Base>]
233
- def pins
234
- store.pins.clone.freeze
235
- end
236
-
237
- # An array of pins based on Ruby keywords (`if`, `end`, etc.).
238
- #
239
- # @return [Enumerable<Solargraph::Pin::Keyword>]
240
- def keyword_pins
241
- store.pins_by_class(Pin::Keyword)
242
- end
243
-
244
- # True if the namespace exists.
245
- #
246
- # @param name [String] The namespace to match
247
- # @param context [String] The context to search
248
- # @return [Boolean]
249
- def namespace_exists? name, context = ''
250
- !qualify(name, context).nil?
251
- end
252
-
253
- # Get suggestions for constants in the specified namespace. The result
254
- # may contain both constant and namespace pins.
255
- #
256
- # @param namespace [String] The namespace
257
- # @param contexts [Array<String>] The contexts
258
- # @return [Array<Solargraph::Pin::Constant, Solargraph::Pin::Namespace>]
259
- def get_constants namespace, *contexts
260
- namespace ||= ''
261
- gates = contexts.clone
262
- gates.push '' if contexts.empty? && namespace.empty?
263
- gates.push namespace unless namespace.empty?
264
- store.constants
265
- .collect(gates)
266
- .select { |pin| namespace.empty? || contexts.empty? || pin.namespace == namespace }
267
- .select { |pin| pin.visibility == :public || pin.namespace == namespace }
268
- end
269
-
270
- # @param namespace [String]
271
- # @param context [String]
272
- # @return [Array<Pin::Namespace>]
273
- def get_namespace_pins namespace, context
274
- store.fqns_pins(qualify(namespace, context))
275
- end
276
-
277
- # Determine fully qualified tag for a given tag used inside the
278
- # definition of another tag ("context"). This method will start
279
- # the search in the specified context until it finds a match for
280
- # the tag.
281
- #
282
- # Does not recurse into qualifying the type parameters, but
283
- # returns any which were passed in unchanged.
284
- #
285
- # @param tag [String, nil] The namespace to
286
- # match, complete with generic parameters set to appropriate
287
- # values if available
288
- # @param gates [Array<String>] The fully qualified context in which
289
- # the tag was referenced; start from here to resolve the name.
290
- # Should not be prefixed with '::'.
291
- # @return [String, nil] fully qualified tag
292
- def qualify tag, *gates
293
- store.constants.qualify(tag, *gates)
294
- end
295
-
296
- # @see Store::Constants#resolve
297
- #
298
- # @param name [String]
299
- # @param gates [Array<String, Array<String>>]
300
- # @return [String, nil]
301
- def resolve name, *gates
302
- store.constants.resolve(name, *gates)
303
- end
304
-
305
- # Get a fully qualified namespace from a reference pin.
306
- #
307
- # @param pin [Pin::Reference]
308
- # @return [String, nil]
309
- def dereference(pin)
310
- store.constants.dereference(pin)
311
- end
312
-
313
- # @param fqns [String]
314
- # @return [Array<Pin::Reference::Extend>]
315
- def get_extends(fqns)
316
- store.get_extends(fqns)
317
- end
318
-
319
- # @param fqns [String]
320
- # @return [Array<Pin::Reference::Include>]
321
- def get_includes(fqns)
322
- store.get_includes(fqns)
323
- end
324
-
325
- # Get an array of instance variable pins defined in specified namespace
326
- # and scope.
327
- #
328
- # @param namespace [String] A fully qualified namespace
329
- # @param scope [Symbol] :instance or :class
330
- # @return [Array<Solargraph::Pin::InstanceVariable>]
331
- def get_instance_variable_pins(namespace, scope = :instance)
332
- result = []
333
- used = [namespace]
334
- result.concat store.get_instance_variables(namespace, scope)
335
- sc_fqns = namespace
336
- while (sc = store.get_superclass(sc_fqns))
337
- sc_fqns = store.constants.dereference(sc)
338
- result.concat store.get_instance_variables(sc_fqns, scope)
339
- end
340
- result
341
- end
342
-
343
- # @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins
344
- # @see Solargraph::Parser::FlowSensitiveTyping#visible_pins
345
- def visible_pins(*args, **kwargs, &blk)
346
- Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk)
347
- end
348
-
349
- # Get an array of class variable pins for a namespace.
350
- #
351
- # @param namespace [String] A fully qualified namespace
352
- # @return [Enumerable<Solargraph::Pin::ClassVariable>]
353
- def get_class_variable_pins(namespace)
354
- prefer_non_nil_variables(store.get_class_variables(namespace))
355
- end
356
-
357
- # @return [Enumerable<Solargraph::Pin::Base>]
358
- def get_symbols
359
- store.get_symbols
360
- end
361
-
362
- # @return [Enumerable<Solargraph::Pin::GlobalVariable>]
363
- def get_global_variable_pins
364
- store.pins_by_class(Pin::GlobalVariable)
365
- end
366
-
367
- # @return [Enumerable<Solargraph::Pin::Block>]
368
- def get_block_pins
369
- store.pins_by_class(Pin::Block)
370
- end
371
-
372
- # Get an array of methods available in a particular context.
373
- #
374
- # @param rooted_tag [String] The fully qualified namespace to search for methods
375
- # @param scope [Symbol] :class or :instance
376
- # @param visibility [Array<Symbol>] :public, :protected, and/or :private
377
- # @param deep [Boolean] True to include superclasses, mixins, etc.
378
- # @return [Array<Solargraph::Pin::Method>]
379
- def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true
380
- if rooted_tag.start_with? 'Array('
381
- # Array() are really tuples - use our fill, as the RBS repo
382
- # does not give us definitions for it
383
- rooted_tag = "Solargraph::Fills::Tuple(#{rooted_tag[6..-2]})"
384
- end
385
- rooted_type = ComplexType.try_parse(rooted_tag)
386
- fqns = rooted_type.namespace
387
- namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
388
- cached = cache.get_methods(rooted_tag, scope, visibility, deep)
389
- return cached.clone unless cached.nil?
390
- # @type [Array<Solargraph::Pin::Method>]
391
- result = []
392
- skip = Set.new
393
- if rooted_tag == ''
394
- # @todo Implement domains
395
- conventions_environ.domains.each do |domain|
396
- type = ComplexType.try_parse(domain)
397
- next if type.undefined?
398
- result.concat inner_get_methods(type.name, type.scope, visibility, deep, skip)
399
- end
400
- result.concat inner_get_methods(rooted_tag, :class, visibility, deep, skip)
401
- result.concat inner_get_methods(rooted_tag, :instance, visibility, deep, skip)
402
- result.concat inner_get_methods('Kernel', :instance, visibility, deep, skip)
403
- else
404
- result.concat inner_get_methods(rooted_tag, scope, visibility, deep, skip)
405
- unless %w[Class Class<Class>].include?(rooted_tag)
406
- result.map! do |pin|
407
- next pin unless pin.path == 'Class#new'
408
- init_pin = get_method_stack(rooted_tag, 'initialize').first
409
- next pin unless init_pin
410
-
411
- type = ComplexType::SELF
412
- new_pin = Pin::Method.new(
413
- name: 'new',
414
- scope: :class,
415
- location: init_pin.location,
416
- return_type: type,
417
- comments: init_pin.comments,
418
- closure: init_pin.closure,
419
- source: init_pin.source,
420
- type_location: init_pin.type_location,
421
- )
422
- new_pin.parameters = init_pin.parameters.map do |init_param|
423
- param = init_param.clone
424
- param.closure = new_pin
425
- param.reset_generated!
426
- param
427
- end.freeze
428
- new_pin.signatures = init_pin.signatures.map do |init_sig|
429
- sig = init_sig.proxy(type)
430
- sig.parameters = init_sig.parameters.map do |param|
431
- param = param.clone
432
- param.closure = new_pin
433
- param.reset_generated!
434
- param
435
- end.freeze
436
- sig.closure = new_pin
437
- sig.reset_generated!
438
- sig
439
- end.freeze
440
- new_pin
441
- end
442
- end
443
- result.concat inner_get_methods('Kernel', :instance, [:public], deep, skip) if visibility.include?(:private)
444
- result.concat inner_get_methods('Module', scope, visibility, deep, skip) if scope == :module
445
- end
446
- result = resolve_method_aliases(result, visibility)
447
- if namespace_pin && rooted_tag != rooted_type.name
448
- result = result.map { |method_pin| method_pin.resolve_generics(namespace_pin, rooted_type) }
449
- end
450
- cache.set_methods(rooted_tag, scope, visibility, deep, result)
451
- result
452
- end
453
-
454
- # Get an array of method pins for a complex type.
455
- #
456
- # The type's namespace and the context should be fully qualified. If the
457
- # context matches the namespace type or is a subclass of the type,
458
- # protected methods are included in the results. If protected methods are
459
- # included and internal is true, private methods are also included.
460
- #
461
- # @example
462
- # api_map = Solargraph::ApiMap.new
463
- # type = Solargraph::ComplexType.parse('String')
464
- # api_map.get_complex_type_methods(type)
465
- #
466
- # @param complex_type [Solargraph::ComplexType] The complex type of the namespace
467
- # @param context [String] The context from which the type is referenced
468
- # @param internal [Boolean] True to include private methods
469
- # @return [Array<Solargraph::Pin::Base>]
470
- def get_complex_type_methods complex_type, context = '', internal = false
471
- # This method does not qualify the complex type's namespace because
472
- # it can cause conflicts between similar names, e.g., `Foo` vs.
473
- # `Other::Foo`. It still takes a context argument to determine whether
474
- # protected and private methods are visible.
475
- return [] if complex_type.undefined? || complex_type.void?
476
- result = Set.new
477
- complex_type.each do |type|
478
- if type.duck_type?
479
- result.add Pin::DuckMethod.new(name: type.to_s[1..-1], source: :api_map)
480
- result.merge get_methods('Object')
481
- else
482
- unless type.nil? || type.name == 'void'
483
- visibility = [:public]
484
- if type.namespace == context || super_and_sub?(type.namespace, context)
485
- visibility.push :protected
486
- visibility.push :private if internal
487
- end
488
- result.merge get_methods(type.tag, scope: type.scope, visibility: visibility)
489
- end
490
- end
491
- end
492
- result.to_a
493
- end
494
-
495
- # Get a stack of method pins for a method name in a potentially
496
- # parameterized namespace. The order of the pins corresponds to
497
- # the ancestry chain, with highest precedence first.
498
- #
499
- # @example
500
- # api_map.get_method_stack('Subclass', 'method_name')
501
- # #=> [ <Subclass#method_name pin>, <Superclass#method_name pin> ]
502
- #
503
- # @param rooted_tag [String] Parameterized namespace, fully qualified
504
- # @param name [String] Method name to look up
505
- # @param scope [Symbol] :instance or :class
506
- # @param visibility [Array<Symbol>] :public, :protected, and/or :private
507
- # @param preserve_generics [Boolean] True to preserve any
508
- # unresolved generic parameters, false to erase them
509
- # @return [Array<Solargraph::Pin::Method>]
510
- def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false
511
- rooted_type = ComplexType.parse(rooted_tag)
512
- fqns = rooted_type.namespace
513
- namespace_pin = store.get_path_pins(fqns).first
514
- methods = if namespace_pin.is_a?(Pin::Constant)
515
- type = namespace_pin.infer(self)
516
- if type.defined?
517
- namespace_pin = store.get_path_pins(type.namespace).first
518
- get_methods(type.namespace, scope: scope, visibility: visibility).select { |p| p.name == name }
519
- else
520
- []
521
- end
522
- else
523
- get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name }
524
- end
525
- methods = erase_generics(namespace_pin, rooted_type, methods) unless preserve_generics
526
- methods
527
- end
528
-
529
- # Get an array of all suggestions that match the specified path.
530
- #
531
- # @deprecated Use #get_path_pins instead.
532
- #
533
- # @param path [String] The path to find
534
- # @return [Array<Solargraph::Pin::Base>]
535
- def get_path_suggestions path
536
- return [] if path.nil?
537
- resolve_method_aliases store.get_path_pins(path)
538
- end
539
-
540
- # Get an array of pins that match the specified path.
541
- #
542
- # @param path [String]
543
- # @return [Array<Pin::Base>]
544
- def get_path_pins path
545
- get_path_suggestions(path)
546
- end
547
-
548
- # Get a list of documented paths that match the query.
549
- #
550
- # @example
551
- # api_map.query('str') # Results will include `String` and `Struct`
552
- #
553
- # @param query [String] The text to match
554
- # @return [Array<String>]
555
- def search query
556
- pins.map(&:path)
557
- .compact
558
- .select { |path| path.downcase.include?(query.downcase) }
559
- end
560
-
561
- # @deprecated This method is likely superfluous. Calling #get_path_pins
562
- # directly should be sufficient.
563
- #
564
- # @param path [String] The path to find
565
- # @return [Enumerable<Pin::Base>]
566
- def document path
567
- get_path_pins(path)
568
- end
569
-
570
- # Get an array of all symbols in the workspace that match the query.
571
- #
572
- # @param query [String]
573
- # @return [Array<Pin::Base>]
574
- def query_symbols query
575
- Pin::Search.new(
576
- source_map_hash.values.flat_map(&:document_symbols),
577
- query
578
- ).results
579
- end
580
-
581
- # @param location [Solargraph::Location]
582
- # @return [Array<Solargraph::Pin::Base>]
583
- def locate_pins location
584
- return [] if location.nil? || !source_map_hash.key?(location.filename)
585
- resolve_method_aliases source_map_hash[location.filename].locate_pins(location)
586
- end
587
-
588
- # @raise [FileNotFoundError] if the cursor's file is not in the ApiMap
589
- # @param cursor [Source::Cursor]
590
- # @return [SourceMap::Clip]
591
- def clip cursor
592
- raise FileNotFoundError, "ApiMap did not catalog #{cursor.filename}" unless source_map_hash.key?(cursor.filename)
593
-
594
- SourceMap::Clip.new(self, cursor)
595
- end
596
-
597
- # Get an array of document symbols from a file.
598
- #
599
- # @param filename [String]
600
- # @return [Array<Pin::Symbol>]
601
- def document_symbols filename
602
- return [] unless source_map_hash.key?(filename) # @todo Raise error?
603
- resolve_method_aliases source_map_hash[filename].document_symbols
604
- end
605
-
606
- # @return [Array<SourceMap>]
607
- def source_maps
608
- source_map_hash.values
609
- end
610
-
611
- # Get a source map by filename.
612
- #
613
- # @param filename [String]
614
- # @return [SourceMap]
615
- def source_map filename
616
- raise FileNotFoundError, "Source map for `#{filename}` not found" unless source_map_hash.key?(filename)
617
- source_map_hash[filename]
618
- end
619
-
620
- # True if the specified file was included in a bundle, i.e., it's either
621
- # included in a workspace or open in a library.
622
- #
623
- # @param filename [String]
624
- def bundled? filename
625
- source_map_hash.keys.include?(filename)
626
- end
627
-
628
- # Check if a class is a superclass of another class.
629
- #
630
- # @param sup [String] The superclass
631
- # @param sub [String] The subclass
632
- # @return [Boolean]
633
- def super_and_sub?(sup, sub)
634
- sup = ComplexType.try_parse(sup)
635
- sub = ComplexType.try_parse(sub)
636
- # @todo If two literals are different values of the same type, it would
637
- # make more sense for super_and_sub? to return true, but there are a
638
- # few callers that currently expect this to be false.
639
- return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s
640
- sup = sup.simplify_literals.to_s
641
- sub = sub.simplify_literals.to_s
642
- return true if sup == sub
643
- sc_fqns = sub
644
- while (sc = store.get_superclass(sc_fqns))
645
- sc_new = store.constants.dereference(sc)
646
- # Cyclical inheritance is invalid
647
- return false if sc_new == sc_fqns
648
- sc_fqns = sc_new
649
- return true if sc_fqns == sup
650
- end
651
- false
652
- end
653
-
654
- # Check if the host class includes the specified module, ignoring
655
- # type parameters used.
656
- #
657
- # @param host_ns [String] The class namesapce (no type parameters)
658
- # @param module_ns [String] The module namespace (no type parameters)
659
- #
660
- # @return [Boolean]
661
- def type_include?(host_ns, module_ns)
662
- store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns)
663
- end
664
-
665
- # @param pins [Enumerable<Pin::Base>]
666
- # @param visibility [Enumerable<Symbol>]
667
- # @return [Array<Pin::Base>]
668
- def resolve_method_aliases pins, visibility = [:public, :private, :protected]
669
- with_resolved_aliases = pins.map do |pin|
670
- next pin unless pin.is_a?(Pin::MethodAlias)
671
- resolved = resolve_method_alias(pin)
672
- next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility)
673
- resolved
674
- end.compact
675
- logger.debug { "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" }
676
- GemPins.combine_method_pins_by_path(with_resolved_aliases)
677
- end
678
-
679
- # @param fq_reference_tag [String] A fully qualified whose method should be pulled in
680
- # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type
681
- # parameter - used to pull generics information
682
- # @param type [ComplexType] The type which is having its
683
- # methods supplemented from fq_reference_tag
684
- # @param scope [Symbol] :class or :instance
685
- # @param visibility [Array<Symbol>] :public, :protected, and/or :private
686
- # @param deep [Boolean]
687
- # @param skip [Set<String>]
688
- # @param no_core [Boolean] Skip core classes if true
689
- # @return [Array<Pin::Base>]
690
- def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core)
691
- logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" }
692
-
693
- # Ensure the types returned by the methods in the referenced
694
- # type are relative to the generic values passed in the
695
- # reference. e.g., Foo<String> might include Enumerable<String>
696
- #
697
- # @todo perform the same translation in the other areas
698
- # here after adding a spec and handling things correctly
699
- # in ApiMap::Store and RbsMap::Conversions for each
700
- resolved_reference_type = ComplexType.parse(fq_reference_tag).force_rooted.resolve_generics(namespace_pin, type)
701
- # @todo Can inner_get_methods be cached? Lots of lookups of base types going on.
702
- methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core)
703
- if namespace_pin && !resolved_reference_type.all_params.empty?
704
- reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first
705
- # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" }
706
- methods = methods.map do |method_pin|
707
- method_pin.resolve_generics(reference_pin, resolved_reference_type)
708
- end
709
- end
710
- # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolved_reference_type: #{resolved_reference_type} for type=#{type}: #{methods.map(&:name)}" }
711
- methods
712
- end
713
-
714
- private
715
-
716
- # A hash of source maps with filename keys.
717
- #
718
- # @return [Hash{String => SourceMap}]
719
- attr_reader :source_map_hash
720
-
721
- # @return [ApiMap::Store]
722
- def store
723
- @store ||= Store.new
724
- end
725
-
726
- # @return [Solargraph::ApiMap::Cache]
727
- attr_reader :cache
728
-
729
- # @param rooted_tag [String] A fully qualified namespace, with
730
- # generic parameter values if applicable
731
- # @param scope [Symbol] :class or :instance
732
- # @param visibility [Array<Symbol>] :public, :protected, and/or :private
733
- # @param deep [Boolean]
734
- # @param skip [Set<String>]
735
- # @param no_core [Boolean] Skip core classes if true
736
- # @return [Array<Pin::Base>]
737
- def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
738
- rooted_type = ComplexType.parse(rooted_tag).force_rooted
739
- fqns = rooted_type.namespace
740
- fqns_generic_params = rooted_type.all_params
741
- namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
742
- return [] if no_core && fqns =~ /^(Object|BasicObject|Class|Module)$/
743
- reqstr = "#{fqns}|#{scope}|#{visibility.sort}|#{deep}"
744
- return [] if skip.include?(reqstr)
745
- skip.add reqstr
746
- result = []
747
- environ = Convention.for_object(self, rooted_tag, scope, visibility, deep, skip, no_core)
748
- # ensure we start out with any immediate methods in this
749
- # namespace so we roughly match the same ordering of get_methods
750
- # and obey the 'deep' instruction
751
- direct_convention_methods, convention_methods_by_reference = environ.pins.partition { |p| p.namespace == rooted_tag }
752
- result.concat direct_convention_methods
753
-
754
- if deep && scope == :instance
755
- store.get_prepends(fqns).reverse.each do |im|
756
- fqim = store.constants.dereference(im)
757
- result.concat inner_get_methods(fqim, scope, visibility, deep, skip, true) unless fqim.nil?
758
- end
759
- end
760
- # Store#get_methods doesn't know about full tags, just
761
- # namespaces; resolving the generics in the method pins is this
762
- # class' responsibility
763
- methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort{ |a, b| a.name <=> b.name }
764
- logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" }
765
- result.concat methods
766
- if deep
767
- result.concat convention_methods_by_reference
768
-
769
- if scope == :instance
770
- store.get_includes(fqns).reverse.each do |ref|
771
- in_tag = dereference(ref)
772
- result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true)
773
- end
774
- rooted_sc_tag = qualify_superclass(rooted_tag)
775
- unless rooted_sc_tag.nil?
776
- result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core)
777
- end
778
- else
779
- logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" }
780
- store.get_extends(fqns).reverse.each do |em|
781
- fqem = dereference(em)
782
- result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil?
783
- end
784
- rooted_sc_tag = qualify_superclass(rooted_tag)
785
- unless rooted_sc_tag.nil?
786
- result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, true)
787
- end
788
- unless no_core || fqns.empty?
789
- type = get_namespace_type(fqns)
790
- result.concat inner_get_methods('Class', :instance, visibility, deep, skip, no_core) if type == :class
791
- result.concat inner_get_methods('Module', :instance, visibility, deep, skip, no_core)
792
- end
793
- end
794
- store.domains(fqns).each do |d|
795
- dt = ComplexType.try_parse(d)
796
- result.concat inner_get_methods(dt.namespace, dt.scope, visibility, deep, skip)
797
- end
798
- end
799
- result
800
- end
801
-
802
- # @return [Hash]
803
- def path_macros
804
- @path_macros ||= {}
805
- end
806
-
807
- # @param fq_sub_tag [String]
808
- # @return [String, nil]
809
- def qualify_superclass fq_sub_tag
810
- store.qualify_superclass fq_sub_tag
811
- end
812
-
813
- # Get the namespace's type (Class or Module).
814
- #
815
- # @param fqns [String] A fully qualified namespace
816
- # @return [Symbol, nil] :class, :module, or nil
817
- def get_namespace_type fqns
818
- return nil if fqns.nil?
819
- # @type [Pin::Namespace, nil]
820
- pin = store.get_path_pins(fqns).select{|p| p.is_a?(Pin::Namespace)}.first
821
- return nil if pin.nil?
822
- pin.type
823
- end
824
-
825
- # Sort an array of pins to put nil or undefined variables last.
826
- #
827
- # @param pins [Enumerable<Pin::BaseVariable>]
828
- # @return [Enumerable<Pin::BaseVariable>]
829
- def prefer_non_nil_variables pins
830
- result = []
831
- nil_pins = []
832
- pins.each do |pin|
833
- if pin.variable? && pin.nil_assignment?
834
- nil_pins.push pin
835
- else
836
- result.push pin
837
- end
838
- end
839
- result + nil_pins
840
- end
841
-
842
- include Logging
843
-
844
- private
845
-
846
- # @param alias_pin [Pin::MethodAlias]
847
- # @return [Pin::Method, nil]
848
- def resolve_method_alias(alias_pin)
849
- ancestors = store.get_ancestors(alias_pin.full_context.reduce_class_type.tag)
850
- original = nil
851
-
852
- # Search each ancestor for the original method
853
- ancestors.each do |ancestor_fqns|
854
- next if ancestor_fqns.nil?
855
- ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}"
856
-
857
- # Search for the original method in the ancestor
858
- original = store.get_path_pins(ancestor_method_path).find do |candidate_pin|
859
- next if candidate_pin == alias_pin
860
-
861
- if candidate_pin.is_a?(Pin::MethodAlias)
862
- # recursively resolve method aliases
863
- resolved = resolve_method_alias(candidate_pin)
864
- break resolved if resolved
865
- end
866
-
867
- candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope
868
- end
869
-
870
- break if original
871
- end
872
-
873
- # @sg-ignore ignore `received nil` for original
874
- create_resolved_alias_pin(alias_pin, original) if original
875
- end
876
-
877
- # Fast path for creating resolved alias pins without individual method stack lookups
878
- # @param alias_pin [Pin::MethodAlias] The alias pin to resolve
879
- # @param original [Pin::Method] The original method pin that was already found
880
- # @return [Pin::Method] The resolved method pin
881
- def create_resolved_alias_pin(alias_pin, original)
882
- # Build the resolved method pin directly (same logic as resolve_method_alias but without lookup)
883
- args = {
884
- location: alias_pin.location,
885
- type_location: original.type_location,
886
- closure: alias_pin.closure,
887
- name: alias_pin.name,
888
- comments: original.comments,
889
- scope: original.scope,
890
- visibility: original.visibility,
891
- signatures: original.signatures.map(&:clone).freeze,
892
- attribute: original.attribute?,
893
- generics: original.generics.clone,
894
- return_type: original.return_type,
895
- source: :resolve_method_alias
896
- }
897
- resolved_pin = Pin::Method.new **args
898
-
899
- # Clone signatures and parameters
900
- resolved_pin.signatures.each do |sig|
901
- sig.parameters = sig.parameters.map(&:clone).freeze
902
- sig.source = :resolve_method_alias
903
- sig.parameters.each do |param|
904
- param.closure = resolved_pin
905
- param.source = :resolve_method_alias
906
- param.reset_generated!
907
- end
908
- sig.closure = resolved_pin
909
- sig.reset_generated!
910
- end
911
-
912
- resolved_pin
913
- end
914
-
915
- # @param namespace_pin [Pin::Namespace]
916
- # @param rooted_type [ComplexType]
917
- # @param pins [Enumerable<Pin::Base>]
918
- # @return [Array<Pin::Base>]
919
- def erase_generics(namespace_pin, rooted_type, pins)
920
- return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type)
921
-
922
- logger.debug("Erasing generics on namespace_pin=#{namespace_pin} / rooted_type=#{rooted_type}")
923
- pins.map do |method_pin|
924
- method_pin.erase_generics(namespace_pin.generics)
925
- end
926
- end
927
-
928
- # @param namespace_pin [Pin::Namespace]
929
- # @param rooted_type [ComplexType]
930
- def should_erase_generics_when_done?(namespace_pin, rooted_type)
931
- has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type)
932
- end
933
-
934
- # @param namespace_pin [Pin::Namespace, Pin::Constant]
935
- def has_generics?(namespace_pin)
936
- namespace_pin.is_a?(Pin::Namespace) && !namespace_pin.generics.empty?
937
- end
938
-
939
- # @param namespace_pin [Pin::Namespace]
940
- # @param rooted_type [ComplexType]
941
- def can_resolve_generics?(namespace_pin, rooted_type)
942
- has_generics?(namespace_pin) && !rooted_type.all_params.empty?
943
- end
944
- end
945
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'yard'
5
+ require 'solargraph/yard_tags'
6
+
7
+ module Solargraph
8
+ # An aggregate provider for information about Workspaces, Sources, gems, and
9
+ # the Ruby core.
10
+ #
11
+ class ApiMap
12
+ autoload :Cache, 'solargraph/api_map/cache'
13
+ autoload :SourceToYard, 'solargraph/api_map/source_to_yard'
14
+ autoload :Store, 'solargraph/api_map/store'
15
+ autoload :Index, 'solargraph/api_map/index'
16
+ autoload :Constants, 'solargraph/api_map/constants'
17
+
18
+ # @return [Array<String>]
19
+ attr_reader :unresolved_requires
20
+
21
+ @@core_map = RbsMap::CoreMap.new
22
+
23
+ # @return [Array<String>]
24
+ attr_reader :missing_docs
25
+
26
+ # @param pins [Array<Solargraph::Pin::Base>]
27
+ # @param loose_unions [Boolean] if true, a potential type can be
28
+ # inferred if ANY of the UniqueTypes in the base chain's
29
+ # ComplexType match it. If false, every single UniqueTypes in
30
+ # the base must be ALL able to independently provide this
31
+ # type. The former is useful during completion, but the
32
+ # latter is best for typechecking at higher levels.
33
+ #
34
+ def initialize pins: [], loose_unions: true
35
+ @source_map_hash = {}
36
+ @cache = Cache.new
37
+ @loose_unions = loose_unions
38
+ index pins
39
+ end
40
+
41
+ # @param out [StringIO, IO, nil] output stream for logging
42
+ # @return [void]
43
+ def self.reset_core out: nil
44
+ @@core_map = RbsMap::CoreMap.new
45
+ end
46
+
47
+ #
48
+ # This is a mutable object, which is cached in the Chain class -
49
+ # if you add any fields which change the results of calls (not
50
+ # just caches), please also change `equality_fields` below.
51
+ #
52
+
53
+ # @param other [Object]
54
+ def eql?(other)
55
+ self.class == other.class &&
56
+ # @sg-ignore flow sensitive typing needs to handle self.class == other.class
57
+ equality_fields == other.equality_fields
58
+ end
59
+
60
+ # @param other [Object]
61
+ def ==(other)
62
+ self.eql?(other)
63
+ end
64
+
65
+ # @return [Integer]
66
+ def hash
67
+ equality_fields.hash
68
+ end
69
+
70
+ attr_reader :loose_unions
71
+
72
+ def to_s
73
+ self.class.to_s
74
+ end
75
+
76
+ # avoid enormous dump
77
+ def inspect
78
+ to_s
79
+ end
80
+
81
+ # @param pins [Array<Pin::Base>]
82
+ # @return [self]
83
+ def index pins
84
+ # @todo This implementation is incomplete. It should probably create a
85
+ # Bench.
86
+ @source_map_hash = {}
87
+ conventions_environ.clear
88
+ cache.clear
89
+ store.update @@core_map.pins, pins
90
+ self
91
+ end
92
+
93
+ # Map a single source.
94
+ #
95
+ # @param source [Source]
96
+ # @param live [Boolean] True for live source map (active editor file)
97
+ # @return [self]
98
+ def map source, live: false
99
+ map = Solargraph::SourceMap.map(source)
100
+ catalog Bench.new(source_maps: [map], live_map: live ? map : nil)
101
+ self
102
+ end
103
+
104
+ # Catalog a bench.
105
+ #
106
+ # @param bench [Bench]
107
+ # @return [self]
108
+ def catalog bench
109
+ @source_map_hash = bench.source_map_hash
110
+ iced_pins = bench.icebox.flat_map(&:pins)
111
+ live_pins = bench.live_map&.all_pins || []
112
+ conventions_environ.clear
113
+ source_map_hash.each_value do |map|
114
+ conventions_environ.merge map.conventions_environ
115
+ end
116
+ unresolved_requires = (bench.external_requires + conventions_environ.requires + bench.workspace.config.required).to_a.compact.uniq
117
+ recreate_docmap = @unresolved_requires != unresolved_requires ||
118
+ workspace.rbs_collection_path != bench.workspace.rbs_collection_path ||
119
+ @doc_map.any_uncached?
120
+
121
+ if recreate_docmap
122
+ @doc_map = DocMap.new(unresolved_requires, bench.workspace, out: nil) # @todo Implement gem preferences
123
+ @unresolved_requires = @doc_map.unresolved_requires
124
+ end
125
+ @cache.clear if store.update(@@core_map.pins, @doc_map.pins, conventions_environ.pins, iced_pins, live_pins)
126
+ @missing_docs = [] # @todo Implement missing docs
127
+ self
128
+ end
129
+
130
+ # @todo need to model type def statement in chains as a symbol so
131
+ # that this overload of 'protected' will typecheck @sg-ignore
132
+ # @sg-ignore
133
+ protected def equality_fields
134
+ [self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires, @missing_docs, @loose_unions]
135
+ end
136
+
137
+ # @return [DocMap]
138
+ def doc_map
139
+ @doc_map ||= DocMap.new([], Workspace.new('.'))
140
+ end
141
+
142
+ # @return [::Array<Gem::Specification>]
143
+ def uncached_gemspecs
144
+ doc_map.uncached_gemspecs || []
145
+ end
146
+
147
+ # @return [Enumerable<Pin::Base>]
148
+ def core_pins
149
+ @@core_map.pins
150
+ end
151
+
152
+ # @param name [String, nil]
153
+ # @return [YARD::Tags::MacroDirective, nil]
154
+ def named_macro name
155
+ # @sg-ignore Need to add nil check here
156
+ store.named_macros[name]
157
+ end
158
+
159
+ # @return [Set<String>]
160
+ def required
161
+ @required ||= Set.new
162
+ end
163
+
164
+ # @return [Environ]
165
+ def conventions_environ
166
+ @conventions_environ ||= Environ.new
167
+ end
168
+
169
+ # @param filename [String]
170
+ # @param position [Position, Array(Integer, Integer)]
171
+ # @return [Source::Cursor]
172
+ def cursor_at filename, position
173
+ position = Position.normalize(position)
174
+ raise FileNotFoundError, "File not found: #{filename}" unless source_map_hash.key?(filename)
175
+ source_map_hash[filename].cursor_at(position)
176
+ end
177
+
178
+ # Get a clip by filename and position.
179
+ #
180
+ # @param filename [String]
181
+ # @param position [Position, Array(Integer, Integer)]
182
+ # @return [SourceMap::Clip]
183
+ def clip_at filename, position
184
+ position = Position.normalize(position)
185
+ clip(cursor_at(filename, position))
186
+ end
187
+
188
+ # Create an ApiMap with a workspace in the specified directory.
189
+ #
190
+ # @param directory [String]
191
+ # @param loose_unions [Boolean] See #initialize
192
+ #
193
+ # @return [ApiMap]
194
+ def self.load directory, loose_unions: true
195
+ api_map = new(loose_unions: loose_unions)
196
+ workspace = Solargraph::Workspace.new(directory)
197
+ # api_map.catalog Bench.new(workspace: workspace)
198
+ library = Library.new(workspace)
199
+ library.map!
200
+ api_map.catalog library.bench
201
+ api_map
202
+ end
203
+
204
+ # @param out [StringIO, IO, nil]
205
+ # @param rebuild [Boolean] whether to rebuild the pins even if they are cached
206
+ # @return [void]
207
+ def cache_all_for_doc_map! out: $stderr, rebuild: false
208
+ doc_map.cache_doc_map_gems!(out, rebuild: rebuild)
209
+ end
210
+
211
+ # @param gemspec [Gem::Specification]
212
+ # @param rebuild [Boolean]
213
+ # @param out [StringIO, IO, nil]
214
+ # @return [void]
215
+ def cache_gem(gemspec, rebuild: false, out: nil)
216
+ doc_map.cache(gemspec, rebuild: rebuild, out: out)
217
+ end
218
+
219
+ class << self
220
+ include Logging
221
+ end
222
+
223
+ # Create an ApiMap with a workspace in the specified directory and cache
224
+ # any missing gems.
225
+ #
226
+ #
227
+ # @param directory [String]
228
+ # @param out [IO, StringIO, nil] The output stream for messages
229
+ # @param loose_unions [Boolean] See #initialize
230
+ #
231
+ # @return [ApiMap]
232
+ def self.load_with_cache directory, out = $stderr, loose_unions: true
233
+ api_map = load(directory, loose_unions: loose_unions)
234
+ if api_map.uncached_gemspecs.empty?
235
+ logger.info { "All gems cached for #{directory}" }
236
+ return api_map
237
+ end
238
+
239
+ api_map.cache_all_for_doc_map!(out: out)
240
+ load(directory, loose_unions: loose_unions)
241
+ end
242
+
243
+ # @return [Array<Solargraph::Pin::Base>]
244
+ def pins
245
+ store.pins.clone.freeze
246
+ end
247
+
248
+ # An array of pins based on Ruby keywords (`if`, `end`, etc.).
249
+ #
250
+ # @return [Enumerable<Solargraph::Pin::Keyword>]
251
+ def keyword_pins
252
+ store.pins_by_class(Pin::Keyword)
253
+ end
254
+
255
+ # True if the namespace exists.
256
+ #
257
+ # @param name [String] The namespace to match
258
+ # @param context [String] The context to search
259
+ # @return [Boolean]
260
+ def namespace_exists? name, context = ''
261
+ !qualify(name, context).nil?
262
+ end
263
+
264
+ # Get suggestions for constants in the specified namespace. The result
265
+ # may contain both constant and namespace pins.
266
+ #
267
+ # @param namespace [String] The namespace
268
+ # @param contexts [Array<String>] The contexts
269
+ # @return [Array<Solargraph::Pin::Constant, Solargraph::Pin::Namespace>]
270
+ def get_constants namespace, *contexts
271
+ namespace ||= ''
272
+ gates = contexts.clone
273
+ gates.push '' if contexts.empty? && namespace.empty?
274
+ gates.push namespace unless namespace.empty?
275
+ store.constants
276
+ .collect(gates)
277
+ .select { |pin| namespace.empty? || contexts.empty? || pin.namespace == namespace }
278
+ .select { |pin| pin.visibility == :public || pin.namespace == namespace }
279
+ end
280
+
281
+ # @param namespace [String]
282
+ # @param context [String]
283
+ # @return [Array<Pin::Namespace>]
284
+ def get_namespace_pins namespace, context
285
+ store.fqns_pins(qualify(namespace, context))
286
+ end
287
+
288
+ # Determine fully qualified tag for a given tag used inside the
289
+ # definition of another tag ("context"). This method will start
290
+ # the search in the specified context until it finds a match for
291
+ # the tag.
292
+ #
293
+ # Does not recurse into qualifying the type parameters, but
294
+ # returns any which were passed in unchanged.
295
+ #
296
+ # @param tag [String, nil] The namespace to
297
+ # match, complete with generic parameters set to appropriate
298
+ # values if available
299
+ # @param gates [Array<String>] The fully qualified context in which
300
+ # the tag was referenced; start from here to resolve the name.
301
+ # Should not be prefixed with '::'.
302
+ # @return [String, nil] fully qualified tag
303
+ def qualify tag, *gates
304
+ store.constants.qualify(tag, *gates)
305
+ end
306
+
307
+ # @see Store::Constants#resolve
308
+ #
309
+ # @param name [String]
310
+ # @param gates [Array<String, Array<String>>]
311
+ # @return [String, nil]
312
+ def resolve name, *gates
313
+ store.constants.resolve(name, *gates)
314
+ end
315
+
316
+ # Get a fully qualified namespace from a reference pin.
317
+ #
318
+ # @param pin [Pin::Reference]
319
+ # @return [String, nil]
320
+ def dereference(pin)
321
+ store.constants.dereference(pin)
322
+ end
323
+
324
+ # @param fqns [String]
325
+ # @return [Array<Pin::Reference::Extend>]
326
+ def get_extends(fqns)
327
+ store.get_extends(fqns)
328
+ end
329
+
330
+ # @param fqns [String]
331
+ # @return [Array<Pin::Reference::Include>]
332
+ def get_includes(fqns)
333
+ store.get_includes(fqns)
334
+ end
335
+
336
+ # Get an array of instance variable pins defined in specified namespace
337
+ # and scope.
338
+ #
339
+ # @param namespace [String] A fully qualified namespace
340
+ # @param scope [Symbol] :instance or :class
341
+ # @return [Array<Solargraph::Pin::InstanceVariable>]
342
+ def get_instance_variable_pins namespace, scope = :instance
343
+ result = []
344
+ used = [namespace]
345
+ result.concat store.get_instance_variables(namespace, scope)
346
+ sc_fqns = namespace
347
+ while (sc = store.get_superclass(sc_fqns))
348
+ # @sg-ignore flow sensitive typing needs to handle "if foo = bar"
349
+ sc_fqns = store.constants.dereference(sc)
350
+ result.concat store.get_instance_variables(sc_fqns, scope)
351
+ end
352
+ result
353
+ end
354
+
355
+ # Find a variable pin by name and where it is used.
356
+ #
357
+ # Resolves our most specific view of this variable's type by
358
+ # preferring pins created by flow-sensitive typing when we have
359
+ # them based on the Closure and Location.
360
+ #
361
+ # @param candidates [Array<Pin::BaseVariable>]
362
+ # @param name [String]
363
+ # @param closure [Pin::Closure]
364
+ # @param location [Location]
365
+ #
366
+ # @return [Pin::BaseVariable, nil]
367
+ def var_at_location(candidates, name, closure, location)
368
+ with_correct_name = candidates.select { |pin| pin.name == name}
369
+ vars_at_location = with_correct_name.reject do |pin|
370
+ # visible_at? excludes the starting position, but we want to
371
+ # include it for this purpose
372
+ (!pin.visible_at?(closure, location) && !pin.starts_at?(location))
373
+ end
374
+
375
+ vars_at_location.inject(&:combine_with)
376
+ end
377
+
378
+ # Get an array of class variable pins for a namespace.
379
+ #
380
+ # @param namespace [String] A fully qualified namespace
381
+ # @return [Enumerable<Solargraph::Pin::ClassVariable>]
382
+ def get_class_variable_pins namespace
383
+ prefer_non_nil_variables(store.get_class_variables(namespace))
384
+ end
385
+
386
+ # @return [Enumerable<Solargraph::Pin::Base>]
387
+ def get_symbols
388
+ store.get_symbols
389
+ end
390
+
391
+ # @return [Enumerable<Solargraph::Pin::GlobalVariable>]
392
+ def get_global_variable_pins
393
+ store.pins_by_class(Pin::GlobalVariable)
394
+ end
395
+
396
+ # @return [Enumerable<Solargraph::Pin::Block>]
397
+ def get_block_pins
398
+ store.pins_by_class(Pin::Block)
399
+ end
400
+
401
+ # Get an array of methods available in a particular context.
402
+ #
403
+ # @param rooted_tag [String] The fully qualified namespace to search for methods
404
+ # @param scope [Symbol] :class or :instance
405
+ # @param visibility [Array<Symbol>] :public, :protected, and/or :private
406
+ # @param deep [Boolean] True to include superclasses, mixins, etc.
407
+ # @return [Array<Solargraph::Pin::Method>]
408
+ def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true
409
+ if rooted_tag.start_with? 'Array('
410
+ # Array() are really tuples - use our fill, as the RBS repo
411
+ # does not give us definitions for it
412
+ rooted_tag = "Solargraph::Fills::Tuple(#{rooted_tag[6..-2]})"
413
+ end
414
+ rooted_type = ComplexType.try_parse(rooted_tag)
415
+ fqns = rooted_type.namespace
416
+ namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
417
+ cached = cache.get_methods(rooted_tag, scope, visibility, deep)
418
+ return cached.clone unless cached.nil?
419
+ # @type [Array<Solargraph::Pin::Method>]
420
+ result = []
421
+ skip = Set.new
422
+ if rooted_tag == ''
423
+ # @todo Implement domains
424
+ conventions_environ.domains.each do |domain|
425
+ type = ComplexType.try_parse(domain)
426
+ next if type.undefined?
427
+ result.concat inner_get_methods(type.name, type.scope, visibility, deep, skip)
428
+ end
429
+ result.concat inner_get_methods(rooted_tag, :class, visibility, deep, skip)
430
+ result.concat inner_get_methods(rooted_tag, :instance, visibility, deep, skip)
431
+ result.concat inner_get_methods('Kernel', :instance, visibility, deep, skip)
432
+ else
433
+ result.concat inner_get_methods(rooted_tag, scope, visibility, deep, skip)
434
+ unless %w[Class Class<Class>].include?(rooted_tag)
435
+ result.map! do |pin|
436
+ next pin unless pin.path == 'Class#new'
437
+ init_pin = get_method_stack(rooted_tag, 'initialize').first
438
+ next pin unless init_pin
439
+
440
+ type = ComplexType::SELF
441
+ new_pin = Pin::Method.new(
442
+ name: 'new',
443
+ scope: :class,
444
+ location: init_pin.location,
445
+ return_type: type,
446
+ comments: init_pin.comments,
447
+ closure: init_pin.closure,
448
+ source: init_pin.source,
449
+ type_location: init_pin.type_location,
450
+ )
451
+ new_pin.parameters = init_pin.parameters.map do |init_param|
452
+ param = init_param.clone
453
+ param.closure = new_pin
454
+ param.reset_generated!
455
+ param
456
+ end.freeze
457
+ new_pin.signatures = init_pin.signatures.map do |init_sig|
458
+ sig = init_sig.proxy(type)
459
+ sig.parameters = init_sig.parameters.map do |param|
460
+ param = param.clone
461
+ param.closure = new_pin
462
+ param.reset_generated!
463
+ param
464
+ end.freeze
465
+ sig.closure = new_pin
466
+ sig.reset_generated!
467
+ sig
468
+ end.freeze
469
+ new_pin
470
+ end
471
+ end
472
+ result.concat inner_get_methods('Kernel', :instance, [:public], deep, skip) if visibility.include?(:private)
473
+ result.concat inner_get_methods('Module', scope, visibility, deep, skip) if scope == :module
474
+ end
475
+ result = resolve_method_aliases(result, visibility)
476
+ if namespace_pin && rooted_tag != rooted_type.name
477
+ result = result.map { |method_pin| method_pin.resolve_generics(namespace_pin, rooted_type) }
478
+ end
479
+ cache.set_methods(rooted_tag, scope, visibility, deep, result)
480
+ result
481
+ end
482
+
483
+ # Get an array of method pins for a complex type.
484
+ #
485
+ # The type's namespace and the context should be fully qualified. If the
486
+ # context matches the namespace type or is a subclass of the type,
487
+ # protected methods are included in the results. If protected methods are
488
+ # included and internal is true, private methods are also included.
489
+ #
490
+ # @example
491
+ # api_map = Solargraph::ApiMap.new
492
+ # type = Solargraph::ComplexType.parse('String')
493
+ # api_map.get_complex_type_methods(type)
494
+ #
495
+ # @param complex_type [Solargraph::ComplexType] The complex type of the namespace
496
+ # @param context [String] The context from which the type is referenced
497
+ # @param internal [Boolean] True to include private methods
498
+ # @return [Array<Solargraph::Pin::Base>]
499
+ def get_complex_type_methods complex_type, context = '', internal = false
500
+ # This method does not qualify the complex type's namespace because
501
+ # it can cause conflicts between similar names, e.g., `Foo` vs.
502
+ # `Other::Foo`. It still takes a context argument to determine whether
503
+ # protected and private methods are visible.
504
+ return [] if complex_type.undefined? || complex_type.void?
505
+ result = Set.new
506
+ complex_type.each do |type|
507
+ if type.duck_type?
508
+ result.add Pin::DuckMethod.new(name: type.to_s[1..-1], source: :api_map)
509
+ result.merge get_methods('Object')
510
+ else
511
+ unless type.nil? || type.name == 'void'
512
+ visibility = [:public]
513
+ if type.namespace == context || super_and_sub?(type.namespace, context)
514
+ visibility.push :protected
515
+ visibility.push :private if internal
516
+ end
517
+ result.merge get_methods(type.tag, scope: type.scope, visibility: visibility)
518
+ end
519
+ end
520
+ end
521
+ result.to_a
522
+ end
523
+
524
+ # Get a stack of method pins for a method name in a potentially
525
+ # parameterized namespace. The order of the pins corresponds to
526
+ # the ancestry chain, with highest precedence first.
527
+ #
528
+ # @example
529
+ # api_map.get_method_stack('Subclass', 'method_name')
530
+ # #=> [ <Subclass#method_name pin>, <Superclass#method_name pin> ]
531
+ #
532
+ # @param rooted_tag [String] Parameterized namespace, fully qualified
533
+ # @param name [String] Method name to look up
534
+ # @param scope [Symbol] :instance or :class
535
+ # @param visibility [Array<Symbol>] :public, :protected, and/or :private
536
+ # @param preserve_generics [Boolean] True to preserve any
537
+ # unresolved generic parameters, false to erase them
538
+ # @return [Array<Solargraph::Pin::Method>]
539
+ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false
540
+ rooted_type = ComplexType.parse(rooted_tag)
541
+ fqns = rooted_type.namespace
542
+ namespace_pin = store.get_path_pins(fqns).first
543
+ methods = if namespace_pin.is_a?(Pin::Constant)
544
+ type = namespace_pin.typify(self)
545
+ type = namespace_pin.probe(self) unless type.defined?
546
+ if type.defined?
547
+ namespace_pin = store.get_path_pins(type.namespace).first
548
+ get_methods(type.namespace, scope: scope, visibility: visibility).select { |p| p.name == name }
549
+ else
550
+ []
551
+ end
552
+ else
553
+ get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name }
554
+ end
555
+ methods = erase_generics(namespace_pin, rooted_type, methods) unless preserve_generics
556
+ methods
557
+ end
558
+
559
+ # Get an array of all suggestions that match the specified path.
560
+ #
561
+ # @deprecated Use #get_path_pins instead.
562
+ #
563
+ # @param path [String] The path to find
564
+ # @return [Array<Solargraph::Pin::Base>]
565
+ def get_path_suggestions path
566
+ return [] if path.nil?
567
+ resolve_method_aliases store.get_path_pins(path)
568
+ end
569
+
570
+ # Get an array of pins that match the specified path.
571
+ #
572
+ # @param path [String]
573
+ # @return [Array<Pin::Base>]
574
+ def get_path_pins path
575
+ get_path_suggestions(path)
576
+ end
577
+
578
+ # Get a list of documented paths that match the query.
579
+ #
580
+ # @example
581
+ # api_map.query('str') # Results will include `String` and `Struct`
582
+ #
583
+ # @param query [String] The text to match
584
+ # @return [Array<String>]
585
+ def search query
586
+ pins.map(&:path)
587
+ .compact
588
+ .select { |path| path.downcase.include?(query.downcase) }
589
+ end
590
+
591
+ # @deprecated This method is likely superfluous. Calling #get_path_pins
592
+ # directly should be sufficient.
593
+ #
594
+ # @param path [String] The path to find
595
+ # @return [Enumerable<Pin::Base>]
596
+ def document path
597
+ get_path_pins(path)
598
+ end
599
+
600
+ # Get an array of all symbols in the workspace that match the query.
601
+ #
602
+ # @param query [String]
603
+ # @return [Array<Pin::Base>]
604
+ def query_symbols query
605
+ Pin::Search.new(
606
+ source_map_hash.values.flat_map(&:document_symbols),
607
+ query
608
+ ).results
609
+ end
610
+
611
+ # @param location [Solargraph::Location]
612
+ # @return [Array<Solargraph::Pin::Base>]
613
+ def locate_pins location
614
+ return [] if location.nil? || !source_map_hash.key?(location.filename)
615
+ resolve_method_aliases source_map_hash[location.filename].locate_pins(location)
616
+ end
617
+
618
+ # @raise [FileNotFoundError] if the cursor's file is not in the ApiMap
619
+ # @param cursor [Source::Cursor]
620
+ # @return [SourceMap::Clip]
621
+ def clip cursor
622
+ # @sg-ignore Need to add nil check here
623
+ raise FileNotFoundError, "ApiMap did not catalog #{cursor.filename}" unless source_map_hash.key?(cursor.filename)
624
+
625
+ SourceMap::Clip.new(self, cursor)
626
+ end
627
+
628
+ # Get an array of document symbols from a file.
629
+ #
630
+ # @param filename [String]
631
+ # @return [Array<Pin::Symbol>]
632
+ def document_symbols filename
633
+ return [] unless source_map_hash.key?(filename) # @todo Raise error?
634
+ resolve_method_aliases source_map_hash[filename].document_symbols
635
+ end
636
+
637
+ # @return [Array<SourceMap>]
638
+ def source_maps
639
+ source_map_hash.values
640
+ end
641
+
642
+ # Get a source map by filename.
643
+ #
644
+ # @param filename [String]
645
+ # @return [SourceMap]
646
+ def source_map filename
647
+ raise FileNotFoundError, "Source map for `#{filename}` not found" unless source_map_hash.key?(filename)
648
+ source_map_hash[filename]
649
+ end
650
+
651
+ # True if the specified file was included in a bundle, i.e., it's either
652
+ # included in a workspace or open in a library.
653
+ #
654
+ # @param filename [String]
655
+ def bundled? filename
656
+ source_map_hash.keys.include?(filename)
657
+ end
658
+
659
+ # Check if a class is a superclass of another class.
660
+ #
661
+ # @param sup [String] The superclass
662
+ # @param sub [String] The subclass
663
+ # @return [Boolean]
664
+ def super_and_sub?(sup, sub)
665
+ sup = ComplexType.try_parse(sup)
666
+ sub = ComplexType.try_parse(sub)
667
+ # @todo If two literals are different values of the same type, it would
668
+ # make more sense for super_and_sub? to return true, but there are a
669
+ # few callers that currently expect this to be false.
670
+ # @sg-ignore flow-sensitive typing should be able to handle redefinition
671
+ return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s
672
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
673
+ sup = sup.simplify_literals.to_s
674
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
675
+ sub = sub.simplify_literals.to_s
676
+ return true if sup == sub
677
+ sc_fqns = sub
678
+ while (sc = store.get_superclass(sc_fqns))
679
+ # @sg-ignore flow sensitive typing needs to handle "if foo = bar"
680
+ sc_new = store.constants.dereference(sc)
681
+ # Cyclical inheritance is invalid
682
+ return false if sc_new == sc_fqns
683
+ sc_fqns = sc_new
684
+ return true if sc_fqns == sup
685
+ end
686
+ false
687
+ end
688
+
689
+ # Check if the host class includes the specified module, ignoring
690
+ # type parameters used.
691
+ #
692
+ # @param host_ns [String] The class namesapce (no type parameters)
693
+ # @param module_ns [String] The module namespace (no type parameters)
694
+ #
695
+ # @return [Boolean]
696
+ def type_include?(host_ns, module_ns)
697
+ store.get_includes(host_ns).map { |inc_tag| inc_tag.type.name }.include?(module_ns)
698
+ end
699
+
700
+ # @param pins [Enumerable<Pin::Base>]
701
+ # @param visibility [Enumerable<Symbol>]
702
+ # @return [Array<Pin::Base>]
703
+ def resolve_method_aliases pins, visibility = [:public, :private, :protected]
704
+ with_resolved_aliases = pins.map do |pin|
705
+ next pin unless pin.is_a?(Pin::MethodAlias)
706
+ resolved = resolve_method_alias(pin)
707
+ # @sg-ignore Need to add nil check here
708
+ next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility)
709
+ resolved
710
+ end.compact
711
+ logger.debug do
712
+ "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}"
713
+ end
714
+ GemPins.combine_method_pins_by_path(with_resolved_aliases)
715
+ end
716
+
717
+ # @return [Workspace]
718
+ def workspace
719
+ doc_map.workspace
720
+ end
721
+
722
+ # @param fq_reference_tag [String] A fully qualified whose method should be pulled in
723
+ # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type
724
+ # parameter - used to pull generics information
725
+ # @param type [ComplexType] The type which is having its
726
+ # methods supplemented from fq_reference_tag
727
+ # @param scope [Symbol] :class or :instance
728
+ # @param visibility [Array<Symbol>] :public, :protected, and/or :private
729
+ # @param deep [Boolean]
730
+ # @param skip [Set<String>]
731
+ # @param no_core [Boolean] Skip core classes if true
732
+ # @return [Array<Pin::Base>]
733
+ def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core)
734
+ logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" }
735
+
736
+ # Ensure the types returned by the methods in the referenced
737
+ # type are relative to the generic values passed in the
738
+ # reference. e.g., Foo<String> might include Enumerable<String>
739
+ #
740
+ # @todo perform the same translation in the other areas
741
+ # here after adding a spec and handling things correctly
742
+ # in ApiMap::Store and RbsMap::Conversions for each
743
+ resolved_reference_type = ComplexType.parse(fq_reference_tag).force_rooted.resolve_generics(namespace_pin, type)
744
+ # @todo Can inner_get_methods be cached? Lots of lookups of base types going on.
745
+ methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core)
746
+ if namespace_pin && !resolved_reference_type.all_params.empty?
747
+ reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first
748
+ # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" }
749
+ methods = methods.map do |method_pin|
750
+ method_pin.resolve_generics(reference_pin, resolved_reference_type)
751
+ end
752
+ end
753
+ # logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolved_reference_type: #{resolved_reference_type} for type=#{type}: #{methods.map(&:name)}" }
754
+ methods
755
+ end
756
+
757
+ # @param fq_sub_tag [String]
758
+ # @return [String, nil]
759
+ def qualify_superclass fq_sub_tag
760
+ store.qualify_superclass fq_sub_tag
761
+ end
762
+
763
+ private
764
+
765
+ # A hash of source maps with filename keys.
766
+ #
767
+ # @return [Hash{String => SourceMap}]
768
+ attr_reader :source_map_hash
769
+
770
+ # @return [ApiMap::Store]
771
+ def store
772
+ @store ||= Store.new
773
+ end
774
+
775
+ # @return [Solargraph::ApiMap::Cache]
776
+ attr_reader :cache
777
+
778
+ # @param rooted_tag [String] A fully qualified namespace, with
779
+ # generic parameter values if applicable
780
+ # @param scope [Symbol] :class or :instance
781
+ # @param visibility [Array<Symbol>] :public, :protected, and/or :private
782
+ # @param deep [Boolean]
783
+ # @param skip [Set<String>]
784
+ # @param no_core [Boolean] Skip core classes if true
785
+ # @return [Array<Pin::Base>]
786
+ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
787
+ rooted_type = ComplexType.parse(rooted_tag).force_rooted
788
+ fqns = rooted_type.namespace
789
+ fqns_generic_params = rooted_type.all_params
790
+ namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
791
+ return [] if no_core && fqns =~ /^(Object|BasicObject|Class|Module)$/
792
+ reqstr = "#{fqns}|#{scope}|#{visibility.sort}|#{deep}"
793
+ return [] if skip.include?(reqstr)
794
+ skip.add reqstr
795
+ result = []
796
+ environ = Convention.for_object(self, rooted_tag, scope, visibility, deep, skip, no_core)
797
+ # ensure we start out with any immediate methods in this
798
+ # namespace so we roughly match the same ordering of get_methods
799
+ # and obey the 'deep' instruction
800
+ direct_convention_methods, convention_methods_by_reference = environ.pins.partition { |p| p.namespace == rooted_tag }
801
+ result.concat direct_convention_methods
802
+
803
+ if deep && scope == :instance
804
+ store.get_prepends(fqns).reverse.each do |im|
805
+ fqim = store.constants.dereference(im)
806
+ result.concat inner_get_methods(fqim, scope, visibility, deep, skip, true) unless fqim.nil?
807
+ end
808
+ end
809
+ # Store#get_methods doesn't know about full tags, just
810
+ # namespaces; resolving the generics in the method pins is this
811
+ # class' responsibility
812
+ methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort{ |a, b| a.name <=> b.name }
813
+ logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" }
814
+ result.concat methods
815
+ if deep
816
+ result.concat convention_methods_by_reference
817
+
818
+ if scope == :instance
819
+ store.get_includes(fqns).reverse.each do |ref|
820
+ in_tag = dereference(ref)
821
+ # @sg-ignore Need to add nil check here
822
+ result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true)
823
+ end
824
+ rooted_sc_tag = qualify_superclass(rooted_tag)
825
+ unless rooted_sc_tag.nil?
826
+ result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope,
827
+ visibility, true, skip, no_core)
828
+ end
829
+ else
830
+ logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" }
831
+ store.get_extends(fqns).reverse.each do |em|
832
+ fqem = dereference(em)
833
+ result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil?
834
+ end
835
+ rooted_sc_tag = qualify_superclass(rooted_tag)
836
+ unless rooted_sc_tag.nil?
837
+ result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope,
838
+ visibility, true, skip, true)
839
+ end
840
+ unless no_core || fqns.empty?
841
+ type = get_namespace_type(fqns)
842
+ result.concat inner_get_methods('Class', :instance, visibility, deep, skip, no_core) if type == :class
843
+ result.concat inner_get_methods('Module', :instance, visibility, deep, skip, no_core)
844
+ end
845
+ end
846
+ store.domains(fqns).each do |d|
847
+ dt = ComplexType.try_parse(d)
848
+ result.concat inner_get_methods(dt.namespace, dt.scope, visibility, deep, skip)
849
+ end
850
+ end
851
+ result
852
+ end
853
+
854
+ # @return [Hash]
855
+ def path_macros
856
+ @path_macros ||= {}
857
+ end
858
+
859
+ # Get the namespace's type (Class or Module).
860
+ #
861
+ # @param fqns [String] A fully qualified namespace
862
+ # @return [Symbol, nil] :class, :module, or nil
863
+ def get_namespace_type fqns
864
+ return nil if fqns.nil?
865
+ # @type [Pin::Namespace, nil]
866
+ pin = store.get_path_pins(fqns).select{|p| p.is_a?(Pin::Namespace)}.first
867
+ return nil if pin.nil?
868
+ pin.type
869
+ end
870
+
871
+ # Sort an array of pins to put nil or undefined variables last.
872
+ #
873
+ # @param pins [Enumerable<Pin::BaseVariable>]
874
+ # @return [Enumerable<Pin::BaseVariable>]
875
+ def prefer_non_nil_variables pins
876
+ result = []
877
+ nil_pins = []
878
+ pins.each do |pin|
879
+ if pin.variable? && pin.nil_assignment?
880
+ nil_pins.push pin
881
+ else
882
+ result.push pin
883
+ end
884
+ end
885
+ result + nil_pins
886
+ end
887
+
888
+ include Logging
889
+
890
+ # @param alias_pin [Pin::MethodAlias]
891
+ # @return [Pin::Method, nil]
892
+ def resolve_method_alias(alias_pin)
893
+ ancestors = store.get_ancestors(alias_pin.full_context.reduce_class_type.tag)
894
+ # @type [Pin::Method, nil]
895
+ original = nil
896
+
897
+ # Search each ancestor for the original method
898
+ ancestors.each do |ancestor_fqns|
899
+ next if ancestor_fqns.nil?
900
+ ancestor_method_path = if alias_pin.original == 'new' && alias_pin.scope == :class
901
+ "#{ancestor_fqns}#initialize"
902
+ else
903
+ "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}"
904
+ end
905
+
906
+ # Search for the original method in the ancestor
907
+ original = store.get_path_pins(ancestor_method_path).find do |candidate_pin|
908
+ next if candidate_pin == alias_pin
909
+
910
+ if candidate_pin.is_a?(Pin::MethodAlias)
911
+ # recursively resolve method aliases
912
+ resolved = resolve_method_alias(candidate_pin)
913
+ break resolved if resolved
914
+ end
915
+
916
+ candidate_pin.is_a?(Pin::Method)
917
+ end
918
+
919
+ break if original
920
+ end
921
+ if original.nil?
922
+ # :nocov:
923
+ Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{alias_pin.full_context.tag} #{alias_pin.original} in #{alias_pin.scope} scope = #{alias_pin.inspect}" }
924
+ return nil
925
+ # :nocov:
926
+ end
927
+
928
+ # @sg-ignore ignore `received nil` for original
929
+ create_resolved_alias_pin(alias_pin, original)
930
+ end
931
+
932
+ # Fast path for creating resolved alias pins without individual method stack lookups
933
+ # @param alias_pin [Pin::MethodAlias] The alias pin to resolve
934
+ # @param original [Pin::Method] The original method pin that was already found
935
+ # @return [Pin::Method] The resolved method pin
936
+ def create_resolved_alias_pin(alias_pin, original)
937
+ # Build the resolved method pin directly (same logic as resolve_method_alias but without lookup)
938
+ args = {
939
+ location: alias_pin.location,
940
+ type_location: original.type_location,
941
+ closure: alias_pin.closure,
942
+ name: alias_pin.name,
943
+ comments: original.comments,
944
+ scope: original.scope,
945
+ visibility: original.visibility,
946
+ signatures: original.signatures.map(&:clone).freeze,
947
+ attribute: original.attribute?,
948
+ generics: original.generics.clone,
949
+ return_type: original.return_type,
950
+ source: :resolve_method_alias
951
+ }
952
+ resolved_pin = Pin::Method.new **args
953
+
954
+ # Clone signatures and parameters
955
+ resolved_pin.signatures.each do |sig|
956
+ sig.parameters = sig.parameters.map(&:clone).freeze
957
+ sig.source = :resolve_method_alias
958
+ sig.parameters.each do |param|
959
+ param.closure = resolved_pin
960
+ param.source = :resolve_method_alias
961
+ param.reset_generated!
962
+ end
963
+ sig.closure = resolved_pin
964
+ sig.reset_generated!
965
+ end
966
+
967
+ resolved_pin
968
+ end
969
+
970
+ # @param namespace_pin [Pin::Namespace]
971
+ # @param rooted_type [ComplexType]
972
+ # @param pins [Enumerable<Pin::Base>]
973
+ # @return [Array<Pin::Base>]
974
+ def erase_generics namespace_pin, rooted_type, pins
975
+ return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type)
976
+
977
+ logger.debug("Erasing generics on namespace_pin=#{namespace_pin} / rooted_type=#{rooted_type}")
978
+ pins.map do |method_pin|
979
+ method_pin.erase_generics(namespace_pin.generics)
980
+ end
981
+ end
982
+
983
+ # @param namespace_pin [Pin::Namespace]
984
+ # @param rooted_type [ComplexType]
985
+ def should_erase_generics_when_done? namespace_pin, rooted_type
986
+ has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type)
987
+ end
988
+
989
+ # @param namespace_pin [Pin::Namespace, Pin::Constant]
990
+ def has_generics?(namespace_pin)
991
+ namespace_pin.is_a?(Pin::Namespace) && !namespace_pin.generics.empty?
992
+ end
993
+
994
+ # @param namespace_pin [Pin::Namespace]
995
+ # @param rooted_type [ComplexType]
996
+ def can_resolve_generics? namespace_pin, rooted_type
997
+ has_generics?(namespace_pin) && !rooted_type.all_params.empty?
998
+ end
999
+ end
1000
+ end