Skip to content

Commit 2dfb359

Browse files
authored
Merge pull request #8626 from dotty-staging/annot-typer
Evaluating an annotation always produces the same result
2 parents f5c8d52 + 9315909 commit 2dfb359

File tree

4 files changed

+108
-8
lines changed

4 files changed

+108
-8
lines changed

compiler/src/dotty/tools/dotc/core/Annotations.scala

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,36 @@ object Annotations {
5151
def tree(implicit ctx: Context): Tree = t
5252
}
5353

54+
/** The context to use to evaluate an annotation */
55+
private def annotCtx(using ctx: Context): Context =
56+
// We should always produce the same annotation tree, no matter when the
57+
// annotation is evaluated. Setting the phase to a pre-transformation phase
58+
// seems to be enough to ensure this (note that after erasure, `ctx.typer`
59+
// will be the Erasure typer, but that doesn't seem to affect the annotation
60+
// trees we create, so we leave it as is)
61+
ctx.withPhaseNoLater(ctx.picklerPhase)
62+
5463
abstract class LazyAnnotation extends Annotation {
5564
protected var mySym: Symbol | (Context => Symbol)
56-
override def symbol(using ctx: Context): Symbol =
65+
override def symbol(using parentCtx: Context): Symbol =
5766
assert(mySym != null)
5867
mySym match {
5968
case symFn: (Context => Symbol) @unchecked =>
6069
mySym = null
61-
mySym = symFn(ctx)
62-
case sym: Symbol if sym.defRunId != ctx.runId =>
70+
mySym = symFn(annotCtx)
71+
case sym: Symbol if sym.defRunId != parentCtx.runId =>
6372
mySym = sym.denot.current.symbol
6473
case _ =>
6574
}
6675
mySym.asInstanceOf[Symbol]
6776

6877
protected var myTree: Tree | (Context => Tree)
69-
def tree(using ctx: Context): Tree =
78+
def tree(using Context): Tree =
7079
assert(myTree != null)
7180
myTree match {
7281
case treeFn: (Context => Tree) @unchecked =>
7382
myTree = null
74-
myTree = treeFn(ctx)
83+
myTree = treeFn(annotCtx)
7584
case _ =>
7685
}
7786
myTree.asInstanceOf[Tree]
@@ -99,12 +108,12 @@ object Annotations {
99108
abstract class LazyBodyAnnotation extends BodyAnnotation {
100109
// Copy-pasted from LazyAnnotation to avoid having to turn it into a trait
101110
protected var myTree: Tree | (Context => Tree)
102-
def tree(using ctx: Context): Tree =
111+
def tree(using Context): Tree =
103112
assert(myTree != null)
104113
myTree match {
105114
case treeFn: (Context => Tree) @unchecked =>
106115
myTree = null
107-
myTree = treeFn(ctx)
116+
myTree = treeFn(annotCtx)
108117
case _ =>
109118
}
110119
myTree.asInstanceOf[Tree]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dotty.tools
2+
3+
import vulpix.TestConfiguration
4+
5+
import org.junit.Test
6+
7+
import dotc.ast.Trees._
8+
import dotc.core.Decorators._
9+
import dotc.core.Contexts._
10+
import dotc.core.Types._
11+
12+
import java.io.File
13+
import java.nio.file._
14+
15+
class AnnotationsTest:
16+
@Test def annotTreeNotErased: Unit =
17+
withJavaCompiled(
18+
VirtualJavaSource("Annot.java",
19+
"public @interface Annot { String[] values() default {}; }"),
20+
VirtualJavaSource("A.java",
21+
"@Annot(values = {}) public class A {}")) { javaOutputDir =>
22+
withContext(javaOutputDir.toString + File.pathSeparator + TestConfiguration.basicClasspath) {
23+
(using ctx: Context) =>
24+
val defn = ctx.definitions
25+
val cls = ctx.requiredClass("A")
26+
val annotCls = ctx.requiredClass("Annot")
27+
val arrayOfString = defn.ArrayType.appliedTo(List(defn.StringType))
28+
29+
ctx.atPhase(ctx.erasurePhase.next) {
30+
val annot = cls.getAnnotation(annotCls)
31+
// Even though we're forcing the annotation after erasure,
32+
// the typed trees should be unerased, so the type of
33+
// the annotation argument should be `arrayOfString` and
34+
// not a `JavaArrayType`.
35+
val arg = annot.get.argument(0).get
36+
assert(arg.tpe.isInstanceOf[AppliedType] && arg.tpe =:= arrayOfString,
37+
s"Argument $arg had type:\n${arg.tpe}\nbut expected type:\n$arrayOfString")
38+
}
39+
}
40+
}

compiler/test/dotty/tools/DottyTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ trait DottyTest extends ContextEscapeDetection {
7777
def checkTypes(source: String, typeStringss: List[List[String]])(assertion: (List[List[Type]], Context) => Unit): Unit = {
7878
val dummyName = "x_x_x"
7979
val vals = typeStringss.flatten.zipWithIndex.map{case (s, x)=> s"val ${dummyName}$x: $s = ???"}.mkString("\n")
80-
val gatheredSource = s" ${source}\n object A$dummyName {$vals}"
80+
val gatheredSource = s"${source}\nobject A$dummyName {$vals}"
8181
checkCompile("typer", gatheredSource) {
8282
(tree, context) =>
8383
implicit val ctx = context
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package dotty.tools
2+
3+
import javax.tools._
4+
import java.io.File
5+
import java.nio.file._
6+
import java.net.URI
7+
import scala.jdk.CollectionConverters._
8+
import dotty.tools.dotc._
9+
import core._
10+
import core.Contexts._
11+
import dotc.core.Comments.{ContextDoc, ContextDocstrings}
12+
13+
/** Initialize a compiler context with the given classpath and
14+
* pass it to `op`.
15+
*/
16+
def withContext[T](classpath: String)(op: Context ?=> T): T =
17+
val compiler = Compiler()
18+
val run = compiler.newRun(initCtx(classpath))
19+
run.compileUnits(Nil) // Initialize phases
20+
op(using run.runContext)
21+
22+
private def initCtx(classpath: String): Context =
23+
val ctx0 = (new ContextBase).initialCtx.fresh
24+
ctx0.setSetting(ctx0.settings.classpath, classpath)
25+
ctx0.setProperty(ContextDoc, new ContextDocstrings)
26+
ctx0
27+
28+
/** Compile `javaSources` with javac, then pass the compilation output directory
29+
* to `op`. This directory will be deleted when op returns.
30+
*/
31+
def withJavaCompiled[T](javaSources: JavaFileObject*)(op: Path => T): T =
32+
val javaOutputDir = Files.createTempDirectory("withJavaCompiled")
33+
try
34+
val javac = ToolProvider.getSystemJavaCompiler()
35+
val options = List("-d", javaOutputDir.toString)
36+
javac.getTask(null, null, null, options.asJava, null, javaSources.asJava).call();
37+
op(javaOutputDir)
38+
finally
39+
deleteDirectory(javaOutputDir.toFile)
40+
41+
/** Recursively delete a directory. */
42+
def deleteDirectory(directory: File): Unit =
43+
directory.listFiles.toList.foreach { file =>
44+
if (file.isDirectory)
45+
deleteDirectory(file)
46+
file.delete()
47+
}
48+
49+
class VirtualJavaSource(fileName: String, code: String) extends SimpleJavaFileObject(
50+
URI.create("string:///" + fileName), JavaFileObject.Kind.SOURCE):
51+
override def getCharContent(ignoreEncodingErrors: Boolean): String = code

0 commit comments

Comments
 (0)