From c00dda3d033b3a68333faeade691381c180c6873 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Fri, 14 Feb 2025 16:12:24 +0000 Subject: [PATCH 1/4] Check exhaustivity of any case class --- .../dotty/tools/dotc/transform/patmat/Space.scala | 7 +------ i22590.arity2.scala | 15 +++++++++++++++ i22590.scala | 9 +++++++++ tests/pos/switches.scala | 1 + tests/warn/i15662.scala | 1 + tests/warn/opaque-match.scala | 2 ++ tests/{pos => warn}/t10373.scala | 4 ++-- 7 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 i22590.arity2.scala create mode 100644 i22590.scala rename tests/{pos => warn}/t10373.scala (74%) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 1ee402deded0..4f749b4ed7f8 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -841,8 +841,6 @@ object SpaceEngine { if Nullables.unsafeNullsEnabled then self.stripNull() else self private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = trace(i"exhaustivityCheckable($sel ${sel.className})") { - val seen = collection.mutable.Set.empty[Symbol] - // Possible to check everything, but be compatible with scalac by default def isCheckable(tp: Type): Boolean = trace(i"isCheckable($tp ${tp.className})"): val tpw = tp.widen.dealias.stripUnsafeNulls() @@ -856,10 +854,7 @@ object SpaceEngine { }) || tpw.isRef(defn.BooleanClass) || classSym.isAllOf(JavaEnum) || - 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 - } + classSym.is(Case) !sel.tpe.hasAnnotation(defn.UncheckedAnnot) && !sel.tpe.hasAnnotation(defn.RuntimeCheckedAnnot) diff --git a/i22590.arity2.scala b/i22590.arity2.scala new file mode 100644 index 000000000000..ddd126210cae --- /dev/null +++ b/i22590.arity2.scala @@ -0,0 +1,15 @@ +sealed trait T_B +case class CC_A() extends T_B +case class CC_C() extends T_B + +sealed trait T_A +case class CC_B[B](a: B,b:T_B) extends T_A + + +@main def test() = { + val v_a: CC_B[Int] = null + val v_b: Int = v_a match { + case CC_B(12, CC_A()) => 0 + case CC_B(_, CC_C()) => 0 + } +} diff --git a/i22590.scala b/i22590.scala new file mode 100644 index 000000000000..41d503ef8c6f --- /dev/null +++ b/i22590.scala @@ -0,0 +1,9 @@ +sealed trait T_A +case class CC_B[T](a: T) extends T_A + +@main def test() = { + val v_a: CC_B[Int] = CC_B(10) + val v_b: Int = v_a match{ + case CC_B(12) => 0 + } +} diff --git a/tests/pos/switches.scala b/tests/pos/switches.scala index bd7e44f1c8cf..beb378debb40 100644 --- a/tests/pos/switches.scala +++ b/tests/pos/switches.scala @@ -42,6 +42,7 @@ class Test { case IntAnyVal(100) => 2 case IntAnyVal(1000) => 3 case IntAnyVal(10000) => 4 + case _ => -1 } } diff --git a/tests/warn/i15662.scala b/tests/warn/i15662.scala index 0cf0e57ed0c3..f17a040b4980 100644 --- a/tests/warn/i15662.scala +++ b/tests/warn/i15662.scala @@ -5,6 +5,7 @@ case class Composite[T](v: T) def m(composite: Composite[?]): Unit = composite match { case Composite[Int](v) => println(v) // warn: cannot be checked at runtime + case _ => println("OTHER") } def m2(composite: Composite[?]): Unit = diff --git a/tests/warn/opaque-match.scala b/tests/warn/opaque-match.scala index 06cca835ab91..f66370def9b2 100644 --- a/tests/warn/opaque-match.scala +++ b/tests/warn/opaque-match.scala @@ -13,8 +13,10 @@ def Test[T] = case _: C => ??? // ok C() match case _: O.T => ??? // warn + case _ => ??? C() match case _: T => ??? // warn + case _ => ??? (??? : Any) match case _: List[O.T] => ??? // warn diff --git a/tests/pos/t10373.scala b/tests/warn/t10373.scala similarity index 74% rename from tests/pos/t10373.scala rename to tests/warn/t10373.scala index 0d91313f694d..ca49e7b1ce16 100644 --- a/tests/pos/t10373.scala +++ b/tests/warn/t10373.scala @@ -1,4 +1,4 @@ -//> using options -Xfatal-warnings -deprecation -feature +//> using options -deprecation -feature abstract class Foo { def bar(): Unit = this match { @@ -7,7 +7,7 @@ abstract class Foo { // Works fine } - def baz(that: Foo): Unit = (this, that) match { + def baz(that: Foo): Unit = (this, that) match { // warn: match may not be exhaustive. case (Foo_1(), _) => //do something case (Foo_2(), _) => //do something // match may not be exhaustive From 4072218f4d956d90ca9d50aaf0793ef3e77664dd Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 17 Feb 2025 11:54:38 +0000 Subject: [PATCH 2/4] Fix fresh exhaustivity warnings in the compiler --- .../tools/backend/jvm/BCodeSkelBuilder.scala | 2 +- .../src/dotty/tools/dotc/core/Comments.scala | 13 ++--- .../dotty/tools/dotc/typer/Implicits.scala | 49 ++++++++++--------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index 1e07254808d6..5390626eb2cc 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -169,7 +169,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { /* ---------------- helper utils for generating classes and fields ---------------- */ - def genPlainClass(cd0: TypeDef) = cd0 match { + def genPlainClass(cd0: TypeDef) = (cd0: @unchecked) match { case TypeDef(_, impl: Template) => assert(cnode == null, "GenBCode detected nested methods.") diff --git a/compiler/src/dotty/tools/dotc/core/Comments.scala b/compiler/src/dotty/tools/dotc/core/Comments.scala index 92160c97973d..b1d1e387c2cf 100644 --- a/compiler/src/dotty/tools/dotc/core/Comments.scala +++ b/compiler/src/dotty/tools/dotc/core/Comments.scala @@ -405,15 +405,10 @@ object Comments { val Trim = "(?s)^[\\s&&[^\n\r]]*(.*?)\\s*$".r val raw = ctx.docCtx.flatMap(_.docstring(sym).map(_.raw)).getOrElse("") - defs(sym) ++= defines(raw).map { - str => { - val start = skipWhitespace(str, "@define".length) - val (key, value) = str.splitAt(skipVariable(str, start)) - key.drop(start) -> value - } - } map { - case (key, Trim(value)) => - variableName(key) -> value.replaceAll("\\s+\\*+$", "") + defs(sym) ++= defines(raw).map { str => + val start = skipWhitespace(str, "@define".length) + val (key, Trim(value)) = str.splitAt(skipVariable(str, start)): @unchecked + variableName(key.drop(start)) -> value.replaceAll("\\s+\\*+$", "") } } diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 9d273ebca866..977c63be4809 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -1383,30 +1383,31 @@ trait Implicits: if alt1.isExtension then // Fall back: if both results are extension method applications, // compare the extension methods instead of their wrappers. - def stripExtension(alt: SearchSuccess) = methPart(stripApply(alt.tree)).tpe - (stripExtension(alt1), stripExtension(alt2)) match - case (ref1: TermRef, ref2: TermRef) => - // ref1 and ref2 might refer to type variables owned by - // alt1.tstate and alt2.tstate respectively, to compare the - // alternatives correctly we need a TyperState that includes - // constraints from both sides, see - // tests/*/extension-specificity2.scala for test cases. - val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint - val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint - def exploreState(alt: SearchSuccess): TyperState = - alt.tstate.fresh(committable = false) - val comparisonState = - if constraintsIn1 && constraintsIn2 then - exploreState(alt1).mergeConstraintWith(alt2.tstate) - else if constraintsIn1 then - exploreState(alt1) - else if constraintsIn2 then - exploreState(alt2) - else - ctx.typerState - - diff = inContext(searchContext().withTyperState(comparisonState)): - compare(ref1, ref2, preferGeneral = true) + def stripExtension(alt: SearchSuccess) = + methPart(stripApply(alt.tree)).tpe: @unchecked match { case ref: TermRef => ref } + val ref1 = stripExtension(alt1) + val ref2 = stripExtension(alt2) + // ref1 and ref2 might refer to type variables owned by + // alt1.tstate and alt2.tstate respectively, to compare the + // alternatives correctly we need a TyperState that includes + // constraints from both sides, see + // tests/*/extension-specificity2.scala for test cases. + val constraintsIn1 = alt1.tstate.constraint ne ctx.typerState.constraint + val constraintsIn2 = alt2.tstate.constraint ne ctx.typerState.constraint + def exploreState(alt: SearchSuccess): TyperState = + alt.tstate.fresh(committable = false) + val comparisonState = + if constraintsIn1 && constraintsIn2 then + exploreState(alt1).mergeConstraintWith(alt2.tstate) + else if constraintsIn1 then + exploreState(alt1) + else if constraintsIn2 then + exploreState(alt2) + else + ctx.typerState + + diff = inContext(searchContext().withTyperState(comparisonState)): + compare(ref1, ref2, preferGeneral = true) else // alt1 is a conversion, prefer extension alt2 over it diff = -1 if diff < 0 then alt2 From daa31fd0f598a48df8a2c64392fc76519e0d490f Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 17 Feb 2025 13:17:36 +0000 Subject: [PATCH 3/4] Fix fresh exhaustivity warnings in scaladoc --- scaladoc/src/dotty/tools/scaladoc/site/templates.scala | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala index c37ff8fe0200..56a0f7f6d6b6 100644 --- a/scaladoc/src/dotty/tools/scaladoc/site/templates.scala +++ b/scaladoc/src/dotty/tools/scaladoc/site/templates.scala @@ -102,11 +102,9 @@ case class TemplateFile( ctx.layouts.getOrElse(name, throw new RuntimeException(s"No layouts named $name in ${ctx.layouts}")) ) - def asJavaElement(o: Object): Object = o match - case m: Map[?, ?] => m.transform { - case (k: String, v: Object) => asJavaElement(v) - }.asJava - case l: List[?] => l.map(x => asJavaElement(x.asInstanceOf[Object])).asJava + def asJavaElement(o: Any): Any = o match + case m: Map[?, ?] => m.transform { (k, v) => asJavaElement(v) }.asJava + case l: List[?] => l.map(asJavaElement).asJava case other => other // Library requires mutable maps.. From 892f048594b65e22498291f388daf4554a315ffe Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 18 Feb 2025 10:10:31 +0000 Subject: [PATCH 4/4] Move exh checking case class tests in --- i22590.arity2.scala => tests/warn/i22590.arity2.scala | 2 +- i22590.scala => tests/warn/i22590.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename i22590.arity2.scala => tests/warn/i22590.arity2.scala (79%) rename i22590.scala => tests/warn/i22590.scala (67%) diff --git a/i22590.arity2.scala b/tests/warn/i22590.arity2.scala similarity index 79% rename from i22590.arity2.scala rename to tests/warn/i22590.arity2.scala index ddd126210cae..8ce84ab299f1 100644 --- a/i22590.arity2.scala +++ b/tests/warn/i22590.arity2.scala @@ -8,7 +8,7 @@ case class CC_B[B](a: B,b:T_B) extends T_A @main def test() = { val v_a: CC_B[Int] = null - val v_b: Int = v_a match { + val v_b: Int = v_a match { // warn: match may not be exhaustive. case CC_B(12, CC_A()) => 0 case CC_B(_, CC_C()) => 0 } diff --git a/i22590.scala b/tests/warn/i22590.scala similarity index 67% rename from i22590.scala rename to tests/warn/i22590.scala index 41d503ef8c6f..1520a07b86b8 100644 --- a/i22590.scala +++ b/tests/warn/i22590.scala @@ -3,7 +3,7 @@ case class CC_B[T](a: T) extends T_A @main def test() = { val v_a: CC_B[Int] = CC_B(10) - val v_b: Int = v_a match{ + val v_b: Int = v_a match{ // warn: match may not be exhaustive. case CC_B(12) => 0 } }