solargraph 0.54.4 → 0.55.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/lib/solargraph/api_map/index.rb +1 -1
  4. data/lib/solargraph/api_map/store.rb +40 -19
  5. data/lib/solargraph/api_map.rb +24 -19
  6. data/lib/solargraph/bench.rb +17 -1
  7. data/lib/solargraph/complex_type/unique_type.rb +88 -7
  8. data/lib/solargraph/complex_type.rb +35 -6
  9. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +51 -0
  10. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +100 -0
  11. data/lib/solargraph/convention/struct_definition.rb +101 -0
  12. data/lib/solargraph/convention.rb +1 -0
  13. data/lib/solargraph/doc_map.rb +42 -18
  14. data/lib/solargraph/language_server/host/message_worker.rb +10 -7
  15. data/lib/solargraph/language_server/host.rb +1 -0
  16. data/lib/solargraph/library.rb +2 -1
  17. data/lib/solargraph/location.rb +8 -0
  18. data/lib/solargraph/parser/comment_ripper.rb +11 -6
  19. data/lib/solargraph/parser/flow_sensitive_typing.rb +226 -0
  20. data/lib/solargraph/parser/node_methods.rb +14 -0
  21. data/lib/solargraph/parser/node_processor.rb +0 -1
  22. data/lib/solargraph/parser/parser_gem/class_methods.rb +11 -6
  23. data/lib/solargraph/parser/parser_gem/node_chainer.rb +10 -10
  24. data/lib/solargraph/parser/parser_gem/node_methods.rb +3 -1
  25. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +21 -0
  26. data/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +21 -1
  27. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +21 -0
  28. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +26 -5
  29. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +41 -0
  30. data/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +28 -0
  31. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +28 -0
  32. data/lib/solargraph/parser/parser_gem/node_processors.rb +10 -0
  33. data/lib/solargraph/parser.rb +1 -0
  34. data/lib/solargraph/pin/base.rb +9 -3
  35. data/lib/solargraph/pin/base_variable.rb +7 -1
  36. data/lib/solargraph/pin/block.rb +2 -0
  37. data/lib/solargraph/pin/breakable.rb +9 -0
  38. data/lib/solargraph/pin/local_variable.rb +7 -1
  39. data/lib/solargraph/pin/method.rb +20 -18
  40. data/lib/solargraph/pin/namespace.rb +10 -7
  41. data/lib/solargraph/pin/parameter.rb +13 -5
  42. data/lib/solargraph/pin/proxy_type.rb +12 -6
  43. data/lib/solargraph/pin/until.rb +18 -0
  44. data/lib/solargraph/pin/while.rb +18 -0
  45. data/lib/solargraph/pin.rb +3 -0
  46. data/lib/solargraph/rbs_map/conversions.rb +8 -8
  47. data/lib/solargraph/rbs_map/core_fills.rb +10 -3
  48. data/lib/solargraph/source/chain/array.rb +4 -3
  49. data/lib/solargraph/source/chain/call.rb +46 -17
  50. data/lib/solargraph/source/chain/constant.rb +1 -1
  51. data/lib/solargraph/source/chain/hash.rb +3 -2
  52. data/lib/solargraph/source/chain/link.rb +2 -0
  53. data/lib/solargraph/source/chain/literal.rb +22 -2
  54. data/lib/solargraph/source/chain/z_super.rb +1 -1
  55. data/lib/solargraph/source/chain.rb +77 -47
  56. data/lib/solargraph/source/source_chainer.rb +2 -2
  57. data/lib/solargraph/source_map/clip.rb +3 -1
  58. data/lib/solargraph/type_checker/checks.rb +4 -0
  59. data/lib/solargraph/type_checker.rb +35 -8
  60. data/lib/solargraph/version.rb +1 -1
  61. data/lib/solargraph/yard_map/mapper/to_method.rb +42 -15
  62. data/lib/solargraph/yardoc.rb +1 -1
  63. metadata +15 -3
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module Convention
5
+ module StructDefinition
6
+ autoload :StructDefintionNode, 'solargraph/convention/struct_definition/struct_definition_node'
7
+ autoload :StructAssignmentNode, 'solargraph/convention/struct_definition/struct_assignment_node'
8
+
9
+ module NodeProcessors
10
+ class StructNode < Parser::NodeProcessor::Base
11
+ def process
12
+ return if struct_definition_node.nil?
13
+
14
+ loc = get_node_location(node)
15
+ nspin = Solargraph::Pin::Namespace.new(
16
+ type: :class,
17
+ location: loc,
18
+ closure: region.closure,
19
+ name: struct_definition_node.class_name,
20
+ comments: comments_for(node),
21
+ visibility: :public,
22
+ gates: region.closure.gates.freeze
23
+ )
24
+ pins.push nspin
25
+
26
+ # define initialize method
27
+ initialize_method_pin = Pin::Method.new(
28
+ name: 'initialize',
29
+ parameters: [],
30
+ scope: :instance,
31
+ location: get_node_location(node),
32
+ closure: nspin,
33
+ visibility: :private,
34
+ comments: comments_for(node)
35
+ )
36
+
37
+ pins.push initialize_method_pin
38
+
39
+ struct_definition_node.attributes.map do |attribute_node, attribute_name|
40
+ initialize_method_pin.parameters.push(
41
+ Pin::Parameter.new(
42
+ name: attribute_name,
43
+ decl: struct_definition_node.keyword_init? ? :kwarg : :arg,
44
+ location: get_node_location(attribute_node),
45
+ closure: initialize_method_pin
46
+ )
47
+ )
48
+ end
49
+
50
+ # define attribute accessors and instance variables
51
+ struct_definition_node.attributes.each do |attribute_node, attribute_name|
52
+ [attribute_name, "#{attribute_name}="].each do |name|
53
+ method_pin = Pin::Method.new(
54
+ name: name,
55
+ parameters: [],
56
+ scope: :instance,
57
+ location: get_node_location(attribute_node),
58
+ closure: nspin,
59
+ comments: attribute_comments(attribute_node, attribute_name),
60
+ visibility: :public
61
+ )
62
+
63
+ pins.push method_pin
64
+
65
+ next unless name.include?('=') # setter
66
+ pins.push Pin::InstanceVariable.new(name: "@#{attribute_name}",
67
+ closure: method_pin,
68
+ location: get_node_location(attribute_node),
69
+ comments: attribute_comments(attribute_node, attribute_name))
70
+ end
71
+ end
72
+
73
+ process_children region.update(closure: nspin, visibility: :public)
74
+ end
75
+
76
+ private
77
+
78
+ # @return [StructDefintionNode, nil]
79
+ def struct_definition_node
80
+ @struct_definition_node ||= if StructDefintionNode.valid?(node)
81
+ StructDefintionNode.new(node)
82
+ elsif StructAssignmentNode.valid?(node)
83
+ StructAssignmentNode.new(node)
84
+ end
85
+ end
86
+
87
+ # @param attribute_node [Parser::AST::Node]
88
+ # @return [String, nil]
89
+ def attribute_comments(attribute_node, attribute_name)
90
+ struct_comments = comments_for(attribute_node)
91
+ return if struct_comments.nil? || struct_comments.empty?
92
+
93
+ struct_comments.split("\n").find do |row|
94
+ row.include?(attribute_name)
95
+ end&.gsub('@param', '@return')&.gsub(attribute_name, '')
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -10,6 +10,7 @@ module Solargraph
10
10
  autoload :Gemfile, 'solargraph/convention/gemfile'
