Skip to content

Commit 800febf

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 fd18546 commit 800febf

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
@@ -164,7 +164,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
164164
UnexpectedPatternForSummonFromID,
165165
AnonymousInstanceCannotBeEmptyID,
166166
TypeSpliceInValPatternID,
167-
ModifierNotAllowedForDefinitionID
167+
ModifierNotAllowedForDefinitionID,
168+
InvalidReferenceInImplicitNotFoundAnnotationID
168169

169170
def errorNumber = ordinal - 2
170171
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2390,3 +2390,11 @@ import ast.tpd
23902390
def msg = s"Modifier `${flag.flagsString}` is not allowed for this definition"
23912391
def explain = ""
23922392
}
2393+
2394+
class InvalidReferenceInImplicitNotFoundAnnotation(typeVar: String, owner: String)(using Context)
2395+
extends ReferenceMsg(InvalidReferenceInImplicitNotFoundAnnotationID) {
2396+
def msg = em"""|Invalid reference to a type variable "${hl(typeVar)}" found in the annotation argument.
2397+
|The variable does not occur in the signature of ${hl(owner)}.
2398+
|""".stripMargin
2399+
def explain = ""
2400+
}

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
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

@@ -926,6 +928,74 @@ object RefChecks {
926928
}
927929

928930
val NoLevelInfo: RefChecks.OptLevelInfo = new OptLevelInfo()
931+
932+
/** Verify that references in the user-defined `@implicitNotFound` message are valid.
933+
* (i.e. they refer to a type variable that really occurs in the signature of the annotated symbol.)
934+
*/
935+
private object checkImplicitNotFoundAnnotation:
936+
937+
/** Warns if the class or trait has an @implicitNotFound annotation
938+
* with invalid type variable references.
939+
*/
940+
def template(sd: SymDenotation)(using Context): Unit =
941+
for
942+
annotation <- sd.getAnnotation(defn.ImplicitNotFoundAnnot)
943+
l@Literal(c: Constant) <- annotation.argument(0)
944+
do forEachTypeVariableReferenceIn(c.stringValue) { case (ref, start) =>
945+
if !sd.typeParams.exists(_.denot.name.show == ref) then
946+
reportInvalidReferences(l, ref, start, sd)
947+
}
948+
949+
/** Warns if the def has parameters with an `@implicitNotFound` annotation
950+
* with invalid type variable references.
951+
*/
952+
def defDef(sd: SymDenotation)(using Context): Unit =
953+
for
954+
paramSymss <- sd.paramSymss
955+
param <- paramSymss
956+
do
957+
for
958+
annotation <- param.getAnnotation(defn.ImplicitNotFoundAnnot)
959+
l@Literal(c: Constant) <- annotation.argument(0)
960+
do forEachTypeVariableReferenceIn(c.stringValue) { case (ref, start) =>
961+
if !sd.paramSymss.flatten.exists(_.name.show == ref) then
962+
reportInvalidReferences(l, ref, start, sd)
963+
}
964+
965+
/** Reports an invalid reference to a type variable `typeRef` that was found in `l` */
966+
private def reportInvalidReferences(
967+
l: Literal,
968+
typeRef: String,
969+
offsetInLiteral: Int,
970+
sd: SymDenotation
971+
)(using Context) =
972+
val msg = InvalidReferenceInImplicitNotFoundAnnotation(
973+
typeRef, if (sd.isConstructor) "the constructor" else sd.name.show)
974+
val span = l.span.shift(offsetInLiteral + 1) // +1 because of 0-based index
975+
val pos = ctx.source.atSpan(span.startPos)
976+
report.warning(msg, pos)
977+
978+
/** Calls the supplied function for each quoted reference to a type variable in <pre>s</pre>.
979+
* The input
980+
*
981+
* ```scala
982+
* "This is a ${T}ype re${F}erence"
983+
* // ^0 ^12 ^22
984+
* ```
985+
*
986+
* will lead to two invocations of `f`, once with `(T, 12)` and once with `(F, 22)` as argument.
987+
*
988+
* @param s The string to query for type variable references.
989+
* @param f A function to apply to every pair of (\<type variable>, \<position in string>).
990+
*/
991+
private def forEachTypeVariableReferenceIn(s: String)(f: (String, Int) => Unit) =
992+
// matches quoted references such as "${(A)}", "${(Abc)}", etc.
993+
val reference = """(?<=\$\{)[a-zA-Z]+(?=\})""".r
994+
val matches = reference.findAllIn(s)
995+
for m <- matches do f(m, matches.start)
996+
997+
end checkImplicitNotFoundAnnotation
998+
929999
}
9301000
import RefChecks._
9311001

@@ -1002,6 +1072,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10021072
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
10031073
checkNoPrivateOverrides(tree)
10041074
checkDeprecatedOvers(tree)
1075+
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
10051076
tree
10061077
}
10071078

@@ -1012,6 +1083,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10121083
if (cls.is(Trait)) tree.parents.foreach(checkParentPrefix(cls, _))
10131084
checkCompanionNameClashes(cls)
10141085
checkAllOverrides(cls)
1086+
checkImplicitNotFoundAnnotation.template(cls.classDenot)
10151087
tree
10161088
}
10171089
catch {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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+
-- [E157] 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)