Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<IncrementalProcessor>,
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<JvmClasspathRoot>().map { it.file })
javaSourceRoots.addAll(contentRoots.filterIsInstance<JavaSourceRoot>().map { it.file })
Expand Down Expand Up @@ -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
)
}
Expand Down Expand Up @@ -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<Parameters> = ThreadLocal()
}

data class Parameters(
val processors: List<IncrementalProcessor>,
val kaptOptions: KaptOptions.Builder
)
}
42 changes: 33 additions & 9 deletions src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class KotlinCompilation {
*/
var pluginClasspaths: List<File> = emptyList()

/**
* Compiler plugins that should be added the compilation
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

*/
var compilerPlugins: List<ComponentRegistrar> = emptyList()

/** Source files to be compiled */
var sources: List<SourceFile> = emptyList()

Expand Down Expand Up @@ -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
)
)

Expand Down Expand Up @@ -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<File>): 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>(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(
Expand Down Expand Up @@ -709,7 +733,7 @@ class KotlinCompilation {
return makeResult(exitCode)
}
} finally {
KaptComponentRegistrar.threadLocalParameters.remove()
MainComponentRegistrar.threadLocalParameters.remove()
}

// step 3: compile Kotlin files
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read a few days ago in the Kotlin slack that the order of registering compiler plugins matters. Right now we are registering Kapt first. Could there be a problem here? For example if a compiler plugin like arrow-meta introduces some new syntax/type checking that isn't legal in vanilla Kotlin the respective plugin would have to be run before Kapt or Kapt wouldn't know how to deal with that. Any thoughts on this @raulraja?


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<Parameters> = ThreadLocal()
}

data class Parameters(
val processors: List<IncrementalProcessor>,
val kaptOptions: KaptOptions.Builder,
val compilerPlugins: List<ComponentRegistrar>
)
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
com.tschuchort.compiletesting.KaptComponentRegistrar
com.tschuchort.compiletesting.MainComponentRegistrar
Original file line number Diff line number Diff line change
@@ -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<String> = setOf(ProcessElem::class.java.canonicalName)

override fun process(p0: MutableSet<out TypeElement>?, 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ class KotlinCompilationTests {
}
}


@Test
fun `Kotlin AP sees Java class`() {
val jSource = SourceFile.java(
Expand Down
38 changes: 38 additions & 0 deletions src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -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<Nothing>("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) }
}