solargraph 0.40.4 → 0.42.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -5
  3. data/CHANGELOG.md +32 -0
  4. data/README.md +15 -0
  5. data/SPONSORS.md +1 -0
  6. data/lib/.rubocop.yml +4 -3
  7. data/lib/solargraph.rb +8 -7
  8. data/lib/solargraph/api_map.rb +40 -111
  9. data/lib/solargraph/bench.rb +13 -16
  10. data/lib/solargraph/compat.rb +15 -1
  11. data/lib/solargraph/diagnostics/rubocop.rb +10 -2
  12. data/lib/solargraph/diagnostics/rubocop_helpers.rb +18 -0
  13. data/lib/solargraph/diagnostics/type_check.rb +1 -1
  14. data/lib/solargraph/language_server/host.rb +108 -7
  15. data/lib/solargraph/language_server/host/diagnoser.rb +9 -1
  16. data/lib/solargraph/language_server/host/sources.rb +1 -1
  17. data/lib/solargraph/language_server/message/completion_item/resolve.rb +1 -0
  18. data/lib/solargraph/language_server/message/extended/environment.rb +3 -3
  19. data/lib/solargraph/language_server/message/initialize.rb +37 -35
  20. data/lib/solargraph/language_server/message/text_document/formatting.rb +28 -7
  21. data/lib/solargraph/language_server/message/text_document/hover.rb +1 -1
  22. data/lib/solargraph/library.rb +132 -22
  23. data/lib/solargraph/parser/rubyvm/node_chainer.rb +0 -1
  24. data/lib/solargraph/parser/rubyvm/node_processors/args_node.rb +0 -1
  25. data/lib/solargraph/shell.rb +5 -1
  26. data/lib/solargraph/source/chain/head.rb +0 -16
  27. data/lib/solargraph/source/source_chainer.rb +2 -1
  28. data/lib/solargraph/source_map/mapper.rb +0 -5
  29. data/lib/solargraph/type_checker/checks.rb +4 -4
  30. data/lib/solargraph/version.rb +1 -1
  31. data/lib/solargraph/workspace.rb +1 -0
  32. data/lib/solargraph/workspace/config.rb +4 -3
  33. data/lib/solargraph/yard_map.rb +41 -39
  34. data/lib/solargraph/yard_map/core_fills.rb +1 -0
  35. data/solargraph.gemspec +1 -0
  36. metadata +16 -2
@@ -17,7 +17,7 @@ module Solargraph
17
17
  if !this_link.nil? && this_link != last_link
18
18
  parts.push this_link
19
19
  end
20
- parts.push pin.detail.gsub(':', '\\:') unless pin.is_a?(Pin::Namespace) || pin.detail.nil?
20
+ parts.push "`#{pin.detail}`" unless pin.is_a?(Pin::Namespace) || pin.detail.nil?
21
21
  parts.push pin.documentation unless pin.documentation.nil? || pin.documentation.empty?
22
22
  unless parts.empty?
23
23
  data = parts.join("\n\n")
@@ -20,9 +20,7 @@ module Solargraph
20
20
  def initialize workspace = Solargraph::Workspace.new, name = nil
21
21
  @workspace = workspace
22
22
  @name = name
23
- api_map.catalog bench
24
- @synchronized = true
25
- @catalog_mutex = Mutex.new
23
+ @synchronized = false
26
24
  end
27
25
 
28
26
  def inspect
@@ -48,9 +46,15 @@ module Solargraph
48
46
  # @return [void]
49
47
  def attach source
50
48
  mutex.synchronize do
51
- @synchronized = (@current == source) if synchronized?
49
+ if @current && @current.filename != source.filename && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename)
50
+ source_map_hash.delete @current.filename
51
+ source_map_external_require_hash.delete @current.filename
52
+ @external_requires = nil
53
+ @synchronized = false
54
+ end
52
55
  @current = source
53
- catalog
56
+ maybe_map @current
57
+ api_map.catalog bench unless synchronized?
54
58
  end
