Skip to content

Commit e2e77b5

Browse files
authored
Merge pull request #12533 from dotty-staging/fix-12530
Fix #12530: Wildcard of value types don't cover null
2 parents 89e57aa + 80de4d2 commit e2e77b5

File tree

6 files changed

+63
-57
lines changed

6 files changed

+63
-57
lines changed

compiler/src/dotty/tools/dotc/reporting/messages.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ import transform.SymUtils._
838838

839839
class MatchCaseOnlyNullWarning()(using Context)
840840
extends PatternMatchMsg(MatchCaseOnlyNullWarningID) {
841-
def msg = em"""Only ${hl("null")} is matched. Consider using ${hl("case null =>")} instead."""
841+
def msg = em"""Unreachable case except for ${hl("null")} (if this is intentional, consider writing ${hl("case null =>")} instead)."""
842842
def explain = ""
843843
}
844844

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

+43-47
Original file line numberDiff line numberDiff line change
@@ -320,21 +320,13 @@ class SpaceEngine(using Context) extends SpaceLogic {
320320
private val scalaConsType = defn.ConsClass.typeRef
321321

322322
private val constantNullType = ConstantType(Constant(null))
323-
private val constantNullSpace = Typ(constantNullType)
324323

325324
/** Does the given tree stand for the literal `null`? */
326325
def isNullLit(tree: Tree): Boolean = tree match {
327326
case Literal(Constant(null)) => true
328327
case _ => false
329328
}
330329

331-
/** Does the given space contain just the value `null`? */
332-
def isNullSpace(space: Space): Boolean = space match {
333-
case Typ(tpe, _) => tpe.dealias == constantNullType || tpe.isNullType
334-
case Or(spaces) => spaces.forall(isNullSpace)
335-
case _ => false
336-
}
337-
338330
override def intersectUnrelatedAtomicTypes(tp1: Type, tp2: Type): Space = trace(s"atomic intersection: ${AndType(tp1, tp2).show}", debug) {
339331
// Precondition: !isSubType(tp1, tp2) && !isSubType(tp2, tp1).
340332
if (!ctx.explicitNulls && (tp1.isNullType || tp2.isNullType)) {
@@ -355,18 +347,18 @@ class SpaceEngine(using Context) extends SpaceLogic {
355347
def project(pat: Tree): Space = pat match {
356348
case Literal(c) =>
357349
if (c.value.isInstanceOf[Symbol])
358-
Typ(c.value.asInstanceOf[Symbol].termRef, false)
350+
Typ(c.value.asInstanceOf[Symbol].termRef, decomposed = false)
359351
else
360-
Typ(ConstantType(c), false)
352+
Typ(ConstantType(c), decomposed = false)
361353

362354
case pat: Ident if isBackquoted(pat) =>
363-
Typ(pat.tpe, false)
355+
Typ(pat.tpe, decomposed = false)
364356

365357
case Ident(nme.WILDCARD) =>
366-
Or(Typ(erase(pat.tpe.stripAnnots), false) :: constantNullSpace :: Nil)
358+
Typ(erase(pat.tpe.stripAnnots, isValue = true), decomposed = false)
367359

368360
case Ident(_) | Select(_, _) =>
369-
Typ(erase(pat.tpe.stripAnnots), false)
361+
Typ(erase(pat.tpe.stripAnnots, isValue = true), decomposed = false)
370362

371363
case Alternative(trees) =>
372364
Or(trees.map(project(_)))
@@ -386,31 +378,31 @@ class SpaceEngine(using Context) extends SpaceLogic {
386378
else {
387379
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos)
388380
if (elemTp.exists)
389-
Prod(erase(pat.tpe.stripAnnots), funRef, projectSeq(pats) :: Nil)
381+
Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, projectSeq(pats) :: Nil)
390382
else
391-
Prod(erase(pat.tpe.stripAnnots), funRef, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)))
383+
Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)))
392384
}
393385
else
394-
Prod(erase(pat.tpe.stripAnnots), funRef, pats.map(project))
386+
Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, pats.map(project))
395387

