Skip to content

Commit 42f5643

Browse files
apiologyclaude
andcommitted
Address PR review comments on #1223
- Add UniqueType#singleton? predicate for nil/true/false, replacing the hardcoded name array in dispatch_literal? - Merge literal_param_arg_matches? into Pin::Parameter#compatible_arg? (its only caller) instead of threading a second, redundant typify call through Source::Chain::Call - Add ComplexType#without_redundant_literals, pulling the literal/non-literal union dedup out of Pin::BaseVariable#probe and into the type hierarchy - rbs_translator.rb: fix @param type [RBS::Types::Bases::Base] annotations that were actually too narrow (the real RBS type is RBS::Types::t, a union most RBS type classes do not inherit Bases::Base from). Removes 3 of the sg-ignore comments entirely. The remaining case/when-narrowing sg-ignores in type_to_tag now reference castwide/solargraph issue 1241, filed to track that the type checker does not narrow a case subject's type inside each branch - shell.rb: revert the unrelated cache_core rebuild-condition one-liner, out of scope for this PR - type_checker.rb: clarify that receiver_type generic resolution is currently restarg-specific, not yet generalized to fixed-arity params - chain_spec.rb: assert the non-literal (simplify_literals) type of true is Boolean, alongside the existing literal-type assertion Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019zMih8CMx6rXoSkYehxFH3
1 parent 1e9556d commit 42f5643

9 files changed

Lines changed: 98 additions & 64 deletions

File tree

lib/solargraph/complex_type.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ def downcast_to_literal_if_possible
192192
ComplexType.new(items.map(&:downcast_to_literal_if_possible))
193193
end
194194

195+
# Drop a literal item (e.g. `0`) when its non-literal base type
196+
# (e.g. `Integer`) is also present in the same union - a wider
197+
# type already subsumes it, so keeping both is redundant and
198+
# reads as if the literal value were still specifically reachable.
199+
#
200+
# @return [ComplexType]
201+
def without_redundant_literals
202+
non_literal_names = items.reject(&:literal?).map(&:name)
203+
new_items = items.reject { |item| item.literal? && non_literal_names.include?(item.non_literal_name) }
204+
ComplexType.new(new_items)
205+
end
206+
195207
# @return [String]
196208
def desc
197209
rooted_tags

lib/solargraph/complex_type/type_methods.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ def nil_type?
5858
@nil_type ||= name.casecmp('nil').zero?
5959
end
6060

61+
# Whether this type is one of Ruby's singleton values (nil,
62+
# true, false) rather than a general class or a multi-valued
63+
# literal (e.g. `0`, `:foo`).
64+
#
65+
# @return [Boolean]
66+
def singleton?
67+
nil_type? || %w[true false].include?(name)
68+
end
69+
6170
def tuple?
6271
@tuple ||= (name == 'Tuple') || (name == 'Array' && subtypes.length >= 1 && fixed_parameters?)
6372
end

lib/solargraph/pin/base_variable.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,10 @@ def probe api_map
187187
unless assignment_types.empty?
188188
# @type [Array<ComplexType::UniqueType>]
189189
items = assignment_types.flat_map(&:items).uniq
190-
# Drop a literal item (e.g. `0`) when its non-literal base
191-
# type (e.g. `Integer`) is also present in the same union -
192-
# a later, wider assignment (`index += 1`) already
193-
# subsumes it, so keeping both is redundant and reads as if
194-
# the literal value were still reachable.
195-
non_literal_names = items.reject(&:literal?).map(&:name)
196-
items = items.reject { |item| item.literal? && non_literal_names.include?(item.non_literal_name) }
197-
type_from_assignment = ComplexType.new(items)
190+
# A later, wider assignment (e.g. `index += 1`) can leave a
191+
# stale literal (e.g. `0`) alongside its own non-literal
192+
# base type in the union - drop the redundant literal.
193+
type_from_assignment = ComplexType.new(items).without_redundant_literals
198194
end
199195
return adjust_type api_map, type_from_assignment unless type_from_assignment.nil?
200196