55
59
  end
56
60
 
@@ -110,9 +114,9 @@ module Solargraph
110
114
  mutex.synchronize do
111
115
  next if File.directory?(filename) || !File.exist?(filename)
112
116
  next unless contain?(filename) || open?(filename) || workspace.would_merge?(filename)
113
- @synchronized = false
114
117
  source = Solargraph::Source.load_string(File.read(filename), filename)
115
118
  workspace.merge(source)
119
+ maybe_map source
116
120
  result = true
117
121
  end
118
122
  result
@@ -158,6 +162,8 @@ module Solargraph
158
162
  position = Position.new(line, column)
159
163
  cursor = Source::Cursor.new(read(filename), position)
160
164
  api_map.clip(cursor).complete
165
+ rescue FileNotFoundError => e
166
+ handle_file_not_found filename, e
161
167
  end
162
168
 
163
169
  # Get definition suggestions for the expression at the specified file and
@@ -186,6 +192,8 @@ module Solargraph
186
192
  else
187
193
  api_map.clip(cursor).define.map { |pin| pin.realize(api_map) }
188
194
  end
195
+ rescue FileNotFoundError => e
196
+ handle_file_not_found(filename, e)
189
197
  end
190
198
 
191
199
  # Get signature suggestions for the method at the specified file and
@@ -246,7 +254,18 @@ module Solargraph
246
254
  end
247
255
 
248
256
  def locate_ref location
249
- api_map.require_reference_at location
257
+ map = source_map_hash[location.filename]
258
+ return if map.nil?
259
+ pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first
260
+ return nil if pin.nil?
261
+ workspace.require_paths.each do |path|
262
+ full = Pathname.new(path).join("#{pin.name}.rb").to_s
263
+ next unless source_map_hash.key?(full)
264
+ return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0))
265
+ end
266
+ api_map.yard_map.require_reference(pin.name)
267
+ rescue FileNotFoundError
268
+ nil
250
269
  end
251
270
 
252
271
  # Get an array of pins that match a path.
@@ -260,14 +279,12 @@ module Solargraph
260
279
  # @param query [String]
261
280
  # @return [Array<YARD::CodeObjects::Base>]
262
281
  def document query
263
- catalog
264
282
  api_map.document query
265
283
  end
266
284
 
267
285
  # @param query [String]
268
286
  # @return [Array<String>]
269
287
  def search query
270
- catalog
271
288
  api_map.search query
272
289
  end
273
290
 
@@ -276,7 +293,6 @@ module Solargraph
276
293
  # @param query [String]
277
294
  # @return [Array<Pin::Base>]
278
295
  def query_symbols query
279
- catalog
280
296
  api_map.query_symbols query
281
297
  end
282
298
 
@@ -295,10 +311,13 @@ module Solargraph
295
311
  # @param path [String]
296
312
  # @return [Array<Solargraph::Pin::Base>]
297
313
  def path_pins path
298
- catalog
299
314
  api_map.get_path_suggestions(path)
300
315
  end
301
316
 
317
+ def source_maps
318
+ source_map_hash.values
319
+ end
320
+
302
321
  # Get the current text of a file in the library.
303
322
  #
304
323
  # @param filename [String]
@@ -318,9 +337,9 @@ module Solargraph
318
337
  # be an option to do so.
319
338
  #
320
339
  return [] unless open?(filename)
321
- catalog
322
340
  result = []
323
341
  source = read(filename)
342
+ catalog
324
343
  repargs = {}
325
344
  workspace.config.reporters.each do |line|
326
345
  if line == 'all!'
@@ -346,7 +365,7 @@ module Solargraph
346
365
  #
347
366
  # @return [void]
348
367
  def catalog
349
- @catalog_mutex.synchronize do
368
+ mutex.synchronize do
350
369
  break if synchronized?
351
370
  logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
352
371
  api_map.catalog bench
