Skip to content

Commit 8fc8aaa

Browse files
committed
Fix #4986: Support implicitNotFound on parameters
Also fixes some complex cases of resolving type variables in `implicitNotFound` message for annotations put on type definitions
1 parent c900cde commit 8fc8aaa

21 files changed

+387
-126
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,8 +2425,8 @@ import ast.tpd
24252425

24262426
class InvalidReferenceInImplicitNotFoundAnnotation(typeVar: String, owner: String)(using Context)
24272427
extends ReferenceMsg(InvalidReferenceInImplicitNotFoundAnnotationID) {
2428-
def msg = em"""|Invalid reference to a type variable "${hl(typeVar)}" found in the annotation argument.
2429-
|The variable does not occur in the signature of ${hl(owner)}.
2428+
def msg = em"""|Invalid reference to a type variable ${hl(typeVar)} found in the annotation argument.
2429+
|The variable does not occur as a parameter in the scope of ${hl(owner)}.
24302430
|""".stripMargin
24312431
def explain = ""
24322432
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import config.Feature
1212
import java.util.regex.Matcher.quoteReplacement
1313
import reporting._
1414

15+
import scala.util.matching.Regex
16+
1517
object ErrorReporting {
1618

1719
import tpd._
@@ -133,11 +135,13 @@ object ErrorReporting {
133135
*/
134136
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
135137
def translate(name: String): Option[String] = {
136-
assert(paramNames.length == args.length)
137138
val idx = paramNames.indexOf(name)
138-
if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None
139+
if (idx >= 0) Some(ex"${args(idx)}") else None
139140
}
140-
"""\$\{\w*\}""".r.replaceSomeIn(raw, m => translate(m.matched.drop(2).init))
141+
142+
"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match {
143+
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse(""))
144+
})
141145
}
142146

143147
def rewriteNotice: String =

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

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ object Implicits:
8282
def strictEquality(using Context): Boolean =
8383
ctx.mode.is(Mode.StrictEquality) || Feature.enabled(nme.strictEquality)
8484

85+
def substitutableTypeSymbolsInScope(sym: Symbol)(using Context): List[Symbol] =
86+
sym.ownersIterator.flatMap { ownerSym =>
87+
ownerSym.paramSymss.flatten.filter(_.isType) ++
88+
ownerSym.typeRef.nonClassTypeMembers.map(_.symbol)
89+
}.toList
90+
8591
/** A common base class of contextual implicits and of-type implicits which
8692
* represents a set of references to implicit definitions.
8793
*/
@@ -812,9 +818,19 @@ trait Implicits:
812818
arg
813819
}
814820

815-
def missingArgMsg(arg: Tree, pt: Type, where: String)(using Context): String = {
816-
817-
def msg(shortForm: String)(headline: String = shortForm) = arg match {
821+
/** @param arg Tree representing a failed result of implicit search
822+
* @param pt Type for which an implicit value was searched
823+
* @param where Description of where the search was performed. Might be empty
824+
* @param paramSymWithMethodCallTree Symbol of the parameter for which the implicit was searched and tree of the method call that triggered the implicit search
825+
*/
826+
def missingArgMsg(
827+
arg: Tree,
828+
pt: Type,
829+
where: String,
830+
paramSymWithMethodCallTree: Option[(Symbol, Tree)] = None
831+
)(using Context): String = {
832+
833+
def formatMsg(shortForm: String)(headline: String = shortForm) = arg match {
818834
case arg: Trees.SearchFailureIdent[?] =>
819835
shortForm
820836
case _ =>
@@ -886,17 +902,11 @@ trait Implicits:
886902
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
887903
userDefinedAmbiguousImplicitMsg(alt, msg)
888904
case _ =>
889-
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
905+
formatMsg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
890906
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
891907
}
892908

893909
case _ =>
894-
val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw =>
895-
err.userDefinedErrorString(
896-
raw,
897-
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
898-
pt.widenExpr.dropDependentRefinement.argInfos))
899-
900910
def hiddenImplicitsAddendum: String =
901911

902912
def hiddenImplicitNote(s: SearchSuccess) =
@@ -928,9 +938,66 @@ trait Implicits:
928938
if normalImports.isEmpty then suggestedImports else normalImports
929939
end hiddenImplicitsAddendum
930940

931-
msg(userDefined.getOrElse(
932-
em"no implicit argument of type $pt was found${location("for")}"))() ++
933-
hiddenImplicitsAddendum
941+
/** @param rawMsg Message template with variables, e.g. "Variable A is ${A}"
942+
* @param sym Symbol of the annotated type or of the method whose parameter was annotated
943+
* @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int
944+
*/
945+
def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = {
946+
val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym)
947+
948+
err.userDefinedErrorString(
949+
rawMsg,
950+
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
951+
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
952+
)
953+
}
954+
955+
/** Extracting the message from a method parameter, e.g. in
956+
*
957+
* trait Foo
958+
*
959+
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
960+
*/
961+
def userDefinedParamMessage = paramSymWithMethodCallTree.flatMap { (sym, applTree) =>
962+
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
963+
val methodOwner = applTree.symbol.owner
964+
val methodOwnerType: Type = applTree match {
965+
case Select(qual, _) => qual.tpe
966+
case TypeApply(Select(qual, _), _) => qual.tpe
967+
case _ => methodOwner.typeRef
968+
}
969+
val substituteTypesFromMethodOwner = (_: Type).asSeenFrom(methodOwnerType, methodOwner)
970+
val substituteMethodTypeParams: Type => Type = applTree match {
971+
case TypeApply(_, targs) =>
972+
val methodTypeParams = applTree.symbol.paramSymss.flatten.filter(_.isType)
973+
val methodTypeArgs = targs.map(_.tpe)
974+
_.subst(methodTypeParams, methodTypeArgs)
975+
case _ =>
976+
identity
977+
}
978+
979+
val substituteType = substituteTypesFromMethodOwner andThen substituteMethodTypeParams
980+
formatAnnotationMessage(rawMsg, sym.owner, substituteType)
981+
}
982+
}
983+
984+
/** Extracting the message from a type, e.g. in
985+
*
986+
* @annotation.implicitNotFound("Foo is missing")
987+
* trait Foo
988+
*
989+
* def foo(implicit foo: Foo): Any = ???
990+
*/
991+
def userDefinedTypeMessage = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map { rawMsg =>
992+
val substituteType = (_: Type).asSeenFrom(pt, pt.typeSymbol)
993+
formatAnnotationMessage(rawMsg, pt.typeSymbol, substituteType)
994+
}
995+
996+
val shortMessage = userDefinedParamMessage.orElse(userDefinedTypeMessage).getOrElse(
997+
em"no implicit argument of type $pt was found${location("for")}"
998+
)
999+
1000+
formatMsg(shortMessage)() ++ hiddenImplicitsAddendum
9341001
}
9351002
}
9361003

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

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import scala.util.matching.Regex._
2323
import Constants.Constant
2424

2525
object RefChecks {
26-
import tpd.{Tree, MemberDef, Literal, Template, DefDef}
26+
import tpd.{Tree, MemberDef, NamedArg, Literal, Template, DefDef}
2727

2828
val name: String = "refchecks"
2929

@@ -943,46 +943,59 @@ object RefChecks {
943943
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
944944
*/
945945
private object checkImplicitNotFoundAnnotation:
946-
947946
/** Warns if the class or trait has an @implicitNotFound annotation
948947
* with invalid type variable references.
949948
*/
949+
def extractSubstitutableTypesNames(sd: SymDenotation)(using Context) =
950+
Implicits.substitutableTypeSymbolsInScope(sd.symbol).map(_.denot.name.show)
951+
950952
def template(sd: SymDenotation)(using Context): Unit =
951-
for
952-
annotation <- sd.getAnnotation(defn.ImplicitNotFoundAnnot)
953-
l@Literal(c: Constant) <- annotation.argument(0)
954-
do forEachTypeVariableReferenceIn(c.stringValue) { case (ref, start) =>
955-
if !sd.typeParams.exists(_.denot.name.show == ref) then
956-
reportInvalidReferences(l, ref, start, sd)
957-
}
953+
val substitutableTypesNames = extractSubstitutableTypesNames(sd)
954+
checkReferences(sd, substitutableTypesNames)
958955

959956
/** Warns if the def has parameters with an `@implicitNotFound` annotation
960957
* with invalid type variable references.
961958
*/
962959
def defDef(sd: SymDenotation)(using Context): Unit =
960+
val substitutableTypesNames = extractSubstitutableTypesNames(sd)
961+
963962
for
964963
paramSymss <- sd.paramSymss
965964
param <- paramSymss
966-
do
967-
for
968-
annotation <- param.getAnnotation(defn.ImplicitNotFoundAnnot)
969-
l@Literal(c: Constant) <- annotation.argument(0)
970-
do forEachTypeVariableReferenceIn(c.stringValue) { case (ref, start) =>
971-
if !sd.paramSymss.flatten.exists(_.name.show == ref) then
972-
reportInvalidReferences(l, ref, start, sd)
973-
}
965+
if param.isTerm
966+
do checkReferences(param.denot, substitutableTypesNames)
967+
968+
private object PositionedStringLiteralArgument:
969+
def unapply(tree: Tree): Option[(String, Span)] = tree match {
970+
case l@Literal(Constant(s: String)) => Some((s, l.span))
971+
case NamedArg(_, l@Literal(Constant(s: String))) => Some((s, l.span))
972+
case _ => None
973+
}
974+
975+
private def checkReferences(sd: SymDenotation, substitutableTypesNames: Seq[String])(using Context): Unit =
976+
for
977+
annotation <- sd.getAnnotation(defn.ImplicitNotFoundAnnot)
978+
PositionedStringLiteralArgument(msg, span) <- annotation.argument(0)
979+
do forEachTypeVariableReferenceIn(msg) { case (ref, start) =>
980+
if !substitutableTypesNames.contains(ref) then
981+
reportInvalidReference(span, ref, start, sd)
982+
}
974983

975-
/** Reports an invalid reference to a type variable `typeRef` that was found in `l` */
976-
private def reportInvalidReferences(
977-
l: Literal,
984+
/** Reports an invalid reference to a type variable `typeRef` that was found in `span` */
985+
private def reportInvalidReference(
986+
span: Span,
978987
typeRef: String,
979-
offsetInLiteral: Int,
988+
variableOffsetinArgumentLiteral: Int,
980989
sd: SymDenotation
981990
)(using Context) =
982-
val msg = InvalidReferenceInImplicitNotFoundAnnotation(
983-
typeRef, if (sd.isConstructor) "the constructor" else sd.name.show)
984-
val span = l.span.shift(offsetInLiteral + 1) // +1 because of 0-based index
985-
val pos = ctx.source.atSpan(span.startPos)
991+
val typeRefName = s"`$typeRef`"
992+
val ownerName =
993+
if sd.isType then s"type `${sd.name.show}`"
994+
else if sd.owner.isConstructor then s"the constructor of `${sd.owner.owner.name.show}`"
995+
else s"method `${sd.owner.name.show}`"
996+
val msg = InvalidReferenceInImplicitNotFoundAnnotation(typeRefName, ownerName)
997+
val startPos = span.shift(variableOffsetinArgumentLiteral + 1).startPos // +1 because of 0-based index
998+
val pos = ctx.source.atSpan(startPos)
986999
report.warning(msg, pos)
9871000

9881001
/** Calls the supplied function for each quoted reference to a type variable in <pre>s</pre>.
@@ -999,10 +1012,15 @@ object RefChecks {
9991012
* @param f A function to apply to every pair of (\<type variable>, \<position in string>).
10001013
*/
10011014
private def forEachTypeVariableReferenceIn(s: String)(f: (String, Int) => Unit) =
1002-
// matches quoted references such as "${(A)}", "${(Abc)}", etc.
1003-
val reference = """(?<=\$\{)[a-zA-Z]+(?=\})""".r
1004-
val matches = reference.findAllIn(s)
1005-
for m <- matches do f(m, matches.start)
1015+
// matches quoted references such as "${A}", "${ Abc }", etc.
1016+
val referencePattern = """\$\{\s*([^}\s]+)\s*\}""".r
1017+
val matches = referencePattern.findAllIn(s)
1018+
for reference <- matches do
1019+
val referenceOffset = matches.start
1020+
val prefixlessReference = reference.replaceFirst("""\$\{\s*""", "")
1021+
val variableOffset = referenceOffset + reference.length - prefixlessReference.length
1022+
val variableName = prefixlessReference.replaceFirst("""\s*\}""", "")
1023+
f(variableName, variableOffset)
10061024

10071025
end checkImplicitNotFoundAnnotation
10081026

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,13 @@ class Typer extends Namer
20032003
checkThisConstrCall(rhs1)
20042004
}
20052005

2006+
if sym.is(Method) && sym.owner.denot.isRefinementClass then
2007+
for annot <- sym.paramSymss.flatten.filter(_.isTerm).flatMap(_.getAnnotation(defn.ImplicitNotFoundAnnot)) do
2008+
report.warning(
2009+
i"The annotation ${defn.ImplicitNotFoundAnnot} is not allowed on parameters of methods defined inside a refinement and it will have no effect",
2010+
annot.tree.sourcePos
2011+
)
2012+
20062013
val ddef2 = assignType(cpy.DefDef(ddef)(name, tparams1, vparamss1, tpt1, rhs1), sym)
20072014

20082015
checkSignatureRepeatedParam(sym)
@@ -3108,12 +3115,20 @@ class Typer extends Namer
31083115
val propFail = propagatedFailure(args)
31093116

31103117
def issueErrors(): Tree = {
3118+
def paramSymWithMethodTree(paramName: TermName) =
3119+
if tree.symbol.exists then
3120+
val paramSyms = tree.symbol.paramSymss.flatten.map(sym => sym.name -> sym).toMap
3121+
Some((paramSyms(paramName), tree))
3122+
else
3123+
None
3124+
31113125
wtp.paramNames.lazyZip(wtp.paramInfos).lazyZip(args).foreach { (paramName, formal, arg) =>
31123126
arg.tpe match {
31133127
case failure: SearchFailureType =>
31143128
report.error(
3115-
missingArgMsg(arg, formal, implicitParamString(paramName, methodStr, tree)),
3116-
tree.srcPos.endPos)
3129+
missingArgMsg(arg, formal, implicitParamString(paramName, methodStr, tree), paramSymWithMethodTree(paramName)),
3130+
tree.srcPos.endPos
3131+
)
31173132
case _ =>
31183133
}
31193134
}
Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
11
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:5:56 ---------------------------------------
22
5 |@annotation.implicitNotFound("An implicit ShouldWarn1[${B}] is not in scope") // error
33
| ^
4-
| Invalid reference to a type variable "B" found in the annotation argument.
5-
| The variable does not occur in the signature of ShouldWarn1.
4+
| Invalid reference to a type variable `B` found in the annotation argument.
5+
| The variable does not occur as a parameter in the scope of type `ShouldWarn1`.
66
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:9:56 ---------------------------------------
77
9 |@annotation.implicitNotFound("An implicit ShouldWarn2[${A}] is not in scope") // error
88
| ^
9-
| Invalid reference to a type variable "A" found in the annotation argument.
10-
| The variable does not occur in the signature of ShouldWarn2.
9+
| Invalid reference to a type variable `A` found in the annotation argument.
10+
| The variable does not occur as a parameter in the scope of type `ShouldWarn2`.
1111
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:13:56 --------------------------------------
1212
13 |@annotation.implicitNotFound("An implicit ShouldWarn3[${A},${B}] is not in scope") // error
1313
| ^
14-
| Invalid reference to a type variable "A" found in the annotation argument.
15-
| The variable does not occur in the signature of ShouldWarn3.
14+
| Invalid reference to a type variable `A` found in the annotation argument.
15+
| The variable does not occur as a parameter in the scope of type `ShouldWarn3`.
1616
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:17:56 --------------------------------------
1717
17 |@annotation.implicitNotFound("An implicit ShouldWarn4[${A},${B}] is not in scope") // error // error
1818
| ^
19-
| Invalid reference to a type variable "A" found in the annotation argument.
20-
| The variable does not occur in the signature of ShouldWarn4.
19+
| Invalid reference to a type variable `A` found in the annotation argument.
20+
| The variable does not occur as a parameter in the scope of type `ShouldWarn4`.
2121
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:17:61 --------------------------------------
2222
17 |@annotation.implicitNotFound("An implicit ShouldWarn4[${A},${B}] is not in scope") // error // error
2323
| ^
24-
| Invalid reference to a type variable "B" found in the annotation argument.
25-
| The variable does not occur in the signature of ShouldWarn4.
24+
| Invalid reference to a type variable `B` found in the annotation argument.
25+
| The variable does not occur as a parameter in the scope of type `ShouldWarn4`.
2626
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:21:61 --------------------------------------
2727
21 |@annotation.implicitNotFound("An implicit ShouldWarn5[${C},${Abc}] is not in scope") // error
2828
| ^
29-
| Invalid reference to a type variable "Abc" found in the annotation argument.
30-
| The variable does not occur in the signature of ShouldWarn5.
29+
| Invalid reference to a type variable `Abc` found in the annotation argument.
30+
| The variable does not occur as a parameter in the scope of type `ShouldWarn5`.
3131
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:44:54 --------------------------------------
3232
44 |class C[A](using @annotation.implicitNotFound("No C[${B}] found") c: Class[A]) // error
3333
| ^
34-
| Invalid reference to a type variable "B" found in the annotation argument.
35-
| The variable does not occur in the signature of the constructor.
34+
| Invalid reference to a type variable `B` found in the annotation argument.
35+
| The variable does not occur as a parameter in the scope of the constructor of `C`.
3636
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:46:62 --------------------------------------
3737
46 |def someMethod1[A](using @annotation.implicitNotFound("No C[${B}] found") sc: C[A]) = 0 // error
3838
| ^
39-
| Invalid reference to a type variable "B" found in the annotation argument.
40-
| The variable does not occur in the signature of someMethod1.
39+
| Invalid reference to a type variable `B` found in the annotation argument.
40+
| The variable does not occur as a parameter in the scope of method `someMethod1`.

0 commit comments

Comments
 (0)