Skip to content

Commit ab1bf38

Browse files
authored
Check exhaustivity of any case class (#22604)
Fixes #22590
2 parents 094e4fb + 892f048 commit ab1bf38

File tree

11 files changed

+64
-47
lines changed

11 files changed

+64
-47
lines changed

Diff for: compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ trait BCodeSkelBuilder extends BCodeHelpers {
169169

170170
/* ---------------- helper utils for generating classes and fields ---------------- */
171171

172-
def genPlainClass(cd0: TypeDef) = cd0 match {
172+
def genPlainClass(cd0: TypeDef) = (cd0: @unchecked) match {
173173
case TypeDef(_, impl: Template) =>
174174
assert(cnode == null, "GenBCode detected nested methods.")
175175

Diff for: compiler/src/dotty/tools/dotc/core/Comments.scala

+4-9
Original file line numberDiff line numberDiff line change
@@ -405,15 +405,10 @@ object Comments {
405405
val Trim = "(?s)^[\\s&&[^\n\r]]*(.*?)\\s*$".r
406406

407407
val raw = ctx.docCtx.flatMap(_.docstring(sym).map(_.raw)).getOrElse("")
408-
defs(sym) ++= defines(raw).map {
409-
str => {
410-
val start = skipWhitespace(str, "@define".length)
411-
val (key, value) = str.splitAt(skipVariable(str, start))
412-
key.drop(start) -> value
413-
}
414-
} map {
415-
case (key, Trim(value)) =>
416-
variableName(key) -> value.replaceAll("\\s+\\*+$", "")
408+
defs(sym) ++= defines(raw).map { str =>
409+
val start = skipWhitespace(str, "@define".length)
410+
val (key, Trim(value)) = str.splitAt(skipVariable(str, start)): @unchecked
411+
variableName(key.drop(start)) -> value.replaceAll("\\s+\\*+$", "")
417412
}
418413
}
419414

Diff for: compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

+1-6
Original file line numberDiff line numberDiff line change
@@ -841,8 +841,6 @@ object SpaceEngine {
841841
if Nullables.unsafeNullsEnabled then self.stripNull() else self
842842

843843
private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = trace(i"exhaustivityCheckable($sel ${sel.className})") {
844-
val seen = collection.mutable.Set.empty[Symbol]
845-
846844
// Possible to check everything, but be compatible with scalac by default
847845
def isCheckable(tp: Type): Boolean = trace(i"isCheckable($tp ${tp.className})"):
848846
val tpw = tp.widen.dealias.stripUnsafeNulls()
@@ -856,10 +854,7 @@ object SpaceEngine {
856854
}) ||
857855
tpw.isRef(defn.BooleanClass) ||
858856
classSym.isAllOf(JavaEnum) ||
859-
classSym.is(Case) && {
860-
if seen.add(classSym) then productSelectorTypes(tpw, sel.srcPos).exists(isCheckable(_))
861-
else true // recursive case class: return true and other members can still fail the check
862-
}
857+
classSym.is(Case)
863858

864859
!sel.tpe.hasAnnotation(defn.UncheckedAnnot)
865860
&& !sel.tpe.hasAnnotation(defn.RuntimeCheckedAnnot)

Diff for: compiler/src/dotty/tools/dotc/typer/Implicits.scala

+25-24
Original file line numberDiff line numberDiff line change
@@ -1383,30 +1383,31 @@ trait Implicits:
13831383
if alt1.isExtension then
13841384
// Fall back: if both results are extension method applications,
13851385
// compare the extension methods instead of their wrappers.
1386-
def stripExtension(alt: SearchSuccess) = methPart(stripApply(alt.tree)).tpe
1387-
(stripExtension(alt1), stripExtension(alt2)) match
1388-
case (ref1: TermRef, ref2: TermRef) =>
1389-
// ref1 and ref2 might refer to type variables owned by
1390-
// alt1.tstate and alt2.tstate respectively, to compare the
1391-
// alternatives correctly we need a TyperState that includes
1392-
// constraints from both sides, see
1393-
// tests/*/extension-specificity2.scala for test cases.
1394-
val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint
1395-
val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint
1396-
def exploreState(alt: SearchSuccess): TyperState =
1397-
alt.tstate.fresh(committable = false)
1398-
val comparisonState =
1399-
if constraintsIn1 && constraintsIn2 then
1400-
exploreState(alt1).mergeConstraintWith(alt2.tstate)
1401-
else if constraintsIn1 then
1402-
exploreState(alt1)
1403-
else if constraintsIn2 then
1404-
exploreState(alt2)
1405-
else
1406-
ctx.typerState
1407-
1408-
diff = inContext(searchContext().withTyperState(comparisonState)):
1409-
compare(ref1, ref2, preferGeneral = true)
1386+
def stripExtension(alt: SearchSuccess) =
1387+
methPart(stripApply(alt.tree)).tpe: @unchecked match { case ref: TermRef => ref }
1388+
val ref1 = stripExtension(alt1)
1389+
val ref2 = stripExtension(alt2)
1390+
// ref1 and ref2 might refer to type variables owned by
1391+
// alt1.tstate and alt2.tstate respectively, to compare the
1392+
// alternatives correctly we need a TyperState that includes
1393+
// constraints from both sides, see
1394+
// tests/*/extension-specificity2.scala for test cases.
1395+
val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint
1396+
val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint
1397+
def exploreState(alt: SearchSuccess): TyperState =
1398+
alt.tstate.fresh(committable = false)
1399+
val comparisonState =
1400+
if constraintsIn1 && constraintsIn2 then
1401+
exploreState(alt1).mergeConstraintWith(alt2.tstate)
1402+
else if constraintsIn1 then
1403+
exploreState(alt1)
1404+
else if constraintsIn2 then
1405+
exploreState(alt2)
1406+
else
1407+
ctx.typerState
1408+
1409+
diff = inContext(searchContext().withTyperState(comparisonState)):
1410+
compare(ref1, ref2, preferGeneral = true)
14101411
else // alt1 is a conversion, prefer extension alt2 over it
14111412
diff = -1
14121413
if diff < 0 then alt2

Diff for: scaladoc/src/dotty/tools/scaladoc/site/templates.scala

+3-5
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,9 @@ case class TemplateFile(
102102
ctx.layouts.getOrElse(name, throw new RuntimeException(s"No layouts named $name in ${ctx.layouts}"))
103103
)
104104

105-
def asJavaElement(o: Object): Object = o match
106-
case m: Map[?, ?] => m.transform {
107-
case (k: String, v: Object) => asJavaElement(v)
108-
}.asJava
109-
case l: List[?] => l.map(x => asJavaElement(x.asInstanceOf[Object])).asJava
105+
def asJavaElement(o: Any): Any = o match
106+
case m: Map[?, ?] => m.transform { (k, v) => asJavaElement(v) }.asJava
107+
case l: List[?] => l.map(asJavaElement).asJava
110108
case other => other
111109

112110
// Library requires mutable maps..

Diff for: tests/pos/switches.scala

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Test {
4242
case IntAnyVal(100) => 2
4343
case IntAnyVal(1000) => 3
4444
case IntAnyVal(10000) => 4
45+
case _ => -1
4546
}
4647
}
4748

Diff for: tests/warn/i15662.scala

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ case class Composite[T](v: T)
55
def m(composite: Composite[?]): Unit =
66
composite match {
77
case Composite[Int](v) => println(v) // warn: cannot be checked at runtime
8+
case _ => println("OTHER")
89
}
910

1011
def m2(composite: Composite[?]): Unit =

Diff for: tests/warn/i22590.arity2.scala

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
sealed trait T_B
2+
case class CC_A() extends T_B
3+
case class CC_C() extends T_B
4+
5+
sealed trait T_A
6+
case class CC_B[B](a: B,b:T_B) extends T_A
7+
8+
9+
@main def test() = {
10+
val v_a: CC_B[Int] = null
11+
val v_b: Int = v_a match { // warn: match may not be exhaustive.
12+
case CC_B(12, CC_A()) => 0
13+
case CC_B(_, CC_C()) => 0
14+
}
15+
}

Diff for: tests/warn/i22590.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sealed trait T_A
2+
case class CC_B[T](a: T) extends T_A
3+
4+
@main def test() = {
5+
val v_a: CC_B[Int] = CC_B(10)
6+
val v_b: Int = v_a match{ // warn: match may not be exhaustive.
7+
case CC_B(12) => 0
8+
}
9+
}

Diff for: tests/warn/opaque-match.scala

+2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ def Test[T] =
1313
case _: C => ??? // ok
1414
C() match
1515
case _: O.T => ??? // warn
16+
case _ => ???
1617
C() match
1718
case _: T => ??? // warn
19+
case _ => ???
1820

1921
(??? : Any) match
2022
case _: List[O.T] => ??? // warn

Diff for: tests/pos/t10373.scala renamed to tests/warn/t10373.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -Xfatal-warnings -deprecation -feature
1+
//> using options -deprecation -feature
22

33
abstract class Foo {
44
def bar(): Unit = this match {
@@ -7,7 +7,7 @@ abstract class Foo {
77
// Works fine
88
}
99

10-
def baz(that: Foo): Unit = (this, that) match {
10+
def baz(that: Foo): Unit = (this, that) match { // warn: match may not be exhaustive.
1111
case (Foo_1(), _) => //do something
1212
case (Foo_2(), _) => //do something
1313
// match may not be exhaustive

0 commit comments

Comments
 (0)