solargraph 0.55.4 → 0.55.5

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/plugins.yml +2 -0
  3. data/.github/workflows/typecheck.yml +2 -0
  4. data/.gitignore +2 -0
  5. data/CHANGELOG.md +3 -0
  6. data/README.md +13 -3
  7. data/lib/solargraph/api_map/index.rb +23 -15
  8. data/lib/solargraph/api_map/store.rb +2 -1
  9. data/lib/solargraph/api_map.rb +53 -27
  10. data/lib/solargraph/complex_type/type_methods.rb +5 -1
  11. data/lib/solargraph/complex_type/unique_type.rb +7 -0
  12. data/lib/solargraph/convention/base.rb +3 -3
  13. data/lib/solargraph/convention.rb +3 -3
  14. data/lib/solargraph/doc_map.rb +189 -43
  15. data/lib/solargraph/gem_pins.rb +53 -38
  16. data/lib/solargraph/language_server/host.rb +9 -1
  17. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +1 -0
  18. data/lib/solargraph/language_server/message/extended/document.rb +5 -2
  19. data/lib/solargraph/language_server/message/extended/document_gems.rb +3 -3
  20. data/lib/solargraph/library.rb +7 -4
  21. data/lib/solargraph/location.rb +13 -0
  22. data/lib/solargraph/parser/parser_gem/class_methods.rb +5 -8
  23. data/lib/solargraph/parser/parser_gem/node_processors/casgn_node.rb +2 -2
  24. data/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +2 -2
  25. data/lib/solargraph/pin/base.rb +268 -24
  26. data/lib/solargraph/pin/base_variable.rb +9 -8
  27. data/lib/solargraph/pin/callable.rb +69 -0
  28. data/lib/solargraph/pin/closure.rb +12 -0
  29. data/lib/solargraph/pin/local_variable.rb +8 -5
  30. data/lib/solargraph/pin/method.rb +134 -17
  31. data/lib/solargraph/pin/parameter.rb +43 -6
  32. data/lib/solargraph/pin/signature.rb +38 -0
  33. data/lib/solargraph/pin_cache.rb +185 -0
  34. data/lib/solargraph/position.rb +9 -0
  35. data/lib/solargraph/range.rb +9 -0
  36. data/lib/solargraph/rbs_map/conversions.rb +19 -8
  37. data/lib/solargraph/rbs_map/core_map.rb +31 -9
  38. data/lib/solargraph/rbs_map/stdlib_map.rb +15 -5
  39. data/lib/solargraph/rbs_map.rb +74 -17
  40. data/lib/solargraph/shell.rb +11 -15
  41. data/lib/solargraph/source_map.rb +0 -17
  42. data/lib/solargraph/version.rb +1 -1
  43. data/lib/solargraph/views/_method.erb +10 -10
  44. data/lib/solargraph/views/_namespace.erb +3 -3
  45. data/lib/solargraph/views/document.erb +10 -10
  46. data/lib/solargraph/workspace.rb +15 -5
  47. data/lib/solargraph/yardoc.rb +3 -11
  48. data/lib/solargraph.rb +10 -12
  49. data/rbs_collection.yaml +19 -0
  50. data/solargraph.gemspec +1 -0
  51. metadata +19 -7
  52. data/lib/solargraph/cache.rb +0 -77
@@ -7,60 +7,75 @@ module Solargraph
7
7
  # documentation.
8
8
  #
9
9
  module GemPins
10
- # Build an array of pins from a gem specification. The process starts with
11
- # YARD, enhances the resulting pins with RBS definitions, and appends RBS
12
- # pins that don't exist in the YARD mapping.
13
- #
10
+ class << self
11
+ include Logging
12
+ end
13
+
14
14
  # @param gemspec [Gem::Specification]
15
15
  # @return [Array<Pin::Base>]
