-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Changes from all commits
4ce3d6a
df6f478
be9f737
539ea0c
ec21ea7
90ceb34
980638f
b0d3aa2
1f1b1ba
1a8af58
237cd34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
* | ||
|
@@ -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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) || | ||
|
@@ -148,7 +215,7 @@ class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer => | |
|
||
val inMatch = s.qualifier.symbol is Case | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @odersky: This logic was proposed by @DarkDimius when I implemented the original
I'd have to verify that this does indeed conflict with If you're sure (or if I get to verifying before you respond), then if I've understood your proposal correctly:
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
if (valueClassesOrAny) tree | ||
if (selTypeParam || valueClassesOrAny) tree | ||
else if (knownStatically) | ||
handleStaticallyKnown(s, scrutinee, selector, inMatch, tree.pos) | ||
else if (falseIfUnrelated && scrutinee <:< selector) | ||
|
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 |
There was a problem hiding this comment.
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).