From d6ae0cbdfa5e19886cadcec3c8ee02d944a36c8f Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Feb 2023 13:01:30 +0100 Subject: [PATCH 1/5] 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? --------------------------------------------------------------------------------------------------------------------- ``` --- .../dotty/tools/dotc/reporting/messages.scala | 193 ++++++++++-------- .../dotty/tools/dotc/CompilationTests.scala | 5 +- library/src/scala/util/boundary.scala | 2 + .../explain/hidden-type-errors.check | 23 +++ .../hidden-type-errors/Macro.scala | 0 .../hidden-type-errors/Test.scala | 0 .../{ => explain}/i11637.check | 4 +- .../{ => explain}/i11637.scala | 0 .../{ => explain}/i15575.check | 4 +- .../{ => explain}/i15575.scala | 0 .../{ => explain}/i16601a.check | 2 +- .../{ => explain}/i16601a.scala | 0 .../explain/labelNotFound.check | 10 + .../explain/labelNotFound.scala | 4 + .../neg-custom-args/hidden-type-errors.check | 28 --- 15 files changed, 148 insertions(+), 127 deletions(-) create mode 100644 tests/neg-custom-args/explain/hidden-type-errors.check rename tests/neg-custom-args/{ => explain}/hidden-type-errors/Macro.scala (100%) rename tests/neg-custom-args/{ => explain}/hidden-type-errors/Test.scala (100%) rename tests/neg-custom-args/{ => explain}/i11637.check (92%) rename tests/neg-custom-args/{ => explain}/i11637.scala (100%) rename tests/neg-custom-args/{ => explain}/i15575.check (87%) rename tests/neg-custom-args/{ => explain}/i15575.scala (100%) rename tests/neg-custom-args/{ => explain}/i16601a.check (89%) rename tests/neg-custom-args/{ => explain}/i16601a.scala (100%) create mode 100644 tests/neg-custom-args/explain/labelNotFound.check create mode 100644 tests/neg-custom-args/explain/labelNotFound.scala delete mode 100644 tests/neg-custom-args/hidden-type-errors.check diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index f41d34b8c17c..fb9eaed094e8 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2562,6 +2562,106 @@ class MissingImplicitArgument( case ambi: AmbiguousImplicits => withoutDisambiguation() case _ => + /** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing + * all occurrences of `${X}` where `X` is in `paramNames` with the + * corresponding shown type in `args`. + */ + def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type])(using Context): String = + def translate(name: String): Option[String] = + val idx = paramNames.indexOf(name) + if (idx >= 0) Some(i"${args(idx)}") else None + """\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match + case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse("")).nn + ) + + /** @param rawMsg Message template with variables, e.g. "Variable A is ${A}" + * @param sym Symbol of the annotated type or of the method whose parameter was annotated + * @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int + */ + def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type)(using Context): String = + val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym) + userDefinedErrorString( + rawMsg, + paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString), + args = substitutableTypesSymbols.map(_.typeRef).map(substituteType) + ) + + /** Extract a user defined error message from a symbol `sym` + * with an annotation matching the given class symbol `cls`. + */ + def userDefinedMsg(sym: Symbol, cls: Symbol)(using Context) = + for + ann <- sym.getAnnotation(cls) + msg <- ann.argumentConstantString(0) + yield msg + + def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol)(using Context): Option[String] = + for + rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot) + if Feature.migrateTo3 || sym != defn.Function1 + // Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore + yield + val substituteType = (_: Type).asSeenFrom(pt, sym) + formatAnnotationMessage(rawMsg, sym, substituteType) + + /** Extracting the message from a method parameter, e.g. in + * + * trait Foo + * + * def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ??? + */ + def userDefinedImplicitNotFoundParamMessage(using Context): Option[String] = + paramSymWithMethodCallTree.flatMap: (sym, applTree) => + userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map: rawMsg => + val fn = tpd.funPart(applTree) + val targs = tpd.typeArgss(applTree).flatten + val methodOwner = fn.symbol.owner + val methodOwnerType = tpd.qualifier(fn).tpe + val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType) + val methodTypeArgs = targs.map(_.tpe) + val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs) + formatAnnotationMessage(rawMsg, sym.owner, substituteType) + + def userDefinedImplicitNotFoundTypeMessage(using Context): Option[String] = + def recur(tp: Type): Option[String] = tp match + case tp: TypeRef => + val sym = tp.symbol + userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info)) + case tp: ClassInfo => + tp.baseClasses.iterator + .map(userDefinedImplicitNotFoundTypeMessageFor) + .find(_.isDefined).flatten + case tp: TypeProxy => + recur(tp.superType) + case tp: AndType => + recur(tp.tp1).orElse(recur(tp.tp2)) + case _ => + None + recur(pt) + + /** The implicitNotFound annotation on the parameter, or else on the type. + * implicitNotFound message strings starting with `...` are intended for + * additional explanations, not the message proper. The leading `...` is + * dropped in this case. + * @param explain The message is used for an additional explanation, not + * the message proper. + */ + def userDefinedImplicitNotFoundMessage(explain: Boolean)(using Context): Option[String] = + def filter(msg: Option[String]) = msg match + case Some(str) => + if str.startsWith("...") then + if explain then Some(str.drop(3)) else None + else if explain then None + else msg + case None => None + filter(userDefinedImplicitNotFoundParamMessage) + .orElse(filter(userDefinedImplicitNotFoundTypeMessage)) + + object AmbiguousImplicitMsg { + def unapply(search: SearchSuccess): Option[String] = + userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot) + } + def msg(using Context): String = def formatMsg(shortForm: String)(headline: String = shortForm) = arg match @@ -2585,29 +2685,6 @@ class MissingImplicitArgument( |But ${tpe.explanation}.""" case _ => headline - /** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing - * all occurrences of `${X}` where `X` is in `paramNames` with the - * corresponding shown type in `args`. - */ - def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = { - def translate(name: String): Option[String] = { - val idx = paramNames.indexOf(name) - if (idx >= 0) Some(i"${args(idx)}") else None - } - - """\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match { - case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse("")).nn - }) - } - - /** Extract a user defined error message from a symbol `sym` - * with an annotation matching the given class symbol `cls`. - */ - def userDefinedMsg(sym: Symbol, cls: Symbol) = for { - ann <- sym.getAnnotation(cls) - msg <- ann.argumentConstantString(0) - } yield msg - def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where" def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) = @@ -2644,39 +2721,6 @@ class MissingImplicitArgument( userDefinedErrorString(raw, params, args) } - /** @param rawMsg Message template with variables, e.g. "Variable A is ${A}" - * @param sym Symbol of the annotated type or of the method whose parameter was annotated - * @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int - */ - def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = { - val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym) - - userDefinedErrorString( - rawMsg, - paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString), - args = substitutableTypesSymbols.map(_.typeRef).map(substituteType) - ) - } - - /** Extracting the message from a method parameter, e.g. in - * - * trait Foo - * - * def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ??? - */ - def userDefinedImplicitNotFoundParamMessage: Option[String] = paramSymWithMethodCallTree.flatMap { (sym, applTree) => - userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg => - val fn = tpd.funPart(applTree) - val targs = tpd.typeArgss(applTree).flatten - val methodOwner = fn.symbol.owner - val methodOwnerType = tpd.qualifier(fn).tpe - val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType) - val methodTypeArgs = targs.map(_.tpe) - val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs) - formatAnnotationMessage(rawMsg, sym.owner, substituteType) - } - } - /** Extracting the message from a type, e.g. in * * @annotation.implicitNotFound("Foo is missing") @@ -2684,37 +2728,6 @@ class MissingImplicitArgument( * * def foo(implicit foo: Foo): Any = ??? */ - def userDefinedImplicitNotFoundTypeMessage: Option[String] = - def recur(tp: Type): Option[String] = tp match - case tp: TypeRef => - val sym = tp.symbol - userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info)) - case tp: ClassInfo => - tp.baseClasses.iterator - .map(userDefinedImplicitNotFoundTypeMessageFor) - .find(_.isDefined).flatten - case tp: TypeProxy => - recur(tp.superType) - case tp: AndType => - recur(tp.tp1).orElse(recur(tp.tp2)) - case _ => - None - recur(pt) - - def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol): Option[String] = - for - rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot) - if Feature.migrateTo3 || sym != defn.Function1 - // Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore - yield - val substituteType = (_: Type).asSeenFrom(pt, sym) - formatAnnotationMessage(rawMsg, sym, substituteType) - - object AmbiguousImplicitMsg { - def unapply(search: SearchSuccess): Option[String] = - userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot) - } - arg.tpe match case ambi: AmbiguousImplicits => (ambi.alt1, ambi.alt2) match @@ -2728,8 +2741,7 @@ class MissingImplicitArgument( i"""No implicit search was attempted${location("for")} |since the expected type $target is not specific enough""" case _ => - val shortMessage = userDefinedImplicitNotFoundParamMessage - .orElse(userDefinedImplicitNotFoundTypeMessage) + val shortMessage = userDefinedImplicitNotFoundMessage(explain = false) .getOrElse(defaultImplicitNotFoundMessage) formatMsg(shortMessage)() end msg @@ -2758,7 +2770,8 @@ class MissingImplicitArgument( .orElse(noChainConversionsNote(ignoredConvertibleImplicits)) .getOrElse(ctx.typer.importSuggestionAddendum(pt)) - def explain(using Context) = "" + def explain(using Context) = userDefinedImplicitNotFoundMessage(explain = true) + .getOrElse("") end MissingImplicitArgument class CannotBeAccessed(tpe: NamedType, superAccess: Boolean)(using Context) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index dee65e404da9..ed531aa404c2 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -143,6 +143,7 @@ class CompilationTests { compileFilesInDir("tests/neg-custom-args/feature", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")), + compileFilesInDir("tests/neg-custom-args/explain", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFile("tests/neg-custom-args/i3246.scala", scala2CompatMode), compileFile("tests/neg-custom-args/overrideClass.scala", scala2CompatMode), @@ -155,9 +156,6 @@ class CompilationTests { compileFile("tests/neg-custom-args/i1754.scala", allowDeepSubtypes), compileFile("tests/neg-custom-args/i12650.scala", allowDeepSubtypes), compileFile("tests/neg-custom-args/i9517.scala", defaultOptions.and("-Xprint-types")), - compileFile("tests/neg-custom-args/i11637.scala", defaultOptions.and("-explain")), - compileFile("tests/neg-custom-args/i15575.scala", defaultOptions.and("-explain")), - compileFile("tests/neg-custom-args/i16601a.scala", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/interop-polytypes.scala", allowDeepSubtypes.and("-Yexplicit-nulls")), compileFile("tests/neg-custom-args/conditionalWarnings.scala", allowDeepSubtypes.and("-deprecation").and("-Xfatal-warnings")), compileFilesInDir("tests/neg-custom-args/isInstanceOf", allowDeepSubtypes and "-Xfatal-warnings"), @@ -182,7 +180,6 @@ class CompilationTests { compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-language:experimental.captureChecking", "-Xfatal-warnings")), - compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")), compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")), compileFile("tests/neg-custom-args/jdk-9-app.scala", defaultOptions.and("-release:8")), diff --git a/library/src/scala/util/boundary.scala b/library/src/scala/util/boundary.scala index 3c6c6982c7ee..ea21446d9682 100644 --- a/library/src/scala/util/boundary.scala +++ b/library/src/scala/util/boundary.scala @@ -1,4 +1,5 @@ package scala.util +import scala.annotation.implicitNotFound /** A boundary that can be exited by `break` calls. * `boundary` and `break` represent a unified and superior alternative for the @@ -34,6 +35,7 @@ object boundary: /** Labels are targets indicating which boundary will be exited by a `break`. */ + @implicitNotFound("...A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?") final class Label[-T] /** Abort current computation and instead return `value` as the value of diff --git a/tests/neg-custom-args/explain/hidden-type-errors.check b/tests/neg-custom-args/explain/hidden-type-errors.check new file mode 100644 index 000000000000..551d1d7b16ba --- /dev/null +++ b/tests/neg-custom-args/explain/hidden-type-errors.check @@ -0,0 +1,23 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/explain/hidden-type-errors/Test.scala:6:24 ------------------------ +6 | val x = X.doSomething("XXX") // error + | ^^^^^^^^^^^^^^^^^^^^ + | Found: String + | Required: Int + |--------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | + | Tree: t12717.A.bar("XXX") + | I tried to show that + | String + | conforms to + | Int + | but the comparison trace ended with `false`: + | + | ==> String <: Int + | ==> String <: Int + | <== String <: Int = false + | <== String <: Int = false + | + | The tests were made under the empty constraint + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg-custom-args/hidden-type-errors/Macro.scala b/tests/neg-custom-args/explain/hidden-type-errors/Macro.scala similarity index 100% rename from tests/neg-custom-args/hidden-type-errors/Macro.scala rename to tests/neg-custom-args/explain/hidden-type-errors/Macro.scala diff --git a/tests/neg-custom-args/hidden-type-errors/Test.scala b/tests/neg-custom-args/explain/hidden-type-errors/Test.scala similarity index 100% rename from tests/neg-custom-args/hidden-type-errors/Test.scala rename to tests/neg-custom-args/explain/hidden-type-errors/Test.scala diff --git a/tests/neg-custom-args/i11637.check b/tests/neg-custom-args/explain/i11637.check similarity index 92% rename from tests/neg-custom-args/i11637.check rename to tests/neg-custom-args/explain/i11637.check index 0664a05f4f86..82424396a43b 100644 --- a/tests/neg-custom-args/i11637.check +++ b/tests/neg-custom-args/explain/i11637.check @@ -1,4 +1,4 @@ --- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:33 ------------------------------------------------ +-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:33 ---------------------------------------- 11 | var h = new HKT3_1[FunctorImpl](); // error // error | ^ | Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any @@ -26,7 +26,7 @@ | | The tests were made under the empty constraint -------------------------------------------------------------------------------------------------------------------- --- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:21 ------------------------------------------------ +-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:21 ---------------------------------------- 11 | var h = new HKT3_1[FunctorImpl](); // error // error | ^ | Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any diff --git a/tests/neg-custom-args/i11637.scala b/tests/neg-custom-args/explain/i11637.scala similarity index 100% rename from tests/neg-custom-args/i11637.scala rename to tests/neg-custom-args/explain/i11637.scala diff --git a/tests/neg-custom-args/i15575.check b/tests/neg-custom-args/explain/i15575.check similarity index 87% rename from tests/neg-custom-args/i15575.check rename to tests/neg-custom-args/explain/i15575.check index f69111efeb96..e254e0a5e22e 100644 --- a/tests/neg-custom-args/i15575.check +++ b/tests/neg-custom-args/explain/i15575.check @@ -1,4 +1,4 @@ --- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:3:27 ------------------------------------------------- +-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:3:27 ----------------------------------------- 3 | def bar[T]: Unit = foo[T & Any] // error | ^ | Type argument T & Any does not conform to lower bound Any @@ -18,7 +18,7 @@ | | The tests were made under the empty constraint --------------------------------------------------------------------------------------------------------------------- --- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:7:14 ------------------------------------------------- +-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:7:14 ----------------------------------------- 7 | val _ = foo[String] // error | ^ | Type argument String does not conform to lower bound CharSequence diff --git a/tests/neg-custom-args/i15575.scala b/tests/neg-custom-args/explain/i15575.scala similarity index 100% rename from tests/neg-custom-args/i15575.scala rename to tests/neg-custom-args/explain/i15575.scala diff --git a/tests/neg-custom-args/i16601a.check b/tests/neg-custom-args/explain/i16601a.check similarity index 89% rename from tests/neg-custom-args/i16601a.check rename to tests/neg-custom-args/explain/i16601a.check index 604f71993ada..63be0d2cd2b2 100644 --- a/tests/neg-custom-args/i16601a.check +++ b/tests/neg-custom-args/explain/i16601a.check @@ -1,4 +1,4 @@ --- [E042] Type Error: tests/neg-custom-args/i16601a.scala:1:27 --------------------------------------------------------- +-- [E042] Type Error: tests/neg-custom-args/explain/i16601a.scala:1:27 ------------------------------------------------- 1 |@main def Test: Unit = new concurrent.ExecutionContext // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ExecutionContext is a trait; it cannot be instantiated diff --git a/tests/neg-custom-args/i16601a.scala b/tests/neg-custom-args/explain/i16601a.scala similarity index 100% rename from tests/neg-custom-args/i16601a.scala rename to tests/neg-custom-args/explain/i16601a.scala diff --git a/tests/neg-custom-args/explain/labelNotFound.check b/tests/neg-custom-args/explain/labelNotFound.check new file mode 100644 index 000000000000..594a838aeeed --- /dev/null +++ b/tests/neg-custom-args/explain/labelNotFound.check @@ -0,0 +1,10 @@ +-- [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? + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg-custom-args/explain/labelNotFound.scala b/tests/neg-custom-args/explain/labelNotFound.scala new file mode 100644 index 000000000000..8eeaed90f331 --- /dev/null +++ b/tests/neg-custom-args/explain/labelNotFound.scala @@ -0,0 +1,4 @@ +object Test: + scala.util.boundary.break(1) // error + + diff --git a/tests/neg-custom-args/hidden-type-errors.check b/tests/neg-custom-args/hidden-type-errors.check deleted file mode 100644 index a373e409af2f..000000000000 --- a/tests/neg-custom-args/hidden-type-errors.check +++ /dev/null @@ -1,28 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/hidden-type-errors/Test.scala:6:24 -------------------------------- -6 | val x = X.doSomething("XXX") // error - | ^^^^^^^^^^^^^^^^^^^^ - | Found: String - | Required: Int - | This location contains code that was inlined from Test.scala:6 - -Explanation -=========== - -Tree: t12717.A.bar("XXX") - -I tried to show that - String -conforms to - Int -but the comparison trace ended with `false`: - - ==> String <: Int - ==> String <: Int (recurring) - ==> String <: Int (recurring) - <== String <: Int (recurring) = false - <== String <: Int (recurring) = false - <== String <: Int = false - -The tests were made under the empty constraint - -1 error found From 7de74bd2a2689af1a33fabc9bdd5e58d5f5d7668 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 28 Feb 2023 14:16:32 +0100 Subject: [PATCH 2/5] Use "explain=" to tag an -explain message in an implicitNotFound This approach mirrors the format used in `@noWarn`. There it is possible to tag encode the category and message in the string. ``` @nowarn("cat=deprecation&msg=Implementation ...") ``` The current implementation for `explain=` only allows this tag at the start of the string. Making the message either a old-style message or an message in the explain. We could extend this to support `msg=` to be able to have both a custom message and an explain. Furthermore we could insert some flags to explicitly state how to display the original message (for example: not-found=hidden, not-found=in-message, not-found=in-explain). --- compiler/src/dotty/tools/dotc/reporting/messages.scala | 9 +++++---- library/src/scala/util/boundary.scala | 2 +- tests/neg-custom-args/explain/labelNotFound.scala | 2 -- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index fb9eaed094e8..2652102eb02c 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -2640,17 +2640,18 @@ class MissingImplicitArgument( recur(pt) /** The implicitNotFound annotation on the parameter, or else on the type. - * implicitNotFound message strings starting with `...` are intended for - * additional explanations, not the message proper. The leading `...` is + * implicitNotFound message strings starting with `explain=` are intended for + * additional explanations, not the message proper. The leading `explain=` is * dropped in this case. * @param explain The message is used for an additional explanation, not * the message proper. */ def userDefinedImplicitNotFoundMessage(explain: Boolean)(using Context): Option[String] = + val explainTag = "explain=" def filter(msg: Option[String]) = msg match case Some(str) => - if str.startsWith("...") then - if explain then Some(str.drop(3)) else None + if str.startsWith(explainTag) then + if explain then Some(str.drop(explainTag.length)) else None else if explain then None else msg case None => None diff --git a/library/src/scala/util/boundary.scala b/library/src/scala/util/boundary.scala index ea21446d9682..2edd754bbb93 100644 --- a/library/src/scala/util/boundary.scala +++ b/library/src/scala/util/boundary.scala @@ -35,7 +35,7 @@ object boundary: /** Labels are targets indicating which boundary will be exited by a `break`. */ - @implicitNotFound("...A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?") + @implicitNotFound("explain=A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?") final class Label[-T] /** Abort current computation and instead return `value` as the value of diff --git a/tests/neg-custom-args/explain/labelNotFound.scala b/tests/neg-custom-args/explain/labelNotFound.scala index 8eeaed90f331..2618600702da 100644 --- a/tests/neg-custom-args/explain/labelNotFound.scala +++ b/tests/neg-custom-args/explain/labelNotFound.scala @@ -1,4 +1,2 @@ object Test: scala.util.boundary.break(1) // error - - From 821023547864dfba0662965dd1f895edf08728f4 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 28 Feb 2023 14:32:56 +0100 Subject: [PATCH 3/5] Helpful implicit not found message for `Quotes` Fixes #16888 --- library/src/scala/quoted/Quotes.scala | 19 +++++++++++++++++++ tests/neg-custom-args/explain/i16888.check | 14 ++++++++++++++ tests/neg-custom-args/explain/i16888.scala | 1 + tests/neg-macros/i6436.check | 2 ++ 4 files changed, 36 insertions(+) create mode 100644 tests/neg-custom-args/explain/i16888.check create mode 100644 tests/neg-custom-args/explain/i16888.scala diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 22ec107aeae8..05c253a76fd3 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -1,6 +1,7 @@ package scala.quoted import scala.annotation.experimental +import scala.annotation.implicitNotFound import scala.reflect.TypeTest /** Current Quotes in scope @@ -21,7 +22,25 @@ transparent inline def quotes(using q: Quotes): q.type = q * * It contains the low-level Typed AST API metaprogramming API. * This API does not have the static type guarantees that `Expr` and `Type` provide. + * `Quotes` are generated from an enclosing `${ ... }` or `scala.staging.run`. For example: + * ```scala sc:nocompile + * import scala.quoted._ + * inline def myMacro: Expr[T] = + * ${ /* (quotes: Quotes) ?=> */ myExpr } + * def myExpr(using Quotes): Expr[T] = + * '{ f(${ /* (quotes: Quotes) ?=> */ myOtherExpr }) } + * } + * def myOtherExpr(using Quotes): Expr[U] = '{ ... } + * ``` */ + +@implicitNotFound("""explain=Maybe this methods is missing a `(using Quotes)` parameter. + +Maybe that splice `$ { ... }` is missing? +Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call). +A splice can be thought as a method with the following signature. + def $[T](body: Quotes ?=> Expr[T]): T +""") trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => // Extension methods for `Expr[T]` diff --git a/tests/neg-custom-args/explain/i16888.check b/tests/neg-custom-args/explain/i16888.check new file mode 100644 index 000000000000..e184ea418a94 --- /dev/null +++ b/tests/neg-custom-args/explain/i16888.check @@ -0,0 +1,14 @@ +-- [E172] Type Error: tests/neg-custom-args/explain/i16888.scala:1:38 -------------------------------------------------- +1 |def test = summon[scala.quoted.Quotes] // error + | ^ + | No given instance of type quoted.Quotes was found for parameter x of method summon in object Predef + |--------------------------------------------------------------------------------------------------------------------- + | Explanation (enabled by `-explain`) + |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Maybe this methods is missing a `(using Quotes)` parameter. + | + | Maybe that splice `$ { ... }` is missing? + | Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call). + | A splice can be thought as a method with the following signature. + | def $[T](body: Quotes ?=> Expr[T]): T + --------------------------------------------------------------------------------------------------------------------- diff --git a/tests/neg-custom-args/explain/i16888.scala b/tests/neg-custom-args/explain/i16888.scala new file mode 100644 index 000000000000..9d3fd0f2f57e --- /dev/null +++ b/tests/neg-custom-args/explain/i16888.scala @@ -0,0 +1 @@ +def test = summon[scala.quoted.Quotes] // error diff --git a/tests/neg-macros/i6436.check b/tests/neg-macros/i6436.check index 43e93b2e64e5..d563abb5424c 100644 --- a/tests/neg-macros/i6436.check +++ b/tests/neg-macros/i6436.check @@ -2,6 +2,8 @@ 5 | case '{ StringContext(${Varargs(parts)}*) } => // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | No given instance of type scala.quoted.Quotes was found + | + | longer explanation available when compiling with `-explain` -- [E006] Not Found Error: tests/neg-macros/i6436.scala:6:34 ----------------------------------------------------------- 6 | val ps: Seq[Expr[String]] = parts // error | ^^^^^ From ff9961702c37bea80514660cc3023ad81f8a8fc6 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 7 Mar 2023 14:41:40 +0100 Subject: [PATCH 4/5] Update library/src/scala/quoted/Quotes.scala --- library/src/scala/quoted/Quotes.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 05c253a76fd3..ddd7eb7d66fe 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -34,7 +34,7 @@ transparent inline def quotes(using q: Quotes): q.type = q * ``` */ -@implicitNotFound("""explain=Maybe this methods is missing a `(using Quotes)` parameter. +@implicitNotFound("""explain=Maybe this method is missing a `(using Quotes)` parameter. Maybe that splice `$ { ... }` is missing? Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call). From 4b83f1ff392340b5829ef5ea67453ed159600241 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 7 Mar 2023 17:21:21 +0100 Subject: [PATCH 5/5] Update tests/neg-custom-args/explain/i16888.check --- tests/neg-custom-args/explain/i16888.check | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neg-custom-args/explain/i16888.check b/tests/neg-custom-args/explain/i16888.check index e184ea418a94..53103576d158 100644 --- a/tests/neg-custom-args/explain/i16888.check +++ b/tests/neg-custom-args/explain/i16888.check @@ -5,7 +5,7 @@ |--------------------------------------------------------------------------------------------------------------------- | Explanation (enabled by `-explain`) |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Maybe this methods is missing a `(using Quotes)` parameter. + | Maybe this method is missing a `(using Quotes)` parameter. | | Maybe that splice `$ { ... }` is missing? | Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call).