lib/solargraph/pin/parameter.rb

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,12 @@ def compatible_arg? atype, api_map
227227
ptype = typify api_map
228228
return true if ptype.undefined?
229229

230-
return true if atype.conforms_to?(api_map,
231-
ptype,
232-
:method_call,
233-
%i[allow_empty_params allow_undefined])
234-
ptype.generic?
230+
return false unless atype.conforms_to?(api_map,
231+
ptype,
232+
:method_call,
233+
%i[allow_empty_params allow_undefined]) || ptype.generic?
234+
235+
literal_arg_matches? ptype, atype
235236
end
236237

237238
# @sg-ignore flow sensitive typing needs to handle attrs
@@ -247,6 +248,42 @@ def generate_complex_type
247248
nil
248249
end
249250

251+
# #compatible_arg? alone is too permissive for picking *which*
252+
# overload to use for return-type inference: it treats any
253+
# Integer as "compatible" with a literal-0-typed parameter
254+
# (correct for general call-validity checking - you can call
255+
# `array[i]` with any Integer `i` - but wrong for overload
256+
# *selection*, where it would make the first literal-typed
257+
# overload always win over the safe catch-all for any argument
258+
# that merely happens to be assignable to it). When this
259+
# parameter's type is a literal type, require every possible
260+
# value of the argument to also be literal, so a non-literal
261+
# (or not-entirely-literal) argument falls through to a less
262+
# specific overload instead.
263+
#
264+
# @param ptype [ComplexType]
265+
# @param atype [ComplexType]
266+
# @return [Boolean]
267+
def literal_arg_matches? ptype, atype
268+
return true unless ptype.items.any? { |item| dispatch_literal?(item) }
269+
270+
atype.items.all?(&:literal?)
271+
end
272+
273+
# nil/true/false are technically "literal" per
274+
# ComplexType::UniqueType#literal? (their non_literal_name is
275+
# NilClass/TrueClass/FalseClass), but they're singletons, not
276+
# dispatch-relevant values the way `0` vs `1` are for tuple
277+
# indexing - excluding them keeps ordinary `T?`/nilable params
278+
# (extremely common, e.g. String#split's `(Regexp | string |
279+
# nil pattern)`) from tripping #literal_arg_matches?.
280+
#
281+
# @param unique_type [ComplexType::UniqueType]
282+
# @return [Boolean]
283+
def dispatch_literal? unique_type
284+
unique_type.literal? && !unique_type.singleton?
285+
end
286+
250287
# @return [YARD::Tags::Tag, nil]
251288
def param_tag
252289
# @sg-ignore Need to add nil check here

lib/solargraph/rbs_translator.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module RbsTranslator
1212
'NilClass' => 'nil'
1313
}
1414

15-
# @param type [RBS::Types::Bases::Base]
15+
# @param type [RBS::Types::t]
1616
# @return [ComplexType]
1717
def self.to_complex_type(type)
1818
tag = type_to_tag(type)
@@ -27,13 +27,10 @@ def self.to_complex_type(type)
2727
def self.to_parameter_pin(param_type, name, decl, closure)
2828
return_type = case decl
2929
when :restarg
30-
# @sg-ignore RBS type understanding issue - see to_complex_type
3130
RbsTranslator.to_restarg_return_type(param_type.type)
3231
when :kwrestarg
33-
# @sg-ignore RBS type understanding issue - see to_complex_type
3432
RbsTranslator.to_kwrestarg_return_type(param_type.type)
3533
else
36-
# @sg-ignore RBS type understanding issue - to_complex_type's own param type is too narrow
3734
RbsTranslator.to_complex_type(param_type.type)
3835
end
3936
Solargraph::Pin::Parameter.new(decl: decl, name: name, closure: closure, return_type: return_type, source: :rbs, type_location: to_sg_location(param_type.location) || closure.type_location)
@@ -45,7 +42,7 @@ def self.to_parameter_pin(param_type, name, decl, closure)
4542
# element type isn't known (e.g. an untyped inline `#:`
4643
# annotation), falls back to a bare, unparameterized Array.
4744
#
48-
# @param elem_rbs_type [RBS::Types::Bases::Base]
45+
# @param elem_rbs_type [RBS::Types::t]
4946
# @return [ComplexType]
5047
def self.to_restarg_return_type elem_rbs_type
5148
elem_type = RbsTranslator.to_complex_type(elem_rbs_type)
@@ -56,7 +53,7 @@ def self.to_restarg_return_type elem_rbs_type
5653
# Likewise, the type of the local variable a kwrestarg is
5754
# captured into - a wrapped Hash of Symbol to its per-value type.
5855
#
59-
# @param elem_rbs_type [RBS::Types::Bases::Base]
56+
# @param elem_rbs_type [RBS::Types::t]
6057
# @return [ComplexType]
6158
def self.to_kwrestarg_return_type elem_rbs_type
6259
elem_type = RbsTranslator.to_complex_type(elem_rbs_type)
@@ -152,19 +149,30 @@ def self.to_sg_location(location)
152149
class << self
153150
private
154151

