Skip to content

Commit 671aed2

Browse files
committed
Support single Java source files in kotlinc arguments
E.g. "kotlinc foo.kt test/Bar.java" will compile foo.kt, and declarations from Bar.java will be accessible to Kotlin code in foo.kt. The change in AbstractTopLevelMembersInvocationTest is needed because an incorrect configuration was created in that test where a library jar was also a Java source root (the compiler is never configured this way in production), which led to an exception in JavaCoreProjectEnvironment#addSourcesToClasspath #KT-17697 Fixed
1 parent 511c9f8 commit 671aed2

File tree

20 files changed

+252
-31
lines changed

20 files changed

+252
-31
lines changed

compiler/cli/src/org/jetbrains/kotlin/cli/jvm/K2JVMCompiler.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.jetbrains.kotlin.cli.jvm
1818

19+
import com.intellij.ide.highlighter.JavaFileType
1920
import com.intellij.openapi.Disposable
2021
import org.jetbrains.kotlin.cli.common.CLICompiler
2122
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
@@ -94,11 +95,16 @@ class K2JVMCompiler : CLICompiler<K2JVMCompilerArguments>() {
9495
}
9596
else if (arguments.module == null) {
9697
for (arg in arguments.freeArgs) {
97-
configuration.addKotlinSourceRoot(arg)
9898
val file = File(arg)
99-
if (file.isDirectory) {
99+
if (file.extension == JavaFileType.DEFAULT_EXTENSION) {
100100
configuration.addJavaSourceRoot(file)
101101
}
102+
else {
103+
configuration.addKotlinSourceRoot(arg)
104+
if (file.isDirectory) {
105+
configuration.addJavaSourceRoot(file)
106+
}
107+
}
102108
}
103109
}
104110

compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.intellij.psi.search.GlobalSearchScope
2626
import gnu.trove.THashMap
2727
import org.jetbrains.kotlin.cli.jvm.index.JavaRoot
2828
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex
29+
import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex
2930
import org.jetbrains.kotlin.load.java.structure.JavaClass
3031
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
3132
import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryClassSignatureParser
@@ -36,21 +37,23 @@ import org.jetbrains.kotlin.name.ClassId
3637
import org.jetbrains.kotlin.name.FqName
3738
import org.jetbrains.kotlin.resolve.jvm.KotlinCliJavaFileManager
3839
import org.jetbrains.kotlin.util.PerformanceCounter
40+
import org.jetbrains.kotlin.utils.addIfNotNull
3941
import java.util.*
40-
import kotlin.properties.Delegates
4142

4243
// TODO: do not inherit from CoreJavaFileManager to avoid accidental usage of its methods which do not use caches/indices
4344
// Currently, the only relevant usage of this class as CoreJavaFileManager is at CoreJavaDirectoryService.getPackage,
4445
// which is indirectly invoked from PsiPackage.getSubPackages
4546
class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJavaFileManager(myPsiManager), KotlinCliJavaFileManager {
4647
private val perfCounter = PerformanceCounter.create("Find Java class")
47-
private var index: JvmDependenciesIndex by Delegates.notNull()
48+
private lateinit var index: JvmDependenciesIndex
49+
private lateinit var singleJavaFileRootsIndex: SingleJavaFileRootsIndex
4850
private val topLevelClassesCache: MutableMap<FqName, VirtualFile?> = THashMap()
4951
private val allScope = GlobalSearchScope.allScope(myPsiManager.project)
5052
private var useFastClassFilesReading = false
5153

52-
fun initialize(packagesCache: JvmDependenciesIndex, useFastClassFilesReading: Boolean) {
53-
this.index = packagesCache
54+
fun initialize(index: JvmDependenciesIndex, singleJavaFileRootsIndex: SingleJavaFileRootsIndex, useFastClassFilesReading: Boolean) {
55+
this.index = index
56+
this.singleJavaFileRootsIndex = singleJavaFileRootsIndex
5457
this.useFastClassFilesReading = useFastClassFilesReading
5558
}
5659

@@ -64,6 +67,7 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
6467
index.findClass(classId) { dir, type ->
6568
findVirtualFileGivenPackage(dir, relativeClassName, type)
6669
}
70+
?: singleJavaFileRootsIndex.findJavaSourceClass(classId)
6771
}?.takeIf { it in searchScope }
6872
}
6973

@@ -153,6 +157,13 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
153157
// traverse all
154158
true
155159
}
160+
161+
result.addIfNotNull(
162+
singleJavaFileRootsIndex.findJavaSourceClass(classId)
163+
?.takeIf { it in scope }
164+
?.findPsiClassInVirtualFile(relativeClassName)
165+
)
166+
156167
if (result.isNotEmpty()) {
157168
return@time result.toTypedArray()
158169
}
@@ -169,10 +180,10 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
169180
//abort on first found
170181
false
171182
}
172-
if (found) {
173-
return PsiPackageImpl(myPsiManager, packageName)
183+
if (!found) {
184+
found = singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName).isNotEmpty()
174185
}
175-
return null
186+
return if (found) PsiPackageImpl(myPsiManager, packageName) else null
176187
}
177188

