solargraph 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +3 -0
  3. data/.gitattributes +2 -0
  4. data/.github/workflows/linting.yml +6 -5
  5. data/.github/workflows/plugins.yml +48 -35
  6. data/.github/workflows/rspec.yml +15 -28
  7. data/.github/workflows/typecheck.yml +3 -2
  8. data/.gitignore +1 -0
  9. data/.rubocop.yml +38 -6
  10. data/.rubocop_todo.yml +53 -966
  11. data/CHANGELOG.md +24 -0
  12. data/Gemfile +3 -1
  13. data/README.md +3 -3
  14. data/Rakefile +26 -23
  15. data/bin/solargraph +9 -8
  16. data/lib/solargraph/api_map/cache.rb +110 -110
  17. data/lib/solargraph/api_map/constants.rb +288 -279
  18. data/lib/solargraph/api_map/index.rb +204 -193
  19. data/lib/solargraph/api_map/source_to_yard.rb +110 -97
  20. data/lib/solargraph/api_map/store.rb +395 -384
  21. data/lib/solargraph/api_map.rb +1029 -945
  22. data/lib/solargraph/bench.rb +44 -45
  23. data/lib/solargraph/complex_type/conformance.rb +176 -0
  24. data/lib/solargraph/complex_type/type_methods.rb +240 -228
  25. data/lib/solargraph/complex_type/unique_type.rb +641 -482
  26. data/lib/solargraph/complex_type.rb +557 -444
  27. data/lib/solargraph/convention/data_definition/data_assignment_node.rb +61 -61
  28. data/lib/solargraph/convention/data_definition/data_definition_node.rb +93 -91
  29. data/lib/solargraph/convention/data_definition.rb +108 -105
  30. data/lib/solargraph/convention/gemfile.rb +15 -15
  31. data/lib/solargraph/convention/gemspec.rb +23 -23
  32. data/lib/solargraph/convention/rakefile.rb +17 -17
  33. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +62 -61
  34. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +103 -102
  35. data/lib/solargraph/convention/struct_definition.rb +168 -164
  36. data/lib/solargraph/convention.rb +78 -78
  37. data/lib/solargraph/converters/dd.rb +19 -17
  38. data/lib/solargraph/converters/dl.rb +17 -15
  39. data/lib/solargraph/converters/dt.rb +17 -15
  40. data/lib/solargraph/converters/misc.rb +3 -1
  41. data/lib/solargraph/diagnostics/require_not_found.rb +54 -53
  42. data/lib/solargraph/diagnostics/rubocop.rb +119 -118
  43. data/lib/solargraph/diagnostics/rubocop_helpers.rb +70 -68
  44. data/lib/solargraph/diagnostics/type_check.rb +56 -55
  45. data/lib/solargraph/diagnostics/update_errors.rb +37 -41
  46. data/lib/solargraph/doc_map.rb +438 -439
  47. data/lib/solargraph/equality.rb +34 -34
  48. data/lib/solargraph/gem_pins.rb +98 -98
  49. data/lib/solargraph/language_server/error_codes.rb +20 -20
  50. data/lib/solargraph/language_server/host/diagnoser.rb +89 -89
  51. data/lib/solargraph/language_server/host/dispatch.rb +130 -130
  52. data/lib/solargraph/language_server/host/message_worker.rb +113 -112
  53. data/lib/solargraph/language_server/host/sources.rb +100 -99
  54. data/lib/solargraph/language_server/host.rb +886 -878
  55. data/lib/solargraph/language_server/message/base.rb +97 -97
  56. data/lib/solargraph/language_server/message/client/register_capability.rb +13 -15
  57. data/lib/solargraph/language_server/message/completion_item/resolve.rb +58 -60
  58. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +108 -114
  59. data/lib/solargraph/language_server/message/extended/document.rb +24 -23
  60. data/lib/solargraph/language_server/message/extended/document_gems.rb +32 -32
  61. data/lib/solargraph/language_server/message/extended/download_core.rb +20 -19
  62. data/lib/solargraph/language_server/message/extended/search.rb +20 -20
  63. data/lib/solargraph/language_server/message/initialize.rb +197 -191
  64. data/lib/solargraph/language_server/message/text_document/completion.rb +58 -56
  65. data/lib/solargraph/language_server/message/text_document/definition.rb +49 -40
  66. data/lib/solargraph/language_server/message/text_document/document_highlight.rb +23 -16
  67. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +36 -26
  68. data/lib/solargraph/language_server/message/text_document/formatting.rb +150 -148
  69. data/lib/solargraph/language_server/message/text_document/hover.rb +58 -58
  70. data/lib/solargraph/language_server/message/text_document/prepare_rename.rb +18 -11
  71. data/lib/solargraph/language_server/message/text_document/references.rb +23 -16
  72. data/lib/solargraph/language_server/message/text_document/rename.rb +26 -19
  73. data/lib/solargraph/language_server/message/text_document/signature_help.rb +25 -24
  74. data/lib/solargraph/language_server/message/text_document/type_definition.rb +33 -25
  75. data/lib/solargraph/language_server/message/workspace/did_change_configuration.rb +41 -35
  76. data/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +48 -40
  77. data/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +32 -26
  78. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +33 -23
  79. data/lib/solargraph/language_server/message.rb +94 -94
  80. data/lib/solargraph/language_server/request.rb +29 -27
  81. data/lib/solargraph/language_server/transport/data_reader.rb +72 -74
  82. data/lib/solargraph/language_server/uri_helpers.rb +49 -49
  83. data/lib/solargraph/library.rb +702 -683
  84. data/lib/solargraph/location.rb +85 -82
  85. data/lib/solargraph/logging.rb +55 -37
  86. data/lib/solargraph/page.rb +92 -92
  87. data/lib/solargraph/parser/comment_ripper.rb +84 -69
  88. data/lib/solargraph/parser/flow_sensitive_typing.rb +471 -255
  89. data/lib/solargraph/parser/node_processor/base.rb +122 -92
  90. data/lib/solargraph/parser/node_processor.rb +63 -62
  91. data/lib/solargraph/parser/parser_gem/class_methods.rb +165 -149
  92. data/lib/solargraph/parser/parser_gem/flawed_builder.rb +19 -19
  93. data/lib/solargraph/parser/parser_gem/node_chainer.rb +191 -166
  94. data/lib/solargraph/parser/parser_gem/node_methods.rb +507 -486
  95. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +22 -22
  96. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +61 -59
  97. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +24 -15
  98. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +45 -46
  99. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +60 -53
  100. data/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +38 -37
  101. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +53 -23
  102. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +42 -40
  103. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +30 -29
  104. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +61 -59
  105. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +98 -98
  106. data/lib/solargraph/parser/parser_gem/node_processors/or_node.rb +22 -0
  107. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +17 -17
  108. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +39 -38
  109. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +51 -52
  110. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +302 -291
  111. data/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +29 -29
  112. data/lib/solargraph/parser/parser_gem/node_processors/when_node.rb +23 -0
  113. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +33 -29
  114. data/lib/solargraph/parser/parser_gem/node_processors.rb +74 -70
  115. data/lib/solargraph/parser/parser_gem.rb +14 -12
  116. data/lib/solargraph/parser/region.rb +75 -69
  117. data/lib/solargraph/parser/snippet.rb +19 -17
  118. data/lib/solargraph/parser.rb +25 -23
  119. data/lib/solargraph/pin/base.rb +773 -729
  120. data/lib/solargraph/pin/base_variable.rb +375 -126
  121. data/lib/solargraph/pin/block.rb +127 -104
  122. data/lib/solargraph/pin/breakable.rb +15 -9
  123. data/lib/solargraph/pin/callable.rb +275 -231
  124. data/lib/solargraph/pin/closure.rb +69 -72
  125. data/lib/solargraph/pin/common.rb +97 -79
  126. data/lib/solargraph/pin/compound_statement.rb +55 -0
  127. data/lib/solargraph/pin/constant.rb +43 -45
  128. data/lib/solargraph/pin/conversions.rb +129 -123
  129. data/lib/solargraph/pin/delegated_method.rb +131 -120
  130. data/lib/solargraph/pin/documenting.rb +116 -114
  131. data/lib/solargraph/pin/instance_variable.rb +38 -34
  132. data/lib/solargraph/pin/keyword.rb +16 -20
  133. data/lib/solargraph/pin/local_variable.rb +31 -75
  134. data/lib/solargraph/pin/method.rb +724 -672
  135. data/lib/solargraph/pin/method_alias.rb +42 -34
  136. data/lib/solargraph/pin/namespace.rb +122 -115
  137. data/lib/solargraph/pin/parameter.rb +339 -275
  138. data/lib/solargraph/pin/proxy_type.rb +42 -39
  139. data/lib/solargraph/pin/reference/override.rb +47 -47
  140. data/lib/solargraph/pin/reference/superclass.rb +17 -15
  141. data/lib/solargraph/pin/reference/type_alias.rb +16 -0
  142. data/lib/solargraph/pin/reference.rb +59 -39
  143. data/lib/solargraph/pin/search.rb +62 -61
  144. data/lib/solargraph/pin/signature.rb +64 -61
  145. data/lib/solargraph/pin/symbol.rb +54 -53
  146. data/lib/solargraph/pin/until.rb +16 -18
  147. data/lib/solargraph/pin/while.rb +16 -18
  148. data/lib/solargraph/pin.rb +46 -44
  149. data/lib/solargraph/pin_cache.rb +248 -245
  150. data/lib/solargraph/position.rb +139 -119
  151. data/lib/solargraph/range.rb +113 -112
  152. data/lib/solargraph/rbs_map/conversions.rb +952 -823
  153. data/lib/solargraph/rbs_map/core_fills.rb +91 -84
  154. data/lib/solargraph/rbs_map/core_map.rb +65 -58
  155. data/lib/solargraph/rbs_map/stdlib_map.rb +71 -43
  156. data/lib/solargraph/rbs_map.rb +207 -163
  157. data/lib/solargraph/server_methods.rb +16 -16
  158. data/lib/solargraph/shell.rb +555 -352
  159. data/lib/solargraph/source/chain/array.rb +39 -37
  160. data/lib/solargraph/source/chain/call.rb +377 -337
  161. data/lib/solargraph/source/chain/class_variable.rb +13 -13
  162. data/lib/solargraph/source/chain/constant.rb +30 -26
  163. data/lib/solargraph/source/chain/global_variable.rb +13 -13
  164. data/lib/solargraph/source/chain/hash.rb +37 -34
  165. data/lib/solargraph/source/chain/if.rb +30 -28
  166. data/lib/solargraph/source/chain/instance_variable.rb +36 -13
  167. data/lib/solargraph/source/chain/link.rb +99 -109
  168. data/lib/solargraph/source/chain/literal.rb +51 -48
  169. data/lib/solargraph/source/chain/or.rb +29 -23
  170. data/lib/solargraph/source/chain/q_call.rb +13 -11
  171. data/lib/solargraph/source/chain/variable.rb +15 -13
  172. data/lib/solargraph/source/chain/z_super.rb +28 -30
  173. data/lib/solargraph/source/chain.rb +304 -291
  174. data/lib/solargraph/source/change.rb +89 -82
  175. data/lib/solargraph/source/cursor.rb +172 -166
  176. data/lib/solargraph/source/encoding_fixes.rb +22 -23
  177. data/lib/solargraph/source/source_chainer.rb +218 -194
  178. data/lib/solargraph/source/updater.rb +59 -55
  179. data/lib/solargraph/source.rb +522 -498
  180. data/lib/solargraph/source_map/clip.rb +245 -226
  181. data/lib/solargraph/source_map/data.rb +37 -34
  182. data/lib/solargraph/source_map/mapper.rb +286 -259
  183. data/lib/solargraph/source_map.rb +224 -212
  184. data/lib/solargraph/type_checker/problem.rb +34 -32
  185. data/lib/solargraph/type_checker/rules.rb +157 -84
  186. data/lib/solargraph/type_checker.rb +888 -814
  187. data/lib/solargraph/version.rb +5 -5
  188. data/lib/solargraph/workspace/config.rb +258 -255
  189. data/lib/solargraph/workspace/gemspecs.rb +367 -0
  190. data/lib/solargraph/workspace/require_paths.rb +98 -97
  191. data/lib/solargraph/workspace.rb +242 -220
  192. data/lib/solargraph/yard_map/helpers.rb +49 -44
  193. data/lib/solargraph/yard_map/mapper/to_method.rb +136 -130
  194. data/lib/solargraph/yard_map/mapper/to_namespace.rb +32 -31
  195. data/lib/solargraph/yard_map/mapper.rb +84 -79
  196. data/lib/solargraph/yard_map.rb +17 -18
  197. data/lib/solargraph/yard_tags.rb +20 -20
  198. data/lib/solargraph/yardoc.rb +90 -87
  199. data/lib/solargraph.rb +128 -105
  200. data/rbs/fills/rubygems/0/dependency.rbs +193 -0
  201. data/rbs/fills/tuple/tuple.rbs +28 -0
  202. data/rbs/shims/ast/0/node.rbs +5 -0
  203. data/rbs/shims/diff-lcs/1.5/diff-lcs.rbs +11 -0
  204. data/rbs_collection.yaml +1 -1
  205. data/solargraph.gemspec +37 -35
  206. metadata +52 -51
  207. data/lib/solargraph/type_checker/checks.rb +0 -124
  208. data/lib/solargraph/type_checker/param_def.rb +0 -37
  209. data/lib/solargraph/yard_map/to_method.rb +0 -89
  210. data/sig/shims/ast/0/node.rbs +0 -5
  211. /data/{sig → rbs}/shims/ast/2.4/.rbs_meta.yaml +0 -0
  212. /data/{sig → rbs}/shims/ast/2.4/ast.rbs +0 -0
  213. /data/{sig → rbs}/shims/parser/3.2.0.1/builders/default.rbs +0 -0
  214. /data/{sig → rbs}/shims/parser/3.2.0.1/manifest.yaml +0 -0
  215. /data/{sig → rbs}/shims/parser/3.2.0.1/parser.rbs +0 -0
  216. /data/{sig → rbs}/shims/parser/3.2.0.1/polyfill.rbs +0 -0
  217. /data/{sig → rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml +0 -0
  218. /data/{sig → rbs}/shims/thor/1.2.0.1/manifest.yaml +0 -0
  219. /data/{sig → rbs}/shims/thor/1.2.0.1/thor.rbs +0 -0
@@ -1,683 +1,702 @@
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 '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
+ # @type [String, nil]
61
+ current_filename = @current&.filename
62
+ if @current && (!source || current_filename != source.filename) && current_filename && source_map_hash.key?(current_filename) && !workspace.has_file?(current_filename)
63
+ source_map_hash.delete current_filename
64
+ source_map_external_require_hash.delete current_filename
65
+ @external_requires = nil
66
+ end
67
+ changed = source && @current != source
68
+ @current = source
69
+ maybe_map @current
70
+ catalog if changed
71
+ end
72
+
73
+ # True if the specified file is currently attached.
74
+ #
75
+ # @param filename [String]
76
+ # @return [Boolean]
77
+ def attached? filename
78
+ !@current.nil? && @current.filename == filename
79
+ end
80
+ alias open? attached?
81
+
82
+ # Detach the specified file if it is currently attached to the library.
83
+ #
84
+ # @param filename [String]
85
+ # @return [Boolean] True if the specified file was detached
86
+ def detach filename
87
+ return false if @current.nil? || @current.filename != filename
88
+ attach nil
89
+ true
90
+ end
91
+
92
+ # True if the specified file is included in the workspace (but not
93
+ # necessarily open).
94
+ #
95
+ # @param filename [String]
96
+ # @return [Boolean]
97
+ def contain? filename
98
+ workspace.has_file?(filename)
99
+ end
100
+
101
+ # Create a source to be added to the workspace. The file is ignored if it is
102
+ # neither open in the library nor included in the workspace.
103
+ #
104
+ # @param filename [String]
105
+ # @param text [String] The contents of the file
106
+ # @return [Boolean] True if the file was added to the workspace.
107
+ def create filename, text
108
+ return false unless contain?(filename) || open?(filename)
109
+ source = Solargraph::Source.load_string(text, filename)
110
+ workspace.merge(source)
111
+ true
112
+ end
113
+
114
+ # Create file sources from files on disk. A file is ignored if it is
115
+ # neither open in the library nor included in the workspace.
116
+ #
117
+ # @param filenames [Array<String>]
118
+ # @return [Boolean] True if at least one file was added to the workspace.
119
+ def create_from_disk *filenames
120
+ sources = filenames
121
+ .reject { |filename| File.directory?(filename) || !File.exist?(filename) }
122
+ .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) }
123
+ result = workspace.merge(*sources)
124
+ sources.each { |source| maybe_map source }
125
+ result
126
+ end
127
+
128
+ # Delete files from the library. Deleting a file will make it unavailable
129
+ # for checkout and optionally remove it from the workspace unless the
130
+ # workspace configuration determines that it should still exist.
131
+ #
132
+ # @param filenames [Array<String>]
133
+ # @return [Boolean] True if any file was deleted
134
+ def delete *filenames
135
+ result = false
136
+ filenames.each do |filename|
137
+ detach filename
138
+ source_map_hash.delete(filename)
139
+ result ||= workspace.remove(filename)
140
+ end
141
+ result
142
+ end
143
+
144
+ # Close a file in the library. Closing a file will make it unavailable for
145
+ # checkout although it may still exist in the workspace.
146
+ #
147
+ # @param filename [String]
148
+ # @return [void]
149
+ def close filename
150
+ return unless @current&.filename == filename
151
+
152
+ @current = nil
153
+ catalog unless workspace.has_file?(filename)
154
+ end
155
+
156
+ # Get completion suggestions at the specified file and location.
157
+ #
158
+ # @param filename [String] The file to analyze
159
+ # @param line [Integer] The zero-based line number
160
+ # @param column [Integer] The zero-based column number
161
+ # @return [SourceMap::Completion, nil]
162
+ # @todo Take a Location instead of filename/line/column
163
+ def completions_at filename, line, column
164
+ sync_catalog
165
+ position = Position.new(line, column)
166
+ cursor = Source::Cursor.new(read(filename), position)
167
+ mutex.synchronize { api_map.clip(cursor).complete }
168
+ rescue FileNotFoundError => e
169
+ handle_file_not_found filename, e
170
+ end
171
+
172
+ # Get definition suggestions for the expression at the specified file and
173
+ # location.
174
+ #
175
+ # @param filename [String] The file to analyze
176
+ # @param line [Integer] The zero-based line number
177
+ # @param column [Integer] The zero-based column number
178
+ # @return [Array<Solargraph::Pin::Base>, nil]
179
+ # @todo Take filename/position instead of filename/line/column
180
+ def definitions_at filename, line, column
181
+ sync_catalog
182
+ position = Position.new(line, column)
183
+ cursor = Source::Cursor.new(read(filename), position)
184
+ if cursor.comment?
185
+ source = read(filename)
186
+ offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column))
187
+ # @type [MatchData, nil]
188
+ lft = source.code[0..(offset - 1)]&.match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i)
189
+ # @type [MatchData, nil]
190
+ rgt = source.code[offset..]&.match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i)
191
+ if lft && rgt
192
+ # @sg-ignore lft and rgt are checked for nil above
193
+ tag = (lft[1] + rgt[1]).sub(/:+$/, '')
194
+ clip = mutex.synchronize { api_map.clip(cursor) }
195
+ clip.translate tag
196
+ else
197
+ []
198
+ end
199
+ else
200
+ mutex.synchronize do
201
+ clip = api_map.clip(cursor)
202
+ clip.define.map { |pin| pin.realize(api_map) }
203
+ end
204
+ end
205
+ rescue FileNotFoundError => e
206
+ handle_file_not_found(filename, e)
207
+ end
208
+
209
+ # Get type definition suggestions for the expression at the specified file and
210
+ # location.
211
+ #
212
+ # @param filename [String] The file to analyze
213
+ # @param line [Integer] The zero-based line number
214
+ # @param column [Integer] The zero-based column number
215
+ # @return [Array<Solargraph::Pin::Base>, nil]
216
+ # @todo Take filename/position instead of filename/line/column
217
+ def type_definitions_at filename, line, column
218
+ sync_catalog
219
+ position = Position.new(line, column)
220
+ cursor = Source::Cursor.new(read(filename), position)
221
+ mutex.synchronize { api_map.clip(cursor).types }
222
+ rescue FileNotFoundError => e
223
+ handle_file_not_found filename, e
224
+ end
225
+
226
+ # Get signature suggestions for the method at the specified file and
227
+ # location.
228
+ #
229
+ # @param filename [String] The file to analyze
230
+ # @param line [Integer] The zero-based line number
231
+ # @param column [Integer] The zero-based column number
232
+ # @return [Array<Solargraph::Pin::Base>]
233
+ # @todo Take filename/position instead of filename/line/column
234
+ def signatures_at filename, line, column
235
+ sync_catalog
236
+ position = Position.new(line, column)
237
+ cursor = Source::Cursor.new(read(filename), position)
238
+ mutex.synchronize { api_map.clip(cursor).signify }
239
+ end
240
+
241
+ # @param filename [String]
242
+ # @param line [Integer]
243
+ # @param column [Integer]
244
+ # @param strip [Boolean] Strip special characters from variable names
245
+ # @param only [Boolean] Search for references in the current file only
246
+ # @return [Array<Solargraph::Location>]
247
+ # @todo Take a Location instead of filename/line/column
248
+ def references_from filename, line, column, strip: false, only: false
249
+ sync_catalog
250
+ cursor = Source::Cursor.new(read(filename), [line, column])
251
+ clip = mutex.synchronize { api_map.clip(cursor) }
252
+ pin = clip.define.first
253
+ return [] unless pin
254
+
255
+ result = []
256
+ files = if only
257
+ [api_map.source_map(filename)]
258
+ else
259
+ (workspace.sources + (@current ? [@current] : []))
260
+ end
261
+ files.uniq(&:filename).each do |source|
262
+ found = source.references(pin.name)
263
+ found.select! do |loc|
264
+ referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character)&.first
265
+ referenced&.path == pin.path
266
+ end
267
+ if pin.path == 'Class#new'
268
+ caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first
269
+ if caller.defined?
270
+ found.select! do |loc|
271
+ clip = api_map.clip_at(loc.filename, loc.range.start)
272
+ other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first
273
+ caller == other
274
+ end
275
+ else
276
+ found.clear
277
+ end
278
+ end
279
+ # HACK: for language clients that exclude special characters from the start of variable names
280
+ if strip && (match = cursor.word.match(/^[^a-z0-9_]+/i))
281
+ found.map! do |loc|
282
+ # @sg-ignore Unresolved call to []
283
+ 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))
284
+ end
285
+ end
286
+ result.concat(found.sort do |a, b|
287
+ a.range.start.line <=> b.range.start.line
288
+ end)
289
+ end
290
+ result.uniq
291
+ end
292
+
293
+ # Get the pins at the specified location or nil if the pin does not exist.
294
+ #
295
+ # @param location [Location]
296
+ # @return [Array<Solargraph::Pin::Base>]
297
+ def locate_pins location
298
+ sync_catalog
299
+ mutex.synchronize { api_map.locate_pins(location).map { |pin| pin.realize(api_map) } }
300
+ end
301
+
302
+ # Match a require reference to a file.
303
+ #
304
+ # @param location [Location]
305
+ # @return [Location, nil]
306
+ def locate_ref location
307
+ map = source_map_hash[location.filename]
308
+ return if map.nil?
309
+ pin = map.requires.select { |p| p.location&.range&.contain?(location.range.start) }.first
310
+ return nil if pin.nil?
311
+ # @param full [String]
312
+ return_if_match = proc do |full|
313
+ if source_map_hash.key?(full)
314
+ return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0))
315
+ end
316
+ end
317
+ workspace.require_paths.each do |path|
318
+ full = File.join path, pin.name
319
+ return_if_match.call(full)
320
+ return_if_match.call(full << '.rb')
321
+ end
322
+ nil
323
+ rescue FileNotFoundError
324
+ nil
325
+ end
326
+
327
+ # Get an array of pins that match a path.
328
+ #
329
+ # @param path [String]
330
+ # @return [Enumerable<Solargraph::Pin::Base>]
331
+ def get_path_pins path
332
+ sync_catalog
333
+ mutex.synchronize { api_map.get_path_suggestions(path) }
334
+ end
335
+
336
+ # @param query [String]
337
+ # @return [Enumerable<YARD::CodeObjects::Base>]
338
+ # @return [Array(ApiMap, Enumerable<Pin::Base>)]
339
+ def document query
340
+ sync_catalog
341
+ mutex.synchronize { [api_map, api_map.get_path_pins(query)] }
342
+ end
343
+
344
+ # @param query [String]
345
+ # @return [Array<String>]
346
+ def search query
347
+ sync_catalog
348
+ mutex.synchronize { api_map.search query }
349
+ end
350
+
351
+ # Get an array of all symbols in the workspace that match the query.
352
+ #
353
+ # @param query [String]
354
+ # @return [Array<Pin::Base>]
355
+ def query_symbols query
356
+ sync_catalog
357
+ mutex.synchronize { api_map.query_symbols query }
358
+ end
359
+
360
+ # Get an array of document symbols.
361
+ #
362
+ # Document symbols are composed of namespace, method, and constant pins.
363
+ # The results of this query are appropriate for building the response to a
364
+ # textDocument/documentSymbol message in the language server protocol.
365
+ #
366
+ # @param filename [String]
367
+ # @return [Array<Solargraph::Pin::Base>]
368
+ def document_symbols filename
369
+ sync_catalog
370
+ mutex.synchronize { api_map.document_symbols(filename) }
371
+ end
372
+
373
+ # @param path [String]
374
+ # @return [Enumerable<Solargraph::Pin::Base>]
375
+ def path_pins path
376
+ sync_catalog
377
+ mutex.synchronize { api_map.get_path_suggestions(path) }
378
+ end
379
+
380
+ # @return [Array<SourceMap>]
381
+ def source_maps
382
+ source_map_hash.values
383
+ end
384
+
385
+ # Get the current text of a file in the library.
386
+ #
387
+ # @param filename [String]
388
+ # @return [String]
389
+ def read_text filename
390
+ source = read(filename)
391
+ source.code
392
+ end
393
+
394
+ # Get diagnostics about a file.
395
+ #
396
+ # @param filename [String]
397
+ # @return [Array<Hash>]
398
+ def diagnose filename
399
+ # @todo Only open files get diagnosed. Determine whether anything or
400
+ # everything in the workspace should get diagnosed, or if there should
401
+ # be an option to do so.
402
+ #
403
+ sync_catalog
404
+ return [] unless open?(filename)
405
+ result = []
406
+ source = read(filename)
407
+
408
+ # @type [Hash{Class<Solargraph::Diagnostics::Base> => Array<String>}]
409
+ repargs = {}
410
+ workspace.config.reporters.each do |line|
411
+ if line == 'all!'
412
+ Diagnostics.reporters.each do |reporter_name|
413
+ r = Diagnostics.reporter(reporter_name)
414
+ repargs[r] ||= [] if r
415
+ end
416
+ else
417
+ args = line.split(':').map(&:strip)
418
+ name = args.shift
419
+ reporter = Diagnostics.reporter(name)
420
+ raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil?
421
+ # @sg-ignore Hash errors
422
+ repargs[reporter] ||= []
423
+ # @sg-ignore Hash errors
424
+ repargs[reporter].concat args
425
+ end
426
+ end
427
+ repargs.each_pair do |reporter, args|
428
+ result.concat reporter.new(*args.uniq).diagnose(source, api_map)
429
+ end
430
+ result
431
+ end
432
+
433
+ # Update the ApiMap from the library's workspace and open files.
434
+ #
435
+ # @return [void]
436
+ def catalog
437
+ @sync_count += 1
438
+ end
439
+
440
+ # @return [Bench]
441
+ def bench
442
+ Bench.new(
443
+ source_maps: source_map_hash.values,
444
+ workspace: workspace,
445
+ external_requires: external_requires,
446
+ # @sg-ignore OK if @current.filename is nil
447
+ live_map: @current ? source_map_hash[@current.filename] : nil
448
+ )
449
+ end
450
+
451
+ # Create a library from a directory.
452
+ #
453
+ # @param directory [String] The path to be used for the workspace
454
+ # @param name [String, nil]
455
+ # @return [Solargraph::Library]
456
+ def self.load directory = '', name = nil
457
+ Solargraph::Library.new(Solargraph::Workspace.new(directory), name)
458
+ end
459
+
460
+ # Try to merge a source into the library's workspace. If the workspace is
461
+ # not configured to include the source, it gets ignored.
462
+ #
463
+ # @param source [Source]
464
+ # @return [Boolean] True if the source was merged into the workspace.
465
+ def merge source
466
+ result = workspace.merge(source)
467
+ maybe_map source
468
+ result
469
+ end
470
+
471
+ # @return [Hash{String => SourceMap}]
472
+ def source_map_hash
473
+ @source_map_hash ||= {}
474
+ end
475
+
476
+ def mapped?
477
+ (workspace.filenames - source_map_hash.keys).empty?
478
+ end
479
+
480
+ # @return [SourceMap, Boolean]
481
+ def next_map
482
+ return false if mapped?
483
+ src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
484
+ if src
485
+ Logging.logger.debug "Mapping #{src.filename}"
486
+ # @sg-ignore OK if src.filename is nil
487
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
488
+ # @sg-ignore OK if src.filename is nil
489
+ source_map_hash[src.filename]
490
+ else
491
+ false
492
+ end
493
+ end
494
+
495
+ # @return [self]
496
+ def map!
497
+ workspace.sources.each do |src|
498
+ # @sg-ignore OK if src.filename is nil
499
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
500
+ # @sg-ignore OK if src.filename is nil
501
+ find_external_requires source_map_hash[src.filename]
502
+ end
503
+ self
504
+ end
505
+
506
+ # @return [Array<Solargraph::Pin::Base>]
507
+ def pins
508
+ @pins ||= []
509
+ end
510
+
511
+ # @return [Set<String>]
512
+ def external_requires
513
+ @external_requires ||= source_map_external_require_hash.values.flatten.to_set
514
+ end
515
+
516
+ private
517
+
518
+ # @return [Hash{String => Array<String>}]
519
+ def source_map_external_require_hash
520
+ @source_map_external_require_hash ||= {}
521
+ end
522
+
523
+ # @param source_map [SourceMap]
524
+ # @return [void]
525
+ def find_external_requires source_map
526
+ # @type [Set<String>]
527
+ new_set = source_map.requires.to_set(&:name)
528
+ _filenames = nil
529
+ filenames = -> { _filenames ||= workspace.filenames.to_set }
530
+ # @sg-ignore OK if source_map.filename is nil
531
+ source_map_external_require_hash[source_map.filename] = new_set.reject do |path|
532
+ workspace.require_paths.any? do |base|
533
+ full = File.join(base, path)
534
+ filenames[].include?(full) or filenames[].include?(full << '.rb')
535
+ end
536
+ end
537
+ @external_requires = nil
538
+ end
539
+
540
+ # @return [Thread::Mutex]
541
+ def mutex
542
+ @mutex ||= Mutex.new
543
+ end
544
+
545
+ # @return [ApiMap]
546
+ def api_map
547
+ @api_map ||= Solargraph::ApiMap.new
548
+ end
549
+
550
+ # Get the source for an open file or create a new source if the file
551
+ # exists on disk. Sources created from disk are not added to the open
552
+ # workspace files, i.e., the version on disk remains the authoritative
553
+ # version.
554
+ #
555
+ # @raise [FileNotFoundError] if the file does not exist
556
+ # @param filename [String]
557
+ # @return [Solargraph::Source]
558
+ def read filename
559
+ return @current if @current && @current.filename == filename
560
+ raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename)
561
+ workspace.source(filename)
562
+ end
563
+
564
+ # @param filename [String]
565
+ # @param error [FileNotFoundError]
566
+ # @return [nil]
567
+ def handle_file_not_found filename, error
568
+ raise error unless workspace.source(filename)
569
+ Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap"
570
+ nil
571
+ end
572
+
573
+ # @param source [Source, nil]
574
+ # @return [void]
575
+ def maybe_map source
576
+ return unless source
577
+ # @sg-ignore Wrong argument type for Solargraph::Workspace#has_file?: filename expected String, received String, nil
578
+ return unless @current == source || workspace.has_file?(source.filename)
579
+ if source_map_hash.key?(source.filename)
580
+ new_map = Solargraph::SourceMap.map(source)
581
+ # @sg-ignore OK if source.filename is nil
582
+ source_map_hash[source.filename] = new_map
583
+ else
584
+ # @sg-ignore OK if source.filename is nil
585
+ source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
586
+ end
587
+ end
588
+
589
+ # @return [Set<Gem::Specification>]
590
+ def cache_errors
591
+ @cache_errors ||= Set.new
592
+ end
593
+
594
+ # @return [void]
595
+ def cache_next_gemspec
596
+ return if @cache_progress
597
+
598
+ spec = cacheable_specs.first
599
+ return end_cache_progress unless spec
600
+
601
+ pending = api_map.uncached_gemspecs.length - cache_errors.length - 1
602
+
603
+ if Yardoc.processing?(spec)
604
+ logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)"
605
+ queued_gemspec_cache.push(spec)
606
+ return if pending - queued_gemspec_cache.length < 1
607
+
608
+ catalog
609
+ sync_catalog
610
+ else
611
+ logger.info "Caching #{spec.name} #{spec.version}"
612
+ Thread.new do
613
+ report_cache_progress spec.name, pending
614
+ _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s)
615
+ if s.success?
616
+ logger.info "Cached #{spec.name} #{spec.version}"
617
+ else
618
+ cache_errors.add spec
619
+ logger.warn "Error caching gemspec #{spec.name} #{spec.version}"
620
+ logger.warn e
621
+ end
622
+ end_cache_progress
623
+ catalog
624
+ sync_catalog
625
+ end
626
+ end
627
+ end
628
+
629
+ # @return [Array<Gem::Specification>]
630
+ def cacheable_specs
631
+ cacheable = api_map.uncached_yard_gemspecs +
632
+ api_map.uncached_rbs_collection_gemspecs -
633
+ queued_gemspec_cache -
634
+ cache_errors.to_a
635
+ return cacheable unless cacheable.empty?
636
+
637
+ queued_gemspec_cache
638
+ end
639
+
640
+ # @return [Array<Gem::Specification>]
641
+ def queued_gemspec_cache
642
+ @queued_gemspec_cache ||= []
643
+ end
644
+
645
+ # @param gem_name [String]
646
+ # @param pending [Integer]
647
+ # @return [void]
648
+ def report_cache_progress gem_name, pending
649
+ @total ||= pending
650
+ # @sg-ignore Wrong argument type for Integer#>: arg_0 expected Numeric, received Integer, nil
651
+ @total = pending if pending > @total
652
+ # @sg-ignore Unresolved call to - on Integer, nil
653
+ finished = @total - pending
654
+ # @sg-ignore @total should always be an Integer
655
+ pct = if @total.zero?
656
+ 0
657
+ else
658
+ # @sg-ignore Unresolved call to to_f
659
+ ((finished.to_f / @total) * 100).to_i
660
+ end
661
+ message = "#{gem_name}#{" (+#{pending})" if pending.positive?}"
662
+ # "
663
+ unless @cache_progress
664
+ @cache_progress = LanguageServer::Progress.new('Caching gem')
665
+ # If we don't send both a begin and a report, the progress notification
666
+ # might get stuck in the status bar forever
667
+ # @sg-ignore Unresolved call to begin
668
+ @cache_progress.begin(message, pct)
669
+ changed
670
+ notify_observers @cache_progress
671
+ end
672
+ # @sg-ignore Unresolved call to report
673
+ @cache_progress.report(message, pct)
674
+ changed
675
+ notify_observers @cache_progress
676
+ end
677
+
678
+ # @return [void]
679
+ def end_cache_progress
680
+ changed if @cache_progress&.finish('done')
681
+ notify_observers @cache_progress
682
+ @cache_progress = nil
683
+ @total = nil
684
+ end
685
+
686
+ # @return [void]
687
+ def sync_catalog
688
+ return if @sync_count.zero?
689
+
690
+ mutex.synchronize do
691
+ logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
692
+ source_map_hash.each_value { |map| find_external_requires(map) }
693
+ api_map.catalog bench
694
+ logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"
695
+ logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs"
696
+ logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs"
697
+ cache_next_gemspec
698
+ @sync_count = 0
699
+ end
700
+ end
701
+ end
702
+ end