From 9f14daa68215517669633fb1993ca5fb634cc567 Mon Sep 17 00:00:00 2001 From: Ivan Gavrilovic Date: Sat, 2 Mar 2019 12:58:45 +0000 Subject: [PATCH] Incremental annotation processing with KAPT Add support for incremental annotation processors in KAPT. These processors conform to https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing specification. Support is provided by using javac compiler APIs and recording the source file structure. At runtime, processors are instrumented with custom Filer that is used to keep track of generated files. In order to support classpath changes, stub generation task is used to generated a list of changed FQCNs, and this is simply used by KAPT. Both worker and non-worker mode are supported. #KT-23880 --- .../daemon/common/CompilationOptions.kt | 4 +- .../kotlin/daemon/CompileServiceImpl.kt | 13 +- .../incremental/IncrementalCompilerRunner.kt | 2 +- .../IncrementalJvmCompilerRunner.kt | 29 ++- .../gradle/incapt/IncrementalProcessor.java | 55 +++++ .../jetbrains/kotlin/gradle/BaseGradleIT.kt | 4 +- .../kotlin/gradle/KaptIncrementalIT.kt | 8 +- .../KaptIncrementalWithAggregatingApt.kt | 86 +++++++ .../gradle/KaptIncrementalWithIsolatingApt.kt | 113 +++++++++ .../GradleKotlinCompilerWork.kt | 3 +- .../IncrementalCompilationEnvironment.kt | 3 +- .../kapt/Kapt3KotlinGradleSubplugin.kt | 29 ++- .../internal/kapt/KaptGenerateStubsTask.kt | 5 + .../kotlin/gradle/internal/kapt/KaptTask.kt | 30 +++ .../internal/kapt/KaptWithKotlincTask.kt | 22 +- .../internal/kapt/KaptWithoutKotlincTask.kt | 20 +- .../kotlin/gradle/internal/subpluginUtils.kt | 17 +- .../plugin/android/Android25ProjectHandler.kt | 1 + .../jetbrains/kotlin/gradle/tasks/Tasks.kt | 7 +- .../org/jetbrains/kotlin/kapt3/base/Kapt.kt | 2 +- .../kotlin/kapt3/base/KaptContext.kt | 15 +- .../kotlin/kapt3/base/KaptOptions.kt | 43 +++- .../kotlin/kapt3/base/ProcessorLoader.kt | 21 +- .../kotlin/kapt3/base/annotationProcessing.kt | 27 ++- .../base/incremental/IncrementalAptCache.kt | 78 ++++++ .../kotlin/kapt3/base/incremental/cache.kt | 157 ++++++++++++ .../base/incremental/classStructureCache.kt | 170 +++++++++++++ .../incrementalProcessorDiscovery.kt | 62 +++++ .../base/incremental/incrementalProcessors.kt | 163 +++++++++++++ .../kapt3/base/incremental/javacVisitors.kt | 182 ++++++++++++++ .../kapt3/base/javac/KaptJavaCompiler.kt | 2 + .../kapt3-base/test/JavaKaptContextTest.kt | 42 ++-- .../AggregatingIncrementalProcessorTest.kt | 75 ++++++ ...otationProcessorDependencyCollectorTest.kt | 44 ++++ .../DynamicIncrementalProcessorTest.kt | 75 ++++++ .../base/incremental/IncrementalKaptTest.kt | 101 ++++++++ .../IncrementalProcessorDiscoveryTest.kt | 105 +++++++++ .../IsolatingIncrementalProcessorTest.kt | 110 +++++++++ .../incremental/JavaClassCacheManagerTest.kt | 223 ++++++++++++++++++ .../TestComplexIncrementalAptCache.kt | 210 +++++++++++++++++ .../TestSimpleIncrementalAptCache.kt | 79 +++++++ .../kotlin/kapt3/base/incremental/fixtures.kt | 124 ++++++++++ .../testData/runner/incremental/Address.java | 4 + .../runner/incremental/Observable.java | 3 + .../testData/runner/incremental/User.java | 4 + .../incremental/complex/GenericNumber.java | 9 + .../runner/incremental/complex/MyEnum.java | 8 + .../runner/incremental/complex/MyNumber.java | 32 +++ .../incremental/complex/NumberAnnotation.java | 9 + .../incremental/complex/NumberException.java | 4 + .../incremental/complex/NumberHolder.java | 16 ++ .../incremental/complex/NumberManager.java | 16 ++ plugins/kapt3/kapt3-cli/src/KaptCliOption.kt | 27 +++ .../jetbrains/kotlin/kapt3/Kapt3Extension.kt | 2 +- .../org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt | 6 + .../AbstractKotlinKapt3IntegrationTest.kt | 6 +- 56 files changed, 2649 insertions(+), 58 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/java/org/jetbrains/kotlin/gradle/incapt/IncrementalProcessor.java create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithAggregatingApt.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithIsolatingApt.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalAptCache.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/cache.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/classStructureCache.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessorDiscovery.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessors.kt create mode 100644 plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/javacVisitors.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AggregatingIncrementalProcessorTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AnnotationProcessorDependencyCollectorTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/DynamicIncrementalProcessorTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalKaptTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalProcessorDiscoveryTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IsolatingIncrementalProcessorTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/JavaClassCacheManagerTest.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestComplexIncrementalAptCache.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestSimpleIncrementalAptCache.kt create mode 100644 plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/fixtures.kt create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/Observable.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/User.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/GenericNumber.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyEnum.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyNumber.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberAnnotation.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberException.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberHolder.java create mode 100644 plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberManager.java diff --git a/compiler/daemon/daemon-common/src/org/jetbrains/kotlin/daemon/common/CompilationOptions.kt b/compiler/daemon/daemon-common/src/org/jetbrains/kotlin/daemon/common/CompilationOptions.kt index eaf004fe7bd..66419a67145 100644 --- a/compiler/daemon/daemon-common/src/org/jetbrains/kotlin/daemon/common/CompilationOptions.kt +++ b/compiler/daemon/daemon-common/src/org/jetbrains/kotlin/daemon/common/CompilationOptions.kt @@ -64,7 +64,8 @@ class IncrementalCompilationOptions( */ val outputFiles: List, val multiModuleICSettings: MultiModuleICSettings, - val modulesInfo: IncrementalModuleInfo + val modulesInfo: IncrementalModuleInfo, + val classpathFqNamesHistory: File? = null ) : CompilationOptions(compilerMode, targetPlatform, reportCategories, reportSeverity, requestedCompilationResults) { companion object { const val serialVersionUID: Long = 0 @@ -80,6 +81,7 @@ class IncrementalCompilationOptions( "multiModuleICSettings=$multiModuleICSettings, " + "usePreciseJavaTracking=$usePreciseJavaTracking" + "outputFiles=$outputFiles" + + "classpathFqNamesHistory=$classpathFqNamesHistory" + ")" } } diff --git a/compiler/daemon/src/org/jetbrains/kotlin/daemon/CompileServiceImpl.kt b/compiler/daemon/src/org/jetbrains/kotlin/daemon/CompileServiceImpl.kt index 3c3d7fef32b..2fc8211ed0d 100644 --- a/compiler/daemon/src/org/jetbrains/kotlin/daemon/CompileServiceImpl.kt +++ b/compiler/daemon/src/org/jetbrains/kotlin/daemon/CompileServiceImpl.kt @@ -41,7 +41,10 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.metadata.K2MetadataCompiler import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.daemon.common.* -import org.jetbrains.kotlin.daemon.report.* +import org.jetbrains.kotlin.daemon.report.CompileServicesFacadeMessageCollector +import org.jetbrains.kotlin.daemon.report.DaemonMessageReporter +import org.jetbrains.kotlin.daemon.report.DaemonMessageReporterPrintStreamAdapter +import org.jetbrains.kotlin.daemon.report.getICReporter import org.jetbrains.kotlin.incremental.* import org.jetbrains.kotlin.incremental.components.ExpectActualTracker import org.jetbrains.kotlin.incremental.components.LookupTracker @@ -580,15 +583,19 @@ class CompileServiceImpl( } } + val outputFiles = incrementalCompilationOptions.outputFiles.toMutableList() + incrementalCompilationOptions.classpathFqNamesHistory?.let { outputFiles.add(it) } + val compiler = IncrementalJvmCompilerRunner( workingDir, javaSourceRoots, reporter, buildHistoryFile = incrementalCompilationOptions.multiModuleICSettings.buildHistoryFile, - outputFiles = incrementalCompilationOptions.outputFiles, + outputFiles = outputFiles, usePreciseJavaTracking = incrementalCompilationOptions.usePreciseJavaTracking, modulesApiHistory = modulesApiHistory, - kotlinSourceFilesExtensions = allKotlinExtensions + kotlinSourceFilesExtensions = allKotlinExtensions, + classpathFqNamesHistory = incrementalCompilationOptions.classpathFqNamesHistory ) return try { compiler.compile(allKotlinFiles, k2jvmArgs, compilerMessageCollector, changedFiles) diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt index 844c890787f..fff8690a757 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalCompilerRunner.kt @@ -313,7 +313,7 @@ abstract class IncrementalCompilerRunner< open fun runWithNoDirtyKotlinSources(caches: CacheManager): Boolean = false - private fun processChangesAfterBuild( + protected open fun processChangesAfterBuild( compilationMode: CompilationMode, currentBuildInfo: BuildInfo, dirtyData: DirtyData diff --git a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt index f9abe3dd809..e60f8a7a7a1 100644 --- a/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt +++ b/compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/IncrementalJvmCompilerRunner.kt @@ -47,6 +47,7 @@ import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import java.io.File +import java.io.ObjectOutputStream fun makeIncrementally( cachesDir: File, @@ -109,7 +110,8 @@ class IncrementalJvmCompilerRunner( buildHistoryFile: File, outputFiles: Collection, private val modulesApiHistory: ModulesApiHistory, - override val kotlinSourceFilesExtensions: List = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS + override val kotlinSourceFilesExtensions: List = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS, + private val classpathFqNamesHistory: File? = null ) : IncrementalCompilerRunner( workingDir, "caches-jvm", @@ -126,6 +128,8 @@ class IncrementalJvmCompilerRunner( override fun destinationDir(args: K2JVMCompilerArguments): File = args.destinationAsFile + private var dirtyClasspathChanges: Collection = emptySet() + private val psiFileFactory: PsiFileFactory by lazy { val rootDisposable = Disposer.newDisposable() val configuration = CompilerConfiguration() @@ -163,6 +167,7 @@ class IncrementalJvmCompilerRunner( } is ChangesEither.Known -> { dirtyFiles.addByDirtySymbols(classpathChanges.lookupSymbols) + dirtyClasspathChanges = classpathChanges.fqNames dirtyFiles.addByDirtyClasses(classpathChanges.fqNames) } } @@ -253,6 +258,28 @@ class IncrementalJvmCompilerRunner( } } + override fun processChangesAfterBuild(compilationMode: CompilationMode, currentBuildInfo: BuildInfo, dirtyData: DirtyData) { + super.processChangesAfterBuild(compilationMode, currentBuildInfo, dirtyData) + + classpathFqNamesHistory ?: return + classpathFqNamesHistory.mkdirs() + + val historyFiles = classpathFqNamesHistory.listFiles() + if (dirtyClasspathChanges.isEmpty() && historyFiles.isNotEmpty()) { + // Don't write an empty file. We check there is at least one file so that downstream task can mark what it has processed. + return + } + + if (historyFiles.size > 10) { + historyFiles.minBy { it.lastModified() }!!.delete() + } + val newHistoryFile = classpathFqNamesHistory.resolve(System.currentTimeMillis().toString()) + ObjectOutputStream(newHistoryFile.outputStream().buffered()).use { + val listOfNames = dirtyClasspathChanges.map { it.toString() }.toList() + it.writeObject(listOfNames) + } + } + override fun postCompilationHook(exitCode: ExitCode) {} override fun updateCaches( diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/java/org/jetbrains/kotlin/gradle/incapt/IncrementalProcessor.java b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/java/org/jetbrains/kotlin/gradle/incapt/IncrementalProcessor.java new file mode 100644 index 00000000000..4d48157a802 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/java/org/jetbrains/kotlin/gradle/incapt/IncrementalProcessor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 IncrementalProcessor extends AbstractProcessor { + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton("example.ExampleAnnotation"); + } + + @Override + public boolean process(Set 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("\npublic class ").append(name).append(" {}"); + } + catch (IOException ignored) { + } + } + } + + return false; + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt index 788b1500f2d..bf424e676e8 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BaseGradleIT.kt @@ -197,7 +197,7 @@ abstract class BaseGradleIT { val parallelTasksInProject: Boolean? = null ) - data class KaptOptions(val verbose: Boolean, val useWorkers: Boolean) + data class KaptOptions(val verbose: Boolean, val useWorkers: Boolean, val incrementalKapt: Boolean = false, val includeCompileClasspath: Boolean = true) open inner class Project( val projectName: String, @@ -619,6 +619,8 @@ abstract class BaseGradleIT { options.kaptOptions?.also { kaptOptions -> add("-Pkapt.verbose=${kaptOptions.verbose}") add("-Pkapt.use.worker.api=${kaptOptions.useWorkers}") + add("-Pkapt.incremental.apt=${kaptOptions.incrementalKapt}") + add("-Pkapt.include.compile.classpath=${kaptOptions.includeCompileClasspath}") } options.parallelTasksInProject?.let { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt index 9caf1998f6b..1191415305c 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalIT.kt @@ -6,12 +6,12 @@ import org.jetbrains.kotlin.gradle.util.getFileByName import org.jetbrains.kotlin.gradle.util.modify import org.junit.Test -class KaptIncrementalIT : BaseGradleIT() { +open class KaptIncrementalIT : BaseGradleIT() { companion object { private val EXAMPLE_ANNOTATION_REGEX = "@(field:)?example.ExampleAnnotation".toRegex() } - private fun getProject() = + open fun getProject() = Project( "kaptIncrementalCompilationProject", GradleVersionRequired.None @@ -25,13 +25,13 @@ class KaptIncrementalIT : BaseGradleIT() { @Test fun testAddNewLine() { - val project = Project("simple", directoryPrefix = "kapt2") + val project = getProject() project.build("clean", "build") { assertSuccessful() } - project.projectFile("test.kt").modify { "\n$it" } + project.projectFile("useB.kt").modify { "\n$it" } project.build("build") { assertSuccessful() assertTasksExecuted(":kaptGenerateStubsKotlin", ":compileKotlin") diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithAggregatingApt.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithAggregatingApt.kt new file mode 100644 index 00000000000..dcf1383dfe9 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithAggregatingApt.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 + +import org.jetbrains.kotlin.gradle.util.modify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class KaptIncrementalWithAggregatingApt : KaptIncrementalIT() { + + override fun getProject() = + Project( + "kaptIncrementalCompilationProject", + GradleVersionRequired.None + ).apply { + setupIncrementalAptProject("AGGREGATING") + } + + override fun defaultBuildOptions(): BuildOptions = + super.defaultBuildOptions().copy( + incremental = true, + kaptOptions = KaptOptions( + verbose = true, + useWorkers = true, + incrementalKapt = true, + includeCompileClasspath = false + ) + ) + + @Test + fun testIncrementalChanges() { + val project = getProject() + + var aptTimestamp = 0L + + project.build("clean", "build") { + assertSuccessful() + + val classpathHistory = + fileInWorkingDir("build/kotlin/kaptGenerateStubsKotlin/classpath-fq-history").listFiles().asList().single() + val stubsTimestamp = classpathHistory.name.toLong() + + aptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong() + assertTrue(stubsTimestamp < aptTimestamp) + } + + project.projectFile("useB.kt").modify { current -> "$current\nfun otherFunction() {}" } + project.build("build") { + assertSuccessful() + + val newAptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong() + assertTrue(aptTimestamp < newAptTimestamp) + + assertEquals( + setOf( + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/UtilKt.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/foo/A.java").absolutePath + ), getProcessedSources(output) + ) + } + + project.projectFile("JavaClass.java").modify { current -> + val lastBrace = current.lastIndexOf("}") + current.substring(0, lastBrace) + "private void anotherFun() {}\n }" + } + project.build("build") { + assertSuccessful() + assertEquals( + setOf( + project.projectFile("JavaClass.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/baz/UtilKt.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/foo/A.java").absolutePath + ), + getProcessedSources(output) + ) + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithIsolatingApt.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithIsolatingApt.kt new file mode 100644 index 00000000000..06e01eadb15 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KaptIncrementalWithIsolatingApt.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 + +import org.jetbrains.kotlin.gradle.incapt.IncrementalProcessor +import org.jetbrains.kotlin.gradle.util.modify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class KaptIncrementalWithIsolatingApt : KaptIncrementalIT() { + + override fun getProject() = + Project( + "kaptIncrementalCompilationProject", + GradleVersionRequired.None + ).apply { + setupIncrementalAptProject("ISOLATING") + } + + override fun defaultBuildOptions(): BuildOptions = + super.defaultBuildOptions().copy( + incremental = true, + kaptOptions = KaptOptions( + verbose = true, + useWorkers = true, + incrementalKapt = true, + includeCompileClasspath = false + ) + ) + + @Test + fun testIncrementalChanges() { + val project = getProject() + + var aptTimestamp = 0L + + project.build("clean", "build") { + assertSuccessful() + + val classpathHistory = + fileInWorkingDir("build/kotlin/kaptGenerateStubsKotlin/classpath-fq-history").listFiles().asList().single() + val stubsTimestamp = classpathHistory.name.toLong() + + aptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong() + assertTrue(stubsTimestamp < aptTimestamp) + } + + project.projectFile("useB.kt").modify { current -> "$current\nfun otherFunction() {}" } + project.build("build") { + assertSuccessful() + + val newAptTimestamp = fileInWorkingDir("build/tmp/kapt3/incApCache/main/last-build-ts.bin").readText().toLong() + assertTrue(aptTimestamp < newAptTimestamp) + + assertEquals(setOf(fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath), getProcessedSources(output)) + } + + project.projectFile("B.kt").modify { current -> + val lastBrace = current.lastIndexOf("}") + current.substring(0, lastBrace) + "fun anotherFun() {}\n }" + } + project.build("build") { + assertSuccessful() + assertEquals( + setOf( + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/B.java").absolutePath, + fileInWorkingDir("build/tmp/kapt3/stubs/main/bar/UseBKt.java").absolutePath + ), + getProcessedSources(output) + ) + } + } +} + +private const val patternApt = "Processing java sources with annotation processors:" +fun getProcessedSources(output: String): Set { + val logging = output.lines().single { it.contains(patternApt) } + val indexOf = logging.indexOf(patternApt) + patternApt.length + return logging.drop(indexOf).split(",").map { it.trim() }.toSet() +} + +fun BaseGradleIT.Project.setupIncrementalAptProject(procType: String) { + setupWorkingDir() + val buildFile = projectDir.resolve("build.gradle") + val content = buildFile.readText() + 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() + it.putNextEntry(ZipEntry("META-INF/gradle/incremental.annotation.processors")) + it.write("${IncrementalProcessor::class.java.name},$procType".toByteArray()) + it.closeEntry() + it.putNextEntry(ZipEntry("META-INF/services/javax.annotation.processing.Processor")) + it.write(IncrementalProcessor::class.java.name.toByteArray()) + it.closeEntry() + } + + val updatedContent = content.replace( + Regex("^\\s*kapt\\s\"org\\.jetbrain.*$", RegexOption.MULTILINE), + " kapt files(\"$processorPath\")" + ) + buildFile.writeText(updatedContent) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerWork.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerWork.kt index a76e3f30d6a..953f8506ad8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerWork.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerWork.kt @@ -273,7 +273,8 @@ internal class GradleKotlinCompilerWork @Inject constructor( usePreciseJavaTracking = icEnv.usePreciseJavaTracking, outputFiles = outputFiles, multiModuleICSettings = icEnv.multiModuleICSettings, - modulesInfo = incrementalModuleInfo!! + modulesInfo = incrementalModuleInfo!!, + classpathFqNamesHistory = icEnv.classpathFqNamesHistory ) log.info("Options for KOTLIN DAEMON: $compilationOptions") diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/IncrementalCompilationEnvironment.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/IncrementalCompilationEnvironment.kt index c1a9df5a0e7..525ff7e3ab1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/IncrementalCompilationEnvironment.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/compilerRunner/IncrementalCompilationEnvironment.kt @@ -15,7 +15,8 @@ internal class IncrementalCompilationEnvironment( val workingDir: File, val usePreciseJavaTracking: Boolean = false, val disableMultiModuleIC: Boolean = false, - val multiModuleICSettings: MultiModuleICSettings + val multiModuleICSettings: MultiModuleICSettings, + val classpathFqNamesHistory: File? = null ) : Serializable { companion object { const val serialVersionUID: Long = 0 diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt index 6d845dea9f2..e466a9df50f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/Kapt3KotlinGradleSubplugin.kt @@ -90,6 +90,7 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { private val USE_WORKER_API = "kapt.use.worker.api" private val INFO_AS_WARNINGS = "kapt.info.as.warnings" private val INCLUDE_COMPILE_CLASSPATH = "kapt.include.compile.classpath" + private val INCREMENTAL_APT = "kapt.incremental.apt" const val KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME = "kotlinKaptWorkerDependencies" @@ -119,6 +120,10 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { return isWorkerAPISupported() && hasProperty(USE_WORKER_API) && property(USE_WORKER_API) == "true" } + fun Project.isIncrementalKapt(): Boolean { + return hasProperty(INCREMENTAL_APT) && property(INCREMENTAL_APT) == "true" + } + fun Project.isInfoAsWarnings(): Boolean { return hasProperty(INFO_AS_WARNINGS) && property(INFO_AS_WARNINGS) == "true" } @@ -157,6 +162,8 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { private fun Kapt3SubpluginContext.getKaptIncrementalDataDir() = createAndReturnTemporaryKaptDirectory("incrementalData") + private fun Kapt3SubpluginContext.getKaptIncrementalAnnotationProcessingCache() = createAndReturnTemporaryKaptDirectory("incApCache") + private fun Kapt3SubpluginContext.createAndReturnTemporaryKaptDirectory(name: String): File { val dir = File(project.buildDir, "tmp/kapt3/$name/$sourceSetName") dir.mkdirs() @@ -227,7 +234,9 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { ) val kaptGenerateStubsTask = context.createKaptGenerateStubsTask() - val kaptTask = context.createKaptKotlinTask(useWorkerApi = project.isUseWorkerApi()) + val kaptTask = context.createKaptKotlinTask( + useWorkerApi = project.isUseWorkerApi(), + classpathHistoryDir = kaptGenerateStubsTask.getClasspathFqNamesHistoryDir()) kaptGenerateStubsTask.source(*kaptConfigurations.toTypedArray()) @@ -365,7 +374,7 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { } } - private fun Kapt3SubpluginContext.createKaptKotlinTask(useWorkerApi: Boolean): KaptTask { + private fun Kapt3SubpluginContext.createKaptKotlinTask(useWorkerApi: Boolean, classpathHistoryDir: File? = null): KaptTask { val taskClass = if (useWorkerApi) KaptWithoutKotlincTask::class.java else KaptWithKotlincTask::class.java val kaptTask = project.tasks.create(getKaptTaskName("kapt"), taskClass) @@ -379,6 +388,12 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { kaptTask.classesDir = classesOutputDir kaptTask.includeCompileClasspath = includeCompileClasspath + kaptTask.isIncremental = project.isIncrementalKapt() + if (kaptTask.isIncremental) { + kaptTask.incAptCache = getKaptIncrementalAnnotationProcessingCache() + kaptTask.classpathDirtyFqNamesHistoryDir = project.files(classpathHistoryDir) + } + kotlinCompilation?.run { output.apply { addClassesDir { project.files(classesOutputDir).builtBy(kaptTask) } @@ -403,6 +418,16 @@ class Kapt3KotlinGradleSubplugin : KotlinGradleSubplugin { } if (kaptTask is KaptWithKotlincTask) { + if (kaptTask.isIncremental) { + kaptTask.pluginOptions.addPluginArgument( + getCompilerPluginId(), + SubpluginOption("incrementalCache", kaptTask.incAptCache!!.absolutePath)) + + kaptTask.pluginOptions.addPluginArgument( + getCompilerPluginId(), + SubpluginOption("classpathFqNamesHistory", kaptTask.classpathDirtyFqNamesHistoryDir.singleFile!!.absolutePath)) + } + buildAndAddOptionsTo(kaptTask, kaptTask.pluginOptions, aptMode = "apt") } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptGenerateStubsTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptGenerateStubsTask.kt index 61beb0bc253..58144b1b20b 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptGenerateStubsTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptGenerateStubsTask.kt @@ -74,6 +74,11 @@ open class KaptGenerateStubsTask : KotlinCompile() { super.setSource(sourceRootsContainer.set(sources)) } + @Internal + override fun getClasspathFqNamesHistoryDir(): File? { + return taskBuildDirectory.resolve("classpath-fq-history") + } + private fun isSourceRootAllowed(source: File): Boolean = !destinationDir.isParentOf(source) && !stubsDir.isParentOf(source) && diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt index d185724bdbb..3ed7c9a35bc 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptTask.kt @@ -5,9 +5,11 @@ import org.gradle.api.artifacts.Configuration import org.gradle.api.file.FileCollection import org.gradle.api.internal.ConventionTask import org.gradle.api.tasks.* +import org.gradle.api.tasks.incremental.IncrementalTaskInputs import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.cacheOnlyIfEnabledForKotlin +import org.jetbrains.kotlin.gradle.tasks.clearLocalState import org.jetbrains.kotlin.gradle.tasks.isBuildCacheSupported import org.jetbrains.kotlin.gradle.utils.isJavaFile import java.io.File @@ -45,6 +47,11 @@ abstract class KaptTask : ConventionTask(), TaskWithLocalState { @get:Internal internal lateinit var kaptClasspathConfigurations: List + /** Output directory that contains caches necessary to support incremental annotation processing. */ + @get:OutputDirectory + @get:Optional + var incAptCache: File? = null + @get:OutputDirectory internal lateinit var classesDir: File @@ -60,6 +67,12 @@ abstract class KaptTask : ConventionTask(), TaskWithLocalState { @get:Input internal var includeCompileClasspath: Boolean = true + @get:InputFiles + internal var classpathDirtyFqNamesHistoryDir: FileCollection = project.files() + + @get:Input + internal var isIncremental = true + // @Internal because _abiClasspath and _nonAbiClasspath are used for actual checks @get:Internal val classpath: FileCollection @@ -142,6 +155,23 @@ abstract class KaptTask : ConventionTask(), TaskWithLocalState { } } + // TODO(gavra): Here we assume that kotlinc and javac output is available for incremental runs. We should insert some checks. + @Internal + protected fun getCompiledSources() = listOfNotNull(kotlinCompileTask.destinationDir, kotlinCompileTask.javaOutputDir) + + protected fun getChangedFiles(inputs: IncrementalTaskInputs): List { + return if (!isIncremental || !inputs.isIncremental || !getCompiledSources().all { it.exists() }) { + clearLocalState() + emptyList() + } else { + with(mutableSetOf()) { + inputs.outOfDate { this.add(it.file) } + inputs.removed { this.add(it.file) } + return@with this.toList() + } + } + } + private fun hasAnnotationProcessors(file: File): Boolean { val processorEntryPath = "META-INF/services/javax.annotation.processing.Processor" diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithKotlincTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithKotlincTask.kt index 807fbbcb94f..74db041d8f6 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithKotlincTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithKotlincTask.kt @@ -12,17 +12,22 @@ import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.incremental.IncrementalTaskInputs import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.compilerRunner.GradleCompilerEnvironment import org.jetbrains.kotlin.compilerRunner.GradleCompilerRunner +import org.jetbrains.kotlin.compilerRunner.IncrementalCompilationEnvironment import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger import org.jetbrains.kotlin.compilerRunner.OutputItemsCollectorImpl +import org.jetbrains.kotlin.gradle.incremental.ChangedFiles import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions import org.jetbrains.kotlin.gradle.logging.GradlePrintingMessageCollector import org.jetbrains.kotlin.gradle.tasks.clearLocalState import org.jetbrains.kotlin.gradle.utils.toSortedPathsArray +import org.jetbrains.kotlin.incremental.ChangedFiles +import java.io.File open class KaptWithKotlincTask : KaptTask(), CompilerArgumentAwareWithInput { @get:Internal @@ -46,17 +51,28 @@ open class KaptWithKotlincTask : KaptTask(), CompilerArgumentAwareWithInput = emptyList() + @TaskAction - fun compile() { + fun compile(inputs: IncrementalTaskInputs) { logger.debug("Running kapt annotation processing using the Kotlin compiler") checkAnnotationProcessorClasspath() - clearLocalState() + + changedFiles = getChangedFiles(inputs) val args = prepareCompilerArguments() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt index aef44800cb4..c44d2b0cb31 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/kapt/KaptWithoutKotlincTask.kt @@ -9,8 +9,10 @@ import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.incremental.IncrementalTaskInputs import org.gradle.workers.IsolationMode import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.gradle.incremental.ChangedFiles import org.jetbrains.kotlin.gradle.internal.Kapt3KotlinGradleSubplugin.Companion.KAPT_WORKER_DEPENDENCIES_CONFIGURATION_NAME import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper import org.jetbrains.kotlin.gradle.tasks.clearLocalState @@ -45,10 +47,9 @@ open class KaptWithoutKotlincTask @Inject constructor(private val workerExecutor lateinit var javacOptions: Map @TaskAction - fun compile() { + fun compile(inputs: IncrementalTaskInputs) { logger.info("Running kapt annotation processing using the Gradle Worker API") checkAnnotationProcessorClasspath() - clearLocalState() val compileClasspath = classpath.files.toMutableList() if (project.plugins.none { it is KotlinAndroidPluginWrapper }) { @@ -66,6 +67,11 @@ open class KaptWithoutKotlincTask @Inject constructor(private val workerExecutor compileClasspath, javaSourceRoots.toList(), + getChangedFiles(inputs), + getCompiledSources(), + incAptCache, + classpathDirtyFqNamesHistoryDir.singleOrNull(), + destinationDir, classesDir, stubsDir, @@ -155,6 +161,11 @@ private class KaptExecution @Inject constructor( compileClasspath, javaSourceRoots, + changedFiles, + compiledSources, + incAptCache, + classpathFqNamesHistory, + sourcesOutputDir, classesOutputDir, stubsOutputDir, @@ -186,6 +197,11 @@ private data class KaptOptionsForWorker( val compileClasspath: List, val javaSourceRoots: List, + val changedFiles: List, + val compiledSources: List, + val incAptCache: File?, + val classpathFqNamesHistory: File?, + val sourcesOutputDir: File, val classesOutputDir: File, val stubsOutputDir: File, diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/subpluginUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/subpluginUtils.kt index fbddd3c33d1..d1fd3df9d2f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/subpluginUtils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/subpluginUtils.kt @@ -43,15 +43,24 @@ fun encodePluginOptions(options: Map>): String { return Base64.getEncoder().encodeToString(os.toByteArray()) } -internal fun CompilerPluginOptions.withWrappedKaptOptions(withApClasspath: Iterable): CompilerPluginOptions { +internal fun CompilerPluginOptions.withWrappedKaptOptions( + withApClasspath: Iterable, changedFiles: List = emptyList(), compiledSourcesDir: List = emptyList() +): CompilerPluginOptions { val resultOptionsByPluginId: MutableMap> = subpluginOptionsByPluginId.toMutableMap() resultOptionsByPluginId.compute(Kapt3KotlinGradleSubplugin.KAPT_SUBPLUGIN_ID) { _, kaptOptions -> - val kaptOptionsWithClasspath = - kaptOptions.orEmpty() + withApClasspath.map { FilesSubpluginOption("apclasspath", listOf(it)) } + val changedFilesOption = FilesSubpluginOption("changedFile", changedFiles).takeIf { changedFiles.isNotEmpty() } + val compiledSourcesOption = + FilesSubpluginOption("compiledSourcesDir", compiledSourcesDir).takeIf { compiledSourcesDir.isNotEmpty() } - wrapPluginOptions(kaptOptionsWithClasspath, "configuration") + val kaptOptionsWithClasspath = + kaptOptions.orEmpty() + + withApClasspath.map { FilesSubpluginOption("apclasspath", listOf(it)) } + + changedFilesOption + + compiledSourcesOption + + wrapPluginOptions(kaptOptionsWithClasspath.filterNotNull(), "configuration") } val result = CompilerPluginOptions() diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt index 0e84e7dfe66..0117da6bfea 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/android/Android25ProjectHandler.kt @@ -79,6 +79,7 @@ class Android25ProjectHandler( .map { it.file.absolutePath } .toTypedArray() } + kotlinTask.javaOutputDir = javaTask.destinationDir } override fun getSourceProviders(variantData: BaseVariant): Iterable = diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt index 5776e242743..1df0010af5d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/tasks/Tasks.kt @@ -433,7 +433,8 @@ open class KotlinCompile : AbstractKotlinCompile(), Kotl taskBuildDirectory, usePreciseJavaTracking = usePreciseJavaTracking, disableMultiModuleIC = disableMultiModuleIC(), - multiModuleICSettings = multiModuleICSettings + multiModuleICSettings = multiModuleICSettings, + classpathFqNamesHistory = getClasspathFqNamesHistoryDir() ) } else null @@ -475,6 +476,10 @@ open class KotlinCompile : AbstractKotlinCompile(), Kotl return false } + @Optional + @Internal + internal open fun getClasspathFqNamesHistoryDir(): File? = null + // override setSource to track source directory sets and files (for generated android folders) override fun setSource(sources: Any?) { sourceRootsContainer.set(sources) diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/Kapt.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/Kapt.kt index e8efcaeb7e7..c6a5b8134d3 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/Kapt.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/Kapt.kt @@ -34,7 +34,7 @@ object Kapt { logger.info { options.logString("stand-alone mode") } - val javaSourceFiles = options.collectJavaSourceFiles() + val javaSourceFiles = options.collectJavaSourceFiles(kaptContext.cacheManager) val processorLoader = ProcessorLoader(options, logger) diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptContext.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptContext.kt index 77321a80323..43329afc6b8 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptContext.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptContext.kt @@ -13,6 +13,7 @@ import com.sun.tools.javac.util.Log import com.sun.tools.javac.util.Options import org.jetbrains.kotlin.base.kapt3.KaptFlag import org.jetbrains.kotlin.base.kapt3.KaptOptions +import org.jetbrains.kotlin.kapt3.base.incremental.JavaClassCacheManager import org.jetbrains.kotlin.kapt3.base.javac.KaptJavaCompiler import org.jetbrains.kotlin.kapt3.base.javac.KaptJavaFileManager import org.jetbrains.kotlin.kapt3.base.javac.KaptJavaLog @@ -29,6 +30,7 @@ open class KaptContext(val options: KaptOptions, val withJdk: Boolean, val logge val fileManager: KaptJavaFileManager private val javacOptions: Options val javaLog: KaptJavaLog + val cacheManager: JavaClassCacheManager? protected open fun preregisterTreeMaker(context: Context) {} @@ -76,8 +78,14 @@ open class KaptContext(val options: KaptOptions, val withJdk: Boolean, val logge put("accessInternalAPI", "true") } + val compileClasspath = if (options.changedFiles.isEmpty()) { + options.compileClasspath + } else { + options.compileClasspath + options.compiledSources + } + putJavacOption("CLASSPATH", "CLASS_PATH", - options.compileClasspath.joinToString(File.pathSeparator) { it.canonicalPath }) + compileClasspath.joinToString(File.pathSeparator) { it.canonicalPath }) @Suppress("SpellCheckingInspection") putJavacOption("PROCESSORPATH", "PROCESSOR_PATH", @@ -106,10 +114,15 @@ open class KaptContext(val options: KaptOptions, val withJdk: Boolean, val logge ClassReader.instance(context).saveParameterNames = true + cacheManager = options.incrementalCache?.let { + JavaClassCacheManager(it, options.classpathFqNamesHistory!!) + } + javaLog = compiler.log as KaptJavaLog } override fun close() { + cacheManager?.close() compiler.close() fileManager.close() } diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt index 32b7460e3bd..f0bbaef8f7d 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/KaptOptions.kt @@ -5,6 +5,8 @@ package org.jetbrains.kotlin.base.kapt3 +import org.jetbrains.kotlin.kapt3.base.incremental.JavaClassCacheManager +import org.jetbrains.kotlin.kapt3.base.incremental.SourcesToReprocess import java.io.File import java.nio.file.Files @@ -13,6 +15,11 @@ class KaptOptions( val compileClasspath: List, val javaSourceRoots: List, + val changedFiles: List, + val compiledSources: List, + val incrementalCache: File?, + val classpathFqNamesHistory: File?, + val sourcesOutputDir: File, val classesOutputDir: File, val stubsOutputDir: File, @@ -36,6 +43,11 @@ class KaptOptions( val compileClasspath: MutableList = mutableListOf() val javaSourceRoots: MutableList = mutableListOf() + val changedFiles: MutableList = mutableListOf() + val compiledSources: MutableList = mutableListOf() + var incrementalCache: File? = null + var classpathFqNamesHistory: File? = null + var sourcesOutputDir: File? = null var classesOutputDir: File? = null var stubsOutputDir: File? = null @@ -62,6 +74,7 @@ class KaptOptions( return KaptOptions( projectBaseDir, compileClasspath, javaSourceRoots, + changedFiles, compiledSources, incrementalCache, classpathFqNamesHistory, sourcesOutputDir, classesOutputDir, stubsOutputDir, incrementalDataOutputDir, processingClasspath, processors, processingOptions, javacOptions, KaptFlags.fromSet(flags), mode, detectMemoryLeaks @@ -116,12 +129,25 @@ enum class AptMode(override val stringValue: String) : KaptSelector { get() = this != APT_ONLY } -fun KaptOptions.collectJavaSourceFiles(): List { - return (javaSourceRoots + stubsOutputDir) - .sortedBy { Files.isSymbolicLink(it.toPath()) } // Get non-symbolic paths first - .flatMap { root -> root.walk().filter { it.isFile && it.extension == "java" }.toList() } - .sortedBy { Files.isSymbolicLink(it.toPath()) } // This time is for .java files - .distinctBy { it.canonicalPath } +fun KaptOptions.collectJavaSourceFiles(cacheManager: JavaClassCacheManager? = null): List { + fun allSources(): List { + return (javaSourceRoots + stubsOutputDir) + .sortedBy { Files.isSymbolicLink(it.toPath()) } // Get non-symbolic paths first + .flatMap { root -> root.walk().filter { it.isFile && it.extension == "java" }.toList() } + .sortedBy { Files.isSymbolicLink(it.toPath()) } // This time is for .java files + .distinctBy { it.canonicalPath } + } + + return if (cacheManager != null && changedFiles.isNotEmpty()) { + val toReprocess = cacheManager.invalidateAndGetDirtyFiles(changedFiles.filter { it.extension == "java" }) + + when (toReprocess) { + is SourcesToReprocess.FullRebuild -> allSources() + is SourcesToReprocess.Incremental -> toReprocess.toReprocess.filter { it.exists() } + } + } else { + allSources() + } } fun KaptOptions.logString(additionalInfo: String = "") = buildString { @@ -146,4 +172,9 @@ fun KaptOptions.logString(additionalInfo: String = "") = buildString { appendln("AP options: $processingOptions") appendln("Javac options: $javacOptions") + + appendln("[incremental apt] Changed files: $changedFiles") + appendln("[incremental apt] Compiled sources directories: ${compiledSources.joinToString()}") + appendln("[incremental apt] Cache directory for incremental compilation: $incrementalCache") + appendln("[incremental apt] Classpath fq names history dir: $classpathFqNamesHistory") } \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt index 7a5bea20c65..73228ce6a9f 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/ProcessorLoader.kt @@ -7,6 +7,9 @@ package org.jetbrains.kotlin.kapt3.base import org.jetbrains.kotlin.base.kapt3.KaptFlag import org.jetbrains.kotlin.base.kapt3.KaptOptions +import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor +import org.jetbrains.kotlin.kapt3.base.incremental.getIncrementalProcessorsFromClasspath import org.jetbrains.kotlin.kapt3.base.util.KaptLogger import org.jetbrains.kotlin.kapt3.base.util.info import java.io.Closeable @@ -17,7 +20,7 @@ import java.net.URLClassLoader import java.util.* import javax.annotation.processing.Processor -class LoadedProcessors(val processors: List, val classLoader: ClassLoader) +class LoadedProcessors(val processors: List, val classLoader: ClassLoader) open class ProcessorLoader(private val options: KaptOptions, private val logger: KaptLogger) : Closeable { private var annotationProcessingClassLoader: URLClassLoader? = null @@ -48,7 +51,21 @@ open class ProcessorLoader(private val options: KaptOptions, private val logger: logger.info { "Annotation processors: " + processors.joinToString { it::class.java.canonicalName } } } - return LoadedProcessors(processors, classLoader) + return LoadedProcessors(wrapInIncrementalProcessor(processors, classpath), classLoader) + } + + private fun wrapInIncrementalProcessor(processors: List, classpath: Iterable): List { + val processorNames = processors.map {it.javaClass.name}.toSet() + + val processorsInfo: Map = getIncrementalProcessorsFromClasspath(processorNames, classpath) + + val nonIncremental = processorNames.filter { !processorsInfo.containsKey(it) } + return if (nonIncremental.isNotEmpty()) { + logger.info("Incremental KAPT support is disabled. Processors that are not incremental: ${nonIncremental.joinToString()}.") + processors.map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL) } + } else { + processors.map { IncrementalProcessor(it, processorsInfo.getValue(it.javaClass.name)) } + } } open fun doLoadProcessors(classLoader: URLClassLoader): List { diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/annotationProcessing.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/annotationProcessing.kt index b44efe19fcd..5562e79e95f 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/annotationProcessing.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/annotationProcessing.kt @@ -5,13 +5,16 @@ package org.jetbrains.kotlin.kapt3.base -import com.sun.tools.javac.comp.CompileStates.* +import com.sun.source.util.Trees +import com.sun.tools.javac.comp.CompileStates.CompileState import com.sun.tools.javac.main.JavaCompiler import com.sun.tools.javac.processing.AnnotationProcessingError import com.sun.tools.javac.processing.JavacFiler import com.sun.tools.javac.processing.JavacProcessingEnvironment import com.sun.tools.javac.tree.JCTree import org.jetbrains.kotlin.base.kapt3.KaptFlag +import org.jetbrains.kotlin.base.kapt3.collectJavaSourceFiles +import org.jetbrains.kotlin.kapt3.base.incremental.* import org.jetbrains.kotlin.kapt3.base.util.KaptBaseError import org.jetbrains.kotlin.kapt3.base.util.isJava9OrLater import org.jetbrains.kotlin.kapt3.base.util.measureTimeMillisWithResult @@ -27,10 +30,11 @@ import com.sun.tools.javac.util.List as JavacList fun KaptContext.doAnnotationProcessing( javaSourceFiles: List, - processors: List, + processors: List, additionalSources: JavacList = JavacList.nil() ) { val processingEnvironment = JavacProcessingEnvironment.instance(context) + val wrappedProcessors = processors.map { ProcessorWrapper(it) } val compilerAfterAP: JavaCompiler @@ -42,25 +46,42 @@ fun KaptContext.doAnnotationProcessing( compiler.initProcessAnnotations(wrappedProcessors) } + if (logger.isVerbose) { + logger.info("Processing java sources with annotation processors: ${javaSourceFiles.joinToString()}") + } val parsedJavaFiles = parseJavaFiles(javaSourceFiles) + val listener = cacheManager?.let { + if (processors.any { it.kind == DeclaredProcType.NON_INCREMENTAL}) return@let null + + val recordTypesListener = MentionedTypesTaskListener(cacheManager.javaCache, Trees.instance(processingEnvironment)) + compiler.getTaskListeners().add(recordTypesListener) + recordTypesListener + } + compilerAfterAP = try { javaLog.interceptorData.files = parsedJavaFiles.map { it.sourceFile to it }.toMap() val analyzedFiles = compiler.stopIfErrorOccurred( CompileState.PARSE, compiler.enterTrees(parsedJavaFiles + additionalSources) ) + listener?.let { compiler.getTaskListeners().remove(it) } + + val l = System.currentTimeMillis() if (isJava9OrLater()) { val processAnnotationsMethod = compiler.javaClass.getMethod("processAnnotations", JavacList::class.java) processAnnotationsMethod.invoke(compiler, analyzedFiles) compiler } else { - compiler.processAnnotations(analyzedFiles) + val processAnnotations = compiler.processAnnotations(analyzedFiles) + processAnnotations } } catch (e: AnnotationProcessingError) { throw KaptBaseError(KaptBaseError.Kind.EXCEPTION, e.cause ?: e) } + cacheManager?.updateCache(processors) + val log = compilerAfterAP.log val filer = processingEnvironment.filer as JavacFiler diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalAptCache.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalAptCache.kt new file mode 100644 index 00000000000..2a4dea1795c --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalAptCache.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import java.io.File +import java.io.Serializable + +class IncrementalAptCache : Serializable { + + private val aggregatingGenerated: MutableSet = mutableSetOf() + private val isolatingMapping: MutableMap = mutableMapOf() + // Annotations claimed by aggregating annotation processors + private val aggregatingClaimedAnnotations: MutableSet = mutableSetOf() + + var isIncremental = true + private set + + fun updateCache(processors: List): Boolean { + val aggregating = mutableListOf() + val isolating = mutableListOf() + val nonIncremental = mutableListOf() + processors.forEach { + when (it.getRuntimeType()) { + RuntimeProcType.AGGREGATING -> aggregating.add(it) + RuntimeProcType.ISOLATING -> isolating.add(it) + RuntimeProcType.NON_INCREMENTAL -> nonIncremental.add(it) + } + } + + if (nonIncremental.isNotEmpty()) { + invalidateCache() + return false + } + + aggregatingGenerated.clear() + aggregatingGenerated.addAll(aggregating.flatMap { it.getGeneratedToSources().keys }) + + aggregatingClaimedAnnotations.clear() + aggregatingClaimedAnnotations.addAll(aggregating.flatMap { it.supportedAnnotationTypes }) + + for (isolatingProcessor in isolating) { + isolatingProcessor.getGeneratedToSources().forEach { + isolatingMapping[it.key] = it.value!! + } + } + return true + } + + fun invalidateAggregatingAndGetAnnotations(): Set { + aggregatingGenerated.forEach { it.delete() } + aggregatingGenerated.clear() + + return aggregatingClaimedAnnotations + } + + fun invalidateIsolatingGenerated(fromSources: Set) { + var changedSources = fromSources.toSet() + + // We need to do it in a loop because mapping could be: [AGenerated.java -> A.java, AGeneratedGenerated.java -> AGenerated.java] + while(changedSources.isNotEmpty()) { + val generated = isolatingMapping.filter { changedSources.contains(it.value) }.keys + generated.forEach { + it.delete() + isolatingMapping.remove(it) + } + changedSources = generated + } + } + + private fun invalidateCache() { + isIncremental = false + aggregatingGenerated.clear() + isolatingMapping.clear() + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/cache.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/cache.kt new file mode 100644 index 00000000000..41d0e18e5a0 --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/cache.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import java.io.* + +// TODO(gavra): switch away from Java serialization +class JavaClassCacheManager(private val file: File, private val classpathFqNamesHistory: File) : Closeable { + + internal val javaCacheFile = file.resolve("java-cache.bin") + internal val javaCache = maybeGetJavaCacheFromFile() + + internal val aptCacheFile = file.resolve("apt-cache.bin") + internal val aptCache = maybeGetAptCacheFromFile() + + internal val lastBuildTimestamp = file.resolve("last-build-ts.bin") + + private var closed = false + + private fun getDirtyFqNamesFromClasspath(): ClasspathChanged { + if (!lastBuildTimestamp.exists()) return ClasspathChanged.FullRebuild + + val lastTimestamp = lastBuildTimestamp.readText() + + val (before, after) = classpathFqNamesHistory.listFiles().partition { it.name < lastTimestamp } + + if (before.isEmpty()) { + return ClasspathChanged.FullRebuild + } + + val dirtyFqNames = mutableSetOf() + after.forEach{ file -> + ObjectInputStream(file.inputStream().buffered()).use { + @Suppress("UNCHECKED_CAST") + dirtyFqNames.addAll(it.readObject() as Collection) + } + } + + return if (dirtyFqNames.isNotEmpty()) { + // TODO(gavra): We need to handle constants from classpath that might change between runs being incremental. One solution + // would be to fetch the changed symbols alongside changed fqNames, and to check if the symbol is a constant using ASM. + ClasspathChanged.FullRebuild + } else { + ClasspathChanged.Incremental(dirtyFqNames) + } + } + + fun updateCache(processors: List) { + if (!aptCache.updateCache(processors)) { + javaCache.invalidateAll() + } + } + + /** + * From set of changed sources, get list of files to recompile using structural information and dependency information from + * annotation processing. + */ + fun invalidateAndGetDirtyFiles(changedSources: Collection): SourcesToReprocess { + if (!aptCache.isIncremental) { + return SourcesToReprocess.FullRebuild + } + + val dirtyFqNamesFromClasspath = getDirtyFqNamesFromClasspath() + return when (dirtyFqNamesFromClasspath) { + is ClasspathChanged.FullRebuild -> SourcesToReprocess.FullRebuild + is ClasspathChanged.Incremental -> { + val changes = Changes(changedSources, dirtyFqNamesFromClasspath.dirtyFqNames) + val filesToReprocess = javaCache.invalidateEntriesForChangedFiles(changes) + + when (filesToReprocess) { + is SourcesToReprocess.FullRebuild -> SourcesToReprocess.FullRebuild + is SourcesToReprocess.Incremental -> { + val toReprocess = filesToReprocess.toReprocess.toMutableSet() + + aptCache.invalidateIsolatingGenerated(toReprocess) + if (!toReprocess.isEmpty()) { + // only if there are some files to reprocess we should invalidate the aggregating ones + toReprocess.addAll( + javaCache.invalidateEntriesAnnotatedWith(aptCache.invalidateAggregatingAndGetAnnotations()) + ) + } + + SourcesToReprocess.Incremental(toReprocess.toList()) + } + } + } + } + } + + private fun maybeGetAptCacheFromFile(): IncrementalAptCache{ + + return if (aptCacheFile.exists()) { + try { + ObjectInputStream(BufferedInputStream(aptCacheFile.inputStream())).use { + it.readObject() as IncrementalAptCache + } + } catch (e: Throwable) { + // cache corrupt + IncrementalAptCache() + } + } else { + IncrementalAptCache() + } + } + + private fun maybeGetJavaCacheFromFile(): JavaClassCache { + return if (javaCacheFile.exists()) { + try { + ObjectInputStream(BufferedInputStream(javaCacheFile.inputStream())).use { + it.readObject() as JavaClassCache + } + } catch (e: Throwable) { + JavaClassCache() + } + } else { + JavaClassCache() + } + } + + override fun close() { + if (closed) return + + with(javaCacheFile) { + delete() + parentFile.mkdirs() + ObjectOutputStream(BufferedOutputStream(outputStream())).use { + it.writeObject(javaCache) + } + } + + with(aptCacheFile) { + delete() + parentFile.mkdirs() + ObjectOutputStream(BufferedOutputStream(outputStream())).use { + it.writeObject(aptCache) + } + } + + with(lastBuildTimestamp) { + writeText(System.currentTimeMillis().toString()) + } + closed = true + } +} + +sealed class SourcesToReprocess { + class Incremental(val toReprocess: List): SourcesToReprocess() + object FullRebuild : SourcesToReprocess() +} + +sealed class ClasspathChanged { + class Incremental(val dirtyFqNames: Set) : ClasspathChanged() + object FullRebuild : ClasspathChanged() +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/classStructureCache.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/classStructureCache.kt new file mode 100644 index 00000000000..6a15ba59dab --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/classStructureCache.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import com.sun.tools.javac.processing.JavacProcessingEnvironment +import java.io.File +import java.io.Serializable +import java.net.URI + +class JavaClassCache() : Serializable { + private var sourceCache = mutableMapOf() + + /** Map from types to files they are mentioned in. */ + @Transient + private var dependencyCache = mutableMapOf>() + @Transient + private var nonTransitiveCache = mutableMapOf>() + + fun addSourceStructure(sourceStructure: SourceFileStructure) { + sourceCache[sourceStructure.sourceFile] = sourceStructure + } + + private fun readObject(input: java.io.ObjectInputStream) { + @Suppress("UNCHECKED_CAST") + sourceCache = input.readObject() as MutableMap + + dependencyCache = HashMap(sourceCache.size * 4) + for (sourceInfo in sourceCache.values) { + for (mentionedType in sourceInfo.getMentionedTypes()) { + val dependants = dependencyCache[mentionedType] ?: mutableSetOf() + dependants.add(sourceInfo.sourceFile) + dependencyCache[mentionedType] = dependants + } + } + nonTransitiveCache = HashMap(sourceCache.size * 2) + for (sourceInfo in sourceCache.values) { + for (privateType in sourceInfo.getPrivateTypes()) { + val dependants = nonTransitiveCache[privateType] ?: mutableSetOf() + dependants.add(sourceInfo.sourceFile) + nonTransitiveCache[privateType] = dependants + } + } + } + + fun isAlreadyProcessed(sourceFile: URI) = sourceCache.containsKey(sourceFile) + + /** Used for testing only. */ + internal fun getStructure(sourceFile: File) = sourceCache[sourceFile.toURI()] + + /** + * Invalidate cache entires for the specified files, and any files that depend on the changed ones. It returns the set of files that + * should be re-processed. + * */ + fun invalidateEntriesForChangedFiles(changes: Changes): SourcesToReprocess { + val allDirtyFiles = mutableSetOf() + var currentDirtyFiles = changes.sourceChanges.map { it.toURI() } + + // check for constants first because they cause full rebuilt + for (sourceChange in currentDirtyFiles) { + val structure = sourceCache[sourceChange] ?: continue + if (structure.getDefinedConstants().isNotEmpty()) { + // TODO(gavra): compare constant values, and only full rebuild if the value changes + invalidateAll() + return SourcesToReprocess.FullRebuild + } + } + + while (currentDirtyFiles.isNotEmpty()) { + + val nextRound = mutableSetOf() + for (dirtyFile in currentDirtyFiles) { + allDirtyFiles.add(dirtyFile) + + val structure = sourceCache.remove(dirtyFile) ?: continue + val dirtyTypes = structure.getDeclaredTypes() + + dirtyTypes.forEach { type -> + nonTransitiveCache[type]?.let { + allDirtyFiles.addAll(it) + } + + dependencyCache[type]?.let { + nextRound.addAll(it) + } + } + } + + currentDirtyFiles = nextRound.filter { !allDirtyFiles.contains(it) } + } + + return SourcesToReprocess.Incremental(allDirtyFiles.map { File(it) }) + } + + /** + * For aggregating annotation processors, we always need to reprocess all files annotated with an annotation claimed by the aggregating + * annotation processor. This search is not transitive. + */ + fun invalidateEntriesAnnotatedWith(annotations: Set): Set { + val patterns = annotations.map { JavacProcessingEnvironment.validImportStringToPattern(it) } + val matchesAnyPattern = { name: String -> patterns.any { it.matcher(name).matches() } } + + val toReprocess = mutableSetOf() + + for (cacheEntry in sourceCache) { + if (cacheEntry.value.getMentionedAnnotations().any(matchesAnyPattern)) { + toReprocess.add(cacheEntry.key) + } + } + + toReprocess.forEach { + sourceCache.remove(it) + } + + return toReprocess.map { File(it) }.toSet() + } + + internal fun invalidateAll() { + sourceCache.clear() + } +} + + +private val IGNORE_TYPES = { name: String -> name == "java.lang.Object" } + +class SourceFileStructure( + val sourceFile: URI +) : Serializable { + + private val declaredTypes: MutableSet = mutableSetOf() + + private val mentionedTypes: MutableSet = mutableSetOf() + private val privateTypes: MutableSet = mutableSetOf() + + private val definedConstants: MutableMap = mutableMapOf() + private val mentionedAnnotations: MutableSet = mutableSetOf() + + fun getDeclaredTypes(): Set = declaredTypes + fun getMentionedTypes(): Set = mentionedTypes + fun getPrivateTypes(): Set = privateTypes + fun getDefinedConstants(): Map = definedConstants + fun getMentionedAnnotations(): Set = mentionedAnnotations + + fun addDeclaredType(declaredType: String) { + declaredTypes.add(declaredType) + } + + fun addMentionedType(mentionedType: String) { + mentionedType.takeUnless(IGNORE_TYPES)?.let { + mentionedTypes.add(it) + } + } + + fun addDefinedConstant(name: String, value: Any) { + definedConstants[name] = value + } + + fun addMentionedAnnotations(name: String) { + mentionedAnnotations.add(name) + } + + fun addPrivateType(name: String) { + privateTypes.add(name) + } +} + + +class Changes(val sourceChanges: Collection, val dirtyFqNamesFromClasspath: Set) \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessorDiscovery.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessorDiscovery.kt new file mode 100644 index 00000000000..acebea39cd8 --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessorDiscovery.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import java.io.File +import java.io.InputStream +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +private val INCREMENTAL_DECLARED_TYPES = + setOf(DeclaredProcType.AGGREGATING.name, DeclaredProcType.ISOLATING.name, DeclaredProcType.DYNAMIC.name) +private const val INCREMENTAL_ANNOTATION_FLAG = "META-INF/gradle/incremental.annotation.processors" + +/** Checks the incremental annotation processor information for the annotation processor classpath. */ +fun getIncrementalProcessorsFromClasspath( + names: Set, classpath: Iterable): Map { + val finalValues = mutableMapOf() + + classpath.forEach { entry -> + val fromEntry = processSingleClasspathEntry(entry) + fromEntry.filter { names.contains(it.key) }.forEach { finalValues[it.key] = it.value } + + if (finalValues.size == names.size) return finalValues + } + + return finalValues +} + +private fun processSingleClasspathEntry(rootFile: File): Map { + val text: List = when { + rootFile.isDirectory -> { + val markerFile = rootFile.resolve(INCREMENTAL_ANNOTATION_FLAG) + if (markerFile.exists()) { + markerFile.bufferedReader().readLines() + } else { + emptyList() + } + } + rootFile.extension == "jar" -> ZipFile(rootFile).use { zipFile -> + val content: InputStream? = zipFile.getInputStream(ZipEntry(INCREMENTAL_ANNOTATION_FLAG)) + + content?.bufferedReader()?.readLines() ?: emptyList() + } + else -> emptyList() + } + + val nameToType = mutableMapOf() + for (line in text) { + val parts = line.split(",") + if (parts.size == 2) { + val kind = parts[1].toUpperCase(Locale.ENGLISH) + if (INCREMENTAL_DECLARED_TYPES.contains(kind)) { + nameToType[parts[0]] = enumValueOf(kind) + } + } + } + return nameToType +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessors.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessors.kt new file mode 100644 index 00000000000..e99bc228fe9 --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessors.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import com.sun.tools.javac.code.Symbol +import java.io.File +import java.io.InputStream +import java.net.URI +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import javax.annotation.processing.Filer +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.Processor +import javax.lang.model.element.Element +import javax.lang.model.element.PackageElement +import javax.tools.FileObject +import javax.tools.JavaFileManager +import javax.tools.JavaFileObject + +private val ALLOWED_RUNTIME_TYPES = setOf(RuntimeProcType.AGGREGATING.name, RuntimeProcType.ISOLATING.name) + +class IncrementalProcessor(private val processor: Processor, val kind: DeclaredProcType) : Processor by processor { + + private var dependencyCollector = lazy { createDependencyCollector() } + + override fun init(processingEnv: ProcessingEnvironment) { + if (kind == DeclaredProcType.NON_INCREMENTAL) { + processor.init(processingEnv) + } else { + val originalFiler = processingEnv.filer + val incrementalFiler = IncrementalFiler(originalFiler) + val incProcEnvironment = IncrementalProcessingEnvironment(processingEnv, incrementalFiler) + processor.init(incProcEnvironment) + incrementalFiler.dependencyCollector = dependencyCollector.value + } + } + + /** This has to invoked only once the processors has been initialized, because this accesses Processor.getSupportedOptions(). */ + private fun createDependencyCollector(): AnnotationProcessorDependencyCollector { + val type = if (kind == DeclaredProcType.DYNAMIC) { + val fromOptions = supportedOptions.singleOrNull { it.startsWith("org.gradle.annotation.processing.") } + if (fromOptions == null) { + RuntimeProcType.NON_INCREMENTAL + } else { + val declaredType = fromOptions.drop("org.gradle.annotation.processing.".length).toUpperCase() + if (ALLOWED_RUNTIME_TYPES.contains(declaredType)) { + enumValueOf(declaredType) + } else { + RuntimeProcType.NON_INCREMENTAL + } + } + } else { + kind.toRuntimeType() + } + + return AnnotationProcessorDependencyCollector(type) + } + + fun getGeneratedToSources() = dependencyCollector.value.getGeneratedToSources() + fun getRuntimeType(): RuntimeProcType = dependencyCollector.value.getRuntimeType() +} + +internal class IncrementalProcessingEnvironment(private val processingEnv: ProcessingEnvironment, private val incFiler: IncrementalFiler) : + ProcessingEnvironment by processingEnv { + override fun getFiler(): Filer = incFiler +} + +internal class IncrementalFiler(private val filer: Filer) : Filer by filer { + + internal var dependencyCollector: AnnotationProcessorDependencyCollector? = null + + override fun createSourceFile(name: CharSequence?, vararg originatingElements: Element?): JavaFileObject { + val createdSourceFile = filer.createSourceFile(name, *originatingElements) + dependencyCollector!!.add(createdSourceFile.toUri(), originatingElements) + return createdSourceFile + } + + override fun createClassFile(name: CharSequence?, vararg originatingElements: Element?): JavaFileObject { + val createdClassFile = filer.createClassFile(name, *originatingElements) + dependencyCollector!!.add(createdClassFile.toUri(), originatingElements) + return createdClassFile + } + + override fun createResource( + location: JavaFileManager.Location?, + pkg: CharSequence?, + relativeName: CharSequence?, + vararg originatingElements: Element? + ): FileObject { + val createdResource = filer.createResource(location, pkg, relativeName, *originatingElements) + dependencyCollector!!.add(createdResource.toUri(), originatingElements) + + return createdResource + } +} + +internal class AnnotationProcessorDependencyCollector(private val runtimeProcType: RuntimeProcType) { + private val generatedToSource = mutableMapOf() + private var isFullRebuild = !runtimeProcType.isIncremental + + internal fun add(createdFile: URI, originatingElements: Array) { + if (isFullRebuild) return + + val generatedFile = File(createdFile) + if (runtimeProcType == RuntimeProcType.AGGREGATING) { + generatedToSource[generatedFile] = null + } else { + val srcFiles = getSrcFiles(originatingElements) + if (srcFiles.size != 1) { + isFullRebuild = true + } else { + generatedToSource[generatedFile] = srcFiles.single() + } + } + } + + + internal fun getGeneratedToSources(): Map = if (isFullRebuild) emptyMap() else generatedToSource + internal fun getRuntimeType(): RuntimeProcType { + return if (isFullRebuild) { + RuntimeProcType.NON_INCREMENTAL + } else { + runtimeProcType + } + } +} + +private fun getSrcFiles(elements: Array): List { + return elements.filterNotNull().mapNotNull { + var origin = it + while (origin.enclosingElement != null && origin.enclosingElement !is PackageElement) { + origin = origin.enclosingElement + } + (origin as? Symbol.ClassSymbol)?.sourcefile?.let { src -> File(src.toUri()) } + } +} + +enum class DeclaredProcType { + AGGREGATING { + override fun toRuntimeType() = RuntimeProcType.AGGREGATING + }, + ISOLATING { + override fun toRuntimeType() = RuntimeProcType.ISOLATING + }, + DYNAMIC { + override fun toRuntimeType() = throw IllegalStateException("This should not be used") + }, + NON_INCREMENTAL { + override fun toRuntimeType() = RuntimeProcType.NON_INCREMENTAL + }; + + abstract fun toRuntimeType(): RuntimeProcType +} + +enum class RuntimeProcType(val isIncremental: Boolean) { + AGGREGATING(true), + ISOLATING(true), + NON_INCREMENTAL(false), +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/javacVisitors.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/javacVisitors.kt new file mode 100644 index 00000000000..53fbe30c40c --- /dev/null +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/javacVisitors.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental + +import com.sun.source.tree.* +import com.sun.source.util.SimpleTreeVisitor +import com.sun.source.util.TaskEvent +import com.sun.source.util.TaskListener +import com.sun.source.util.Trees +import com.sun.tools.javac.code.Symbol +import com.sun.tools.javac.tree.JCTree +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier + +class MentionedTypesTaskListener( + private val cache: JavaClassCache, + private val trees: Trees +) : TaskListener { + + var time = 0L + override fun started(e: TaskEvent) { + // do nothing, we just process on finish + } + + override fun finished(e: TaskEvent) { + if (e.kind != TaskEvent.Kind.ENTER || cache.isAlreadyProcessed(e.sourceFile.toUri())) return + + val l = System.currentTimeMillis() + val compilationUnit = e.compilationUnit + + val structure = SourceFileStructure(e.sourceFile.toUri()) + + val treeVisitor = TypeTreeVisitor(compilationUnit, trees, structure) + compilationUnit.typeDecls.forEach { + it.accept(treeVisitor, Visibility.ABI) + } + cache.addSourceStructure(structure) + time += System.currentTimeMillis() - l + } +} + +private enum class Visibility { + ABI, NON_ABI +} + +private class TypeTreeVisitor(val unit: CompilationUnitTree, val trees: Trees, val sourceStructure: SourceFileStructure) : + SimpleTreeVisitor() { + + override fun visitClass(node: ClassTree, visibility: Visibility): Void? { + node as JCTree.JCClassDecl + sourceStructure.addDeclaredType(node.sym.fullname.toString()) + sourceStructure.addMentionedType(node.sym.fullname.toString()) + + node.modifiers.annotations.forEach { visit(it, visibility) } + node.typeParameters.forEach { visit(it, visibility) } + visit(node.extendsClause, visibility) + node.implementsClause.forEach { visit(it, visibility) } + node.members.forEach { visit(it, visibility) } + + return null + } + + override fun visitParameterizedType(node: ParameterizedTypeTree, visibility: Visibility): Void? { + visit(node.type, visibility) + node.typeArguments.forEach { visit(it, visibility) } + + return null + } + + override fun visitMethod(node: MethodTree, visibility: Visibility): Void? { + val methodVisibility = if (node.modifiers.flags.contains(Modifier.PRIVATE)) Visibility.NON_ABI else Visibility.ABI + + node.modifiers.annotations.forEach { visit(it, methodVisibility) } + visit(node.returnType, methodVisibility) + node.parameters.forEach { visit(it, methodVisibility) } + visit(node.defaultValue, methodVisibility) + node.throws.forEach { visit(it, methodVisibility) } + node.typeParameters.forEach { visit(it, methodVisibility) } + + return null + } + + override fun visitAnnotatedType(node: AnnotatedTypeTree, visibility: Visibility): Void? { + node.annotations.forEach { visit(it, visibility) } + visit(node.underlyingType, visibility) + + return null + } + + override fun visitVariable(node: VariableTree, visibility: Visibility): Void? { + node as JCTree.JCVariableDecl + val newVisibility = if (node.sym.getKind() == ElementKind.FIELD) { + val flags = node.modifiers.getFlags() + + node.sym.constValue?.let { constValue -> + if (flags.contains(Modifier.FINAL) + && flags.contains(Modifier.STATIC) + && !flags.contains(Modifier.PRIVATE) + ) { + sourceStructure.addDefinedConstant(node.sym.simpleName.toString(), constValue) + } + } + if (flags.contains(Modifier.PRIVATE)) Visibility.NON_ABI else Visibility.ABI + } else { + visibility + } + + visit(node.getType(), newVisibility) + node.modifiers.annotations.forEach { visit(it, newVisibility) } + + return null + } + + override fun visitIdentifier(node: IdentifierTree, visibility: Visibility): Void? { + node as JCTree.JCIdent + maybeAddToTracker(node.sym, visibility) + return null + } + + override fun visitArrayType(node: ArrayTypeTree, visibility: Visibility): Void? { + visit(node.type, visibility) + return null + } + + override fun visitNewArray(node: NewArrayTree, visibility: Visibility): Void? { + node.annotations.forEach { visit(it, visibility) } + node.dimAnnotations.forEach { visit(it, visibility) } + visit(node.type, visibility) + node.initializers.forEach { visit(it, visibility) } + + return null + } + + override fun visitAnnotation(node: AnnotationTree, visibility: Visibility): Void? { + visit(node.annotationType, visibility) + node.arguments.forEach { visit(it, visibility) } + + return null + } + + override fun visitTypeParameter(node: TypeParameterTree, visibility: Visibility): Void? { + node.annotations.forEach { visit(it, visibility) } + node.bounds.forEach { visit(it, visibility) } + return null + } + + override fun visitMemberSelect(node: MemberSelectTree, visibility: Visibility): Void? { + // TODO (gavra): explore this for constant usage tracking + node as JCTree.JCFieldAccess + + if (!maybeAddToTracker(node.sym, visibility)) { + visit(node.expression, visibility) + } + return null + } + + override fun visitWildcard(node: WildcardTree, visibility: Visibility): Void? { + visit(node.bound, visibility) + return null + } + + private fun maybeAddToTracker(symbol: Symbol, visibility: Visibility): Boolean { + val kind = symbol.getKind() + if (!kind.isInterface && !kind.isClass) { + return false + } + + val qualifiedName = symbol.qualifiedName.toString() + if (symbol.getKind() == ElementKind.ANNOTATION_TYPE) { + sourceStructure.addMentionedAnnotations(qualifiedName) + } + + when (visibility) { + Visibility.ABI -> sourceStructure.addMentionedType(qualifiedName) + Visibility.NON_ABI -> sourceStructure.addPrivateType(qualifiedName) + } + return true + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/javac/KaptJavaCompiler.kt b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/javac/KaptJavaCompiler.kt index 5c83bd19e5b..55b7610693a 100644 --- a/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/javac/KaptJavaCompiler.kt +++ b/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/javac/KaptJavaCompiler.kt @@ -17,6 +17,8 @@ class KaptJavaCompiler(context: Context) : JavaCompiler(context) { return if (shouldStop(cs)) JavacList.nil() else list } + fun getTaskListeners() = this.taskListener + companion object { internal fun preRegister(context: Context) { context.put(compilerKey, Context.Factory(::KaptJavaCompiler)) diff --git a/plugins/kapt3/kapt3-base/test/JavaKaptContextTest.kt b/plugins/kapt3/kapt3-base/test/JavaKaptContextTest.kt index 9ea8532bcfe..d8fbcb07eb4 100644 --- a/plugins/kapt3/kapt3-base/test/JavaKaptContextTest.kt +++ b/plugins/kapt3/kapt3-base/test/JavaKaptContextTest.kt @@ -11,6 +11,8 @@ import org.jetbrains.kotlin.base.kapt3.KaptFlag import org.jetbrains.kotlin.base.kapt3.KaptOptions import org.jetbrains.kotlin.kapt3.base.KaptContext import org.jetbrains.kotlin.kapt3.base.doAnnotationProcessing +import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor import org.jetbrains.kotlin.kapt3.base.util.KaptBaseError import org.jetbrains.kotlin.kapt3.base.util.WriterBackedKaptLogger import org.junit.Test @@ -25,32 +27,36 @@ class JavaKaptContextTest : TestCase() { companion object { private val TEST_DATA_DIR = File("plugins/kapt3/kapt3-base/testData/runner") - fun simpleProcessor() = object : AbstractProcessor() { - override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { - for (annotation in annotations) { - val annotationName = annotation.simpleName.toString() - val annotatedElements = roundEnv.getElementsAnnotatedWith(annotation) + fun simpleProcessor() = IncrementalProcessor( + object : AbstractProcessor() { + override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { + for (annotation in annotations) { + val annotationName = annotation.simpleName.toString() + val annotatedElements = roundEnv.getElementsAnnotatedWith(annotation) - for (annotatedElement in annotatedElements) { - val generatedClassName = annotatedElement.simpleName.toString().capitalize() + annotationName.capitalize() - val file = processingEnv.filer.createSourceFile("generated." + generatedClassName) - file.openWriter().use { - it.write(""" + for (annotatedElement in annotatedElements) { + val generatedClassName = annotatedElement.simpleName.toString().capitalize() + annotationName.capitalize() + val file = processingEnv.filer.createSourceFile("generated." + generatedClassName) + file.openWriter().use { + it.write( + """ package generated; class $generatedClassName {} - """.trimIndent()) + """.trimIndent() + ) + } } } + + return true } - return true - } - - override fun getSupportedAnnotationTypes() = setOf("test.MyAnnotation") - } + override fun getSupportedAnnotationTypes() = setOf("test.MyAnnotation") + }, DeclaredProcType.NON_INCREMENTAL + ) } - private fun doAnnotationProcessing(javaSourceFile: File, processor: Processor, outputDir: File) { + private fun doAnnotationProcessing(javaSourceFile: File, processor: IncrementalProcessor, outputDir: File) { val options = KaptOptions.Builder().apply { projectBaseDir = javaSourceFile.parentFile @@ -93,7 +99,7 @@ class JavaKaptContextTest : TestCase() { } try { - doAnnotationProcessing(File(TEST_DATA_DIR, "Simple.java"), processor, TEST_DATA_DIR) + doAnnotationProcessing(File(TEST_DATA_DIR, "Simple.java"), IncrementalProcessor(processor, DeclaredProcType.NON_INCREMENTAL), TEST_DATA_DIR) } catch (e: KaptBaseError) { assertEquals(KaptBaseError.Kind.EXCEPTION, e.kind) assertEquals("Here we are!", e.cause!!.message) diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AggregatingIncrementalProcessorTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AggregatingIncrementalProcessorTest.kt new file mode 100644 index 00000000000..544ac579df8 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AggregatingIncrementalProcessorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.RuntimeProcType +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class AggregatingIncrementalProcessorTest { + @JvmField + @Rule + val tmp: TemporaryFolder = TemporaryFolder() + + private lateinit var generatedSources: File + + @Before + fun setUp() { + generatedSources = tmp.newFolder() + } + + @Test + fun testDependenciesRecorded() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val aggregating = SimpleProcessor().toAggregating() + runAnnotationProcessing(srcFiles, listOf(aggregating), generatedSources) + + assertEquals(RuntimeProcType.AGGREGATING, aggregating.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to null, + generatedSources.resolve("test/AddressGenerated.java") to null + ), + aggregating.getGeneratedToSources() + ) + } + + @Test + fun testNoSourcesToProcess() { + val srcFiles = listOf("Observable.java").map { File(TEST_DATA_DIR, it) } + val aggregating = SimpleProcessor().toAggregating() + runAnnotationProcessing(srcFiles, listOf(aggregating), generatedSources) + + assertEquals(RuntimeProcType.AGGREGATING, aggregating.getRuntimeType()) + assertEquals(emptyMap(), aggregating.getGeneratedToSources()) + } + + @Test + fun testGeneratingSourcesClassesResources() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val aggregating = SimpleCreatingClassFilesAndResources().toAggregating() + runAnnotationProcessing(srcFiles, listOf(aggregating), generatedSources) + + assertEquals(RuntimeProcType.AGGREGATING, aggregating.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to null, + generatedSources.resolve("test/UserGeneratedClass.class") to null, + generatedSources.resolve("test/UserGeneratedResource") to null, + generatedSources.resolve("test/AddressGenerated.java") to null, + generatedSources.resolve("test/AddressGeneratedClass.class") to null, + generatedSources.resolve("test/AddressGeneratedResource") to null + ), + aggregating.getGeneratedToSources() + ) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AnnotationProcessorDependencyCollectorTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AnnotationProcessorDependencyCollectorTest.kt new file mode 100644 index 00000000000..c95ec020d66 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/AnnotationProcessorDependencyCollectorTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.AnnotationProcessorDependencyCollector +import org.jetbrains.kotlin.kapt3.base.incremental.RuntimeProcType +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File + +class AnnotationProcessorDependencyCollectorTest { + @Test + fun testAggregating() { + val aggregating = AnnotationProcessorDependencyCollector(RuntimeProcType.AGGREGATING) + val generated = listOf("GeneratedA.java", "GeneratedB.java", "GeneratedC.java").map { File(it).toURI() } + generated.forEach { aggregating.add(it, emptyArray()) } + + assertEquals(aggregating.getGeneratedToSources(), generated.map { File(it) to null }.toMap()) + assertEquals(aggregating.getRuntimeType(), RuntimeProcType.AGGREGATING) + } + + @Test + fun testIsolatingWithoutOrigin() { + val isolating = AnnotationProcessorDependencyCollector(RuntimeProcType.ISOLATING) + isolating.add(File("GeneratedA.java").toURI(), emptyArray()) + + assertEquals(isolating.getRuntimeType(), RuntimeProcType.NON_INCREMENTAL) + assertEquals(isolating.getGeneratedToSources(), emptyMap()) + } + + @Test + fun testNonIncremental() { + val nonIncremental = AnnotationProcessorDependencyCollector(RuntimeProcType.NON_INCREMENTAL) + nonIncremental.add(File("GeneratedA.java").toURI(), emptyArray()) + nonIncremental.add(File("GeneratedB.java").toURI(), emptyArray()) + + assertEquals(nonIncremental.getRuntimeType(), RuntimeProcType.NON_INCREMENTAL) + assertEquals(nonIncremental.getGeneratedToSources(), emptyMap()) + } +} + diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/DynamicIncrementalProcessorTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/DynamicIncrementalProcessorTest.kt new file mode 100644 index 00000000000..1210210ad25 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/DynamicIncrementalProcessorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.RuntimeProcType +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + + +class DynamicIncrementalProcessorTest { + @JvmField + @Rule + val tmp: TemporaryFolder = TemporaryFolder() + + private lateinit var generatedSources: File + + @Before + fun setUp() { + generatedSources = tmp.newFolder() + } + + @Test + fun testIfIsolating() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val dynamic = DynamicProcessor(kind = RuntimeProcType.ISOLATING).toDynamic() + runAnnotationProcessing(srcFiles, listOf(dynamic), generatedSources) + + assertEquals(RuntimeProcType.ISOLATING, dynamic.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/User.java").absoluteFile, + generatedSources.resolve("test/AddressGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java").absoluteFile + ), + dynamic.getGeneratedToSources() + ) + } + + @Test + fun testIfAggregating() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val dynamic = DynamicProcessor(kind = RuntimeProcType.AGGREGATING).toDynamic() + runAnnotationProcessing(srcFiles, listOf(dynamic), generatedSources) + + assertEquals(RuntimeProcType.AGGREGATING, dynamic.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to null, + generatedSources.resolve("test/AddressGenerated.java") to null + ), + dynamic.getGeneratedToSources() + ) + } + + @Test + fun testIfNonIncremental() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val dynamic = DynamicProcessor(kind = RuntimeProcType.NON_INCREMENTAL).toDynamic() + runAnnotationProcessing(srcFiles, listOf(dynamic), generatedSources) + + assertEquals(RuntimeProcType.NON_INCREMENTAL, dynamic.getRuntimeType()) + assertEquals( + emptyMap(), + dynamic.getGeneratedToSources() + ) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalKaptTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalKaptTest.kt new file mode 100644 index 00000000000..92f04c930b6 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalKaptTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.base.kapt3.KaptOptions +import org.jetbrains.kotlin.base.kapt3.collectJavaSourceFiles +import org.jetbrains.kotlin.kapt3.base.KaptContext +import org.jetbrains.kotlin.kapt3.base.doAnnotationProcessing +import org.jetbrains.kotlin.kapt3.base.util.WriterBackedKaptLogger +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class IncrementalKaptTest { + + @Rule + @JvmField + var tmp = TemporaryFolder() + + @Test + fun testIncrementalRun() { + val sourcesDir = tmp.newFolder().resolve("test").also { base -> + base.mkdir() + listOf("User.java", "Address.java", "Observable.java").map { + TEST_DATA_DIR.resolve(it).copyTo(base.resolve(it)) + } + } + + val outputDir = tmp.newFolder() + val incrementalCacheDir = tmp.newFolder() + val classpathHistory = tmp.newFolder().also { + it.resolve("0").createNewFile() + } + val options = KaptOptions.Builder().apply { + projectBaseDir = tmp.newFolder() + javaSourceRoots.add(sourcesDir) + + sourcesOutputDir = outputDir + classesOutputDir = outputDir + stubsOutputDir = outputDir + incrementalDataOutputDir = outputDir + + incrementalCache = incrementalCacheDir + classpathFqNamesHistory = classpathHistory + }.build() + + val logger = WriterBackedKaptLogger(isVerbose = true) + KaptContext(options, true, logger).use { + it.doAnnotationProcessing( + options.collectJavaSourceFiles(it.cacheManager), listOf(SimpleProcessor().toIsolating()) + ) + } + + val classesOutput = tmp.newFolder() + compileSources(sourcesDir.listFiles().asIterable(), classesOutput) + + val addressTimestamp = outputDir.resolve("test/AddressGenerated.java").lastModified() + assertTrue(outputDir.resolve("test/UserGenerated.java").exists()) + assertTrue(outputDir.resolve("test/AddressGenerated.java").exists()) + + val optionsForSecondRun = KaptOptions.Builder().apply { + projectBaseDir = tmp.newFolder() + + sourcesOutputDir = outputDir + classesOutputDir = outputDir + stubsOutputDir = outputDir + incrementalDataOutputDir = outputDir + + incrementalCache = incrementalCacheDir + classpathFqNamesHistory = classpathHistory + compiledSources.add(classesOutput) + changedFiles.add(sourcesDir.resolve("User.java")) + }.build() + + KaptContext(optionsForSecondRun, true, logger).use { + val sourcesToReprocess = optionsForSecondRun.collectJavaSourceFiles(it.cacheManager) + assertFalse(outputDir.resolve("test/UserGenerated.java").exists()) + + it.doAnnotationProcessing( + sourcesToReprocess, listOf(SimpleProcessor().toIsolating()) + ) + } + + assertEquals(addressTimestamp, outputDir.resolve("test/AddressGenerated.java").lastModified()) + assertTrue(outputDir.resolve("test/UserGenerated.java").exists()) + + sourcesDir.resolve("User.java").delete() + KaptContext(optionsForSecondRun, true, logger).use { + it.doAnnotationProcessing( + optionsForSecondRun.collectJavaSourceFiles(it.cacheManager), listOf(SimpleProcessor().toIsolating()) + ) + } + + assertEquals(addressTimestamp, outputDir.resolve("test/AddressGenerated.java").lastModified()) + assertFalse(outputDir.resolve("test/UserGenerated.java").exists()) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalProcessorDiscoveryTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalProcessorDiscoveryTest.kt new file mode 100644 index 00000000000..361d91f3887 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IncrementalProcessorDiscoveryTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType +import org.jetbrains.kotlin.kapt3.base.incremental.getIncrementalProcessorsFromClasspath +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class IncrementalProcessorDiscoveryTest { + + private val markerFileContent = """ + Input1Processor1,AGGREGATING + Input1Processor2,ISOLATING + Input1Processor3,DYNAMIC + Input1Processor4,UNKNOWN + Input1Processor5 this is malformed input + """.trimIndent(); + + @Rule + @JvmField + var tmp = TemporaryFolder() + + @Test + fun locateInJars() { + val inputJar = tmp.root.resolve("inputJar.jar") + ZipOutputStream(inputJar.outputStream()).use { + it.putNextEntry(ZipEntry("META-INF/gradle/incremental.annotation.processors")) + it.write(markerFileContent.toByteArray()) + it.closeEntry() + } + + val info = getIncrementalProcessorsFromClasspath( + setOf("Input1Processor4", "Input1Processor3", "Input1Processor2", "Input1Processor1"), + listOf(inputJar) + ) + + assertEquals( + mapOf( + "Input1Processor1" to DeclaredProcType.AGGREGATING, + "Input1Processor2" to DeclaredProcType.ISOLATING, + "Input1Processor3" to DeclaredProcType.DYNAMIC + ), + info + ) + } + + @Test + fun locateInDir() { + val inputDir = tmp.root.resolve("inputDir") + inputDir.resolve("META-INF/gradle/incremental.annotation.processors").let { + it.parentFile.mkdirs() + it.writeText(markerFileContent) + } + + val info = getIncrementalProcessorsFromClasspath( + setOf("Input1Processor4", "Input1Processor3", "Input1Processor2", "Input1Processor1"), + listOf(inputDir) + ) + + assertEquals( + mapOf( + "Input1Processor1" to DeclaredProcType.AGGREGATING, + "Input1Processor2" to DeclaredProcType.ISOLATING, + "Input1Processor3" to DeclaredProcType.DYNAMIC + ), + info + ) + } + + @Test + fun locateInJarsAndDirs() { + val inputJar = tmp.root.resolve("inputJar.jar") + ZipOutputStream(inputJar.outputStream()).use { + it.putNextEntry(ZipEntry("META-INF/gradle/incremental.annotation.processors")) + it.write("InputJarProcessor,ISOLATING".toByteArray()) + it.closeEntry() + } + + val inputDir = tmp.root.resolve("inputDir") + inputDir.resolve("META-INF/gradle/incremental.annotation.processors").let { + it.parentFile.mkdirs() + it.writeText("InputDirProcessor,DYNAMIC") + } + + val info = getIncrementalProcessorsFromClasspath( + setOf("InputJarNonIncrementalProcessor", "InputJarProcessor", "InputDirNonIncrementalProcessor", "InputDirProcessor"), + listOf(inputJar, inputDir) + ) + assertEquals( + mapOf( + "InputJarProcessor" to DeclaredProcType.ISOLATING, + "InputDirProcessor" to DeclaredProcType.DYNAMIC + ), + info + ) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IsolatingIncrementalProcessorTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IsolatingIncrementalProcessorTest.kt new file mode 100644 index 00000000000..e74c1614dd2 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/IsolatingIncrementalProcessorTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.RuntimeProcType +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class IsolationgIncrementalProcessorTest { + @JvmField + @Rule + val tmp: TemporaryFolder = TemporaryFolder() + + private lateinit var generatedSources: File + + @Before + fun setUp() { + generatedSources = tmp.newFolder() + } + + @Test + fun testDependenciesRecorded() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val isolating = SimpleProcessor().toIsolating() + runAnnotationProcessing(srcFiles, listOf(isolating), generatedSources) + + assertEquals(RuntimeProcType.ISOLATING, isolating.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/User.java").absoluteFile, + generatedSources.resolve("test/AddressGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java").absoluteFile + ), + isolating.getGeneratedToSources() + ) + } + + @Test + fun testNoSourcesToProcess() { + val srcFiles = listOf("Observable.java").map { File(TEST_DATA_DIR, it) } + val isolating = SimpleProcessor().toIsolating() + runAnnotationProcessing(srcFiles, listOf(isolating), generatedSources) + + assertEquals(RuntimeProcType.ISOLATING, isolating.getRuntimeType()) + assertEquals(emptyMap(), isolating.getGeneratedToSources()) + } + + @Test + fun testGeneratingSourcesClassesResources() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val isolating = SimpleCreatingClassFilesAndResources().toIsolating() + runAnnotationProcessing(srcFiles, listOf(isolating), generatedSources) + + assertEquals(RuntimeProcType.ISOLATING, isolating.getRuntimeType()) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to TEST_DATA_DIR.resolve("User.java").absoluteFile, + generatedSources.resolve("test/UserGeneratedClass.class") to TEST_DATA_DIR.resolve("User.java").absoluteFile, + generatedSources.resolve("test/UserGeneratedResource") to TEST_DATA_DIR.resolve("User.java").absoluteFile, + generatedSources.resolve("test/AddressGenerated.java") to TEST_DATA_DIR.resolve("Address.java").absoluteFile, + generatedSources.resolve("test/AddressGeneratedClass.class") to TEST_DATA_DIR.resolve("Address.java").absoluteFile, + generatedSources.resolve("test/AddressGeneratedResource") to TEST_DATA_DIR.resolve("Address.java").absoluteFile + ), + isolating.getGeneratedToSources() + ) + } + + @Test + fun testWrongOriginElement() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val isolating = SimpleProcessor(wrongOrigin = true).toIsolating() + runAnnotationProcessing(srcFiles, listOf(isolating), generatedSources) + + assertEquals(RuntimeProcType.NON_INCREMENTAL, isolating.getRuntimeType()) + assertEquals(emptyMap(), isolating.getGeneratedToSources()) + } + + @Test + fun testTwoIsolating() { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + val isolating = listOf( + SimpleProcessor().toIsolating(), + SimpleProcessor(generatedSuffix = "Two").toIsolating() + ) + runAnnotationProcessing(srcFiles, isolating, generatedSources) + + isolating.forEach { assertEquals(RuntimeProcType.ISOLATING, it.getRuntimeType()) } + assertEquals( + mapOf( + generatedSources.resolve("test/UserGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/User.java").absoluteFile, + generatedSources.resolve("test/AddressGenerated.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java").absoluteFile + ), isolating[0].getGeneratedToSources() + ) + + assertEquals( + mapOf( + generatedSources.resolve("test/UserGeneratedTwo.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/User.java").absoluteFile, + generatedSources.resolve("test/AddressGeneratedTwo.java") to File("plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java").absoluteFile + ), isolating[1].getGeneratedToSources() + ) + } +} diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/JavaClassCacheManagerTest.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/JavaClassCacheManagerTest.kt new file mode 100644 index 00000000000..b9e75828d99 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/JavaClassCacheManagerTest.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt3.base.incremental; + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.ObjectOutputStream + +class JavaClassCacheManagerTest { + + @Rule + @JvmField + var tmp = TemporaryFolder() + + private lateinit var cache: JavaClassCacheManager + private lateinit var cacheDir: File + private lateinit var classpathHistory: File + + @Before + fun setUp() { + cacheDir = tmp.newFolder() + classpathHistory = tmp.newFolder() + cache = JavaClassCacheManager(cacheDir, classpathHistory) + } + + + @Test + fun testClosingCache() { + cache.close() + + assertTrue(cacheDir.resolve("java-cache.bin").exists()) + assertTrue(cacheDir.resolve("apt-cache.bin").exists()) + assertTrue(cacheDir.resolve("last-build-ts.bin").exists()) + } + + @Test + fun testMentionedTypes() { + SourceFileStructure(File("Mentioned.java").toURI()).also { + it.addDeclaredType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Src.java").toURI()).also { + it.addDeclaredType("test.Src") + it.addMentionedType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("ReferencesSrc.java").toURI()).also { + it.addDeclaredType("test.ReferencesSrc") + it.addPrivateType("test.Src") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(File("Mentioned.java"))) as SourcesToReprocess.Incremental + assertEquals( + listOf( + File("Mentioned.java").absoluteFile, + File("Src.java").absoluteFile, + File("ReferencesSrc.java").absoluteFile + ), dirtyFiles.toReprocess + ) + } + + @Test + fun testPrivateTypes() { + SourceFileStructure(File("Mentioned.java").toURI()).also { + it.addDeclaredType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Src.java").toURI()).also { + it.addDeclaredType("test.Src") + it.addPrivateType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("ReferencesSrc.java").toURI()).also { + it.addDeclaredType("test.ReferencesSrc") + it.addPrivateType("test.Src") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(File("Mentioned.java"))) as SourcesToReprocess.Incremental + assertEquals( + listOf( + File("Mentioned.java").absoluteFile, + File("Src.java").absoluteFile + ), dirtyFiles.toReprocess + ) + } + + @Test + fun testMultipleDeclared() { + SourceFileStructure(File("TwoTypes.java").toURI()).also { + it.addDeclaredType("test.TwoTypes") + it.addDeclaredType("test.AnotherType") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("ReferencesTwoTypes.java").toURI()).also { + it.addDeclaredType("test.ReferencesTwoTypes") + it.addPrivateType("test.TwoTypes") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("ReferencesAnotherType.java").toURI()).also { + it.addDeclaredType("test.ReferencesAnotherType") + it.addPrivateType("test.AnotherType") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(File("TwoTypes.java"))) as SourcesToReprocess.Incremental + assertEquals( + listOf( + File("TwoTypes.java").absoluteFile, + File("ReferencesTwoTypes.java").absoluteFile, + File("ReferencesAnotherType.java").absoluteFile + ), dirtyFiles.toReprocess + ) + } + + @Test + fun testNoClasspathHistory() { + SourceFileStructure(File("Src.java").toURI()).also { + it.addDeclaredType("test.Src") + cache.javaCache.addSourceStructure(it) + } + cache.close() + classpathHistory.resolve(Long.MAX_VALUE.toString()).createNewFile() + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf()) + assertEquals(SourcesToReprocess.FullRebuild, dirtyFiles) + } + + @Test + fun testWithClasspathHistoryButNoNewChanges() { + SourceFileStructure(File("Src.java").toURI()).also { + it.addDeclaredType("test.Src") + it.addMentionedType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + ObjectOutputStream(classpathHistory.resolve("0").outputStream()).use { + it.writeObject(listOf("test.Mentioned")) + } + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf()) as SourcesToReprocess.Incremental + assertEquals(emptyList(), dirtyFiles.toReprocess) + } + + @Test + fun testWithClasspathHistoryWithChanges() { + SourceFileStructure(File("Src.java").toURI()).also { + it.addDeclaredType("test.Src") + it.addMentionedType("test.Mentioned") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + ObjectOutputStream(classpathHistory.resolve(Long.MAX_VALUE.toString()).outputStream()).use { + it.writeObject(listOf("test.Mentioned")) + } + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf()) + assertEquals(SourcesToReprocess.FullRebuild, dirtyFiles) + } + + @Test + fun testDefinesConstant() { + SourceFileStructure(File("Constants.java").toURI()).also { + it.addDeclaredType("test.Constants") + it.addDefinedConstant("CONST", 123) + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Unrelated1.java").toURI()).also { + it.addDeclaredType("test.Unrelated1") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Unrelated2.java").toURI()).also { + it.addDeclaredType("test.Unrelated2") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(File("Constants.java"))) + assertEquals(SourcesToReprocess.FullRebuild, dirtyFiles) + } + + @Test + fun testWithAnnotations() { + SourceFileStructure(File("Annotated1.java").toURI()).also { + it.addDeclaredType("test.Annotated1") + it.addMentionedAnnotations("test.Annotation") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Annotated2.java").toURI()).also { + it.addDeclaredType("test.Annotated2") + it.addMentionedAnnotations("com.test.MyAnnotation") + cache.javaCache.addSourceStructure(it) + } + SourceFileStructure(File("Annotated3.java").toURI()).also { + it.addDeclaredType("test.Annotated3") + it.addMentionedAnnotations("Runnable") + cache.javaCache.addSourceStructure(it) + } + prepareForIncremental() + + assertEquals(setOf(File("Annotated1.java").absoluteFile), cache.javaCache.invalidateEntriesAnnotatedWith(setOf("test.Annotation"))) + assertEquals(setOf(File("Annotated2.java").absoluteFile), cache.javaCache.invalidateEntriesAnnotatedWith(setOf("com.test.*"))) + assertEquals(setOf(File("Annotated3.java").absoluteFile), cache.javaCache.invalidateEntriesAnnotatedWith(setOf("*"))) + } + + private fun prepareForIncremental() { + cache.close() + classpathHistory.resolve("0").createNewFile() + cache = JavaClassCacheManager(cacheDir, classpathHistory) + } +} diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestComplexIncrementalAptCache.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestComplexIncrementalAptCache.kt new file mode 100644 index 00000000000..07f6fac57c6 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestComplexIncrementalAptCache.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.JavaClassCacheManager +import org.jetbrains.kotlin.kapt3.base.incremental.MentionedTypesTaskListener +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +private val MY_TEST_DIR = File("plugins/kapt3/kapt3-base/testData/runner/incremental/complex") + +class TestComplexIncrementalAptCache { + + companion object { + @ClassRule + @JvmField + var tmp = TemporaryFolder() + + private lateinit var cache: JavaClassCacheManager + private lateinit var generatedSources: File + + @JvmStatic + @BeforeClass + fun setUp() { + val classpathHistory = tmp.newFolder() + cache = JavaClassCacheManager(tmp.newFolder(), classpathHistory) + generatedSources = tmp.newFolder() + cache.close() + classpathHistory.resolve("0").createNewFile() + val processor = SimpleProcessor().toAggregating() + val srcFiles = listOf( + "MyEnum.java", + "MyNumber.java", + "NumberAnnotation.java", + "NumberException.java", + "NumberHolder.java", + "NumberManager.java", + "GenericNumber.java" + ).map { File(MY_TEST_DIR, it) } + runAnnotationProcessing( + srcFiles, + listOf(processor), + generatedSources + ) { trees -> MentionedTypesTaskListener(cache.javaCache, trees) } + cache.updateCache(listOf(processor)) + } + } + + @Test + fun testEnum() { + val myEnum = cache.javaCache.getStructure(MY_TEST_DIR.resolve("MyEnum.java"))!! + + assertEquals(setOf("test.MyEnum"), myEnum.getDeclaredTypes()) + assertEquals(emptySet(), myEnum.getMentionedAnnotations()) + assertEquals(emptySet(), myEnum.getPrivateTypes()) + assertEquals(setOf("test.MyEnum", "test.TypeGeneratedByApt"), myEnum.getMentionedTypes()) + assertEquals(emptyMap(), myEnum.getDefinedConstants()) + } + + @Test + fun testMyNumber() { + val myNumber = cache.javaCache.getStructure(MY_TEST_DIR.resolve("MyNumber.java"))!! + + assertEquals( + setOf( + "test.MyNumber", + "test.FieldAnnotation", + "test.MethodAnnotation", + "test.ParameterAnnotation", + "test.TypeUseAnnotation", + "test.AnotherTypeUseAnnotation", + "test.ThrowTypeUseAnnotation" + ), myNumber.getDeclaredTypes() + ) + assertEquals( + setOf( + "java.lang.annotation.Target", + "test.FieldAnnotation", + "test.ParameterAnnotation", + "test.MethodAnnotation", + "test.TypeUseAnnotation", + "test.AnotherTypeUseAnnotation", + "test.ThrowTypeUseAnnotation" + ), myNumber.getMentionedAnnotations() + ) + assertEquals( + setOf( + "test.FieldAnnotation", + "java.lang.String", + "test.ParameterAnnotation", + "test.MethodAnnotation", + "test.AnotherTypeUseAnnotation", + "test.ThrowTypeUseAnnotation", + "java.lang.Number", + "java.lang.RuntimeException" + ), myNumber.getPrivateTypes() + ) + assertEquals( + setOf( + "test.MyNumber", + "java.lang.annotation.Target", + "test.FieldAnnotation", + "test.ParameterAnnotation", + "test.MethodAnnotation", + "test.TypeUseAnnotation", + "test.AnotherTypeUseAnnotation", + "test.ThrowTypeUseAnnotation", + "java.util.HashSet" + ), myNumber.getMentionedTypes() + ) + assertEquals(emptyMap(), myNumber.getDefinedConstants()) + } + + @Test + fun testAnnotation() { + val numberAnnotation = cache.javaCache.getStructure(MY_TEST_DIR.resolve("NumberAnnotation.java"))!! + + assertEquals(setOf("test.NumberAnnotation", "test.BaseAnnotation"), numberAnnotation.getDeclaredTypes()) + assertEquals(setOf("test.BaseAnnotation"), numberAnnotation.getMentionedAnnotations()) + assertEquals(emptySet(), numberAnnotation.getPrivateTypes()) + assertEquals( + setOf( + "test.BaseAnnotation", + "test.NumberAnnotation", + "java.lang.Class", + "test.MyEnum", + "test.NumberManager" + ), numberAnnotation.getMentionedTypes() + ) + assertEquals(emptyMap(), numberAnnotation.getDefinedConstants()) + } + + @Test + fun testNumberException() { + val numberException = cache.javaCache.getStructure(MY_TEST_DIR.resolve("NumberException.java"))!! + + assertEquals(setOf("test.NumberException"), numberException.getDeclaredTypes()) + assertEquals(emptySet(), numberException.getMentionedAnnotations()) + assertEquals(emptySet(), numberException.getPrivateTypes()) + assertEquals(setOf("test.NumberException", "java.lang.RuntimeException"), numberException.getMentionedTypes()) + assertEquals(emptyMap(), numberException.getDefinedConstants()) + } + + @Test + fun testNumberHolder() { + val numberHolder = cache.javaCache.getStructure(MY_TEST_DIR.resolve("NumberHolder.java"))!! + + assertEquals(setOf("test.NumberHolder", "test.NumberHolder.MyInnerClass"), numberHolder.getDeclaredTypes()) + assertEquals(setOf("test.NumberAnnotation"), numberHolder.getMentionedAnnotations()) + assertEquals(setOf("test.NumberManager"), numberHolder.getPrivateTypes()) + assertEquals( + setOf( + "test.NumberHolder", + "test.NumberHolder.MyInnerClass", + "test.NumberAnnotation", + "test.NumberManager", + "test.MyNumber", + "java.util.HashSet", + "java.lang.Runnable", + "java.lang.String", + "test.NumberException" + ), numberHolder.getMentionedTypes() + ) + assertEquals(emptyMap(), numberHolder.getDefinedConstants()) + } + + @Test + fun testNumberManager() { + val numberManager = cache.javaCache.getStructure(MY_TEST_DIR.resolve("NumberManager.java"))!! + + assertEquals(setOf("test.NumberManager"), numberManager.getDeclaredTypes()) + assertEquals(emptySet(), numberManager.getMentionedAnnotations()) + assertEquals(setOf("test.MyEnum"), numberManager.getPrivateTypes()) + assertEquals( + setOf( + "test.NumberManager", + "java.lang.String", + "test.NumberHolder" + ), numberManager.getMentionedTypes() + ) + assertEquals(mapOf("CONST" to "STRING_CONST", "INT_CONST" to 246), numberManager.getDefinedConstants()) + } + + @Test + fun testGenericNumber() { + val genericNumber = cache.javaCache.getStructure(MY_TEST_DIR.resolve("GenericNumber.java"))!! + + assertEquals(setOf("test.GenericNumber"), genericNumber.getDeclaredTypes()) + assertEquals(emptySet(), genericNumber.getMentionedAnnotations()) + assertEquals(emptySet(), genericNumber.getPrivateTypes()) + assertEquals( + setOf( + "test.GenericNumber", + "java.util.HashSet", + "java.lang.Runnable", + "java.lang.Cloneable", + "java.lang.CharSequence", + "java.lang.Number" + ), genericNumber.getMentionedTypes() + ) + assertEquals(emptyMap(), genericNumber.getDefinedConstants()) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestSimpleIncrementalAptCache.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestSimpleIncrementalAptCache.kt new file mode 100644 index 00000000000..5dfa2846833 --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/TestSimpleIncrementalAptCache.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor +import org.jetbrains.kotlin.kapt3.base.incremental.JavaClassCacheManager +import org.jetbrains.kotlin.kapt3.base.incremental.MentionedTypesTaskListener +import org.jetbrains.kotlin.kapt3.base.incremental.SourcesToReprocess +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class TestSimpleIncrementalAptCache { + + @Rule + @JvmField + var tmp = TemporaryFolder() + + private lateinit var cache: JavaClassCacheManager + private lateinit var generatedSources: File + + @Before + fun setUp() { + val classpathHistory = tmp.newFolder() + cache = JavaClassCacheManager(tmp.newFolder(), classpathHistory) + generatedSources = tmp.newFolder() + cache.close() + classpathHistory.resolve("0").createNewFile() + } + + @Test + fun testAggregatingAnnotations() { + runProcessor(SimpleProcessor().toAggregating()) + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(TEST_DATA_DIR.resolve("User.java"))) as SourcesToReprocess.Incremental + assertEquals( + listOf(TEST_DATA_DIR.resolve("User.java").absoluteFile, TEST_DATA_DIR.resolve("Address.java").absoluteFile), + dirtyFiles.toReprocess + ) + assertFalse(generatedSources.resolve("test/UserGenerated.java").exists()) + assertFalse(generatedSources.resolve("test/AddressGenerated.java").exists()) + } + + @Test + fun testIsolatingAnnotations() { + runProcessor(SimpleProcessor().toIsolating()) + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(TEST_DATA_DIR.resolve("User.java"))) as SourcesToReprocess.Incremental + assertFalse(generatedSources.resolve("test/UserGenerated.java").exists()) + assertEquals( + listOf(TEST_DATA_DIR.resolve("User.java").absoluteFile), + dirtyFiles.toReprocess + ) + } + + @Test + fun testNonIncremental() { + runProcessor(SimpleProcessor().toNonIncremental()) + + val dirtyFiles = cache.invalidateAndGetDirtyFiles(listOf(TEST_DATA_DIR.resolve("User.java"))) + assertTrue(dirtyFiles is SourcesToReprocess.FullRebuild) + } + + private fun runProcessor(processor: IncrementalProcessor) { + val srcFiles = listOf("User.java", "Address.java", "Observable.java").map { File(TEST_DATA_DIR, it) } + runAnnotationProcessing( + srcFiles, + listOf(processor), + generatedSources + ) { trees -> MentionedTypesTaskListener(cache.javaCache, trees) } + cache.updateCache(listOf(processor)) + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/fixtures.kt b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/fixtures.kt new file mode 100644 index 00000000000..404ff7e260b --- /dev/null +++ b/plugins/kapt3/kapt3-base/test/org/jetbrains/kotlin/kapt3/base/incremental/fixtures.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.kapt.base.test.org.jetbrains.kotlin.kapt3.base.incremental + +import com.sun.source.util.TaskListener +import com.sun.source.util.Trees +import com.sun.tools.javac.api.JavacTaskImpl +import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor +import org.jetbrains.kotlin.kapt3.base.incremental.RuntimeProcType +import java.io.File +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.Filer +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.TypeElement +import javax.tools.StandardLocation +import javax.tools.ToolProvider + +val TEST_DATA_DIR = File("plugins/kapt3/kapt3-base/testData/runner/incremental") + +fun runAnnotationProcessing( + srcFiles: List, + processor: List, + generatedSources: File, + listener: (Trees) -> TaskListener? = { null } +) { + val compiler = ToolProvider.getSystemJavaCompiler() + compiler.getStandardFileManager(null, null, null).use { fileManager -> + val javaSrcs = fileManager.getJavaFileObjectsFromFiles(srcFiles) + val compilationTask = + compiler.getTask( + null, + fileManager, + null, + listOf("-proc:only", "-s", generatedSources.absolutePath, "-d", generatedSources.absolutePath), + null, + javaSrcs + ) as JavacTaskImpl + + val taskListener = listener(Trees.instance(compilationTask)) + taskListener?.let { compilationTask.addTaskListener(it) } + + compilationTask.setProcessors(processor) + compilationTask.call() + } +} + +fun compileSources(srcFiles: Iterable, outputDir: File) { + val compiler = ToolProvider.getSystemJavaCompiler() + compiler.getStandardFileManager(null, null, null).use { fileManager -> + val compilationTask = + compiler.getTask( + null, + fileManager, + null, + listOf("-d", outputDir.absolutePath), + null, + fileManager.getJavaFileObjectsFromFiles(srcFiles) + ) as JavacTaskImpl + + compilationTask.call() + } +} + +fun SimpleProcessor.toAggregating() = IncrementalProcessor(this, DeclaredProcType.AGGREGATING) +fun SimpleProcessor.toIsolating() = IncrementalProcessor(this, DeclaredProcType.ISOLATING) +fun SimpleProcessor.toNonIncremental() = IncrementalProcessor(this, DeclaredProcType.NON_INCREMENTAL) +fun DynamicProcessor.toDynamic() = IncrementalProcessor(this, DeclaredProcType.DYNAMIC) + +open class SimpleProcessor(private val wrongOrigin: Boolean = false, private val generatedSuffix: String = "") : AbstractProcessor() { + lateinit var filer: Filer + + override fun init(processingEnv: ProcessingEnvironment?) { + super.init(processingEnv) + filer = processingEnv!!.filer + } + + override fun getSupportedAnnotationTypes(): MutableSet = mutableSetOf("test.Observable") + + override fun process(annotations: MutableSet, roundEnv: RoundEnvironment): Boolean { + if (annotations.isEmpty()) return false + + roundEnv.getElementsAnnotatedWith(annotations.single()).forEach { + it as TypeElement + + val generatedName = "${it.qualifiedName}Generated$generatedSuffix" + filer.createSourceFile(generatedName, it.takeUnless { wrongOrigin }).openWriter().use { it.write("") } + } + + return false + } + + override fun getSupportedSourceVersion(): SourceVersion { + return SourceVersion.latest() + } +} + +class DynamicProcessor(private val kind: RuntimeProcType) : SimpleProcessor() { + override fun getSupportedOptions(): MutableSet { + return mutableSetOf("org.gradle.annotation.processing.${kind.name}") + } +} + +class SimpleCreatingClassFilesAndResources : SimpleProcessor() { + override fun process(annotations: MutableSet, roundEnv: RoundEnvironment): Boolean { + super.process(annotations, roundEnv) + + if (annotations.isEmpty()) return false + roundEnv.getElementsAnnotatedWith(annotations.single()).forEach { + it as TypeElement + + val generatedName = "${it.qualifiedName}Generated" + filer.createClassFile("${generatedName}Class", it).openWriter().use { it.write("") } + filer.createResource(StandardLocation.SOURCE_OUTPUT, "test", "${it.simpleName}GeneratedResource", it).openWriter().use { it.write("") } + } + + return false + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java new file mode 100644 index 00000000000..ee7a24473db --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/Address.java @@ -0,0 +1,4 @@ +package test; + +@Observable +public class Address{} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/Observable.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/Observable.java new file mode 100644 index 00000000000..8f128af5ac8 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/Observable.java @@ -0,0 +1,3 @@ +package test; + +public @interface Observable {} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/User.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/User.java new file mode 100644 index 00000000000..da888cd209e --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/User.java @@ -0,0 +1,4 @@ +package test; + +@Observable +public class User {} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/GenericNumber.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/GenericNumber.java new file mode 100644 index 00000000000..12c60551279 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/GenericNumber.java @@ -0,0 +1,9 @@ +package test; + +import java.util.HashSet; + +public class GenericNumber { + java.util.HashSet usingGenerics(HashSet set) { + return null; + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyEnum.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyEnum.java new file mode 100644 index 00000000000..939449f5738 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyEnum.java @@ -0,0 +1,8 @@ +package test; + +public enum MyEnum { + VALUE; + + public void process(test.TypeGeneratedByApt toBeGenerated) { + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyNumber.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyNumber.java new file mode 100644 index 00000000000..06d9a6e4fa8 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/MyNumber.java @@ -0,0 +1,32 @@ +package test; + +import java.lang.annotation.*; +import java.util.*; +import static java.lang.annotation.ElementType.*; + + +public class MyNumber extends @TypeUseAnnotation HashSet { + @FieldAnnotation + private String value; + + @MethodAnnotation + private void getPrintedValue(@ParameterAnnotation String format) throws @ThrowTypeUseAnnotation RuntimeException{ + } + + private <@AnotherTypeUseAnnotation T extends Number> void accept(T visitor) { + } +} + +@interface FieldAnnotation {} +@interface MethodAnnotation {} +@interface ParameterAnnotation {} + +@Target(value={TYPE_PARAMETER, TYPE_USE}) +@interface TypeUseAnnotation {} + +@Target(value={TYPE_PARAMETER, TYPE_USE}) +@interface AnotherTypeUseAnnotation {} + + +@Target(value={TYPE_PARAMETER, TYPE_USE}) +@interface ThrowTypeUseAnnotation {} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberAnnotation.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberAnnotation.java new file mode 100644 index 00000000000..035215d31c3 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberAnnotation.java @@ -0,0 +1,9 @@ +package test; + +public @interface NumberAnnotation { + Class[] classReferences() default { NumberManager.class}; + MyEnum enumReference() default MyEnum.VALUE; + BaseAnnotation otherAnnotation() default @test.BaseAnnotation; +} + +@interface BaseAnnotation {} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberException.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberException.java new file mode 100644 index 00000000000..678ad780ef5 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberException.java @@ -0,0 +1,4 @@ +package test; + +public class NumberException extends RuntimeException { +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberHolder.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberHolder.java new file mode 100644 index 00000000000..40345cc73f3 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberHolder.java @@ -0,0 +1,16 @@ +package test; + +@NumberAnnotation +public class NumberHolder extends java.util.HashSet implements Runnable { + + private NumberManager manager; + + String getStringValue(NumberManager usingManager) { + return null; + } + + public void run() throws NumberException { + } + + class MyInnerClass {} +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberManager.java b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberManager.java new file mode 100644 index 00000000000..cac3fa1b5e2 --- /dev/null +++ b/plugins/kapt3/kapt3-base/testData/runner/incremental/complex/NumberManager.java @@ -0,0 +1,16 @@ +package test; + +public class NumberManager { + + static final String CONST = "STRING_CONST"; + static final int INT_CONST = 123 + 123; + static int NOT_A_CONST = 1000; + + T[] getAllHolders() { + return null; + } + + private MyEnum getMyEnum() { + return null; + } +} \ No newline at end of file diff --git a/plugins/kapt3/kapt3-cli/src/KaptCliOption.kt b/plugins/kapt3/kapt3-cli/src/KaptCliOption.kt index aa1bf735bb4..628fb854421 100644 --- a/plugins/kapt3/kapt3-cli/src/KaptCliOption.kt +++ b/plugins/kapt3/kapt3-cli/src/KaptCliOption.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.kapt.cli import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption import org.jetbrains.kotlin.kapt.cli.CliToolOption.Format.* +import java.io.File class CliToolOption(val name: String, val format: Format) { enum class Format { @@ -69,6 +70,32 @@ enum class KaptCliOption( INCREMENTAL_DATA_OUTPUT_DIR_OPTION("incrementalData", "", "Output path for incremental data"), + CHANGED_FILES( + "changedFile", + "", + "Use only in apt mode. Changed java source file that should be processed when using incremental annotation processing.", + true + ), + + COMPILED_SOURCES_DIR( + "compiledSourcesDir", + "", + "Use only in apt mode. Compiled sources (.class files) from previous compilation. This is typically a kotlinc or javac output.", + true + ), + + INCREMENTAL_CACHE( + "incrementalCache", + "", + "Use only in apt mode. Output directory for cache necessary to support incremental annotation processing." + ), + + CLASSPATH_FQ_NAMES_HISTORY( + "classpathFqNamesHistory", + "", + "Use only in apt mode. Directory containing history of classpath fq name changes." + ), + ANNOTATION_PROCESSOR_CLASSPATH_OPTION( "apclasspath", "", diff --git a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt index 75884465025..31e083c3796 100644 --- a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt +++ b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Extension.kt @@ -216,7 +216,7 @@ abstract class AbstractKapt3Extension( private fun runAnnotationProcessing(kaptContext: KaptContext, processors: LoadedProcessors) { if (!options.mode.runAnnotationProcessing) return - val javaSourceFiles = options.collectJavaSourceFiles() + val javaSourceFiles = options.collectJavaSourceFiles(kaptContext.cacheManager) logger.info { "Java source files: " + javaSourceFiles.joinToString { it.canonicalPath } } val (annotationProcessingTime) = measureTimeMillis { diff --git a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt index b4a5372cfb2..198c6e11337 100644 --- a/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt +++ b/plugins/kapt3/kapt3-compiler/src/org/jetbrains/kotlin/kapt3/Kapt3Plugin.kt @@ -21,6 +21,7 @@ import com.intellij.openapi.project.Project import org.jetbrains.kotlin.analyzer.AnalysisResult import org.jetbrains.kotlin.base.kapt3.* import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot @@ -96,6 +97,11 @@ class Kapt3CommandLineProcessor : CommandLineProcessor { STUBS_OUTPUT_DIR_OPTION -> stubsOutputDir = File(value) INCREMENTAL_DATA_OUTPUT_DIR_OPTION -> incrementalDataOutputDir = File(value) + CHANGED_FILES -> changedFiles.addAll(value.split(File.pathSeparator).map { File(it) }) + COMPILED_SOURCES_DIR -> compiledSources.addAll(value.split(File.pathSeparator).map { File(it) }) + INCREMENTAL_CACHE -> incrementalCache = File(value) + CLASSPATH_FQ_NAMES_HISTORY -> classpathFqNamesHistory = File(value) + ANNOTATION_PROCESSOR_CLASSPATH_OPTION -> processingClasspath += File(value) ANNOTATION_PROCESSORS_OPTION -> processors.addAll(value.split(',').map { it.trim() }.filter { it.isNotEmpty() }) diff --git a/plugins/kapt3/kapt3-compiler/test/org/jetbrains/kotlin/kapt3/test/AbstractKotlinKapt3IntegrationTest.kt b/plugins/kapt3/kapt3-compiler/test/org/jetbrains/kotlin/kapt3/test/AbstractKotlinKapt3IntegrationTest.kt index 66bd700f034..34f1092ca19 100644 --- a/plugins/kapt3/kapt3-compiler/test/org/jetbrains/kotlin/kapt3/test/AbstractKotlinKapt3IntegrationTest.kt +++ b/plugins/kapt3/kapt3-compiler/test/org/jetbrains/kotlin/kapt3/test/AbstractKotlinKapt3IntegrationTest.kt @@ -28,6 +28,8 @@ import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.kapt3.* import org.jetbrains.kotlin.kapt3.base.KaptContext import org.jetbrains.kotlin.kapt3.base.LoadedProcessors +import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor import org.jetbrains.kotlin.kapt3.javac.KaptJavaFileObject import org.jetbrains.kotlin.kapt3.stubs.ClassFileToSourceStubConverter import org.jetbrains.kotlin.kapt3.stubs.ClassFileToSourceStubConverter.KaptStub @@ -171,7 +173,9 @@ abstract class AbstractKotlinKapt3IntegrationTest : CodegenTestCase() { internal var savedStubs: String? = null internal var savedBindings: Map? = null - override fun loadProcessors() = LoadedProcessors(processors, Kapt3ExtensionForTests::class.java.classLoader) + override fun loadProcessors() = LoadedProcessors( + processors.map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL) }, + Kapt3ExtensionForTests::class.java.classLoader) override fun saveStubs(kaptContext: KaptContext, stubs: List) { if (this.savedStubs != null) {