178189
private fun findVirtualFileGivenPackage(
@@ -216,6 +227,11 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
216227
true
217228
})
218229

230+
for (classId in singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName)) {
231+
assert(!classId.isNestedClass) { "ClassId of a single .java source class should not be nested: $classId" }
232+
result.add(classId.shortClassName.asString())
233+
}
234+
219235
return result
220236
}
221237

compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*
7272
import org.jetbrains.kotlin.cli.common.toBooleanLenient
7373
import org.jetbrains.kotlin.cli.jvm.JvmRuntimeVersionsConsistencyChecker
7474
import org.jetbrains.kotlin.cli.jvm.config.*
75-
import org.jetbrains.kotlin.cli.jvm.index.JavaRoot
76-
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesDynamicCompoundIndex
77-
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex
78-
import org.jetbrains.kotlin.cli.jvm.index.JvmUpdateableDependenciesIndexFactory
75+
import org.jetbrains.kotlin.cli.jvm.index.*
7976
import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem
8077
import org.jetbrains.kotlin.cli.jvm.modules.JavaModuleInfo
8178
import org.jetbrains.kotlin.cli.jvm.modules.ModuleGraph
@@ -207,11 +204,16 @@ class KotlinCoreEnvironment private constructor(
207204
// REPL and kapt2 update classpath dynamically
208205
val indexFactory = JvmUpdateableDependenciesIndexFactory()
209206

210-
rootsIndex = indexFactory.makeIndexFor(initialRoots)
207+
val (roots, singleJavaFileRoots) =
208+
initialRoots.partition { (file) -> file.isDirectory || file.extension != JavaFileType.DEFAULT_EXTENSION }
209+
rootsIndex = indexFactory.makeIndexFor(roots)
211210
updateClasspathFromRootsIndex(rootsIndex)
212211

213-
(ServiceManager.getService(project, CoreJavaFileManager::class.java) as KotlinCliJavaFileManagerImpl)
214-
.initialize(rootsIndex, configuration.getBoolean(JVMConfigurationKeys.USE_FAST_CLASS_FILES_READING))
212+
(ServiceManager.getService(project, CoreJavaFileManager::class.java) as KotlinCliJavaFileManagerImpl).initialize(
213+
rootsIndex,
214+
SingleJavaFileRootsIndex(singleJavaFileRoots),
215+
configuration.getBoolean(JVMConfigurationKeys.USE_FAST_CLASS_FILES_READING)
216+
)
215217

216218
val finderFactory = CliVirtualFileFinderFactory(rootsIndex)
217219
project.registerService(MetadataFinderFactory::class.java, finderFactory)
@@ -230,7 +232,7 @@ class KotlinCoreEnvironment private constructor(
230232

231233
private val allJavaFiles: List<File>
232234
get() = configuration.javaSourceRoots
233-
.mapNotNull(this::findLocalDirectory)
235+
.mapNotNull(this::findLocalFile)
234236
.flatMap { it.javaFiles }
235237
.map { File(it.canonicalPath) }
236238

@@ -337,13 +339,9 @@ class KotlinCoreEnvironment private constructor(
337339
fun tryUpdateClasspath(files: Iterable<File>): List<File>? = updateClasspath(files.map(::JvmClasspathRoot))
338340

339341
fun contentRootToVirtualFile(root: JvmContentRoot): VirtualFile? {
340-
when (root) {
341-
is JvmClasspathRoot -> {
342-
return if (root.file.isFile) findJarRoot(root) else findLocalDirectory(root)
343-
}
344-
is JavaSourceRoot -> {
345-
return if (root.file.isDirectory) findLocalDirectory(root) else null
346-
}
342+
return when (root) {
343+
is JvmClasspathRoot -> if (root.file.isFile) findJarRoot(root) else findLocalFile(root)
344+
is JavaSourceRoot -> findLocalFile(root)
347345
else -> throw IllegalStateException("Unexpected root: $root")
348346
}
349347
}
@@ -352,19 +350,16 @@ class KotlinCoreEnvironment private constructor(
352350

353351
fun findJarFile(path: String) = applicationEnvironment.jarFileSystem.findFileByPath(path)
354352

355-
private fun findLocalDirectory(root: JvmContentRoot): VirtualFile? {
353+
private fun findLocalFile(root: JvmContentRoot): VirtualFile? {
356354
val path = root.file
357-
val localFile = findLocalDirectory(path.absolutePath)
355+
val localFile = findLocalFile(path.absolutePath)
358356
if (localFile == null) {
359357
report(STRONG_WARNING, "Classpath entry points to a non-existent location: $path")
360358
return null
361359
}
362360
return localFile
363361
}
364362

365-
internal fun findLocalDirectory(absolutePath: String): VirtualFile? =
366-
applicationEnvironment.localFileSystem.findFileByPath(absolutePath)
367-
368363
private fun findJarRoot(root: JvmClasspathRoot): VirtualFile? =
369364
applicationEnvironment.jarFileSystem.findFileByPath("${root.file}${URLUtil.JAR_SEPARATOR}")
370365

compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ object KotlinToJVMBytecodeCompiler {
362362
override fun analyze(): AnalysisResult {
363363
val project = environment.project
364364
val moduleOutputs = environment.configuration.get(JVMConfigurationKeys.MODULES)?.mapNotNull { module ->
365-
environment.findLocalDirectory(module.getOutputDirectory())
365+
environment.findLocalFile(module.getOutputDirectory())
366366
}.orEmpty()
367367
val sourcesOnly = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, environment.getSourceFiles())
368368
// To support partial and incremental compilation, we add the scope which contains binaries from output directories
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2010-2017 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.cli.jvm.index
18+
19+
import com.intellij.lang.java.lexer.JavaLexer
20+
import com.intellij.openapi.vfs.VirtualFile
21+
import com.intellij.pom.java.LanguageLevel
22+
import com.intellij.psi.impl.source.tree.ElementType
23+
import com.intellij.psi.tree.IElementType
24+
import org.jetbrains.kotlin.name.ClassId
25+
import org.jetbrains.kotlin.name.FqName
26+
import org.jetbrains.kotlin.name.Name
27+
28+
class SingleJavaFileRootsIndex(private val roots: List<JavaRoot>) {
29+
init {
30+
for ((file) in roots) {
31+
assert(!file.isDirectory) { "Should not be a directory: $file" }
32+
}
33+
}
34+
35+
private val classIdsInRoots = ArrayList<List<ClassId>>(roots.size)
36+
37+
fun findJavaSourceClass(classId: ClassId): VirtualFile? =
38+
roots.indices
39+
.find { index -> classId in getClassIdsForRootAt(index) }
40+
?.let { index -> roots[index].file }
41+
42+
fun findJavaSourceClasses(packageFqName: FqName): List<ClassId> =
43+
roots.indices.flatMap(this::getClassIdsForRootAt).filter { root -> root.packageFqName == packageFqName }
44+
45+
private fun getClassIdsForRootAt(index: Int): List<ClassId> {
46+
for (i in classIdsInRoots.size..index) {
47+
classIdsInRoots.add(JavaSourceClassIdReader(roots[i].file).readClassIds())
48+
}
49+
return classIdsInRoots[index]
50+
}
51+
52+
/**
53+
* Given a .java file, [readClassIds] uses lexer to determine which classes are declared in that file
54+
*/
55+
private class JavaSourceClassIdReader(file: VirtualFile) {
56+
private val lexer = JavaLexer(LanguageLevel.JDK_1_9).apply {
57+
start(String(file.contentsToByteArray()))
58+
}
59+
private var braceBalance = 0
60+
61+
private fun at(type: IElementType): Boolean = lexer.tokenType == type
62+
63+
private fun end(): Boolean = lexer.tokenType == null
64+
65+
private fun advance() {
66+
when {
67+
at(ElementType.LBRACE) -> braceBalance++
68+
at(ElementType.RBRACE) -> braceBalance--
69+
}
70+
lexer.advance()
71+
}
72+
73+
private fun tokenText(): String = lexer.tokenText
74+
75+
private fun atClass(): Boolean =
76+
braceBalance == 0 && lexer.tokenType in CLASS_KEYWORDS
77+
78+
fun readClassIds(): List<ClassId> {
79+
var packageFqName = FqName.ROOT
80+
while (!end() && !at(ElementType.PACKAGE_KEYWORD) && !atClass()) {
81+
advance()
82+
}
83+
if (at(ElementType.PACKAGE_KEYWORD)) {
84+
val packageName = StringBuilder()
85+
while (!end() && !at(ElementType.SEMICOLON)) {
86+
if (at(ElementType.IDENTIFIER) || at(ElementType.DOT)) {
87+
packageName.append(tokenText())
88+
}
89+
advance()
90+
}
91+
packageFqName = FqName(packageName.toString())
92+
}
93+
94+
val result = ArrayList<ClassId>(1)
95+
96+
while (true) {
97+
while (!end() && !atClass()) {
98+
advance()
99+
}
100+
if (end()) break
101+
while (!end() && !at(ElementType.IDENTIFIER)) {
102+
advance()
103+
}
104+
if (end()) break
105+
result.add(ClassId(packageFqName, Name.identifier(tokenText())))
106+
}
107+
108+
return result
109+
}
110+
111+
companion object {
112+
private val CLASS_KEYWORDS = setOf(ElementType.CLASS_KEYWORD, ElementType.INTERFACE_KEYWORD, ElementType.ENUM_KEYWORD)
113+
}
114+
}
115+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
$TESTDATA_DIR$/singleJavaFileRoots/test.kt
2+
$TESTDATA_DIR$/singleJavaFileRoots/DefaultPackage.java
3+
$TESTDATA_DIR$/singleJavaFileRoots/lib/A.java
4+
$TESTDATA_DIR$/singleJavaFileRoots/lib/B.java
5+
$TESTDATA_DIR$/singleJavaFileRoots/lib/ext/SeveralClasses.java
6+
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/ClassClass.java
7+
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/InterfaceClass.java
8+
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/EnumClass.java
9+
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/AnnotationClass.java
10+
-d
11+
$TEMP_DIR$
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:7:9: error: cannot access class 'C'. Check your module classpath for missing or conflicting dependencies
2+
B().c()
3+
^
4+
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:8:5: error: unresolved reference: C
5+
C().a()
6+
^
7+
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:12:5: error: cannot access '<init>': it is public/*package*/ in 'PackageLocal1'
8+
PackageLocal1()
9+
^
10+
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:13:5: error: cannot access '<init>': it is public/*package*/ in 'PackageLocal2'
11+
PackageLocal2()
12+
^
13+
COMPILATION_ERROR
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public class DefaultPackage {}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package lib;
2+
3+
public class A {
4+
public B b() {}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package lib;
2+
3+
public class B {
4+
public C c() {}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package lib;
2+
3+
public class C {
4+
public A a() {}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package lib.classKinds;
2+
3+
public @interface AnnotationClass {
4+
EnumClass value();
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package lib.classKinds;
2+
3+
public class ClassClass {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package lib.classKinds;
2+
3+
public enum EnumClass { ENTRY }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package lib.classKinds;
2+
3+
public interface InterfaceClass {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package lib. /* comment */ ext;
2+
3+
class
4+
PackageLocal1 {}
5+
6+
class /* comment */ PackageLocal2 {}

0 commit comments

Comments
 (0)