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,683 +1,729 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
- require 'observer'
5
- require 'open3'
6
-
7
- module Solargraph
8
- # A Library handles coordination between a Workspace and an ApiMap.
9
- #
10
- class Library
11
- include Logging
12
- include Observable
13
-
14
- # @return [Solargraph::Workspace]
15
- attr_reader :workspace
16
-
17
- # @return [String, nil]
18
- attr_reader :name
19
-
20
- # @return [Source, nil]
21
- attr_reader :current
22
-
23
- # @return [LanguageServer::Progress, nil]
24
- attr_reader :cache_progress
25
-
26
- # @param workspace [Solargraph::Workspace]
27
- # @param name [String, nil]
28
- def initialize workspace = Solargraph::Workspace.new, name = nil
29
- @workspace = workspace
30
- @name = name
31
- # @type [Integer, nil]
32
- @total = nil
33
- # @type [Source, nil]
34
- @current = nil
35
- @sync_count = 0
36
- end
37
-
38
- def inspect
39
- # Let's not deal with insane data dumps in spec failures
40
- to_s
41
- end
42
-
43
- # True if the ApiMap is up to date with the library's workspace and open
44
- # files.
45
- #
46
- # @return [Boolean]
47
- def synchronized?
48
- @sync_count < 2
49
- end
50
-
51
- # Attach a source to the library.
52
- #
53
- # The attached source does not need to be a part of the workspace. The
54
- # library will include it in the ApiMap while it's attached. Only one
55
- # source can be attached to the library at a time.
56
- #
57
- # @param source [Source, nil]
58
- # @return [void]
59
- def attach source
60
- if @current && (!source || @current.filename != source.filename) && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename)
61
- source_map_hash.delete @current.filename
62
- source_map_external_require_hash.delete @current.filename
63
- @external_requires = nil
64
- end
65
- changed = source && @current != source
66
- @current = source
67
- maybe_map @current
68
- catalog if changed
69
- end
70
-
71
- # True if the specified file is currently attached.
72
- #
73
- # @param filename [String]
74
- # @return [Boolean]
75
- def attached? filename
76
- !@current.nil? && @current.filename == filename
77
- end
78
- alias open? attached?
79
-
80
- # Detach the specified file if it is currently attached to the library.
81
- #
82
- # @param filename [String]
83
- # @return [Boolean] True if the specified file was detached
84
- def detach filename
85
- return false if @current.nil? || @current.filename != filename
86
- attach nil
87
- true
88
- end
89
-
90
- # True if the specified file is included in the workspace (but not
91
- # necessarily open).
92
- #
93
- # @param filename [String]
94
- # @return [Boolean]
95
- def contain? filename
96
- workspace.has_file?(filename)
97
- end
98
-
99
- # Create a source to be added to the workspace. The file is ignored if it is
100
- # neither open in the library nor included in the workspace.
101
- #
102
- # @param filename [String]
103
- # @param text [String] The contents of the file
104
- # @return [Boolean] True if the file was added to the workspace.
105
- def create filename, text
106
- return false unless contain?(filename) || open?(filename)
107
- source = Solargraph::Source.load_string(text, filename)
108
- workspace.merge(source)
109
- true
110
- end
111
-
112
- # Create file sources from files on disk. A file is ignored if it is
113
- # neither open in the library nor included in the workspace.
114
- #
115
- # @param filenames [Array<String>]
116
- # @return [Boolean] True if at least one file was added to the workspace.
117
- def create_from_disk *filenames
118
- sources = filenames
119
- .reject { |filename| File.directory?(filename) || !File.exist?(filename) }
120
- .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) }
121
- result = workspace.merge(*sources)
122
- sources.each { |source| maybe_map source }
123
- result
124
- end
125
-
126
- # Delete files from the library. Deleting a file will make it unavailable
127
- # for checkout and optionally remove it from the workspace unless the
128
- # workspace configuration determines that it should still exist.
129
- #
130
- # @param filenames [Array<String>]
131
- # @return [Boolean] True if any file was deleted
132
- def delete *filenames
133
- result = false
134
- filenames.each do |filename|
135
- detach filename
136
- source_map_hash.delete(filename)
137
- result ||= workspace.remove(filename)
138
- end
139
- result
140
- end
141
-
142
- # Close a file in the library. Closing a file will make it unavailable for
143
- # checkout although it may still exist in the workspace.
144
- #
145
- # @param filename [String]
146
- # @return [void]
147
- def close filename
148
- return unless @current&.filename == filename
149
-
150
- @current = nil
151
- catalog unless workspace.has_file?(filename)
152
- end
153
-
154
- # Get completion suggestions at the specified file and location.
155
- #
156
- # @param filename [String] The file to analyze
157
- # @param line [Integer] The zero-based line number
158
- # @param column [Integer] The zero-based column number
159
- # @return [SourceMap::Completion, nil]
160
- # @todo Take a Location instead of filename/line/column
161
- def completions_at filename, line, column
162
- sync_catalog
163
- position = Position.new(line, column)
164
- cursor = Source::Cursor.new(read(filename), position)
165
- mutex.synchronize { api_map.clip(cursor).complete }
166
- rescue FileNotFoundError => e
167
- handle_file_not_found filename, e
168
- end
169
-
170
- # Get definition suggestions for the expression at the specified file and
171
- # location.
172
- #
173
- # @param filename [String] The file to analyze
174
- # @param line [Integer] The zero-based line number
175
- # @param column [Integer] The zero-based column number
176
- # @return [Array<Solargraph::Pin::Base>, nil]
177
- # @todo Take filename/position instead of filename/line/column
178
- def definitions_at filename, line, column
179
- sync_catalog
180
- position = Position.new(line, column)
181
- cursor = Source::Cursor.new(read(filename), position)
182
- if cursor.comment?
183
- source = read(filename)
184
- offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column))
185
- lft = source.code[0..offset-1].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i)
186
- rgt = source.code[offset..-1].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i)
187
- if lft && rgt
188
- tag = (lft[1] + rgt[1]).sub(/:+$/, '')
189
- clip = mutex.synchronize { api_map.clip(cursor) }
190
- clip.translate tag
191
- else
192
- []
193
- end
194
- else
195
- mutex.synchronize do
196
- clip = api_map.clip(cursor)
197
- clip.define.map { |pin| pin.realize(api_map) }
198
- end
199
- end
200
- rescue FileNotFoundError => e
201
- handle_file_not_found(filename, e)
202
- end
203
-
204
- # Get type definition suggestions for the expression at the specified file and
205
- # location.
206
- #
207
- # @param filename [String] The file to analyze
208
- # @param line [Integer] The zero-based line number
209
- # @param column [Integer] The zero-based column number
210
- # @return [Array<Solargraph::Pin::Base>, nil]
211
- # @todo Take filename/position instead of filename/line/column
212
- def type_definitions_at filename, line, column
213
- sync_catalog
214
- position = Position.new(line, column)
215
- cursor = Source::Cursor.new(read(filename), position)
216
- mutex.synchronize { api_map.clip(cursor).types }
217
- rescue FileNotFoundError => e
218
- handle_file_not_found filename, e
219
- end
220
-
221
- # Get signature suggestions for the method at the specified file and
222
- # location.
223
- #
224
- # @param filename [String] The file to analyze
225
- # @param line [Integer] The zero-based line number
226
- # @param column [Integer] The zero-based column number
227
- # @return [Array<Solargraph::Pin::Base>]
228
- # @todo Take filename/position instead of filename/line/column
229
- def signatures_at filename, line, column
230
- sync_catalog
231
- position = Position.new(line, column)
232
- cursor = Source::Cursor.new(read(filename), position)
233
- mutex.synchronize { api_map.clip(cursor).signify }
234
- end
235
-
236
- # @param filename [String]
237
- # @param line [Integer]
238
- # @param column [Integer]
239
- # @param strip [Boolean] Strip special characters from variable names
240
- # @param only [Boolean] Search for references in the current file only
241
- # @return [Array<Solargraph::Location>]
242
- # @todo Take a Location instead of filename/line/column
243
- def references_from filename, line, column, strip: false, only: false
244
- sync_catalog
245
- cursor = Source::Cursor.new(read(filename), [line, column])
246
- clip = mutex.synchronize { api_map.clip(cursor) }
247
- pin = clip.define.first
248
- return [] unless pin
249
- result = []
250
- files = if only
251
- [api_map.source_map(filename)]
252
- else
253
- (workspace.sources + (@current ? [@current] : []))
254
- end
255
- files.uniq(&:filename).each do |source|
256
- found = source.references(pin.name)
257
- found.select! do |loc|
258
- referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character).first
259
- referenced&.path == pin.path
260
- end
261
- if pin.path == 'Class#new'
262
- caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first
263
- if caller.defined?
264
- found.select! do |loc|
265
- clip = api_map.clip_at(loc.filename, loc.range.start)
266
- other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first
267
- caller == other
268
- end
269
- else
270
- found.clear
271
- end
272
- end
273
- # HACK: for language clients that exclude special characters from the start of variable names
274
- if strip && match = cursor.word.match(/^[^a-z0-9_]+/i)
275
- found.map! do |loc|
276
- Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column))
277
- end
278
- end
279
- result.concat(found.sort do |a, b|
280
- a.range.start.line <=> b.range.start.line
281
- end)
282
- end
283
- result.uniq
284
- end
285
-
286
- # Get the pins at the specified location or nil if the pin does not exist.
287
- #
288
- # @param location [Location]
289
- # @return [Array<Solargraph::Pin::Base>]
290
- def locate_pins location
291
- sync_catalog
292
- mutex.synchronize { api_map.locate_pins(location).map { |pin| pin.realize(api_map) } }
293
- end
294
-
295
- # Match a require reference to a file.
296
- #
297
- # @param location [Location]
298
- # @return [Location, nil]
299
- def locate_ref location
300
- map = source_map_hash[location.filename]
301
- return if map.nil?
302
- pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first
303
- return nil if pin.nil?
304
- # @param full [String]
305
- return_if_match = proc do |full|
306
- if source_map_hash.key?(full)
307
- return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0))
308
- end
309
- end
310
- workspace.require_paths.each do |path|
311
- full = File.join path, pin.name
312
- return_if_match.(full)
313
- return_if_match.(full << ".rb")
314
- end
315
- nil
316
- rescue FileNotFoundError
317
- nil
318
- end
319
-
320
- # Get an array of pins that match a path.
321
- #
322
- # @param path [String]
323
- # @return [Enumerable<Solargraph::Pin::Base>]
324
- def get_path_pins path
325
- sync_catalog
326
- mutex.synchronize { api_map.get_path_suggestions(path) }
327
- end
328
-
329
- # @param query [String]
330
- # @return [Enumerable<YARD::CodeObjects::Base>]
331
- # @return [Array(ApiMap, Enumerable<Pin::Base>)]
332
- def document query
333
- sync_catalog
334
- mutex.synchronize { [api_map, api_map.get_path_pins(query)] }
335
- end
336
-
337
- # @param query [String]
338
- # @return [Array<String>]
339
- def search query
340
- sync_catalog
341
- mutex.synchronize { api_map.search query }
342
- end
343
-
344
- # Get an array of all symbols in the workspace that match the query.
345
- #
346
- # @param query [String]
347
- # @return [Array<Pin::Base>]
348
- def query_symbols query
349
- sync_catalog
350
- mutex.synchronize { api_map.query_symbols query }
351
- end
352
-
353
- # Get an array of document symbols.
354
- #
355
- # Document symbols are composed of namespace, method, and constant pins.
356
- # The results of this query are appropriate for building the response to a
357
- # textDocument/documentSymbol message in the language server protocol.
358
- #
359
- # @param filename [String]
360
- # @return [Array<Solargraph::Pin::Base>]
361
- def document_symbols filename
362
- sync_catalog
363
- mutex.synchronize { api_map.document_symbols(filename) }
364
- end
365
-
366
- # @param path [String]
367
- # @return [Enumerable<Solargraph::Pin::Base>]
368
- def path_pins path
369
- sync_catalog
370
- mutex.synchronize { api_map.get_path_suggestions(path) }
371
- end
372
-
373
- # @return [Array<SourceMap>]
374
- def source_maps
375
- source_map_hash.values
376
- end
377
-
378
- # Get the current text of a file in the library.
379
- #
380
- # @param filename [String]
381
- # @return [String]
382
- def read_text filename
383
- source = read(filename)
384
- source.code
385
- end
386
-
387
- # Get diagnostics about a file.
388
- #
389
- # @param filename [String]
390
- # @return [Array<Hash>]
391
- def diagnose filename
392
- # @todo Only open files get diagnosed. Determine whether anything or
393
- # everything in the workspace should get diagnosed, or if there should
394
- # be an option to do so.
395
- #
396
- sync_catalog
397
- return [] unless open?(filename)
398
- result = []
399
- source = read(filename)
400
-
401
- # @type [Hash{Class<Solargraph::Diagnostics::Base> => Array<String>}]
402
- repargs = {}
403
- workspace.config.reporters.each do |line|
404
- if line == 'all!'
405
- Diagnostics.reporters.each do |reporter_name|
406
- repargs[Diagnostics.reporter(reporter_name)] ||= []
407
- end
408
- else
409
- args = line.split(':').map(&:strip)
410
- name = args.shift
411
- reporter = Diagnostics.reporter(name)
412
- raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil?
413
- repargs[reporter] ||= []
414
- repargs[reporter].concat args
415
- end
416
- end
417
- repargs.each_pair do |reporter, args|
418
- result.concat reporter.new(*args.uniq).diagnose(source, api_map)
419
- end
420
- result
421
- end
422
-
423
- # Update the ApiMap from the library's workspace and open files.
424
- #
425
- # @return [void]
426
- def catalog
427
- @sync_count += 1
428
- end
429
-
430
- # @return [Bench]
431
- def bench
432
- Bench.new(
433
- source_maps: source_map_hash.values,
434
- workspace: workspace,
435
- external_requires: external_requires,
436
- live_map: @current ? source_map_hash[@current.filename] : nil
437
- )
438
- end
439
-
440
- # Create a library from a directory.
441
- #
442
- # @param directory [String] The path to be used for the workspace
443
- # @param name [String, nil]
444
- # @return [Solargraph::Library]
445
- def self.load directory = '', name = nil
446
- Solargraph::Library.new(Solargraph::Workspace.new(directory), name)
447
- end
448
-
449
- # Try to merge a source into the library's workspace. If the workspace is
450
- # not configured to include the source, it gets ignored.
451
- #
452
- # @param source [Source]
453
- # @return [Boolean] True if the source was merged into the workspace.
454
- def merge source
455
- result = workspace.merge(source)
456
- maybe_map source
457
- result
458
- end
459
-
460
- # @return [Hash{String => SourceMap}]
461
- def source_map_hash
462
- @source_map_hash ||= {}
463
- end
464
-
465
- def mapped?
466
- (workspace.filenames - source_map_hash.keys).empty?
467
- end
468
-
469
- # @return [SourceMap, Boolean]
470
- def next_map
471
- return false if mapped?
472
- src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
473
- if src
474
- Logging.logger.debug "Mapping #{src.filename}"
475
- source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
476
- source_map_hash[src.filename]
477
- else
478
- false
479
- end
480
- end
481
-
482
- # @return [self]
483
- def map!
484
- workspace.sources.each do |src|
485
- source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
486
- find_external_requires source_map_hash[src.filename]
487
- end
488
- self
489
- end
490
-
491
- # @return [Array<Solargraph::Pin::Base>]
492
- def pins
493
- @pins ||= []
494
- end
495
-
496
- # @return [Set<String>]
497
- def external_requires
498
- @external_requires ||= source_map_external_require_hash.values.flatten.to_set
499
- end
500
-
501
- private
502
-
503
- # @return [Hash{String => Array<String>}]
504
- def source_map_external_require_hash
505
- @source_map_external_require_hash ||= {}
506
- end
507
-
508
- # @param source_map [SourceMap]
509
- # @return [void]
510
- def find_external_requires source_map
511
- # @type [Set<String>]
512
- new_set = source_map.requires.map(&:name).to_set
513
- # return if new_set == source_map_external_require_hash[source_map.filename]
514
- _filenames = nil
515
- filenames = ->{ _filenames ||= workspace.filenames.to_set }
516
- source_map_external_require_hash[source_map.filename] = new_set.reject do |path|
517
- workspace.require_paths.any? do |base|
518
- full = File.join(base, path)
519
- filenames[].include?(full) or filenames[].include?(full << ".rb")
520
- end
521
- end
522
- @external_requires = nil
523
- end
524
-
525
- # @return [Thread::Mutex]
526
- def mutex
527
- @mutex ||= Mutex.new
528
- end
529
-
530
- # @return [ApiMap]
531
- def api_map
532
- @api_map ||= Solargraph::ApiMap.new
533
- end
534
-
535
- # Get the source for an open file or create a new source if the file
536
- # exists on disk. Sources created from disk are not added to the open
537
- # workspace files, i.e., the version on disk remains the authoritative
538
- # version.
539
- #
540
- # @raise [FileNotFoundError] if the file does not exist
541
- # @param filename [String]
542
- # @return [Solargraph::Source]
543
- def read filename
544
- return @current if @current && @current.filename == filename
545
- raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename)
546
- workspace.source(filename)
547
- end
548
-
549
- # @param filename [String]
550
- # @param error [FileNotFoundError]
551
- # @return [nil]
552
- def handle_file_not_found filename, error
553
- if workspace.source(filename)
554
- Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap"
555
- nil
556
- else
557
- raise error
558
- end
559
- end
560
-
561
- # @param source [Source, nil]
562
- # @return [void]
563
- def maybe_map source
564
- return unless source
565
- return unless @current == source || workspace.has_file?(source.filename)
566
- if source_map_hash.key?(source.filename)
567
- new_map = Solargraph::SourceMap.map(source)
568
- source_map_hash[source.filename] = new_map
569
- else
570
- source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
571
- end
572
- end
573
-
574
- # @return [Set<Gem::Specification>]
575
- def cache_errors
576
- @cache_errors ||= Set.new
577
- end
578
-
579
- # @return [void]
580
- def cache_next_gemspec
581
- return if @cache_progress
582
-
583
- spec = cacheable_specs.first
584
- return end_cache_progress unless spec
585
-
586
- pending = api_map.uncached_gemspecs.length - cache_errors.length - 1
587
-
588
- if Yardoc.processing?(spec)
589
- logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)"
590
- queued_gemspec_cache.push(spec)
591
- return if pending - queued_gemspec_cache.length < 1
592
-
593
- catalog
594
- sync_catalog
595
- else
596
- logger.info "Caching #{spec.name} #{spec.version}"
597
- Thread.new do
598
- report_cache_progress spec.name, pending
599
- _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s)
600
- if s.success?
601
- logger.info "Cached #{spec.name} #{spec.version}"
602
- else
603
- cache_errors.add spec
604
- logger.warn "Error caching gemspec #{spec.name} #{spec.version}"
605
- logger.warn e
606
- end
607
- end_cache_progress
608
- catalog
609
- sync_catalog
610
- end
611
- end
612
- end
613
-
614
- # @return [Array<Gem::Specification>]
615
- def cacheable_specs
616
- cacheable = api_map.uncached_yard_gemspecs +
617
- api_map.uncached_rbs_collection_gemspecs -
618
- queued_gemspec_cache -
619
- cache_errors.to_a
620
- return cacheable unless cacheable.empty?
621
-
622
- queued_gemspec_cache
623
- end
624
-
625
- # @return [Array<Gem::Specification>]
626
- def queued_gemspec_cache
627
- @queued_gemspec_cache ||= []
628
- end
629
-
630
- # @param gem_name [String]
631
- # @param pending [Integer]
632
- # @return [void]
633
- def report_cache_progress gem_name, pending
634
- @total ||= pending
635
- @total = pending if pending > @total
636
- finished = @total - pending
637
- pct = if @total.zero?
638
- 0
639
- else
640
- ((finished.to_f / @total.to_f) * 100).to_i
641
- end
642
- message = "#{gem_name}#{pending > 0 ? " (+#{pending})" : ''}"
643
- # "
644
- if @cache_progress
645
- @cache_progress.report(message, pct)
646
- else
647
- @cache_progress = LanguageServer::Progress.new('Caching gem')
648
- # If we don't send both a begin and a report, the progress notification
649
- # might get stuck in the status bar forever
650
- @cache_progress.begin(message, pct)
651
- changed
652
- notify_observers @cache_progress
653
- @cache_progress.report(message, pct)
654
- end
655
- changed
656
- notify_observers @cache_progress
657
- end
658
-
659
- # @return [void]
660
- def end_cache_progress
661
- changed if @cache_progress&.finish('done')
662
- notify_observers @cache_progress
663
- @cache_progress = nil
664
- @total = nil
665
- end
666
-
667
- # @return [void]
668
- def sync_catalog
669
- return if @sync_count == 0
670
-
671
- mutex.synchronize do
672
- logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
673
- source_map_hash.values.each { |map| find_external_requires(map) }
674
- api_map.catalog bench
675
- logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"
676
- logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs"
677
- logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs"
678
- cache_next_gemspec
679
- @sync_count = 0
680
- end
681
- end
682
- end
683
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'pathname'
5
+ require 'observer'
6
+ require 'open3'
7
+
8
+ # @!parse
9
+ # class ::Gem::Specification
10
+ # # @return [String]
11
+ # def name; end
12
+ # end
13
+
14
+ module Solargraph
15
+ # A Library handles coordination between a Workspace and an ApiMap.
16
+ #
17
+ class Library
18
+ include Logging
19
+ include Observable
20
+
21
+ # @return [Solargraph::Workspace]
22
+ attr_reader :workspace
23
+
24
+ # @return [String, nil]
25
+ attr_reader :name
26
+
27
+ # @return [Source, nil]
28
+ attr_reader :current
29
+
30
+ # @return [LanguageServer::Progress, nil]
31
+ attr_reader :cache_progress
32
+
33
+ # @param workspace [Solargraph::Workspace]
34
+ # @param name [String, nil]
35
+ def initialize workspace = Solargraph::Workspace.new, name = nil
36
+ @workspace = workspace
37
+ @name = name
38
+ # @type [Integer, nil]
39
+ @total = nil
40
+ # @type [Source, nil]
41
+ @current = nil
42
+ @sync_count = 0
43
+ @cache_progress = nil
44
+ end
45
+
46
+ def inspect
47
+ # Let's not deal with insane data dumps in spec failures
48
+ to_s
49
+ end
50
+
51
+ # True if the ApiMap is up to date with the library's workspace and open
52
+ # files.
53
+ #
54
+ # @return [Boolean]
55
+ def synchronized?
56
+ @sync_count < 2
57
+ end
58
+
59
+ # Attach a source to the library.
60
+ #
61
+ # The attached source does not need to be a part of the workspace. The
62
+ # library will include it in the ApiMap while it's attached. Only one
63
+ # source can be attached to the library at a time.
64
+ #
65
+ # @param source [Source, nil]
66
+ # @return [void]
67
+ def attach source
68
+ # @sg-ignore Need to add nil check here
69
+ if @current && (!source || @current.filename != source.filename) && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename)
70
+ # @sg-ignore Need to add nil check here
71
+ source_map_hash.delete @current.filename
72
+ # @sg-ignore Need to add nil check here
73
+ source_map_external_require_hash.delete @current.filename
74
+ @external_requires = nil
75
+ end
76
+ changed = source && @current != source
77
+ @current = source
78
+ maybe_map @current
79
+ catalog if changed
80
+ end
81
+
82
+ # True if the specified file is currently attached.
83
+ #
84
+ # @param filename [String]
85
+ # @return [Boolean]
86
+ def attached? filename
87
+ !@current.nil? && @current.filename == filename
88
+ end
89
+ alias open? attached?
90
+
91
+ # Detach the specified file if it is currently attached to the library.
92
+ #
93
+ # @param filename [String]
94
+ # @return [Boolean] True if the specified file was detached
95
+ def detach filename
96
+ return false if @current.nil? || @current.filename != filename
97
+ attach nil
98
+ true
99
+ end
100
+
101
+ # True if the specified file is included in the workspace (but not
102
+ # necessarily open).
103
+ #
104
+ # @param filename [String]
105
+ # @return [Boolean]
106
+ def contain? filename
107
+ workspace.has_file?(filename)
108
+ end
109
+
110
+ # Create a source to be added to the workspace. The file is ignored if it is
111
+ # neither open in the library nor included in the workspace.
112
+ #
113
+ # @param filename [String]
114
+ # @param text [String] The contents of the file
115
+ # @return [Boolean] True if the file was added to the workspace.
116
+ def create filename, text
117
+ return false unless contain?(filename) || open?(filename)
118
+ source = Solargraph::Source.load_string(text, filename)
119
+ workspace.merge(source)
120
+ true
121
+ end
122
+
123
+ # Create file sources from files on disk. A file is ignored if it is
124
+ # neither open in the library nor included in the workspace.
125
+ #
126
+ # @param filenames [Array<String>]
127
+ # @return [Boolean] True if at least one file was added to the workspace.
128
+ def create_from_disk *filenames
129
+ sources = filenames
130
+ .reject { |filename| File.directory?(filename) || !File.exist?(filename) }
131
+ .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) }
132
+ result = workspace.merge(*sources)
133
+ sources.each { |source| maybe_map source }
134
+ result
135
+ end
136
+
137
+ # Delete files from the library. Deleting a file will make it unavailable
138
+ # for checkout and optionally remove it from the workspace unless the
139
+ # workspace configuration determines that it should still exist.
140
+ #
141
+ # @param filenames [Array<String>]
142
+ # @return [Boolean] True if any file was deleted
143
+ def delete *filenames
144
+ result = false
145
+ filenames.each do |filename|
146
+ detach filename
147
+ source_map_hash.delete(filename)
148
+ result ||= workspace.remove(filename)
149
+ end
150
+ result
151
+ end
152
+
153
+ # Close a file in the library. Closing a file will make it unavailable for
154
+ # checkout although it may still exist in the workspace.
155
+ #
156
+ # @param filename [String]
157
+ # @return [void]
158
+ def close filename
159
+ return unless @current&.filename == filename
160
+
161
+ @current = nil
162
+ catalog unless workspace.has_file?(filename)
163
+ end
164
+
165
+ # Get completion suggestions at the specified file and location.
166
+ #
167
+ # @param filename [String] The file to analyze
168
+ # @param line [Integer] The zero-based line number
169
+ # @param column [Integer] The zero-based column number
170
+ # @return [SourceMap::Completion, nil]
171
+ # @todo Take a Location instead of filename/line/column
172
+ def completions_at filename, line, column
173
+ sync_catalog
174
+ position = Position.new(line, column)
175
+ cursor = Source::Cursor.new(read(filename), position)
176
+ mutex.synchronize { api_map.clip(cursor).complete }
177
+ rescue FileNotFoundError => e
178
+ handle_file_not_found filename, e
179
+ end
180
+
181
+ # Get definition suggestions for the expression at the specified file and
182
+ # location.
183
+ #
184
+ # @param filename [String] The file to analyze
185
+ # @param line [Integer] The zero-based line number
186
+ # @param column [Integer] The zero-based column number
187
+ # @return [Array<Solargraph::Pin::Base>, nil]
188
+ # @todo Take filename/position instead of filename/line/column
189
+ def definitions_at filename, line, column
190
+ sync_catalog
191
+ position = Position.new(line, column)
192
+ cursor = Source::Cursor.new(read(filename), position)
193
+ if cursor.comment?
194
+ source = read(filename)
195
+ offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column))
196
+ # @sg-ignore Need to add nil check here
197
+ # @type [MatchData, nil]
198
+ lft = source.code[0..offset-1].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i)
199
+ # @sg-ignore Need to add nil check here
200
+ # @type [MatchData, nil]
201
+ rgt = source.code[offset..-1].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i)
202
+ if lft && rgt
203
+ # @sg-ignore Need to add nil check here
204
+ tag = (lft[1] + rgt[1]).sub(/:+$/, '')
205
+ clip = mutex.synchronize { api_map.clip(cursor) }
206
+ clip.translate tag
207
+ else
208
+ []
209
+ end
210
+ else
211
+ mutex.synchronize do
212
+ clip = api_map.clip(cursor)
213
+ clip.define.map { |pin| pin.realize(api_map) }
214
+ end
215
+ end
216
+ rescue FileNotFoundError => e
217
+ handle_file_not_found(filename, e)
218
+ end
219
+
220
+ # Get type definition suggestions for the expression at the specified file and
221
+ # location.
222
+ #
223
+ # @param filename [String] The file to analyze
224
+ # @param line [Integer] The zero-based line number
225
+ # @param column [Integer] The zero-based column number
226
+ # @return [Array<Solargraph::Pin::Base>, nil]
227
+ # @todo Take filename/position instead of filename/line/column
228
+ def type_definitions_at filename, line, column
229
+ sync_catalog
230
+ position = Position.new(line, column)
231
+ cursor = Source::Cursor.new(read(filename), position)
232
+ mutex.synchronize { api_map.clip(cursor).types }
233
+ rescue FileNotFoundError => e
234
+ handle_file_not_found filename, e
235
+ end
236
+
237
+ # Get signature suggestions for the method at the specified file and
238
+ # location.
239
+ #
240
+ # @param filename [String] The file to analyze
241
+ # @param line [Integer] The zero-based line number
242
+ # @param column [Integer] The zero-based column number
243
+ # @return [Array<Solargraph::Pin::Base>]
244
+ # @todo Take filename/position instead of filename/line/column
245
+ def signatures_at filename, line, column
246
+ sync_catalog
247
+ position = Position.new(line, column)
248
+ cursor = Source::Cursor.new(read(filename), position)
249
+ mutex.synchronize { api_map.clip(cursor).signify }
250
+ end
251
+
252
+ # @param filename [String]
253
+ # @param line [Integer]
254
+ # @param column [Integer]
255
+ # @param strip [Boolean] Strip special characters from variable names
256
+ # @param only [Boolean] Search for references in the current file only
257
+ # @return [Array<Solargraph::Location>]
258
+ # @todo Take a Location instead of filename/line/column
259
+ def references_from filename, line, column, strip: false, only: false
260
+ sync_catalog
261
+ cursor = Source::Cursor.new(read(filename), [line, column])
262
+ clip = mutex.synchronize { api_map.clip(cursor) }
263
+ pin = clip.define.first
264
+ return [] unless pin
265
+ result = []
266
+ files = if only
267
+ [api_map.source_map(filename)]
268
+ else
269
+ (workspace.sources + (@current ? [@current] : []))
270
+ end
271
+ files.uniq(&:filename).each do |source|
272
+ found = source.references(pin.name)
273
+ found.select! do |loc|
274
+ # @sg-ignore Need to add nil check here
275
+ # @type [Solargraph::Pin::Base, nil]
276
+ referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character).first
277
+ referenced&.path == pin.path
278
+ end
279
+ if pin.path == 'Class#new'
280
+ # @todo flow sensitive typing should allow shadowing of Kernel#caller
281
+ caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first
282
+ if caller.defined?
283
+ found.select! do |loc|
284
+ clip = api_map.clip_at(loc.filename, loc.range.start)
285
+ other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first
286
+ # @todo flow sensitive typing should allow shadowing of Kernel#caller
287
+ caller == other
288
+ end
289
+ else
290
+ found.clear
291
+ end
292
+ end
293
+ # HACK: for language clients that exclude special characters from the start of variable names
294
+ if strip && match = cursor.word.match(/^[^a-z0-9_]+/i)
295
+ found.map! do |loc|
296
+ Solargraph::Location.new(loc.filename,
297
+ # @sg-ignore flow sensitive typing needs to handle if foo = bar
298
+ Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line,
299
+ loc.range.ending.column))
300
+ end
301
+ end
302
+ result.concat(found.sort { |a, b| a.range.start.line <=> b.range.start.line })
303
+ end
304
+ result.uniq
305
+ end
306
+
307
+ # Get the pins at the specified location or nil if the pin does not exist.
308
+ #
309
+ # @param location [Location]
310
+ # @return [Array<Solargraph::Pin::Base>]
311
+ def locate_pins location
312
+ sync_catalog
313
+ mutex.synchronize { api_map.locate_pins(location).map { |pin| pin.realize(api_map) } }
314
+ end
315
+
316
+ # Match a require reference to a file.
317
+ #
318
+ # @param location [Location]
319
+ # @return [Location, nil]
320
+ def locate_ref location
321
+ map = source_map_hash[location.filename]
322
+ return if map.nil?
323
+ # @sg-ignore Need to add nil check here
324
+ pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first
325
+ return nil if pin.nil?
326
+ # @param full [String]
327
+ return_if_match = proc do |full|
328
+ return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) if source_map_hash.key?(full)
329
+ end
330
+ workspace.require_paths.each do |path|
331
+ full = File.join path, pin.name
332
+ return_if_match.(full)
333
+ return_if_match.(full << ".rb")
334
+ end
335
+ nil
336
+ rescue FileNotFoundError
337
+ nil
338
+ end
339
+
340
+ # Get an array of pins that match a path.
341
+ #
342
+ # @param path [String]
343
+ # @return [Enumerable<Solargraph::Pin::Base>]
344
+ def get_path_pins path
345
+ sync_catalog
346
+ mutex.synchronize { api_map.get_path_suggestions(path) }
347
+ end
348
+
349
+ # @param query [String]
350
+ # @return [Enumerable<YARD::CodeObjects::Base>]
351
+ # @return [Array(ApiMap, Enumerable<Pin::Base>)]
352
+ def document query
353
+ sync_catalog
354
+ mutex.synchronize { [api_map, api_map.get_path_pins(query)] }
355
+ end
356
+
357
+ # @param query [String]
358
+ # @return [Array<String>]
359
+ def search query
360
+ sync_catalog
361
+ mutex.synchronize { api_map.search query }
362
+ end
363
+
364
+ # Get an array of all symbols in the workspace that match the query.
365
+ #
366
+ # @param query [String]
367
+ # @return [Array<Pin::Base>]
368
+ def query_symbols query
369
+ sync_catalog
370
+ mutex.synchronize { api_map.query_symbols query }
371
+ end
372
+
373
+ # Get an array of document symbols.
374
+ #
375
+ # Document symbols are composed of namespace, method, and constant pins.
376
+ # The results of this query are appropriate for building the response to a
377
+ # textDocument/documentSymbol message in the language server protocol.
378
+ #
379
+ # @param filename [String]
380
+ # @return [Array<Solargraph::Pin::Base>]
381
+ def document_symbols filename
382
+ sync_catalog
383
+ mutex.synchronize { api_map.document_symbols(filename) }
384
+ end
385
+
386
+ # @param path [String]
387
+ # @return [Enumerable<Solargraph::Pin::Base>]
388
+ def path_pins path
389
+ sync_catalog
390
+ mutex.synchronize { api_map.get_path_suggestions(path) }
391
+ end
392
+
393
+ # @return [Array<SourceMap>]
394
+ def source_maps
395
+ source_map_hash.values
396
+ end
397
+
398
+ # Get the current text of a file in the library.
399
+ #
400
+ # @param filename [String]
401
+ # @return [String]
402
+ def read_text filename
403
+ source = read(filename)
404
+ source.code
405
+ end
406
+
407
+ # Get diagnostics about a file.
408
+ #
409
+ # @param filename [String]
410
+ # @return [Array<Hash>]
411
+ def diagnose filename
412
+ # @todo Only open files get diagnosed. Determine whether anything or
413
+ # everything in the workspace should get diagnosed, or if there should
414
+ # be an option to do so.
415
+ #
416
+ sync_catalog
417
+ return [] unless open?(filename)
418
+ result = []
419
+ source = read(filename)
420
+
421
+ # @type [Hash{Class<Solargraph::Diagnostics::Base> => Array<String>}]
422
+ repargs = {}
423
+ workspace.config.reporters.each do |line|
424
+ if line == 'all!'
425
+ Diagnostics.reporters.each do |reporter_name|
426
+ # @sg-ignore Need to add nil check here
427
+ repargs[Diagnostics.reporter(reporter_name)] ||= []
428
+ end
429
+ else
430
+ args = line.split(':').map(&:strip)
431
+ name = args.shift
432
+ reporter = Diagnostics.reporter(name)
433
+ raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil?
434
+ # @sg-ignore flow sensitive typing needs to handle 'raise if'
435
+ repargs[reporter] ||= []
436
+ # @sg-ignore flow sensitive typing needs to handle 'raise if'
437
+ repargs[reporter].concat args
438
+ end
439
+ end
440
+ repargs.each_pair do |reporter, args|
441
+ result.concat reporter.new(*args.uniq).diagnose(source, api_map)
442
+ end
443
+ result
444
+ end
445
+
446
+ # Update the ApiMap from the library's workspace and open files.
447
+ #
448
+ # @return [void]
449
+ def catalog
450
+ @sync_count += 1
451
+ end
452
+
453
+ # @return [Bench]
454
+ def bench
455
+ Bench.new(
456
+ source_maps: source_map_hash.values,
457
+ workspace: workspace,
458
+ external_requires: external_requires,
459
+ # @sg-ignore Need to add nil check here
460
+ live_map: @current ? source_map_hash[@current.filename] : nil
461
+ )
462
+ end
463
+
464
+ # Create a library from a directory.
465
+ #
466
+ # @param directory [String] The path to be used for the workspace
467
+ # @param name [String, nil]
468
+ # @return [Solargraph::Library]
469
+ def self.load directory = '', name = nil
470
+ Solargraph::Library.new(Solargraph::Workspace.new(directory), name)
471
+ end
472
+
473
+ # Try to merge a source into the library's workspace. If the workspace is
474
+ # not configured to include the source, it gets ignored.
475
+ #
476
+ # @param source [Source]
477
+ # @return [Boolean] True if the source was merged into the workspace.
478
+ def merge source
479
+ result = workspace.merge(source)
480
+ maybe_map source
481
+ result
482
+ end
483
+
484
+ # @return [Hash{String => SourceMap}]
485
+ def source_map_hash
486
+ @source_map_hash ||= {}
487
+ end
488
+
489
+ def mapped?
490
+ (workspace.filenames - source_map_hash.keys).empty?
491
+ end
492
+
493
+ # @return [SourceMap, Boolean]
494
+ def next_map
495
+ return false if mapped?
496
+ # @sg-ignore Need to add nil check here
497
+ src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
498
+ if src
499
+ Logging.logger.debug "Mapping #{src.filename}"
500
+ # @sg-ignore Need to add nil check here
501
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
502
+ # @sg-ignore Need to add nil check here
503
+ source_map_hash[src.filename]
504
+ else
505
+ false
506
+ end
507
+ end
508
+
509
+ # @return [self]
510
+ def map!
511
+ workspace.sources.each do |src|
512
+ # @sg-ignore Need to add nil check here
513
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
514
+ # @sg-ignore Need to add nil check here
515
+ find_external_requires source_map_hash[src.filename]
516
+ end
517
+ self
518
+ end
519
+
520
+ # @return [Array<Solargraph::Pin::Base>]
521
+ def pins
522
+ @pins ||= []
523
+ end
524
+
525
+ # @return [Set<String>]
526
+ def external_requires
527
+ @external_requires ||= source_map_external_require_hash.values.flatten.to_set
528
+ end
529
+
530
+ private
531
+
532
+ # @return [PinCache]
533
+ def pin_cache
534
+ workspace.pin_cache
535
+ end
536
+
537
+ # @return [Hash{String => Array<String>}]
538
+ def source_map_external_require_hash
539
+ @source_map_external_require_hash ||= {}
540
+ end
541
+
542
+ # @param source_map [SourceMap]
543
+ # @return [void]
544
+ def find_external_requires source_map
545
+ # @type [Set<String>]
546
+ new_set = source_map.requires.map(&:name).to_set
547
+ # return if new_set == source_map_external_require_hash[source_map.filename]
548
+ _filenames = nil
549
+ filenames = ->{ _filenames ||= workspace.filenames.to_set }
550
+ # @sg-ignore Need to add nil check here
551
+ source_map_external_require_hash[source_map.filename] = new_set.reject do |path|
552
+ workspace.require_paths.any? do |base|
553
+ full = File.join(base, path)
554
+ filenames[].include?(full) or filenames[].include?(full << ".rb")
555
+ end
556
+ end
557
+ @external_requires = nil
558
+ end
559
+
560
+ # @return [Thread::Mutex]
561
+ def mutex
562
+ @mutex ||= Mutex.new
563
+ end
564
+
565
+ # @return [ApiMap]
566
+ def api_map
567
+ @api_map ||= Solargraph::ApiMap.new
568
+ end
569
+
570
+ # Get the source for an open file or create a new source if the file
571
+ # exists on disk. Sources created from disk are not added to the open
572
+ # workspace files, i.e., the version on disk remains the authoritative
573
+ # version.
574
+ #
575
+ # @raise [FileNotFoundError] if the file does not exist
576
+ # @param filename [String]
577
+ # @return [Solargraph::Source]
578
+ def read filename
579
+ return @current if @current && @current.filename == filename
580
+ raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename)
581
+ workspace.source(filename)
582
+ end
583
+
584
+ # @param filename [String]
585
+ # @param error [FileNotFoundError]
586
+ # @return [nil]
587
+ def handle_file_not_found filename, error
588
+ if workspace.source(filename)
589
+ Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap"
590
+ nil
591
+ else
592
+ raise error
593
+ end
594
+ end
595
+
596
+ # @param source [Source, nil]
597
+ # @return [void]
598
+ def maybe_map source
599
+ return unless source
600
+ # @sg-ignore Need to add nil check here
601
+ return unless @current == source || workspace.has_file?(source.filename)
602
+ # @sg-ignore Need to add nil check here
603
+ if source_map_hash.key?(source.filename)
604
+ new_map = Solargraph::SourceMap.map(source)
605
+ # @sg-ignore Need to add nil check here
606
+ source_map_hash[source.filename] = new_map
607
+ else
608
+ # @sg-ignore Need to add nil check here
609
+ source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
610
+ end
611
+ end
612
+
613
+ # @return [Set<Gem::Specification>]
614
+ def cache_errors
615
+ @cache_errors ||= Set.new
616
+ end
617
+
618
+ # @return [void]
619
+ def cache_next_gemspec
620
+ return if @cache_progress
621
+
622
+ spec = cacheable_specs.first
623
+ return end_cache_progress unless spec
624
+
625
+ pending = api_map.uncached_gemspecs.length - cache_errors.length - 1
626
+
627
+ if pin_cache.yardoc_processing?(spec)
628
+ logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)"
629
+ queued_gemspec_cache.push(spec)
630
+ return if pending - queued_gemspec_cache.length < 1
631
+
632
+ catalog
633
+ sync_catalog
634
+ else
635
+ logger.info "Caching #{spec.name} #{spec.version}"
636
+ Thread.new do
637
+ report_cache_progress spec.name, pending
638
+ kwargs = {}
639
+ kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty?
640
+ _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s,
641
+ **kwargs)
642
+ if s.success?
643
+ logger.info "Cached #{spec.name} #{spec.version}"
644
+ else
645
+ cache_errors.add spec
646
+ logger.warn "Error caching gemspec #{spec.name} #{spec.version}"
647
+ logger.warn e
648
+ end
649
+ end_cache_progress
650
+ catalog
651
+ sync_catalog
652
+ end
653
+ end
654
+ end
655
+
656
+ # @return [Array<Gem::Specification>]
657
+ def cacheable_specs
658
+ cacheable = api_map.uncached_gemspecs +
659
+ queued_gemspec_cache -
660
+ cache_errors.to_a
661
+ return cacheable unless cacheable.empty?
662
+
663
+ queued_gemspec_cache
664
+ end
665
+
666
+ # @return [Array<Gem::Specification>]
667
+ def queued_gemspec_cache
668
+ @queued_gemspec_cache ||= []
669
+ end
670
+
671
+ # @param gem_name [String]
672
+ # @param pending [Integer]
673
+ # @return [void]
674
+ def report_cache_progress gem_name, pending
675
+ @total ||= pending
676
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
677
+ @total = pending if pending > @total
678
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
679
+ finished = @total - pending
680
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
681
+ pct = if @total.zero?
682
+ 0
683
+ else
684
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
685
+ ((finished.to_f / @total.to_f) * 100).to_i
686
+ end
687
+ message = "#{gem_name}#{pending > 0 ? " (+#{pending})" : ''}"
688
+ # "
689
+ if @cache_progress
690
+ @cache_progress.report(message, pct)
691
+ else
692
+ @cache_progress = LanguageServer::Progress.new('Caching gem')
693
+ # If we don't send both a begin and a report, the progress notification
694
+ # might get stuck in the status bar forever
695
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
696
+ @cache_progress.begin(message, pct)
697
+ changed
698
+ notify_observers @cache_progress
699
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
700
+ @cache_progress.report(message, pct)
701
+ end
702
+ changed
703
+ notify_observers @cache_progress
704
+ end
705
+
706
+ # @return [void]
707
+ def end_cache_progress
708
+ changed if @cache_progress&.finish('done')
709
+ notify_observers @cache_progress
710
+ @cache_progress = nil
711
+ @total = nil
712
+ end
713
+
714
+ # @return [void]
715
+ def sync_catalog
716
+ return if @sync_count == 0
717
+
718
+ mutex.synchronize do
719
+ logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
720
+ source_map_hash.values.each { |map| find_external_requires(map) }
721
+ api_map.catalog bench
722
+ logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"
723
+ logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs"
724
+ cache_next_gemspec
725
+ @sync_count = 0
726
+ end
727
+ end
728
+ end
729
+ end