Skip to content

Evaluating an annotation always produces the same result #8626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down
40 changes: 40 additions & 0 deletions compiler/test/dotty/tools/AnnotationsTests.scala
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/DottyTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions compiler/test/dotty/tools/compilerSupport.scala
Original file line number Diff line number Diff line change
@@ -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