solargraph 0.60.2 → 0.60.4

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: 6827dc4bfa0ff6effa9c70ae5dc13bae988d29450b12bbfaca306bd4067f02ee
4
- data.tar.gz: a0301099ab04a1e2701971b819a51248abab41d49f03c83f724b1b1ec7d6b519
3
+ metadata.gz: 7c7397d5a9bbee07b12baa9372ce14f61216e5a310aa0f68605acabf8a9430e2
4
+ data.tar.gz: aeecf5c5d1ea7b1040fd3ae4bd9d1bff1685e468bd92b30dedfdd8ded2f1cd04
5
5
  SHA512:
6
- metadata.gz: 6973d2d9b0230a817e109553e92b9234830feb98429f46c0e1ad882f1f0a63574a9f38fc77d15303f8dac5f57e99815df0e501a798d37060d0753f60dc4d8e7b
7
- data.tar.gz: 42a08cb0653d48efffe32ab4c8dd01a7bee94bba8c8e55e8fc5fca953b382935415da7260e862808ae07e9bf61f504e95fdaf5e3aa165ae0fd2e2a919c59db87
6
+ metadata.gz: f4abae37c4fc8b57efe4a7b3550a2edbfc64455e8852f81660220f4a67facf101eafd058805b58cf0be6f9d5b9d16317cd21f6cd3b47f0c666c48418ee0d54c2
7
+ data.tar.gz: e14586769600dd63e2a96b8bcb4a5e4aa9bb045948e87e8c06b6606e711fed6816347051505eb756df55fed375a0419431dd02a44c2082d6d1ec0e2b13c6564f
@@ -129,12 +129,8 @@ jobs:
129
129
  - name: clone https://github.com/lekemula/solargraph-rspec/
130
130
  run: |
131
131
  cd ..
132
- # git clone https://github.com/lekemula/solargraph-rspec.git
133
-
134
- # pending https://github.com/lekemula/solargraph-rspec/pull/31
135
- git clone https://github.com/apiology/solargraph-rspec.git
132
+ git clone https://github.com/lekemula/solargraph-rspec.git
136
133
  cd solargraph-rspec
137
- git checkout test_solargraph_prereleases
138
134
  - name: Set up Ruby
139
135
  uses: ruby/setup-ruby@v1
140
136
  with:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## 0.60.4 - August 30, 2026