155-
# @param type [RBS::Types::Bases::Base]
152+
# @param type [RBS::Types::t]
156153
# @return [String]
157154
def type_to_tag type
155+
# Every branch below narrows `type` by class via `when`, but
156+
# the type checker doesn't propagate that narrowing to calls
157+
# inside the branch body - it still sees the full RBS::Types::t
158+
# union, so calls to members that only exist on the matched
159+
# class (e.g. #type, #types, #literal, #name, #args) need an
160+
# inline ignore comment. Tracked at
161+
# https://github.com/castwide/solargraph/issues/1241
158162
case type
159163
when RBS::Types::Optional
164+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
160165
"#{type_to_tag(type.type)}, nil"
161166
when RBS::Types::Bases::Bool
162167
'Boolean'
163168
when RBS::Types::Tuple
169+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
164170
"Array(#{type.types.map { |t| type_to_tag(t) }.join(', ')})"
165171
when RBS::Types::Literal
172+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
166173
type.literal.inspect
167174
when RBS::Types::Union
175+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
168176
type.types.map { |t| type_to_tag(t) }.join(', ')
169177
when RBS::Types::Record
170178
# @todo Better record support
@@ -174,13 +182,15 @@ def type_to_tag type
174182
when RBS::Types::Bases::Void
175183
'void'
176184
when RBS::Types::Variable
185+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
177186
"#{Solargraph::ComplexType::GENERIC_TAG_NAME}<#{type.name}>"
178187
when RBS::Types::Bases::Self, RBS::Types::Bases::Instance
179188
'self'
180189
when RBS::Types::Bases::Top
181190
# `Top` is the most super superclass
182191
'BasicObject'
183192
when RBS::Types::Intersection
193+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
184194
type.types.map { |member| type_to_tag(member) }.join(', ')
185195
when RBS::Types::Proc
186196
'Proc'
@@ -192,9 +202,11 @@ def type_to_tag type
192202
# `Interface represents a mix-in module which can be considered a
193203
# subtype of a consumer of it
194204
#
205+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
195206
type_tag(type.name, type.args)
196207
when RBS::Types::ClassSingleton
197208
# e.g., singleton(String)
209+
# @sg-ignore https://github.com/castwide/solargraph/issues/1241 - case/when doesn't narrow type
198210
type_tag(type.name)
199211
when RBS::Types::Bases::Any, RBS::Types::Bases::Bottom
200212
# `Bottom`` is used in contexts where nothing will ever return

lib/solargraph/shell.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def gems *names
190190
names.each do |name|
191191
if name == 'core'
192192
# @sg-ignore cache_core and core? are dynamically defined
193-
PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild]
193+
PinCache.cache_core(out: $stdout) # if !PinCache.core? || options[:rebuild]
194194
next
195195
end
196196

