solargraph 0.23.6 → 0.24.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5b6ac3cd79425340ddf486c9b3447115553a374a3c782d8c8a103825b4461b9
4
- data.tar.gz: 4561568bb8075f955777c2a17a2a19a91c14b198df6c8f5798168b01abdeac19
3
+ metadata.gz: 102d3ab02b96835a5e3efc7a5ec5f07b2bd53c94660d16905f6c7264e6ac520a
4
+ data.tar.gz: 29aab3c067c6e9e95eeb5cf108ae5f71560f325aa02612bb69957840706e9d43
5
5
  SHA512:
6
- metadata.gz: 489953a5c791e8475b217b4033a502196a1d0aa0112f5d778ce2dba9ed488e773b867fb596ca02a1f81a6143b426d54007ab0448f979d1c9ad98344034fa96a1
7
- data.tar.gz: 390e4c2bf98f7733232f551749114229afb6e4f7271ac1792e2034cd223de6e0b636ab001281ad225b7f8267b0a8e1e48e927be0a88bab63a4ded3c7779e3bc3
6
+ metadata.gz: eed6ec2beee0afb8bcb8fc976f1d1487b5d79cd44acc1c27c89751f896da3ede2f8ec33da4d14278ea597e3308c3770cf6952d57dabf19de67d759b0a1464c1a
7
+ data.tar.gz: c7e7b79cf903a07e1237598a3d0f645aff86602128703af72531ee12d8bb7c1bf85a9445a097aec9f90ac8bd5dab9a9fcb24aa95fc6935250f24d017e8b85872
@@ -36,6 +36,7 @@ module Solargraph
36
36
  autoload :Page, 'solargraph/page'
37
37
  autoload :Library, 'solargraph/library'
38
38
  autoload :Diagnostics, 'solargraph/diagnostics'
39
+ autoload :ComplexType, 'solargraph/complex_type'
39
40
 
40
41
  YARDOC_PATH = File.join(File.realpath(File.dirname(__FILE__)), '..', 'yardoc')
41
42
  YARD_EXTENSION_FILE = File.join(File.realpath(File.dirname(__FILE__)), 'yard-solargraph.rb')
@@ -3,6 +3,9 @@ require 'set'
3
3
  require 'time'
4
4
 
5
5
  module Solargraph
6
+ # An aggregate provider for information about workspaces, sources, gems, and
7
+ # the Ruby core.
8
+ #
6
9
  class ApiMap
7
10
  autoload :Cache, 'solargraph/api_map/cache'
8
11
  autoload :SourceToYard, 'solargraph/api_map/source_to_yard'
@@ -120,13 +123,15 @@ module Solargraph
120
123
  virtualize source
121
124
  end
122
125
 
123
- # Refresh the ApiMap.
126
+ # Refresh the ApiMap. This method checks for pending changes before
127
+ # performing the refresh unless the `force` parameter is true.
124
128
  #
125
129
  # @param force [Boolean] Perform a refresh even if the map is not "stale."
130
+ # @return [Boolean] True if a refresh was performed.
126
131
  def refresh force = false
127
- return unless @force or changed?
132
+ return false unless force or changed?
128
133
  if force
129
- @api_map = ApiMap::Store.new(@sources)
134
+ refresh_store_and_maps
130
135
  else
131
136
  store.remove *(current_workspace_sources.reject{ |s| workspace.sources.include?(s) })
132
137
  @sources = workspace.sources
@@ -134,6 +139,7 @@ module Solargraph
134
139
  store.update *(@sources.select{ |s| @stime.nil? or s.stime > @stime })
135
140
  end
136
141
  @stime = Time.new
142
+ true
137
143
  end
138
144
 
139
145
  # True if a workspace file has been created, modified, or deleted since
@@ -319,10 +325,26 @@ module Solargraph
319
325
  elsif fragment.signature.include?('::') and !fragment.signature.include?('.')
320
326
  result.concat get_constants(fragment.base, fragment.namespace)
321
327
  else