16
- def self.build(gemspec)
17
- yard_pins = build_yard_pins(gemspec)
18
- rbs_map = RbsMap.from_gemspec(gemspec)
19
- combine yard_pins, rbs_map
16
+ def self.build_yard_pins(gemspec)
17
+ Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec)
18
+ yardoc = Yardoc.load!(gemspec)
19
+ YardMap::Mapper.new(yardoc, gemspec).map
20
+ end
21
+
22
+ # @param pins [Array<Pin::Base>]
23
+ def self.combine_method_pins_by_path(pins)
24
+ # bad_pins = pins.select { |pin| pin.is_a?(Pin::Method) && pin.path == 'StringIO.open' && pin.source == :rbs }; raise "wtf: #{bad_pins}" if bad_pins.length > 1
25
+ method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method }
26
+ by_path = method_pins.group_by(&:path)
27
+ by_path.transform_values! do |pins|
28
+ GemPins.combine_method_pins(*pins)
29
+ end
30
+ by_path.values + alias_pins
31
+ end
32
+
33
+ def self.combine_method_pins(*pins)
34
+ out = pins.reduce(nil) do |memo, pin|
35
+ next pin if memo.nil?
36
+ if memo == pin && memo.source != :combined
37
+ # @todo we should track down situations where we are handled
38
+ # the same pin from the same source here and eliminate them -
39
+ # this is an efficiency workaround for now
40
+ next memo
41
+ end
42
+ memo.combine_with(pin)
43
+ end
44
+ logger.debug { "GemPins.combine_method_pins(pins.length=#{pins.length}, pins=#{pins}) => #{out.inspect}" }
45
+ out
20
46
  end
21
47
 
22
48
  # @param yard_pins [Array<Pin::Base>]
23
49
  # @param rbs_map [RbsMap]
24
50
  # @return [Array<Pin::Base>]
25
- def self.combine(yard_pins, rbs_map)
51
+ def self.combine(yard_pins, rbs_pins)
26
52
  in_yard = Set.new
27
- combined = yard_pins.map do |yard|
28
- in_yard.add yard.path
29
- next yard unless yard.is_a?(Pin::Method)
53
+ rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins)
54
+ combined = yard_pins.map do |yard_pin|
55
+ in_yard.add yard_pin.path
56
+ rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
57
+ next yard_pin unless rbs_pin && yard_pin.class == Pin::Method
30
58
 
31
- rbs = rbs_map.path_pin(yard.path, Pin::Method)
32
- next yard unless rbs
59
+ unless rbs_pin
60
+ logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" }
61
+ next yard_pin
62
+ end
33
63
 
34
- # @sg-ignore
35
- yard.class.new(
36
- location: yard.location,
37
- closure: yard.closure,
38
- name: yard.name,
39
- comments: yard.comments,
40
- scope: yard.scope,
41
- parameters: rbs.parameters,
42
- generics: rbs.generics,
43
- node: yard.node,
44
- signatures: yard.signatures,
45
- return_type: best_return_type(rbs.return_type, yard.return_type),
46
- source: :gem_pins
47
- )
64
+ out = combine_method_pins(rbs_pin, yard_pin)
65
+ logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" }
66
+ out
48
67
  end
49
- in_rbs = rbs_map.pins.reject { |pin| in_yard.include?(pin.path) }
50
- combined + in_rbs
68
+ in_rbs_only = rbs_pins.select do |pin|
69
+ pin.path.nil? || !in_yard.include?(pin.path)
70
+ end
71
+ out = combined + in_rbs_only
72
+ logger.debug { "GemPins#combine: Returning #{out.length} combined pins" }
73
+ out
51
74
  end
52
75
 
53
76
  class << self
54
77
  private
55
78
 
56
- # @param gemspec [Gem::Specification]
57
- # @return [Array<Pin::Base>]
58
- def build_yard_pins(gemspec)
59
- Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec)
60
- yardoc = Yardoc.load!(gemspec)
61
- YardMap::Mapper.new(yardoc, gemspec).map
62
- end
63
-
64
79
  # Select the first defined type.
65
80
  #
66
81
  # @param choices [Array<ComplexType>]
@@ -299,6 +299,10 @@ module Solargraph
299
299
  end
300
300
  end
301
301
 
302
+ def command_path
303
+ options['commandPath'] || 'solargraph'
304
+ end
305
+
302
306
  # Prepare multiple folders.
303
307
  #
304
308
  # @param array [Array<Hash{String => String}>]
@@ -598,7 +602,11 @@ module Solargraph
598
602
  # @return [Array]
599
603
  def document query
600
604
  result = []
