Skip to content

Commit 29a1887

Browse files
committed
Restore full bypass for the interface-typed expectation case
Moving the structural check into erased_type_conforms? let generic interfaces (e.g. _Each[Elem], _ToAry[T]) fall through into the subsequent subtype/parameter comparison, which isn't generic-parameter-aware for interfaces (see issue castwide#1267) and could wrongly reject a match the old blanket bypass would have allowed. Move the check back to where the old ignore_interface? ran, at the top of conforms_to_unique_type?, so once the interface question is settled (now via the real structural verdict, with :allow_unmatched_interface as fallback), nothing downstream runs - matching the old bypass's shape exactly, just with a real verdict behind it instead of a blind rule check. Verified against castwide/solargraph's own downstream solargraph-rspec integration suite (run_solargraph_rspec_specs CI job): its 3 pre-existing failures (Array<Integer> => Array generic-loss cases) reproduce identically on unmodified castwide/master at the exact commit this branch forked from, so they're unrelated to this PR either way - this commit is a defensive correctness fix, not a regression fix.
1 parent 2a62989 commit 29a1887

1 file changed

Lines changed: 31 additions & 28 deletions

File tree

lib/solargraph/complex_type/conformance.rb

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ def conforms_to_unique_type?
4141
# :nocov:
4242
end
4343

44-
return true if ignore_unmatchable_interface?
44+
interface_verdict = interface_bypass_verdict
45+
return interface_verdict unless interface_verdict.nil?
46+
4547
return true if conforms_via_reverse_match?
4648

4749
downcast_inferred = inferred.downcast_to_literal_if_possible
@@ -86,14 +88,34 @@ def conforms_via_stripped_expected_parameters?
8688
with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type?
8789
end
8890

89-
# An interface-typed expectation is verified structurally in
90-
# #erased_type_conforms?. There's no equivalent structural check when
91-
# the *inferred* type is itself an abstract interface (e.g., a method
92-
# returns `_ToAry`), since Solargraph doesn't know which concrete type
93-
# will show up at runtime, so :allow_unmatched_interface remains a
94-
# blanket escape hatch for that direction only.
95-
def ignore_unmatchable_interface?
96-
inferred.interface? && rules.include?(:allow_unmatched_interface)
91+
# Resolves interface-typed conformance before any parameter/subtype
92+
# comparisons run, the same way the old blanket
93+
# :allow_unmatched_interface bypass did. That's deliberate: RBS
94+
# interfaces can have their own type parameters (e.g. `_Each[Elem]`),
95+
# and this doesn't verify those (see
96+
# https://github.com/castwide/solargraph/issues/1267), so once the
97+
# interface question is settled, comparing `expected`'s subtypes
98+
# against `inferred`'s would either be meaningless or wrong.
99+
#
100+
# There's no structural check when the *inferred* type is itself an
101+
# abstract interface (e.g., a method returns `_ToAry`), since
102+
# Solargraph doesn't know which concrete type will show up at
103+
# runtime, so :allow_unmatched_interface remains a blanket escape
104+
# hatch for that direction.
105+
#
106+
# @return [Boolean, nil] true/false if the interface question
107+
# settles conformance outright, nil if there's no interface
108+
# involved (or no verdict could be reached and no fallback rule
109+
# applies), meaning normal conformance checking should proceed
110+
def interface_bypass_verdict
111+
return true if inferred.interface? && rules.include?(:allow_unmatched_interface)
112+
return nil unless expected.interface?
113+
114+
verdict = structural_interface_verdict
115+
return verdict unless verdict.nil?
116+
return true if rules.include?(:allow_unmatched_interface)
117+
118+
nil
97119
end
98120

99121
def can_strip_expected_parameters?
@@ -109,8 +131,6 @@ def conforms_via_reverse_match?
109131
end
110132

111133
def erased_type_conforms?
112-
return true if expected.interface? && interface_conforms?
113-
114134
case variance
115135
when :invariant
116136
return false unless inferred.name == expected.name
@@ -134,23 +154,6 @@ def erased_type_conforms?
134154
true
135155
end
136156

137-
# Whether `inferred` satisfies the `expected` RBS interface (e.g.
138-
# `Hash::_Key`, `_ToAry`). Prefers real duck-type verification —
139-
# `inferred` conforms if its method stack has every method the
140-
# interface itself declares (methods it inherits from `Object` are
141-
# ignored, since practically everything provides those) — and only
142-
# falls back to the blanket :allow_unmatched_interface rule when no
143-
# verdict could be reached, e.g. the interface pin isn't in the
144-
# ApiMap.
145-
#
146-
# @return [Boolean]
147-
def interface_conforms?
148-
verdict = structural_interface_verdict
149-
return verdict unless verdict.nil?
150-
151-
rules.include?(:allow_unmatched_interface)
152-
end
153-
154157
# The methods `expected` declares directly on itself, excluding ones
155158
# inherited from `Object` and other ancestors.
156159
#

0 commit comments

Comments
 (0)