Skip to content

Commit b177971

Browse files
author
Niklas Vest
committed
Fixes #4008 validates annotation arguments
Both, annotated function arguments and annotated classes / traits, are now checked for invalid type variable references and warn at declaration site.
1 parent 0535f59 commit b177971

File tree

5 files changed

+172
-3
lines changed

5 files changed

+172
-3
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
165165
AnonymousInstanceCannotBeEmptyID,
166166
TypeSpliceInValPatternID,
167167
ModifierNotAllowedForDefinitionID,
168-
CannotExtendJavaEnumID
168+
CannotExtendJavaEnumID,
169+
InvalidReferenceInImplicitNotFoundAnnotationID
169170

170171
def errorNumber = ordinal - 2
171172
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,3 +2404,11 @@ import ast.tpd
24042404
def msg = s"Modifier `${flag.flagsString}` is not allowed for this definition"
24052405
def explain = ""
24062406
}
2407+
2408+
class InvalidReferenceInImplicitNotFoundAnnotation(typeVar: String, owner: String)(using Context)
2409+
extends ReferenceMsg(InvalidReferenceInImplicitNotFoundAnnotationID) {
2410+
def msg = em"""|Invalid reference to a type variable "${hl(typeVar)}" found in the annotation argument.
2411+
|The variable does not occur in the signature of ${hl(owner)}.
2412+
|""".stripMargin
2413+
def explain = ""
2414+
}

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

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package typer
44
import transform._
55
import core._
66
import Symbols._, Types._, Contexts._, Flags._, Names._, NameOps._
7-
import StdNames._, Denotations._, SymUtils._, Phases._
7+
import StdNames._, Denotations._, SymUtils._, Phases._, SymDenotations._
88
import NameKinds.DefaultGetterName
99
import Annotations._
1010
import util.Spans._
@@ -19,9 +19,11 @@ import Decorators._
1919
import typer.ErrorReporting._
2020
import config.Feature.{warnOnMigration, migrateTo3}
2121
import reporting._
22+
import scala.util.matching.Regex._
23+
import Constants.Constant
2224

2325
object RefChecks {
24-
import tpd.{Tree, MemberDef}
26+
import tpd.{Tree, MemberDef, Literal, Template, DefDef}
2527

2628
val name: String = "refchecks"
2729

@@ -936,6 +938,74 @@ object RefChecks {
936938
}
937939

938940
val NoLevelInfo: RefChecks.OptLevelInfo = new OptLevelInfo()
941+
942+
/** Verify that references in the user-defined `@implicitNotFound` message are valid.
943+
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
944+
*/
945+
private object checkImplicitNotFoundAnnotation:
946+
947+
/** Warns if the class or trait has an @implicitNotFound annotation
948+
* with invalid type variable references.
949+
*/
950+
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+
}
958+
959+
/** Warns if the def has parameters with an `@implicitNotFound` annotation
960+
* with invalid type variable references.
961+
*/
962+
def defDef(sd: SymDenotation)(using Context): Unit =
963+
for
964+
paramSymss <- sd.paramSymss
965+
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+
}
974+
975+
/** Reports an invalid reference to a type variable `typeRef` that was found in `l` */
976+
private def reportInvalidReferences(
977+
l: Literal,
978+
typeRef: String,
979+
offsetInLiteral: Int,
980+
sd: SymDenotation
981+
)(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)
986+
report.warning(msg, pos)
987+
988+
/** Calls the supplied function for each quoted reference to a type variable in <pre>s</pre>.
989+
* The input
990+
*
991+
* ```scala
992+
* "This is a ${T}ype re${F}erence"
993+
* // ^0 ^12 ^22
994+
* ```
995+
*
996+
* will lead to two invocations of `f`, once with `(T, 12)` and once with `(F, 22)` as argument.
997+
*
998+
* @param s The string to query for type variable references.
999+
* @param f A function to apply to every pair of (\<type variable>, \<position in string>).
1000+
*/
1001+
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)
1006+
1007+
end checkImplicitNotFoundAnnotation
1008+
9391009
}
9401010
import RefChecks._
9411011

@@ -1012,6 +1082,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10121082
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
10131083
checkNoPrivateOverrides(tree)
10141084
checkDeprecatedOvers(tree)
1085+
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
10151086
tree
10161087
}
10171088

