diff --git a/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt b/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt index e807ac4a..e889e93d 100644 --- a/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt +++ b/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt @@ -47,17 +47,19 @@ import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension import java.io.File -import javax.annotation.processing.Processor -internal class KaptComponentRegistrar : ComponentRegistrar { +internal class KaptComponentRegistrar( + private val processors: List, + private val kaptOptions: KaptOptions.Builder +) : ComponentRegistrar { override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { - if (threadLocalParameters.get().processors.isEmpty()) + if (processors.isEmpty()) return val contentRoots = configuration[CLIConfigurationKeys.CONTENT_ROOTS] ?: emptyList() - val optionsBuilder = threadLocalParameters.get().kaptOptions.apply { + val optionsBuilder = kaptOptions.apply { projectBaseDir = project.basePath?.let(::File) compileClasspath.addAll(contentRoots.filterIsInstance().map { it.file }) javaSourceRoots.addAll(contentRoots.filterIsInstance().map { it.file }) @@ -88,7 +90,7 @@ internal class KaptComponentRegistrar : ComponentRegistrar { val kapt3AnalysisCompletedHandlerExtension = object : AbstractKapt3Extension(options, logger, configuration) { override fun loadProcessors() = LoadedProcessors( - processors = threadLocalParameters.get().processors, + processors = processors, classLoader = this::class.java.classLoader ) } @@ -161,17 +163,4 @@ internal class KaptComponentRegistrar : ComponentRegistrar { } } - companion object { - /** This kapt compiler plugin is instantiated by K2JVMCompiler using - * a service locator. So we can't just pass parameters to it easily. - * Instead we need to use a thread-local global variable to pass - * any parameters that change between compilations - */ - val threadLocalParameters: ThreadLocal = ThreadLocal() - } - - data class Parameters( - val processors: List, - val kaptOptions: KaptOptions.Builder - ) } \ No newline at end of file diff --git a/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt b/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt index 71d75222..1be2a43d 100644 --- a/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt +++ b/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt @@ -70,6 +70,11 @@ class KotlinCompilation { */ var pluginClasspaths: List = emptyList() + /** + * Compiler plugins that should be added the compilation + */ + var compilerPlugins: List = emptyList() + /** Source files to be compiled */ var sources: List = emptyList() @@ -436,16 +441,18 @@ class KotlinCompilation { it.flags.addAll(KaptFlag.MAP_DIAGNOSTIC_LOCATIONS, KaptFlag.VERBOSE) } - /* The kapt compiler plugin (KaptComponentRegistrar) + /** The main compiler plugin (MainComponentRegistrar) * is instantiated by K2JVMCompiler using * a service locator. So we can't just pass parameters to it easily. * Instead we need to use a thread-local global variable to pass * any parameters that change between compilations + * */ - KaptComponentRegistrar.threadLocalParameters.set( - KaptComponentRegistrar.Parameters( + MainComponentRegistrar.threadLocalParameters.set( + MainComponentRegistrar.Parameters( annotationProcessors.map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL) }, - kaptOptions + kaptOptions, + compilerPlugins ) ) @@ -516,23 +523,40 @@ class KotlinCompilation { } .find { resourcesPath -> ServiceLoaderLite.findImplementations(ComponentRegistrar::class.java, listOf(resourcesPath.toFile())) - .any { implementation -> implementation == KaptComponentRegistrar::class.java.name } + .any { implementation -> implementation == MainComponentRegistrar::class.java.name } }?.toString() ?: throw AssertionError("Could not get path to ComponentRegistrar service from META-INF") } /** Performs the 3rd compilation step to compile Kotlin source files */ private fun compileKotlin(sourceFiles: List): ExitCode { + + /** + * Here the list of compiler plugins is set + * + * To avoid that the annotation processors are executed twice, + * the list is set to empty + */ + MainComponentRegistrar.threadLocalParameters.set( + MainComponentRegistrar.Parameters( + listOf(), + KaptOptions.Builder(), + compilerPlugins + ) + ) + + val sources = sourceFiles + kaptKotlinGeneratedDir.listFilesRecursively() + kaptSourceDir.listFilesRecursively() // if no Kotlin sources are available, skip the compileKotlin step - if(sources.filter(File::hasKotlinFileExtension).isEmpty()) + if(sources.none(File::hasKotlinFileExtension)) return ExitCode.OK // in this step also include source files generated by kapt in the previous step - val k2JvmArgs = commonK2JVMArgs().also { - it.freeArgs = sources.map(File::getAbsolutePath).distinct() + val k2JvmArgs = commonK2JVMArgs().also {jvmArgs-> + jvmArgs.pluginClasspaths = (jvmArgs.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath()) + jvmArgs.freeArgs = sources.map(File::getAbsolutePath).distinct() } val compilerMessageCollector = PrintingMessageCollector( @@ -709,7 +733,7 @@ class KotlinCompilation { return makeResult(exitCode) } } finally { - KaptComponentRegistrar.threadLocalParameters.remove() + MainComponentRegistrar.threadLocalParameters.remove() } // step 3: compile Kotlin files diff --git a/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt b/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt new file mode 100644 index 00000000..40449eea --- /dev/null +++ b/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tschuchort.compiletesting + +import org.jetbrains.kotlin.base.kapt3.KaptOptions +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor + +internal class MainComponentRegistrar : ComponentRegistrar { + + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + val parameters = threadLocalParameters.get() + KaptComponentRegistrar(parameters.processors,parameters.kaptOptions).registerProjectComponents(project,configuration) + + parameters.compilerPlugins.forEach { componentRegistrar-> + componentRegistrar.registerProjectComponents(project,configuration) + } + } + + companion object { + /** This compiler plugin is instantiated by K2JVMCompiler using + * a service locator. So we can't just pass parameters to it easily. + * Instead we need to use a thread-local global variable to pass + * any parameters that change between compilations + */ + val threadLocalParameters: ThreadLocal = ThreadLocal() + } + + data class Parameters( + val processors: List, + val kaptOptions: KaptOptions.Builder, + val compilerPlugins: List + ) +} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar b/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar index 5997746c..80e59f28 100644 --- a/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +++ b/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar @@ -1 +1 @@ -com.tschuchort.compiletesting.KaptComponentRegistrar \ No newline at end of file +com.tschuchort.compiletesting.MainComponentRegistrar \ No newline at end of file diff --git a/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt b/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt new file mode 100644 index 00000000..4d590c53 --- /dev/null +++ b/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt @@ -0,0 +1,71 @@ +package com.tschuchort.compiletesting + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.atLeastOnce +import com.nhaarman.mockitokotlin2.verify +import org.assertj.core.api.Assertions +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.element.TypeElement + +class CompilerPluginsTest { + + @Test + fun `when compiler plugins are added they get executed`() { + + val mockPlugin = Mockito.mock(ComponentRegistrar::class.java) + + val result = defaultCompilerConfig().apply { + sources = listOf(SourceFile.new("emptyKotlinFile.kt", "")) + compilerPlugins = listOf(mockPlugin) + inheritClassPath = true + }.compile() + + verify(mockPlugin, atLeastOnce()).registerProjectComponents(any(), any()) + + Assertions.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } + + @Test + fun `when compiler plugins and annotation processors are added they get executed`() { + + val annotationProcessor = object : AbstractProcessor() { + override fun getSupportedAnnotationTypes(): Set = setOf(ProcessElem::class.java.canonicalName) + + override fun process(p0: MutableSet?, p1: RoundEnvironment?): Boolean { + p1?.getElementsAnnotatedWith(ProcessElem::class.java)?.forEach { + Assert.assertEquals("JSource", it?.simpleName.toString()) + } + return false + } + } + + val mockPlugin = Mockito.mock(ComponentRegistrar::class.java) + + val jSource = SourceFile.kotlin( + "JSource.kt", """ + package com.tschuchort.compiletesting; + + @ProcessElem + class JSource { + fun foo() { } + } + """ + ) + + val result = defaultCompilerConfig().apply { + sources = listOf(jSource) + annotationProcessors = listOf(annotationProcessor) + compilerPlugins = listOf(mockPlugin) + inheritClassPath = true + }.compile() + + verify(mockPlugin, atLeastOnce()).registerProjectComponents(any(), any()) + + Assertions.assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt b/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt index 4232b767..76a0de5e 100644 --- a/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt +++ b/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt @@ -464,6 +464,7 @@ class KotlinCompilationTests { } } + @Test fun `Kotlin AP sees Java class`() { val jSource = SourceFile.java( diff --git a/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt b/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt new file mode 100644 index 00000000..5bb7b09c --- /dev/null +++ b/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt @@ -0,0 +1,38 @@ +package com.tschuchort.compiletesting + +import io.github.classgraph.ClassGraph +import org.assertj.core.api.Assertions +import java.io.File + +fun defaultCompilerConfig(): KotlinCompilation { + return KotlinCompilation( ).apply { + inheritClassPath = false + skipRuntimeVersionCheck = true + correctErrorTypes = true + verbose = true + reportOutputFiles = false + messageOutputStream = System.out + } +} + + +fun assertClassLoadable(compileResult: KotlinCompilation.Result, className: String): Class<*> { + try { + val clazz = compileResult.classLoader.loadClass(className) + Assertions.assertThat(clazz).isNotNull + return clazz + } + catch(e: ClassNotFoundException) { + return Assertions.fail("Class $className could not be loaded") + } +} + +/** + * Returns the classpath for a dependency (format $name-$version). + * This is necessary to know the actual location of a dependency + * which has been included in test runtime (build.gradle). + */ +fun classpathOf(dependency: String): File { + val regex = Regex(".*$dependency\\.jar") + return ClassGraph().classpathFiles.first { classpath -> classpath.name.matches(regex) } +} \ No newline at end of file