2
+ - Fix @yieldparam type lost on multi-overload block-form methods (#1289) (#1290)
3
+ - Read inline RBS superclass params only on the class line (#1301)
4
+ - Stop recording YARD's implicit Object superclass (#1315)
5
+ - Improve overload resolution and macro handling in Chain::Call (#1247)
6
+ - Fix Chain#nullable? leaking nil from earlier &. into later calls (#1271)
7
+ - Handle YARD bug with open singleton classes (#1279)
8
+ - Fix duck_types_match? to check the inferred type's own duck interface (#1295)
9
+ - Use the original repo (run_solargraph_rspec_specs job) (#1291)
10
+
11
+ ## 0.60.3 - August 5, 2026
12
+ - Pin rdoc to ~> 7.0 (#1216)
13
+ - (tag: v0.60.2) Release 0.60.2 (rdoc fix)
14
+ - Fix stale Hash::_Key expectations from upstream RBS core drift (#1224)
15
+ - Bump overcommit to ~> 0.71.0 (#1221)
16
+ - Resolve remote constants (#1234)
17
+ - Fix exponential blowup in Pin::Method#combine_same_type_arity_signatures (#1238)
18
+ - Respect #- yard comment-block separator (#1218)
19
+ - Typecheck attribute return tags against their backing ivar's type (#1219)
20
+ - Fix recursive-mutex deadlock in Library#sync_catalog (#1220)
21
+ - Fix Gemspecs#in_this_bundle? crash when no Gemfile is discoverable (#1225)
22
+ - Remove duplicate dead methods in RbsMap::Conversions (#1253)
23
+
1
24
  ## 0.60.2 - June 27, 2026
2
25
  - Extraneous debug output (#1214)
3
26
  - Pin rdoc to ~> 7.0 (#1216)
@@ -155,7 +155,7 @@ module Solargraph
155
155
  mixin = resolve(ref.name, ref.reference_gates)
156
156
  next unless mixin
157
157
 
158
- resolved = simple_resolve(name, mixin, internal)
158
+ resolved = resolve(name, mixin)
159
159
  return [resolved, gates[(idx + 1)..]] if resolved
160
160
  end
161
161
  end
@@ -403,11 +403,10 @@ module Solargraph
403
403
  # @param candidates [Array<Pin::BaseVariable>]
404
404
  # @param name [String]
405
405
  # @param closure [Pin::Closure]
406
- # @param location [Location]
406
+ # @param location [Location, nil]
407
407
  #
408
408
  # @return [Pin::BaseVariable, nil]
409
409
  def var_at_location candidates, name, closure, location
410
- # @todo Location can be nil if clips have trouble finding node recipients
411
410
  return unless location
412
411
 
413
412
  with_correct_name = candidates.select { |pin| pin.name == name }
@@ -227,7 +227,7 @@ module Solargraph
227
227
  expected = expected.downcast_to_literal_if_possible
228
228
  inferred = downcast_to_literal_if_possible
229
229
 
230
- return duck_types_match?(api_map, expected, inferred) if expected.duck_type?
230
+ return duck_types_match?(api_map, expected, inferred, rules) if expected.duck_type?
231
231
 
232
232
  if rules.include? :allow_any_match
233
233
  inferred.any? do |inf|
@@ -245,18 +245,31 @@ module Solargraph
245
245
  # @param api_map [ApiMap]
246
246
  # @param expected [ComplexType, UniqueType]
247
247
  # @param inferred [ComplexType, UniqueType]
248
+ # @param rules [Array<Symbol>]
248
249
  # @return [Boolean]
249
- def duck_types_match? api_map, expected, inferred
250
+ def duck_types_match? api_map, expected, inferred, rules = []
250
251
  raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type?
252
+ allow_any_match = rules.include?(:allow_any_match)
251
253
  expected.each do |exp|
252
254
  next unless exp.duck_type?
253
- quack = exp.to_s[1..]
254
- # @sg-ignore Need to add nil check here
255
- return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty?
255
+ quack = exp.to_s[1..] || ''
256
+ matched = allow_any_match ? inferred.any? { |inf| duck_type_provides?(api_map, inf, quack) } : inferred.all? { |inf| duck_type_provides?(api_map, inf, quack) }
257
+ return false unless matched
256
258
  end
257
259
  true
258
260
  end
259
261
 
262
+ # @param api_map [ApiMap]
263
+ # @param inf [UniqueType]
264
+ # @param quack [String]
265
+ # @return [Boolean]
266
+ def duck_type_provides? api_map, inf, quack
267
+ return true if inf.duck_type? && inf.to_s[1..] == quack
268
+
269
+ !api_map.get_method_stack(inf.namespace, quack, scope: inf.scope).empty?
270
+ end
271
+ private :duck_type_provides?
272
+
260
273
  # @return [String]
261
274
  def rooted_tags
262
275
  map(&:rooted_tag).join(', ')
@@ -605,8 +605,12 @@ module Solargraph
605
605
  queued_gemspec_cache.push(spec)
606
606
  return if pending - queued_gemspec_cache.length < 1
607
607
 
608
- catalog
609
- sync_catalog
608
+ # Try the next cacheable gemspec. This method is always called
609
+ # from inside sync_catalog's mutex (either directly or via this
610
+ # same recursive call), so recursing through sync_catalog here
611
+ # would try to re-lock a mutex this thread already holds and
612
+ # deadlock (https://github.com/castwide/solargraph/issues/1111).
613
+ cache_next_gemspec
610
614
  else
611
615
  logger.info "Caching #{spec.name} #{spec.version}"
612
616
  Thread.new do
@@ -27,7 +27,6 @@ module Solargraph
27
27
  source: :parser
28
28
  )
29
29
  pins.push nspin
30
- Solargraph.logger.warn "Superclass: #{superclass_name}" if superclass_name&.start_with?('Array')
31
30
  if superclass_name
32
31
  pins.push Pin::Reference::Superclass.new(
33
32
  location: loc,
@@ -41,12 +40,22 @@ module Solargraph
41
40
 
42
41
  private
43
42
 
44
- # @param comments [String]
43
+ # Type arguments for a generic superclass in inline RBS syntax, e.g.,
44
+ # `class Foo < Array #[String]`. Only recognized where RBS defines
45
+ # it: directly after the superclass, on the same line, with no space
46
+ # between `#` and `[`. Anything else is an ordinary comment.
47
+ #
45
48
  # @return [String, nil]
46
49
  def parameters_from_inline_rbs
47
- source = region.source.code_for(node)
48
- match = source.match(/[^\n]*?#\s?+\[([^\]]*)/)
49
- return unless match && match[1]
50
+ superclass = node.children[1]
51
+ return unless superclass
52
+
53
+ source = region.source.code
54
+ pos = get_node_end_position(superclass)
55
+ offset = Position.line_char_to_offset(source, pos.line, pos.character)
56
+ eol = source.index("\n", offset) || source.length
57
+ match = source[offset...eol].to_s.match(/\A\s*#\[([^\]]*)\]/)
58
+ return unless match
50
59
 
51
60
  code = match[1].strip
52
61
  return if code.empty?
@@ -148,12 +148,13 @@ module Solargraph
148
148
 
149
149
  # @param parameters [::Array<Parameter>]
150
150
  # @param return_type [ComplexType, nil]
151
+ # @param tag_docstring [YARD::Docstring] source of @yieldparam/@yieldreturn tags; pass an @overload tag's own docstring here
151
152
  # @return [Signature]
152
- def generate_signature parameters, return_type
153
+ def generate_signature parameters, return_type, tag_docstring = docstring
153
154
  # @type [Pin::Signature, nil]
154
155
  block = nil
155
- yieldparam_tags = docstring.tags(:yieldparam)
156
- yieldreturn_tags = docstring.tags(:yieldreturn)
156
+ yieldparam_tags = tag_docstring.tags(:yieldparam)
157
+ yieldreturn_tags = tag_docstring.tags(:yieldreturn)
157
158
  generics = docstring.tags(:generic).map(&:name)
158
159
  needs_block_param_signature =
159
160
  parameters.last&.block? || !yieldreturn_tags.empty? || !yieldparam_tags.empty?
@@ -509,11 +510,6 @@ module Solargraph
509
510
  #
510
511
  # @return [Array<Pin::Signature>]
511
512
  def combine_same_type_arity_signatures same_type_arity_signatures
512
- # @todo Stubbing this method while we debug an infinite loop bug in Ruby 3.x.
513
- # The body below is intentionally preserved for when the stub is removed.
514
- return same_type_arity_signatures
515
-
516
- # rubocop:disable Lint/UnreachableCode
517
513
  # This is an O(n^2) operation, so bail out if n is not small
518
514
  return same_type_arity_signatures if same_type_arity_signatures.length > 10
519
515
 
@@ -521,7 +517,9 @@ module Solargraph
521
517
  # @param new_signature [Pin::Signature]
522
518
  same_type_arity_signatures.reduce([]) do |old_signatures, new_signature|
523
519
  next old_signatures + [new_signature] if old_signatures.empty?
524
- old_signatures.flat_map do |old_signature|
520
+
521
+ merged = false
522
+ combined = old_signatures.map do |old_signature|
525
523
  potential_new_signature = old_signature.combine_with(new_signature)
526
524
 
527
525
  if potential_new_signature.type_arity == old_signature.type_arity
@@ -534,13 +532,14 @@ module Solargraph
534
532
  # based on types, not just arity, allowing for type
535
533
  # information describing how methods behave based on
536
534
  # their input types)
537
- old_signatures - [old_signature] + [potential_new_signature]
535
+ merged = true
536
+ potential_new_signature
538
537
  else
539
- old_signatures + [new_signature]
538
+ old_signature
540
539
  end
541
540
  end
541
+ merged ? combined : old_signatures + [new_signature]
542
542
  end
543
- # rubocop:enable Lint/UnreachableCode
544
543
  end
545
544
 
546
545
  # @param name [String]
@@ -749,7 +748,9 @@ module Solargraph
749
748
  top_type = generate_complex_type
750
749
  result = []
751
750
  result.push generate_signature(parameters, top_type) if top_type.defined?
752
- result.concat(overloads.map { |meth| generate_signature(meth.parameters, meth.return_type) }) unless overloads.empty?
751
+ # @param meth [Pin::Signature]
752
+ # @param tag [YARD::Tags::OverloadTag]
753
+ result.concat(overloads.zip(docstring.tags(:overload).select(&:parameters)).map { |meth, tag| generate_signature(meth.parameters, meth.return_type, tag.docstring) }) unless overloads.empty?
753
754
  result.push generate_signature(parameters, @return_type || ComplexType::UNDEFINED) if result.empty?
754
755
  result
755
756
  end
@@ -159,27 +159,6 @@ module Solargraph
159
159
  RBS_TO_CLASS.fetch(ns, ns)
160
160
  end
161
161
 
162
- # @param type_name [RBS::TypeName]
163
- # @param type_args [Enumerable<RBS::Types::Bases::Base>]
164
- # @return [ComplexType::UniqueType]
165
- def build_type type_name, type_args = []
166
- # we use .absolute? below to tell the type object what to
167
- # expect
168
- rbs_name = type_name.relative!.to_s
169
- base = RBS_TO_CLASS.fetch(rbs_name, rbs_name)
170
-
171
- params = type_args.map { |a| RbsTranslator.to_complex_type(a) }
172
- # @todo Tuples are in flux
173
- # tuples have their own class and are handled in other_type_to_type
174
- if base == 'Hash' && params.length == 2
175
- ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: type_name.absolute?,
176
- parameters_type: :hash)
177
- else
178
- ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: type_name.absolute?,
179
- parameters_type: :list)
180
- end
181
- end
182
-
183
162
  # @param decl [RBS::AST::Declarations::Module::Self]
184
163
  # @param closure [Pin::Namespace]
185
164
  # @return [void]
@@ -561,7 +540,8 @@ module Solargraph
561
540
  def method_def_to_sigs decl, pin
562
541
  # rubocop:disable Style/SafeNavigationChainLength
563
542
  implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') || false
564
- # rubocop:enable Style/SafeNavigationChainLength # @param overload [RBS::AST::Members::MethodDefinition::Overload]
543
+ # rubocop:enable Style/SafeNavigationChainLength
544
+ # @param overload [RBS::AST::Members::MethodDefinition::Overload]
565
545
  decl.overloads.map do |overload|
566
546
  type_location = location_decl_to_pin_location(overload.method_type.location)
567
547
  generics = overload.method_type.type_params.map(&:name).map(&:to_s)
@@ -587,84 +567,6 @@ module Solargraph
587
567
  Location.new(location.name.to_s, range)
588
568
  end
589
569
 
590
- # @param type [RBS::MethodType, RBS::Types::Block]
591
- # @param pin [Pin::Method]
592
- # @param implicit_nil [Boolean]
593
- # @return [Array(Array<Pin::Parameter>, ComplexType)]
594
- def parts_of_function type, pin, implicit_nil
595
- type_location = pin.type_location
596
- if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction)
597
- return [
598
- [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs,
599
- type_location: type_location)],
600
- method_type_to_type(type, implicit_nil)
601
- ]
602
- end
603
-
604
- parameters = []
605
- arg_num = -1
606
- type.type.required_positionals.each do |param|
607
- # @sg-ignore Unresolved call to name
608
- name = param.name ? param.name.to_s : "arg_#{arg_num += 1}"
609
- parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin,
610
- # @sg-ignore RBS generic type understanding issue
611
- return_type: other_type_to_type(param.type),
612
- source: :rbs, type_location: type_location)
613
- end
614
- type.type.optional_positionals.each do |param|
615
- # @sg-ignore Unresolved call to name
616
- name = param.name ? param.name.to_s : "arg_#{arg_num += 1}"
617
- parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin,
618
- # @sg-ignore RBS generic type understanding issue
619
- return_type: other_type_to_type(param.type),
620
- type_location: type_location,
621
- source: :rbs)
622
- end
623
- if type.type.rest_positionals
624
- name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg_#{arg_num += 1}"
625
- inner_rest_positional_type = other_type_to_type(type.type.rest_positionals.type)
626
- rest_positional_type = ComplexType::UniqueType.new('Array',
627
- [],
628
- [inner_rest_positional_type],
629
- rooted: true, parameters_type: :list)
630
- parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin,
631
- source: :rbs, type_location: type_location,
632
- return_type: rest_positional_type)
633
- end
634
- type.type.trailing_positionals.each do |param|
635
- # @sg-ignore Unresolved call to name
636
- name = param.name ? param.name.to_s : "arg_#{arg_num += 1}"
637
- parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs,
638
- type_location: type_location)
639
- end
640
- type.type.required_keywords.each do |orig, param|
641
- # @sg-ignore Unresolved call to to_s
642
- name = orig ? orig.to_s : "arg_#{arg_num += 1}"
643
- parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin,
644
- # @sg-ignore RBS generic type understanding issue
645
- return_type: other_type_to_type(param.type),
646
- source: :rbs, type_location: type_location)
647
- end
648
- type.type.optional_keywords.each do |orig, param|
649
- # @sg-ignore Unresolved call to to_s
650
- name = orig ? orig.to_s : "arg_#{arg_num += 1}"
651
- parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin,
652
- # @sg-ignore RBS generic type understanding issue
653
- return_type: other_type_to_type(param.type),
654
- type_location: type_location,
655
- source: :rbs)
656
- end
657
- if type.type.rest_keywords
658
- name = type.type.rest_keywords.name ? type.type.rest_keywords.name.to_s : "arg_#{arg_num += 1}"
659
- parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg,
660
- name: type.type.rest_keywords.name.to_s, closure: pin,
661
- source: :rbs, type_location: type_location)
662
- end
663
-
664
- return_type = method_type_to_type(type, implicit_nil)
665
- [parameters, return_type]
666
- end
667
-
668
570
  # @param type [RBS::MethodType,RBS::Types::Block]
669
571
  # @param pin [Pin::Method]
670
572
  # @param implicit_nil [Boolean]
@@ -14,7 +14,7 @@ module Solargraph
14
14
  # @return [String]
15
15
  attr_reader :word
16
16
 
17
- # @return [Location]
17
+ # @return [Location, nil]
18
18
  attr_reader :location
19
19
 
20
20
  # @return [::Array<Chain>]
@@ -71,6 +71,95 @@ module Solargraph
71
71
 
72
72
  private
73
73
 
74
+ # Checks whether a single overload signature matches the call's
75
+ # arguments/block and, if so, resolves its return type. Threaded
76
+ # through the caller's accumulator so behavior matches the
77
+ # original inline loop: if the overload doesn't match, the
78
+ # incoming type/signature are returned unchanged.
79
+ #
80
+ # @param overload [Pin::Signature]
81
+ # @param pin [Pin::Method]
82
+ # @param api_map [ApiMap]
83
+ # @param name_pin [Pin::Base]
84
+ # @param locals [::Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]
85
+ # @param type [ComplexType]
86
+ # @param new_signature_pin [Pin::Signature, nil]
87
+ # @return [::Array(ComplexType, Pin::Signature)]
88
+ def match_overload_type overload, pin, api_map, name_pin, locals, type, new_signature_pin
89
+ return [type, new_signature_pin] unless overload.arity_matches?(arguments, with_block?)
90
+
91
+ match = true
92
+ atypes = []
93
+ arguments.each_with_index do |arg, idx|
94
+ param = overload.parameters[idx]
95
+ if param.nil?
96
+ match = overload.parameters.any?(&:restarg?)
97
+ break
98
+ end
99
+ arg_name_pin = Pin::ProxyType.anonymous(name_pin.context,
100
+ closure: name_pin.closure,
101
+ gates: name_pin.gates,
102
+ source: :chain)
103
+ atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals)
104
+ # @sg-ignore flow sensitive typing should handle is_a? and next
105
+ unless param.compatible_arg?(atype, api_map) || param.restarg?
106
+ match = false
107
+ break
108
+ end
109
+ end
110
+ return [type, new_signature_pin] unless match
111
+
112
+ if overload.block && with_block?
113
+ block_atypes = overload.block.parameters.map(&:return_type)
114
+ # @todo Need to add nil check here
115
+ # @sg-ignore Need to add nil check here
116
+ blocktype = if block.links.map(&:class) == [BlockSymbol]
117
+ # like the bar in foo(&:bar)
118
+ block_symbol_call_type(api_map, name_pin.context, block_atypes, locals)
119
+ else
120
+ block_call_type(api_map, name_pin, locals)
121
+ end
122
+ end
123
+ new_signature_pin = overload.resolve_generics_from_context_until_complete(overload.generics, atypes, nil, nil,
124
+ blocktype)
125
+ # @todo It shouldn't be necessary to choose either generics or macros
126
+ # @sg-ignore Need to add nil check here
127
+ new_return_type = if new_signature_pin.return_type.defined?
128
+ # @sg-ignore Need to add nil check here
129
+ new_signature_pin.return_type
130
+ else
131
+ # @sg-ignore Need to add nil check here
132
+ named_types = pin.parameter_names.zip(arguments.map { |arg| ComplexType.try_parse(simple_convert(arg.node).to_s) }).to_h
133
+ pin.typify(api_map).expand(named_types)
134
+ end
135
+ self_type = if head?
136
+ # If we're at the head of the chain, we called a
137
+ # method somewhere that marked itself as returning
138
+ # self. Given we didn't invoke this on an object,
139
+ # this must be a method in this same class - so we
140
+ # use our own self type
141
+ name_pin.context
142
+ else
143
+ # if we're past the head in the chain, whatever the
144
+ # type of the lhs side is what 'self' will be in its
145
+ # declaration - we can't just use the type of the
146
+ # method pin, as this might be a subclass of the
147
+ # place where the method is defined
148
+ name_pin.binder
149
+ end
150
+ # This same logic applies to the YARD work done by
151
+ # 'with_params()'.
152
+ #
153
+ # qualify(), however, happens in the namespace where
154
+ # the docs were written - from the method pin.
155
+ # @todo Need to add nil check here
156
+ if new_return_type.defined?
157
+ type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, *pin.gates)
158
+ end
159
+ type ||= ComplexType::UNDEFINED
160
+ [type, new_signature_pin]
161
+ end
162
+
74
163
  # @param pins [::Enumerable<Pin::Base>]
75
164
  # @param api_map [ApiMap]
76
165
  # @param name_pin [Pin::Base]
@@ -96,76 +185,20 @@ module Solargraph
96
185
  # @sg-ignore flow sensitive typing should handle is_a? and next
97
186
  # @param ol [Pin::Signature]
98
187
  sorted_overloads.each do |ol|
99
- next unless ol.arity_matches?(arguments, with_block?)
100
- match = true
101
-
102
- atypes = []
103
- arguments.each_with_index do |arg, idx|
104
- param = ol.parameters[idx]
105
- if param.nil?
106
- match = ol.parameters.any?(&:restarg?)
107
- break
108
- end
109
- arg_name_pin = Pin::ProxyType.anonymous(name_pin.context,
110
- closure: name_pin.closure,
111
- gates: name_pin.gates,
112
- source: :chain)
113
- atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals)
114
- unless param.compatible_arg?(atype, api_map) || param.restarg?
115
- match = false
116
- break
117
- end
118
- end
119
- if match
120
- if ol.block && with_block?
121
- block_atypes = ol.block.parameters.map(&:return_type)
122
- # @todo Need to add nil check here
123
- blocktype = if block.links.map(&:class) == [BlockSymbol]
124
- # like the bar in foo(&:bar)
125
- block_symbol_call_type(api_map, name_pin.context, block_atypes, locals)
126
- else
127
- block_call_type(api_map, name_pin, locals)
128
- end
129
- end
130
- new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes, nil, nil,
131
- blocktype)
132
- # @todo It shouldn't be necessary to choose either generics or macros
133
- new_return_type = if new_signature_pin.return_type.defined?
134
- new_signature_pin.return_type
135
- else
136
- named_types = p.parameter_names.zip(arguments.map { |arg| ComplexType.try_parse(simple_convert(arg.node).to_s) }).to_h
137
- p.typify(api_map).expand(named_types)
138
- end
139
- self_type = if head?
140
- # If we're at the head of the chain, we called a
141
- # method somewhere that marked itself as returning
142
- # self. Given we didn't invoke this on an object,
143
- # this must be a method in this same class - so we
144
- # use our own self type
145
- name_pin.context
146
- else
147
- # if we're past the head in the chain, whatever the
148
- # type of the lhs side is what 'self' will be in its
149
- # declaration - we can't just use the type of the
150
- # method pin, as this might be a subclass of the
151
- # place where the method is defined
152
- name_pin.binder
153
- end
154
- # This same logic applies to the YARD work done by
155
- # 'with_params()'.
156
- #
157
- # qualify(), however, happens in the namespace where
158
- # the docs were written - from the method pin.
159
- # @todo Need to add nil check here
160
- if new_return_type.defined?
161
- type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, *p.gates)
162
- end
163
- type ||= ComplexType::UNDEFINED
164
- end
188
+ type, new_signature_pin = match_overload_type(ol, p, api_map, name_pin, locals, type, new_signature_pin)
165
189
  break if type.defined?
