Skip to content

Commit 5258c3c

Browse files
Backport "Better error message when a pattern match extractor is not found." to LTS (#20726)
Backports #18725 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents ea2adba + 8937d0b commit 5258c3c

File tree

10 files changed

+165
-34
lines changed

10 files changed

+165
-34
lines changed

compiler/src/dotty/tools/dotc/core/TypeErrors.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,18 @@ end handleRecursive
131131
* so it requires knowing denot already.
132132
* @param denot
133133
*/
134-
class CyclicReference private (val denot: SymDenotation)(using Context) extends TypeError:
134+
class CyclicReference(val denot: SymDenotation)(using Context) extends TypeError:
135135
var inImplicitSearch: Boolean = false
136136

137-
override def toMessage(using Context): Message =
138-
val cycleSym = denot.symbol
137+
val cycleSym = denot.symbol
138+
139+
// cycleSym.flags would try completing denot and would fail, but here we can use flagsUNSAFE to detect flags
140+
// set by the parser.
141+
def unsafeFlags = cycleSym.flagsUNSAFE
142+
def isMethod = unsafeFlags.is(Method)
143+
def isVal = !isMethod && cycleSym.isTerm
139144

140-
// cycleSym.flags would try completing denot and would fail, but here we can use flagsUNSAFE to detect flags
141-
// set by the parser.
145+
override def toMessage(using Context): Message =
142146
val unsafeFlags = cycleSym.flagsUNSAFE
143147
val isMethod = unsafeFlags.is(Method)
144148
val isVal = !isMethod && cycleSym.isTerm

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2340,7 +2340,7 @@ class ClassCannotExtendEnum(cls: Symbol, parent: Symbol)(using Context) extends
23402340
def explain(using Context) = ""
23412341
}
23422342