lib/solargraph/source/chain/call.rb

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def inferred_pins pins, api_map, name_pin, locals
111111
gates: name_pin.gates,
112112
source: :chain)
113113
atype = atypes[idx] ||= arg.infer(api_map, arg_name_pin, locals)
114-
unless (param.compatible_arg?(atype, api_map) && literal_param_arg_matches?(param, atype, api_map)) || param.restarg?
114+
unless param.compatible_arg?(atype, api_map) || param.restarg?
115115
match = false
116116
break
117117
end
@@ -186,44 +186,6 @@ def inferred_pins pins, api_map, name_pin, locals
186186
end
187187
end
188188

189-
# nil/true/false are technically "literal" per
190-
# ComplexType::UniqueType#literal? (their non_literal_name is
191-
# NilClass/TrueClass/FalseClass), but they're singletons, not
192-
# dispatch-relevant values the way `0` vs `1` are for tuple
193-
# indexing - excluding them keeps ordinary `T?`/nilable params
194-
# (extremely common, e.g. String#split's `(Regexp | string |
195-
# nil pattern)`) from tripping #literal_param_arg_matches?.
196-
#
197-
# @param unique_type [ComplexType::UniqueType]
198-
# @return [Boolean]
199-
def dispatch_literal? unique_type
200-
unique_type.literal? && !%w[nil true false].include?(unique_type.name)
201-
end
202-
203-
# Pin::Parameter#compatible_arg? alone is too permissive for
204-
# picking *which* overload to use for return-type inference: it
205-
# treats any Integer as "compatible" with a literal-0-typed
206-
# parameter (correct for general call-validity checking - you
207-
# can call `array[i]` with any Integer `i` - but wrong for
208-
# overload *selection*, where it would make the first
209-
# literal-typed overload always win over the safe catch-all for
210-
# any argument that merely happens to be assignable to it).
211-
# When the candidate overload's parameter is a literal type,
212-
# require every possible value of the argument to also be
213-
# literal, so a non-literal (or not-entirely-literal) argument
214-
# falls through to a less specific overload instead.
215-
#
216-
# @param param [Pin::Parameter]
217-
# @param atype [ComplexType]
218-
# @param api_map [ApiMap]
219-
# @return [Boolean]
220-
def literal_param_arg_matches? param, atype, api_map
221-
ptype = param.typify(api_map)
222-
return true unless ptype.items.any? { |item| dispatch_literal?(item) }
223-
224-
atype.items.all?(&:literal?)
225-
end
226-
227189
# @param docstring [YARD::Docstring]
228190
# @param context [ComplexType]
229191
# @return [ComplexType, nil]

lib/solargraph/type_checker.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,15 @@ def argument_problems_for chain, api_map, closure_pin, locals, location
434434
# @param sig [Pin::Signature]
435435
# @param pin [Pin::Method]
436436
# @param receiver_type [ComplexType] the type of the object the
437-
# method is being called on, used to resolve the restarg's
438-
# declared type (e.g. `Elem` for `Array#push`) against the
439-
# receiver's actual generic parameters (e.g. `Integer` for an
440-
# `Array<Integer>` receiver)
437+
# method is being called on. Resolving a signature's generics
438+
# (e.g. `Elem`) against the receiver's actual generic
439+
# parameters (e.g. `Integer` for an `Array<Integer>` receiver)
440+
# is a general problem, but this is currently only plumbed
441+
# through to the restarg path below (see #restarg_problems_for)
442+
# - fixed-arity params still get their types from `params`
443+
# (built by #param_details_from_stack), which doesn't resolve
444+
# against the receiver. Generalizing that is tracked as a
445+
# follow-up, not attempted here.
441446
#
442447
# @return [Array<Problem>]
443448
def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin, receiver_type

spec/source/chain_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ class NotCorrect; end
236236
chain = Solargraph::Parser.chain(node, 'test.rb')
237237
type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, [])
238238
expect(type.tag).to eq('true')
239+
expect(type.simplify_literals.tag).to eq('Boolean')
239240
end
240241

241242
it 'infers self from Object#freeze' do

0 commit comments

Comments
 (0)