diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt index a8adced8..76628080 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt @@ -22,7 +22,9 @@ import java.io.OutputStream import java.io.PrintStream import java.net.URI import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths +import kotlin.io.path.absolutePathString /** * Base compilation class for sharing common compiler arguments and @@ -141,6 +143,15 @@ abstract class AbstractKotlinCompilation internal c // Directory for input source files protected val sourcesDir get() = workingDir.resolve("sources") + protected data class SourceWithPath(val path: Path, val isCommonSource: Boolean) + + protected val sourcesWithPath: List by lazy { + sources.map { + val path = Paths.get(it.writeIfNeeded(sourcesDir).absolutePath) + SourceWithPath(path, it.isMultiplatformCommonSource) + } + } + protected inline fun CommonCompilerArguments.trySetDeprecatedOption(optionSimpleName: String, value: T) { try { this.javaClass.getMethod(JvmAbi.setterName(optionSimpleName), T::class.java).invoke(this, value) @@ -169,6 +180,8 @@ abstract class AbstractKotlinCompilation internal c if (languageVersion != null) args.languageVersion = this.languageVersion + args.commonSources = sourcesWithPath.filter { it.isCommonSource }.map { it.path.toString() }.toTypedArray() + configuration(args) /** @@ -203,7 +216,7 @@ 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 { + protected fun compileKotlin(sources: List, compiler: CLICompiler, arguments: A): KotlinCompilation.ExitCode { /** * Here the list of compiler plugins is set @@ -225,7 +238,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)) { + sources.map(Path::absolutePathString).distinct() + if (sources.none(Path::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/KotlinCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt index c0921347..eecc8c40 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt @@ -38,6 +38,7 @@ import java.net.URLClassLoader import java.nio.file.Path import javax.annotation.processing.Processor import javax.tools.* +import kotlin.io.path.absolutePathString data class PluginOption(val pluginId: PluginId, val optionName: OptionName, val optionValue: OptionValue) @@ -350,7 +351,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { } /** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */ - private fun stubsAndApt(sourceFiles: List): ExitCode { + private fun stubsAndApt(sourceFiles: List): ExitCode { if(annotationProcessors.isEmpty()) { log("No services were given. Not running kapt steps.") return ExitCode.OK @@ -396,10 +397,10 @@ class KotlinCompilation : AbstractKotlinCompilation() { ) ) - val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension) - val javaSources = sourceFiles.filter(File::hasJavaFileExtension) + val kotlinSources = sourceFiles.filter(Path::hasKotlinFileExtension) + val javaSources = sourceFiles.filter(Path::hasJavaFileExtension) - val sourcePaths = mutableListOf().apply { + val sourcePaths = mutableListOf().apply { addAll(javaSources) if(kotlinSources.isNotEmpty()) { @@ -414,9 +415,9 @@ class KotlinCompilation : AbstractKotlinCompilation() { 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)) + add(SourceFile.new("emptyKotlinFile.kt", "").writeIfNeeded(kaptBaseDir).toPath()) } - }.map(File::getAbsolutePath).distinct() + }.map(Path::absolutePathString).distinct() if(!isJdk9OrLater()) { try { @@ -446,10 +447,10 @@ class KotlinCompilation : AbstractKotlinCompilation() { } /** Performs the 3rd compilation step to compile Kotlin source files */ - private fun compileJvmKotlin(sourceFiles: List): ExitCode { - val sources = sourceFiles + - kaptKotlinGeneratedDir.listFilesRecursively() + - kaptSourceDir.listFilesRecursively() + private fun compileJvmKotlin(sourceFiles: List): ExitCode { + val sources = sourcesWithPath.map { it.path } + + kaptKotlinGeneratedDir.toPath().listFilesRecursively() + + kaptSourceDir.toPath().listFilesRecursively() return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs()) } @@ -485,9 +486,9 @@ class KotlinCompilation : AbstractKotlinCompilation() { } /** Performs the 4th compilation step to compile Java source files */ - private fun compileJava(sourceFiles: List): ExitCode { - val javaSources = (sourceFiles + kaptSourceDir.listFilesRecursively()) - .filterNot(File::hasKotlinFileExtension) + private fun compileJava(sourceFiles: List): ExitCode { + val javaSources = (sourceFiles + kaptSourceDir.toPath().listFilesRecursively()) + .filterNot(Path::hasKotlinFileExtension) if(javaSources.isEmpty()) return ExitCode.OK @@ -508,7 +509,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand)) val javacArgs = baseJavacArgs(isJavac9OrLater) - val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath)) + val javacProc = ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(Path::absolutePathString)) .directory(workingDir) .redirectErrorStream(true) .start() @@ -558,7 +559,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { OutputStreamWriter(internalMessageStream), javaFileManager, diagnosticCollector, javacArgs, /* classes to be annotation processed */ null, - javaSources.map { FileJavaFileObject(it) } + javaSources.map { FileJavaFileObject(it.toFile()) } .filter { it.kind == JavaFileObject.Kind.SOURCE } ).call() @@ -591,9 +592,6 @@ class KotlinCompilation : AbstractKotlinCompilation() { 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") @@ -618,7 +616,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { withSystemProperty("idea.use.native.fs.for.win", "false") { // step 1 and 2: generate stubs and run annotation processors try { - val exitCode = stubsAndApt(sourceFiles) + val exitCode = stubsAndApt(sourcesWithPath.map { it.path }) if (exitCode != ExitCode.OK) { return makeResult(exitCode) } @@ -627,7 +625,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { } // step 3: compile Kotlin files - compileJvmKotlin(sourceFiles).let { exitCode -> + compileJvmKotlin(sourcesWithPath.map { it.path }).let { exitCode -> if(exitCode != ExitCode.OK) { return makeResult(exitCode) } @@ -635,7 +633,7 @@ class KotlinCompilation : AbstractKotlinCompilation() { } // step 4: compile Java files - return makeResult(compileJava(sourceFiles)) + return makeResult(compileJava(sourcesWithPath.map { it.path })) } private fun makeResult(exitCode: ExitCode): Result { diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt index 52456693..b79dd312 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt @@ -100,9 +100,6 @@ class KotlinJsCompilation : AbstractKotlinCompilation() { sourcesDir.mkdirs() outputDir.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") @@ -119,7 +116,7 @@ class KotlinJsCompilation : AbstractKotlinCompilation() { */ withSystemProperty("idea.use.native.fs.for.win", "false") { // step 1: compile Kotlin files - return makeResult(compileKotlin(sourceFiles, K2JSCompiler(), jsArgs())) + return makeResult(compileKotlin(sourcesWithPath.map { it.path }, K2JSCompiler(), jsArgs())) } } diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt index aac05f11..91ddedf6 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt @@ -11,29 +11,38 @@ import java.io.File abstract class SourceFile { internal abstract fun writeIfNeeded(dir: File): File + /** Marks this source file as a source of the common module in a multiplatform project */ + abstract val isMultiplatformCommonSource: Boolean + companion object { /** * Create a new Java source file for the compilation when the compilation is run + * + * @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project */ - fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true): SourceFile { + fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true, isMultiplatformCommonSource: Boolean = false): SourceFile { require(File(name).hasJavaFileExtension()) val finalContents = if (trimIndent) contents.trimIndent() else contents - return new(name, finalContents) + return new(name, finalContents, isMultiplatformCommonSource = isMultiplatformCommonSource) } /** * Create a new Kotlin source file for the compilation when the compilation is run + * + * @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project */ - fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true): SourceFile { + fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true, isMultiplatformCommonSource: Boolean = false): SourceFile { require(File(name).hasKotlinFileExtension()) val finalContents = if (trimIndent) contents.trimIndent() else contents - return new(name, finalContents) + return new(name, finalContents, isMultiplatformCommonSource = isMultiplatformCommonSource) } /** * Create a new source file for the compilation when the compilation is run + * + * @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project */ - fun new(name: String, contents: String) = object : SourceFile() { + fun new(name: String, contents: String, isMultiplatformCommonSource: Boolean = false) = object : SourceFile() { override fun writeIfNeeded(dir: File): File { val file = dir.resolve(name) file.parentFile.mkdirs() @@ -45,17 +54,23 @@ abstract class SourceFile { return file } + + override val isMultiplatformCommonSource: Boolean = isMultiplatformCommonSource } /** * Compile an existing source file + * + * @param isMultiplatformCommonSource marks this source file as a source of the common module in a multiplatform project */ - fun fromPath(path: File) = object : SourceFile() { + fun fromPath(path: File, isMultiplatformCommonSource: Boolean = false) = object : SourceFile() { init { require(path.isFile) } override fun writeIfNeeded(dir: File): File = path + + override val isMultiplatformCommonSource: Boolean = isMultiplatformCommonSource } } } diff --git a/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt b/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt index 4a33a87b..280e5e0a 100644 --- a/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt +++ b/core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt @@ -31,7 +31,8 @@ internal fun isJdk9OrLater(): Boolean = SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0 internal fun File.listFilesRecursively(): List { - return listFiles().flatMap { file -> + return (listFiles() ?: throw RuntimeException("listFiles() was null. File is not a directory or I/O error occured")) + .flatMap { file -> if(file.isDirectory) file.listFilesRecursively() else @@ -52,6 +53,10 @@ internal fun Path.listFilesRecursively(): List { return files } +internal fun Path.hasKotlinFileExtension() = toFile().hasKotlinFileExtension() + +internal fun Path.hasJavaFileExtension() = toFile().hasJavaFileExtension() + internal fun File.hasKotlinFileExtension() = hasFileExtension(listOf("kt", "kts")) internal fun File.hasJavaFileExtension() = hasFileExtension(listOf("java")) diff --git a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt index 7b8004b0..7dd7d38c 100644 --- a/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt +++ b/core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt @@ -35,6 +35,29 @@ class KotlinCompilationTests { assertClassLoadable(result, "KSource") } + @Test + fun `can compile annotations that may only appear in multiplatform common module sources`() { + val result = KotlinCompilation().apply { + multiplatform = true + sources = listOf( + SourceFile.kotlin( + "kSource.kt", + """ + import kotlin.js.JsExport + + @JsExport + fun add(a: Double, b: Double): Double { + return a + b + } + """.trimIndent(), + isMultiplatformCommonSource = true + ) + ) + }.compile() + + assertThat(ExitCode.OK).isEqualTo(result.exitCode) + } + @Test fun `runs with only java sources`() { val result = defaultCompilerConfig().apply {