Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow @implicitNotFound messages as explanations #16893

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 104 additions & 90 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2562,6 +2562,107 @@ 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 `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(explainTag) then
if explain then Some(str.drop(explainTag.length)) 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
Expand All @@ -2585,29 +2686,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) =
Expand Down Expand Up @@ -2644,77 +2722,13 @@ 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")
* trait Foo
*
* 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
Expand All @@ -2728,8 +2742,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
Expand Down Expand Up @@ -2758,7 +2771,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)
Expand Down
5 changes: 1 addition & 4 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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"),
Expand All @@ -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")),
Expand Down
19 changes: 19 additions & 0 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scala.quoted

import scala.annotation.experimental
import scala.annotation.implicitNotFound
import scala.reflect.TypeTest

/** Current Quotes in scope
Expand All @@ -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 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).
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]`
Expand Down
2 changes: 2 additions & 0 deletions library/src/scala/util/boundary.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,6 +35,7 @@ object boundary:

/** Labels are targets indicating which boundary will be exited by a `break`.
*/
@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
Expand Down
23 changes: 23 additions & 0 deletions tests/neg-custom-args/explain/hidden-type-errors.check
Original file line number Diff line number Diff line change
@@ -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
---------------------------------------------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 14 additions & 0 deletions tests/neg-custom-args/explain/i16888.check
Original file line number Diff line number Diff line change
@@ -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 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).
| A splice can be thought as a method with the following signature.
| def $[T](body: Quotes ?=> Expr[T]): T
---------------------------------------------------------------------------------------------------------------------
1 change: 1 addition & 0 deletions tests/neg-custom-args/explain/i16888.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test = summon[scala.quoted.Quotes] // error
Loading