322
- type = probe.infer_signature_type(fragment.base, fragment.named_path, fragment.locals)
323
- unless type.nil?
324
- namespace, scope = extract_namespace_and_scope(type)
325
- result.concat get_methods(namespace, scope: scope)
328
+ pins = probe.infer_signature_pins(fragment.base, fragment.named_path, fragment.locals)
329
+ unless pins.empty?
330
+ pin = pins.first
331
+ if pin.return_complex_types.any? and pin.return_complex_types.first.duck_type?
332
+ pin.return_complex_types.each do |t|
333
+ next unless t.duck_type?
334
+ result.push Pin::DuckMethod.new(pin.location, t.tag[1..-1])
335
+ end
336
+ result.concat(get_methods('Object', scope: :instance))
337
+ end
338
+ if result.empty?
339
+ pins.each do |pin|
340
+ type = pin.return_type
341
+ unless type.nil?
342
+ namespace, scope = extract_namespace_and_scope(type)
343
+ result.concat get_methods(namespace, scope: scope)
344
+ break
345
+ end
346
+ end
347
+ end
326
348
  end
327
349
  end
328
350
  end
@@ -428,6 +450,7 @@ module Solargraph
428
450
 
429
451
  # Get an array of all symbols in the workspace that match the query.
430
452
  #
453
+ # @param query [String]
431
454
  # @return [Array<Pin::Base>]
432
455
  def query_symbols query
433
456
  result = []
@@ -167,7 +167,8 @@ module Solargraph
167
167
  def virtual_new_pin new_pin, context_pin
168
168
  pin = Pin::Method.new(new_pin.location, new_pin.namespace, new_pin.name, new_pin.docstring, new_pin.scope, new_pin.visibility, new_pin.parameters)
169
169
  # @todo Smelly instance variable access.
170
- pin.instance_variable_set(:@return_type, context_pin.path)
170
+ # pin.instance_variable_set(:@return_type, context_pin.path)
171
+ pin.instance_variable_set(:@return_complex_types, ComplexType.parse(context_pin.path))
171
172
  pin
172
173
  end
173
174
 
@@ -175,9 +176,9 @@ module Solargraph
175
176
  # @param locals [Array<Solargraph::Pin::Base>]
176
177
  # @return [String]
177
178
  def resolve_pin_type pin, locals
178
- pin.return_type
179
179
  return pin.return_type unless pin.return_type.nil?
180
180
  return resolve_block_parameter(pin, locals) if pin.kind == Pin::BLOCK_PARAMETER
181
+ return resolve_method_parameter(pin) if pin.is_a?(Pin::MethodParameter)
181
182
  return resolve_variable(pin, locals) if pin.variable?
182
183
  nil
183
184
  end
@@ -210,6 +211,18 @@ module Solargraph
210
211
  nil
211
212
  end
212
213
 
214
+ def resolve_method_parameter pin
215
+ matches = api_map.get_methods(pin.namespace, scope: pin.scope, visibility: [:public, :private, :protected]).select{|p| p.name == pin.context.name}
216
+ matches.each do |m|
217
+ next unless pin.context.parameters == m.parameters
218
+ next if m.docstring.nil?
219
+ tag = m.docstring.tags(:param).select{|t| t.name == pin.name}.first
220
+ next if tag.nil? or tag.types.nil?
221
+ return tag.types[0]
222
+ end
223
+ nil
224
+ end
225
+
213
226
  def resolve_variable(pin, locals)
214
227
  return nil if pin.nil_assignment?
215
228
  # @todo Do we need the locals here?