396388
case Typed(pat @ UnApply(_, _, _), _) =>
397389
project(pat)
398390

399391
case Typed(_, tpt) =>
400-
Typ(erase(tpt.tpe.stripAnnots), true)
392+
Typ(erase(tpt.tpe.stripAnnots, isValue = true), decomposed = false)
401393

402394
case This(_) =>
403-
Typ(pat.tpe.stripAnnots, false)
395+
Typ(pat.tpe.stripAnnots, decomposed = false)
404396

405397
case EmptyTree => // default rethrow clause of try/catch, check tests/patmat/try2.scala
406-
Typ(WildcardType, false)
398+
Typ(WildcardType, decomposed = false)
407399

408400
case Block(Nil, expr) =>
409401
project(expr)
410402

411403
case _ =>
412404
// Pattern is an arbitrary expression; assume a skolem (i.e. an unknown value) of the pattern type
413-
Typ(pat.tpe.narrow, false)
405+
Typ(pat.tpe.narrow, decomposed = false)
414406
}
415407

416408
private def project(tp: Type): Space = tp match {
@@ -458,29 +450,37 @@ class SpaceEngine(using Context) extends SpaceLogic {
458450
* case (IntExpr(_), IntExpr(_)) =>
459451
* case (BooleanExpr(_), BooleanExpr(_)) =>
460452
* }
453+
*
454+
* @param inArray whether `tp` is a type argument to `Array`
455+
* @param isValue whether `tp` is the type which match against values
456+
*
457+
* If `isValue` is true, then pattern-bound symbols are erased to its upper bound.
458+
* This is needed to avoid spurious unreachable warnings. See tests/patmat/i6197.scala.
461459
*/
462-
private def erase(tp: Type, inArray: Boolean = false): Type = trace(i"$tp erased to", debug) {
460+
private def erase(tp: Type, inArray: Boolean = false, isValue: Boolean = false): Type = trace(i"$tp erased to", debug) {
463461

464462
tp match {
465463
case tp @ AppliedType(tycon, args) =>
466464
if tycon.typeSymbol.isPatternBound then return WildcardType
467465

468466
val args2 =
469-
if (tycon.isRef(defn.ArrayClass)) args.map(arg => erase(arg, inArray = true))
470-
else args.map(arg => erase(arg, inArray = false))
471-
tp.derivedAppliedType(erase(tycon, inArray), args2)
467+
if (tycon.isRef(defn.ArrayClass)) args.map(arg => erase(arg, inArray = true, isValue = false))
468+
else args.map(arg => erase(arg, inArray = false, isValue = false))
469+
tp.derivedAppliedType(erase(tycon, inArray, isValue = false), args2)
472470

473471
case tp @ OrType(tp1, tp2) =>
474-
OrType(erase(tp1, inArray), erase(tp2, inArray), tp.isSoft)
472+
OrType(erase(tp1, inArray, isValue), erase(tp2, inArray, isValue), tp.isSoft)
475473

476474
case AndType(tp1, tp2) =>
477-
AndType(erase(tp1, inArray), erase(tp2, inArray))
475+
AndType(erase(tp1, inArray, isValue), erase(tp2, inArray, isValue))
478476

479477
case tp @ RefinedType(parent, _, _) =>
480-
erase(parent)
478+
erase(parent, inArray, isValue)
481479

482480
case tref: TypeRef if tref.symbol.isPatternBound =>
483-
if (inArray) tref.underlying else WildcardType
481+
if inArray then tref.underlying
482+
else if isValue then tref.superType
483+
else WildcardType
484484

485485
case _ => tp
486486
}
@@ -864,25 +864,28 @@ class SpaceEngine(using Context) extends SpaceLogic {
864864
&& !sel.tpe.widen.isRef(defn.QuotedTypeClass)
865865

866866
def checkRedundancy(_match: Match): Unit = {
867+
debug.println(s"---------------checking redundant patterns ${_match.show}")
868+
867869
val Match(sel, cases) = _match
868870
val selTyp = sel.tpe.widen.dealias
869871

870872
if (!redundancyCheckable(sel)) return
871873

872874
val targetSpace =
873-
if (ctx.explicitNulls || selTyp.classSymbol.isPrimitiveValueClass)
875+
if !selTyp.classSymbol.isNullableClass then
874876
project(selTyp)
875877
else
876878
project(OrType(selTyp, constantNullType, soft = false))
877879

878880
// in redundancy check, take guard as false in order to soundly approximate
879-
def projectPrevCases(cases: List[CaseDef]): List[Space] =
880-
cases.map { x =>
881+
val spaces = cases.map { x =>
882+
val res =
881883
if (x.guard.isEmpty) project(x.pat)
882884
else Empty
883-
}
884885

885-
val spaces = projectPrevCases(cases)
886+
debug.println(s"${x.pat.show} ====> ${res}")
887+
res
888+
}
886889

887890
(1 until cases.length).foreach { i =>
888891
val pat = cases(i).pat
@@ -902,20 +905,13 @@ class SpaceEngine(using Context) extends SpaceLogic {
902905
if (covered == Empty && !isNullLit(pat)) covered = curr
903906

904907
if (isSubspace(covered, prevs)) {
905-
report.warning(MatchCaseUnreachable(), pat.srcPos)
906-
}
907-
908-
// if last case is `_` and only matches `null`, produce a warning
909-
// If explicit nulls are enabled, this check isn't needed because most of the cases
910-
// that would trigger it would also trigger unreachability warnings.
911-
if (!ctx.explicitNulls && i == cases.length - 1 && !isNullLit(pat) ) {
912-
val spaces = flatten(simplify(minus(covered, prevs)))
913-
if spaces.lengthCompare(10) < 0 then
914-
dedup(spaces).toList match
915-
case Typ(`constantNullType`, _) :: Nil =>
916-
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)
917-
case s =>
918-
debug.println("`_` matches = " + s)
908+
if i == cases.length - 1
909+
&& isWildcardArg(pat)
910+
&& pat.tpe.classSymbol.isNullableClass
911+
then
912+
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)
913+
else
914+
report.warning(MatchCaseUnreachable(), pat.srcPos)
919915
}
920916
}
921917
}

compiler/src/scala/quoted/runtime/impl/printers/SourceCode.scala

-7
Original file line numberDiff line numberDiff line change
@@ -1218,13 +1218,6 @@ object SourceCode {
12181218
this += "]"
12191219
printType(tpe.resType)
12201220

1221-
case tpe: TypeLambda =>
1222-
this += "["
1223-
printList(tpe.paramNames.zip(tpe.paramBounds), ", ",
1224-
(x: (String, TypeBounds)) => (this += x._1 += " ").printType(x._2))
1225-
this += "] => "
1226-
printType(tpe.resType)
1227-
12281221
case tpe@TypeBounds(lo, hi) =>
12291222
this += "_ >: "
12301223
printType(lo)

tests/patmat/i12530.check

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
6: Match case Unreachable
2+
14: Match case Unreachable

tests/patmat/i12530.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
object Test {
2+
def foo(a: Boolean, b: Boolean): Unit =
3+
(a, b) match {
4+
case (false, _) =>
5+
case (true, _) =>
6+
case (_, false) => // error: unreachable
7+
}
8+
9+
def bar(a: Option[Boolean], b: Boolean): Unit =
10+
(a, b) match {
11+
case (Some(false), _) =>
12+
case (Some(true), _) =>
13+
case (None, _) =>
14+
case (_, false) => // reachable: null
15+
}
16+
}

tests/patmat/null.check

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
6: Match case Unreachable
1+
6: Pattern Match
22
13: Pattern Match
3-
18: Match case Unreachable
43
20: Pattern Match

0 commit comments

Comments
 (0)