166
190
  end
167
191
  p = p.with_single_signature(new_signature_pin) unless new_signature_pin.nil?
168
192
  next p.proxy(type) if type.defined?
193
+ if !p.macros.empty?
194
+ result = process_macro(p, api_map, name_pin.context, locals)
195
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
196
+ next result unless result.return_type.undefined?
197
+ elsif !p.directives.empty?
198
+ result = process_directive(p, api_map, name_pin.context, locals)
199
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
200
+ next result unless result.return_type.undefined?
201
+ end
169
202
  p
170
203
  end
171
204
  logger.debug do
@@ -186,6 +219,71 @@ module Solargraph
186
219
  end
187
220
  end
188
221
 
222
+ # @param pin [Pin::Base]
223
+ # @param api_map [ApiMap]
224
+ # @param context [ComplexType, ComplexType::UniqueType]
225
+ # @param locals [::Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]
226
+ # @return [Pin::Base]
227
+ def process_macro pin, api_map, context, locals
228
+ pin.macros.each do |macro|
229
+ # @todo 'Wrong argument type for
230
+ # Solargraph::Source::Chain::Call#inner_process_macro:
231
+ # macro expected YARD::Tags::MacroDirective, received
232
+ # generic<Elem>' is because we lose 'rooted' information
233
+ # in the 'Chain::Array' class internally, leaving
234
+ # ::Array#each shadowed when it shouldn't be.
235
+ # @sg-ignore macro is Solargraph::YardMap::Macro, wraps a YARD::Tags::MacroDirective
236
+ result = inner_process_macro(pin, macro, api_map, context, locals)
237
+ return result unless result.return_type.undefined?
238
+ end
239
+ Pin::ProxyType.anonymous(ComplexType::UNDEFINED, source: :chain)
240
+ end
241
+
242
+ # @param pin [Pin::Method]
243
+ # @param api_map [ApiMap]
244
+ # @param context [ComplexType, ComplexType::UniqueType]
245
+ # @param locals [::Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]
246
+ # @return [Pin::ProxyType]
247
+ def process_directive pin, api_map, context, locals
248
+ pin.directives.each do |dir|
249
+ macro = api_map.named_macro(dir.tag.name)
250
+ next if macro.nil?
251
+ # @sg-ignore macro is Solargraph::YardMap::Macro, wraps a YARD::Tags::MacroDirective
252
+ result = inner_process_macro(pin, macro, api_map, context, locals)
253
+ return result unless result.return_type.undefined?
254
+ end
255
+ Pin::ProxyType.anonymous ComplexType::UNDEFINED, source: :chain
256
+ end
257
+
258
+ # @param pin [Pin::Base]
259
+ # @param macro [YARD::Tags::MacroDirective]
260
+ # @param api_map [ApiMap]
261
+ # @param context [ComplexType, ComplexType::UniqueType]
262
+ # @param locals [::Array<Pin::LocalVariable, Pin::Parameter>]
263
+ # @return [Pin::ProxyType]
264
+ def inner_process_macro pin, macro, api_map, context, locals
265
+ vals = arguments.map { |c| Pin::ProxyType.anonymous(c.infer(api_map, pin, locals), source: :chain) }
266
+ txt = macro.tag.text.clone
267
+ # @sg-ignore Need to add nil check here
268
+ if txt.empty? && macro.tag.name
269
+ named = api_map.named_macro(macro.tag.name)
270
+ txt = named.tag.text.clone if named
271
+ end
272
+ i = 1
273
+ vals.each do |v|
274
+ # @sg-ignore Need to add nil check here
275
+ txt.gsub!(/\$#{i}/, v.context.namespace)
276
+ i += 1
277
+ end
278
+ # @sg-ignore Need to add nil check here
279
+ docstring = Solargraph::Source.parse_docstring(txt).to_docstring
280
+ tag = docstring.tag(:return)
281
+ unless tag.nil? || tag.types.nil?
282
+ return Pin::ProxyType.anonymous(ComplexType.try_parse(*tag.types), source: :chain)
283
+ end
284
+ Pin::ProxyType.anonymous(ComplexType::UNDEFINED, source: :chain)
285
+ end
286
+
189
287
  # @param docstring [YARD::Docstring]
190
288
  # @param context [ComplexType]
191
289
  # @return [ComplexType, nil]
@@ -13,10 +13,6 @@ module Solargraph
13
13
  @location = location
14
14
  end
15
15
 
16
- # @sg-ignore Declared return type
17
- # ::Array<::Solargraph::Pin::Base> does not match inferred
18
- # type ::Array<::Solargraph::Pin::BaseVariable, ::NilClass>
19
- # for Solargraph::Source::Chain::InstanceVariable#resolve
20
16
  def resolve api_map, name_pin, locals
21
17
  ivars = api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select do |p|
22
18
  p.name == word
@@ -27,8 +23,7 @@ module Solargraph
27
23
 
28
24
  private
29
25
 
30
- # @todo: Missed nil violation
31
- # @return [Location]
26
+ # @return [Location, nil]
32
27
  attr_reader :location
33
28
  end
34
29
  end
@@ -168,7 +168,7 @@ module Solargraph
168
168
  return ComplexType::UNDEFINED
169
169
  end
170
170
  type = infer_from_definitions(pins, links.last.last_context, api_map, locals)
171
- out = maybe_nil(type)
171
+ out = maybe_nil(type, api_map)
172
172
  logger.debug do
173
173
  "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}, " \
174
174
  "name_pin=#{name_pin}, name_pin.closure=#{name_pin&.closure&.inspect}, " \
@@ -199,8 +199,38 @@ module Solargraph
199
199
  @splat
200
200
  end
201
201
 
202
- def nullable?
203
- links.any?(&:nullable?)
202
+ # Whether this chain's inferred type should have nil added to it
203
+ # because a `&.` earlier in the chain might have short-circuited
204
+ # evaluation to nil.
205
+ #
206
+ # A `&.` only makes the *immediate* call nil-or-normal-result; a
207
+ # later, non-safe-navigated call in the same chain is still
208
+ # invoked on whatever that produced. If receiver is nil at that
209
+ # point, Ruby calls the method on nil itself instead of skipping
210
+ # it - so nil only continues to flow through subsequent links
211
+ # that NilClass defines as returning `self` (e.g., #tap,
212
+ # #itself). Any other subsequent call resolves to a concrete,
213
+ # non-nil-including type (e.g. NilClass#== returns Boolean), and
214
+ # nil no longer needs to be tracked past that point (unless a
215
+ # later `&.` reintroduces it).
216
+ #
217
+ # @param api_map [ApiMap]
218
+ # @return [Boolean]
219
+ def nullable? api_map
220
+ currently_nullable = false
221
+ links.each do |link|
222
+ if link.nullable?
223
+ currently_nullable = true
224
+ next
225
+ end
226
+ next unless currently_nullable
227
+ next unless link.is_a?(Chain::Call)
228
+
229
+ pin = api_map.get_method_stack('NilClass', link.word, scope: :instance).first
230
+ # @sg-ignore Need to add nil check here
231
+ currently_nullable = pin.nil? || pin.return_type.tag == 'self'
232
+ end
233
+ currently_nullable
204
234
  end
205
235
 
206
236
  include Logging
@@ -286,10 +316,11 @@ module Solargraph
286
316
  end
287
317
 
288
318
  # @param type [ComplexType, ComplexType::UniqueType]
319
+ # @param api_map [ApiMap]
289
320
  # @return [ComplexType, ComplexType::UniqueType]
290
- def maybe_nil type
321
+ def maybe_nil type, api_map
291
322
  return type if type.undefined? || type.void? || type.nullable?
292
- return type unless nullable?
323
+ return type unless nullable?(api_map)
293
324
  ComplexType.new(type.items + [ComplexType::NIL])
294
325
  end
295
326
 
@@ -209,7 +209,7 @@ module Solargraph
209
209
  stringified_comments[rng.start.line] ||= begin
210
210
  # @sg-ignore Need to add nil check here
211
211
  buff = associated_comments[rng.start.line]
212
- buff ? stringify_comment_array(buff) : nil
212
+ stringify_comment_array(buff)
213
213
  end
214
214
  end
215
215
 
@@ -249,20 +249,20 @@ module Solargraph
249
249
 
250
250
  # Get a hash of comments grouped by the line numbers of the associated code.
251
251
  #
252
- # @return [Hash{Integer => String, nil}]
252
+ # @return [Hash{Integer => Array<String>}]
253
253
  def associated_comments
254
254
  @associated_comments ||= begin
255
255
  # @type [Hash{Integer => String}]
256
256
  result = {}
257
- buffer = String.new('')
257
+ buffer = []
258
258
  # @type [Integer, nil]
259
259
  last = nil
260
260
  comments.each_pair do |num, snip|
261
261
  if !last || num == last + 1
262
- buffer.concat "#{snip.text}\n"
262
+ buffer.push "#{snip.text}\n"
263
263
  else
264
264
  result[first_not_empty_from(last + 1)] = buffer.clone
265
- buffer.replace "#{snip.text}\n"
265
+ buffer.replace ["#{snip.text}\n"]
266
266
  end
267
267
  last = num
268
268
  end
@@ -305,27 +305,31 @@ module Solargraph
305
305
 
306
306
  # Get a string representation of an array of comments.
307
307
  #
308
- # @param comments [String]
308
+ # @param comments [Array<String>, nil]
309
309
  # @return [String]
310
310
  def stringify_comment_array comments
311
- ctxt = String.new('')
311
+ ctxt = []
312
312
  started = false
313
313
  skip = nil
314
- comments.lines.each do |l|
314
+ comments&.each do |l|
315
+ if l =~ /^#-\R/
316
+ ctxt.clear
317
+ next
318
+ end
315
319
  # Trim the comment and minimum leading whitespace
316
320
  p = l.force_encoding('UTF-8').encode('UTF-8', invalid: :replace, replace: '?').gsub(/^#+/, '')
317
321
  if p.strip.empty?
318
322
  next unless started
319
- ctxt.concat p
323
+ ctxt.push p
320
324
  else
321
325
  here = p.index(/[^ \t]/)
322
326
  # @sg-ignore flow sensitive typing should be able to handle redefinition
323
327
  skip = here if skip.nil? || here < skip
324
- ctxt.concat p[skip..]
328
+ ctxt.push p[skip..]
325
329
  end
326
330
  started = true
327
331
  end
328
- ctxt
332
+ ctxt.join('')
329
333
  end
330
334
 
331
335
  # A hash of line numbers and their associated comments.
@@ -145,8 +145,8 @@ module Solargraph
145
145
  )
146
146
  end
147
147
  # @sg-ignore Need to add nil check here
148
- com_pos = Position.new(line + 1 - comments.lines.length, 0)
149
- process_comment(src_pos, com_pos, comments)
148
+ com_pos = Position.new(line + 1 - comments.length, 0)
149
+ process_comment(src_pos, com_pos, comments.join(''))
150
150
  end
151
151
  rescue StandardError => e
152
152
  raise e.class, "Error processing comment directives in #{@filename}: #{e.message}"
@@ -162,10 +162,19 @@ module Solargraph
162
162
  result.push Problem.new(pin.location, "Untyped method #{pin.path} could not be inferred")
163
163
  end
164
164
  elsif rules.validate_tags?
165
- unless pin.node.nil? || declared.void? || virtual_pin?(pin) || abstract?(pin)
165
+ # Attribute pins never have a node (attr_reader/attr_writer/attr_accessor are
166
+ # synthesized, not parsed method bodies), but their inferred type is still
167
+ # meaningful: #probe walks the backing ivar's assignments via infer_from_iv.
168
+ # Without this carve-out, a manually-written @return tag on an attribute is
169
+ # never cross-checked against how the ivar is actually assigned (e.g. a nilable
170
+ # default in the constructor), so a bad tag silently passes at every level.
171
+ unless (pin.node.nil? && !pin.attribute?) || declared.void? || virtual_pin?(pin) || abstract?(pin)
166
172
  inferred = pin.probe(api_map).self_to_type(pin.full_context)
167
173
  if inferred.undefined?
168
- unless rules.ignore_all_undefined? || external?(pin)
174
+ # An attribute with no locally-discoverable ivar assignment (set via a
175
+ # mixin, metaprogramming, etc.) isn't a tag/inference mismatch - just
176
+ # unproven. Only methods with an actual body get flagged for this.
177
+ unless rules.ignore_all_undefined? || external?(pin) || pin.attribute?
169
178
  result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin)