601
- libraries.each { |lib| result.concat lib.document(query) }
605
+ if libraries.empty?
606
+ result.concat generic_library.document(query)
607
+ else
608
+ libraries.each { |lib| result.concat lib.document(query) }
609
+ end
602
610
  result
603
611
  end
604
612
 
@@ -83,6 +83,7 @@ module Solargraph
83
83
  @fetched = true
84
84
  begin
85
85
  @available ||= begin
86
+ # @sg-ignore
86
87
  # @type [Gem::Dependency, nil]
87
88
  tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first
88
89
  if tuple.nil?
@@ -6,12 +6,15 @@ module Solargraph
6
6
  module Extended
7
7
  class Document < Base
8
8
  def process
9
- objects = host.document(params['query'])
9
+ api_map, pins = host.document(params['query'])
10
10
  page = Solargraph::Page.new(host.options['viewsPath'])
11
- content = page.render('document', layout: true, locals: {objects: objects})
11
+ content = page.render('document', layout: true, locals: { api_map: api_map, pins: pins })
12
12
  set_result(
13
13
  content: content
14
14
  )
15
+ rescue StandardError => e
16
+ Solargraph.logger.warn "Error processing document: [#{e.class}] #{e.message}"
17
+ Solargraph.logger.debug e.backtrace.join("\n")
15
18
  end
16
19
  end
17
20
  end
@@ -11,9 +11,9 @@ module Solargraph
11
11
  #
12
12
  class DocumentGems < Base
13
13
  def process
14
- cmd = "yard gems"
15
- cmd += " --rebuild" if params['rebuild']
16
- o, s = Open3.capture2(cmd)
14
+ cmd = [host.command_path, 'gems']
15
+ cmd.push '--rebuild' if params['rebuild']
16
+ o, s = Open3.capture2(*cmd)
17
17
  if s != 0
18
18
  host.show_message "An error occurred while building gem documentation.", LanguageServer::MessageTypes::ERROR