11
11
  autoload :Gemspec, 'solargraph/convention/gemspec'
12
12
  autoload :Rakefile, 'solargraph/convention/rakefile'
13
+ autoload :StructDefinition, 'solargraph/convention/struct_definition'
13
14
 
14
15
  # @type [Set<Convention::Base>]
15
16
  @@conventions = Set.new
@@ -4,6 +4,8 @@ module Solargraph
4
4
  # A collection of pins generated from required gems.
5
5
  #
6
6
  class DocMap
7
+ include Logging
8
+
7
9
  # @return [Array<String>]
8
10
  attr_reader :requires
9
11
 
@@ -28,12 +30,12 @@ module Solargraph
28
30
 
29
31
  # @return [Array<Gem::Specification>]
30
32
  def gemspecs
31
- @gemspecs ||= required_gem_map.values.compact
33
+ @gemspecs ||= required_gems_map.values.compact.flatten
32
34
  end
33
35
 
34
36
  # @return [Array<String>]
35
37
  def unresolved_requires
36
- @unresolved_requires ||= required_gem_map.select { |_, gemspec| gemspec.nil? }.keys
38
+ @unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
37
39
  end
38
40
 
39
41
  # @return [Hash{Gem::Specification => Array[Pin::Base]}]
@@ -52,20 +54,22 @@ module Solargraph
52
54
  def generate