170
179
  end
171
180
  else
@@ -848,7 +857,7 @@ module Solargraph
848
857
  def all_sg_ignore_lines
849
858
  source.associated_comments.select do |_line, text|
850
859
  # @sg-ignore Need to add nil check here
851
- text.include?('@sg-ignore')
860
+ text.any? { |t| t.include?('@sg-ignore') }
852
861
  end.keys.to_set
853
862
  end
854
863
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solargraph
4
- VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.60.2')
4
+ VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.60.4')
5
5
  end
@@ -220,6 +220,12 @@ module Solargraph
220
220
  # @sg-ignore need boolish support for ? methods
221
221
  def in_this_bundle?
222
222
  Bundler.definition&.lockfile&.to_s&.start_with?(directory)
223
+ rescue Bundler::GemfileNotFound
224
+ # Solargraph itself isn't running under a discoverable Gemfile
225
+ # (e.g. installed and invoked as a standalone gem), so it can't
226
+ # be "this bundle" - fall back to treating the workspace as an
227
+ # external bundle.
228
+ false
223
229
  end
224
230
 
225
231
  # @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
@@ -11,7 +11,7 @@ module Solargraph
11
11
  # @param spec [Gem::Specification, nil]
12
12
  def initialize code_objects, spec = nil
13
13
  @code_objects = code_objects