19
19
  set_result({
@@ -327,9 +327,10 @@ module Solargraph
327
327
 
328
328
  # @param query [String]
329
329
  # @return [Enumerable<YARD::CodeObjects::Base>]
330
+ # @return [Array(ApiMap, Enumerable<Pin::Base>)]
330
331
  def document query
331
332
  sync_catalog
332
- mutex.synchronize { api_map.document query }
333
+ mutex.synchronize { [api_map, api_map.get_path_pins(query)] }
333
334
  end
334
335
 
335
336
  # @param query [String]
@@ -586,8 +587,9 @@ module Solargraph
586
587
 
587
588
  # @return [void]
588
589
  def cache_next_gemspec
589
- return if @cache_progress
590
- spec = api_map.uncached_gemspecs.find { |spec| !cache_errors.include?(spec) }
590
+ return if @cache_progres
591
+ spec = (api_map.uncached_yard_gemspecs + api_map.uncached_rbs_collection_gemspecs).
592
+ find { |spec| !cache_errors.include?(spec) }
591
593
  return end_cache_progress unless spec
592
594
 
593
595
  pending = api_map.uncached_gemspecs.length - cache_errors.length - 1
@@ -654,7 +656,8 @@ module Solargraph
654
656
  api_map.catalog bench
655
657
  source_map_hash.values.each { |map| find_external_requires(map) }
656
658
  logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"
657
- logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs"
659
+ logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs"
660
+ logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs"
658
661
  cache_next_gemspec
659
662
  @sync_count = 0
660
663
  end
@@ -25,6 +25,19 @@ module Solargraph
25
25
  [filename, range]
26
26
  end
27
27
 
28
+ def <=>(other)
29
+ return nil unless other.is_a?(Location)
30
+ if filename == other.filename
31
+ range <=> other.range
32
+ else
33
+ filename <=> other.filename
34
+ end
35
+ end
36
+
37
+ def rbs?
38
+ filename.end_with?('.rbs')
39
+ end
40
+
28
41
  # @param location [self]
29
42
  def contain? location
30
43
  range.contain?(location.range.start) && range.contain?(location.range.ending) && filename == location.filename
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parser/current'
4
- require 'parser/source/buffer'
3
+ require 'prism'
5
4
 
6
5
  # Awaiting ability to use a version containing https://github.com/whitequark/parser/pull/1076
7
6
  #
@@ -39,12 +38,10 @@ module Solargraph
39
38
 
40
39
  # @return [::Parser::Base]
41
40
  def parser
42
- # @todo Consider setting an instance variable. We might not need to
43
- # recreate the parser every time we use it.
44
- parser = ::Parser::CurrentRuby.new(FlawedBuilder.new)
45
- parser.diagnostics.all_errors_are_fatal = true
46
- parser.diagnostics.ignore_warnings = true
47
- parser
41
+ @parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser|
42
+ parser.diagnostics.all_errors_are_fatal = true
43
+ parser.diagnostics.ignore_warnings = true
44
+ end
48
45
  end
49
46
 
50
47
  # @param source [Source]
@@ -30,8 +30,8 @@ module Solargraph
30
30
  process_children
31
31
  end
32
32
 
33
- # TODO: Move this out of [CasgnNode] once [Solargraph::Parser::NodeProcessor] supports
34
- # multiple processors.
33
+ # @todo Move this out of [CasgnNode] once [Solargraph::Parser::NodeProcessor] supports
34
+ # multiple processors.
35
35
  def process_struct_assignment
36
36
  processor_klass = Convention::StructDefinition::NodeProcessors::StructNode
37
37
  processor = processor_klass.new(node, region, pins, locals)
@@ -45,8 +45,8 @@ module Solargraph
45
45
  process_children region.update(closure: nspin, visibility: :public)
46
46
  end
47
47
 
48
- # TODO: Move this out of [NamespaceNode] once [Solargraph::Parser::NodeProcessor] supports
49
- # multiple processors.
48
+ # @todo Move this out of [NamespaceNode] once [Solargraph::Parser::NodeProcessor] supports
49
+ # multiple processors.
50
50
  def process_struct_definition
51
51
  processor_klass = Convention::StructDefinition::NodeProcessors::StructNode
52
52
  processor = processor_klass.new(node, region, pins, locals)
@@ -8,6 +8,7 @@ module Solargraph
8
8
  include Common
9
9
  include Conversions
10
10
  include Documenting
11
+ include Logging
11
12
 
12
13
  # @return [YARD::CodeObjects::Base]
13
14
  attr_reader :code_object
@@ -36,16 +37,273 @@ module Solargraph
36
37
  # @param closure [Solargraph::Pin::Closure, nil]
37
38
  # @param name [String]
38
39
  # @param comments [String]
39
- def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: ''
40
+ # @param source [Symbol, nil]
41
+ # @param docstring [YARD::Docstring, nil]
42
+ # @param directives [::Array<YARD::Tags::Directive>, nil]
43
+ def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', docstring: nil, directives: nil
40
44
  @location = location
41
45
  @type_location = type_location
42
46
  @closure = closure
43
47
  @name = name
44
48
  @comments = comments
45
49
  @source = source
50
+ @identity = nil
51
+ @docstring = docstring
52
+ @directives = directives
46
53
  assert_source_provided
47
54
  end
48
55
 
56
+ # @param other [self]
57
+ # @param attrs [Hash{Symbol => Object}]
58
+ #
59
+ # @return [self]
60
+ def combine_with(other, attrs={})
61
+ raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class
62
+ type_location = choose(other, :type_location)
63
+ location = choose(other, :location)
64
+ combined_name = combine_name(other)
65
+ new_attrs = {
66
+ location: location,
67
+ type_location: type_location,
68
+ name: combined_name,
69
+ closure: choose_pin_attr_with_same_name(other, :closure),
70
+ comments: choose_longer(other, :comments),
71
+ source: :combined,
72
+ docstring: choose(other, :docstring),
73
+ directives: combine_directives(other),
74
+ }.merge(attrs)
75
+ assert_same_macros(other)
76
+ logger.debug { "Base#combine_with(path=#{path}) - other.comments=#{other.comments.inspect}, self.comments = #{self.comments}" }
77
+ out = self.class.new(**new_attrs)
78
+ out.reset_generated!
79
+ out
80
+ end
81
+
82
+ # @param other [self]
83
+ # @param attr [::Symbol]
84
+ # @sg-ignore
85
+ # @return [undefined]
86
+ def choose_longer(other, attr)
87
+ # @type [undefined]
88
+ val1 = send(attr)
89
+ # @type [undefined]
90
+ val2 = other.send(attr)
91
+ return val1 if val1 == val2
92
+ return val2 if val1.nil?
93
+ # @sg-ignore
94
+ val1.length > val2.length ? val1 : val2
95
+ end
96
+
97
+ # @param other [self]
98
+ # @return [::Array<YARD::Tags::Directive>, nil]
99
+ def combine_directives(other)
100
+ return self.directives if other.directives.empty?
101
+ return other.directives if directives.empty?
102
+ [directives + other.directives].uniq
103
+ end
104
+
105
+ # @param other [self]
106
+ # @return [String]
107
+ def combine_name(other)
108
+ if needs_consistent_name? || other.needs_consistent_name?
109
+ assert_same(other, :name)
110
+ else
111
+ choose(other, :name)
112
+ end
113
+ end
114
+
115
+ # @return [void]
116
+ def reset_generated!
117
+ # @return_type doesn't go here as subclasses tend to assign it
118
+ # themselves in constructors, and they will deal with setting
119
+ # it in any methods that call this
120
+ #
121
+ # @docstring also doesn't go here, as there is code which
122
+ # directly manipulates docstring without editing comments
123
+ # (e.g., Api::Map::Store#index processes overrides that way
124
+ #
125
+ # Same with @directives, @macros, @maybe_directives, which
126
+ # regenerate docstring
127
+ @deprecated = nil
128
+ reset_conversions
129
+ end
130
+
131
+ def needs_consistent_name?
132
+ true
133
+ end
134
+
135
+ # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected"
136
+ protected def equality_fields
137
+ [name, location, type_location, closure, source]
138
+ end
139
+
140
+ # @param other [self]
141
+ # @return [ComplexType]
142
+ def combine_return_type(other)
143
+ if return_type.undefined?
144
+ other.return_type
145
+ elsif other.return_type.undefined?
146
+ return_type
147
+ elsif dodgy_return_type_source? && !other.dodgy_return_type_source?
148
+ other.return_type
149
+ elsif other.dodgy_return_type_source? && !dodgy_return_type_source?
150
+ return_type
151
+ else
152
+ all_items = return_type.items + other.return_type.items
153
+ if all_items.any? { |item| item.selfy? } && all_items.any? { |item| item.rooted_tag == context.rooted_tag }
154
+ # assume this was a declaration that should have said 'self'
155
+ all_items.delete_if { |item| item.rooted_tag == context.rooted_tag }
156
+ end
157
+ ComplexType.new(all_items)
158
+ end
159
+ end
160
+
161
+ def dodgy_return_type_source?
162
+ # uses a lot of 'Object' instead of 'self'
163
+ location&.filename&.include?('core_ext/object/')
164
+ end
165
+
166
+ # when choices are arbitrary, make sure the choice is consistent
167
+ #
168
+ # @param other [Pin::Base]
169
+ # @param attr [::Symbol]
170
+ #
171
+ # @return [Object, nil]
172
+ def choose(other, attr)
173
+ results = [self, other].map(&attr).compact
174
+ # true and false are different classes and can't be sorted
175
+ return true if results.any? { |r| r == true || r == false }
176
+ results.min
177
+ rescue
178
+ STDERR.puts("Problem handling #{attr} for \n#{self.inspect}\n and \n#{other.inspect}\n\n#{self.send(attr).inspect} vs #{other.send(attr).inspect}")
179
+ raise
180
+ end
181
+
182
+ # @param other [self]
183
+ # @param attr [Symbol]
184
+ # @sg-ignore
185
+ # @return [undefined]
186
+ def choose_node(other, attr)
187
+ if other.object_id < attr.object_id
188
+ other.send(attr)
189
+ else
190
+ send(attr)
191
+ end
192
+ end
193
+
194
+ # @param other [self]
195
+ # @param attr [::Symbol]
196
+ # @sg-ignore
197
+ # @return [undefined]
198
+ def prefer_rbs_location(other, attr)
199
+ if rbs_location? && !other.rbs_location?
200
+ self.send(attr)
201
+ elsif !rbs_location? && other.rbs_location?
202
+ other.send(attr)
203
+ else
204
+ choose(other, attr)
205
+ end
206
+ end
207
+
208
+ def rbs_location?
209
+ type_location&.rbs?
210
+ end
211
+
212
+ # @param other [self]
213
+ # @return [void]
214
+ def assert_same_macros(other)
215
+ return unless self.source == :yardoc && other.source == :yardoc
216
+ assert_same_count(other, :macros)
217
+ assert_same_array_content(other, :macros) { |macro| macro.tag.name }
218
+ end
219
+
220
+ # @param other [self]
221
+ # @param attr [::Symbol]
222
+ # @return [void]
223
+ # @todo strong typechecking should complain when there are no block-related tags
224
+ def assert_same_array_content(other, attr, &block)
225
+ arr1 = send(attr)
226
+ raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable)
227
+ # @type arr1 [::Enumerable]
228
+ arr2 = other.send(attr)
229
+ raise "Expected #{attr} on #{other} to be an Enumerable, got #{arr2.class}" unless arr2.is_a?(::Enumerable)
230
+ # @type arr2 [::Enumerable]
231
+
232
+ # @sg-ignore
233
+ # @type [undefined]
234
+ values1 = arr1.map(&block)
235
+ # @type [undefined]
236
+ values2 = arr2.map(&block)
237
+ # @sg-ignore
238
+ return arr1 if values1 == values2
239
+ Solargraph.assert_or_log("combine_with_#{attr}".to_sym,
240
+ "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self values = #{values1}\nother values =#{attr} = #{values2}")
241
+ arr1
242
+ end
243
+
244
+ # @param other [self]
245
+ # @param attr [::Symbol]
246
+ #
247
+ # @return [::Enumerable]
248
+ def assert_same_count(other, attr)
249
+ # @type [::Enumerable]
250
+ arr1 = self.send(attr)
251
+ raise "Expected #{attr} on #{self} to be an Enumerable, got #{arr1.class}" unless arr1.is_a?(::Enumerable)
252
+ # @type [::Enumerable]
253
+ arr2 = other.send(attr)
254
+ raise "Expected #{attr} on #{other} to be an Enumerable, got #{arr2.class}" unless arr2.is_a?(::Enumerable)
255
+ return arr1 if arr1.count == arr2.count
256
+ Solargraph.assert_or_log("combine_with_#{attr}".to_sym,
257
+ "Inconsistent #{attr.inspect} count value between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{arr1.inspect}\nother.#{attr} = #{arr2.inspect}")
258
+ arr1
259
+ end
260
+
261
+ # @param other [self]
262
+ # @param attr [::Symbol]
263
+ #
264
+ # @return [Object, nil]
265
+ def assert_same(other, attr)
266
+ return false if other.nil?
267
+ val1 = send(attr)
268
+ val2 = other.send(attr)
269
+ return val1 if val1 == val2
270
+ Solargraph.assert_or_log("combine_with_#{attr}".to_sym,
271
+ "Inconsistent #{attr.inspect} values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}")
272
+ val1
273
+ end
274
+
275
+ # @param other [self]
276
+ # @param attr [::Symbol]
277
+ # @sg-ignore
278
+ # @return [undefined]
279
+ def choose_pin_attr_with_same_name(other, attr)
280
+ # @type [Pin::Base, nil]
281
+ val1 = send(attr)
282
+ # @type [Pin::Base, nil]
283
+ val2 = other.send(attr)
284
+ raise "Expected pin for #{attr} on\n#{self.inspect},\ngot #{val1.inspect}" unless val1.nil? || val1.is_a?(Pin::Base)
285
+ raise "Expected pin for #{attr} on\n#{other.inspect},\ngot #{val2.inspect}" unless val2.nil? || val2.is_a?(Pin::Base)
286
+ if val1&.name != val2&.name
287
+ Solargraph.assert_or_log("combine_with_#{attr}_name".to_sym,
288
+ "Inconsistent #{attr.inspect} name values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}")
289
+ end
290
+ choose_pin_attr(other, attr)
291
+ end
292
+
293
+ def choose_pin_attr(other, attr)
294
+ # @type [Pin::Base, nil]
295
+ val1 = send(attr)
296
+ # @type [Pin::Base, nil]
297
+ val2 = other.send(attr)
298
+ if val1.class != val2.class
299
+ Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym,
300
+ "Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}")
301
+ return val1
302
+ end
303
+ # arbitrary way of choosing a pin
304
+ [val1, val2].compact.min_by { _1.best_location.to_s }
305
+ end
306
+
49
307
  def assert_source_provided