53
55
  @pins = []
54
56
  @uncached_gemspecs = []
55
- required_gem_map.each do |path, gemspec|
56
- if gemspec
57
- try_cache gemspec
58
- else
57
+ required_gems_map.each do |path, gemspecs|
58
+ if gemspecs.nil?
59
59
  try_stdlib_map path
60
+ else
61
+ gemspecs.each do |gemspec|
62
+ try_cache gemspec
63
+ end
60
64
  end
61
65
  end
62
66
  dependencies.each { |dep| try_cache dep }
63
67
  @uncached_gemspecs.uniq!
64
68
  end
65
69
 
66
- # @return [Hash{String => Gem::Specification, nil}]
67
- def required_gem_map
68
- @required_gem_map ||= requires.to_h { |path| [path, resolve_path_to_gemspec(path)] }
70
+ # @return [Hash{String => Array<Gem::Specification>}]
71
+ def required_gems_map
72
+ @required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] }
69
73
  end
70
74
 
71
75
  # @return [Hash{String => Gem::Specification}]
@@ -125,10 +129,26 @@ module Solargraph
125
129
  end
126
130
 
127
131
  # @param path [String]
128
- # @return [Gem::Specification, nil]
129
- def resolve_path_to_gemspec path
132
+ # @return [::Array<Gem::Specification>, nil]
133
+ def resolve_path_to_gemspecs path
130
134
  return nil if path.empty?
131
135
 
136
+ if path == 'bundler/require'
137
+ # find only the gems bundler is now using
138
+ gemspecs = Bundler.definition.locked_gems.specs.flat_map do |lazy_spec|
139
+ logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version} from #{path}"
140
+ [Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)]
141
+ rescue Gem::MissingSpecError => e
142
+ logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess")
143
+ # can happen in local filesystem references
144
+ specs = resolve_path_to_gemspecs lazy_spec.name
145
+ logger.info "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil?
146
+ next specs
147
+ end.compact
148
+
149
+ return gemspecs
150
+ end
151
+
132
152
  gemspec = Gem::Specification.find_by_path(path)
133
153
  if gemspec.nil?
134
154
  gem_name_guess = path.split('/').first
@@ -142,16 +162,17 @@ module Solargraph
142
162
  gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file }
143
163
  rescue Gem::MissingSpecError
144
164
  Solargraph.logger.debug "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}"
145
- nil
165
+ []
146
166
  end
147
167
  end
148
- gemspec_or_preference gemspec
168
+ return nil if gemspec.nil?
169
+ [gemspec_or_preference(gemspec)]
149
170
  end
150
171
 
151
- # @param gemspec [Gem::Specification, nil]
152
- # @return [Gem::Specification, nil]
172
+ # @param gemspec [Gem::Specification]
173
+ # @return [Gem::Specification]
153
174
  def gemspec_or_preference gemspec
154
- return gemspec unless gemspec && preference_map.key?(gemspec.name)
175
+ return gemspec unless preference_map.key?(gemspec.name)
155
176
  return gemspec if gemspec.version == preference_map[gemspec.name].version
156
177
 
157
178
  change_gemspec_version gemspec, preference_map[by_path.name].version
@@ -170,12 +191,15 @@ module Solargraph
170
191
  # @param gemspec [Gem::Specification]
