Skip to content

Commit d6ae0cb

Browse files
committed
Allow @implicitNotFound messages as explanations
A problem of the @implicitNotFOund mechanism so far was that the user defined message replaced the compiler-generated one, which might lose valuable information. This commit adds an alternative where an @implicitNotFound message that starts with `...` is taken as an explanation (without the ...) enabled under -explain. The compiler-generated message is then kept as the explicit error message. We apply the mechanism for an @implicitNotFound message for `boundary.Label`. This now produces messages like this one: ``` -- [E172] Type Error: tests/neg-custom-args/explain/labelNotFound.scala:2:30 ------------------------------------------- 2 | scala.util.boundary.break(1) // error | ^ |No given instance of type scala.util.boundary.Label[Int] was found for parameter label of method break in object boundary |--------------------------------------------------------------------------------------------------------------------- | Explanation (enabled by `-explain`) |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | A Label is generated from an enclosing `scala.util.boundary` call. | Maybe that boundary is missing? --------------------------------------------------------------------------------------------------------------------- ```
1 parent 3d251d6 commit d6ae0cb

File tree

15 files changed

+148
-127
lines changed

15 files changed

+148
-127
lines changed

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

Lines changed: 103 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2562,6 +2562,106 @@ class MissingImplicitArgument(
25622562
case ambi: AmbiguousImplicits => withoutDisambiguation()
25632563
case _ =>
25642564

2565+
/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
2566+
* all occurrences of `${X}` where `X` is in `paramNames` with the
2567+
* corresponding shown type in `args`.
2568+
*/
2569+
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type])(using Context): String =
2570+
def translate(name: String): Option[String] =
2571+
val idx = paramNames.indexOf(name)
2572+
if (idx >= 0) Some(i"${args(idx)}") else None
2573+
"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match
2574+
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse("")).nn
2575+
)
2576+
2577+
/** @param rawMsg Message template with variables, e.g. "Variable A is ${A}"
2578+
* @param sym Symbol of the annotated type or of the method whose parameter was annotated
2579+
* @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int
2580+
*/
2581+
def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type)(using Context): String =
2582+
val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym)
2583+
userDefinedErrorString(
2584+
rawMsg,
2585+
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
2586+
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
2587+
)
2588+
2589+
/** Extract a user defined error message from a symbol `sym`
2590+
* with an annotation matching the given class symbol `cls`.
2591+
*/
2592+
def userDefinedMsg(sym: Symbol, cls: Symbol)(using Context) =
2593+
for
2594+
ann <- sym.getAnnotation(cls)
2595+
msg <- ann.argumentConstantString(0)
2596+
yield msg
2597+
2598+
def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol)(using Context): Option[String] =
2599+
for
2600+
rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot)
2601+
if Feature.migrateTo3 || sym != defn.Function1
2602+
// Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore
2603+
yield
2604+
val substituteType = (_: Type).asSeenFrom(pt, sym)
2605+
formatAnnotationMessage(rawMsg, sym, substituteType)
2606+
2607+
/** Extracting the message from a method parameter, e.g. in
2608+
*
2609+
* trait Foo
2610+
*
2611+
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
2612+
*/
2613+
def userDefinedImplicitNotFoundParamMessage(using Context): Option[String] =
2614+
paramSymWithMethodCallTree.flatMap: (sym, applTree) =>
2615+
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map: rawMsg =>
2616+
val fn = tpd.funPart(applTree)
2617+
val targs = tpd.typeArgss(applTree).flatten
2618+
val methodOwner = fn.symbol.owner
2619+
val methodOwnerType = tpd.qualifier(fn).tpe
2620+
val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType)
2621+
val methodTypeArgs = targs.map(_.tpe)
2622+
val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs)
2623+
formatAnnotationMessage(rawMsg, sym.owner, substituteType)
2624+
2625+
def userDefinedImplicitNotFoundTypeMessage(using Context): Option[String] =
2626+
def recur(tp: Type): Option[String] = tp match
2627+
case tp: TypeRef =>
2628+
val sym = tp.symbol
2629+
userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info))
2630+
case tp: ClassInfo =>
2631+
tp.baseClasses.iterator
2632+
.map(userDefinedImplicitNotFoundTypeMessageFor)
2633+
.find(_.isDefined).flatten
2634+
case tp: TypeProxy =>
2635+
recur(tp.superType)
2636+
case tp: AndType =>
2637+
recur(tp.tp1).orElse(recur(tp.tp2))
2638+
case _ =>
2639+
None
2640+
recur(pt)
2641+
2642+
/** The implicitNotFound annotation on the parameter, or else on the type.
2643+
* implicitNotFound message strings starting with `...` are intended for
2644+
* additional explanations, not the message proper. The leading `...` is
2645+
* dropped in this case.
2646+
* @param explain The message is used for an additional explanation, not
2647+
* the message proper.
2648+
*/
2649+
def userDefinedImplicitNotFoundMessage(explain: Boolean)(using Context): Option[String] =
2650+
def filter(msg: Option[String]) = msg match
2651+
case Some(str) =>
2652+
if str.startsWith("...") then
2653+
if explain then Some(str.drop(3)) else None
2654+
else if explain then None
2655+
else msg
2656+
case None => None
2657+
filter(userDefinedImplicitNotFoundParamMessage)
2658+
.orElse(filter(userDefinedImplicitNotFoundTypeMessage))
2659+
2660+
object AmbiguousImplicitMsg {
2661+
def unapply(search: SearchSuccess): Option[String] =
2662+
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
2663+
}
2664+
25652665
def msg(using Context): String =
25662666