@@ -0,0 +1,90 @@
1
+ module Solargraph
2
+ class ComplexType
3
+ # @return [String]
4
+ attr_reader :name
5
+
6
+ # @return [String]
7
+ attr_reader :substring
8
+
9
+ # @return [String]
10
+ attr_reader :tag
11
+
12
+ # @return [Array<ComplexType>]
13
+ attr_reader :subtypes
14
+
15
+ # Create a ComplexType with the specified name and an optional substring.
16
+ # The substring is parameter of a parameterized type, e.g., for the type
17
+ # `Array<String>`, the name is `Array` and the substring is `String`.
18
+ #
19
+ # @param name [String] The name of the type
20
+ # @param substring [String] The substring of the type
21
+ def initialize name, substring = ''
22
+ @name = name
23
+ @substring = substring
24
+ @tag = name
25
+ @tag += "<#{substring}>" unless substring.empty?
26
+ @subtypes = []
27
+ @subtypes.concat(ComplexType.parse(substring)) unless substring.empty?
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def duck_type?
32
+ @duck_type ||= name.start_with?('#')
33
+ end
34
+
35
+ # @return [Boolean]
36
+ def nil_type?
37
+ @nil_type ||= (name.downcase == 'nil')
38
+ end
39
+
40
+ # @return [String]
41
+ def namespace
42
+ @namespace ||= 'Object' if duck_type?
43
+ @namespace ||= 'NilClass' if nil_type?
44
+ @namespace ||= ((name == 'Class' or name == 'Module') and !subtypes.empty?) ? subtypes.first.name : name
45
+ end
46
+
47
+ # @return [Symbol] :class or :instance
48
+ def scope
49
+ @scope ||= :instance if duck_type? or nil_type?
50
+ @scope ||= ((name == 'Class' or name == 'Module') and !subtypes.empty?) ? :class : :instance
51
+ end
52
+
53
+ class << self
54
+ # @param *strings [Array<String>] The type definitions to parse
55
+ # @return [Array<ComplexType>]
56
+ def parse *strings
57
+ types = []
58
+ strings.each do |type_string|
59
+ point_stack = 0
60
+ base = ''
61
+ subtype_string = ''
62
+ type_string.each_char do |char|
63
+ if char == '<'
64
+ point_stack += 1
65
+ next if point_stack == 1
66
+ elsif char == '>'
67
+ point_stack -= 1
68
+ raise 'Invalid close in type' if point_stack < 0
69
+ elsif char == ',' and point_stack == 0
70
+ types.push ComplexType.new base.strip, subtype_string.strip
71
+ base = ''
72
+ subtype_string = ''
73
+ next
74
+ end
75
+ if point_stack == 0 and char != '>'
76
+ base += char
77
+ elsif point_stack != 0
78
+ subtype_string += char
79
+ end
80
+ end
81
+ base.strip!
82
+ subtype_string.strip!
83
+ raise 'Unclosed subtype' if point_stack != 0
84
+ types.push ComplexType.new base, subtype_string
85
+ end
86
+ types
87
+ end
88
+ end
89
+ end
90
+ end
@@ -7,6 +7,7 @@ module Solargraph
7
7
  autoload :Severities, 'solargraph/diagnostics/severities'
8
8
  autoload :Rubocop, 'solargraph/diagnostics/rubocop'
9
9
  autoload :RequireNotFound, 'solargraph/diagnostics/require_not_found'
10
+ autoload :TypeNotDefined, 'solargraph/diagnostics/type_not_defined'
10
11
 
11
12
  class << self
12
13
  # Add a reporter with a name to identify it in .solargraph.yml files.
@@ -42,5 +43,6 @@ module Solargraph
42
43
 
43
44
  register 'rubocop', Rubocop
44
45
  register 'require_not_found', RequireNotFound
46
+ register 'type_not_defined', TypeNotDefined
45
47
  end
46
48
  end
@@ -1,9 +1,12 @@
1
1
  module Solargraph
2
2
  module Diagnostics
3
3
  class Base
4
- # @param source [Solargraph::Source]
5
- # @param source [Solargraph::ApiMap]
4
+ # Perform a diagnosis on a Source within the context of an ApiMap.
5
+ # The result is an array of hash objects that conform to the LSP's
6
+ # Diagnostic specification.
6
7
  #
8
+ # @param source [Solargraph::Source]
9
+ # @param api_map [Solargraph::ApiMap]
7
10
  # @return [Array<Hash>]
8
11
  def diagnose source, api_map
9
12
  end