@@ -1022,6 +1093,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10221093
if (cls.is(Trait)) tree.parents.foreach(checkParentPrefix(cls, _))
10231094
checkCompanionNameClashes(cls)
10241095
checkAllOverrides(cls)
1096+
checkImplicitNotFoundAnnotation.template(cls.classDenot)
10251097
tree
10261098
}
10271099
catch {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:5:56 ---------------------------------------
2+
5 |@annotation.implicitNotFound("An implicit ShouldWarn1[${B}] is not in scope") // error
3+
| ^
4+
| Invalid reference to a type variable "B" found in the annotation argument.
5+
| The variable does not occur in the signature of ShouldWarn1.
6+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:9:56 ---------------------------------------
7+
9 |@annotation.implicitNotFound("An implicit ShouldWarn2[${A}] is not in scope") // error
8+
| ^
9+
| Invalid reference to a type variable "A" found in the annotation argument.
10+
| The variable does not occur in the signature of ShouldWarn2.
11+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:13:56 --------------------------------------
12+
13 |@annotation.implicitNotFound("An implicit ShouldWarn3[${A},${B}] is not in scope") // error
13+
| ^
14+
| Invalid reference to a type variable "A" found in the annotation argument.
15+
| The variable does not occur in the signature of ShouldWarn3.
16+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:17:56 --------------------------------------
17+
17 |@annotation.implicitNotFound("An implicit ShouldWarn4[${A},${B}] is not in scope") // error // error
18+
| ^
19+
| Invalid reference to a type variable "A" found in the annotation argument.
20+
| The variable does not occur in the signature of ShouldWarn4.
21+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:17:61 --------------------------------------
22+
17 |@annotation.implicitNotFound("An implicit ShouldWarn4[${A},${B}] is not in scope") // error // error
23+
| ^
24+
| Invalid reference to a type variable "B" found in the annotation argument.
25+
| The variable does not occur in the signature of ShouldWarn4.
26+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:21:61 --------------------------------------
27+
21 |@annotation.implicitNotFound("An implicit ShouldWarn5[${C},${Abc}] is not in scope") // error
28+
| ^
29+
| Invalid reference to a type variable "Abc" found in the annotation argument.
30+
| The variable does not occur in the signature of ShouldWarn5.
31+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:44:54 --------------------------------------
32+
44 |class C[A](using @annotation.implicitNotFound("No C[${B}] found") c: Class[A]) // error
33+
| ^
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.
36+
-- [E158] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:46:62 --------------------------------------
37+
46 |def someMethod1[A](using @annotation.implicitNotFound("No C[${B}] found") sc: C[A]) = 0 // error
38+
| ^
39+
| Invalid reference to a type variable "B" found in the annotation argument.
40+
| The variable does not occur in the signature of someMethod1.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// ===== Template annotations =====
2+
3+
4+
// class, 1TP, invalid ref
5+
@annotation.implicitNotFound("An implicit ShouldWarn1[${B}] is not in scope") // error
6+
class ShouldWarn1[A]
7+
8+
// trait, 1TP, invalid ref
9+
@annotation.implicitNotFound("An implicit ShouldWarn2[${A}] is not in scope") // error
10+
trait ShouldWarn2[B]
11+
12+
// trait, 2TP, 1 invalid ref
13+
@annotation.implicitNotFound("An implicit ShouldWarn3[${A},${B}] is not in scope") // error
14+
trait ShouldWarn3[B, C]
15+
16+
// class, 2TP, 2 invalid refs
17+
@annotation.implicitNotFound("An implicit ShouldWarn4[${A},${B}] is not in scope") // error // error
18+
class ShouldWarn4[C, D]
19+
20+
// class, 2TP, 1 invalid multi-char refs
21+
@annotation.implicitNotFound("An implicit ShouldWarn5[${C},${Abc}] is not in scope") // error
22+
class ShouldWarn5[C, D]
23+
24+
// trait, 1TP, valid ref
25+
@annotation.implicitNotFound("An implicit ShouldntWarn1[${A}] is not in scope")
26+
trait ShouldntWarn1[A]
27+
28+
// class, 2TP, only one ref but that one is valid
29+
@annotation.implicitNotFound("An implicit ShouldntWarn2[${A}, ...] is not in scope")
30+
class ShouldntWarn2[A, B]
31+
32+
// trait, 2TP, 2 valid refs
33+
@annotation.implicitNotFound("An implicit ShouldntWarn3[${A}, ${B}] is not in scope")
34+
trait ShouldntWarn3[A, B]
35+
36+
// class, 2TP, 2 valid refs
37+
@annotation.implicitNotFound("An implicit ShouldntWarn4[${Hello},${World}] is not in scope")
38+
class ShouldntWarn4[Hello, World]
39+
40+
// ===== DefDef param annotations =====
41+
42+
43+
@annotation.implicitNotFound("Hopefully you don't see this!")
44+
class C[A](using @annotation.implicitNotFound("No C[${B}] found") c: Class[A]) // error
45+
46+
def someMethod1[A](using @annotation.implicitNotFound("No C[${B}] found") sc: C[A]) = 0 // error
47+
48+
def someMethod2[A](using @annotation.implicitNotFound("No C[${A}] found") sc: C[A]) = ""

0 commit comments

Comments
 (0)