From 3f9878bef1c27adf21929f3ecbde763ffbb22ff3 Mon Sep 17 00:00:00 2001 From: Jeon Yoonjae Date: Mon, 27 Oct 2025 06:43:04 +0900 Subject: [PATCH 1/2] Use upper bound of abstract types in exhaustivity checking (#23909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/scala/scala3/issues/23620, https://github.com/scala/scala3/issues/24246 If the upper bound of an abstract type is not `sealed` but is effectively sealed, it is not handled correctly, since classSym could return a type that is not `sealed` (Object in the issue above). If the type were not abstract, it would pass the logic that checks whether it is an Or, And, etc., and would be handled properly. This PR makes exhaustivity checking use the upper bound of abstract types that are effectively sealed. --------- Co-authored-by: Zieliński Patryk <75637004+zielinsky@users.noreply.github.com> --- .../tools/dotc/transform/patmat/Space.scala | 12 +++++- tests/pos/i23620.scala | 18 +++++++++ tests/warn/i23620b.check | 24 ++++++++++++ tests/warn/i23620b.scala | 38 +++++++++++++++++++ tests/warn/i24246.check | 8 ++++ tests/warn/i24246.scala | 10 +++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 tests/pos/i23620.scala create mode 100644 tests/warn/i23620b.check create mode 100644 tests/warn/i23620b.scala create mode 100644 tests/warn/i24246.check create mode 100644 tests/warn/i24246.scala diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 21c58374a342..1d529664c99d 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -687,6 +687,8 @@ object SpaceEngine { else NoType }.filter(_.exists) parts + case tref: TypeRef if tref.isUpperBoundedAbstract => + rec(tref.info.hiBound, mixins) case _ => ListOfNoType end rec @@ -702,6 +704,10 @@ object SpaceEngine { && !cls.hasAnonymousChild // can't name anonymous classes as counter-examples && cls.children.nonEmpty // can't decompose without children + extension (tref: TypeRef) + def isUpperBoundedAbstract(using Context): Boolean = + tref.symbol.isAbstractOrAliasType && !tref.info.hiBound.isNothingType + val ListOfNoType = List(NoType) val ListOfTypNoType = ListOfNoType.map(Typ(_, decomposed = true)) @@ -826,7 +832,11 @@ object SpaceEngine { classSym.is(Case) && { if seen.add(classSym) then productSelectorTypes(tpw, sel.srcPos).exists(isCheckable(_)) else true // recursive case class: return true and other members can still fail the check - } + } || + (tpw.isInstanceOf[TypeRef] && { + val tref = tpw.asInstanceOf[TypeRef] + tref.isUpperBoundedAbstract && isCheckable(tref.info.hiBound) + }) !sel.tpe.hasAnnotation(defn.UncheckedAnnot) && { diff --git a/tests/pos/i23620.scala b/tests/pos/i23620.scala new file mode 100644 index 000000000000..aa81f09ee182 --- /dev/null +++ b/tests/pos/i23620.scala @@ -0,0 +1,18 @@ +trait Foo +trait Bar + +type FooOrBar = FooOrBar.Type +object FooOrBar: + opaque type Type <: (Foo | Bar) = Foo | Bar + + def bar: FooOrBar = new Bar {} + +trait Buz + +@main def main = + val p: FooOrBar | Buz = FooOrBar.bar + + p match + case _: Foo => println("foo") + case _: Buz => println("buz") + case _: Bar => println("bar") diff --git a/tests/warn/i23620b.check b/tests/warn/i23620b.check new file mode 100644 index 000000000000..556448701026 --- /dev/null +++ b/tests/warn/i23620b.check @@ -0,0 +1,24 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/i23620b.scala:20:2 -------------------------------------------- +20 | p match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: Bar + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/i23620b.scala:23:2 -------------------------------------------- +23 | p2 match { // warn + | ^^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: Bar + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/i23620b.scala:37:2 -------------------------------------------- +37 | x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: B + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/i23620b.scala b/tests/warn/i23620b.scala new file mode 100644 index 000000000000..dfdac45347a2 --- /dev/null +++ b/tests/warn/i23620b.scala @@ -0,0 +1,38 @@ +trait Foo +trait Bar + +type FooOrBar = FooOrBar.Type +object FooOrBar: + opaque type Type <: (Foo | Bar) = Foo | Bar + + def bar: FooOrBar = new Bar {} + +type OnlyFoo = OnlyFoo.Type +object OnlyFoo: + opaque type Type <: (Foo | Bar) = Foo + + def foo: OnlyFoo = new Foo {} + +@main def main = + val p: FooOrBar= FooOrBar.bar + val p2: OnlyFoo = OnlyFoo.foo + + p match // warn + case _: Foo => println("foo") + + p2 match { // warn + case _: Foo => println("foo") + } + +sealed trait S +trait Z + +case object A extends S, Z +case object B extends S, Z + +trait HasT: + type T <: S & Z + +def nonExhaustive(h: HasT, x: h.T) = + x match // warn + case A => () diff --git a/tests/warn/i24246.check b/tests/warn/i24246.check new file mode 100644 index 000000000000..fa197419d86d --- /dev/null +++ b/tests/warn/i24246.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/i24246.scala:8:2 ---------------------------------------------- +8 | x match { // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: ZZ + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/i24246.scala b/tests/warn/i24246.scala new file mode 100644 index 000000000000..f5bd02692774 --- /dev/null +++ b/tests/warn/i24246.scala @@ -0,0 +1,10 @@ +trait X + +sealed trait Y +case object YY extends Y, X +case object ZZ extends Y, X + +def foo[A <: X & Y](x: A): Unit = + x match { // warn + case YY => () + } From 7e2aa570b75dcc6e14d578d3a479f75ba2a990c3 Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Mon, 10 Nov 2025 15:03:05 +0100 Subject: [PATCH 2/2] Use upper bound of abstract types in exhaustivity checking (#23909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/scala/scala3/issues/23620, https://github.com/scala/scala3/issues/24246 If the upper bound of an abstract type is not `sealed` but is effectively sealed, it is not handled correctly, since classSym could return a type that is not `sealed` (Object in the issue above). If the type were not abstract, it would pass the logic that checks whether it is an Or, And, etc., and would be handled properly. This PR makes exhaustivity checking use the upper bound of abstract types that are effectively sealed. --------- Co-authored-by: Zieliński Patryk <75637004+zielinsky@users.noreply.github.com> [Cherry-picked 520668fdc50b3b6cfb07c1291851f5a19e694cc0][modified]