Skip to content

Commit 3e105f2

Browse files
authored
Better error message when a pattern match extractor is not found. (#18725)
Fixes #18684
2 parents 2fa54e8 + eed38ec commit 3e105f2

13 files changed

+169
-37
lines changed

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

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

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

143-
// cycleSym.flags would try completing denot and would fail, but here we can use flagsUNSAFE to detect flags
144-
// set by the parser.
148+
override def toMessage(using Context): Message =
145149
val unsafeFlags = cycleSym.flagsUNSAFE
146150
val isMethod = unsafeFlags.is(Method)
147151
val isVal = !isMethod && cycleSym.isTerm

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
202202
case ImplausiblePatternWarningID // erorNumber: 186
203203
case SynchronizedCallOnBoxedClassID // errorNumber: 187
204204
case VarArgsParamCannotBeGivenID // erorNumber: 188
205+
case ExtractorNotFoundID // errorNumber: 189
205206

206207
def errorNumber = ordinal - 1
207208

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

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

2366-
class NotAnExtractor(tree: untpd.Tree)(using Context) extends SyntaxMsg(NotAnExtractorID) {
2366+
class NotAnExtractor(tree: untpd.Tree)(using Context) extends PatternMatchMsg(NotAnExtractorID) {
23672367
def msg(using Context) = i"$tree cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method"
23682368
def explain(using Context) =
23692369
i"""|An ${hl("unapply")} method should be defined in an ${hl("object")} as follow:
@@ -2376,6 +2376,24 @@ class NotAnExtractor(tree: untpd.Tree)(using Context) extends SyntaxMsg(NotAnExt
23762376
|This mechanism is used for instance in pattern ${hl("case List(x1, ..., xn)")}"""
23772377
}
23782378

2379+
class ExtractorNotFound(val name: Name)(using Context) extends NotFoundMsg(ExtractorNotFoundID):
2380+
def msg(using Context) = i"no pattern match extractor named $name was found"
2381+
def explain(using Context) =
2382+
i"""An application $name(...) in a pattern can refer to an extractor
2383+
|which defines an unapply or unapplySeq method. Example:
2384+
|
2385+
| object split:
2386+
| def unapply(x: String) =
2387+
| val (leading, trailing) = x.splitAt(x.length / 2)
2388+
| Some((leading, trailing))
2389+
|
2390+
| val split(fst, snd) = "HiHo"
2391+
|
2392+
|The extractor pattern `split(fst, snd)` defines `fst` as the first half "Hi" and
2393+
|`snd` as the second half "Ho" of the right hand side "HiHo". Case classes and
2394+
|enum cases implicitly define extractors with the name of the class or enum case.
2395+
|Here, no extractor named $name was found, so the pattern could not be typed."""
2396+
23792397
class MemberWithSameNameAsStatic()(using Context)
23802398
extends SyntaxMsg(MemberWithSameNameAsStaticID) {
23812399
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
@@ -1295,15 +1295,22 @@ trait Applications extends Compatibility {
12951295

12961296
/** Report errors buffered in state.
12971297
* @pre state has errors to report
1298-
* If there is a single error stating that "unapply" is not a member, print
1299-
* the more informative "notAnExtractor" message instead.
1298+
* If the last reported error states that "unapply" is not a member, report
1299+
* the more informative `NotAnExtractor` message instead.
1300+
* If the last reported error states that the qualifier was not found, report
1301+
* the more informative `ExtractorNotFound` message instead.
13001302
*/
13011303
def reportErrors(tree: Tree, state: TyperState): Tree =
13021304
assert(state.reporter.hasErrors)
1303-
if saysNotFound(state, nme.unapply) then notAnExtractor(tree)
1304-
else
1305-
state.reporter.flush()
1306-
tree
1305+
if saysNotFound(state, nme.unapply) then
1306+
notAnExtractor(tree)
1307+
else qual match
1308+
case qual: Ident if saysNotFound(state, qual.name) =>
1309+
report.error(ExtractorNotFound(qual.name), tree.srcPos)
1310+
tree
1311+
case _ =>
1312+
state.reporter.flush()
1313+
tree
13071314

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

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,6 +3184,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
31843184
case _ => typedUnadapted(desugar(tree, pt), pt, locked)
31853185
}
31863186

3187+
def handleTypeError(ex: TypeError): Tree = ex match
3188+
case ex: CyclicReference
3189+
if ctx.reporter.errorsReported
3190+
&& xtree.span.isZeroExtent
3191+
&& ex.isVal =>
3192+
// Don't report a "recursive val ... needs type" if errors were reported
3193+
// previously and the span of the offending tree is empty. In this case,
3194+
// it's most likely that this is desugared code, and the error message would
3195+
// be redundant and confusing.
3196+
xtree.withType(ErrorType(ex.toMessage))
3197+
case _ =>
3198+
// Use focussed sourcePos since tree might be a large definition
3199+
// and a large error span would hide all errors in interior.
3200+
// TODO: Not clear that hiding is what we want, actually
3201+
errorTree(xtree, ex, xtree.srcPos.focus)
3202+
31873203
try
31883204
val ifpt = defn.asContextFunctionType(pt)
31893205
val result =
@@ -3206,11 +3222,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
32063222
result.tpe.stripTypeVar match
32073223
case e: ErrorType if !unsimplifiedType.isErroneous => errorTree(xtree, e.msg, xtree.srcPos)
32083224
case _ => result
3209-
catch case ex: TypeError => errorTree(xtree, ex, xtree.srcPos.focus)
3210-
// use focussed sourcePos since tree might be a large definition
3211-
// and a large error span would hide all errors in interior.
3212-
// TODO: Not clear that hiding is what we want, actually
3213-
}
3225+
catch case ex: TypeError =>
3226+
handleTypeError(ex)
3227+
}
32143228
}
32153229