171
192
  # @return [Array<Gem::Specification>]
172
193
  def fetch_dependencies gemspec
194
+ # @param spec [Gem::Dependency]
173
195
  only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps|
174
196
  Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}"
175
- dep = Gem::Specification.find_by_name(spec.name, spec.requirement)
197
+ dep = Gem.loaded_specs[spec.name]
198
+ # @todo is next line necessary?
199
+ dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement)
176
200
  deps.merge fetch_dependencies(dep) if deps.add?(dep)
177
201
  rescue Gem::MissingSpecError
178
- Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirements} for #{gemspec.name} not found."
202
+ Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found."
179
203
  end.to_a
180
204
  end
181
205
 
@@ -7,9 +7,16 @@ module Solargraph
7
7
  #
8
8
  class MessageWorker
9
9
  UPDATE_METHODS = [
10
- 'textDocument/didOpen',
11
10
  'textDocument/didChange',
12
- 'workspace/didChangeWatchedFiles'
11
+ 'textDocument/didClose',
12
+ 'textDocument/didOpen',
13
+ 'textDocument/didSave',
14
+ 'workspace/didChangeConfiguration',
15
+ 'workspace/didChangeWatchedFiles',
16
+ 'workspace/didCreateFiles',
17
+ 'workspace/didChangeWorkspaceFolders',
18
+ 'workspace/didDeleteFiles',
19
+ 'workspace/didRenameFiles'
13
20
  ].freeze
14
21
 
15
22
  # @param host [Host]
@@ -84,11 +91,7 @@ module Solargraph
84
91
  idx = messages.find_index do |msg|
85
92
  UPDATE_METHODS.include?(msg['method']) || version_dependent?(msg)
86
93
  end
87
- return messages.shift unless idx
88
-
89
- msg = messages[idx]
90
- messages.delete_at idx
91
- msg
94
+ idx ? messages.delete_at(idx) : messages.shift
92
95
  end
93
96
 
94
97
  # True if the message requires a previous update to have executed in
@@ -749,6 +749,7 @@ module Solargraph
749
749
  return change if source.code.length + 1 != change['text'].length
750
750
  diffs = Diff::LCS.diff(source.code, change['text'])
751
751
  return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
752
+ # @sg-ignore push this upstream
752
753
  # @type [Diff::LCS::Change]
753
754
  diff = diffs.first.first
754
755
  return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element)
@@ -430,7 +430,8 @@ module Solargraph
430
430
  Bench.new(
431
431
  source_maps: source_map_hash.values,
432
432
  workspace: workspace,
433
- external_requires: external_requires
433
+ external_requires: external_requires,
434
+ live_map: @current ? source_map_hash[@current.filename] : nil
434
435
  )
435
436
  end
436
437
 
@@ -30,6 +30,14 @@ module Solargraph
30
30
  range.contain?(location.range.start) && range.contain?(location.range.ending) && filename == location.filename
31
31
  end
32
32
 
33
+ def inspect
34
+ "<#{self.class.name}: filename=#{filename}, range=#{range.inspect}>"
35
+ end
36
+
37
+ def to_s
38
+ inspect
39
+ end
40
+
33
41
  # @return [Hash]
34
42
  def to_hash
35
43
  {
@@ -13,6 +13,7 @@ module Solargraph
13
13
  end
14
14
 
15
15
  def on_comment *args
16
+ # @type [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))]
16
17
  result = super
17
18
  if @buffer_lines[result[2][0]][0..result[2][1]].strip =~ /^#/
18
19
  chomped = result[1].chomp
@@ -24,24 +25,28 @@ module Solargraph
24
25
  result
25
26
  end
26
27
 
28
+ # @param result [Array(Symbol, String, Array([Integer, nil], [Integer, nil]))]
29
+ # @return [void]
30
+ def create_snippet(result)
31
+ chomped = result[1].chomp
32
+ @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0] || 0, result[2][1] || 0, result[2][0] || 0, (result[2][1] || 0) + chomped.length), chomped)
33
+ end
34
+
27
35
  def on_embdoc_beg *args