@@ -355,6 +374,14 @@ module Solargraph
355
374
  end
356
375
  end
357
376
 
377
+ def bench
378
+ Bench.new(
379
+ source_maps: source_map_hash.values,
380
+ workspace: workspace,
381
+ external_requires: external_requires
382
+ )
383
+ end
384
+
358
385
  # Get an array of foldable ranges for the specified file.
359
386
  #
360
387
  # @deprecated The library should not need to handle folding ranges. The
@@ -381,16 +408,75 @@ module Solargraph
381
408
  # @param source [Source]
382
409
  # @return [Boolean] True if the source was merged into the workspace.
383
410
  def merge source
411
+ Logging.logger.debug "Merging source: #{source.filename}"
384
412
  result = false
385
413
  mutex.synchronize do
386
414
  result = workspace.merge(source)
387
- @synchronized = !result if synchronized?
415
+ maybe_map source
388
416
  end
417
+ # catalog
389
418
  result
390
419
  end
391
420
 
421
+ def source_map_hash
422
+ @source_map_hash ||= {}
423
+ end
424
+
425
+ def mapped?
426
+ (workspace.filenames - source_map_hash.keys).empty?
427
+ end
428
+
429
+ def next_map
430
+ return false if mapped?
431
+ mutex.synchronize do
432
+ @synchronized = false
433
+ src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
434
+ if src
435
+ Logging.logger.debug "Mapping #{src.filename}"
436
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
437
+ find_external_requires(source_map_hash[src.filename])
438
+ source_map_hash[src.filename]
439
+ else
440
+ false
441
+ end
442
+ end
443
+ end
444
+
445
+ def map!
446
+ workspace.sources.each do |src|
447
+ source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
448
+ find_external_requires(source_map_hash[src.filename])
449
+ end
450
+ self
451
+ end
452
+
453
+ def pins
454
+ @pins ||= []
455
+ end
456
+
457
+ def external_requires
458
+ @external_requires ||= source_map_external_require_hash.values.flatten.to_set
459
+ end
460
+
392
461
  private
393
462
 
463
+ def source_map_external_require_hash
464
+ @source_map_external_require_hash ||= {}
465
+ end
466
+
467
+ # @param source_map [SourceMap]
468
+ def find_external_requires source_map
469
+ new_set = source_map.requires.map(&:name).to_set
470
+ # return if new_set == source_map_external_require_hash[source_map.filename]
471
+ source_map_external_require_hash[source_map.filename] = new_set.reject do |path|
472
+ workspace.require_paths.any? do |base|
473
+ full = Pathname.new(base).join("#{path}.rb").to_s
474
+ workspace.filenames.include?(full)
475
+ end
476
+ end
477
+ @external_requires = nil
478
+ end
479
+
394
480
  # @return [Mutex]
395
481
  def mutex
396
482
  @mutex ||= Mutex.new
@@ -401,14 +487,6 @@ module Solargraph
401
487
  @api_map ||= Solargraph::ApiMap.new
402
488
  end
403
489
 
404
- # @return [Bench]
405
- def bench
406
- Bench.new(
407
- workspace: workspace,
408
- opened: @current ? [@current] : []
409
- )
410
- end
411
-
412
490
  # Get the source for an open file or create a new source if the file
413
491
  # exists on disk. Sources created from disk are not added to the open
414
492
  # workspace files, i.e., the version on disk remains the authoritative
@@ -422,5 +500,37 @@ module Solargraph
422
500
  raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename)
423
501
  workspace.source(filename)
424
502
  end
