-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Improve logic when to emit pattern type error #18093
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package dotty.tools | ||
package dotc | ||
package typer | ||
|
||
import core.* | ||
import Types.*, Contexts.*, Symbols.*, Flags.*, Constants.* | ||
import reporting.* | ||
import Decorators.i | ||
|
||
/** A module for linter checks done at typer */ | ||
object Linter: | ||
import ast.tpd.* | ||
|
||
/** If -Wnonunit-statement is set, warn about statements in blocks that are non-unit expressions. | ||
* @return true if a warning was issued, false otherwise | ||
*/ | ||
def warnOnInterestingResultInStatement(t: Tree)(using Context): Boolean = | ||
|
||
def isUninterestingSymbol(sym: Symbol): Boolean = | ||
sym == NoSymbol || | ||
sym.isConstructor || | ||
sym.is(Package) || | ||
sym.isPackageObject || | ||
sym == defn.BoxedUnitClass || | ||
sym == defn.AnyClass || | ||
sym == defn.AnyRefAlias || | ||
sym == defn.AnyValClass | ||
|
||
def isUninterestingType(tpe: Type): Boolean = | ||
tpe == NoType || | ||
tpe.typeSymbol == defn.UnitClass || | ||
defn.isBottomClass(tpe.typeSymbol) || | ||
tpe =:= defn.UnitType || | ||
tpe.typeSymbol == defn.BoxedUnitClass || | ||
tpe =:= defn.AnyValType || | ||
tpe =:= defn.AnyType || | ||
tpe =:= defn.AnyRefType | ||
|
||
def isJavaApplication(t: Tree): Boolean = t match | ||
case Apply(f, _) => f.symbol.is(JavaDefined) && !defn.ObjectClass.isSubClass(f.symbol.owner) | ||
case _ => false | ||
|
||
def checkInterestingShapes(t: Tree): Boolean = t match | ||
case If(_, thenpart, elsepart) => checkInterestingShapes(thenpart) || checkInterestingShapes(elsepart) | ||
case Block(_, res) => checkInterestingShapes(res) | ||
case Match(_, cases) => cases.exists(k => checkInterestingShapes(k.body)) | ||
case _ => checksForInterestingResult(t) | ||
|
||
def checksForInterestingResult(t: Tree): Boolean = | ||
!t.isDef // ignore defs | ||
&& !isUninterestingSymbol(t.symbol) // ctors, package, Unit, Any | ||
&& !isUninterestingType(t.tpe) // bottom types, Unit, Any | ||
&& !isThisTypeResult(t) // buf += x | ||
&& !isSuperConstrCall(t) // just a thing | ||
&& !isJavaApplication(t) // Java methods are inherently side-effecting | ||
// && !treeInfo.hasExplicitUnit(t) // suppressed by explicit expr: Unit // TODO Should explicit `: Unit` be added as warning suppression? | ||
|
||
if ctx.settings.WNonUnitStatement.value && !ctx.isAfterTyper && checkInterestingShapes(t) then | ||
val where = t match | ||
case Block(_, res) => res | ||
case If(_, thenpart, Literal(Constant(()))) => | ||
thenpart match { | ||
case Block(_, res) => res | ||
case _ => thenpart | ||
} | ||
case _ => t | ||
report.warning(UnusedNonUnitValue(where.tpe), t.srcPos) | ||
true | ||
else false | ||
end warnOnInterestingResultInStatement | ||
|
||
/** If -Wimplausible-patterns is set, warn about pattern values that can match the scrutinee | ||
* type only if there would be some user-defined equality method that equates values of the | ||
* two types. | ||
*/ | ||
def warnOnImplausiblePattern(pat: Tree, selType: Type)(using Context): Unit = | ||
// approximate type params with bounds | ||
def approx = new ApproximatingTypeMap { | ||
var alreadyExpanding: List[TypeRef] = Nil | ||
def apply(tp: Type) = tp.dealias match | ||
case tp: TypeRef if !tp.symbol.isClass => | ||
if alreadyExpanding contains tp then tp else | ||
val saved = alreadyExpanding | ||
alreadyExpanding ::= tp | ||
val res = expandBounds(tp.info.bounds) | ||
alreadyExpanding = saved | ||
res | ||
case _ => | ||
mapOver(tp) | ||
} | ||
|
||
// Is it possible that a value of `clsA` is equal to a value of `clsB`? | ||
// This ignores user-defined equals methods, but makes an exception | ||
// for numeric classes. | ||
def canOverlap(clsA: ClassSymbol, clsB: ClassSymbol): Boolean = | ||
clsA.mayHaveCommonChild(clsB) | ||
|| clsA.isNumericValueClass // this is quite coarse, but matches to what was done before | ||
|| clsB.isNumericValueClass | ||
|
||
// Can type `a` possiblly have a common instance with type `b`? | ||
def canEqual(a: Type, b: Type): Boolean = trace(i"canEqual $a $b"): | ||
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. Minor: There is 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. I looked at that, but it seems too different to be easily adaptable. And I don't want linter code to complicate the other code. So if we could use provably disjoint as is, it would be OK. But it seems the only point where we could use it is to implement the |
||
b match | ||
case _: TypeRef | _: AppliedType if b.typeSymbol.isClass => | ||
a match | ||
case a: TermRef if a.symbol.isOneOf(Module | Enum) => | ||
(a frozen_<:< b) // fast track | ||
|| (a frozen_<:< approx(b)) | ||
case _: TypeRef | _: AppliedType if a.typeSymbol.isClass => | ||
if a.isNullType then !b.isNotNull | ||
else canOverlap(a.typeSymbol.asClass, b.typeSymbol.asClass) | ||
case a: TypeProxy => | ||
canEqual(a.superType, b) | ||
case a: AndOrType => | ||
canEqual(a.tp1, b) || canEqual(a.tp2, b) | ||
case b: TypeProxy => | ||
canEqual(a, b.superType) | ||
case b: AndOrType => | ||
// we lose precision with and/or types, but it's hard to do better and | ||
// still compute `canEqual(A & B, B & A) = true`. | ||
canEqual(a, b.tp1) || canEqual(a, b.tp2) | ||
|
||
if ctx.settings.WimplausiblePatterns.value && !canEqual(pat.tpe, selType) then | ||
report.warning(ImplausiblePatternWarning(pat, selType), pat.srcPos) | ||
end warnOnImplausiblePattern | ||
|
||
end Linter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
// scalac: -Wimplausible-patterns | ||
object Test { | ||
sealed abstract class Foo[T] | ||
case object Bar1 extends Foo[Int] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
-- [E186] Type Error: tests/neg-custom-args/fatal-warnings/i9740.scala:10:9 -------------------------------------------- | ||
10 | case RecoveryCompleted => println("Recovery completed") // error | ||
| ^^^^^^^^^^^^^^^^^ | ||
| Implausible pattern: | ||
| RecoveryCompleted could match selector of type object TypedRecoveryCompleted | ||
| only if there is an `equals` method identifying elements of the two types. | ||
-- [E186] Type Error: tests/neg-custom-args/fatal-warnings/i9740.scala:15:9 -------------------------------------------- | ||
15 | case RecoveryCompleted => // error | ||
| ^^^^^^^^^^^^^^^^^ | ||
| Implausible pattern: | ||
| RecoveryCompleted could match selector of type TypedRecoveryCompleted | ||
| only if there is an `equals` method identifying elements of the two types. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
// scalac: -Wimplausible-patterns | ||
enum Recovery: | ||
case RecoveryCompleted | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
object i0 { | ||
1 match { | ||
def this(): Int // error | ||
def this() // error | ||
def this() // error | ||
} | ||
} |
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.
Not sure if the space is intentional:
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.
I did that intentionally since I wanted to have some visual space between the program code and the actual text. Let's see how it works in practice.