Skip to content

Commit bd377fb

Browse files
committed
Emit strict pattern binding warnings by default
allow old syntax when source version < 3.2 fixup checkIrrefutable logic defer change in desugaring wip: migration and rewriting enable rewrites for all migration modes >= 3.2 use gradual migration warning add source version specific tests remove pos 3.2-migration test; not relevant with new migration strategy fixup: mention -rewrite in diagnostic
1 parent a29c082 commit bd377fb

File tree

18 files changed

+191
-199
lines changed

18 files changed

+191
-199
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,7 @@ object desugar {
15271527
Function(derivedValDef(gen.pat, named, tpt, EmptyTree, Modifiers(Param)) :: Nil, body)
15281528
case _ =>
15291529
val matchCheckMode =
1530-
if (gen.checkMode == GenCheckMode.Check) MatchCheck.IrrefutableGenFrom
1530+
if (gen.checkMode == GenCheckMode.Check || gen.checkMode == GenCheckMode.CheckAndFilter) MatchCheck.IrrefutableGenFrom
15311531
else MatchCheck.None
15321532
makeCaseLambda(CaseDef(gen.pat, EmptyTree, body) :: Nil, matchCheckMode)
15331533
}
@@ -1614,13 +1614,11 @@ object desugar {
16141614
case IdPattern(_) => true
16151615
case _ => false
16161616

1617-
def needsNoFilter(gen: GenFrom): Boolean =
1618-
if (gen.checkMode == GenCheckMode.FilterAlways) // pattern was prefixed by `case`
1619-
false
1620-
else
1621-
gen.checkMode != GenCheckMode.FilterNow
1622-
|| isVarBinding(gen.pat)
1623-
|| isIrrefutable(gen.pat, gen.expr)
1617+
def needsNoFilter(gen: GenFrom): Boolean = gen.checkMode match
1618+
case GenCheckMode.FilterAlways => false // pattern was prefixed by `case`
1619+
case GenCheckMode.FilterNow | GenCheckMode.CheckAndFilter => isVarBinding(gen.pat) || isIrrefutable(gen.pat, gen.expr)
1620+
case GenCheckMode.Check => true
1621+
case GenCheckMode.Ignore => true
16241622

16251623
/** rhs.name with a pattern filter on rhs unless `pat` is irrefutable when
16261624
* matched against `rhs`.

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
168168
enum GenCheckMode {
169169
case Ignore // neither filter nor check since filtering was done before
170170
case Check // check that pattern is irrefutable
171-
case FilterNow // filter out non-matching elements since we are not yet in 3.x
171+
case CheckAndFilter // both check and filter (transitional period starting with 3.2)
172+
case FilterNow // filter out non-matching elements if we are not in 3.2 or later
172173
case FilterAlways // filter out non-matching elements since pattern is prefixed by `case`
173174
}
174175

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,9 +2514,10 @@ object Parsers {
25142514
def generatorRest(pat: Tree, casePat: Boolean): GenFrom =
25152515
atSpan(startOffset(pat), accept(LARROW)) {
25162516
val checkMode =
2517-
if (casePat) GenCheckMode.FilterAlways
2518-
else if sourceVersion.isAtLeast(future) then GenCheckMode.Check
2519-
else GenCheckMode.FilterNow // filter for now, to keep backwards compat
2517+
if casePat then GenCheckMode.FilterAlways
2518+
else if sourceVersion.isAtLeast(`3.2`) then GenCheckMode.CheckAndFilter
2519+
else if sourceVersion.isAtLeast(`future`) then GenCheckMode.Check
2520+
else GenCheckMode.FilterNow // filter on source version < 3.2, for backward compat
25202521
GenFrom(pat, subExpr(), checkMode)
25212522
}
25222523

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -826,23 +826,25 @@ trait Checking {
826826
val addendum =
827827
i"""
828828
|
829-
|If $usage is intentional, this can be communicated by $fix.${err.rewriteNotice}"""
829+
|If $usage is intentional, this can be communicated by $fix.
830+
|This patch can be inserted automatically under -rewrite -source 3.2-migration."""
830831
val pos =
831832
if isPatDef then reason match
832833
case NonConforming => sel.srcPos
833834
case RefutableExtractor => pat.source.atSpan(pat.span union sel.span)
834835
else pat.srcPos
835-
report.warning(em"$msg$addendum", pos)
836+
report.gradualErrorOrMigrationWarning(em"$msg$addendum", pos, warnFrom = `3.2`, errorFrom = `future`)
836837
false
837838
}
838839

839840
def check(pat: Tree, pt: Type): Boolean = (pt <:< pat.tpe) || fail(pat, pt, Reason.NonConforming)
840841

841842
def recur(pat: Tree, pt: Type): Boolean =
842-
!sourceVersion.isAtLeast(future) || // only for 3.x for now since mitigations work only after this PR
843-
pt.hasAnnotation(defn.UncheckedAnnot) || {
843+
!sourceVersion.isAtLeast(`3.2`)
844+
|| pt.hasAnnotation(defn.UncheckedAnnot)
845+
|| {
844846
patmatch.println(i"check irrefutable $pat: ${pat.tpe} against $pt")
845-
pat match {
847+
pat match
846848
case Bind(_, pat1) =>
847849
recur(pat1, pt)
848850
case UnApply(fn, _, pats) =>
@@ -860,7 +862,6 @@ trait Checking {
860862
case _ =>
861863
check(pat, pt)
862864
}
863-
}
864865

865866
recur(pat, pt)
866867
}

compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,6 @@ object ErrorReporting {
153153
|
154154
|The tests were made under $constraintText"""
155155

156-
def rewriteNotice: String =
157-
if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite."
158-
else ""
159-
160156
def whyFailedStr(fail: FailedExtension) =
161157
i""" failed with
162158
|

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
16131613
tree.selector.removeAttachment(desugar.CheckIrrefutable) match {
16141614
case Some(checkMode) =>
16151615
val isPatDef = checkMode == desugar.MatchCheck.IrrefutablePatDef
1616-
if !checkIrrefutable(sel, pat, isPatDef) && sourceVersion == `future-migration` then
1616+
if !checkIrrefutable(sel, pat, isPatDef) && sourceVersion.isAtLeast(`3.2`) && sourceVersion.isMigrating then
16171617
if isPatDef then uncheckedBrackets(tree.selector) match
16181618
case None =>
16191619
patch(Span(tree.selector.span.end), ": @unchecked")

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ class CompilationTests {
7373
aggregateTests(
7474
compileFile("tests/rewrites/rewrites.scala", scala2CompatMode.and("-rewrite", "-indent")),
7575
compileFile("tests/rewrites/rewrites3x.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
76-
compileFile("tests/rewrites/filtering-fors.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
77-
compileFile("tests/rewrites/refutable-pattern-bindings.scala", defaultOptions.and("-rewrite", "-source", "future-migration")),
76+
compileFile("tests/rewrites/filtering-fors.scala", defaultOptions.and("-rewrite", "-source", "3.2-migration")),
77+
compileFile("tests/rewrites/refutable-pattern-bindings.scala", defaultOptions.and("-rewrite", "-source", "3.2-migration")),
7878
compileFile("tests/rewrites/i8982.scala", defaultOptions.and("-indent", "-rewrite")),
7979
compileFile("tests/rewrites/i9632.scala", defaultOptions.and("-indent", "-rewrite")),
8080
compileFile("tests/rewrites/i11895.scala", defaultOptions.and("-indent", "-rewrite")),
@@ -203,7 +203,6 @@ class CompilationTests {
203203
compileFile("tests/run-custom-args/typeclass-derivation1.scala", defaultOptions.without(yCheckOptions: _*)),
204204
compileFile("tests/run-custom-args/tuple-cons.scala", allowDeepSubtypes),
205205
compileFile("tests/run-custom-args/i5256.scala", allowDeepSubtypes),
206-
compileFile("tests/run-custom-args/fors.scala", defaultOptions.and("-source", "future")),
207206
compileFile("tests/run-custom-args/no-useless-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
208207
compileFile("tests/run-custom-args/defaults-serizaliable-no-forwarders.scala", defaultOptions and "-Xmixin-force-forwarders:false"),
209208
compileFilesInDir("tests/run-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")),

docs/_docs/reference/changed-features/pattern-bindings.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ movedTo: https://docs.scala-lang.org/scala3/reference/changed-features/pattern-b
77
In Scala 2, pattern bindings in `val` definitions and `for` expressions are
88
loosely typed. Potentially failing matches are still accepted at compile-time,
99
but may influence the program's runtime behavior.
10-
From Scala 3.1 on, type checking rules will be tightened so that warnings are reported at compile-time instead.
10+
From Scala 3.2 on, type checking rules will be tightened so that warnings are reported at compile-time instead.
1111

1212
## Bindings in Pattern Definitions
1313

@@ -16,7 +16,7 @@ val xs: List[Any] = List(1, 2, 3)
1616
val (x: String) :: _ = xs // error: pattern's type String is more specialized
1717
// than the right-hand side expression's type Any
1818
```
19-
This code gives a compile-time warning in Scala 3.1 (and also in Scala 3.0 under the `-source future` setting) whereas it will fail at runtime with a `ClassCastException` in Scala 2. In Scala 3.1, a pattern binding is only allowed if the pattern is _irrefutable_, that is, if the right-hand side's type conforms to the pattern's type. For instance, the following is OK:
19+
This code gives a compile-time warning in Scala 3.2 (and also earlier Scala 3.x under the `-source future` setting) whereas it will fail at runtime with a `ClassCastException` in Scala 2. In Scala 3.2, a pattern binding is only allowed if the pattern is _irrefutable_, that is, if the right-hand side's type conforms to the pattern's type. For instance, the following is OK:
2020
```scala
2121
val pair = (1, true)
2222
val (x, y) = pair
@@ -25,7 +25,7 @@ Sometimes one wants to decompose data anyway, even though the pattern is refutab
2525
```scala
2626
val first :: rest = elems // error
2727
```
28-
This works in Scala 2. In fact it is a typical use case for Scala 2's rules. But in Scala 3.1 it will give a warning. One can avoid the warning by marking the right-hand side with an [`@unchecked`](https://scala-lang.org/api/3.x/scala/unchecked.html) annotation:
28+
This works in Scala 2. In fact it is a typical use case for Scala 2's rules. But in Scala 3.2 it will give a warning. One can avoid the warning by marking the right-hand side with an [`@unchecked`](https://scala-lang.org/api/3.x/scala/unchecked.html) annotation:
2929
```scala
3030
val first :: rest = elems: @unchecked // OK
3131
```
@@ -40,7 +40,7 @@ val elems: List[Any] = List((1, 2), "hello", (3, 4))
4040
for (x, y) <- elems yield (y, x) // error: pattern's type (Any, Any) is more specialized
4141
// than the right-hand side expression's type Any
4242
```
43-
This code gives a compile-time warning in Scala 3.1 whereas in Scala 2 the list `elems`
43+
This code gives a compile-time warning in Scala 3.2 whereas in Scala 2 the list `elems`
4444
is filtered to retain only the elements of tuple type that match the pattern `(x, y)`.
4545
The filtering functionality can be obtained in Scala 3 by prefixing the pattern with `case`:
4646
```scala
@@ -56,4 +56,4 @@ Generator ::= [‘case’] Pattern1 ‘<-’ Expr ;
5656

5757
## Migration
5858

59-
The new syntax is supported in Scala 3.0. However, to enable smooth cross compilation between Scala 2 and Scala 3, the changed behavior and additional type checks are only enabled under the `-source future` setting. They will be enabled by default in version 3.1 of the language.
59+
The new syntax is supported in Scala 3.0. However, to enable smooth cross compilation between Scala 2 and Scala 3, the changed behavior and additional type checks are only enabled under the `-source future` setting. They will be enabled by default in version 3.2 of the language.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// These tests should fail under -Xfatal-warnings with source version source version 3.2 or later
2+
import language.`3.2`
3+
4+
object Test:
5+
// from filtering-fors.scala
6+
val xs: List[AnyRef] = ???
7+
8+
for ((x: String) <- xs) do () // error
9+
for (y@ (x: String) <- xs) do () // error
10+
for ((x, y) <- xs) do () // error
11+
12+
for ((x: String) <- xs if x.isEmpty) do () // error
13+
for ((x: String) <- xs; y = x) do () // error
14+
for ((x: String) <- xs; (y, z) <- xs) do () // error // error
15+
for (case (x: String) <- xs; (y, z) <- xs) do () // error
16+
for ((x: String) <- xs; case (y, z) <- xs) do () // error
17+
18+
val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4))
19+
for ((x, y) <- pairs) yield (y, x) // error
20+
21+
// from unchecked-patterns.scala
22+
val y :: ys = List(1, 2, 3) // error
23+
val (1, c) = (1, 2) // error
24+
val 1 *: cs = 1 *: Tuple() // error
25+
26+
val (_: Int | _: AnyRef) = ??? : AnyRef // error
27+
28+
val 1 = 2 // error
29+
30+
object Positive { def unapply(i: Int): Option[Int] = Some(i).filter(_ > 0) }
31+
object Always1 { def unapply(i: Int): Some[Int] = Some(i) }
32+
object Pair { def unapply(t: (Int, Int)): t.type = t }
33+
object Triple { def unapply(t: (Int, Int, Int)): (Int, Int, Int) = t }
34+
35+
val Positive(p) = 5 // error
36+
val Some(s1) = Option(1) // error

tests/neg/patbind-messages.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// scalac: -source:future -Werror
1+
// scalac: -Werror
22
object Test {
33
// refutable extractor
44
object Positive { def unapply(i: Int): Option[Int] = Some(i).filter(_ > 0) }
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// These tests should pass under -Xfatal-warnings with source version less than 3.2
2+
import language.`3.0-migration`
3+
4+
object Test:
5+
// from filtering-fors.scala
6+
val xs: List[AnyRef] = ???
7+
8+
for ((x: String) <- xs) do ()
9+
for (y@ (x: String) <- xs) do ()
10+
for ((x, y) <- xs) do ()
11+
12+
for ((x: String) <- xs if x.isEmpty) do ()
13+
for ((x: String) <- xs; y = x) do ()
14+
for ((x: String) <- xs; (y, z) <- xs) do ()
15+
for (case (x: String) <- xs; (y, z) <- xs) do ()
16+
for ((x: String) <- xs; case (y, z) <- xs) do ()
17+
18+
val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4))
19+
for ((x, y) <- pairs) yield (y, x)
20+
21+
// from unchecked-patterns.scala
22+
val y :: ys = List(1, 2, 3)
23+
val (1, c) = (1, 2)
24+
val 1 *: cs = 1 *: Tuple()
25+
26+
val (_: Int | _: AnyRef) = ??? : AnyRef
27+
28+
val 1 = 2
29+
30+
object Positive { def unapply(i: Int): Option[Int] = Some(i).filter(_ > 0) }
31+
object Always1 { def unapply(i: Int): Some[Int] = Some(i) }
32+
object Pair { def unapply(t: (Int, Int)): t.type = t }
33+
object Triple { def unapply(t: (Int, Int, Int)): (Int, Int, Int) = t }
34+
35+
val Positive(p) = 5
36+
val Some(s1) = Option(1)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// These tests should pass under -Xfatal-warnings with source version less than 3.2
2+
import language.`3.1`
3+
4+
object Test:
5+
// from filtering-fors.scala
6+
val xs: List[AnyRef] = ???
7+
8+
for ((x: String) <- xs) do ()
9+
for (y@ (x: String) <- xs) do ()
10+
for ((x, y) <- xs) do ()
11+
12+
for ((x: String) <- xs if x.isEmpty) do ()
13+
for ((x: String) <- xs; y = x) do ()
14+
for ((x: String) <- xs; (y, z) <- xs) do ()
15+
for (case (x: String) <- xs; (y, z) <- xs) do ()
16+
for ((x: String) <- xs; case (y, z) <- xs) do ()
17+
18+
val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4))
19+
for ((x, y) <- pairs) yield (y, x)
20+
21+
// from unchecked-patterns.scala
22+
val y :: ys = List(1, 2, 3)
23+
val (1, c) = (1, 2)
24+
val 1 *: cs = 1 *: Tuple()
25+
26+
val (_: Int | _: AnyRef) = ??? : AnyRef
27+
28+
val 1 = 2
29+
30+
object Positive { def unapply(i: Int): Option[Int] = Some(i).filter(_ > 0) }
31+
object Always1 { def unapply(i: Int): Some[Int] = Some(i) }
32+
object Pair { def unapply(t: (Int, Int)): t.type = t }
33+
object Triple { def unapply(t: (Int, Int, Int)): (Int, Int, Int) = t }
34+
35+
val Positive(p) = 5
36+
val Some(s1) = Option(1)

tests/run-custom-args/fors.check

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)