Skip to content

Commit

Permalink
Always check exhaustivity of custom extractors/guards
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand committed Aug 28, 2020
1 parent 4986873 commit 1df4b16
Show file tree
Hide file tree
Showing 30 changed files with 348 additions and 27 deletions.
2 changes: 0 additions & 2 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -174,8 +174,6 @@ trait ScalaSettings extends AbsScalaSettings
def isAtLeastJunit = isTruthy || XmixinForceForwarders.value == "junit"
}

val XstrictPatmatAnalysis = BooleanSetting ("-Xstrict-patmat-analysis", "Assume pattern guards are false for the purposes of exhaustivity analysis")

// XML parsing options
object XxmlSettings extends MultiChoiceEnumeration {
val coalescing = Choice("coalescing", "Convert PCData to Text and coalesce sibling nodes")
Expand Down
Expand Up @@ -529,7 +529,6 @@ trait MatchAnalysis extends MatchApproximation {
// - back off (to avoid crying exhaustive too often) in unhandled cases
val start = if (StatisticsStatics.areSomeColdStatsEnabled) statistics.startTimer(statistics.patmatAnaExhaust) else null
var backoff = false
val strict = settings.XstrictPatmatAnalysis.value

val approx = new TreeMakersToProps(prevBinder)
val symbolicCases = approx.approximateMatch(cases, approx.onUnknown { tm =>
Expand All @@ -538,8 +537,9 @@ trait MatchAnalysis extends MatchApproximation {
case ExtractorTreeMaker(_, _, _)
| ProductExtractorTreeMaker(_, _)
| GuardTreeMaker(_) =>
if (strict) False else True
case _ => // debug.patmat("backing off due to "+ tm)
False
case _ =>
debug.patmat("backing off due to "+ tm)
backoff = true
False
})
Expand Down Expand Up @@ -904,7 +904,7 @@ trait MatchAnalysis extends MatchApproximation {
// then we can safely ignore these counter examples since we will eventually encounter
// both counter examples separately
case _ if inSameDomain =>
if (settings.XstrictPatmatAnalysis.value) Some(WildcardExample) else None
Some(WildcardExample)

// not a valid counter-example, possibly since we have a definite type but there was a field mismatch
// TODO: improve reasoning -- in the mean time, a false negative is better than an annoying false positive
Expand Down
24 changes: 20 additions & 4 deletions test/files/neg/t5365.check
@@ -1,15 +1,31 @@
t5365.scala:3: warning: match may not be exhaustive.
It would fail on the following input: None
It would fail on the following inputs: None, Some(_)
def nonExhautiveIfWeAssumeGuardsTrueOrFalse(x: Option[Int]): Int = x match {
^
t5365.scala:7: warning: match may not be exhaustive.
It would fail on the following input: Some(_)
def nonExhautiveIfWeAssumeGuardsFalse(x: Option[Int]): Int = x match {
^
t5365.scala:12: warning: match may not be exhaustive.
It would fail on the following input: Some(_)
def inverseGuards(x: Option[Int]): Int = x match {
^
t5365.scala:18: warning: match may not be exhaustive.
It would fail on the following input: None
It would fail on the following inputs: None, Some(_)
def extractor(x: Option[Int]) = x match {
^
t5365.scala:21: warning: match may not be exhaustive.
It would fail on the following input: None
It would fail on the following inputs: None, Some(_)
def repeatedExtractor(x: Option[Int]) = x match {
^
t5365.scala:24: warning: match may not be exhaustive.
It would fail on the following input: Some(_)
def extractorStrict(x: Option[Int]) = x match {
^
t5365.scala:28: warning: match may not be exhaustive.
It would fail on the following input: Some(_)
def repeatedExtractorStrict(x: Option[Int]) = x match {
^
error: No warnings can be incurred under -Xfatal-warnings.
three warnings found
7 warnings found
one error found
2 changes: 1 addition & 1 deletion test/files/neg/t5365b.scala
@@ -1,4 +1,4 @@
// scalac: -Xfatal-warnings -Xstrict-patmat-analysis
// scalac: -Xfatal-warnings
class C {
def nonExhautiveIfWeAssumeGuardsTrueOrFalse(x: Option[Int]): Int = x match {
case Some(n) if n % 2 == 0 => n
Expand Down
2 changes: 1 addition & 1 deletion test/files/neg/t5365c.scala
@@ -1,4 +1,4 @@
// scalac: -Xfatal-warnings -Xstrict-patmat-analysis -Xlint:strict-unsealed-patmat
// scalac: -Xfatal-warnings -Xlint:strict-unsealed-patmat
object C {
trait Z
final case class Q(i: Int) extends Z
Expand Down
2 changes: 1 addition & 1 deletion test/files/neg/t5365d.scala
@@ -1,4 +1,4 @@
// scalac: -Xfatal-warnings -Xstrict-patmat-analysis
// scalac: -Xfatal-warnings
object D {
sealed trait T
final case class C(i: Int) extends T
Expand Down
12 changes: 8 additions & 4 deletions test/files/neg/t5365e.check
@@ -1,13 +1,13 @@
t5365e.scala:8: warning: match may not be exhaustive.
It would fail on the following input: Bar(_)
It would fail on the following inputs: Bar(_), Foo(_)
def f0(x: Exh) = x match { case Foo() => () } // don't back off
^
t5365e.scala:9: warning: match may not be exhaustive.
It would fail on the following input: Bar(_)
It would fail on the following inputs: Bar(_), Foo(_)
def f1(x: Exh) = x match { case Foo(x) => x } // don't back off
^
t5365e.scala:10: warning: match may not be exhaustive.
It would fail on the following input: Bar(_)
It would fail on the following inputs: Bar(_), Foo(_)
def f2(x: Exh) = x match { case Foo(x, y) => x + y } // don't back off
^
t5365e.scala:11: warning: match may not be exhaustive.
Expand All @@ -18,6 +18,10 @@ t5365e.scala:12: warning: match may not be exhaustive.
It would fail on the following input: Foo(_)
def b1(x: Exh) = x match { case Bar(x) => x } // inexhaustive
^
t5365e.scala:13: warning: match may not be exhaustive.
It would fail on the following input: Foo(_)
def fb(x: Exh) = x match { case Foo(x) => x case Bar(x) => x } // pessimistically inexhaustive
^
error: No warnings can be incurred under -Xfatal-warnings.
5 warnings found
6 warnings found
one error found
8 changes: 1 addition & 7 deletions test/files/neg/t5365e.scala
Expand Up @@ -10,11 +10,5 @@ class Main {
def f2(x: Exh) = x match { case Foo(x, y) => x + y } // don't back off
def fX(x: Exh) = x match { case Foo(xs @ _*) => xs } // don't back off
def b1(x: Exh) = x match { case Bar(x) => x } // inexhaustive
def fb(x: Exh) = x match { case Foo(x) => x case Bar(x) => x } // optimistically exhaustive
// ^ under -Xstrict-patmat-analysis pessimistically approximates case Foo(x) as inexhaustive:
// test/files/neg/t5365e.scala:12: warning: match may not be exhaustive.
// It would fail on the following input: Foo(_)
// def fb(x: Exh) = x match { case Foo(x) => x case Bar(x) => x } // optimistically exhaustive
// ^
// ... and the counter-example needs work -.- ...
def fb(x: Exh) = x match { case Foo(x) => x case Bar(x) => x } // pessimistically inexhaustive
}
14 changes: 13 additions & 1 deletion test/files/neg/t7623.check
Expand Up @@ -4,12 +4,24 @@ t7623.scala:22: warning: A repeated case parameter or extracted sequence is not
t7623.scala:24: warning: Sequence wildcard (_*) does not align with repeated case parameter or extracted sequence; the result may be unexpected.
def h = "" match { case X(s, t, u @ _*) => } // warn
^
t7623.scala:10: warning: match may not be exhaustive.
It would fail on the following input: _
def f = C("") match { case C(s) => }
^
t7623.scala:12: warning: A repeated case parameter or extracted sequence is not matched by a sequence wildcard (_*), and may fail at runtime.
def g = C("") match { case C(s, t) => } // warn
^
t7623.scala:12: warning: match may not be exhaustive.
It would fail on the following input: _
def g = C("") match { case C(s, t) => } // warn
^
t7623.scala:14: warning: Sequence wildcard (_*) does not align with repeated case parameter or extracted sequence; the result may be unexpected.
def h = C("") match { case C(s, t, u @ _*) => } // warn
^
t7623.scala:14: warning: match may not be exhaustive.
It would fail on the following input: _
def h = C("") match { case C(s, t, u @ _*) => } // warn
^
error: No warnings can be incurred under -Xfatal-warnings.
four warnings found
7 warnings found
one error found
6 changes: 5 additions & 1 deletion test/files/neg/unchecked-refinement.check
Expand Up @@ -10,7 +10,11 @@ unchecked-refinement.scala:24: warning: a pattern match on a refinement type is
unchecked-refinement.scala:25: warning: a pattern match on a refinement type is unchecked
/* nowarn - todo */ case x: AnyRef { def size: Int } if b => x.size // this could/should do a static conformance test and not warn
^
unchecked-refinement.scala:23: warning: match may not be exhaustive.
It would fail on the following inputs: List(_), Nil
def f4(xs: List[Int]) = xs match {
^
warning: there was one feature warning; re-run with -feature for details
error: No warnings can be incurred under -Xfatal-warnings.
5 warnings found
6 warnings found
one error found
1 change: 1 addition & 0 deletions test/files/pos/t4649.scala
Expand Up @@ -3,5 +3,6 @@ object Test {
// @annotation.tailrec
def lazyFilter[E](s: Stream[E], p: E => Boolean): Stream[E] = s match {
case h #:: t => if (p(h)) h #:: lazyFilter(t, p) else lazyFilter(t, p)
case _ => Stream.empty[E]
}
}
3 changes: 3 additions & 0 deletions test/files/pos/t6675.scala
Expand Up @@ -9,13 +9,16 @@ object LeftOrRight {
object Test {
(Left((0, 0)): Either[(Int, Int), (Int, Int)]) match {
case LeftOrRight(pair @ (a, b)) => a // false -Xlint warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)"
case _ => sys.error("LeftOrRight.unapply is (non-explicitly) irrefutable")
}

(Left((0, 0)): Either[(Int, Int), (Int, Int)]) match {
case LeftOrRight((a, b)) => a // false -Xlint warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)"
case _ => sys.error("LeftOrRight.unapply is (non-explicitly) irrefutable")
}

(Left((0, 0)): Either[(Int, Int), (Int, Int)]) match {
case LeftOrRight(a, b) => a // false -Xlint warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)"
case _ => sys.error("LeftOrRight.unapply is (non-explicitly) irrefutable")
}
}
64 changes: 64 additions & 0 deletions test/files/run/lisp.check
@@ -1,3 +1,67 @@
lisp.scala:211: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("=", FUN({
^
lisp.scala:214: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("+", FUN({
^
lisp.scala:217: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("-", FUN({
^
lisp.scala:219: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("*", FUN({
^
lisp.scala:221: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("/", FUN({
^
lisp.scala:223: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("car", FUN({
^
lisp.scala:225: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("cdr", FUN({
^
lisp.scala:230: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("cons", FUN({
^
lisp.scala:402: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("=", Lambda{
^
lisp.scala:404: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("+", Lambda{
^
lisp.scala:407: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("-", Lambda{
^
lisp.scala:409: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("*", Lambda{
^
lisp.scala:411: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("/", Lambda{
^
lisp.scala:414: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("cons", Lambda{
^
lisp.scala:416: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("car", Lambda{
^
lisp.scala:418: warning: match may not be exhaustive.
It would fail on the following input: _
.extend("cdr", Lambda{
^
(lambda (x) (+ (* x x) 1))
(lambda (x) (+ (* x x) 1))

Expand Down
4 changes: 4 additions & 0 deletions test/files/run/lists-run.check
@@ -0,0 +1,4 @@
lists-run.scala:177: warning: match may not be exhaustive.
It would fail on the following input: List((x: String forSome x not in "foo"))
def show(xs: List[String]) = xs match {
^
8 changes: 8 additions & 0 deletions test/files/run/macro-expand-unapply-a.check
@@ -1,2 +1,10 @@
Test_2.scala:5: warning: match may not be exhaustive.
It would fail on the following input: _
List(1, 2) match { case UnapplyMacro(x, y) => println((x, y)) }
^
Test_2.scala:6: warning: match may not be exhaustive.
It would fail on the following input: _
List(1, 2, 3) match { case UnapplyMacro(x, y, z) => println((x, y, z)) }
^
(1,2)
(1,2,3)
4 changes: 4 additions & 0 deletions test/files/run/matchonstream.check
@@ -1 +1,5 @@
matchonstream.scala:2: warning: match may not be exhaustive.
It would fail on the following input: _
Stream.from(1) match { case Stream(1, 2, x @_*) => println("It worked!") }
^
It worked!

0 comments on commit 1df4b16

Please sign in to comment.