[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.
This commit is contained in:
Mads Ager
2022-01-28 12:18:14 +01:00
committed by Alexander Udalov
parent fca1909f4a
commit e2c7290214
2 changed files with 59 additions and 3 deletions
@@ -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 }
}
@@ -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)
}
}