14
- @macro_code_objects = code_objects.select { |co| co.is_a?(YARD::CodeObjects::MacroObject) }
14
+ @macro_code_objects = code_objects.grep(YARD::CodeObjects::MacroObject)
15
15
  @spec = spec
16
16
  # @type [Array<Solargraph::Pin::Base>]
17
17
  @pins = []
@@ -32,13 +32,17 @@ module Solargraph
32
32
 
33
33
  private
34
34
 
35
+ def core_store
36
+ @core_store ||= ApiMap::Store.new(RbsMap::CoreMap.new.pins)
37
+ end
38
+
35
39
  # @param code_object [YARD::CodeObjects::Base]
36
40
  # @return [Array<Pin::Base>]
37
41
  def generate_pins code_object
38
42
  result = []
39
43
  case code_object
40
44
  when YARD::CodeObjects::NamespaceObject
41
- nspin = ToNamespace.make(code_object, @spec, @namespace_pins[code_object.namespace.to_s])
45
+ nspin = namespace_with_bug_fix(code_object)
42
46
  @namespace_pins[code_object.path] = nspin
43
47
  result.push nspin
44
48
  if code_object.is_a?(YARD::CodeObjects::ClassObject) && !code_object.superclass.nil?
@@ -50,7 +54,16 @@ module Solargraph
50
54
  else