@@ -0,0 +1,110 @@
1
+ module Solargraph
2
+ module Diagnostics
3
+ # TypeNotDefined reports methods with undefined return types, untagged
4
+ # parameters, and invalid param tags.
5
+ #
6
+ class TypeNotDefined < Base
7
+ def diagnose source, api_map
8
+ result = []
9
+ source.pins.select{|p| p.kind == Pin::METHOD or p.kind == Pin::ATTRIBUTE}.each do |pin|
10
+ result.concat check_return_type(pin, api_map, source)
11
+ result.concat check_param_types(pin, api_map, source)
12
+ result.concat check_param_tags(pin, api_map, source)
13
+ end
14
+ result
15
+ end
16
+
17
+ private
18
+
19
+ def check_return_type pin, api_map, source
20
+ return [] if (pin.name == 'initialize' and pin.scope == :instance) or (pin.name == 'new' and pin.scope == :class)
21
+ result = []
22
+ unless defined_return_type?(pin, api_map)
23
+ result.push(
24
+ range: extract_first_line(pin, source),
25
+ severity: Diagnostics::Severities::WARNING,
26
+ source: 'Solargraph',
27
+ message: "Method `#{pin.name}` has undefined return type."
28
+ )
29
+ end
30
+ result
31
+ end
32
+
33
+ def check_param_types pin, api_map, source
34
+ return [] if pin.name == 'new' and pin.scope == :class
35
+ result = []
36
+ pin.parameter_names.each do |par|
37
+ next if defined_param_type?(pin, par, api_map)
38
+ result.push(
39
+ range: extract_first_line(pin, source),
40
+ severity: Diagnostics::Severities::WARNING,
41
+ source: 'Solargraph',
42
+ message: "Method `#{pin.name}` has undefined param `#{par}`."
43
+ )
44
+ end
45
+ result
46
+ end
47
+
48
+ def check_param_tags pin, api_map, source
49
+ result = []
50
+ unless pin.docstring.nil?
51
+ pin.docstring.tags(:param).each do |par|
52
+ next if pin.parameter_names.include?(par.name)
53
+ result.push(
54
+ range: extract_first_line(pin, source),
55
+ severity: Diagnostics::Severities::WARNING,
56
+ source: 'Solargraph',
57
+ message: "Method `#{pin.name}` has mistagged param `#{par.name}`."
58
+ )
59
+ end
60
+ end
61
+ result
62
+ end
63
+
64
+ def extract_first_line pin, source
65
+ {
66
+ start: {
67
+ line: pin.location.range.start.line,
68
+ character: pin.location.range.start.character
69
+ },
70
+ end: {
71
+ line: pin.location.range.start.line,
72
+ character: last_character(pin.location.range.start, source)
73
+ }
74
+ }
75
+ end
76
+
77
+ def defined_return_type? pin, api_map
78
+ return true unless pin.return_type.nil?
79
+ matches = api_map.get_methods(pin.namespace, scope: pin.scope, visibility: [:public, :private, :protected]).select{|p| p.name == pin.name}
80
+ matches.shift
81
+ matches.any?{|m| !m.return_type.nil?}
82
+ end
83
+
84
+ def defined_param_type? pin, param, api_map
85
+ return true if param_in_docstring?(param, pin.docstring)
86
+ matches = api_map.get_methods(pin.namespace, scope: pin.scope, visibility: [:public, :private, :protected]).select{|p| p.name == pin.name}
87
+ matches.shift
88
+ matches.each do |m|
89
+ next unless pin.parameter_names == m.parameter_names
90
+ return true if param_in_docstring?(param, m.docstring)
91
+ end
92
+ false
93
+ end
94
+
95
+ def param_in_docstring? param, docstring
96
+ return false if docstring.nil?
97
+ tags = docstring.tags(:param)
98
+ tags.any?{|t| t.name == param}
99
+ end
100
+
101
+ # @param position [Solargraph::Source::Position]
102
+ # @param source [Solargraph::Source]
103
+ # @return [Integer]
104
+ def last_character position, source
105
+ cursor = Source::Position.to_offset(source.code, position)
106
+ source.code.index(/[\r\n]/, cursor) || source.code.length
107
+ end
108
+ end
109
+ end
110
+ end
@@ -477,12 +477,18 @@ module Solargraph
477
477
  begin
