Proper support of aggregated processors

This commit is contained in:
Mikhael Bogdanov
2020-11-12 13:43:46 +01:00
committed by max-kammerer
parent f382a55b17
commit 6748560184
16 changed files with 525 additions and 28 deletions
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.incapt;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.*;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
/**
* Simple processor that generates resource file that contains names of annotated elements.
*/
public class IncrementalAggregatingProcessor extends AbstractProcessor {
private Set<String> values = new TreeSet<String>();
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton("example.KotlinFilerGenerated");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
if (element instanceof TypeElement || element instanceof ExecutableElement || element instanceof VariableElement) {
values.add(element.getSimpleName().toString());
}
}
}
if (roundEnv.processingOver() && !values.isEmpty()) {
try (Writer writer = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "generated.txt").openWriter()) {
for (String value : values) {
writer.append(value).append("\n");
}
}
catch (IOException e) {
throw new RuntimeException(e);
}
values.clear();
}
return true;
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.incapt;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Set;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
/** Simple processor that generates a class for every annotated element (class, field, method). */
public class IncrementalBinaryIsolatingProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton("example.ExampleAnnotation");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) return true;
for (Element element : roundEnv.getElementsAnnotatedWith(annotations.iterator().next())) {
if (element instanceof TypeElement || element instanceof ExecutableElement || element instanceof VariableElement) {
String name = element.getSimpleName().toString();
name = name.substring(0, 1).toUpperCase() + name.substring(1) + "Generated";
System.out.println("kapt: IncrementalBinaryIsolatingProcessor " + name);
String packageName;
if (element instanceof TypeElement) {
packageName = element.getEnclosingElement().getSimpleName().toString();
}
else {
packageName = element.getEnclosingElement().getEnclosingElement().getSimpleName().toString();
}
String generatedClassName = packageName + "." + name;
try (OutputStream stream = processingEnv.getFiler().createClassFile(generatedClassName, element).openOutputStream()) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
writer.visit(Opcodes.V1_8,
ACC_PUBLIC | ACC_SUPER,
generatedClassName.replaceAll("\\.", "/"),
null,
"java/lang/Object",
null);
writer.visitAnnotation(Type.getObjectType("example/KotlinFilerGenerated").getDescriptor(), true);
writer.visitEnd();
stream.write(writer.toByteArray());
}
catch (IOException ignored) {
}
}
}
return false;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.incapt;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.Set;
/** Simple processor that generates a class for every annotated element (class, field, method). */
public class IncrementalIsolatingProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton("example.ExampleAnnotation");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) return true;
for (Element element : roundEnv.getElementsAnnotatedWith(annotations.iterator().next())) {
if (element instanceof TypeElement || element instanceof ExecutableElement || element instanceof VariableElement) {
String name = element.getSimpleName().toString();
name = name.substring(0, 1).toUpperCase() + name.substring(1) + "Generated";
String packageName;
if (element instanceof TypeElement) {
packageName = element.getEnclosingElement().getSimpleName().toString();
}
else {
packageName = element.getEnclosingElement().getEnclosingElement().getSimpleName().toString();
}
try (Writer writer = processingEnv.getFiler().createSourceFile(packageName + "." + name, element).openWriter()) {
writer.append("package ").append(packageName).append(";");
writer.append("\n@example.KotlinFilerGenerated").append("\n");
writer.append("\npublic class ").append(name).append(" {}");
}
catch (IOException ignored) {
}
}
}
return false;
}
}
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.incapt.*
import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -34,6 +35,11 @@ class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() {
)
)
private fun jdk9Options(javaHome: File): BuildOptions {
Assume.assumeTrue("JDK 9 isn't available", javaHome.isDirectory)
return defaultBuildOptions().copy(javaHome = javaHome)
}
@Test
fun testIncrementalChanges() {
val project = getProject()
@@ -80,8 +86,7 @@ class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() {
@Test
fun testIncrementalChangesWithJdk9() {
val javaHome = File(System.getProperty("jdk9Home")!!)
Assume.assumeTrue("JDK 9 isn't available", javaHome.isDirectory)
val options = defaultBuildOptions().copy(javaHome = javaHome)
val options = jdk9Options(javaHome)
val project = getProject()
project.build("clean", "build", options = options) {
@@ -111,7 +116,7 @@ class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() {
GradleVersionRequired.None
).apply {
setupWorkingDir()
val processorPath = generateProcessor("AGGREGATING")
val processorPath = generateProcessor("AGGREGATING" to IncrementalProcessor::class.java)
projectDir.resolve("app/build.gradle").appendText(
"""
@@ -202,4 +207,160 @@ class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() {
)
}
}
@Test
fun testIncrementalAggregatingChanges() {
doIncrementalAggregatingChanges()
}
@Test
fun testIncrementalAggregatingChangesWithJdk9() {
val javaHome = File(System.getProperty("jdk9Home")!!)
doIncrementalAggregatingChanges(buildOptions = jdk9Options(javaHome))
}
@Test
fun testIncrementalBinaryAggregatingChanges() {
doIncrementalAggregatingChanges(true)
}
@Test
fun testIncrementalBinaryAggregatingChangesWithJdk9() {
val javaHome = File(System.getProperty("jdk9Home")!!)
val options = jdk9Options(javaHome)
doIncrementalAggregatingChanges(true, options)
}
private fun CompiledProject.checkAggregatingResource(check: (List<String>) -> Unit) {
val aggregatingResource = "build/tmp/kapt3/classes/main/generated.txt"
assertFileExists(aggregatingResource)
val lines = fileInWorkingDir(aggregatingResource).readLines()
check(lines)
}
fun doIncrementalAggregatingChanges(isBinary: Boolean = false, buildOptions: BuildOptions = defaultBuildOptions()) {
val project = Project(
"kaptIncrementalAggregatingProcessorProject",
GradleVersionRequired.None
).apply {
setupIncrementalAptProject(
"ISOLATING" to if (isBinary) IncrementalBinaryIsolatingProcessor::class.java else IncrementalIsolatingProcessor::class.java,
"AGGREGATING" to IncrementalAggregatingProcessor::class.java
)
}
project.build("clean", "build", options = buildOptions) {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/WithAnnotation.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/noAnnotations.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/error/NonExistentClass.java").canonicalPath
), getProcessedSources(output)
)
checkAggregatingResource { lines ->
assertEquals(1, lines.size)
assertTrue(lines.contains("WithAnnotationGenerated"))
}
}
//change file without annotations
project.projectFile("noAnnotations.kt").modify { current -> "$current\nfun otherFunction() {}" }
project.build("build", options = buildOptions) {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/NoAnnotationsKt.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/error/NonExistentClass.java").canonicalPath
), getProcessedSources(output)
)
checkAggregatingResource { lines ->
assertEquals(1, lines.size)
assertTrue(lines.contains("WithAnnotationGenerated"))
}
}
//add new file with annotations
val newFile = File(project.projectDir, "src/main/java/baz/BazClass.kt")
newFile.parentFile.mkdirs()
newFile.createNewFile()
project.projectFile("BazClass.kt").modify {
"""
package baz
@example.ExampleAnnotation
class BazClass() {}
""".trimIndent()
}
project.build("build", options = buildOptions) {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/BazClass.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/error/NonExistentClass.java").canonicalPath
),
getProcessedSources(output)
)
checkAggregatingResource { lines ->
assertEquals(2, lines.size)
assertTrue(lines.contains("WithAnnotationGenerated"))
assertTrue(lines.contains("BazClassGenerated"))
}
}
//move annotation to nested class
project.projectFile("BazClass.kt").modify {
"""
package baz
class BazClass() {
@example.ExampleAnnotation
class BazNested {}
}
""".trimIndent()
}
project.build("build", options = buildOptions) {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/BazClass.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/error/NonExistentClass.java").canonicalPath
),
getProcessedSources(output)
)
checkAggregatingResource { lines ->
assertEquals(2, lines.size)
assertTrue(lines.contains("WithAnnotationGenerated"))
assertTrue(lines.contains("BazNestedGenerated"))
}
}
//change file without annotations to check that nested class is aggregated
project.projectFile("noAnnotations.kt").modify { current -> "$current\nfun otherFunction2() {}" }
project.build("build", options = buildOptions) {
assertSuccessful()
assertEquals(
setOf(
fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/NoAnnotationsKt.java").canonicalPath,
fileInWorkingDir("build/tmp/kapt3/stubs/main/error/NonExistentClass.java").canonicalPath
), getProcessedSources(output)
)
checkAggregatingResource { lines ->
assertEquals(2, lines.size)
assertTrue(lines.contains("WithAnnotationGenerated"))
assertTrue(lines.contains("BazNestedGenerated"))
}
}
}
}
@@ -201,10 +201,21 @@ fun getProcessedSources(output: String): Set<String> {
return logging.drop(indexOf).split(",").map { it.trim() }.filter { !it.isEmpty() }.toSet()
}
fun BaseGradleIT.Project.setupIncrementalAptProject(procType: String, buildFile: File = projectDir.resolve("build.gradle")) {
fun BaseGradleIT.Project.setupIncrementalAptProject(
procType: String,
buildFile: File = projectDir.resolve("build.gradle"),
procClass: Class<*> = IncrementalProcessor::class.java
) {
setupIncrementalAptProject(procType to procClass, buildFile = buildFile)
}
fun BaseGradleIT.Project.setupIncrementalAptProject(
vararg processors: Pair<String, Class<*>>,
buildFile: File = projectDir.resolve("build.gradle")
) {
setupWorkingDir()
val content = buildFile.readText()
val processorPath = generateProcessor(procType)
val processorPath = generateProcessor(*processors)
val updatedContent = content.replace(
Regex("^\\s*kapt\\s\"org\\.jetbrain.*$", RegexOption.MULTILINE),
@@ -213,20 +224,27 @@ fun BaseGradleIT.Project.setupIncrementalAptProject(procType: String, buildFile:
buildFile.writeText(updatedContent)
}
fun BaseGradleIT.Project.generateProcessor(procType: String): File {
fun BaseGradleIT.Project.generateProcessor(vararg processors: Pair<String, Class<*>>): File {
val processorPath = projectDir.resolve("incrementalProcessor.jar")
ZipOutputStream(processorPath.outputStream()).use {
val path = IncrementalProcessor::class.java.name.replace(".", "/") + ".class"
val inputStream = IncrementalProcessor::class.java.classLoader.getResourceAsStream(path)
it.putNextEntry(ZipEntry(path))
it.write(inputStream.readBytes())
it.closeEntry()
for ((_, procClass) in processors) {
val path = procClass.name.replace(".", "/") + ".class"
procClass.classLoader.getResourceAsStream(path).use { inputStream ->
it.putNextEntry(ZipEntry(path))
it.write(inputStream.readBytes())
it.closeEntry()
}
}
it.putNextEntry(ZipEntry("META-INF/gradle/incremental.annotation.processors"))
it.write("${IncrementalProcessor::class.java.name},$procType".toByteArray())
it.write(processors.joinToString("\n") { (procType, procClass) ->
"${procClass.name},$procType"
}.toByteArray())
it.closeEntry()
it.putNextEntry(ZipEntry("META-INF/services/javax.annotation.processing.Processor"))
it.write(IncrementalProcessor::class.java.name.toByteArray())
it.write(processors.joinToString("\n") { (_, procClass) ->
procClass.name
}.toByteArray())
it.closeEntry()
}
return processorPath
@@ -0,0 +1,26 @@
buildscript {
repositories {
mavenLocal()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: "java"
apply plugin: "kotlin"
apply plugin: "kotlin-kapt"
repositories {
jcenter()
mavenLocal()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:annotation-processor-example:$kotlin_version"
kapt "org.jetbrains.kotlin:annotation-processor-example:$kotlin_version"
kapt "org.ow2.asm:asm:9.0"
testImplementation 'junit:junit:4.12'
}
@@ -0,0 +1,7 @@
package bar
class noAnnotations {
val valB = "text"
fun funB() {}
}