diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c033eb9c..7d83977a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,32 @@ -name: Continuous Integration +name: CI -on: [ push, pull_request, workflow_dispatch ] +on: + # Only run push on main + push: + branches: + - main + paths-ignore: + - '**/*.md' + # Always run on PRs + pull_request: + branches: [ main ] + +concurrency: + group: 'ci-${{ github.event.merge_group.head_ref || github.head_ref }}-${{ github.workflow }}' + cancel-in-progress: true jobs: build: - name: Build + name: "${{ matrix.platform }}" runs-on: ${{ matrix.platform }} strategy: + fail-fast: false matrix: - platform: [ 'windows-latest', 'ubuntu-latest' ] + platform: [ 'windows-latest', 'ubuntu-latest', 'macos-latest' ] steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/actions/wrapper-validation@v3 - name: Set up JDK uses: actions/setup-java@v4 diff --git a/.gitignore b/.gitignore index eb5b5e08..ad67a397 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,7 @@ tags .idea/**/gradle.xml .idea/**/libraries .gradle/ +.kotlin/ # CMake cmake-build-debug/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 00049115..5d66f3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Changelog ========= +0.5.0 +----- + +_2024-06-05_ + +- Update to Kotlin `2.0.0`. +- Update to KSP `2.0.0-1.0.22`. +- Change `supportsK2` to true by default. +- Change `disableStandardScript` to true by default. This doesn't seem to work reliably in K2 testing. +- Update kapt class location references. +- Support Kapt4 (AKA kapt.k2). +- Support KSP2. +- Introduce a new `KotlinCompilation.useKsp()` API to simplify KSP configuration. +- Update to ClassGraph `4.8.173`. + +Note that in order to test Kapt 3 or KSP 1, you must now also set `languageVersion` to `1.9` in your `KotlinCompilation` configuration. + 0.4.1 ----- @@ -12,7 +29,7 @@ _2024-03-25_ - Update to classgraph `4.8.168`. - Update to Okio `3.9.0`. -Special thanks to [@jbarr21](https://github.com/jbarr21). +Special thanks to [@jbarr21](https://github.com/jbarr21) for contributing to this release! 0.4.0 ----- diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 016a4778..faa1dce8 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,12 +19,9 @@ buildConfig { } dependencies { - implementation(libs.autoService) ksp(libs.autoService.ksp) - testImplementation(libs.kotlinpoet) - testImplementation(libs.javapoet) - + implementation(libs.autoService) implementation(libs.okio) implementation(libs.classgraph) @@ -34,10 +31,12 @@ dependencies { testRuntimeOnly(libs.intellij.core) testRuntimeOnly(libs.intellij.util) - // The Kotlin compiler should be near the end of the list because its .jar file includes - // an obsolete version of Guava api(libs.kotlin.compilerEmbeddable) api(libs.kotlin.annotationProcessingEmbeddable) + api(libs.kotlin.kapt4) + + testImplementation(libs.kotlinpoet) + testImplementation(libs.javapoet) testImplementation(libs.kotlin.junit) testImplementation(libs.mockito) testImplementation(libs.mockitoKotlin) @@ -47,13 +46,12 @@ dependencies { tasks.withType().configureEach { val isTest = name.contains("test", ignoreCase = true) compilerOptions { - freeCompilerArgs.addAll( + freeCompilerArgs.add( // https://github.com/tschuchortdev/kotlin-compile-testing/pull/63 - "-Xno-optimized-callable-references", - "-Xskip-runtime-version-check", + "-Xno-optimized-callable-references" ) if (isTest) { - freeCompilerArgs.add("-opt-in=org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") + optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } } } diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt index e881800d..a322eaff 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt @@ -1,7 +1,14 @@ package com.tschuchort.compiletesting +import java.io.File +import java.io.OutputStream +import java.io.PrintStream +import java.net.URI +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths import okio.Buffer -import org.jetbrains.kotlin.base.kapt3.KaptOptions import org.jetbrains.kotlin.cli.common.CLICompiler import org.jetbrains.kotlin.cli.common.ExitCode import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments @@ -9,21 +16,13 @@ import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments import org.jetbrains.kotlin.cli.common.arguments.validateArguments import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector -import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.Services +import org.jetbrains.kotlin.kapt3.base.KaptOptions import org.jetbrains.kotlin.util.ServiceLoaderLite -import java.io.File -import java.io.OutputStream -import java.io.PrintStream -import java.net.URI -import java.net.URL -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths /** * Base compilation class for sharing common compiler arguments and @@ -102,6 +101,7 @@ abstract class AbstractKotlinCompilation internal c var reportPerformance: Boolean = false var languageVersion: String? = null + var apiVersion: String? = null /** Enable experimental multiplatform support */ var multiplatform: Boolean = false @@ -128,10 +128,20 @@ abstract class AbstractKotlinCompilation internal c } /** Enable support for the new K2 compiler. */ - var supportsK2 = false + var supportsK2 = true /** Disables compiler scripting support. */ - var disableStandardScript = false + var disableStandardScript = true + + /** Tools that need to run before the compilation. */ + val precursorTools: MutableMap = mutableMapOf() + + protected val extraGeneratedSources = mutableListOf() + + /** Registers extra directories with generated sources, such as sources generated by KSP. */ + fun registerGeneratedSourcesDir(dir: File) { + extraGeneratedSources.add(dir) + } // Directory for input source files protected val sourcesDir get() = workingDir.resolve("sources") @@ -153,6 +163,10 @@ abstract class AbstractKotlinCompilation internal c args.languageVersion = this.languageVersion } + if (apiVersion != null) { + args.apiVersion = this.apiVersion + } + configuration(args) /** @@ -188,6 +202,26 @@ abstract class AbstractKotlinCompilation internal c /** Performs the compilation step to compile Kotlin source files */ protected fun compileKotlin(sources: List, compiler: CLICompiler, arguments: A): KotlinCompilation.ExitCode { + if (this is KotlinCompilation) { + // Execute precursor tools first + for (tool in precursorTools) { + val exitCode = try { + tool.value.execute(this, internalMessageStream, sources) + } catch (t: Throwable) { + t.message?.let { internalMessageStream.println(it) } + t.printStackTrace(internalMessageStream) + KotlinCompilation.ExitCode.INTERNAL_ERROR + } + if (exitCode != KotlinCompilation.ExitCode.OK) { + return exitCode + } + } + } + + // Update sources to include any generated in a precursor tool + val generatedSourceFiles = extraGeneratedSources.flatMap(File::listFilesRecursively).filter(File::hasKotlinFileExtension) + .map { it.absoluteFile } + val finalSources = sources + generatedSourceFiles /** * Here the list of compiler plugins is set @@ -208,7 +242,7 @@ abstract class AbstractKotlinCompilation internal c // in this step also include source files generated by kapt in the previous step val args = arguments.also { args -> args.freeArgs = - sources.map(File::getAbsolutePath).distinct() + if (sources.none(File::hasKotlinFileExtension)) { + finalSources.map(File::getAbsolutePath).distinct() + if (finalSources.none(File::hasKotlinFileExtension)) { /* __HACK__: The Kotlin compiler expects at least one Kotlin source file or it will crash, so we trick the compiler by just including an empty .kt-File. We need the compiler to run even if there are no Kotlin files because some compiler plugins may also process Java files. */ diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt index 9fae4a6b..327fbcba 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt @@ -1,8 +1,8 @@ package com.tschuchort.compiletesting +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import java.io.File import java.net.URLClassLoader -import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi /** Result of the compilation. */ @ExperimentalCompilerApi diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/JavacUtils.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/JavacUtils.kt index b2f3343d..6c99c292 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/JavacUtils.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/JavacUtils.kt @@ -7,7 +7,6 @@ import java.nio.charset.Charset import javax.tools.JavaCompiler import javax.tools.JavaFileObject import javax.tools.SimpleJavaFileObject -import kotlin.IllegalArgumentException /** * A [JavaFileObject] created from a source [File]. diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt index 5fd09322..9edda345 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt @@ -17,10 +17,6 @@ package com.tschuchort.compiletesting import org.jetbrains.kotlin.analyzer.AnalysisResult -import org.jetbrains.kotlin.base.kapt3.AptMode -import org.jetbrains.kotlin.base.kapt3.KaptFlag -import org.jetbrains.kotlin.base.kapt3.KaptOptions -import org.jetbrains.kotlin.base.kapt3.logString import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector @@ -39,15 +35,13 @@ import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor import org.jetbrains.kotlin.kapt3.AbstractKapt3Extension import org.jetbrains.kotlin.kapt3.Kapt3ComponentRegistrar -import org.jetbrains.kotlin.kapt3.base.Kapt -import org.jetbrains.kotlin.kapt3.base.LoadedProcessors +import org.jetbrains.kotlin.kapt3.base.* import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor import org.jetbrains.kotlin.kapt3.base.util.KaptLogger import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.BindingTrace import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension -import org.jetbrains.kotlin.resolve.jvm.extensions.PartialAnalysisHandlerExtension import java.io.File @ExperimentalCompilerApi diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt index 2265a03e..c6b3af04 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt @@ -17,9 +17,14 @@ package com.tschuchort.compiletesting import com.facebook.buck.jvm.java.javax.SynchronizedToolProvider -import org.jetbrains.kotlin.base.kapt3.AptMode -import org.jetbrains.kotlin.base.kapt3.KaptFlag -import org.jetbrains.kotlin.base.kapt3.KaptOptions +import com.tschuchort.compiletesting.kapt.toPluginOptions +import java.io.File +import java.io.OutputStreamWriter +import java.nio.file.Path +import javax.annotation.processing.Processor +import javax.tools.Diagnostic +import javax.tools.DiagnosticCollector +import javax.tools.JavaFileObject import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector @@ -29,647 +34,631 @@ import org.jetbrains.kotlin.config.JVMAssertionsMode import org.jetbrains.kotlin.config.JvmDefaultMode import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.config.Services +import org.jetbrains.kotlin.kapt3.base.AptMode +import org.jetbrains.kotlin.kapt3.base.KaptFlag +import org.jetbrains.kotlin.kapt3.base.KaptOptions import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger -import java.io.File -import java.io.OutputStreamWriter -import java.net.URLClassLoader -import java.nio.file.Path -import javax.annotation.processing.Processor -import javax.tools.Diagnostic -import javax.tools.DiagnosticCollector -import javax.tools.JavaFileObject +import org.jetbrains.kotlin.kapt4.Kapt4CompilerPluginRegistrar -data class PluginOption(val pluginId: PluginId, val optionName: OptionName, val optionValue: OptionValue) +data class PluginOption( + val pluginId: PluginId, + val optionName: OptionName, + val optionValue: OptionValue, +) typealias PluginId = String + typealias OptionName = String + typealias OptionValue = String @ExperimentalCompilerApi @Suppress("MemberVisibilityCanBePrivate") class KotlinCompilation : AbstractKotlinCompilation() { - /** Arbitrary arguments to be passed to kapt */ - var kaptArgs: MutableMap = mutableMapOf() + /** Arbitrary arguments to be passed to kapt */ + var kaptArgs: MutableMap = mutableMapOf() + + /** Arbitrary flags to be passed to kapt */ + var kaptFlags: MutableSet = mutableSetOf() + + /** Enables the new Kapt 4 impl for K2 support. */ + var useKapt4: Boolean? = null - /** Arbitrary flags to be passed to kapt */ - var kaptFlags: MutableSet = mutableSetOf() + /** Annotation processors to be passed to kapt */ + var annotationProcessors: List = emptyList() - /** Annotation processors to be passed to kapt */ - var annotationProcessors: List = emptyList() + /** Include Kotlin runtime in to resulting .jar */ + var includeRuntime: Boolean = false - /** Include Kotlin runtime in to resulting .jar */ - var includeRuntime: Boolean = false + /** Make kapt correct error types */ + var correctErrorTypes: Boolean = true - /** Make kapt correct error types */ - var correctErrorTypes: Boolean = true + /** Name of the generated .kotlin_module file */ + var moduleName: String? = null - /** Name of the generated .kotlin_module file */ - var moduleName: String? = null + /** Target version of the generated JVM bytecode */ + var jvmTarget: String = JvmTarget.DEFAULT.description - /** Target version of the generated JVM bytecode */ - var jvmTarget: String = JvmTarget.DEFAULT.description + /** Generate metadata for Java 1.8 reflection on method parameters */ + var javaParameters: Boolean = false - /** Generate metadata for Java 1.8 reflection on method parameters */ - var javaParameters: Boolean = false + /** Use the old JVM backend */ + var useOldBackend: Boolean = false - /** Use the old JVM backend */ - var useOldBackend: Boolean = false + /** Paths where to find Java 9+ modules */ + var javaModulePath: Path? = null - /** Paths where to find Java 9+ modules */ - var javaModulePath: Path? = null + /** + * Root modules to resolve in addition to the initial modules, or all modules on the module path + * if is ALL-MODULE-PATH + */ + var additionalJavaModules: MutableList = mutableListOf() - /** - * Root modules to resolve in addition to the initial modules, - * or all modules on the module path if is ALL-MODULE-PATH - */ - var additionalJavaModules: MutableList = mutableListOf() + /** Don't generate not-null assertions for arguments of platform types */ + var noCallAssertions: Boolean = false - /** Don't generate not-null assertions for arguments of platform types */ - var noCallAssertions: Boolean = false + /** Don't generate not-null assertion for extension receiver arguments of platform types */ + var noReceiverAssertions: Boolean = false - /** Don't generate not-null assertion for extension receiver arguments of platform types */ - var noReceiverAssertions: Boolean = false + /** Don't generate not-null assertions on parameters of methods accessible from Java */ + var noParamAssertions: Boolean = false - /** Don't generate not-null assertions on parameters of methods accessible from Java */ - var noParamAssertions: Boolean = false + /** Generate nullability assertions for non-null Java expressions */ + @Deprecated("Removed in Kotlinc, this does nothing now.") + var strictJavaNullabilityAssertions: Boolean? = null + + /** Disable optimizations */ + var noOptimize: Boolean = false - /** Generate nullability assertions for non-null Java expressions */ - @Deprecated("Removed in Kotlinc, this does nothing now.") - var strictJavaNullabilityAssertions: Boolean? = null - - /** Disable optimizations */ - var noOptimize: Boolean = false - - /** - * Normalize constructor calls (disable: don't normalize; enable: normalize), - * default is 'disable' in language version 1.2 and below, 'enable' since language version 1.3 - * - * {disable|enable} - */ - @Deprecated("Removed in Kotlinc, this does nothing now.") - var constructorCallNormalizationMode: String? = null - - /** Assert calls behaviour {always-enable|always-disable|jvm|legacy} */ - var assertionsMode: String? = JVMAssertionsMode.DEFAULT.description - - /** Path to the .xml build file to compile */ - var buildFile: File? = null - - /** Compile multifile classes as a hierarchy of parts and facade */ - var inheritMultifileParts: Boolean = false - - /** Use type table in metadata serialization */ - var useTypeTable: Boolean = false - - /** Allow Kotlin runtime libraries of incompatible versions in the classpath */ - @Deprecated("Removed in Kotlinc, this does nothing now.") - var skipRuntimeVersionCheck: Boolean? = null - - /** Path to JSON file to dump Java to Kotlin declaration mappings */ - var declarationsOutputPath: File? = null - - /** Combine modules for source files and binary dependencies into a single module */ - @Deprecated("Removed in Kotlinc, this does nothing now.") - var singleModule: Boolean = false - - /** Suppress the \"cannot access built-in declaration\" error (useful with -no-stdlib) */ - var suppressMissingBuiltinsError: Boolean = false - - /** Script resolver environment in key-value pairs (the value could be quoted and escaped) */ - var scriptResolverEnvironment: MutableMap = mutableMapOf() - - /** Java compiler arguments */ - var javacArguments: MutableList = mutableListOf() - - /** Package prefix for Java files */ - var javaPackagePrefix: String? = null - - /** - * Specify behavior for Checker Framework compatqual annotations (NullableDecl/NonNullDecl). - * Default value is 'enable' - */ - var supportCompatqualCheckerFrameworkAnnotations: String? = null - - /** Do not throw NPE on explicit 'equals' call for null receiver of platform boxed primitive type */ - @Deprecated("Removed in Kotlinc, this does nothing now.") - var noExceptionOnExplicitEqualsForBoxedNull: Boolean? = null - - /** Allow to use '@JvmDefault' annotation for JVM default method support. - * {disable|enable|compatibility} - * */ - var jvmDefault: String = JvmDefaultMode.DEFAULT.description - - /** Generate metadata with strict version semantics (see kdoc on Metadata.extraInt) */ - var strictMetadataVersionSemantics: Boolean = false - - /** - * Transform '(' and ')' in method names to some other character sequence. - * This mode can BREAK BINARY COMPATIBILITY and is only supposed to be used as a workaround - * of an issue in the ASM bytecode framework. See KT-29475 for more details - */ - var sanitizeParentheses: Boolean = false - - /** Paths to output directories for friend modules (whose internals should be visible) */ - var friendPaths: List = emptyList() - - /** - * Path to the JDK to be used - * - * If null, no JDK will be used with kotlinc (option -no-jdk) - * and the system java compiler will be used with empty bootclasspath - * (on JDK8) or --system none (on JDK9+). This can be useful if all - * the JDK classes you need are already on the (inherited) classpath. - * */ - var jdkHome: File? by default { processJdkHome } - - /** - * Path to the kotlin-stdlib.jar - * If none is given, it will be searched for in the host - * process' classpaths - */ - var kotlinStdLibJar: File? by default { - HostEnvironment.kotlinStdLibJar - } - - /** - * Path to the kotlin-stdlib-jdk*.jar - * If none is given, it will be searched for in the host - * process' classpaths - */ - var kotlinStdLibJdkJar: File? by default { - HostEnvironment.kotlinStdLibJdkJar - } - - /** - * Path to the kotlin-reflect.jar - * If none is given, it will be searched for in the host - * process' classpaths - */ - var kotlinReflectJar: File? by default { - HostEnvironment.kotlinReflectJar - } - - /** - * Path to the kotlin-script-runtime.jar - * If none is given, it will be searched for in the host - * process' classpaths - */ - var kotlinScriptRuntimeJar: File? by default { - HostEnvironment.kotlinScriptRuntimeJar - } - - /** - * Path to the tools.jar file needed for kapt when using a JDK 8. - * - * Note: Using a tools.jar file with a JDK 9 or later leads to an - * internal compiler error! - */ - var toolsJar: File? by default { - if (!isJdk9OrLater()) - jdkHome?.let { findToolsJarFromJdk(it) } - ?: HostEnvironment.toolsJar - else - null - } - - // *.class files, Jars and resources (non-temporary) that are created by the - // compilation will land here - val classesDir get() = workingDir.resolve("classes") - - // Base directory for kapt stuff - private val kaptBaseDir get() = workingDir.resolve("kapt") - - // Java annotation processors that are compile by kapt will put their generated files here - val kaptSourceDir get() = kaptBaseDir.resolve("sources") - - // Output directory for Kotlin source files generated by kapt - val kaptKotlinGeneratedDir get() = kaptArgs[OPTION_KAPT_KOTLIN_GENERATED] - ?.let { path -> - require(File(path).isDirectory) { "$OPTION_KAPT_KOTLIN_GENERATED must be a directory" } - File(path) - } - ?: File(kaptBaseDir, "kotlinGenerated") - - val kaptStubsDir get() = kaptBaseDir.resolve("stubs") - val kaptIncrementalDataDir get() = kaptBaseDir.resolve("incrementalData") - - private val extraGeneratedSources = mutableListOf() - - /** Registers extra directories with generated sources, such as sources generated by KSP. */ - fun registerGeneratedSourcesDir(dir: File) { - extraGeneratedSources.add(dir) - } - - /** ExitCode of the entire Kotlin compilation process */ - enum class ExitCode { - OK, INTERNAL_ERROR, COMPILATION_ERROR, SCRIPT_EXECUTION_ERROR - } - - // setup common arguments for the two kotlinc calls - private fun commonK2JVMArgs() = commonArguments(K2JVMCompilerArguments()) { args -> - args.destination = classesDir.absolutePath - args.classpath = commonClasspaths().joinToString(separator = File.pathSeparator) - - if(jdkHome != null) { - args.jdkHome = jdkHome!!.absolutePath - } - else { - log("Using option -no-jdk. Kotlinc won't look for a JDK.") - args.noJdk = true - } - - args.includeRuntime = includeRuntime - - // the compiler should never look for stdlib or reflect in the - // kotlinHome directory (which is null anyway). We will put them - // in the classpath manually if they're needed - args.noStdlib = true - args.noReflect = true - - if(moduleName != null) - args.moduleName = moduleName - - args.jvmTarget = jvmTarget - args.javaParameters = javaParameters - args.useOldBackend = useOldBackend - - if(javaModulePath != null) - args.javaModulePath = javaModulePath!!.toString() - - args.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray() - args.noCallAssertions = noCallAssertions - args.noParamAssertions = noParamAssertions - args.noReceiverAssertions = noReceiverAssertions - - args.noOptimize = noOptimize - - if(assertionsMode != null) - args.assertionsMode = assertionsMode - - if(buildFile != null) - args.buildFile = buildFile!!.toString() - - args.inheritMultifileParts = inheritMultifileParts - args.useTypeTable = useTypeTable - - if(declarationsOutputPath != null) - args.declarationsOutputPath = declarationsOutputPath!!.toString() - - if(javacArguments.isNotEmpty()) - args.javacArguments = javacArguments.toTypedArray() - - if(supportCompatqualCheckerFrameworkAnnotations != null) - args.supportCompatqualCheckerFrameworkAnnotations = supportCompatqualCheckerFrameworkAnnotations - - args.jvmDefault = jvmDefault - args.strictMetadataVersionSemantics = strictMetadataVersionSemantics - args.sanitizeParentheses = sanitizeParentheses - - if(friendPaths.isNotEmpty()) - args.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray() - - if(scriptResolverEnvironment.isNotEmpty()) - args.scriptResolverEnvironment = scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray() - - args.javaPackagePrefix = javaPackagePrefix - args.suppressMissingBuiltinsError = suppressMissingBuiltinsError - args.disableStandardScript = disableStandardScript - } - - /** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */ - private fun stubsAndApt(sourceFiles: List): ExitCode { - if(annotationProcessors.isEmpty()) { - log("No services were given. Not running kapt steps.") - return ExitCode.OK - } - - val kaptOptions = KaptOptions.Builder().also { - it.stubsOutputDir = kaptStubsDir - it.sourcesOutputDir = kaptSourceDir - it.incrementalDataOutputDir = kaptIncrementalDataDir - it.classesOutputDir = classesDir - it.processingOptions.apply { - putAll(kaptArgs) - putIfAbsent(OPTION_KAPT_KOTLIN_GENERATED, kaptKotlinGeneratedDir.absolutePath) - } - - it.mode = AptMode.STUBS_AND_APT - - it.flags.apply { - addAll(kaptFlags) - - if (verbose) { - addAll(KaptFlag.MAP_DIAGNOSTIC_LOCATIONS, KaptFlag.VERBOSE) - } - } - } - - val compilerMessageCollector = PrintingMessageCollector( - internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose - ) - - val kaptLogger = MessageCollectorBackedKaptLogger(kaptOptions.build(), compilerMessageCollector) - - /** 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 - * - */ - MainComponentRegistrar.threadLocalParameters.set( - MainComponentRegistrar.ThreadLocalParameters( - annotationProcessors.map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL, kaptLogger) }, - kaptOptions, - componentRegistrars, - compilerPluginRegistrars, - supportsK2, - ) - ) - - val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension) - val javaSources = sourceFiles.filter(File::hasJavaFileExtension) - - val sourcePaths = mutableListOf().apply { - addAll(javaSources) - - if(kotlinSources.isNotEmpty()) { - addAll(kotlinSources) - } - else { - /* __HACK__: The K2JVMCompiler expects at least one Kotlin source file or it will crash. - We still need kapt to run even if there are no Kotlin sources because it executes APs - on Java sources as well. Alternatively we could call the JavaCompiler instead of kapt - to do annotation processing when there are only Java sources, but that's quite a lot - of work (It can not be done in the compileJava step because annotation processors on - Java files might generate Kotlin files which then need to be compiled in the - compileKotlin step before the compileJava step). So instead we trick K2JVMCompiler - by just including an empty .kt-File. */ - add(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(kaptBaseDir)) - } - }.map(File::getAbsolutePath).distinct() - - if(!isJdk9OrLater()) { - try { - Class.forName("com.sun.tools.javac.util.Context") - } - catch (e: ClassNotFoundException) { - require(toolsJar != null) { - "toolsJar must not be null on JDK 8 or earlier if it's classes aren't already on the classpath" - } - - require(toolsJar!!.exists()) { "toolsJar file does not exist" } - (ClassLoader.getSystemClassLoader() as URLClassLoader).addUrl(toolsJar!!.toURI().toURL()) - } - } - - if (pluginClasspaths.isNotEmpty()) - warn("Included plugins in pluginsClasspaths will be executed twice.") - - val k2JvmArgs = commonK2JVMArgs().also { - it.freeArgs = sourcePaths - it.pluginClasspaths = (it.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath()) - } - - return convertKotlinExitCode( - K2JVMCompiler().exec(compilerMessageCollector, Services.EMPTY, k2JvmArgs) - ) - } - - /** Performs the 3rd compilation step to compile Kotlin source files */ - private fun compileJvmKotlin(sourceFiles: List): ExitCode { - val sources = sourceFiles + - kaptKotlinGeneratedDir.listFilesRecursively() + - kaptSourceDir.listFilesRecursively() - - return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs()) - } - - /** - * Base javac arguments that only depend on the the arguments given by the user - * Depending on which compiler implementation is actually used, more arguments - * may be added - */ - private fun baseJavacArgs(isJavac9OrLater: Boolean) = mutableListOf().apply { - if(verbose) { - add("-verbose") - add("-Xlint:path") // warn about invalid paths in CLI - add("-Xlint:options") // warn about invalid options in CLI - - if(isJavac9OrLater) - add("-Xlint:module") // warn about issues with the module system - } - - addAll("-d", classesDir.absolutePath) - - add("-proc:none") // disable annotation processing - - if(allWarningsAsErrors) - add("-Werror") - - addAll(javacArguments) - - // also add class output path to javac classpath so it can discover - // already compiled Kotlin classes - addAll("-cp", (commonClasspaths() + classesDir) - .joinToString(File.pathSeparator, transform = File::getAbsolutePath)) - } - - /** Performs the 4th compilation step to compile Java source files */ - private fun compileJava(sourceFiles: List): ExitCode { - val javaSources = sourceFiles - .plus(kaptSourceDir.listFilesRecursively()) - .plus(extraGeneratedSources.flatMap(File::listFilesRecursively)) - .distinct() - .filterNot(File::hasKotlinFileExtension) - - if(javaSources.isEmpty()) - return ExitCode.OK - - if(jdkHome != null && jdkHome!!.canonicalPath != processJdkHome.canonicalPath) { - /* If a JDK home is given, try to run javac from there so it uses the same JDK - as K2JVMCompiler. Changing the JDK of the system java compiler via the - "--system" and "-bootclasspath" options is not so easy. - If the jdkHome is the same as the current process, we still run an in process compilation because it is - expensive to fork a process to compile. - */ - log("compiling java in a sub-process because a jdkHome is specified") - val jdkBinFile = File(jdkHome, "bin") - check(jdkBinFile.exists()) { "No JDK bin folder found at: ${jdkBinFile.toPath()}" } - - val javacCommand = jdkBinFile.absolutePath + File.separatorChar + "javac" - - val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand)) - val javacArgs = baseJavacArgs(isJavac9OrLater) - - val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath)) - .directory(workingDir) - .redirectErrorStream(true) - .start() - - javacProc.inputStream.copyTo(internalMessageStream) - javacProc.errorStream.copyTo(internalMessageStream) - - return when(javacProc.waitFor()) { - 0 -> ExitCode.OK - 1 -> ExitCode.COMPILATION_ERROR - else -> ExitCode.INTERNAL_ERROR - } + /** + * Normalize constructor calls (disable: don't normalize; enable: normalize), default is 'disable' + * in language version 1.2 and below, 'enable' since language version 1.3 + * + * {disable|enable} + */ + @Deprecated("Removed in Kotlinc, this does nothing now.") + var constructorCallNormalizationMode: String? = null + + /** Assert calls behaviour {always-enable|always-disable|jvm|legacy} */ + var assertionsMode: String? = JVMAssertionsMode.DEFAULT.description + + /** Path to the .xml build file to compile */ + var buildFile: File? = null + + /** Compile multifile classes as a hierarchy of parts and facade */ + var inheritMultifileParts: Boolean = false + + /** Use type table in metadata serialization */ + var useTypeTable: Boolean = false + + /** Allow Kotlin runtime libraries of incompatible versions in the classpath */ + @Deprecated("Removed in Kotlinc, this does nothing now.") + var skipRuntimeVersionCheck: Boolean? = null + + /** Combine modules for source files and binary dependencies into a single module */ + @Deprecated("Removed in Kotlinc, this does nothing now.") var singleModule: Boolean = false + + /** Suppress the \"cannot access built-in declaration\" error (useful with -no-stdlib) */ + var suppressMissingBuiltinsError: Boolean = false + + /** Script resolver environment in key-value pairs (the value could be quoted and escaped) */ + var scriptResolverEnvironment: MutableMap = mutableMapOf() + + /** Java compiler arguments */ + var javacArguments: MutableList = mutableListOf() + + /** Package prefix for Java files */ + var javaPackagePrefix: String? = null + + /** + * Specify behavior for Checker Framework compatqual annotations (NullableDecl/NonNullDecl). + * Default value is 'enable' + */ + var supportCompatqualCheckerFrameworkAnnotations: String? = null + + /** + * Do not throw NPE on explicit 'equals' call for null receiver of platform boxed primitive type + */ + @Deprecated("Removed in Kotlinc, this does nothing now.") + var noExceptionOnExplicitEqualsForBoxedNull: Boolean? = null + + /** + * Allow to use '@JvmDefault' annotation for JVM default method support. + * {disable|enable|compatibility} + */ + var jvmDefault: String = JvmDefaultMode.DISABLE.description + + /** Generate metadata with strict version semantics (see kdoc on Metadata.extraInt) */ + var strictMetadataVersionSemantics: Boolean = false + + /** + * Transform '(' and ')' in method names to some other character sequence. This mode can BREAK + * BINARY COMPATIBILITY and is only supposed to be used as a workaround of an issue in the ASM + * bytecode framework. See KT-29475 for more details + */ + var sanitizeParentheses: Boolean = false + + /** Paths to output directories for friend modules (whose internals should be visible) */ + var friendPaths: List = emptyList() + + /** + * Path to the JDK to be used + * + * If null, no JDK will be used with kotlinc (option -no-jdk) and the system java compiler will be + * used with empty bootclasspath (on JDK8) or --system none (on JDK9+). This can be useful if all + * the JDK classes you need are already on the (inherited) classpath. + */ + var jdkHome: File? by default { processJdkHome } + + /** + * Path to the kotlin-stdlib.jar If none is given, it will be searched for in the host process' + * classpaths + */ + var kotlinStdLibJar: File? by default { HostEnvironment.kotlinStdLibJar } + + /** + * Path to the kotlin-stdlib-jdk*.jar If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinStdLibJdkJar: File? by default { HostEnvironment.kotlinStdLibJdkJar } + + /** + * Path to the kotlin-reflect.jar If none is given, it will be searched for in the host process' + * classpaths + */ + var kotlinReflectJar: File? by default { HostEnvironment.kotlinReflectJar } + + /** + * Path to the kotlin-script-runtime.jar If none is given, it will be searched for in the host + * process' classpaths + */ + var kotlinScriptRuntimeJar: File? by default { HostEnvironment.kotlinScriptRuntimeJar } + + /** + * Path to the tools.jar file needed for kapt when using a JDK 8. + * + * Note: Using a tools.jar file with a JDK 9 or later leads to an internal compiler error! + */ + var toolsJar: File? by default { + if (!isJdk9OrLater()) jdkHome?.let { findToolsJarFromJdk(it) } ?: HostEnvironment.toolsJar + else null + } + + // *.class files, Jars and resources (non-temporary) that are created by the + // compilation will land here + val classesDir + get() = workingDir.resolve("classes") + + // Base directory for kapt stuff + private val kaptBaseDir + get() = workingDir.resolve("kapt") + + // Java annotation processors that are compile by kapt will put their generated files here + val kaptSourceDir + get() = kaptBaseDir.resolve("sources") + + // Output directory for Kotlin source files generated by kapt + val kaptKotlinGeneratedDir + get() = + kaptArgs[OPTION_KAPT_KOTLIN_GENERATED]?.let { path -> + require(File(path).isDirectory) { "$OPTION_KAPT_KOTLIN_GENERATED must be a directory" } + File(path) + } ?: File(kaptBaseDir, "kotlinGenerated") + + val kaptStubsDir + get() = kaptBaseDir.resolve("stubs") + + val kaptIncrementalDataDir + get() = kaptBaseDir.resolve("incrementalData") + + /** ExitCode of the entire Kotlin compilation process */ + enum class ExitCode { + OK, + INTERNAL_ERROR, + COMPILATION_ERROR, + SCRIPT_EXECUTION_ERROR + } + + private fun useKapt4(): Boolean { + return (useKapt4 ?: languageVersion?.startsWith("2")) == true + } + + // setup common arguments for the two kotlinc calls + private fun commonK2JVMArgs() = + commonArguments(K2JVMCompilerArguments()) { args -> + args.destination = classesDir.absolutePath + args.classpath = commonClasspaths().joinToString(separator = File.pathSeparator) + + if (jdkHome != null) { + args.jdkHome = jdkHome!!.absolutePath + } else { + log("Using option -no-jdk. Kotlinc won't look for a JDK.") + args.noJdk = true + } + + args.includeRuntime = includeRuntime + + // the compiler should never look for stdlib or reflect in the + // kotlinHome directory (which is null anyway). We will put them + // in the classpath manually if they're needed + args.noStdlib = true + args.noReflect = true + + if (moduleName != null) args.moduleName = moduleName + + args.jvmTarget = jvmTarget + args.javaParameters = javaParameters + args.useOldBackend = useOldBackend + + if (javaModulePath != null) args.javaModulePath = javaModulePath!!.toString() + + args.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray() + args.noCallAssertions = noCallAssertions + args.noParamAssertions = noParamAssertions + args.noReceiverAssertions = noReceiverAssertions + + args.noOptimize = noOptimize + + if (assertionsMode != null) args.assertionsMode = assertionsMode + + if (buildFile != null) args.buildFile = buildFile!!.toString() + + args.inheritMultifileParts = inheritMultifileParts + args.useTypeTable = useTypeTable + + if (javacArguments.isNotEmpty()) args.javacArguments = javacArguments.toTypedArray() + + if (supportCompatqualCheckerFrameworkAnnotations != null) + args.supportCompatqualCheckerFrameworkAnnotations = + supportCompatqualCheckerFrameworkAnnotations + + args.jvmDefault = jvmDefault + args.strictMetadataVersionSemantics = strictMetadataVersionSemantics + args.sanitizeParentheses = sanitizeParentheses + + if (friendPaths.isNotEmpty()) + args.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray() + + if (scriptResolverEnvironment.isNotEmpty()) + args.scriptResolverEnvironment = + scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray() + + args.javaPackagePrefix = javaPackagePrefix + args.suppressMissingBuiltinsError = suppressMissingBuiltinsError + args.disableStandardScript = disableStandardScript + } + + /** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */ + private fun stubsAndApt(sourceFiles: List): ExitCode { + if (annotationProcessors.isEmpty()) { + log("No services were given. Not running kapt steps.") + return ExitCode.OK + } + + val kaptOptions = + KaptOptions.Builder().also { + it.stubsOutputDir = kaptStubsDir + it.sourcesOutputDir = kaptSourceDir + it.incrementalDataOutputDir = kaptIncrementalDataDir + it.classesOutputDir = classesDir + it.processingOptions.apply { + putAll(kaptArgs) + putIfAbsent(OPTION_KAPT_KOTLIN_GENERATED, kaptKotlinGeneratedDir.absolutePath) } - else { - /* If no JDK is given, we will use the host process' system java compiler. - If it is set to `null`, we will erase the bootclasspath. The user is then on their own to somehow - provide the JDK classes via the regular classpath because javac won't - work at all without them */ - log("jdkHome is not specified. Using system java compiler of the host process.") - val isJavac9OrLater = isJdk9OrLater() - val javacArgs = baseJavacArgs(isJavac9OrLater).apply { - if (jdkHome == null) { - log("jdkHome is set to null, removing boot classpath from java compilation") - // erase bootclasspath or JDK path because no JDK was specified - if (isJavac9OrLater) - addAll("--system", "none") - else - addAll("-bootclasspath", "") - } - } - - val javac = SynchronizedToolProvider.systemJavaCompiler - val javaFileManager = javac.getStandardFileManager(null, null, null) - val diagnosticCollector = DiagnosticCollector() - - fun printDiagnostics() = diagnosticCollector.diagnostics.forEach { diag -> - // Print toString() for these to get the full error message - when(diag.kind) { - Diagnostic.Kind.ERROR -> error(diag.toString()) - Diagnostic.Kind.WARNING, - Diagnostic.Kind.MANDATORY_WARNING -> warn(diag.toString()) - else -> log(diag.toString()) - } - } - - try { - val noErrors = javac.getTask( - OutputStreamWriter(internalMessageStream), javaFileManager, - diagnosticCollector, javacArgs, - /* classes to be annotation processed */ null, - javaSources.map { FileJavaFileObject(it) } - .filter { it.kind == JavaFileObject.Kind.SOURCE } - ).call() - - printDiagnostics() - - return if(noErrors) - ExitCode.OK - else - ExitCode.COMPILATION_ERROR - } - catch (e: Exception) { - if (e is RuntimeException) { - printDiagnostics() - error(e.toString()) - return ExitCode.INTERNAL_ERROR - } - else - throw e - } + + it.mode = AptMode.STUBS_AND_APT + + it.flags.apply { + addAll(kaptFlags) + + if (verbose) { + addAll(KaptFlag.MAP_DIAGNOSTIC_LOCATIONS, KaptFlag.VERBOSE) + } + } + } + + val compilerMessageCollector = + PrintingMessageCollector(internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose) + + val kaptLogger = MessageCollectorBackedKaptLogger(kaptOptions.build(), compilerMessageCollector) + + /* + * 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 + */ + MainComponentRegistrar.threadLocalParameters.set( + MainComponentRegistrar.ThreadLocalParameters( + annotationProcessors.map { + IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL, kaptLogger) + }, + kaptOptions, + componentRegistrars, + compilerPluginRegistrars, + supportsK2, + ) + ) + + val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension) + val javaSources = sourceFiles.filter(File::hasJavaFileExtension) + + val sourcePaths = javaSources.plus(kotlinSources).map(File::getAbsolutePath).distinct() + + if (pluginClasspaths.isNotEmpty()) { + warn("Included plugins in pluginsClasspaths will be executed twice.") + } + + val isK2 = useKapt4() + if (isK2) { + this.compilerPluginRegistrars += Kapt4CompilerPluginRegistrar() + this.kotlincArguments += kaptOptions.toPluginOptions() + } + + val k2JvmArgs = + commonK2JVMArgs().also { + it.freeArgs = sourcePaths + it.pluginClasspaths = (it.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath()) + if (kotlinSources.isEmpty()) { + it.allowNoSourceFiles = true } - } - - /** Runs the compilation task */ - fun compile(): JvmCompilationResult { - // make sure all needed directories exist - sourcesDir.mkdirs() - classesDir.mkdirs() - kaptSourceDir.mkdirs() - kaptStubsDir.mkdirs() - kaptIncrementalDataDir.mkdirs() - kaptKotlinGeneratedDir.mkdirs() - - // write given sources to working directory - val sourceFiles = sources.map { it.writeIfNeeded(sourcesDir) } - - pluginClasspaths.forEach { filepath -> - if (!filepath.exists()) { - error("Plugin $filepath not found") - return makeResult(ExitCode.INTERNAL_ERROR) - } - } - - /* - There are 4 steps to the compilation process: - 1. Generate stubs (using kotlinc with kapt plugin which does no further compilation) - 2. Run apt (using kotlinc with kapt plugin which does no further compilation) - 3. Run kotlinc with the normal Kotlin sources and Kotlin sources generated in step 2 - 4. Run javac with Java sources and the compiled Kotlin classes - */ - - /* Work around for warning that sometimes happens: - "Failed to initialize native filesystem for Windows - java.lang.RuntimeException: Could not find installation home path. - Please make sure bin/idea.properties is present in the installation directory" - See: https://github.com/arturbosch/detekt/issues/630 - */ - withSystemProperty("idea.use.native.fs.for.win", "false") { - // step 1 and 2: generate stubs and run annotation processors - try { - val exitCode = stubsAndApt(sourceFiles) - if (exitCode != ExitCode.OK) { - return makeResult(exitCode) - } - } finally { - MainComponentRegistrar.threadLocalParameters.remove() - } - - // step 3: compile Kotlin files - compileJvmKotlin(sourceFiles).let { exitCode -> - if(exitCode != ExitCode.OK) { - return makeResult(exitCode) - } - } - } - - // step 4: compile Java files - return makeResult(compileJava(sourceFiles)) - } - - private fun makeResult(exitCode: ExitCode): JvmCompilationResult { - val messages = internalMessageBuffer.readUtf8() - - if(exitCode != ExitCode.OK) - searchSystemOutForKnownErrors(messages) - - return JvmCompilationResult(exitCode, messages, this) - } - - private fun commonClasspaths() = mutableListOf().apply { - addAll(classpaths) - addAll(listOfNotNull(kotlinStdLibJar, kotlinStdLibCommonJar, kotlinStdLibJdkJar, - kotlinReflectJar, kotlinScriptRuntimeJar - )) - - if(inheritClassPath) { - addAll(hostClasspaths) - log("Inheriting classpaths: " + hostClasspaths.joinToString(File.pathSeparator)) - } - }.distinct() - - companion object { - const val OPTION_KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" + } + + return convertKotlinExitCode( + K2JVMCompiler().exec(compilerMessageCollector, Services.EMPTY, k2JvmArgs) + ) + } + + /** Performs the 3rd compilation step to compile Kotlin source files */ + private fun compileJvmKotlin(sourceFiles: List): ExitCode { + val sources = + sourceFiles + + kaptKotlinGeneratedDir.listFilesRecursively() + + kaptSourceDir.listFilesRecursively() + + return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs()) + } + + /** + * Base javac arguments that only depend on the arguments given by the user Depending on which + * compiler implementation is actually used, more arguments may be added + */ + private fun baseJavacArgs(isJavac9OrLater: Boolean) = + mutableListOf().apply { + if (verbose) { + add("-verbose") + add("-Xlint:path") // warn about invalid paths in CLI + add("-Xlint:options") // warn about invalid options in CLI + + if (isJavac9OrLater) add("-Xlint:module") // warn about issues with the module system + } + + addAll("-d", classesDir.absolutePath) + + add("-proc:none") // disable annotation processing + + if (allWarningsAsErrors) add("-Werror") + + addAll(javacArguments) + + // also add class output path to javac classpath so it can discover + // already compiled Kotlin classes + addAll( + "-cp", + (commonClasspaths() + classesDir).joinToString( + File.pathSeparator, + transform = File::getAbsolutePath, + ), + ) } + + /** Performs the 4th compilation step to compile Java source files */ + private fun compileJava(sourceFiles: List): ExitCode { + val javaSources = + sourceFiles + .plus(kaptSourceDir.listFilesRecursively()) + .plus( + extraGeneratedSources + .flatMap(File::listFilesRecursively) + .filter(File::hasJavaFileExtension) + ) + .distinct() + .filterNot(File::hasKotlinFileExtension) + + if (javaSources.isEmpty()) return ExitCode.OK + + if (jdkHome != null && jdkHome!!.canonicalPath != processJdkHome.canonicalPath) { + /* If a JDK home is given, try to run javac from there so it uses the same JDK + as K2JVMCompiler. Changing the JDK of the system java compiler via the + "--system" and "-bootclasspath" options is not so easy. + If the jdkHome is the same as the current process, we still run an in process compilation because it is + expensive to fork a process to compile. + */ + log("compiling java in a sub-process because a jdkHome is specified") + val jdkBinFile = File(jdkHome, "bin") + check(jdkBinFile.exists()) { "No JDK bin folder found at: ${jdkBinFile.toPath()}" } + + val javacCommand = jdkBinFile.absolutePath + File.separatorChar + "javac" + + val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand)) + val javacArgs = baseJavacArgs(isJavac9OrLater) + + val javacProc = + ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath)) + .directory(workingDir) + .redirectErrorStream(true) + .start() + + javacProc.inputStream.copyTo(internalMessageStream) + javacProc.errorStream.copyTo(internalMessageStream) + + return when (javacProc.waitFor()) { + 0 -> ExitCode.OK + 1 -> ExitCode.COMPILATION_ERROR + else -> ExitCode.INTERNAL_ERROR + } + } else { + /* If no JDK is given, we will use the host process' system java compiler. + If it is set to `null`, we will erase the bootclasspath. The user is then on their own to somehow + provide the JDK classes via the regular classpath because javac won't + work at all without them */ + log("jdkHome is not specified. Using system java compiler of the host process.") + val isJavac9OrLater = isJdk9OrLater() + val javacArgs = + baseJavacArgs(isJavac9OrLater).apply { + if (jdkHome == null) { + log("jdkHome is set to null, removing boot classpath from java compilation") + // erase bootclasspath or JDK path because no JDK was specified + if (isJavac9OrLater) addAll("--system", "none") else addAll("-bootclasspath", "") + } + } + + val javac = SynchronizedToolProvider.systemJavaCompiler + val javaFileManager = javac.getStandardFileManager(null, null, null) + val diagnosticCollector = DiagnosticCollector() + + fun printDiagnostics() = + diagnosticCollector.diagnostics.forEach { diag -> + // Print toString() for these to get the full error message + when (diag.kind) { + Diagnostic.Kind.ERROR -> error(diag.toString()) + Diagnostic.Kind.WARNING, + Diagnostic.Kind.MANDATORY_WARNING -> warn(diag.toString()) + else -> log(diag.toString()) + } + } + + try { + val noErrors = + javac + .getTask( + OutputStreamWriter(internalMessageStream), + javaFileManager, + diagnosticCollector, + javacArgs, + /* classes to be annotation processed */ null, + javaSources + .map { FileJavaFileObject(it) } + .filter { it.kind == JavaFileObject.Kind.SOURCE }, + ) + .call() + + printDiagnostics() + + return if (noErrors) ExitCode.OK else ExitCode.COMPILATION_ERROR + } catch (e: Exception) { + if (e is RuntimeException) { + printDiagnostics() + error(e.toString()) + return ExitCode.INTERNAL_ERROR + } else throw e + } + } + } + + /** Runs the compilation task */ + fun compile(): JvmCompilationResult { + // make sure all needed directories exist + sourcesDir.mkdirs() + classesDir.mkdirs() + kaptSourceDir.mkdirs() + kaptStubsDir.mkdirs() + kaptIncrementalDataDir.mkdirs() + kaptKotlinGeneratedDir.mkdirs() + + // write given sources to working directory + val sourceFiles = sources.map { it.writeIfNeeded(sourcesDir) } + + pluginClasspaths.forEach { filepath -> + if (!filepath.exists()) { + error("Plugin $filepath not found") + return makeResult(ExitCode.INTERNAL_ERROR) + } + } + + /* + There are 4 steps to the compilation process: + 1. Generate stubs (using kotlinc with kapt plugin which does no further compilation) + 2. Run apt (using kotlinc with kapt plugin which does no further compilation) + 3. Run kotlinc with the normal Kotlin sources and Kotlin sources generated in step 2 + 4. Run javac with Java sources and the compiled Kotlin classes + */ + + /* Work around for warning that sometimes happens: + "Failed to initialize native filesystem for Windows + java.lang.RuntimeException: Could not find installation home path. + Please make sure bin/idea.properties is present in the installation directory" + See: https://github.com/arturbosch/detekt/issues/630 + */ + withSystemProperty("idea.use.native.fs.for.win", "false") { + // step 1 and 2: generate stubs and run annotation processors + try { + val exitCode = stubsAndApt(sourceFiles) + if (exitCode != ExitCode.OK) { + return makeResult(exitCode) + } + } finally { + MainComponentRegistrar.threadLocalParameters.remove() + } + + // step 3: compile Kotlin files + compileJvmKotlin(sourceFiles).let { exitCode -> + if (exitCode != ExitCode.OK) { + return makeResult(exitCode) + } + } + } + + // step 4: compile Java files + return makeResult(compileJava(sourceFiles)) + } + + private fun makeResult(exitCode: ExitCode): JvmCompilationResult { + val messages = internalMessageBuffer.readUtf8() + + if (exitCode != ExitCode.OK) searchSystemOutForKnownErrors(messages) + + return JvmCompilationResult(exitCode, messages, this) + } + + internal fun commonClasspaths() = + mutableListOf() + .apply { + addAll(classpaths) + addAll( + listOfNotNull( + kotlinStdLibJar, + kotlinStdLibCommonJar, + kotlinStdLibJdkJar, + kotlinReflectJar, + kotlinScriptRuntimeJar, + ) + ) + + if (inheritClassPath) { + addAll(hostClasspaths) + log("Inheriting classpaths: " + hostClasspaths.joinToString(File.pathSeparator)) + } + } + .distinct() + + companion object { + const val OPTION_KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" + } } /** - * Adds the output directory of [previousResult] to the classpath of this compilation. This is a convenience for + * Adds the output directory of [previousResult] to the classpath of this compilation. This is a + * convenience for + * * ``` * this.classpaths += previousResult.outputDirectory * ``` */ @ExperimentalCompilerApi fun KotlinCompilation.addPreviousResultToClasspath( - previousResult: JvmCompilationResult -): KotlinCompilation = apply { - classpaths += previousResult.outputDirectory -} + previousResult: JvmCompilationResult +): KotlinCompilation = apply { classpaths += previousResult.outputDirectory } diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt index 0e4c7200..ea229b99 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt @@ -3,13 +3,14 @@ package com.tschuchort.compiletesting import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi -import java.io.* +import java.io.File @ExperimentalCompilerApi @Suppress("MemberVisibilityCanBePrivate") class KotlinJsCompilation : AbstractKotlinCompilation() { - var outputFileName: String = "test.js" + @Deprecated("It is senseless to use with IR compiler. Only for compatibility.") + var outputFileName: String? = null /** * Generate unpacked KLIB into parent directory of output JS file. In combination with -meta-info @@ -64,7 +65,9 @@ class KotlinJsCompilation : AbstractKotlinCompilation() { args.noStdlib = true args.moduleKind = "commonjs" - args.outputFile = File(outputDir, outputFileName).absolutePath + outputFileName?.let { + args.outputFile = File(outputDir, it).absolutePath + } args.outputDir = outputDir.absolutePath args.sourceMapBaseDirs = jsClasspath().joinToString(separator = File.pathSeparator) args.libraries = listOfNotNull(kotlinStdLibJsJar).joinToString(separator = ":") diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/MainCommandLineProcessor.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/MainCommandLineProcessor.kt index e581397d..0ca714a8 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/MainCommandLineProcessor.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/MainCommandLineProcessor.kt @@ -6,7 +6,6 @@ import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.jetbrains.kotlin.config.CompilerConfiguration -import java.util.* @ExperimentalCompilerApi @AutoService(CommandLineProcessor::class) diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt index 3ef9a103..8c9c7edc 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt @@ -17,12 +17,13 @@ package com.tschuchort.compiletesting import com.google.auto.service.AutoService -import org.jetbrains.kotlin.base.kapt3.KaptOptions import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CommonConfigurationKeys.USE_FIR import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.kapt3.base.KaptOptions import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor @ExperimentalCompilerApi @@ -30,7 +31,7 @@ import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor internal class MainComponentRegistrar : ComponentRegistrar, CompilerPluginRegistrar() { override val supportsK2: Boolean - get() = getThreadLocalParameters("supportsK2")?.supportsK2 ?: false + get() = getThreadLocalParameters("supportsK2")?.supportsK2 != false // Handle unset parameters gracefully because this plugin may be accidentally called by other tools that // discover it on the classpath (for example the kotlin jupyter kernel). @@ -46,8 +47,8 @@ internal class MainComponentRegistrar : ComponentRegistrar, CompilerPluginRegist override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { val parameters = getThreadLocalParameters("registerExtensions") ?: return - parameters.compilerPluginRegistrar.forEach { componentRegistrar -> - with(componentRegistrar) { + parameters.compilerPluginRegistrar.forEach { pluginRegistrar -> + with(pluginRegistrar) { registerExtensions(configuration) } } @@ -68,8 +69,10 @@ internal class MainComponentRegistrar : ComponentRegistrar, CompilerPluginRegist componentRegistrar.registerProjectComponents(project, configuration) } - KaptComponentRegistrar(parameters.processors, parameters.kaptOptions) - .registerProjectComponents(project, configuration) + if (!configuration.getBoolean(USE_FIR)) { + KaptComponentRegistrar(parameters.processors, parameters.kaptOptions) + .registerProjectComponents(project, configuration) + } } companion object { diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/PrecursorTool.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/PrecursorTool.kt new file mode 100644 index 00000000..0c1c1417 --- /dev/null +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/PrecursorTool.kt @@ -0,0 +1,17 @@ +package com.tschuchort.compiletesting + +import java.io.File +import java.io.PrintStream +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +/** + * A standalone tool that can be run before the KotlinCompilation begins. + */ +@ExperimentalCompilerApi +fun interface PrecursorTool { + fun execute( + compilation: KotlinCompilation, + output: PrintStream, + sources: List, + ): KotlinCompilation.ExitCode +} diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt index 0fe75c3d..ffa7fbf2 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt @@ -1,12 +1,13 @@ package com.tschuchort.compiletesting import okio.Buffer -import java.io.* +import java.io.File +import java.io.FileDescriptor +import java.io.FileOutputStream +import java.io.PrintStream import java.net.URL import java.net.URLClassLoader import java.nio.charset.Charset -import java.nio.file.* -import java.nio.file.attribute.BasicFileAttributes import javax.lang.model.SourceVersion internal fun MutableCollection.addAll(vararg elems: E) = addAll(elems) diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/kapt/util.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/kapt/util.kt new file mode 100644 index 00000000..1d781dc0 --- /dev/null +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/kapt/util.kt @@ -0,0 +1,82 @@ +package com.tschuchort.compiletesting.kapt + +import org.jetbrains.kotlin.kapt.cli.KaptCliOption +import org.jetbrains.kotlin.kapt3.base.KaptFlag +import org.jetbrains.kotlin.kapt3.base.KaptOptions +import java.io.File + +fun KaptOptions.Builder.toPluginOptions(): List { + val options = mutableListOf() + for (option in KaptCliOption.entries) { + fun Any.pluginOption(value: String = this.toString()) { + options += listOf("-P", "plugin:" + KaptCliOption.ANNOTATION_PROCESSING_COMPILER_PLUGIN_ID + ":" + option.optionName + "=" + value) + } + + when (option) { + KaptCliOption.SOURCE_OUTPUT_DIR_OPTION -> sourcesOutputDir?.pluginOption() + KaptCliOption.CLASS_OUTPUT_DIR_OPTION -> classesOutputDir?.pluginOption() + KaptCliOption.STUBS_OUTPUT_DIR_OPTION -> stubsOutputDir?.pluginOption() + KaptCliOption.INCREMENTAL_DATA_OUTPUT_DIR_OPTION -> incrementalDataOutputDir?.pluginOption() + + KaptCliOption.CHANGED_FILES -> { + for (file in changedFiles) { + file.pluginOption() + } + } + KaptCliOption.COMPILED_SOURCES_DIR -> compiledSources.joinToString(File.pathSeparator).pluginOption() + KaptCliOption.INCREMENTAL_CACHE -> incrementalCache?.pluginOption() + KaptCliOption.CLASSPATH_CHANGES -> { + for (change in classpathChanges) { + change.pluginOption() + } + } + KaptCliOption.PROCESS_INCREMENTALLY -> (KaptFlag.INCREMENTAL_APT in flags).pluginOption() + + KaptCliOption.ANNOTATION_PROCESSOR_CLASSPATH_OPTION -> { + for (path in processingClasspath) { + path.pluginOption() + } + } + KaptCliOption.ANNOTATION_PROCESSORS_OPTION -> processors + .map(String::trim) + .filterNot(String::isEmpty) + .joinToString(",") + .pluginOption() + + KaptCliOption.APT_OPTION_OPTION -> { + for ((k, v) in processingOptions) { + "$k=$v".pluginOption() + } + } + KaptCliOption.JAVAC_OPTION_OPTION -> { + for ((k, v) in javacOptions) { + "$k=$v".pluginOption() + } + } + + KaptCliOption.VERBOSE_MODE_OPTION -> (KaptFlag.VERBOSE in flags).pluginOption() + KaptCliOption.USE_LIGHT_ANALYSIS_OPTION -> (KaptFlag.USE_LIGHT_ANALYSIS in flags).pluginOption() + KaptCliOption.CORRECT_ERROR_TYPES_OPTION -> (KaptFlag.CORRECT_ERROR_TYPES in flags).pluginOption() + KaptCliOption.DUMP_DEFAULT_PARAMETER_VALUES -> (KaptFlag.DUMP_DEFAULT_PARAMETER_VALUES in flags).pluginOption() + KaptCliOption.MAP_DIAGNOSTIC_LOCATIONS_OPTION -> (KaptFlag.MAP_DIAGNOSTIC_LOCATIONS in flags).pluginOption() + KaptCliOption.INFO_AS_WARNINGS_OPTION -> (KaptFlag.INFO_AS_WARNINGS in flags).pluginOption() + KaptCliOption.STRICT_MODE_OPTION -> (KaptFlag.STRICT in flags).pluginOption() + KaptCliOption.STRIP_METADATA_OPTION -> (KaptFlag.STRIP_METADATA in flags).pluginOption() + KaptCliOption.KEEP_KDOC_COMMENTS_IN_STUBS -> (KaptFlag.KEEP_KDOC_COMMENTS_IN_STUBS in flags).pluginOption() + KaptCliOption.USE_K2 -> {} + + KaptCliOption.SHOW_PROCESSOR_STATS -> (KaptFlag.SHOW_PROCESSOR_STATS in flags).pluginOption() + KaptCliOption.DUMP_PROCESSOR_STATS -> processorsStatsReportFile?.pluginOption() + KaptCliOption.DUMP_FILE_READ_HISTORY -> fileReadHistoryReportFile?.pluginOption() + KaptCliOption.INCLUDE_COMPILE_CLASSPATH -> (KaptFlag.INCLUDE_COMPILE_CLASSPATH in flags).pluginOption() + + KaptCliOption.DETECT_MEMORY_LEAKS_OPTION -> detectMemoryLeaks.stringValue.pluginOption() + KaptCliOption.APT_MODE_OPTION -> mode.stringValue.pluginOption() + + else -> { + // Deprecated or unsupported options + } + } + } + return options +} \ No newline at end of file diff --git a/core/src/test/java/com/tschuchort/compiletesting/JavaTestProcessor.java b/core/src/test/java/com/tschuchort/compiletesting/JavaTestProcessor.java index 49aaad93..8daf6052 100644 --- a/core/src/test/java/com/tschuchort/compiletesting/JavaTestProcessor.java +++ b/core/src/test/java/com/tschuchort/compiletesting/JavaTestProcessor.java @@ -3,8 +3,10 @@ import com.squareup.javapoet.JavaFile; import com.squareup.kotlinpoet.FileSpec; import com.squareup.kotlinpoet.TypeSpec; -import kotlin.text.Charsets; - +import java.io.File; +import java.io.FileOutputStream; +import java.util.LinkedHashSet; +import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; @@ -12,10 +14,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; -import java.io.File; -import java.io.FileOutputStream; -import java.util.LinkedHashSet; -import java.util.Set; +import kotlin.text.Charsets; public class JavaTestProcessor extends AbstractProcessor { diff --git a/core/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt b/core/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt index a8964135..268b1a8d 100644 --- a/core/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt +++ b/core/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt @@ -7,13 +7,26 @@ import org.assertj.core.api.Assertions.assertThat import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.junit.Assert import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import org.mockito.Mockito import java.net.URL import javax.annotation.processing.AbstractProcessor import javax.annotation.processing.RoundEnvironment import javax.lang.model.element.TypeElement -class CompilerPluginsTest { +@RunWith(Parameterized::class) +class CompilerPluginsTest( + private val useK2: Boolean +) { + companion object { + @Parameterized.Parameters(name = "useK2={0}") + @JvmStatic + fun data() = arrayOf( + arrayOf(true), + arrayOf(false) + ) + } @Test fun `when compiler plugins are added they get executed`() { @@ -21,7 +34,7 @@ class CompilerPluginsTest { val mockPlugin = Mockito.mock(ComponentRegistrar::class.java) val fakeRegistrar = FakeCompilerPluginRegistrar() - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.new("emptyKotlinFile.kt", "")) componentRegistrars = listOf(mockPlugin) compilerPluginRegistrars = listOf(fakeRegistrar) @@ -61,7 +74,7 @@ class CompilerPluginsTest { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(annotationProcessor) componentRegistrars = listOf(mockPlugin) diff --git a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt index 61c13bc7..5cc13d34 100644 --- a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt +++ b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt @@ -3,6 +3,7 @@ package com.tschuchort.compiletesting import com.nhaarman.mockitokotlin2.* import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import com.tschuchort.compiletesting.MockitoAdditionalMatchersKotlin.Companion.not +import org.assertj.core.api.AbstractStringAssert import org.assertj.core.api.Assertions.assertThat import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption import org.jetbrains.kotlin.compiler.plugin.CliOption @@ -11,6 +12,8 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import java.io.ByteArrayOutputStream import java.io.File import java.nio.file.Files @@ -18,16 +21,38 @@ import javax.annotation.processing.AbstractProcessor import javax.annotation.processing.RoundEnvironment import javax.lang.model.element.TypeElement -@Suppress("MemberVisibilityCanBePrivate") -class KotlinCompilationTests { +@RunWith(Parameterized::class) +class KotlinCompilationTests( + private val useK2: Boolean +) { + companion object { + @Parameterized.Parameters(name = "useK2={0}") + @JvmStatic + fun data() = arrayOf( + arrayOf(true), + arrayOf(false) + ) + } + @Rule @JvmField val temporaryFolder = TemporaryFolder() val kotlinTestProc = KotlinTestProcessor() val javaTestProc = JavaTestProcessor() + private fun AbstractStringAssert<*>.containsIgnoringCase( + k1: String, + k2: String, + ) { + if (useK2) { + containsIgnoringCase(k2) + } else { + containsIgnoringCase(k1) + } + } + @Test fun `runs with only kotlin sources`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) }.compile() @@ -37,7 +62,7 @@ class KotlinCompilationTests { @Test fun `runs with only java sources`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.java("JSource.java", "class JSource {}")) }.compile() @@ -47,7 +72,7 @@ class KotlinCompilationTests { @Test fun `runs with no sources`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = emptyList() }.compile() @@ -60,7 +85,7 @@ class KotlinCompilationTests { writeText("class KSource") } - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.fromPath(sourceFile)) }.compile() @@ -80,7 +105,7 @@ class KotlinCompilationTests { writeText("package b\n\nclass KSource") } - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf( SourceFile.fromPath(sourceFileA), SourceFile.fromPath(sourceFileB)) @@ -93,7 +118,7 @@ class KotlinCompilationTests { @Test fun `runs with sources in directory`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.kotlin("com/foo/bar/kSource.kt", """ package com.foo.bar class KSource""")) @@ -114,7 +139,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) }.compile() @@ -133,21 +158,24 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) jdkHome = null }.compile() assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) - assertThat(result.messages).containsIgnoringCase("unresolved reference: java") + assertThat(result.messages).containsIgnoringCase( + k1 = "unresolved reference: java", + k2 = "Unresolved reference 'java'" + ) } @Test fun `can compile Kotlin without JDK`() { val source = SourceFile.kotlin("kSource.kt", "class KClass") - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) jdkHome = null }.compile() @@ -169,7 +197,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) }.compile() @@ -190,7 +218,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) jdkHome = null }.compile() @@ -214,7 +242,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) inheritClassPath = true }.compile() @@ -235,7 +263,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) inheritClassPath = false }.compile() @@ -257,7 +285,7 @@ class KotlinCompilationTests { """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) inheritClassPath = true }.compile() @@ -279,13 +307,16 @@ class KotlinCompilationTests { """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) inheritClassPath = false }.compile() assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) - assertThat(result.messages).containsIgnoringCase("unresolved reference: KotlinCompilationTests") + assertThat(result.messages).containsIgnoringCase( + k1 = "unresolved reference: KotlinCompilationTests", + k2 = "Unresolved reference 'KotlinCompilationTests'" + ) } @Test @@ -301,7 +332,7 @@ class KotlinCompilationTests { """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) annotationProcessors = listOf(object : AbstractProcessor() { override fun process(annotations: MutableSet?, roundEnv: RoundEnvironment?): Boolean { @@ -335,7 +366,7 @@ class KotlinCompilationTests { """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) }.compile() @@ -371,7 +402,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource, jSource) }.compile() @@ -400,7 +431,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource, jSource) }.compile() @@ -420,7 +451,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(javaTestProc) inheritClassPath = true @@ -445,7 +476,7 @@ class KotlinCompilationTests { } """) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(javaTestProc) inheritClassPath = true @@ -471,7 +502,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -497,7 +528,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -523,7 +554,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -553,7 +584,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -580,7 +611,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -607,7 +638,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -634,7 +665,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -647,7 +678,7 @@ class KotlinCompilationTests { @Test fun `detects the plugin provided for compilation via pluginClasspaths property`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) pluginClasspaths = listOf(classpathOf("kotlin-scripting-compiler-${BuildConfig.KOTLIN_VERSION}")) }.compile() @@ -660,7 +691,7 @@ class KotlinCompilationTests { @Test fun `returns an internal error when adding a non existing plugin for compilation`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) pluginClasspaths = listOf(File("./non-existing-plugin.jar")) }.compile() @@ -682,7 +713,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -705,7 +736,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -728,7 +759,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -752,7 +783,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource) annotationProcessors = listOf(kotlinTestProc) inheritClassPath = true @@ -776,7 +807,7 @@ class KotlinCompilationTests { override val pluginOptions = listOf(CliOption("test_option_name", "", "")) }) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(kSource) inheritClassPath = false pluginOptions = listOf(PluginOption("myPluginId", "test_option_name", "test_value")) @@ -815,7 +846,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(jSource, kSource) annotationProcessors = emptyList() inheritClassPath = true @@ -832,7 +863,7 @@ class KotlinCompilationTests { """ class JSource {} """.trimIndent()) - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(source) }.compile() assertThat(result.exitCode).isEqualTo(ExitCode.OK) @@ -855,7 +886,7 @@ class KotlinCompilationTests { fakeJdkHome.mkdirs() Files.createLink(fakeJdkHome.resolve("bin").toPath(), processJdkHome.toPath()) val logsStream = ByteArrayOutputStream() - val compiler = defaultCompilerConfig().apply { + val compiler = defaultCompilerConfig(useK2).apply { sources = listOf(source) jdkHome = fakeJdkHome messageOutputStream = logsStream @@ -882,7 +913,7 @@ class KotlinCompilationTests { """ ) - val result = defaultCompilerConfig() + val result = defaultCompilerConfig(useK2) .apply { sources = listOf(kSource1) inheritClassPath = true @@ -902,7 +933,7 @@ class KotlinCompilationTests { """ ) - defaultCompilerConfig() + defaultCompilerConfig(useK2) .apply { sources = listOf(kSource2) inheritClassPath = true @@ -918,7 +949,7 @@ class KotlinCompilationTests { @Test fun `runs the K2 compiler without compiler plugins`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) componentRegistrars = emptyList() pluginClasspaths = emptyList() @@ -931,9 +962,10 @@ class KotlinCompilationTests { assertClassLoadable(result, "KSource") } + @Ignore("This test is not set up correctly for K2 to work") @Test fun `can compile code with multi-platform expect modifier`() { - val result = defaultCompilerConfig().apply { + val result = defaultCompilerConfig(useK2).apply { sources = listOf( SourceFile.kotlin("kSource1.kt", "expect interface MppInterface"), SourceFile.kotlin("kSource2.kt", "actual interface MppInterface") diff --git a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinTestProcessor.kt b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinTestProcessor.kt index 6c9cd2be..10a18227 100644 --- a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinTestProcessor.kt +++ b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinTestProcessor.kt @@ -1,14 +1,16 @@ package com.tschuchort.compiletesting import com.squareup.javapoet.JavaFile -import com.squareup.javapoet.TypeSpec as JavaTypeSpec import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec import java.io.File -import javax.annotation.processing.* +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment import javax.lang.model.SourceVersion import javax.lang.model.element.TypeElement import javax.tools.Diagnostic +import com.squareup.javapoet.TypeSpec as JavaTypeSpec annotation class ProcessElem diff --git a/core/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt b/core/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt index e57a3934..2b552761 100644 --- a/core/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt +++ b/core/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt @@ -4,13 +4,19 @@ import io.github.classgraph.ClassGraph import org.assertj.core.api.Assertions import java.io.File -fun defaultCompilerConfig(): KotlinCompilation { +fun defaultCompilerConfig(useK2: Boolean): KotlinCompilation { return KotlinCompilation().apply { inheritClassPath = false correctErrorTypes = true verbose = true reportOutputFiles = false messageOutputStream = System.out + if (useK2) { + languageVersion = "2.0" + useKapt4 = true + } else { + languageVersion = "1.9" + } } } diff --git a/gradle.properties b/gradle.properties index 5829bc42..4b4e1899 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,8 +2,10 @@ kotlin.code.style=official kotlin.incremental=false kapt.include.compile.classpath=false +systemProp.kct.test.useKsp2=true + GROUP=dev.zacsweers.kctfork -VERSION_NAME=0.4.1 +VERSION_NAME=0.5.0 POM_DESCRIPTION=A library that enables testing of Kotlin annotation processors, compiler plugins and code generation. POM_INCEPTION_YEAR=2019 POM_URL=https\://github.com/zacsweers/kotlin-compile-testing diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce0d076e..962f2aef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,8 @@ [versions] -idea = "242.2339" # (see https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html) -kotlin = "1.9.23" -ksp = "1.9.23-1.0.19" +idea = "242.8057" # (see https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html) +kotlin = "2.0.0" +kotlinpoet = "1.17.0" +ksp = "2.0.0-1.0.22" [plugins] buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0" } @@ -14,7 +15,7 @@ mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.28.0" } autoService = "com.google.auto.service:auto-service-annotations:1.1.1" autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.1.0" -classgraph = "io.github.classgraph:classgraph:4.8.168" +classgraph = "io.github.classgraph:classgraph:4.8.173" intellij-core = { module = "com.jetbrains.intellij.platform:core", version.ref = "idea" } intellij-util = { module = "com.jetbrains.intellij.platform:util", version.ref = "idea" } @@ -23,19 +24,23 @@ javapoet = "com.squareup:javapoet:1.13.0" kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-annotationProcessingEmbeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } +kotlin-kapt4 = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-compiler", version.ref = "kotlin" } kotlin-scriptingCompiler = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -kotlinpoet = "com.squareup:kotlinpoet:1.16.0" +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet"} +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet"} ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" } ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +ksp-aaEmbeddable = { module = "com.google.devtools.ksp:symbol-processing-aa-embeddable", version.ref = "ksp" } +ksp-commonDeps = { module = "com.google.devtools.ksp:symbol-processing-common-deps", version.ref = "ksp" } okio = "com.squareup.okio:okio:3.9.0" truth = { module = "com.google.truth:truth", version = "1.4.2" } junit = "junit:junit:4.13.2" -mockito = "org.mockito:mockito-core:5.11.0" +mockito = "org.mockito:mockito-core:5.12.0" mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" assertJ = "org.assertj:assertj-core:3.25.3" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd491..e6441136 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..a4413138 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..b740cf13 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/ksp/build.gradle.kts b/ksp/build.gradle.kts index 285dc351..3e2b1849 100644 --- a/ksp/build.gradle.kts +++ b/ksp/build.gradle.kts @@ -5,20 +5,25 @@ plugins { alias(libs.plugins.mavenPublish) } -tasks.withType() +tasks + .withType() .matching { it.name.contains("test", ignoreCase = true) } .configureEach { - compilerOptions { - freeCompilerArgs.add("-opt-in=org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") - } + compilerOptions { optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } } dependencies { - compileOnly(libs.kotlin.compilerEmbeddable) api(projects.core) - compileOnly(libs.ksp.api) + api(libs.ksp.api) + implementation(libs.ksp) - testImplementation(libs.ksp.api) + implementation(libs.ksp.commonDeps) + implementation(libs.ksp.aaEmbeddable) + + testImplementation(libs.kotlinpoet.ksp) + testImplementation(libs.autoService) { + because("To test accessing inherited classpath symbols") + } testImplementation(libs.kotlin.junit) testImplementation(libs.mockito) testImplementation(libs.mockitoKotlin) diff --git a/ksp/src/main/kotlin/com/tschuchort/compiletesting/FilteringMessageCollector.kt b/ksp/src/main/kotlin/com/tschuchort/compiletesting/FilteringMessageCollector.kt new file mode 100644 index 00000000..86c313b4 --- /dev/null +++ b/ksp/src/main/kotlin/com/tschuchort/compiletesting/FilteringMessageCollector.kt @@ -0,0 +1,26 @@ +package com.tschuchort.compiletesting + +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation +import org.jetbrains.kotlin.cli.common.messages.MessageCollector + +internal fun MessageCollector.filterBy(levels: Set): MessageCollector { + return FilteringMessageCollector(this) { it in levels } +} + +private class FilteringMessageCollector( + private val delegate: MessageCollector, + private val filter: (CompilerMessageSeverity) -> Boolean, +) : MessageCollector by delegate { + override fun report( + severity: CompilerMessageSeverity, + message: String, + location: CompilerMessageSourceLocation?, + ) { + return if (filter(severity)) { + delegate.report(severity, message, location) + } else { + // Do nothing + } + } +} diff --git a/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt b/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt index 05f60838..74d7d55d 100644 --- a/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt +++ b/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt @@ -1,6 +1,4 @@ -/** - * Adds support for KSP (https://goo.gle/ksp). - */ +/** Adds support for KSP (https://goo.gle/ksp). */ package com.tschuchort.compiletesting import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension @@ -8,7 +6,10 @@ import com.google.devtools.ksp.KspOptions import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger +import java.io.File +import java.util.EnumSet import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot @@ -22,229 +23,250 @@ import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.languageVersionSettings import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull -import java.io.File -/** - * The list of symbol processors for the kotlin compilation. - * https://goo.gle/ksp - */ +/** Configure the given KSP tool for this compilation. */ @OptIn(ExperimentalCompilerApi::class) -var KotlinCompilation.symbolProcessorProviders: List - get() = getKspRegistrar().providers - set(value) { - val registrar = getKspRegistrar() - registrar.providers = value - } +fun KotlinCompilation.configureKsp(useKsp2: Boolean = false, body: KspTool.() -> Unit) { + if (useKsp2) { + useKsp2() + } + getKspTool().body() +} -/** - * The directory where generated KSP sources are written - */ +/** The list of symbol processors for the kotlin compilation. https://goo.gle/ksp */ +@OptIn(ExperimentalCompilerApi::class) +var KotlinCompilation.symbolProcessorProviders: MutableList + get() = getKspTool().symbolProcessorProviders + set(value) { + val tool = getKspTool() + tool.symbolProcessorProviders.clear() + tool.symbolProcessorProviders.addAll(value) + } + +/** The directory where generated KSP sources are written */ @OptIn(ExperimentalCompilerApi::class) val KotlinCompilation.kspSourcesDir: File - get() = kspWorkingDir.resolve("sources") + get() = kspWorkingDir.resolve("sources") -/** - * Arbitrary arguments to be passed to ksp - */ +/** Arbitrary arguments to be passed to ksp */ @OptIn(ExperimentalCompilerApi::class) +@Deprecated( + "Use kspProcessorOptions", + replaceWith = + ReplaceWith("kspProcessorOptions", "com.tschuchort.compiletesting.kspProcessorOptions"), +) var KotlinCompilation.kspArgs: MutableMap - get() = getKspRegistrar().options - set(value) { - val registrar = getKspRegistrar() - registrar.options = value - } + get() = kspProcessorOptions + set(options) { + kspProcessorOptions = options + } -/** - * Controls for enabling incremental processing in KSP. - */ +/** Arbitrary processor options to be passed to ksp */ +@OptIn(ExperimentalCompilerApi::class) +var KotlinCompilation.kspProcessorOptions: MutableMap + get() = getKspTool().processorOptions + set(options) { + val tool = getKspTool() + tool.processorOptions.clear() + tool.processorOptions.putAll(options) + } + +/** Controls for enabling incremental processing in KSP. */ @OptIn(ExperimentalCompilerApi::class) var KotlinCompilation.kspIncremental: Boolean - get() = getKspRegistrar().incremental - set(value) { - val registrar = getKspRegistrar() - registrar.incremental = value - } + get() = getKspTool().incremental + set(value) { + val tool = getKspTool() + tool.incremental = value + } -/** - * Controls for enabling incremental processing logs in KSP. - */ +/** Controls for enabling incremental processing logs in KSP. */ @OptIn(ExperimentalCompilerApi::class) var KotlinCompilation.kspIncrementalLog: Boolean - get() = getKspRegistrar().incrementalLog - set(value) { - val registrar = getKspRegistrar() - registrar.incrementalLog = value - } + get() = getKspTool().incrementalLog + set(value) { + val tool = getKspTool() + tool.incrementalLog = value + } -/** - * Controls for enabling all warnings as errors in KSP. - */ +/** Controls for enabling all warnings as errors in KSP. */ @OptIn(ExperimentalCompilerApi::class) var KotlinCompilation.kspAllWarningsAsErrors: Boolean - get() = getKspRegistrar().allWarningsAsErrors - set(value) { - val registrar = getKspRegistrar() - registrar.allWarningsAsErrors = value - } + get() = getKspTool().allWarningsAsErrors + set(value) { + val tool = getKspTool() + tool.allWarningsAsErrors = value + } /** - * Run processors and compilation in a single compiler invocation if true. - * See [com.google.devtools.ksp.KspCliOption.WITH_COMPILATION_OPTION]. + * Run processors and compilation in a single compiler invocation if true. See + * [com.google.devtools.ksp.KspCliOption.WITH_COMPILATION_OPTION]. */ @OptIn(ExperimentalCompilerApi::class) var KotlinCompilation.kspWithCompilation: Boolean - get() = getKspRegistrar().withCompilation - set(value) { - val registrar = getKspRegistrar() - registrar.withCompilation = value - } + get() = getKspTool().withCompilation + set(value) { + val tool = getKspTool() + tool.withCompilation = value + } +/** Sets logging levels for KSP. Default is all. */ @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspJavaSourceDir: File - get() = kspSourcesDir.resolve("java") +var KotlinCompilation.kspLoggingLevels: Set + get() = getKspTool().loggingLevels + set(value) { + val tool = getKspTool() + tool.loggingLevels = value + } @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspKotlinSourceDir: File - get() = kspSourcesDir.resolve("kotlin") +internal val KotlinCompilation.kspJavaSourceDir: File + get() = kspSourcesDir.resolve("java") @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspResources: File - get() = kspSourcesDir.resolve("resources") +internal val KotlinCompilation.kspKotlinSourceDir: File + get() = kspSourcesDir.resolve("kotlin") -/** - * The working directory for KSP - */ @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspWorkingDir: File - get() = workingDir.resolve("ksp") +internal val KotlinCompilation.kspResources: File + get() = kspSourcesDir.resolve("resources") -/** - * The directory where compiled KSP classes are written - */ +/** The working directory for KSP */ +@OptIn(ExperimentalCompilerApi::class) +internal val KotlinCompilation.kspWorkingDir: File + get() = workingDir.resolve("ksp") + +/** The directory where compiled KSP classes are written */ // TODO this seems to be ignored by KSP and it is putting classes into regular classes directory // but we still need to provide it in the KSP options builder as it is required // once it works, we should make the property public. @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspClassesDir: File - get() = kspWorkingDir.resolve("classes") +internal val KotlinCompilation.kspClassesDir: File + get() = kspWorkingDir.resolve("classes") -/** - * The directory where compiled KSP caches are written - */ +/** The directory where compiled KSP caches are written */ @OptIn(ExperimentalCompilerApi::class) -private val KotlinCompilation.kspCachesDir: File - get() = kspWorkingDir.resolve("caches") +internal val KotlinCompilation.kspCachesDir: File + get() = kspWorkingDir.resolve("caches") /** - * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined instead of being - * loaded via ServiceLocator. + * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined + * instead of being loaded via ServiceLocator. */ private class KspTestExtension( - options: KspOptions, - processorProviders: List, - logger: KSPLogger -) : AbstractKotlinSymbolProcessingExtension( - options = options, - logger = logger, - testMode = false -) { - private val loadedProviders = processorProviders - - override fun loadProviders() = loadedProviders + options: KspOptions, + processorProviders: List, + logger: KSPLogger, +) : AbstractKotlinSymbolProcessingExtension(options = options, logger = logger, testMode = false) { + private val loadedProviders = processorProviders + + override fun loadProviders() = loadedProviders } -/** - * Registers the [KspTestExtension] to load the given list of processors. - */ +/** Registers the [KspTestExtension] to load the given list of processors. */ @OptIn(ExperimentalCompilerApi::class) -private class KspCompileTestingComponentRegistrar( - private val compilation: KotlinCompilation -) : ComponentRegistrar { - var providers = emptyList() - - var options: MutableMap = mutableMapOf() - - var incremental: Boolean = false - var incrementalLog: Boolean = false - var allWarningsAsErrors: Boolean = false - var withCompilation: Boolean = false +internal class KspCompileTestingComponentRegistrar(private val compilation: KotlinCompilation) : + ComponentRegistrar, KspTool { + override var symbolProcessorProviders = mutableListOf() + override var processorOptions = mutableMapOf() + override var incremental: Boolean = false + override var incrementalLog: Boolean = false + override var allWarningsAsErrors: Boolean = false + override var withCompilation: Boolean = false + override var loggingLevels: Set = + EnumSet.allOf(CompilerMessageSeverity::class.java) - override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { - if (providers.isEmpty()) { - return - } - val options = KspOptions.Builder().apply { - this.projectBaseDir = compilation.kspWorkingDir + override fun registerProjectComponents( + project: MockProject, + configuration: CompilerConfiguration, + ) { + if (symbolProcessorProviders.isEmpty()) { + return + } + val options = + KspOptions.Builder() + .apply { + this.projectBaseDir = compilation.kspWorkingDir - this.processingOptions.putAll(compilation.kspArgs) + this.processingOptions.putAll(compilation.kspArgs) - this.incremental = this@KspCompileTestingComponentRegistrar.incremental - this.incrementalLog = this@KspCompileTestingComponentRegistrar.incrementalLog - this.allWarningsAsErrors = this@KspCompileTestingComponentRegistrar.allWarningsAsErrors - this.withCompilation = this@KspCompileTestingComponentRegistrar.withCompilation + this.incremental = this@KspCompileTestingComponentRegistrar.incremental + this.incrementalLog = this@KspCompileTestingComponentRegistrar.incrementalLog + this.allWarningsAsErrors = this@KspCompileTestingComponentRegistrar.allWarningsAsErrors + this.withCompilation = this@KspCompileTestingComponentRegistrar.withCompilation - this.cachesDir = compilation.kspCachesDir.also { - it.deleteRecursively() - it.mkdirs() + this.cachesDir = + compilation.kspCachesDir.also { + it.deleteRecursively() + it.mkdirs() } - this.kspOutputDir = compilation.kspSourcesDir.also { - it.deleteRecursively() - it.mkdirs() + this.kspOutputDir = + compilation.kspSourcesDir.also { + it.deleteRecursively() + it.mkdirs() } - this.classOutputDir = compilation.kspClassesDir.also { - it.deleteRecursively() - it.mkdirs() + this.classOutputDir = + compilation.kspClassesDir.also { + it.deleteRecursively() + it.mkdirs() } - this.javaOutputDir = compilation.kspJavaSourceDir.also { - it.deleteRecursively() - it.mkdirs() - compilation.registerGeneratedSourcesDir(it) + this.javaOutputDir = + compilation.kspJavaSourceDir.also { + it.deleteRecursively() + it.mkdirs() + compilation.registerGeneratedSourcesDir(it) } - this.kotlinOutputDir = compilation.kspKotlinSourceDir.also { - it.deleteRecursively() - it.mkdirs() + this.kotlinOutputDir = + compilation.kspKotlinSourceDir.also { + it.deleteRecursively() + it.mkdirs() } - this.resourceOutputDir = compilation.kspResources.also { - it.deleteRecursively() - it.mkdirs() + this.resourceOutputDir = + compilation.kspResources.also { + it.deleteRecursively() + it.mkdirs() } - this.languageVersionSettings = configuration.languageVersionSettings - configuration[CLIConfigurationKeys.CONTENT_ROOTS] - ?.filterIsInstance() - ?.forEach { - this.javaSourceRoots.add(it.file) - } - - }.build() - - // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102 - @Suppress("invisible_member") - val messageCollector = PrintingMessageCollector( - compilation.internalMessageStreamAccess, - MessageRenderer.GRADLE_STYLE, - compilation.verbose - ) - val messageCollectorBasedKSPLogger = MessageCollectorBasedKSPLogger( - messageCollector = messageCollector, - wrappedMessageCollector = messageCollector, - allWarningsAsErrors = allWarningsAsErrors + this.languageVersionSettings = configuration.languageVersionSettings + configuration[CLIConfigurationKeys.CONTENT_ROOTS] + ?.filterIsInstance() + ?.forEach { this.javaSourceRoots.add(it.file) } + } + .build() + + // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102 + @Suppress("invisible_member", "invisible_reference") + val messageCollector = + PrintingMessageCollector( + compilation.internalMessageStreamAccess, + MessageRenderer.GRADLE_STYLE, + compilation.verbose, ) - val registrar = KspTestExtension(options, providers, messageCollectorBasedKSPLogger) - AnalysisHandlerExtension.registerExtension(project, registrar) - // Dummy extension point; Required by dropPsiCaches(). - CoreApplicationEnvironment.registerExtensionPoint(project.extensionArea, PsiTreeChangeListener.EP.name, PsiTreeChangeAdapter::class.java) - } + .filterBy(loggingLevels) + val messageCollectorBasedKSPLogger = + MessageCollectorBasedKSPLogger( + messageCollector = messageCollector, + wrappedMessageCollector = messageCollector, + allWarningsAsErrors = allWarningsAsErrors, + ) + val registrar = + KspTestExtension(options, symbolProcessorProviders, messageCollectorBasedKSPLogger) + AnalysisHandlerExtension.registerExtension(project, registrar) + // Dummy extension point; Required by dropPsiCaches(). + CoreApplicationEnvironment.registerExtensionPoint( + project.extensionArea, + PsiTreeChangeListener.EP.name, + PsiTreeChangeAdapter::class.java, + ) + } } -/** - * Gets the test registrar from the plugin list or adds if it does not exist. - */ +/** Gets the test registrar from the plugin list or adds if it does not exist. */ @OptIn(ExperimentalCompilerApi::class) -private fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar { - componentRegistrars.firstIsInstanceOrNull()?.let { - return it - } - val kspRegistrar = KspCompileTestingComponentRegistrar(this) - componentRegistrars += kspRegistrar - return kspRegistrar +internal fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar { + componentRegistrars.firstIsInstanceOrNull()?.let { + return it + } + val kspRegistrar = KspCompileTestingComponentRegistrar(this) + componentRegistrars += kspRegistrar + return kspRegistrar } diff --git a/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp2.kt b/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp2.kt new file mode 100644 index 00000000..668e5086 --- /dev/null +++ b/ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp2.kt @@ -0,0 +1,127 @@ +/* Adds support for KSP (https://goo.gle/ksp). */ +package com.tschuchort.compiletesting + +import com.google.devtools.ksp.impl.KotlinSymbolProcessing +import com.google.devtools.ksp.processing.KSPJvmConfig +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import java.io.File +import java.io.PrintStream +import java.util.EnumSet +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +@ExperimentalCompilerApi +class Ksp2PrecursorTool : PrecursorTool, KspTool { + override var withCompilation: Boolean + get() = false + set(value) { + // Irrelevant/unavailable on KSP 2 + } + + override val symbolProcessorProviders: MutableList = mutableListOf() + override val processorOptions: MutableMap = mutableMapOf() + override var incremental: Boolean = false + override var incrementalLog: Boolean = false + override var allWarningsAsErrors: Boolean = false + override var loggingLevels: Set = + EnumSet.allOf(CompilerMessageSeverity::class.java) + + // Extra hook for direct configuration of KspJvmConfig.Builder, for advanced use cases + var onBuilder: (KSPJvmConfig.Builder.() -> Unit)? = null + + override fun execute( + compilation: KotlinCompilation, + output: PrintStream, + sources: List, + ): KotlinCompilation.ExitCode { + if (symbolProcessorProviders.isEmpty()) { + return KotlinCompilation.ExitCode.OK + } + + val config = + KSPJvmConfig.Builder() + .apply { + projectBaseDir = compilation.kspWorkingDir.absoluteFile + + incremental = this@Ksp2PrecursorTool.incremental + incrementalLog = this@Ksp2PrecursorTool.incrementalLog + allWarningsAsErrors = this@Ksp2PrecursorTool.allWarningsAsErrors + processorOptions = this@Ksp2PrecursorTool.processorOptions.toMap() + + jvmTarget = compilation.jvmTarget + jdkHome = compilation.jdkHome + languageVersion = compilation.languageVersion ?: "2.0" + apiVersion = compilation.apiVersion ?: "2.0" + + // TODO adopt new roots model + moduleName = compilation.moduleName ?: "main" + sourceRoots = sources.filter { it.extension == "kt" }.mapNotNull { it.parentFile.absoluteFile }.distinct() + javaSourceRoots = sources.filter { it.extension == "java" }.mapNotNull { it.parentFile.absoluteFile }.distinct() + @Suppress("invisible_member", "invisible_reference") + libraries = compilation.classpaths + compilation.commonClasspaths() + + cachesDir = + compilation.kspCachesDir.also { + it.deleteRecursively() + it.mkdirs() + }.absoluteFile + outputBaseDir = + compilation.kspSourcesDir.also { + it.deleteRecursively() + it.mkdirs() + }.absoluteFile + classOutputDir = + compilation.kspClassesDir.also { + it.deleteRecursively() + it.mkdirs() + }.absoluteFile + javaOutputDir = + compilation.kspJavaSourceDir.also { + it.deleteRecursively() + it.mkdirs() + compilation.registerGeneratedSourcesDir(it) + }.absoluteFile + kotlinOutputDir = + compilation.kspKotlinSourceDir.also { + it.deleteRecursively() + it.mkdirs() + compilation.registerGeneratedSourcesDir(it) + }.absoluteFile + resourceOutputDir = + compilation.kspResources.also { + it.deleteRecursively() + it.mkdirs() + }.absoluteFile + + onBuilder?.invoke(this) + } + .build() + + val messageCollector = + PrintingMessageCollector(output, MessageRenderer.GRADLE_STYLE, compilation.verbose) + .filterBy(loggingLevels) + val logger = + TestKSPLogger( + messageCollector = messageCollector, + allWarningsAsErrors = config.allWarningsAsErrors, + ) + + return try { + when (KotlinSymbolProcessing(config, symbolProcessorProviders.toList(), logger).execute()) { + KotlinSymbolProcessing.ExitCode.OK -> KotlinCompilation.ExitCode.OK + KotlinSymbolProcessing.ExitCode.PROCESSING_ERROR -> + KotlinCompilation.ExitCode.COMPILATION_ERROR + } + } finally { + logger.reportAll() + } + } +} + +/** Enables KSP2. */ +@OptIn(ExperimentalCompilerApi::class) +fun KotlinCompilation.useKsp2() { + precursorTools.getOrPut("ksp2", ::Ksp2PrecursorTool) +} diff --git a/ksp/src/main/kotlin/com/tschuchort/compiletesting/KspTool.kt b/ksp/src/main/kotlin/com/tschuchort/compiletesting/KspTool.kt new file mode 100644 index 00000000..b40a798e --- /dev/null +++ b/ksp/src/main/kotlin/com/tschuchort/compiletesting/KspTool.kt @@ -0,0 +1,22 @@ +package com.tschuchort.compiletesting + +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +sealed interface KspTool { + val symbolProcessorProviders: MutableList + val processorOptions: MutableMap + var incremental: Boolean + var incrementalLog: Boolean + var allWarningsAsErrors: Boolean + var withCompilation: Boolean + var loggingLevels: Set +} + +/** Gets or creates the [KspTool] if it doesn't exist. */ +@OptIn(ExperimentalCompilerApi::class) +internal fun KotlinCompilation.getKspTool(): KspTool { + val ksp2Tool = precursorTools["ksp2"] as? Ksp2PrecursorTool? + return ksp2Tool ?: getKspRegistrar() +} diff --git a/ksp/src/main/kotlin/com/tschuchort/compiletesting/TestKSPLogger.kt b/ksp/src/main/kotlin/com/tschuchort/compiletesting/TestKSPLogger.kt new file mode 100644 index 00000000..4f83f734 --- /dev/null +++ b/ksp/src/main/kotlin/com/tschuchort/compiletesting/TestKSPLogger.kt @@ -0,0 +1,70 @@ +package com.tschuchort.compiletesting + +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.FileLocation +import com.google.devtools.ksp.symbol.KSNode +import com.google.devtools.ksp.symbol.NonExistLocation +import java.io.PrintWriter +import java.io.StringWriter +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector + +internal class TestKSPLogger( + private val messageCollector: MessageCollector, + private val allWarningsAsErrors: Boolean, +) : KSPLogger { + + companion object { + const val PREFIX = "[ksp] " + } + + data class Event(val severity: CompilerMessageSeverity, val message: String) + + val recordedEvents = mutableListOf() + + private val reportToCompilerSeverity = + setOf(CompilerMessageSeverity.ERROR, CompilerMessageSeverity.EXCEPTION) + + private var reportedToCompiler = false + + private fun convertMessage(message: String, symbol: KSNode?): String = + when (val location = symbol?.location) { + is FileLocation -> "$PREFIX${location.filePath}:${location.lineNumber}: $message" + is NonExistLocation, + null -> "$PREFIX$message" + } + + override fun logging(message: String, symbol: KSNode?) { + recordedEvents.add(Event(CompilerMessageSeverity.LOGGING, convertMessage(message, symbol))) + } + + override fun info(message: String, symbol: KSNode?) { + recordedEvents.add(Event(CompilerMessageSeverity.INFO, convertMessage(message, symbol))) + } + + override fun warn(message: String, symbol: KSNode?) { + val severity = + if (allWarningsAsErrors) CompilerMessageSeverity.ERROR else CompilerMessageSeverity.WARNING + recordedEvents.add(Event(severity, convertMessage(message, symbol))) + } + + override fun error(message: String, symbol: KSNode?) { + recordedEvents.add(Event(CompilerMessageSeverity.ERROR, convertMessage(message, symbol))) + } + + override fun exception(e: Throwable) { + val writer = StringWriter() + e.printStackTrace(PrintWriter(writer)) + recordedEvents.add(Event(CompilerMessageSeverity.EXCEPTION, writer.toString())) + } + + fun reportAll() { + for (event in recordedEvents) { + if (!reportedToCompiler && event.severity in reportToCompilerSeverity) { + reportedToCompiler = true + messageCollector.report(event.severity, "Error occurred in KSP, check log for detail") + } + messageCollector.report(event.severity, event.message) + } + } +} diff --git a/ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt b/ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt index 348957c7..1436478a 100644 --- a/ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt +++ b/ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt @@ -1,15 +1,25 @@ package com.tschuchort.compiletesting -import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated -/** - * Helper class to write tests, only used in Ksp Compile Testing tests, not a public API. - */ -internal open class AbstractTestSymbolProcessor( - protected val codeGenerator: CodeGenerator -) : SymbolProcessor { - override fun process(resolver: Resolver): List { +fun simpleProcessor(process: (resolver: Resolver, codeGenerator: CodeGenerator) -> Unit) = + SymbolProcessorProvider { env -> + object : SymbolProcessor { + override fun process(resolver: Resolver): List { + process(resolver, env.codeGenerator) return emptyList() + } } -} \ No newline at end of file + } + +/** Helper class to write tests, only used in Ksp Compile Testing tests, not a public API. */ +internal open class AbstractTestSymbolProcessor(protected val codeGenerator: CodeGenerator) : + SymbolProcessor { + override fun process(resolver: Resolver): List { + return emptyList() + } +} diff --git a/ksp/src/test/kotlin/com/tschuchort/compiletesting/KspTest.kt b/ksp/src/test/kotlin/com/tschuchort/compiletesting/KspTest.kt index 43a5dbe1..66099b19 100644 --- a/ksp/src/test/kotlin/com/tschuchort/compiletesting/KspTest.kt +++ b/ksp/src/test/kotlin/com/tschuchort/compiletesting/KspTest.kt @@ -1,5 +1,6 @@ package com.tschuchort.compiletesting +import com.google.auto.service.AutoService import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver @@ -10,82 +11,135 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.inOrder import com.nhaarman.mockitokotlin2.mock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.writeTo import com.tschuchort.compiletesting.KotlinCompilation.ExitCode +import com.tschuchort.compiletesting.SourceFile.Companion.java +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import java.util.EnumSet import java.util.Locale import java.util.concurrent.atomic.AtomicInteger import kotlin.text.Typography.ellipsis import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import org.junit.runners.Parameterized import org.mockito.Mockito.`when` -@RunWith(JUnit4::class) -class KspTest { - @Test - fun failedKspTest() { - val instance = mock() - val providerInstance = mock() - `when`(providerInstance.create(any())).thenReturn(instance) - `when`(instance.process(any())).thenThrow( - RuntimeException("intentional fail") - ) - val result = KotlinCompilation().apply { - sources = listOf(DUMMY_KOTLIN_SRC) - symbolProcessorProviders = listOf(providerInstance) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.INTERNAL_ERROR) - assertThat(result.messages).contains("intentional fail") +@RunWith(Parameterized::class) +class KspTest(private val useKSP2: Boolean) { + companion object { + private val DUMMY_KOTLIN_SRC = + kotlin( + "foo.bar.Dummy.kt", + """ + class Dummy {} + """ + .trimIndent(), + ) + + private val DUMMY_JAVA_SRC = + java( + "foo.bar.DummyJava.java", + """ + class DummyJava {} + """ + .trimIndent(), + ) + + @JvmStatic + @Parameterized.Parameters(name = "useKSP2={0}") + fun data(): Collection> { + return listOf(arrayOf(true), arrayOf(false)) + } + } + + private fun newCompilation(): KotlinCompilation { + return KotlinCompilation().apply { + inheritClassPath = true + if (useKSP2) { + useKsp2() + } else { + languageVersion = "1.9" + } } + } - @Test - fun allProcessorMethodsAreCalled() { - val instance = mock() - val providerInstance = mock() - `when`(providerInstance.create(any())).thenReturn(instance) - val result = KotlinCompilation().apply { - sources = listOf(DUMMY_KOTLIN_SRC) - symbolProcessorProviders = listOf(providerInstance) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - providerInstance.inOrder { - verify().create(any()) + @Test + fun failedKspTest() { + val instance = mock() + val providerInstance = mock() + `when`(providerInstance.create(any())).thenReturn(instance) + `when`(instance.process(any())).thenThrow(RuntimeException("intentional fail")) + val result = + newCompilation() + .apply { + sources = listOf(DUMMY_KOTLIN_SRC) + symbolProcessorProviders += providerInstance } - instance.inOrder { - verify().process(any()) - verify().finish() + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.INTERNAL_ERROR) + assertThat(result.messages).contains("intentional fail") + } + + @Test + fun allProcessorMethodsAreCalled() { + val instance = mock() + val providerInstance = mock() + `when`(providerInstance.create(any())).thenReturn(instance) + val result = + newCompilation() + .apply { + sources = listOf(DUMMY_KOTLIN_SRC) + symbolProcessorProviders += providerInstance } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + providerInstance.inOrder { verify().create(any()) } + instance.inOrder { + verify().process(any()) + verify().finish() } + } - @Test - fun allProcessorMethodsAreCalledWhenOnlyJavaFilesArePresent() { - val instance = mock() - val providerInstance = mock() - `when`(providerInstance.create(any())).thenReturn(instance) - val result = KotlinCompilation().apply { - sources = listOf(DUMMY_JAVA_SRC) - symbolProcessorProviders = listOf(providerInstance) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - providerInstance.inOrder { - verify().create(any()) - } - instance.inOrder { - verify().process(any()) - verify().finish() + @Test + fun allProcessorMethodsAreCalledWhenOnlyJavaFilesArePresent() { + val instance = mock() + val providerInstance = mock() + `when`(providerInstance.create(any())).thenReturn(instance) + val result = + newCompilation() + .apply { + sources = listOf(DUMMY_JAVA_SRC) + symbolProcessorProviders += providerInstance } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + providerInstance.inOrder { verify().create(any()) } + instance.inOrder { + verify().process(any()) + verify().finish() } + } - @Test - fun processorGeneratedCodeIsVisible() { - val annotation = SourceFile.kotlin( - "TestAnnotation.kt", """ + @Test + fun processorGeneratedCodeIsVisible() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ package foo.bar annotation class TestAnnotation - """.trimIndent() - ) - val targetClass = SourceFile.kotlin( - "AppCode.kt", """ + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ package foo.bar import foo.bar.generated.AppCode_Gen @TestAnnotation @@ -95,361 +149,565 @@ class KspTest { AppCode_Gen() } } - """.trimIndent() - ) - val result = KotlinCompilation().apply { - sources = listOf(annotation, targetClass) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - val symbols = resolver.getSymbolsWithAnnotation("foo.bar.TestAnnotation").toList() - if (symbols.isNotEmpty()) { - assertThat(symbols.size).isEqualTo(1) - val klass = symbols.first() - check(klass is KSClassDeclaration) - val qName = klass.qualifiedName ?: error("should've found qualified name") - val genPackage = "${qName.getQualifier()}.generated" - val genClassName = "${qName.getShortName()}_Gen" - codeGenerator.createNewFile( - dependencies = Dependencies.ALL_FILES, - packageName = genPackage, - fileName = genClassName - ).bufferedWriter().use { - it.write( - """ + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(annotation, targetClass) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation("foo.bar.TestAnnotation").toList() + if (symbols.isNotEmpty()) { + assertThat(symbols.size).isEqualTo(1) + val klass = symbols.first() + check(klass is KSClassDeclaration) + val qName = klass.qualifiedName ?: error("should've found qualified name") + val genPackage = "${qName.getQualifier()}.generated" + val genClassName = "${qName.getShortName()}_Gen" + codeGenerator + .createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = genPackage, + fileName = genClassName, + ) + .bufferedWriter() + .use { + it.write( + """ package $genPackage class $genClassName() {} - """.trimIndent() - ) - } - } - return emptyList() + """ + .trimIndent() + ) } } - }) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - } + return emptyList() + } + } + } + } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + } - @Test - fun multipleProcessors() { - // access generated code by multiple processors - val source = SourceFile.kotlin( - "foo.bar.Dummy.kt", """ + @Test + fun multipleProcessors() { + // access generated code by multiple processors + val source = + kotlin( + "foo.bar.Dummy.kt", + """ package foo.bar import generated.A import generated.B import generated.C class Dummy(val a:A, val b:B, val c:C) - """.trimIndent() - ) - val result = KotlinCompilation().apply { - sources = listOf(source) - symbolProcessorProviders = listOf( - SymbolProcessorProvider { env -> ClassGeneratingProcessor(env.codeGenerator, "generated", "A") }, - SymbolProcessorProvider { env -> ClassGeneratingProcessor(env.codeGenerator, "generated", "B") }) - symbolProcessorProviders = symbolProcessorProviders + - SymbolProcessorProvider { env -> ClassGeneratingProcessor(env.codeGenerator, "generated", "C") } - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - } - - @Test - fun readProcessors() { - val instance1 = mock() - val instance2 = mock() - KotlinCompilation().apply { - symbolProcessorProviders = listOf(instance1) - assertThat(symbolProcessorProviders).containsExactly(instance1) - symbolProcessorProviders = listOf(instance2) - assertThat(symbolProcessorProviders).containsExactly(instance2) - symbolProcessorProviders = symbolProcessorProviders + instance1 - assertThat(symbolProcessorProviders).containsExactly(instance2, instance1) + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(source) + symbolProcessorProviders += + listOf( + SymbolProcessorProvider { env -> + ClassGeneratingProcessor(env.codeGenerator, "generated", "A") + }, + SymbolProcessorProvider { env -> + ClassGeneratingProcessor(env.codeGenerator, "generated", "B") + }, + SymbolProcessorProvider { env -> + ClassGeneratingProcessor(env.codeGenerator, "generated", "C") + }, + ) } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + } + + @Test + fun readProcessors() { + val instance1 = mock() + val instance2 = mock() + newCompilation().apply { + symbolProcessorProviders += instance1 + assertThat(symbolProcessorProviders).containsExactly(instance1) + symbolProcessorProviders = mutableListOf(instance2) + assertThat(symbolProcessorProviders).containsExactly(instance2) + symbolProcessorProviders = (symbolProcessorProviders + instance1).toMutableList() + assertThat(symbolProcessorProviders).containsExactly(instance2, instance1) } + } - @Test - fun incremental() { - KotlinCompilation().apply { - // Disabled by default - assertThat(kspIncremental).isFalse() - assertThat(kspIncrementalLog).isFalse() - kspIncremental = true - assertThat(kspIncremental).isTrue() - kspIncrementalLog = true - assertThat(kspIncrementalLog).isTrue() - } + @Test + fun incremental() { + newCompilation().apply { + // Disabled by default + assertThat(kspIncremental).isFalse() + assertThat(kspIncrementalLog).isFalse() + kspIncremental = true + assertThat(kspIncremental).isTrue() + kspIncrementalLog = true + assertThat(kspIncrementalLog).isTrue() } + } - @Test - fun outputDirectoryContents() { - val compilation = KotlinCompilation().apply { - sources = listOf(DUMMY_KOTLIN_SRC) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - ClassGeneratingProcessor(env.codeGenerator, "generated", "Gen") - }) + @Test + fun outputDirectoryContents() { + val compilation = + newCompilation().apply { + sources = listOf(DUMMY_KOTLIN_SRC) + symbolProcessorProviders += SymbolProcessorProvider { env -> + ClassGeneratingProcessor(env.codeGenerator, "generated", "Gen") } - val result = compilation.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - val generatedSources = compilation.kspSourcesDir.walkTopDown().filter { - it.isFile - }.toList() - assertThat(generatedSources).containsExactly( - compilation.kspSourcesDir.resolve("kotlin/generated/Gen.kt") - ) - } + } + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + val generatedSources = compilation.kspSourcesDir.walkTopDown().filter { it.isFile }.toList() + assertThat(generatedSources) + .containsExactly(compilation.kspSourcesDir.resolve("kotlin/generated/Gen.kt")) + } - @Test - fun findSymbols() { - val javaSource = SourceFile.java( - "JavaSubject.java", - """ + @Test + fun findSymbols() { + val javaSource = + java( + "JavaSubject.java", + """ @${SuppressWarnings::class.qualifiedName}("") class JavaSubject {} - """.trimIndent() - ) - val kotlinSource = SourceFile.kotlin( - "KotlinSubject.kt", """ + .trimIndent(), + ) + val kotlinSource = + kotlin( + "KotlinSubject.kt", + """ @${SuppressWarnings::class.qualifiedName}("") class KotlinSubject {} - """.trimIndent() - ) - val result = mutableListOf() - val compilation = KotlinCompilation().apply { - sources = listOf(javaSource, kotlinSource) - symbolProcessorProviders += SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - resolver.getSymbolsWithAnnotation( - SuppressWarnings::class.java.canonicalName - ).filterIsInstance() - .forEach { - result.add(it.qualifiedName!!.asString()) - } - return emptyList() - } - } + """ + .trimIndent(), + ) + val result = mutableListOf() + val compilation = + newCompilation().apply { + sources = listOf(javaSource, kotlinSource) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + resolver + .getSymbolsWithAnnotation(SuppressWarnings::class.java.canonicalName) + .filterIsInstance() + .forEach { result.add(it.qualifiedName!!.asString()) } + return emptyList() } + } } - compilation.compile() - assertThat(result).containsExactlyInAnyOrder( - "JavaSubject", "KotlinSubject" + } + compilation.compile() + assertThat(result).containsExactlyInAnyOrder("JavaSubject", "KotlinSubject") + } + + class InheritedClasspathClass + + @Test + fun findInheritedClasspathSymbols() { + val javaSource = + java( + "JavaSubject.java", + """ + import com.tschuchort.compiletesting.InheritedClasspathClass; + + @${AutoService::class.qualifiedName}(Runnable.class) + class JavaSubject { + public InheritedClasspathClass create() { + return new InheritedClasspathClass(); + } + } + """ + .trimIndent(), + ) + val kotlinSource = + kotlin( + "KotlinSubject.kt", + """ + import java.lang.Runnable + import com.tschuchort.compiletesting.InheritedClasspathClass + + @${AutoService::class.qualifiedName}(Runnable::class) + class KotlinSubject { + fun create(): InheritedClasspathClass { + return InheritedClasspathClass() + } + } + """ + .trimIndent(), + ) + val result = mutableListOf() + val compilation = + newCompilation().apply { + sources = listOf(javaSource, kotlinSource) + inheritClassPath = true + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + resolver + .getSymbolsWithAnnotation(AutoService::class.java.canonicalName) + .filterIsInstance() + .forEach { result.add(it.qualifiedName!!.asString()) } + return emptyList() + } + } + } + } + compilation.compile() + assertThat(result).containsExactlyInAnyOrder("JavaSubject", "KotlinSubject") + } + + // This test ensures that we can access files on the same source compilation as the test itself + @Test + fun inheritedSourceClasspath() { + val source = kotlin( + "Example.kt", + """ + package test + + import com.tschuchort.compiletesting.ClasspathTestAnnotation + import com.tschuchort.compiletesting.AnnotationEnumValue + import com.tschuchort.compiletesting.AnotherAnnotation + + @ClasspathTestAnnotation( + enumValue = AnnotationEnumValue.ONE, + enumValueArray = [AnnotationEnumValue.ONE, AnnotationEnumValue.TWO], + anotherAnnotation = AnotherAnnotation(""), + anotherAnnotationArray = [AnotherAnnotation("Hello")] ) - } + class Example + """ + ) - internal class ClassGeneratingProcessor( - codeGenerator: CodeGenerator, - private val packageName: String, - private val className: String, - times: Int = 1 - ) : AbstractTestSymbolProcessor(codeGenerator) { - val times = AtomicInteger(times) - override fun process(resolver: Resolver): List { - super.process(resolver) - if (times.decrementAndGet() == 0) { - codeGenerator.createNewFile( - dependencies = Dependencies.ALL_FILES, - packageName = packageName, - fileName = className - ).bufferedWriter().use { - it.write( - """ + val compilation = + newCompilation().apply { + sources = listOf(source) + symbolProcessorProviders += simpleProcessor { resolver, codeGenerator -> + resolver + .getSymbolsWithAnnotation(ClasspathTestAnnotation::class.java.canonicalName) + .filterIsInstance() + .filterNot { !it.simpleName.asString().startsWith("Gen_") } + .forEach { + val annotation = it.annotations.first().toAnnotationSpec() + FileSpec.get( + it.packageName.asString(), TypeSpec.classBuilder("Gen_${it.simpleName.asString()}") + .addAnnotation(annotation) + .build() + ) + .writeTo( + codeGenerator, + aggregating = false + ) + } + } + } + + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + } + + internal class ClassGeneratingProcessor( + codeGenerator: CodeGenerator, + private val packageName: String, + private val className: String, + times: Int = 1, + ) : AbstractTestSymbolProcessor(codeGenerator) { + val times = AtomicInteger(times) + + override fun process(resolver: Resolver): List { + super.process(resolver) + if (times.decrementAndGet() == 0) { + codeGenerator + .createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = packageName, + fileName = className, + ) + .bufferedWriter() + .use { + it.write( + """ package $packageName class $className() {} - """.trimIndent() - ) - } + """ + .trimIndent() + ) + } + } + return emptyList() + } + } + + @Test + fun nonErrorMessagesAreReadable() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ + package foo.bar + annotation class TestAnnotation + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ + package foo.bar + @TestAnnotation + class AppCode + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(annotation, targetClass) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + env.logger.logging("This is a log message") + env.logger.info("This is an info message") + env.logger.warn("This is an warn message") + return emptyList() + } } - return emptyList() + } } - } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + assertThat(result.messages).contains("This is a log message") + assertThat(result.messages).contains("This is an info message") + assertThat(result.messages).contains("This is an warn message") + } - @Test - fun nonErrorMessagesAreReadable() { - val annotation = SourceFile.kotlin( - "TestAnnotation.kt", """ + @Test + fun loggingLevels() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ package foo.bar annotation class TestAnnotation - """.trimIndent() - ) - val targetClass = SourceFile.kotlin( - "AppCode.kt", """ + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ package foo.bar @TestAnnotation class AppCode - """.trimIndent() - ) - val result = KotlinCompilation().apply { - sources = listOf(annotation, targetClass) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - env.logger.logging("This is a log message") - env.logger.info("This is an info message") - env.logger.warn("This is an warn message") - return emptyList() - } - } - }) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - assertThat(result.messages).contains("This is a log message") - assertThat(result.messages).contains("This is an info message") - assertThat(result.messages).contains("This is an warn message") - } + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(annotation, targetClass) + kspLoggingLevels = + EnumSet.of(CompilerMessageSeverity.INFO, CompilerMessageSeverity.WARNING) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + env.logger.logging("This is a log message") + env.logger.info("This is an info message") + env.logger.warn("This is an warn message") + return emptyList() + } + } + } + } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + assertThat(result.messages).contains("This is an info message") + assertThat(result.messages).contains("This is an warn message") + } - @Test - fun errorMessagesAreReadable() { - val annotation = SourceFile.kotlin( - "TestAnnotation.kt", """ + @Test + fun errorMessagesAreReadable() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ package foo.bar annotation class TestAnnotation - """.trimIndent() - ) - val targetClass = SourceFile.kotlin( - "AppCode.kt", """ + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ package foo.bar @TestAnnotation class AppCode - """.trimIndent() - ) - val result = KotlinCompilation().apply { - sources = listOf(annotation, targetClass) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - env.logger.error("This is an error message") - env.logger.exception(Throwable("This is a failure")) - return emptyList() - } - } - }) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) - assertThat(result.messages).contains("This is an error message") - assertThat(result.messages).contains("This is a failure") - } + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(annotation, targetClass) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + env.logger.error("This is an error message") + env.logger.exception(Throwable("This is a failure")) + return emptyList() + } + } + } + } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR) + assertThat(result.messages).contains("This is an error message") + assertThat(result.messages).contains("This is a failure") + } - @Test - fun messagesAreEncodedAndDecodedWithUtf8() { - val annotation = SourceFile.kotlin( - "TestAnnotation.kt", """ + @Test + fun messagesAreEncodedAndDecodedWithUtf8() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ package foo.bar annotation class TestAnnotation - """.trimIndent() - ) - val targetClass = SourceFile.kotlin( - "AppCode.kt", """ + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ package foo.bar @TestAnnotation class AppCode - """.trimIndent() - ) - val result = KotlinCompilation().apply { - sources = listOf(annotation, targetClass) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - env.logger.logging("This is a log message with ellipsis $ellipsis") - env.logger.info("This is an info message with unicode \uD83D\uDCAB") - env.logger.warn("This is an warn message with emoji 🔥") - return emptyList() - } - } - }) - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - assertThat(result.messages).contains("This is a log message with ellipsis $ellipsis") - assertThat(result.messages).contains("This is an info message with unicode \uD83D\uDCAB") - assertThat(result.messages).contains("This is an warn message with emoji 🔥") - } + """ + .trimIndent(), + ) + val result = + newCompilation() + .apply { + sources = listOf(annotation, targetClass) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + env.logger.logging("This is a log message with ellipsis $ellipsis") + env.logger.info("This is an info message with unicode \uD83D\uDCAB") + env.logger.warn("This is an warn message with emoji 🔥") + return emptyList() + } + } + } + } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + assertThat(result.messages).contains("This is a log message with ellipsis $ellipsis") + assertThat(result.messages).contains("This is an info message with unicode \uD83D\uDCAB") + assertThat(result.messages).contains("This is an warn message with emoji 🔥") + } - // This test exercises both using withCompilation (for in-process compilation of generated sources) - // and generating Java sources (to ensure generated java files are compiled too) - @Test - fun withCompilationAndJavaTest() { - val annotation = SourceFile.kotlin( - "TestAnnotation.kt", """ + // This test exercises both using withCompilation (for in-process compilation of generated + // sources) + // and generating Java sources (to ensure generated java files are compiled too) + @Test + fun withCompilationAndJavaTest() { + val annotation = + kotlin( + "TestAnnotation.kt", + """ package foo.bar annotation class TestAnnotation - """.trimIndent() - ) - val targetClass = SourceFile.kotlin( - "AppCode.kt", """ + """ + .trimIndent(), + ) + val targetClass = + kotlin( + "AppCode.kt", + """ package foo.bar @TestAnnotation class AppCode - """.trimIndent() - ) - val compilation = KotlinCompilation() - val result = compilation.apply { - sources = listOf(annotation, targetClass) - symbolProcessorProviders = listOf(SymbolProcessorProvider { env -> - object : AbstractTestSymbolProcessor(env.codeGenerator) { - override fun process(resolver: Resolver): List { - resolver.getSymbolsWithAnnotation("foo.bar.TestAnnotation") - .forEach { symbol -> - check(symbol is KSClassDeclaration) { "Expected class declaration" } - @Suppress("DEPRECATION") - val simpleName = "${symbol.simpleName.asString().capitalize(Locale.US)}Dummy" - env.codeGenerator.createNewFile( - dependencies = Dependencies.ALL_FILES, - packageName = "foo.bar", - fileName = simpleName, - extensionName = "java" - ).bufferedWriter().use { - //language=JAVA - it.write( - """ + """ + .trimIndent(), + ) + val compilation = newCompilation() + val result = + compilation + .apply { + sources = listOf(annotation, targetClass) + symbolProcessorProviders += SymbolProcessorProvider { env -> + object : AbstractTestSymbolProcessor(env.codeGenerator) { + override fun process(resolver: Resolver): List { + resolver.getSymbolsWithAnnotation("foo.bar.TestAnnotation").forEach { symbol -> + check(symbol is KSClassDeclaration) { "Expected class declaration" } + @Suppress("DEPRECATION") + val simpleName = "${symbol.simpleName.asString().capitalize(Locale.US)}Dummy" + env.codeGenerator + .createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = "foo.bar", + fileName = simpleName, + extensionName = "java", + ) + .bufferedWriter() + .use { + // language=JAVA + it.write( + """ package foo.bar; class ${simpleName}Java { } - """.trimIndent() - ) - } - env.codeGenerator.createNewFile( - dependencies = Dependencies.ALL_FILES, - packageName = "foo.bar", - fileName = "${simpleName}Kt", - extensionName = "kt" - ).bufferedWriter().use { - //language=KOTLIN - it.write( """ + .trimIndent() + ) + } + env.codeGenerator + .createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = "foo.bar", + fileName = "${simpleName}Kt", + extensionName = "kt", + ) + .bufferedWriter() + .use { + // language=KOTLIN + it.write( + """ package foo.bar class ${simpleName}Kt { } - """.trimIndent() - ) - } - } - return emptyList() + """ + .trimIndent() + ) } } - }) - kspWithCompilation = true - }.compile() - assertThat(result.exitCode).isEqualTo(ExitCode.OK) - assertThat(result.classLoader.loadClass("foo.bar.AppCodeDummyJava")).isNotNull() - assertThat(result.classLoader.loadClass("foo.bar.AppCodeDummyKt")).isNotNull() - } - - companion object { - private val DUMMY_KOTLIN_SRC = SourceFile.kotlin( - "foo.bar.Dummy.kt", """ - class Dummy {} - """.trimIndent() - ) - - private val DUMMY_JAVA_SRC = SourceFile.java( - "foo.bar.DummyJava.java", """ - class DummyJava {} - """.trimIndent() - ) - } + return emptyList() + } + } + } + kspWithCompilation = true + } + .compile() + assertThat(result.exitCode).isEqualTo(ExitCode.OK) + assertThat(result.classLoader.loadClass("foo.bar.AppCodeDummyJava")).isNotNull() + assertThat(result.classLoader.loadClass("foo.bar.AppCodeDummyKt")).isNotNull() + } } diff --git a/ksp/src/test/kotlin/com/tschuchort/compiletesting/TestClasses.kt b/ksp/src/test/kotlin/com/tschuchort/compiletesting/TestClasses.kt new file mode 100644 index 00000000..f670d597 --- /dev/null +++ b/ksp/src/test/kotlin/com/tschuchort/compiletesting/TestClasses.kt @@ -0,0 +1,14 @@ +package com.tschuchort.compiletesting + +enum class AnnotationEnumValue { + ONE, TWO, THREE +} + +annotation class AnotherAnnotation(val input: String) + +annotation class ClasspathTestAnnotation( + val enumValue: AnnotationEnumValue, + val enumValueArray: Array, + val anotherAnnotation: AnotherAnnotation, + val anotherAnnotationArray: Array, +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2ccbaa14..9bf6f879 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,16 +37,6 @@ dependencyResolutionManagement { google() - // Kotlin dev repository, useful for testing against Kotlin dev builds. Usually only tested on CI shadow jobs - // https://kotlinlang.slack.com/archives/C0KLZSCHF/p1616514468003200?thread_ts=1616509748.001400&cid=C0KLZSCHF - maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/") { - name = "Kotlin-Dev" - content { - // this repository *only* contains Kotlin artifacts (don't try others here) - includeGroupByRegex("org\\.jetbrains.*") - } - } - maven("https://www.jetbrains.com/intellij-repository/releases") { name = "Intellij" } @@ -83,16 +73,6 @@ pluginManagement { google() - // Kotlin bootstrap repository, useful for testing against Kotlin dev builds. Usually only tested on CI shadow jobs - // https://kotlinlang.slack.com/archives/C0KLZSCHF/p1616514468003200?thread_ts=1616509748.001400&cid=C0KLZSCHF - maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/") { - name = "Kotlin-Dev" - content { - // this repository *only* contains Kotlin artifacts (don't try others here) - includeGroupByRegex("org\\.jetbrains.*") - } - } - // Gradle's plugin portal proxies jcenter, which we don't want. To avoid this, we specify // exactly which dependencies to pull from here. exclusiveContent { @@ -108,7 +88,7 @@ pluginManagement { } } } - plugins { id("com.gradle.enterprise") version "3.12.1" } + plugins { id("com.gradle.enterprise") version "3.17.4" } } rootProject.name = "kotlin-compile-testing"