diff --git a/spec/compiler/semantic/is_a_spec.cr b/spec/compiler/semantic/is_a_spec.cr index f54126b70625..66df0965f1a7 100644 --- a/spec/compiler/semantic/is_a_spec.cr +++ b/spec/compiler/semantic/is_a_spec.cr @@ -258,4 +258,15 @@ describe "Semantic: is_a?" do end CRYSTAL end + + it "matches `is_a?(GenericA) && is_a?(GenericB)` when GenericB <= GenericA (#10831)" do + run(<<-CRYSTAL).to_b.should be_true + class A; end + class B(T) < A; end + class C(T) < B(T); end + + x = C(Int32).new.as(A) + x.is_a?(B) && x.is_a?(C) + CRYSTAL + end end diff --git a/src/compiler/crystal/semantic/type_intersect.cr b/src/compiler/crystal/semantic/type_intersect.cr index 490503dbc4c1..851b46d0b5b3 100644 --- a/src/compiler/crystal/semantic/type_intersect.cr +++ b/src/compiler/crystal/semantic/type_intersect.cr @@ -214,7 +214,13 @@ module Crystal def self.common_descendent(type1 : GenericClassType, type2 : GenericClassType) return type1 if type1 == type2 - common_descendent_instance_and_generic(type1, type2) + # Try both directions: e.g. for `class C(T) < B(T)`, `common_descendent(B, C)` + # should resolve to `C` because every C is also a B. Without the symmetric + # check, chained type filters like `x.is_a?(B) && x.is_a?(C)` narrow `x` + # to `B(T)` and `is_a?(C)` returns `false` even when the runtime value is + # actually a C (#10831). + common_descendent_instance_and_generic(type1, type2) || + common_descendent_instance_and_generic(type2, type1) end def self.common_descendent(type1 : Type, type2 : AliasType)