25672667
def formatMsg(shortForm: String)(headline: String = shortForm) = arg match
@@ -2585,29 +2685,6 @@ class MissingImplicitArgument(
25852685
|But ${tpe.explanation}."""
25862686
case _ => headline
25872687

2588-
/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
2589-
* all occurrences of `${X}` where `X` is in `paramNames` with the
2590-
* corresponding shown type in `args`.
2591-
*/
2592-
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
2593-
def translate(name: String): Option[String] = {
2594-
val idx = paramNames.indexOf(name)
2595-
if (idx >= 0) Some(i"${args(idx)}") else None
2596-
}
2597-
2598-
"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match {
2599-
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse("")).nn
2600-
})
2601-
}
2602-
2603-
/** Extract a user defined error message from a symbol `sym`
2604-
* with an annotation matching the given class symbol `cls`.
2605-
*/
2606-
def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
2607-
ann <- sym.getAnnotation(cls)
2608-
msg <- ann.argumentConstantString(0)
2609-
} yield msg
2610-
26112688
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"
26122689

26132690
def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) =
@@ -2644,77 +2721,13 @@ class MissingImplicitArgument(
26442721
userDefinedErrorString(raw, params, args)
26452722
}
26462723

2647-
/** @param rawMsg Message template with variables, e.g. "Variable A is ${A}"
2648-
* @param sym Symbol of the annotated type or of the method whose parameter was annotated
2649-
* @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int
2650-
*/
2651-
def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = {
2652-
val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym)
2653-
2654-
userDefinedErrorString(
2655-
rawMsg,
2656-
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
2657-
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
2658-
)
2659-
}
2660-
2661-
/** Extracting the message from a method parameter, e.g. in
2662-
*
2663-
* trait Foo
2664-
*
2665-
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
2666-
*/
2667-
def userDefinedImplicitNotFoundParamMessage: Option[String] = paramSymWithMethodCallTree.flatMap { (sym, applTree) =>
2668-
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
2669-
val fn = tpd.funPart(applTree)
2670-
val targs = tpd.typeArgss(applTree).flatten
2671-
val methodOwner = fn.symbol.owner
2672-
val methodOwnerType = tpd.qualifier(fn).tpe
2673-
val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType)
2674-
val methodTypeArgs = targs.map(_.tpe)
2675-
val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs)
2676-
formatAnnotationMessage(rawMsg, sym.owner, substituteType)
2677-
}
2678-
}
2679-
26802724
/** Extracting the message from a type, e.g. in
26812725
*
26822726
* @annotation.implicitNotFound("Foo is missing")
26832727
* trait Foo
26842728
*
26852729
* def foo(implicit foo: Foo): Any = ???
26862730
*/
2687-
def userDefinedImplicitNotFoundTypeMessage: Option[String] =
2688-
def recur(tp: Type): Option[String] = tp match
2689-
case tp: TypeRef =>
2690-
val sym = tp.symbol
2691-
userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info))
2692-
case tp: ClassInfo =>
2693-
tp.baseClasses.iterator
2694-
.map(userDefinedImplicitNotFoundTypeMessageFor)
2695-
.find(_.isDefined).flatten
2696-
case tp: TypeProxy =>
2697-
recur(tp.superType)
2698-
case tp: AndType =>
2699-
recur(tp.tp1).orElse(recur(tp.tp2))
2700-
case _ =>
2701-
None
2702-
recur(pt)
2703-
2704-
def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol): Option[String] =
2705-
for
2706-
rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot)
2707-
if Feature.migrateTo3 || sym != defn.Function1
2708-
// Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore
2709-
yield
2710-
val substituteType = (_: Type).asSeenFrom(pt, sym)
2711-
formatAnnotationMessage(rawMsg, sym, substituteType)
2712-
2713-
object AmbiguousImplicitMsg {
2714-
def unapply(search: SearchSuccess): Option[String] =
2715-
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
2716-
}
2717-
27182731
arg.tpe match
27192732
case ambi: AmbiguousImplicits =>
27202733
(ambi.alt1, ambi.alt2) match
@@ -2728,8 +2741,7 @@ class MissingImplicitArgument(
27282741
i"""No implicit search was attempted${location("for")}
27292742
|since the expected type $target is not specific enough"""
27302743
case _ =>
2731-
val shortMessage = userDefinedImplicitNotFoundParamMessage
2732-
.orElse(userDefinedImplicitNotFoundTypeMessage)
2744+
val shortMessage = userDefinedImplicitNotFoundMessage(explain = false)
27332745
.getOrElse(defaultImplicitNotFoundMessage)
27342746
formatMsg(shortMessage)()
27352747
end msg
@@ -2758,7 +2770,8 @@ class MissingImplicitArgument(
27582770
.orElse(noChainConversionsNote(ignoredConvertibleImplicits))
27592771
.getOrElse(ctx.typer.importSuggestionAddendum(pt))
27602772

2761-
def explain(using Context) = ""
2773+
def explain(using Context) = userDefinedImplicitNotFoundMessage(explain = true)
2774+
.getOrElse("")
27622775
end MissingImplicitArgument
27632776

27642777
class CannotBeAccessed(tpe: NamedType, superAccess: Boolean)(using Context)

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class CompilationTests {
143143
compileFilesInDir("tests/neg-custom-args/feature", defaultOptions.and("-Xfatal-warnings", "-feature")),
144144
compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")),
145145
compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")),
146+
compileFilesInDir("tests/neg-custom-args/explain", defaultOptions.and("-explain")),
146147
compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
147148
compileFile("tests/neg-custom-args/i3246.scala", scala2CompatMode),
148149
compileFile("tests/neg-custom-args/overrideClass.scala", scala2CompatMode),
@@ -155,9 +156,6 @@ class CompilationTests {
155156
compileFile("tests/neg-custom-args/i1754.scala", allowDeepSubtypes),
156157
compileFile("tests/neg-custom-args/i12650.scala", allowDeepSubtypes),
157158
compileFile("tests/neg-custom-args/i9517.scala", defaultOptions.and("-Xprint-types")),
158-
compileFile("tests/neg-custom-args/i11637.scala", defaultOptions.and("-explain")),
159-
compileFile("tests/neg-custom-args/i15575.scala", defaultOptions.and("-explain")),
160-
compileFile("tests/neg-custom-args/i16601a.scala", defaultOptions.and("-explain")),
161159
compileFile("tests/neg-custom-args/interop-polytypes.scala", allowDeepSubtypes.and("-Yexplicit-nulls")),
162160
compileFile("tests/neg-custom-args/conditionalWarnings.scala", allowDeepSubtypes.and("-deprecation").and("-Xfatal-warnings")),
163161
compileFilesInDir("tests/neg-custom-args/isInstanceOf", allowDeepSubtypes and "-Xfatal-warnings"),
@@ -182,7 +180,6 @@ class CompilationTests {
182180
compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
183181
compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
184182
compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-language:experimental.captureChecking", "-Xfatal-warnings")),
185-
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
186183
compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")),
187184
compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")),
188185
compileFile("tests/neg-custom-args/jdk-9-app.scala", defaultOptions.and("-release:8")),

library/src/scala/util/boundary.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package scala.util
2+
import scala.annotation.implicitNotFound
23

34
/** A boundary that can be exited by `break` calls.
45
* `boundary` and `break` represent a unified and superior alternative for the
@@ -34,6 +35,7 @@ object boundary:
3435

3536
/** Labels are targets indicating which boundary will be exited by a `break`.
3637
*/
38+
@implicitNotFound("...A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?")
3739
final class Label[-T]
3840

3941
/** Abort current computation and instead return `value` as the value of
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/explain/hidden-type-errors/Test.scala:6:24 ------------------------
2+
6 | val x = X.doSomething("XXX") // error
3+
| ^^^^^^^^^^^^^^^^^^^^
4+
| Found: String
5+
| Required: Int
6+
|---------------------------------------------------------------------------------------------------------------------
7+
| Explanation (enabled by `-explain`)
8+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
9+
|
10+
| Tree: t12717.A.bar("XXX")
11+
| I tried to show that
12+
| String
13+
| conforms to
14+
| Int
15+
| but the comparison trace ended with `false`:
16+
|
17+
| ==> String <: Int
18+
| ==> String <: Int
19+
| <== String <: Int = false
20+
| <== String <: Int = false
21+
|
22+
| The tests were made under the empty constraint
23+
---------------------------------------------------------------------------------------------------------------------

tests/neg-custom-args/i11637.check renamed to tests/neg-custom-args/explain/i11637.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:33 ------------------------------------------------
1+
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:33 ----------------------------------------
22
11 | var h = new HKT3_1[FunctorImpl](); // error // error
33
| ^
44
| Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any
@@ -26,7 +26,7 @@
2626
|
2727
| The tests were made under the empty constraint
2828
--------------------------------------------------------------------------------------------------------------------
29-
-- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:21 ------------------------------------------------
29+
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:21 ----------------------------------------
3030
11 | var h = new HKT3_1[FunctorImpl](); // error // error
3131
| ^
3232
| Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any

tests/neg-custom-args/i15575.check renamed to tests/neg-custom-args/explain/i15575.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:3:27 -------------------------------------------------
1+
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:3:27 -----------------------------------------
22
3 | def bar[T]: Unit = foo[T & Any] // error
33
| ^
44
| Type argument T & Any does not conform to lower bound Any
@@ -18,7 +18,7 @@
1818
|
1919
| The tests were made under the empty constraint
2020
---------------------------------------------------------------------------------------------------------------------
21-
-- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:7:14 -------------------------------------------------
21+
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:7:14 -----------------------------------------
2222
7 | val _ = foo[String] // error
2323
| ^
2424
| Type argument String does not conform to lower bound CharSequence

tests/neg-custom-args/i16601a.check renamed to tests/neg-custom-args/explain/i16601a.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- [E042] Type Error: tests/neg-custom-args/i16601a.scala:1:27 ---------------------------------------------------------
1+
-- [E042] Type Error: tests/neg-custom-args/explain/i16601a.scala:1:27 -------------------------------------------------
22
1 |@main def Test: Unit = new concurrent.ExecutionContext // error
33
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
44
| ExecutionContext is a trait; it cannot be instantiated
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- [E172] Type Error: tests/neg-custom-args/explain/labelNotFound.scala:2:30 -------------------------------------------
2+
2 | scala.util.boundary.break(1) // error
3+
| ^
4+
|No given instance of type scala.util.boundary.Label[Int] was found for parameter label of method break in object boundary
5+
|---------------------------------------------------------------------------------------------------------------------
6+
| Explanation (enabled by `-explain`)
7+
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
8+
| A Label is generated from an enclosing `scala.util.boundary` call.
9+
| Maybe that boundary is missing?
10+
---------------------------------------------------------------------------------------------------------------------
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
object Test:
2+
scala.util.boundary.break(1) // error
3+
4+

tests/neg-custom-args/hidden-type-errors.check

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

0 commit comments

Comments
 (0)