Skip to content

Fix #1828: add warning when patternmatching on generics #1834

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

Closed
wants to merge 11 commits into from
Closed
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
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ class Definitions {
List(AnyClass.typeRef), EmptyScope)
def SingletonType = SingletonClass.typeRef

lazy val ListType: TypeRef = ctx.requiredClassRef("scala.collection.immutable.List")
def ListClass(implicit ctx: Context) = ListType.symbol.asClass

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed (see below).

lazy val SeqType: TypeRef = ctx.requiredClassRef("scala.collection.Seq")
def SeqClass(implicit ctx: Context) = SeqType.symbol.asClass

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum ErrorMessageID {
CyclicReferenceInvolvingID,
CyclicReferenceInvolvingImplicitID,
SuperQualMustBeParentID,
ErasedTypeID,
;

public int errorNumber() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1221,4 +1221,11 @@ object messages {
|Attempting to define a field in a method signature after a varargs field is an error.
|""".stripMargin
}

case class ErasedType(val explanation: String = "")(implicit ctx: Context)
extends Message(ErasedTypeID) {
val kind = "Erased Type"
val msg =
i"abstract type pattern is unchecked since it is eliminated by erasure"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import dotty.tools.dotc.util.Positions._
import TreeTransforms.{MiniPhaseTransform, TransformerInfo}
import core._
import Contexts.Context, Types._, Constants._, Decorators._, Symbols._
import TypeUtils._, TypeErasure._, Flags._
import TypeUtils._, TypeErasure._, Flags._, TypeApplications._
import reporting.diagnostic.messages._

/** Implements partial evaluation of `sc.isInstanceOf[Sel]` according to:
*
Expand All @@ -18,6 +19,11 @@ import TypeUtils._, TypeErasure._, Flags._
* This is a generalized solution to raising an error on unreachable match
* cases and warnings on other statically known results of `isInstanceOf`.
*
* This phase also warns if the erased type parameter of a parameterized type
* is used in a match where it would be erased to `Object` or if the
* typeparameters are removed. Both of these cases could cause surprising
* behavior for the users.
*
* Steps taken:
*
* 1. `evalTypeApply` will establish the matrix and choose the appropriate
Expand Down Expand Up @@ -128,6 +134,67 @@ class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer =>
val selClassNonFinal = selClass && !(selector.typeSymbol is Final)
val selFinalClass = selClass && (selector.typeSymbol is Final)

// Check if the selector's potential type parameters will be erased, and if so warn
val selTypeParam = tree.args.head.tpe.widen match {
case tp @ AppliedType(_, args @ (arg :: _)) =>
// If the type is `Array[X]` where `X` is a primitive value
// class. In the future, when we have a solid implementation of
// Arrays of value classes, we might be able to relax this check.
val anyValArray = tp.isRef(defn.ArrayClass) && arg.typeSymbol.isPrimitiveValueClass
// param is: Any | AnyRef | java.lang.Object
val topType = defn.ObjectType <:< arg
// has @unchecked annotation to suppress warnings
val hasUncheckedAnnot = arg.hasAnnotation(defn.UncheckedAnnot)

// Shouldn't warn when matching on a subclass with underscore
// params or type binding
val matchingUnderscoresOrTypeBindings = args.forall(_ match {
case tr: TypeRef =>
tr.symbol.is(BindDefinedType)
case TypeBounds(lo, hi) =>
(lo eq defn.NothingType) && (hi eq defn.AnyType)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eq usually works, but is not in general a reliable method to compare types, since hash consing is not guaranteed. I recommend to use instead:

lo.isRef(defn.NothingClass) && hi.isRef(defn.AnyClass)

case _ => false
}) && selector <:< scrutinee

// we don't want to warn when matching on `List` from `Seq` e.g:
// (xs: Seq[Int]) match { case xs: List[Int] => ??? }
val matchingSeqToList = {
val hasSameTypeArgs = s.qualifier.tpe.widen match {
case AppliedType(_, scrutArg :: Nil) =>
(scrutArg eq arg) || arg <:< scrutArg
case _ => false
}

scrutinee.isRef(defn.SeqClass) &&
tp.isRef(defn.ListClass) &&
hasSameTypeArgs
}

val shouldWarn =
!topType && !hasUncheckedAnnot &&
!matchingUnderscoresOrTypeBindings && !matchingSeqToList &&
!anyValArray

if (shouldWarn) ctx.uncheckedWarning(
ErasedType(hl"""|Since type parameters are erased, you should not match on them in
|${"match"} expressions."""),
tree.pos
)
true
case _ =>
if (tree.args.head.symbol.is(TypeParam)) {
ctx.uncheckedWarning(
ErasedType(
hl"""|`${tree.args.head.tpe}` will be erased to `${selector}`. Which means that the specified
|behavior could be different during runtime."""
),
tree.pos
)
true
}
else false
}

// Cases ---------------------------------
val valueClassesOrAny =
ValueClasses.isDerivedValueClass(scrutinee.typeSymbol) ||
Expand All @@ -148,7 +215,7 @@ class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer =>

val inMatch = s.qualifier.symbol is Case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a correct discrimination for isInstanceOf's used in pattern matching! It would also cover case objects and enum cases. We need a more robust check. Maybe have a special, second isInstanceOf symbol?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@odersky: This logic was proposed by @DarkDimius when I implemented the original IsInstanceOfEvaluator, he could perhaps know of some reason why this isn't a problem. But the basic idea was that:

val bindings created inside a pattern match desugaring would get the case flag. This was so that we can know that they were specifically created by the PatternMatcher.

I'd have to verify that this does indeed conflict with case objects.

If you're sure (or if I get to verifying before you respond), then if I've understood your proposal correctly:

  • PatternMatcher should be changed to emit a special caseIsInstanceOf symbol (or similar)

  • IsInstanceOfEvaluator should intercept these and replace them with either regular isInstanceOf checks or the evaluated expressions.

    should it exclusively do this for the special symbol, or do we still want the optimization to be performed elsewhere?

Last - do you want this fix included in this PR or can it be separate? The PR doesn't otherwise touch the original logic. I'd open a second PR with the implementation during the day so that we don't forget.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caseIsInstanceOf idea looks right (exclusive or not: no idea, propose something :-) It could be a separate PR.


if (valueClassesOrAny) tree
if (selTypeParam || valueClassesOrAny) tree
else if (knownStatically)
handleStaticallyKnown(s, scrutinee, selector, inMatch, tree.pos)
else if (falseIfUnrelated && scrutinee <:< selector)
Expand Down
3 changes: 2 additions & 1 deletion compiler/test/dotty/tools/dotc/repl/TestREPL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.junit.Test

/** A subclass of REPL used for testing.
* It takes a transcript of a REPL session in `script`. The transcript
* starts with the first input prompt `scala> ` and ends with `scala> :quit` and a newline.
* starts with the first input prompt `scala> ` and ends with `scala> :quit`.
* Invoking `process()` on the `TestREPL` runs all input lines and
* collects then interleaved with REPL output in a string writer `out`.
* Invoking `check()` checks that the collected output matches the original
Expand All @@ -26,6 +26,7 @@ class TestREPL(script: String) extends REPL {
override def context(ctx: Context) = {
val fresh = ctx.fresh
fresh.setSetting(ctx.settings.color, "never")
fresh.setSetting(ctx.settings.unchecked, true)
fresh.setSetting(ctx.settings.classpath, Jars.dottyReplDeps.mkString(":"))
fresh.initialize()(fresh)
fresh
Expand Down
64 changes: 64 additions & 0 deletions tests/repl/erasure.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
scala> def unsafeCast[S](a: Any) = a match { case s: S => s; case _ => ??? }
-- [E048] Erased Type Unchecked Warning: <console> -----------------------------
4 |def unsafeCast[S](a: Any) = a match { case s: S => s; case _ => ??? }
| ^
| abstract type pattern is unchecked since it is eliminated by erasure

longer explanation available when compiling with `-explain`
def unsafeCast[S](a: Any): [S](a: Any)S
scala> unsafeCast[String](1)
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at .<init>(<console>:6)
at .<clinit>(<console>)
at RequestResult$.<init>(<console>:3)
at RequestResult$.<clinit>(<console>)
at RequestResult$result(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.jav...
scala> class A; def unsafeCast2[S <: A](a: Any) = a match { case s: S => s; case _ => ??? }
-- [E048] Erased Type Unchecked Warning: <console> -----------------------------
4 |class A; def unsafeCast2[S <: A](a: Any) = a match { case s: S => s; case _ => ??? }
| ^
| abstract type pattern is unchecked since it is eliminated by erasure

longer explanation available when compiling with `-explain`
defined class A
def unsafeCast2[S <: A](a: Any): [S <: A](a: Any)S
scala> def matchArray1[A](xs: Array[A]) = xs match { case xs: Array[Int] => xs; case xs: Array[A] => ??? }
def matchArray1[A](xs: Array[A]): [A](xs: Array[A])Array[Int]
scala> def matchArray2[A](xs: Array[Any]) = xs match { case xs: Array[Int] => xs; case xs: Array[A] => ??? }
-- [E048] Erased Type Unchecked Warning: <console> -----------------------------
5 |def matchArray2[A](xs: Array[Any]) = xs match { case xs: Array[Int] => xs; case xs: Array[A] => ??? }
| ^
| abstract type pattern is unchecked since it is eliminated by erasure

longer explanation available when compiling with `-explain`
def matchArray2[A](xs: Array[Any]): [A](xs: Array[Any])Array[Int]
scala> def matchArray3[A](xs: Array[A]) = xs match { case xs: Array[Int] => xs; case xs: Array[AnyRef] => ???; case xs: Array[Any] => ??? }
def matchArray3[A](xs: Array[A]): [A](xs: Array[A])Array[Int]
scala> def matchArray4(xs: Array[Any]) = xs match { case xs: Array[Int] => xs; case xs: Array[A] => ???; case xs: Array[Any] => ??? }
-- [E048] Erased Type Unchecked Warning: <console> -----------------------------
5 |def matchArray4(xs: Array[Any]) = xs match { case xs: Array[Int] => xs; case xs: Array[A] => ???; case xs: Array[Any] => ??? }
| ^
| abstract type pattern is unchecked since it is eliminated by erasure

longer explanation available when compiling with `-explain`
def matchArray4(xs: Array[Any]): Array[Int]
scala> def matchArray5[A](xs: Array[Any]) = xs match { case xs: Array[Int] => xs; case xs: Array[A @unchecked] => ??? }
def matchArray5[A](xs: Array[Any]): [A](xs: Array[Any])Array[Int]
scala> def matchList1(xs: List[Any]) = xs match { case xs: List[Int @unchecked] => ??? }
def matchList1(xs: List[Any]): Nothing
scala> def matchList2(xs: List[Any]) = xs match { case List() => ???; case _ => ??? }
def matchList2(xs: List[Any]): Nothing
scala> def matchList3(xs: Seq[_]) = xs match { case List() => ???; case _ => ??? }
def matchList3(xs: Seq[_]): Nothing
scala> val xs: Seq[Int] = Seq(1,2,3)
val xs: scala.collection.Seq[Int] = List(1, 2, 3)
scala> val xsMatch = xs match { case ss: List[Int] => 1 }
val xsMatch: Int = 1
scala> trait Foo[X]; trait Bar[X,Y]
defined trait Foo
defined trait Bar
scala> val underScoreMatch = (Nil: Any) match { case _: Foo[_] => ???; case _: Bar[_,_] => ???; case _ => 0 }
val underScoreMatch: Int = 0
scala> :quit