50
308
  Solargraph.assert_or_log(:source, "source not provided - #{@path} #{@source} #{self.class}") if source.nil?
51
309
  end
@@ -144,6 +402,7 @@ module Solargraph
144
402
  # Pin equality is determined using the #nearly? method and also
145
403
  # requiring both pins to have the same location.
146
404
  #
405
+ # @param other [self]
147
406
  def == other
148
407
  return false unless nearly? other
149
408
  comments == other.comments && location == other.location
@@ -158,13 +417,13 @@ module Solargraph
158
417
 
159
418
  # @return [YARD::Docstring]
160
419
  def docstring
161
- parse_comments unless defined?(@docstring)
420
+ parse_comments unless @docstring
162
421
  @docstring ||= Solargraph::Source.parse_docstring('').to_docstring
163
422
  end
164
423
 
165
424
  # @return [::Array<YARD::Tags::Directive>]
166
425
  def directives
167
- parse_comments unless defined?(@directives)
426
+ parse_comments unless @directives
168
427
  @directives
169
428
  end
170
429
 
@@ -182,7 +441,7 @@ module Solargraph
182
441
  #
183
442
  # @return [Boolean]
184
443
  def maybe_directives?
185
- return !@directives.empty? if defined?(@directives)
444
+ return !@directives.empty? if defined?(@directives) && @directives
186
445
  @maybe_directives ||= comments.include?('@!')