503
+
504
+ def handle_file_not_found filename, error
505
+ if workspace.source(filename)
506
+ Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap"
507
+ nil
508
+ else
509
+ raise error
510
+ end
511
+ end
512
+
513
+ def maybe_map source
514
+ if source_map_hash.key?(source.filename)
515
+ return if source_map_hash[source.filename].code == source.code &&
516
+ source_map_hash[source.filename].source.synchronized? &&
517
+ source.synchronized?
518
+ if source.synchronized?
519
+ new_map = Solargraph::SourceMap.map(source)
520
+ unless source_map_hash[source.filename].try_merge!(new_map)
521
+ source_map_hash[source.filename] = new_map
522
+ find_external_requires(source_map_hash[source.filename])
523
+ @synchronized = false
524
+ end
525
+ else
526
+ # @todo Smelly instance variable access
527
+ source_map_hash[source.filename].instance_variable_set(:@source, source)
528
+ end
529
+ else
530
+ source_map_hash[source.filename] = Solargraph::SourceMap.map(source)
531
+ find_external_requires(source_map_hash[source.filename])
532
+ @synchronized = false
533
+ end
534
+ end
425
535
  end
426
536
  end
@@ -109,7 +109,6 @@ module Solargraph
109
109
  end
110
110
 
111
111
  def node_to_argchains node
112
- # @todo Process array, splat, argscat
113
112
  return [] unless Parser.is_ast_node?(node)
114
113
  if [:ZARRAY, :ARRAY, :LIST].include?(node.type)
115
114
  node.children[0..-2].map { |c| NodeChainer.chain(c) }
@@ -32,7 +32,6 @@ module Solargraph
32
32
  region.closure.parameters.push locals.last
33
33
  end
34
34
  end
35
- # @todo Optional args, keyword args, etc.
36
35
  if node.children[6]
37
36
  locals.push Solargraph::Pin::Parameter.new(
38
37
  location: region.closure.location,
@@ -74,7 +74,11 @@ module Solargraph
74
74
  desc 'download-core [VERSION]', 'Download core documentation'
75
75
  def download_core version = nil
76
76
  ver = version || Solargraph::YardMap::CoreDocs.best_download
77
- puts "Downloading docs for #{ver}..."
77
+ if RUBY_VERSION != ver
78
+ puts "Documentation for #{RUBY_VERSION} is not available. Reverting to closest match..."
79
+ else
80
+ puts "Downloading docs for #{ver}..."
81
+ end
78
82
  Solargraph::YardMap::CoreDocs.download ver
79
83
  # Clear cached documentation if it exists
80
84
  FileUtils.rm_rf Dir.glob(File.join(Solargraph::YardMap::CoreDocs.cache_dir, ver, '*.ser'))
@@ -13,22 +13,6 @@ module Solargraph
13
13
  # return super_pins(api_map, name_pin) if word == 'super'
14
14
  []
15
15
  end
16
-
17
- # @todo This is temporary. Chain heads need to handle arguments to
18
- # `super`.
19
- # def arguments
20
- # []
21
- # end
22
-
23
- private
24
-
25
- # # @param api_map [ApiMap]
26
- # # @param name_pin [Pin::Base]
27
- # # @return [Array<Pin::Base>]
28
- # def super_pins api_map, name_pin
29
- # pins = api_map.get_method_stack(name_pin.namespace, name_pin.name, scope: name_pin.scope)
30
- # pins.reject{|p| p.path == name_pin.path}
31
- # end
32
16
  end
33
17
  end
34
18
  end
@@ -34,6 +34,7 @@ module Solargraph
34
34
  # Special handling for files that end with an integer and a period
35
35
  return Chain.new([Chain::Literal.new('Integer'), Chain::UNDEFINED_CALL]) if phrase =~ /^[0-9]+\.$/
36
36
  return Chain.new([Chain::Literal.new('Symbol')]) if phrase.start_with?(':') && !phrase.start_with?('::')
37
+ return SourceChainer.chain(source, Position.new(position.line, position.character + 1)) if end_of_phrase.strip == '::' && source.code[Position.to_offset(source.code, position)].to_s.match?(/[a-z]/i)
37
38
  begin
38
39
  return Chain.new([]) if phrase.end_with?('..')
39
40
  node = nil
@@ -64,7 +65,7 @@ module Solargraph
64
65
  elsif end_of_phrase.strip == '::'
65
66
  chain.links.push Chain::UNDEFINED_CONSTANT
66
67
  end
67
- elsif chain.links.last.is_a?(Source::Chain::Constant) && end_of_phrase.strip == '::' && !source.code[Position.to_offset(source.code, position)].match?(/[a-z]/i)
68
+ elsif chain.links.last.is_a?(Source::Chain::Constant) && end_of_phrase.strip == '::'
68
69
  chain.links.push Source::Chain::UNDEFINED_CONSTANT
69
70
  end
70
71
  chain
@@ -71,11 +71,6 @@ module Solargraph
71
71
  pos = Solargraph::Position.new(comment_position.line + line_num - 1, comment_position.column)
72
72
  process_directive(source_position, pos, d)
73
73
  last_line = line_num + 1
74
- # @todo The below call assumes the topmost comment line. The above
75
- # process occasionally emits incorrect comment positions due to
76
- # blank lines in comment blocks, but at least it processes all the
77
- # directives.
78
- # process_directive(source_position, comment_position, d)
79
74
  end
80
75
  end
81
76
 
@@ -75,7 +75,7 @@ module Solargraph
75
75
  true
76
76
  end
77
77
 
78
- # @param type [ComplexType]
78
+ # @param type [ComplexType::UniqueType]
79
79
  # @return [String]
80
80
  def fuzz type
81
81
  if type.parameters?
@@ -86,13 +86,13 @@ module Solargraph
86
86
  end
87
87
 
88
88
  # @param api_map [ApiMap]
89
- # @param cls1 [ComplexType]
90
- # @param cls2 [ComplexType]
89
+ # @param cls1 [ComplexType::UniqueType]
90
+ # @param cls2 [ComplexType::UniqueType]
91
91
  # @return [Boolean]
92
92
  def either_way?(api_map, cls1, cls2)
93
93
  f1 = fuzz(cls1)
94
94
  f2 = fuzz(cls2)
95
- api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1)
95
+ api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1)
96
96
  end