51
55
  code_object.superclass.to_s
52
56
  end
53
- result.push Solargraph::Pin::Reference::Superclass.new(name: superclass, closure: nspin, source: :yard_map)
57
+ # YARD fills in `Object` for every class whose definition it never
58
+ # saw a superclass clause on - including a bare `class Foo`
59
+ # reopening, which asserts nothing about ancestry - and keeps no
60
+ # record of which case it was. A class with no superclass reference
61
+ # already resolves to Object, so recording this one adds nothing and
62
+ # can shadow a superclass declared in another source.
63
+ unless superclass.delete_prefix('::') == 'Object'
64
+ result.push Solargraph::Pin::Reference::Superclass.new(name: superclass, closure: nspin,
65
+ source: :yard_map)
66
+ end
54
67
  end
55
68
  # @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName'
56
69
  code_object.class_mixins.each do |m|
@@ -72,6 +85,8 @@ module Solargraph
72
85
  # @todo Check the visibility of <Class>.new
73
86
  result.push ToMethod.make(code_object, 'new', :class, :public, closure, @spec)
74
87
  result.push ToMethod.make(code_object, 'initialize', :instance, :private, closure, @spec)
88
+ elsif closure&.source == :yard_map_hack
89
+ result.push ToMethod.make(code_object, nil, :instance, nil, closure, @spec)
75
90
  else