28
36
  result = super
29
- chomped = result[1].chomp
30
- @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped)
37
+ create_snippet(result)
31
38
  result
32
39
  end
33
40
 
34
41
  def on_embdoc *args
35
42
  result = super
36
- chomped = result[1].chomp
37
- @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped)
43
+ create_snippet(result)
38
44
  result
39
45
  end
40
46
 
41
47
  def on_embdoc_end *args
42
48
  result = super
43
- chomped = result[1].chomp
44
- @comments[result[2][0]] = Snippet.new(Range.from_to(result[2][0], result[2][1], result[2][0], result[2][1] + chomped.length), chomped)
49
+ create_snippet(result)
45
50
  result
46
51
  end
47
52
 
@@ -0,0 +1,226 @@
1
+ module Solargraph
2
+ module Parser
3
+ class FlowSensitiveTyping
4
+ include Solargraph::Parser::NodeMethods
5
+
6
+ # @param locals [Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]
7
+ def initialize(locals, enclosing_breakable_pin = nil)
8
+ @locals = locals
9
+ @enclosing_breakable_pin = enclosing_breakable_pin
10
+ end
11
+
12
+ # @param and_node [Parser::AST::Node]
13
+ def process_and(and_node, true_ranges = [])
14
+ lhs = and_node.children[0]
15
+ rhs = and_node.children[1]
16
+
17
+ before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1)
18
+ before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column)
19
+
20
+ rhs_presence = Range.new(before_rhs_pos,
21
+ get_node_end_position(rhs))
22
+ process_isa(lhs, true_ranges + [rhs_presence])
23
+ end
24
+
25
+ # @param if_node [Parser::AST::Node]
26
+ def process_if(if_node)
27
+ #
28
+ # See if we can refine a type based on the result of 'if foo.nil?'
29
+ #
30
+ # [3] pry(main)> require 'parser/current'; Parser::CurrentRuby.parse("if foo.is_a? Baz; then foo; else bar; end")
31
+ # => s(:if,
32
+ # s(:send,
33
+ # s(:send, nil, :foo), :is_a?,
34
+ # s(:const, nil, :Baz)),
35
+ # s(:send, nil, :foo),
36
+ # s(:send, nil, :bar))
37
+ # [4] pry(main)>
38
+ conditional_node = if_node.children[0]
39
+ then_clause = if_node.children[1]
40
+ else_clause = if_node.children[2]
41
+
42
+ true_ranges = []
43
+ if always_breaks?(else_clause)
44
+ unless enclosing_breakable_pin.nil?
45
+ rest_of_breakable_body = Range.new(get_node_end_position(if_node),
46
+ get_node_end_position(enclosing_breakable_pin.node))
47
+ true_ranges << rest_of_breakable_body
48
+ end
49
+ end
50
+
51
+ unless then_clause.nil?
52
+ #
53
+ # Add specialized locals for the then clause range
54
+ #
55
+ before_then_clause_loc = then_clause.location.expression.adjust(begin_pos: -1)
56
+ before_then_clause_pos = Position.new(before_then_clause_loc.line, before_then_clause_loc.column)
57
+ true_ranges << Range.new(before_then_clause_pos,
58
+ get_node_end_position(then_clause))
59
+ end
60
+
61
+ process_conditional(conditional_node, true_ranges)
62
+ end
63
+
64
+ class << self
65
+ include Logging
66
+ end
67
+
68
+ # Find a variable pin by name and where it is used.
69
+ #
70
+ # Resolves our most specific view of this variable's type by
71
+ # preferring pins created by flow-sensitive typing when we have
72
+ # them based on the Closure and Location.
73
+ #
74
+ # @param pins [Array<Pin::LocalVariable>]
75
+ # @param closure [Pin::Closure]
76
+ # @param location [Location]
77
+ def self.visible_pins(pins, name, closure, location)
78
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location})" }
79
+ pins_with_name = pins.select { |p| p.name == name }
80
+ if pins_with_name.empty?
81
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => [] - no pins with name" }
82
+ return []
83
+ end
84
+ pins_with_specific_visibility = pins.select { |p| p.name == name && p.presence && p.visible_at?(closure, location) }
85
+ if pins_with_specific_visibility.empty?
86
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_name} - no pins with specific visibility" }
87
+ return pins_with_name
88
+ end
89
+ visible_pins_specific_to_this_closure = pins_with_specific_visibility.select { |p| p.closure == closure }
90
+ if visible_pins_specific_to_this_closure.empty?
91
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_specific_visibility} - no visible pins specific to this closure (#{closure})}" }
92
+ return pins_with_specific_visibility
93
+ end
94
+ flow_defined_pins = pins_with_specific_visibility.select { |p| p.presence_certain? }
95
+ if flow_defined_pins.empty?
96
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{visible_pins_specific_to_this_closure} - no flow-defined pins" }
97
+ return visible_pins_specific_to_this_closure
98
+ end
99
+
100
+ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{flow_defined_pins}" }
101
+
102
+ flow_defined_pins
103
+ end
104
+
105
+ include Logging
106
+
107
+ private
108
+
109
+ # @param pin [Pin::LocalVariable]
110
+ # @param if_node [Parser::AST::Node]
111
+ def add_downcast_local(pin, downcast_type_name, presence)
112
+ # @todo Create pin#update method
113
+ new_pin = Solargraph::Pin::LocalVariable.new(
114
+ location: pin.location,
115
+ closure: pin.closure,
116
+ name: pin.name,
117
+ assignment: pin.assignment,
118
+ comments: pin.comments,
119
+ presence: presence,
120
+ return_type: ComplexType.try_parse(downcast_type_name),
121
+ presence_certain: true
122
+ )
123
+ locals.push(new_pin)
124
+ end
125
+
126
+ # @param facts_by_pin [Hash{Pin::LocalVariable => Array<Hash{Symbol => String}>}]
127
+ # @param presences [Array<Range>]
128
+ # @return [void]
129
+ def process_facts(facts_by_pin, presences)
130
+ #
131
+ # Add specialized locals for the rest of the block
132
+ #
133
+ facts_by_pin.each_pair do |pin, facts|
134
+ facts.each do |fact|
135
+ downcast_type_name = fact.fetch(:type)
136
+ presences.each do |presence|
137
+ add_downcast_local(pin, downcast_type_name, presence)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ # @param conditional_node [Parser::AST::Node]
144
+ def process_conditional(conditional_node, true_ranges)
145
+ if conditional_node.type == :send
146
+ process_isa(conditional_node, true_ranges)
147
+ elsif conditional_node.type == :and
148
+ process_and(conditional_node, true_ranges)
149
+ end
150
+ end
151
+
152
+ # @param isa_node [Parser::AST::Node]
153
+ # @return [Array(String, String)]
154
+ def parse_isa(isa_node)
155
+ return unless isa_node.type == :send && isa_node.children[1] == :is_a?
156
+ # Check if conditional node follows this pattern:
157
+ # s(:send,
158
+ # s(:send, nil, :foo), :is_a?,
159
+ # s(:const, nil, :Baz)),
160
+ isa_receiver = isa_node.children[0]
161
+ isa_type_name = type_name(isa_node.children[2])
162
+ return unless isa_type_name
163
+
164
+ # check if isa_receiver looks like this:
165
+ # s(:send, nil, :foo)
166
+ # and set variable_name to :foo
167
+ if isa_receiver.type == :send && isa_receiver.children[0].nil? && isa_receiver.children[1].is_a?(Symbol)
168
+ variable_name = isa_receiver.children[1].to_s
169
+ end
170
+ # or like this:
171
+ # (lvar :repr)
172
+ variable_name = isa_receiver.children[0].to_s if isa_receiver.type == :lvar
173
+ return unless variable_name
174
+
175
+ [isa_type_name, variable_name]
176
+ end
177
+
178
+ def find_local(variable_name, position)
179
+ pins = locals.select { |pin| pin.name == variable_name && pin.presence.include?(position) }
180
+ return unless pins.length == 1
181
+ pins.first
182
+ end
183
+
184
+ def process_isa(isa_node, true_presences)
185
+ isa_type_name, variable_name = parse_isa(isa_node)
186
+ return if variable_name.nil? || variable_name.empty?
187
+ isa_position = Range.from_node(isa_node).start
188
+
189
+ pin = find_local(variable_name, isa_position)
190
+ return unless pin
191
+
192
+ if_true = {}
193
+ if_true[pin] ||= []
194
+ if_true[pin] << { type: isa_type_name }
195
+ process_facts(if_true, true_presences)
196
+ end
197
+
198
+ # @param node [Parser::AST::Node]
199
+ def type_name(node)
200
+ # e.g.,
201
+ # s(:const, nil, :Baz)
202
+ return unless node.type == :const
203
+ module_node = node.children[0]
204
+ class_node = node.children[1]
205
+
206
+ return class_node.to_s if module_node.nil?
207
+
208
+ module_type_name = type_name(module_node)
209
+ return unless module_type_name
210
+
211
+ "#{module_type_name}::#{class_node}"
212
+ end
213
+
214
+ # @todo "return type could not be inferred" should not trigger here
215
+ # @sg-ignore
216
+ # @param clause_node [Parser::AST::Node]
217
+ def always_breaks?(clause_node)
218
+ clause_node&.type == :break
219
+ end
220
+
221
+ attr_reader :locals
222
+
223
+ attr_reader :enclosing_breakable_pin
224
+ end
225
+ end
226
+ end
@@ -78,6 +78,20 @@ module Solargraph
78
78
  def convert_hash node
79
79
  raise NotImplementedError
80
80
  end
81
+
82
+ # @abstract
83
+ # @param node [Parser::AST::Node]
84
+ # @return [Position]
85
+ def get_node_start_position(node)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # @abstract
90
+ # @param node [Parser::AST::Node]
91
+ # @return [Position]
92
+ def get_node_end_position(node)
93
+ raise NotImplementedError
94
+ end
81
95
  end
82
96
  end
83
97
  end
@@ -9,7 +9,6 @@ module Solargraph
9
9
  autoload :Base, 'solargraph/parser/node_processor/base'
10
10
 
11
11
  class << self
12
- # @type [Hash<Symbol, Class<NodeProcessor::Base>>]
13
12
  @@processors ||= {}
14
13
 
15
14
  # Register a processor for a node type.
@@ -3,6 +3,15 @@
3
3
  require 'parser/current'
4
4
  require 'parser/source/buffer'
5
5
 
6
+ # Awaiting ability to use a version containing https://github.com/whitequark/parser/pull/1076
7
+ #
8
+ # @!parse
9
+ # class ::Parser::Base < ::Parser::Builder
10
+ # # @return [Integer]
11
+ # def version; end
12
+ # end
13
+ # class ::Parser::CurrentRuby < ::Parser::Base; end
14
+
6
15
  module Solargraph
7
16
  module Parser
8
17
  module ParserGem
@@ -11,13 +20,9 @@ module Solargraph
11
20
  # @param filename [String, nil]
12
21
  # @return [Array(Parser::AST::Node, Hash{Integer => String})]
13
22
  def parse_with_comments code, filename = nil
14
- buffer = ::Parser::Source::Buffer.new(filename, 0)
15
- buffer.source = code
16
- node = parser.parse(buffer)
23
+ node = parse(code, filename)
17
24
  comments = CommentRipper.new(code, filename, 0).parse
18
25
  [node, comments]
19
- rescue ::Parser::SyntaxError => e
20
- raise Parser::SyntaxError, e.message
21
26
  end
22
27
 
23
28
  # @param code [String]
@@ -28,7 +33,7 @@ module Solargraph
28
33
  buffer = ::Parser::Source::Buffer.new(filename, line)
29
34
  buffer.source = code
30
35
  parser.parse(buffer)
31
- rescue ::Parser::SyntaxError => e
36
+ rescue ::Parser::SyntaxError, ::Parser::UnknownEncodingInMagicComment => e
32
37
  raise Parser::SyntaxError, e.message
33
38
  end
34
39