Skip to content

Commit e2e292b

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 e2e292b

File tree

5 files changed

+169
-3
lines changed

5 files changed

+169
-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: 81 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,81 @@ object RefChecks {
926928
}
927929

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

@@ -1002,6 +1079,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10021079
override def transformDefDef(tree: DefDef)(using Context): DefDef = {
10031080
checkNoPrivateOverrides(tree)
10041081
checkDeprecatedOvers(tree)
1082+
checkImplicitNotFoundAnnotation.defDef(tree.symbol.denot)
10051083
tree
10061084
}
10071085

@@ -1012,6 +1090,7 @@ class RefChecks extends MiniPhase { thisPhase =>
10121090
if (cls.is(Trait)) tree.parents.foreach(checkParentPrefix(cls, _))
10131091
checkCompanionNameClashes(cls)
10141092
checkAllOverrides(cls)
1093+
checkImplicitNotFoundAnnotation.template(cls.classDenot)
10151094
tree
10161095
}
10171096
catch {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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:21:61 --------------------------------------
17+
21 |@annotation.implicitNotFound("An implicit ShouldWarn5[${C},${Abc}] is not in scope") // error
18+
| ^
19+
| Invalid reference to a type variable "Abc" found in the annotation argument.
20+
| The variable does not occur in the signature of ShouldWarn5.
21+
-- [E157] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:44:54 --------------------------------------
22+
44 |class C[A](using @annotation.implicitNotFound("No C[${B}] found") c: Class[A]) // 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 the constructor.
26+
-- [E157] Reference Error: tests/neg-custom-args/fatal-warnings/i4008.scala:46:62 --------------------------------------
27+
46 |def someMethod1[A](using @annotation.implicitNotFound("No C[${B}] found") sc: C[A]) = 0 // error
28+
| ^
29+
| Invalid reference to a type variable "B" found in the annotation argument.
30+
| 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")
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)