187
446
  end
188
447
 
@@ -221,26 +480,6 @@ module Solargraph
221
480
  probe api_map
222
481
  end
223
482
 
224
- # Try to merge data from another pin. Merges are only possible if the
225
- # pins are near matches (see the #nearly? method). The changes should
226
- # not have any side effects on the API surface.
227
- #
228
- # @param pin [Pin::Base] The pin to merge into this one
229
- # @return [Boolean] True if the pins were merged
230
- def try_merge! pin
231
- return false unless nearly?(pin)
232
- @location = pin.location
233
- @closure = pin.closure
234
- return true if comments == pin.comments
235
- @comments = pin.comments
236
- @docstring = pin.docstring
237
- @return_type = pin.return_type
238
- @documentation = nil
239
- @deprecated = nil
240
- reset_conversions
241
- true
242
- end
243
-
244
483
  def proxied?
245
484
  @proxied ||= false
246
485
  end
@@ -313,6 +552,7 @@ module Solargraph
313
552
  "[#{inner_desc}]"
314
553
  end
315
554
 
555
+ # @return [String]
316
556
  def inspect
317
557
  "#<#{self.class} `#{self.inner_desc}`#{all_location_text} via #{source.inspect}>"
318
558
  end
@@ -343,6 +583,10 @@ module Solargraph
343
583
  # @return [ComplexType]
344
584
  attr_writer :return_type
345
585
 
586
+ attr_writer :docstring
587
+
588
+ attr_writer :directives
589
+
346
590
  private
347
591
 
348
592
  # @return [void]
@@ -21,6 +21,15 @@ module Solargraph
21
21
  @return_type = return_type
22
22
  end
23
23
 
24
+ def combine_with(other, attrs={})
25
+ attrs.merge({
26
+ assignment: assert_same(other, :assignment),
27
+ mass_assignment: assert_same(other, :mass_assignment),
28
+ return_type: combine_return_type(other),
29
+ })
30
+ super(other, attrs)
31
+ end
32
+
24
33
  def completion_item_kind
25
34
  Solargraph::LanguageServer::CompletionItemKinds::VARIABLE
26
35
  end
@@ -99,14 +108,6 @@ module Solargraph
99
108
  assignment == other.assignment
100
109
  end
101
110
 
102
- # @param pin [self]
103
- def try_merge! pin
104
- return false unless super
105
- @assignment = pin.assignment
106
- @return_type = pin.return_type
107
- true
108
- end
109
-
110
111
  def type_desc
111
112
  "#{super} = #{assignment&.type.inspect}"
112
113
  end