2343-
class NotAnExtractor(tree: untpd.Tree)(using Context) extends SyntaxMsg(NotAnExtractorID) {
2343+
class NotAnExtractor(tree: untpd.Tree)(using Context) extends PatternMatchMsg(NotAnExtractorID) {
23442344
def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method"
23452345
def explain(using Context) =
23462346
i"""|An ${hl("unapply")} method should be defined in an ${hl("object")} as follow:
@@ -2353,6 +2353,24 @@ class NotAnExtractor(tree: untpd.Tree)(using Context) extends SyntaxMsg(NotAnExt
23532353
|This mechanism is used for instance in pattern ${hl("case List(x1, ..., xn)")}"""
23542354
}
23552355

2356+
class ExtractorNotFound(val name: Name)(using Context) extends NotFoundMsg(ExtractorNotFoundID):
2357+
def msg(using Context) = i"no pattern match extractor named $name was found"
2358+
def explain(using Context) =
2359+
i"""An application $name(...) in a pattern can refer to an extractor
2360+
|which defines an unapply or unapplySeq method. Example:
2361+
|
2362+
| object split:
2363+
| def unapply(x: String) =
2364+
| val (leading, trailing) = x.splitAt(x.length / 2)
2365+
| Some((leading, trailing))
2366+
|
2367+
| val split(fst, snd) = "HiHo"
2368+
|
2369+
|The extractor pattern `split(fst, snd)` defines `fst` as the first half "Hi" and
2370+
|`snd` as the second half "Ho" of the right hand side "HiHo". Case classes and
2371+
|enum cases implicitly define extractors with the name of the class or enum case.
2372+
|Here, no extractor named $name was found, so the pattern could not be typed."""
2373+
23562374
class MemberWithSameNameAsStatic()(using Context)
23572375
extends SyntaxMsg(MemberWithSameNameAsStaticID) {
23582376
def msg(using Context) = i"Companion classes cannot define members with same name as a ${hl("@static")} member"

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,15 +1278,22 @@ trait Applications extends Compatibility {
12781278

12791279
/** Report errors buffered in state.
12801280
* @pre state has errors to report
1281-
* If there is a single error stating that "unapply" is not a member, print
1282-
* the more informative "notAnExtractor" message instead.
1281+
* If the last reported error states that "unapply" is not a member, report
1282+
* the more informative `NotAnExtractor` message instead.
1283+
* If the last reported error states that the qualifier was not found, report
1284+
* the more informative `ExtractorNotFound` message instead.
12831285
*/
12841286
def reportErrors(tree: Tree, state: TyperState): Tree =
12851287
assert(state.reporter.hasErrors)
1286-
if saysNotFound(state, nme.unapply) then notAnExtractor(tree)
1287-
else
1288-
state.reporter.flush()
1289-
tree
1288+
if saysNotFound(state, nme.unapply) then
1289+
notAnExtractor(tree)
1290+
else qual match
1291+
case qual: Ident if saysNotFound(state, qual.name) =>
1292+
report.error(ExtractorNotFound(qual.name), tree.srcPos)
1293+
tree
1294+
case _ =>
1295+
state.reporter.flush()
1296+
tree
12901297

12911298
/** If this is a term ref tree, try to typecheck with its type name.
12921299
* If this refers to a type alias, follow the alias, and if

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3098,6 +3098,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
30983098
case _ => typedUnadapted(desugar(tree, pt), pt, locked)
30993099
}
31003100

3101+
def handleTypeError(ex: TypeError): Tree = ex match
3102+
case ex: CyclicReference
3103+
if ctx.reporter.errorsReported
3104+
&& xtree.span.isZeroExtent
3105+
&& ex.isVal =>
3106+
// Don't report a "recursive val ... needs type" if errors were reported
3107+
// previously and the span of the offending tree is empty. In this case,
3108+
// it's most likely that this is desugared code, and the error message would
3109+
// be redundant and confusing.
3110+
xtree.withType(ErrorType(ex.toMessage))
3111+
case _ =>
3112+
// Use focussed sourcePos since tree might be a large definition
3113+
// and a large error span would hide all errors in interior.
3114+
// TODO: Not clear that hiding is what we want, actually
3115+
errorTree(xtree, ex, xtree.srcPos.focus)
3116+
31013117
try
31023118
val ifpt = defn.asContextFunctionType(pt)
31033119
val result =
@@ -3115,10 +3131,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
31153131
case xtree => typedUnnamed(xtree)
31163132

31173133
simplify(result, pt, locked)
3118-
catch case ex: TypeError => errorTree(xtree, ex, xtree.srcPos.focus)
3119-
// use focussed sourcePos since tree might be a large definition
3120-
// and a large error span would hide all errors in interior.
3121-
// TODO: Not clear that hiding is what we want, actually
3134+
catch case ex: TypeError =>
3135+
handleTypeError(ex)
31223136
}
31233137
}
31243138

tests/neg/bad-unapplies.check

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
| both match arguments (C)
88
|
99
| longer explanation available when compiling with `-explain`
10-
-- [E127] Syntax Error: tests/neg/bad-unapplies.scala:23:9 -------------------------------------------------------------
10+
-- [E127] Pattern Match Error: tests/neg/bad-unapplies.scala:23:9 ------------------------------------------------------
1111
23 | case B("2") => // error (cannot be used as an extractor)
1212
| ^
1313
| B cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method
1414
|
1515
| longer explanation available when compiling with `-explain`
16-
-- [E127] Syntax Error: tests/neg/bad-unapplies.scala:24:9 -------------------------------------------------------------
16+
-- [E127] Pattern Match Error: tests/neg/bad-unapplies.scala:24:9 ------------------------------------------------------
1717
24 | case D("2") => // error (cannot be used as an extractor)
1818
| ^
1919
| D cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method
@@ -31,9 +31,9 @@
3131
| Wrong number of argument patterns for F; expected: ()
3232
|
3333
| longer explanation available when compiling with `-explain`
34-
-- [E006] Not Found Error: tests/neg/bad-unapplies.scala:27:9 ----------------------------------------------------------
34+
-- [E189] Not Found Error: tests/neg/bad-unapplies.scala:27:9 ----------------------------------------------------------
3535
27 | case G("2") => // error (Not found: G)
3636
| ^
37-
| Not found: G
37+
| no pattern match extractor named G was found
3838
|
3939
| longer explanation available when compiling with `-explain`

tests/neg/i18684.check

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
-- [E189] Not Found Error: tests/neg/i18684.scala:3:6 ------------------------------------------------------------------
2+
3 | val s(): String = "hello, world" // error
3+
| ^
4+
| no pattern match extractor named s was found
5+
|---------------------------------------------------------------------------------------------------------------------
6+
| Explanation (enabled by `-explain`)
7+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
8+
| An application s(...) in a pattern can refer to an extractor
9+
| which defines an unapply or unapplySeq method. Example:
10+
|
11+
| object split:
12+
| def unapply(x: String) =
13+
| val (leading, trailing) = x.splitAt(x.length / 2)
14+
| Some((leading, trailing))
15+
|
16+
| val split(fst, snd) = "HiHo"
17+
|
18+
| The extractor pattern `split(fst, snd)` defines `fst` as the first half "Hi" and
19+
| `snd` as the second half "Ho" of the right hand side "HiHo". Case classes and
20+
| enum cases implicitly define extractors with the name of the class or enum case.
21+
| Here, no extractor named s was found, so the pattern could not be typed.
22+
---------------------------------------------------------------------------------------------------------------------
23+
-- [E189] Not Found Error: tests/neg/i18684.scala:5:6 ------------------------------------------------------------------
24+
5 | val i() = 22 // error
25+
| ^
26+
| no pattern match extractor named i was found
27+
|---------------------------------------------------------------------------------------------------------------------
28+
| Explanation (enabled by `-explain`)
29+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
30+
| An application i(...) in a pattern can refer to an extractor
31+
| which defines an unapply or unapplySeq method. Example:
32+
|
33+
| object split:
34+
| def unapply(x: String) =
35+
| val (leading, trailing) = x.splitAt(x.length / 2)
36+
| Some((leading, trailing))
37+
|
38+
| val split(fst, snd) = "HiHo"
39+
|
40+
| The extractor pattern `split(fst, snd)` defines `fst` as the first half "Hi" and
41+
| `snd` as the second half "Ho" of the right hand side "HiHo". Case classes and
42+
| enum cases implicitly define extractors with the name of the class or enum case.
43+
| Here, no extractor named i was found, so the pattern could not be typed.
44+
---------------------------------------------------------------------------------------------------------------------
45+
-- [E189] Not Found Error: tests/neg/i18684.scala:10:8 -----------------------------------------------------------------
46+
10 | val foo() = "33" // error
47+
| ^^^
48+
| no pattern match extractor named foo was found
49+
|--------------------------------------------------------------------------------------------------------------------
50+
| Explanation (enabled by `-explain`)
51+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
52+
| An application foo(...) in a pattern can refer to an extractor
53+
| which defines an unapply or unapplySeq method. Example:
54+
|
55+
| object split:
56+
| def unapply(x: String) =
57+
| val (leading, trailing) = x.splitAt(x.length / 2)
58+
| Some((leading, trailing))
59+
|
60+
| val split(fst, snd) = "HiHo"
61+
|
62+
| The extractor pattern `split(fst, snd)` defines `fst` as the first half "Hi" and
63+
| `snd` as the second half "Ho" of the right hand side "HiHo". Case classes and
64+
| enum cases implicitly define extractors with the name of the class or enum case.
65+
| Here, no extractor named foo was found, so the pattern could not be typed.
66+
--------------------------------------------------------------------------------------------------------------------
67+
-- [E127] Pattern Match Error: tests/neg/i18684.scala:12:6 -------------------------------------------------------------
68+
12 | val inner(x) = 3 // error
69+
| ^^^^^
70+
| Test.inner cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method
71+
|--------------------------------------------------------------------------------------------------------------------
72+
| Explanation (enabled by `-explain`)
73+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
74+
| An unapply method should be defined in an object as follow:
75+
| - If it is just a test, return a Boolean. For example case even()
76+
| - If it returns a single sub-value of type T, return an Option[T]
77+
| - If it returns several sub-values T1,...,Tn, group them in an optional tuple Option[(T1,...,Tn)]
78+
|
79+
| Sometimes, the number of sub-values isn't fixed and we would like to return a sequence.
80+
| For this reason, you can also define patterns through unapplySeq which returns Option[Seq[T]].
81+
| This mechanism is used for instance in pattern case List(x1, ..., xn)
82+
--------------------------------------------------------------------------------------------------------------------

tests/neg/i18684.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//> using options -explain
2+
object Test:
3+
val s(): String = "hello, world" // error
4+
5+
val i() = 22 // error
6+
7+
def foo(): String = "22"
8+
9+
object inner:
10+
val foo() = "33" // error
11+
12+
val inner(x) = 3 // error

tests/neg/i5101.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
-- [E006] Not Found Error: tests/neg/i5101.scala:11:11 -----------------------------------------------------------------
1+
-- [E189] Not Found Error: tests/neg/i5101.scala:11:11 -----------------------------------------------------------------
22
11 | case A0(_) => // error
33
| ^^
4-
| Not found: A0
4+
| no pattern match extractor named A0 was found
55
|
66
| longer explanation available when compiling with `-explain`

tests/neg/t5702-neg-bad-and-wild.check

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
|
2828
| longer explanation available when compiling with `-explain`
2929
-- [E032] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:23:17 ---------------------------------------------------
30-
23 | val K(ns @ _*, xx) = k // error: pattern expected // error
30+
23 | val K(ns @ _*, xx) = k // error: pattern expected
3131
| ^
3232
| pattern expected
3333
|
@@ -38,22 +38,16 @@
3838
| x is already defined as value x
3939
|
4040
| Note that overloaded methods must all be defined in the same group of toplevel definitions
41-
-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------
41+
-- [E189] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------
4242
12 | case List(1, _*3,) => // error: pattern expected // error
4343
| ^
44-
| Not found: *
44+
| no pattern match extractor named * was found
4545
|
4646
| longer explanation available when compiling with `-explain`
47-
-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------
47+
-- [E189] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------
4848
13 | case List(1, _*3:) => // error // error
4949
| ^
50-
| Not found: *
51-
|
52-
| longer explanation available when compiling with `-explain`
53-
-- [E045] Cyclic Error: tests/neg/t5702-neg-bad-and-wild.scala:23:19 ---------------------------------------------------
54-
23 | val K(ns @ _*, xx) = k // error: pattern expected // error
55-
| ^
56-
| Recursive value $1$ needs type
50+
| no pattern match extractor named * was found
5751
|
5852
| longer explanation available when compiling with `-explain`
5953
-- Warning: tests/neg/t5702-neg-bad-and-wild.scala:13:22 ---------------------------------------------------------------

tests/neg/t5702-neg-bad-and-wild.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ object Test {
2020
// good syntax, bad semantics, detected by typer
2121
//gowild.scala:14: error: star patterns must correspond with varargs parameters
2222
val K(x @ _*) = k
23-
val K(ns @ _*, xx) = k // error: pattern expected // error
23+
val K(ns @ _*, xx) = k // error: pattern expected
2424
val K(x) = k // error: x is already defined as value x
2525
val (b, _ * ) = (5,6) // ok
2626
// no longer complains

0 commit comments

Comments
 (0)