97
97
  end
98
98
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- VERSION = '0.40.4'
4
+ VERSION = '0.42.1'
5
5
  end
@@ -18,6 +18,7 @@ module Solargraph
18
18
 
19
19
  # @return [Array<String>]
20
20
  attr_reader :gemnames
21
+ alias source_gems gemnames
21
22
 
22
23
  # @param directory [String]
23
24
  # @param config [Config, nil]
@@ -170,9 +170,10 @@ module Solargraph
170
170
  # @param globs [Array<String>]
171
171
  # @return [Array<String>]
172
172
  def process_globs globs
173
- result = []
174
- globs.each do |glob|
175
- result.concat Dir[File.join directory, glob].map{ |f| f.gsub(/\\/, '/') }
173
+ result = globs.flat_map do |glob|
174
+ Dir[File.join directory, glob]
175
+ .map{ |f| f.gsub(/\\/, '/') }
176
+ .select { |f| File.file?(f) }
176
177
  end
177
178
  result
178
179
  end
@@ -20,6 +20,8 @@ module Solargraph
20
20
  autoload :Helpers, 'solargraph/yard_map/helpers'
21
21
  autoload :ToMethod, 'solargraph/yard_map/to_method'
22
22
 
23
+ include ApiMap::BundlerMethods
24
+
23
25
  CoreDocs.require_minimum
24
26
 
25
27
  def stdlib_paths
@@ -37,33 +39,16 @@ module Solargraph
37
39
  end
38
40
  end
39
41
 
40
- # @return [Array<String>]
41
- attr_reader :required
42
-
43
42
  # @return [Boolean]
44
43
  attr_writer :with_dependencies
45
44
 
46
- # A hash of gem names and the version numbers to include in the map.
47
- #
48
- # @return [Hash{String => String}]
49
- attr_reader :gemset
50
-
51
- # @param required [Array<String>]
52
- # @param gemset [Hash{String => String}]
45
+ # @param required [Array<String>, Set<String>]
46
+ # @param directory [String]
47
+ # @param source_gems [Array<String>, Set<String>]
53
48
  # @param with_dependencies [Boolean]