478
478
  changed = false
479
479
  @change_queue.sort!{|a, b| a['textDocument']['version'] <=> b['textDocument']['version']}
480
+ pending = {}
481
+ @change_queue.each do |obj|
482
+ pending[obj['textDocument']['uri']] ||= 0
483
+ pending[obj['textDocument']['uri']] += 1
484
+ end
480
485
  @change_queue.delete_if do |change|
481
486
  filename = uri_to_file(change['textDocument']['uri'])
482
487
  source = library.checkout(filename)
483
488
  if change['textDocument']['version'] == source.version + change['contentChanges'].length
489
+ pending[change['textDocument']['uri']] -= 1
484
490
  updater = generate_updater(change)
485
- library.synchronize updater
491
+ library.synchronize updater, pending[change['textDocument']['uri']] == 0
486
492
  @diagnostics_queue.push change['textDocument']['uri']
487
493
  changed = true
488
494
  next true
@@ -491,8 +497,9 @@ module Solargraph
491
497
  # increments the version by one regardless of the number
492
498
  # of changes
493
499
  STDERR.puts "Warning: change applied to #{uri_to_file(change['textDocument']['uri'])} is possibly out of sync"
500
+ pending[change['textDocument']['uri']] -= 1
494
501
  updater = generate_updater(change)
495
- library.synchronize updater
502
+ library.synchronize updater, pending[change['textDocument']['uri']] == 0
496
503
  @diagnostics_queue.push change['textDocument']['uri']
497
504
  changed = true
498
505
  next true
@@ -521,7 +528,6 @@ module Solargraph
521
528
 
522
529
  def start_diagnostics_thread
523
530
  Thread.new do
524
- diagnoser = Diagnostics::Rubocop.new
525
531
  until stopped?
526
532
  sleep 0.1
527
533
  if !options['diagnostics']
@@ -4,7 +4,6 @@ module Solargraph
4
4
  class ExitNotification < Base
5
5
  def process
6
6
  host.stop
7
- exit
8
7
  end
9
8
  end
10
9
  end
@@ -3,7 +3,7 @@ module Solargraph
3
3
  module Message
4
4
  class Shutdown < Base
5
5
  def process
6
- set_result({})
6
+ # Nothing to do?
7
7
  end
8
8
  end
9
9
  end
@@ -2,7 +2,6 @@ module Solargraph
2
2
  # A library handles coordination between a Workspace and an ApiMap.
3
3
  #
4
4
  class Library
5
-
6
5
  # @param workspace [Solargraph::Workspace]
7
6
  def initialize workspace = Solargraph::Workspace.new(nil)
8
7
  @workspace = workspace
@@ -93,6 +92,8 @@ module Solargraph
93
92
  end
94
93
  end
95
94
 
95
+ # @param filename [String]
96
+ # @param version [Integer]
96
97
  def overwrite filename, version
97
98
  source = source_hash[filename]
98
99
  return if source.nil?
@@ -206,14 +207,22 @@ module Solargraph
206
207
  api_map.refresh force
207
208
  end
208
209
 
210
+ # @param query [String]
211
+ # @return [Array<YARD::CodeObject::Base>]
209
212
  def document query
210
213
  api_map.document query
211
214
  end
212
215
 
216
+ # @param query [String]
217
+ # @return [Array<String>]
213
218
  def search query
214
219
  api_map.search query
215
220
  end
216
221
 
222
+ # Get an array of all symbols in the workspace that match the query.
223
+ #
224
+ # @param query [String]
225
+ # @return [Array<Pin::Base>]
217
226
  def query_symbols query
218
227
  api_map.query_symbols query
219
228
  end
@@ -231,9 +240,9 @@ module Solargraph
231
240
  end
232
241
 
233
242
  # @param updater [Solargraph::Source::Updater]
234
- def synchronize updater
243
+ def synchronize updater, reparse = true
235
244
  source = read(updater.filename)
236
- source.synchronize updater
245
+ source.synchronize updater, reparse
237
246
  end
238
247
 
239
248
  # Get the current text of a file in the library.