Skip to content

Backport "Do not consider uninhabited constructors when performing exhaustive match checking" to 3.3 LTS #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -625,16 +625,55 @@ object SpaceEngine {
// For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`.
parts.map(tp.derivedAppliedType(_, targs))

case tp if tp.isDecomposableToChildren =>
def getChildren(sym: Symbol): List[Symbol] =
case tpOriginal if tpOriginal.isDecomposableToChildren =>
// isDecomposableToChildren uses .classSymbol.is(Sealed)
// But that classSymbol could be from an AppliedType
// where the type constructor is a non-class type
// E.g. t11620 where `?1.AA[X]` returns as "sealed"
// but using that we're not going to infer A1[X] and A2[X]
// but end up with A1[<?>] and A2[<?>].
// So we widen (like AppliedType superType does) away
// non-class type constructors.
//
// Can't use `tpOriginal.baseType(cls)` because it causes
// i15893 to return exhaustivity warnings, because instead of:
// <== refineUsingParent(N, class Succ, []) = Succ[<? <: NatT>]
// <== isSub(Succ[<? <: NatT>] <:< Succ[Succ[<?>]]) = true
// we get
// <== refineUsingParent(NatT, class Succ, []) = Succ[NatT]
// <== isSub(Succ[NatT] <:< Succ[Succ[<?>]]) = false
def getAppliedClass(tp: Type): (Type, List[Type]) = tp match
case tp @ AppliedType(_: HKTypeLambda, _) => (tp, Nil)
case tp @ AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => (tp, tp.args)
case tp @ AppliedType(tycon: TypeProxy, _) => getAppliedClass(tycon.superType.applyIfParameterized(tp.args))
case tp => (tp, Nil)
val (tp, typeArgs) = getAppliedClass(tpOriginal)
// This function is needed to get the arguments of the types that will be applied to the class.
// This is necessary because if the arguments of the types contain Nothing,
// then this can affect whether the class will be taken into account during the exhaustiveness check
def getTypeArgs(parent: Symbol, child: Symbol, typeArgs: List[Type]): List[Type] =
val superType = child.typeRef.superType
if typeArgs.exists(_.isBottomType) && superType.isInstanceOf[ClassInfo] then
val parentClass = superType.asInstanceOf[ClassInfo].declaredParents.find(_.classSymbol == parent).get
val paramTypeMap = Map.from(parentClass.argTypes.map(_.typeSymbol).zip(typeArgs))
val substArgs = child.typeRef.typeParamSymbols.map(param => paramTypeMap.getOrElse(param, WildcardType))
substArgs
else Nil
def getChildren(sym: Symbol, typeArgs: List[Type]): List[Symbol] =
sym.children.flatMap { child =>
if child eq sym then List(sym) // i3145: sealed trait Baz, val x = new Baz {}, Baz.children returns Baz...
else if tp.classSymbol == defn.TupleClass || tp.classSymbol == defn.NonEmptyTupleClass then
List(child) // TupleN and TupleXXL classes are used for Tuple, but they aren't Tuple's children
else if (child.is(Private) || child.is(Sealed)) && child.isOneOf(AbstractOrTrait) then getChildren(child)
else List(child)
else if (child.is(Private) || child.is(Sealed)) && child.isOneOf(AbstractOrTrait) then
getChildren(child, getTypeArgs(sym, child, typeArgs))
else
val childSubstTypes = child.typeRef.applyIfParameterized(getTypeArgs(sym, child, typeArgs))
// if a class contains a field of type Nothing,
// then it can be ignored in pattern matching, because it is impossible to obtain an instance of it
val existFieldWithBottomType = childSubstTypes.fields.exists(_.info.isBottomType)
if existFieldWithBottomType then Nil else List(child)
}
val children = trace(i"getChildren($tp)")(getChildren(tp.classSymbol))
val children = trace(i"getChildren($tp)")(getChildren(tp.classSymbol, typeArgs))

val parts = children.map { sym =>
val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym
Expand Down
6 changes: 6 additions & 0 deletions tests/init-global/pos/i18629.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
object Foo {
val bar = List() match {
case List() => ???
case null => ???
}
}
4 changes: 2 additions & 2 deletions tests/patmat/i13931.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Test:
def test = Vector() match
case Seq() => println("empty")
case _ => println("non-empty")
case null => println("non-empty")

def test2 = IndexedSeq() match { case IndexedSeq() => case _ => }
def test2 = IndexedSeq() match { case IndexedSeq() => case null => }
def test3 = IndexedSeq() match { case IndexedSeq(1) => case _ => }
2 changes: 1 addition & 1 deletion tests/plugins/run/scriptWrapper/LineNumberPlugin_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LineNumberPlugin extends StandardPlugin {
val name: String = "linenumbers"
val description: String = "adjusts line numbers of script files"

override def initialize(options: List[String])(using Context): List[PluginPhase] = FixLineNumbers() :: Nil
override def init(options: List[String]): List[PluginPhase] = FixLineNumbers() :: Nil
}

// Loosely follows Mill linenumbers plugin (scan for marker with "original" source, adjust line numbers to match)
Expand Down
10 changes: 10 additions & 0 deletions tests/warn/patmat-nothing-exhaustive.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
enum TestAdt:
case Inhabited
case Uninhabited(no: Nothing)

def test1(t: TestAdt): Int = t match
case TestAdt.Inhabited => 1

def test2(o: Option[Option[Nothing]]): Int = o match
case Some(None) => 1
case None => 2
Loading