From e2c72902143c058c3b9ba0bb2df3b1d5f2413da0 Mon Sep 17 00:00:00 2001 From: Mads Ager Date: Fri, 28 Jan 2022 12:18:14 +0100 Subject: [PATCH] [JVM CLI] Prefer source over classpath. If the class `A` is in a jar `previous.jar`, the following CLI invocation will take that class instead of the `A` class defined in `A.java`: kotlinc -cp previous.jar A.java B.kt This is problematic for build tools that put the jar for a previous build on the classpath when recompiling some of the files. ^KT-51025 Fixed. --- .../compiler/KotlinCliJavaFileManagerImpl.kt | 21 ++++++++-- .../cli/JavaSourceClassInClassPathTest.kt | 41 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 compiler/tests/org/jetbrains/kotlin/cli/JavaSourceClassInClassPathTest.kt diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt index cc9f18d5b49..7469338fc17 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt @@ -73,9 +73,24 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ private fun findVirtualFileForTopLevelClass(classId: ClassId, searchScope: GlobalSearchScope): VirtualFile? { val relativeClassName = classId.relativeClassName.asString() return topLevelClassesCache.getOrPut(classId.packageFqName.child(classId.relativeClassName.pathSegments().first())) { - index.findClass(classId) { dir, type -> - findVirtualFileGivenPackage(dir, relativeClassName, type) - } ?: singleJavaFileRootsIndex.findJavaSourceClass(classId) + // Search java sources first. For build tools, it makes sense to build new files passing all the + // class files for the previous build on the class path. + // + // Suppose we have A.java and B.kt, we compile them and have the class files in previous.jar. + // Now we change both. A field is added to A which is used from B.kt. + // + // For a compilation such as: + // + // kotlinc -cp previous.jar A.java B.kt + // + // we want to make sure that we get the new A.java and not the old version A.class from previous.jar. + // + // Otherwise B.kt will not see the newly added field in A. + singleJavaFileRootsIndex.findJavaSourceClass(classId) + ?: index.findClass(classId) { dir, type -> + findVirtualFileGivenPackage(dir, relativeClassName, type) + } + }?.takeIf { it in searchScope } } diff --git a/compiler/tests/org/jetbrains/kotlin/cli/JavaSourceClassInClassPathTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/JavaSourceClassInClassPathTest.kt new file mode 100644 index 00000000000..12054986fad --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/JavaSourceClassInClassPathTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.cli + +import org.jetbrains.kotlin.cli.common.ExitCode +import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler +import org.jetbrains.kotlin.test.TestCaseWithTmpdir + +class JavaSourceClassInClassPathTest : TestCaseWithTmpdir() { + + // Test that a java source file for a class is taken before a class file for the same + // class on the class path. + fun testDeterministicOutput() { + val aKt = tmpdir.resolve("A.kt").also { + it.writeText("class A") + } + val bKt = tmpdir.resolve("B.kt").also { + it.writeText("fun main() { A() }") + } + val aJava = tmpdir.resolve("A.java").also { + it.writeText("public class A { public int i = 32; }") + } + val bNewKt = tmpdir.resolve("Bnew.kt").also { + it.writeText("fun main() { A().i }") + } + val firstJar = tmpdir.resolve("first.jar") + val (_, exit) = AbstractCliTest.executeCompilerGrabOutput( + K2JVMCompiler(), + listOf(aKt.path, bKt.path, "-d", firstJar.path, "-include-runtime") + ) + assert(exit == ExitCode.OK) + val (_, exit2) = AbstractCliTest.executeCompilerGrabOutput( + K2JVMCompiler(), + listOf("-cp", firstJar.path, aJava.path, bNewKt.path, "-d", firstJar.path, "-include-runtime") + ) + assert(exit2 == ExitCode.OK) + } +}