76
91
  result.push ToMethod.make(code_object, nil, nil, nil, closure, @spec)
77
92
  end
@@ -97,6 +112,24 @@ module Solargraph
97
112
  def macros_for_method_object method_object
98
113
  attached_macros_by_method_object[method_object]
99
114
  end
115
+
116
+ # Handle a bug in YARD where code that opens a constant's singleton class
117
+ # (e.g., `class << ENV`) creates a pin that incorrectly overrides the
118
+ # original constant's value.
119
+ #
120
+ # @param code_object [YARD::CodeObjects::NamespaceObject]
121
+ # @return [Pin::Namespace]
122
+ def namespace_with_bug_fix code_object
123
+ core_pin = core_store.get_path_pins(code_object.path)
124
+ .find { |pin| pin.is_a?(Pin::Constant) }
125
+ if core_pin
126
+ location = Helpers.object_location(code_object, @spec)
127
+ Solargraph.logger.warn "Adjusting YARD pin that conflicts with RBS core (#{code_object.inspect} #{location.inspect})"
128
+ Pin::Namespace.new(name: core_pin.return_type.namespace, location: location, source: :yard_map_hack)
129
+ else
130
+ ToNamespace.make(code_object, @spec, @namespace_pins[code_object.namespace.to_s])
131
+ end
132
+ end
100
133
  end
