diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 81a33206a624..81ff696c41f4 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -51,27 +51,36 @@ object Annotations { def tree(implicit ctx: Context): Tree = t } + /** The context to use to evaluate an annotation */ + private def annotCtx(using ctx: Context): Context = + // We should always produce the same annotation tree, no matter when the + // annotation is evaluated. Setting the phase to a pre-transformation phase + // seems to be enough to ensure this (note that after erasure, `ctx.typer` + // will be the Erasure typer, but that doesn't seem to affect the annotation + // trees we create, so we leave it as is) + ctx.withPhaseNoLater(ctx.picklerPhase) + abstract class LazyAnnotation extends Annotation { protected var mySym: Symbol | (Context => Symbol) - override def symbol(using ctx: Context): Symbol = + override def symbol(using parentCtx: Context): Symbol = assert(mySym != null) mySym match { case symFn: (Context => Symbol) @unchecked => mySym = null - mySym = symFn(ctx) - case sym: Symbol if sym.defRunId != ctx.runId => + mySym = symFn(annotCtx) + case sym: Symbol if sym.defRunId != parentCtx.runId => mySym = sym.denot.current.symbol case _ => } mySym.asInstanceOf[Symbol] protected var myTree: Tree | (Context => Tree) - def tree(using ctx: Context): Tree = + def tree(using Context): Tree = assert(myTree != null) myTree match { case treeFn: (Context => Tree) @unchecked => myTree = null - myTree = treeFn(ctx) + myTree = treeFn(annotCtx) case _ => } myTree.asInstanceOf[Tree] @@ -99,12 +108,12 @@ object Annotations { abstract class LazyBodyAnnotation extends BodyAnnotation { // Copy-pasted from LazyAnnotation to avoid having to turn it into a trait protected var myTree: Tree | (Context => Tree) - def tree(using ctx: Context): Tree = + def tree(using Context): Tree = assert(myTree != null) myTree match { case treeFn: (Context => Tree) @unchecked => myTree = null - myTree = treeFn(ctx) + myTree = treeFn(annotCtx) case _ => } myTree.asInstanceOf[Tree] diff --git a/compiler/test/dotty/tools/AnnotationsTests.scala b/compiler/test/dotty/tools/AnnotationsTests.scala new file mode 100644 index 000000000000..a138c18a06a1 --- /dev/null +++ b/compiler/test/dotty/tools/AnnotationsTests.scala @@ -0,0 +1,40 @@ +package dotty.tools + +import vulpix.TestConfiguration + +import org.junit.Test + +import dotc.ast.Trees._ +import dotc.core.Decorators._ +import dotc.core.Contexts._ +import dotc.core.Types._ + +import java.io.File +import java.nio.file._ + +class AnnotationsTest: + @Test def annotTreeNotErased: Unit = + withJavaCompiled( + VirtualJavaSource("Annot.java", + "public @interface Annot { String[] values() default {}; }"), + VirtualJavaSource("A.java", + "@Annot(values = {}) public class A {}")) { javaOutputDir => + withContext(javaOutputDir.toString + File.pathSeparator + TestConfiguration.basicClasspath) { + (using ctx: Context) => + val defn = ctx.definitions + val cls = ctx.requiredClass("A") + val annotCls = ctx.requiredClass("Annot") + val arrayOfString = defn.ArrayType.appliedTo(List(defn.StringType)) + + ctx.atPhase(ctx.erasurePhase.next) { + val annot = cls.getAnnotation(annotCls) + // Even though we're forcing the annotation after erasure, + // the typed trees should be unerased, so the type of + // the annotation argument should be `arrayOfString` and + // not a `JavaArrayType`. + val arg = annot.get.argument(0).get + assert(arg.tpe.isInstanceOf[AppliedType] && arg.tpe =:= arrayOfString, + s"Argument $arg had type:\n${arg.tpe}\nbut expected type:\n$arrayOfString") + } + } + } diff --git a/compiler/test/dotty/tools/DottyTest.scala b/compiler/test/dotty/tools/DottyTest.scala index 8fc31c2b54d2..5d5bcd15b260 100644 --- a/compiler/test/dotty/tools/DottyTest.scala +++ b/compiler/test/dotty/tools/DottyTest.scala @@ -77,7 +77,7 @@ trait DottyTest extends ContextEscapeDetection { def checkTypes(source: String, typeStringss: List[List[String]])(assertion: (List[List[Type]], Context) => Unit): Unit = { val dummyName = "x_x_x" val vals = typeStringss.flatten.zipWithIndex.map{case (s, x)=> s"val ${dummyName}$x: $s = ???"}.mkString("\n") - val gatheredSource = s" ${source}\n object A$dummyName {$vals}" + val gatheredSource = s"${source}\nobject A$dummyName {$vals}" checkCompile("typer", gatheredSource) { (tree, context) => implicit val ctx = context diff --git a/compiler/test/dotty/tools/compilerSupport.scala b/compiler/test/dotty/tools/compilerSupport.scala new file mode 100644 index 000000000000..a23918d99283 --- /dev/null +++ b/compiler/test/dotty/tools/compilerSupport.scala @@ -0,0 +1,51 @@ +package dotty.tools + +import javax.tools._ +import java.io.File +import java.nio.file._ +import java.net.URI +import scala.jdk.CollectionConverters._ +import dotty.tools.dotc._ +import core._ +import core.Contexts._ +import dotc.core.Comments.{ContextDoc, ContextDocstrings} + +/** Initialize a compiler context with the given classpath and + * pass it to `op`. + */ +def withContext[T](classpath: String)(op: Context ?=> T): T = + val compiler = Compiler() + val run = compiler.newRun(initCtx(classpath)) + run.compileUnits(Nil) // Initialize phases + op(using run.runContext) + +private def initCtx(classpath: String): Context = + val ctx0 = (new ContextBase).initialCtx.fresh + ctx0.setSetting(ctx0.settings.classpath, classpath) + ctx0.setProperty(ContextDoc, new ContextDocstrings) + ctx0 + +/** Compile `javaSources` with javac, then pass the compilation output directory + * to `op`. This directory will be deleted when op returns. + */ +def withJavaCompiled[T](javaSources: JavaFileObject*)(op: Path => T): T = + val javaOutputDir = Files.createTempDirectory("withJavaCompiled") + try + val javac = ToolProvider.getSystemJavaCompiler() + val options = List("-d", javaOutputDir.toString) + javac.getTask(null, null, null, options.asJava, null, javaSources.asJava).call(); + op(javaOutputDir) + finally + deleteDirectory(javaOutputDir.toFile) + +/** Recursively delete a directory. */ +def deleteDirectory(directory: File): Unit = + directory.listFiles.toList.foreach { file => + if (file.isDirectory) + deleteDirectory(file) + file.delete() + } + +class VirtualJavaSource(fileName: String, code: String) extends SimpleJavaFileObject( + URI.create("string:///" + fileName), JavaFileObject.Kind.SOURCE): + override def getCharContent(ignoreEncodingErrors: Boolean): String = code