54
- def initialize(required: [], gemset: {}, with_dependencies: true)
55
- # HACK: YardMap needs its own copy of this array
56
- @required = required.clone
57
- # HACK: Hardcoded YAML handling
58
- @required.push 'psych' if @required.include?('yaml')
49
+ def initialize(required: [], directory: '', source_gems: [], with_dependencies: true)
59
50
  @with_dependencies = with_dependencies
60
- @gem_paths = {}
61
- @stdlib_namespaces = []
62
- @gemset = gemset
63
- @source_gems = []
64
- process_requires
65
- yardocs.uniq!
66
- @pin_select_cache = {}
51
+ change required.to_set, directory, source_gems.to_set
67
52
  end
68
53
 
69
54
  # @return [Array<Solargraph::Pin::Base>]
@@ -76,25 +61,24 @@ module Solargraph
76
61
  @with_dependencies
77
62
  end
78
63
 
79
- # @param new_requires [Array<String>]
80
- # @param new_gemset [Hash{String => String}]
64
+ # @param new_requires [Set<String>] Required paths to use for loading gems
65
+ # @param new_directory [String] The workspace directory
66
+ # @param new_source_gems [Set<String>] Gems under local development (i.e., part of the workspace)
81
67
  # @return [Boolean]
82
- def change new_requires, new_gemset, source_gems = []
68
+ def change new_requires, new_directory, new_source_gems
69
+ return false if new_requires == base_required && new_directory == @directory && new_source_gems == @source_gems
70
+ @gem_paths = {}
71
+ base_required.replace new_requires
72
+ required.replace new_requires
83
73
  # HACK: Hardcoded YAML handling
84
- new_requires.push 'psych' if new_requires.include?('yaml')
85
- if new_requires.uniq.sort == required.uniq.sort && new_gemset == gemset && @source_gems.uniq.sort == source_gems.uniq.sort
86
- false
87
- else
88
- required.clear
89
- required.concat new_requires
90
- @gemset = new_gemset
91
- @source_gems = source_gems
92
- process_requires
93
- @rebindable_method_names = nil
94
- @pin_class_hash = nil
95
- @pin_select_cache = {}
96
- true
97
- end
74
+ required.add 'psych' if new_requires.include?('yaml')
75
+ @source_gems = new_source_gems
76
+ @directory = new_directory
77
+ process_requires
78
+ @rebindable_method_names = nil
79
+ @pin_class_hash = nil
80
+ @pin_select_cache = {}
81
+ true
98
82
  end
99
83
 
100
84
  # @return [Set<String>]
@@ -111,6 +95,11 @@ module Solargraph
111
95
  @yardocs ||= []
112
96
  end
113
97
 
98
+ # @return [Set<String>]
99
+ def required
100
+ @required ||= Set.new
101
+ end
102
+
114
103
  # @return [Array<String>]
115
104
  def unresolved_requires
116
105
  @unresolved_requires ||= []
@@ -163,8 +152,16 @@ module Solargraph
163
152
  @stdlib_pins ||= []
164
153
  end
165
154
 
155
+ def base_required
156
+ @base_required ||= Set.new
157
+ end
158
+
166
159
  private
167
160
 
161
+ def directory
162
+ @directory ||= ''
163
+ end
164
+
168
165
  # @return [YardMap::Cache]
169
166
  def cache
170
167
  @cache ||= YardMap::Cache.new
@@ -193,6 +190,11 @@ module Solargraph
193
190
 
194
191
  # @return [void]
195
192
  def process_requires
193
+ @gemset = if required.include?('bundler/require')
194
+ require_from_bundle(directory)
195
+ else
196
+ {}
197
+ end
196
198
  pins.replace core_pins
197
199
  unresolved_requires.clear
198
200
  stdlib_pins.clear