101
134
  end
102
135
  end
data/solargraph.gemspec CHANGED
@@ -65,7 +65,7 @@ Gem::Specification.new do |s|
65
65
  #
66
66
  # even more specific on RuboCop itself, which is written into _todo
67
67
  # file.
68
- s.add_development_dependency 'overcommit', '~> 0.68.0'
68
+ s.add_development_dependency 'overcommit', '~> 0.71.0'
69
69
  s.add_development_dependency 'rubocop', '~> 1.80.0.0'
70
70
  s.add_development_dependency 'rubocop-rake', '~> 0.7.1'
71
71
  s.add_development_dependency 'rubocop-rspec', '~> 3.6.0'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solargraph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.60.2
4
+ version: 0.60.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Snyder
@@ -419,14 +419,14 @@ dependencies:
419
419
  requirements:
420
420
  - - "~>"
421
421
  - !ruby/object:Gem::Version
422
- version: 0.68.0
422
+ version: 0.71.0
423
423
  type: :development
424
424
  prerelease: false
425
425
  version_requirements: !ruby/object:Gem::Requirement
426
426
  requirements:
427
427
  - - "~>"
428
428
  - !ruby/object:Gem::Version
429
- version: 0.68.0
429
+ version: 0.71.0
430
430
  - !ruby/object:Gem::Dependency
431
431
  name: rubocop
432
432
  requirement: !ruby/object:Gem::Requirement