Skip to content

Commit 9315909

Browse files
committed
Evaluating an annotation always produces the same result
No matter at which phase an annotation is evaluated, the resulting typed tree should always be the same.
1 parent 433d855 commit 9315909

File tree

3 files changed

+107
-7
lines changed

3 files changed

+107
-7
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+
}
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)