32163230
/** Interpolate and simplify the type of the given tree. */

tests/neg-macros/i6997b.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import scala.quoted.*
55
inline def mcr(x: => Any): Any = ${mcrImpl('x)}
66

77
def mcrImpl(body: Expr[Any])(using ctx: Quotes): Expr[Any] = {
8-
val '{$x: $t} = body // error // error
8+
val '{$x: $t} = body // error
99
'{
1010
val tmp: $t = $x.asInstanceOf[$t] // error // error
1111
println(tmp)

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/i18020.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def foo1: Unit =
2424
// then Typer rejects "String" as an infix extractor (like ::)
2525
// which is the second error
2626

27-
def foo2: Unit = // error
27+
def foo2: Unit = // was: error, recursive value _root_ needs type
2828
val _root_ : String = "abc" // error
2929

3030
// i17757

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
@@ -31,7 +31,7 @@
3131
|
3232
| longer explanation available when compiling with `-explain`
3333
-- [E032] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:23:17 ---------------------------------------------------
34-
23 | val K(ns @ _*, xx) = k // error: pattern expected // error
34+
23 | val K(ns @ _*, xx) = k // error: pattern expected
3535
| ^
3636
| pattern expected
3737
|
@@ -46,22 +46,16 @@
4646
| x is already defined as value x
4747
|
4848
| Note that overloaded methods must all be defined in the same group of toplevel definitions
49-
-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------
49+
-- [E189] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------
5050
12 | case List(1, _*3,) => // error: pattern expected // error
5151
| ^
52-
| Not found: *
52+
| no pattern match extractor named * was found
5353
|
5454
| longer explanation available when compiling with `-explain`
55-
-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------
55+
-- [E189] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------
5656
13 | case List(1, _*3:) => // error // error
5757
| ^
58-
| Not found: *
59-
|
60-
| longer explanation available when compiling with `-explain`
61-
-- [E045] Cyclic Error: tests/neg/t5702-neg-bad-and-wild.scala:23:19 ---------------------------------------------------
62-
23 | val K(ns @ _*, xx) = k // error: pattern expected // error
63-
| ^
64-
| Recursive value $1$ needs type
58+
| no pattern match extractor named * was found
6559
|
6660
| longer explanation available when compiling with `-explain`
6761
-- 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) // error: bad use of `*`
2626
// no longer complains

0 commit comments

Comments
 (0)