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
This commit is contained in:
Alexander Udalov
2017-04-27 19:51:30 +03:00
parent 511c9f86b1
commit 671aed252d
20 changed files with 252 additions and 31 deletions
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.cli.jvm package org.jetbrains.kotlin.cli.jvm
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.cli.common.CLICompiler import org.jetbrains.kotlin.cli.common.CLICompiler
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
@@ -94,11 +95,16 @@ class K2JVMCompiler : CLICompiler<K2JVMCompilerArguments>() {
} }
else if (arguments.module == null) { else if (arguments.module == null) {
for (arg in arguments.freeArgs) { for (arg in arguments.freeArgs) {
configuration.addKotlinSourceRoot(arg)
val file = File(arg) val file = File(arg)
if (file.isDirectory) { if (file.extension == JavaFileType.DEFAULT_EXTENSION) {
configuration.addJavaSourceRoot(file) configuration.addJavaSourceRoot(file)
} }
else {
configuration.addKotlinSourceRoot(arg)
if (file.isDirectory) {
configuration.addJavaSourceRoot(file)
}
}
} }
} }
@@ -26,6 +26,7 @@ import com.intellij.psi.search.GlobalSearchScope
import gnu.trove.THashMap import gnu.trove.THashMap
import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.cli.jvm.index.JavaRoot
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex
import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex
import org.jetbrains.kotlin.load.java.structure.JavaClass import org.jetbrains.kotlin.load.java.structure.JavaClass
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryClassSignatureParser import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryClassSignatureParser
@@ -36,21 +37,23 @@ import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.KotlinCliJavaFileManager import org.jetbrains.kotlin.resolve.jvm.KotlinCliJavaFileManager
import org.jetbrains.kotlin.util.PerformanceCounter import org.jetbrains.kotlin.util.PerformanceCounter
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.* import java.util.*
import kotlin.properties.Delegates
// TODO: do not inherit from CoreJavaFileManager to avoid accidental usage of its methods which do not use caches/indices // TODO: do not inherit from CoreJavaFileManager to avoid accidental usage of its methods which do not use caches/indices
// Currently, the only relevant usage of this class as CoreJavaFileManager is at CoreJavaDirectoryService.getPackage, // Currently, the only relevant usage of this class as CoreJavaFileManager is at CoreJavaDirectoryService.getPackage,
// which is indirectly invoked from PsiPackage.getSubPackages // which is indirectly invoked from PsiPackage.getSubPackages
class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJavaFileManager(myPsiManager), KotlinCliJavaFileManager { class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJavaFileManager(myPsiManager), KotlinCliJavaFileManager {
private val perfCounter = PerformanceCounter.create("Find Java class") private val perfCounter = PerformanceCounter.create("Find Java class")
private var index: JvmDependenciesIndex by Delegates.notNull() private lateinit var index: JvmDependenciesIndex
private lateinit var singleJavaFileRootsIndex: SingleJavaFileRootsIndex
private val topLevelClassesCache: MutableMap<FqName, VirtualFile?> = THashMap() private val topLevelClassesCache: MutableMap<FqName, VirtualFile?> = THashMap()
private val allScope = GlobalSearchScope.allScope(myPsiManager.project) private val allScope = GlobalSearchScope.allScope(myPsiManager.project)
private var useFastClassFilesReading = false private var useFastClassFilesReading = false
fun initialize(packagesCache: JvmDependenciesIndex, useFastClassFilesReading: Boolean) { fun initialize(index: JvmDependenciesIndex, singleJavaFileRootsIndex: SingleJavaFileRootsIndex, useFastClassFilesReading: Boolean) {
this.index = packagesCache this.index = index
this.singleJavaFileRootsIndex = singleJavaFileRootsIndex
this.useFastClassFilesReading = useFastClassFilesReading this.useFastClassFilesReading = useFastClassFilesReading
} }
@@ -64,6 +67,7 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
index.findClass(classId) { dir, type -> index.findClass(classId) { dir, type ->
findVirtualFileGivenPackage(dir, relativeClassName, type) findVirtualFileGivenPackage(dir, relativeClassName, type)
} }
?: singleJavaFileRootsIndex.findJavaSourceClass(classId)
}?.takeIf { it in searchScope } }?.takeIf { it in searchScope }
} }
@@ -153,6 +157,13 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
// traverse all // traverse all
true true
} }
result.addIfNotNull(
singleJavaFileRootsIndex.findJavaSourceClass(classId)
?.takeIf { it in scope }
?.findPsiClassInVirtualFile(relativeClassName)
)
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
return@time result.toTypedArray() return@time result.toTypedArray()
} }
@@ -169,10 +180,10 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
//abort on first found //abort on first found
false false
} }
if (found) { if (!found) {
return PsiPackageImpl(myPsiManager, packageName) found = singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName).isNotEmpty()
} }
return null return if (found) PsiPackageImpl(myPsiManager, packageName) else null
} }
private fun findVirtualFileGivenPackage( private fun findVirtualFileGivenPackage(
@@ -216,6 +227,11 @@ class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) : CoreJ
true true
}) })
for (classId in singleJavaFileRootsIndex.findJavaSourceClasses(packageFqName)) {
assert(!classId.isNestedClass) { "ClassId of a single .java source class should not be nested: $classId" }
result.add(classId.shortClassName.asString())
}
return result return result
} }
@@ -72,10 +72,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*
import org.jetbrains.kotlin.cli.common.toBooleanLenient import org.jetbrains.kotlin.cli.common.toBooleanLenient
import org.jetbrains.kotlin.cli.jvm.JvmRuntimeVersionsConsistencyChecker import org.jetbrains.kotlin.cli.jvm.JvmRuntimeVersionsConsistencyChecker
import org.jetbrains.kotlin.cli.jvm.config.* import org.jetbrains.kotlin.cli.jvm.config.*
import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.cli.jvm.index.*
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesDynamicCompoundIndex
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex
import org.jetbrains.kotlin.cli.jvm.index.JvmUpdateableDependenciesIndexFactory
import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem
import org.jetbrains.kotlin.cli.jvm.modules.JavaModuleInfo import org.jetbrains.kotlin.cli.jvm.modules.JavaModuleInfo
import org.jetbrains.kotlin.cli.jvm.modules.ModuleGraph import org.jetbrains.kotlin.cli.jvm.modules.ModuleGraph
@@ -207,11 +204,16 @@ class KotlinCoreEnvironment private constructor(
// REPL and kapt2 update classpath dynamically // REPL and kapt2 update classpath dynamically
val indexFactory = JvmUpdateableDependenciesIndexFactory() val indexFactory = JvmUpdateableDependenciesIndexFactory()
rootsIndex = indexFactory.makeIndexFor(initialRoots) val (roots, singleJavaFileRoots) =
initialRoots.partition { (file) -> file.isDirectory || file.extension != JavaFileType.DEFAULT_EXTENSION }
rootsIndex = indexFactory.makeIndexFor(roots)
updateClasspathFromRootsIndex(rootsIndex) updateClasspathFromRootsIndex(rootsIndex)
(ServiceManager.getService(project, CoreJavaFileManager::class.java) as KotlinCliJavaFileManagerImpl) (ServiceManager.getService(project, CoreJavaFileManager::class.java) as KotlinCliJavaFileManagerImpl).initialize(
.initialize(rootsIndex, configuration.getBoolean(JVMConfigurationKeys.USE_FAST_CLASS_FILES_READING)) rootsIndex,
SingleJavaFileRootsIndex(singleJavaFileRoots),
configuration.getBoolean(JVMConfigurationKeys.USE_FAST_CLASS_FILES_READING)
)
val finderFactory = CliVirtualFileFinderFactory(rootsIndex) val finderFactory = CliVirtualFileFinderFactory(rootsIndex)
project.registerService(MetadataFinderFactory::class.java, finderFactory) project.registerService(MetadataFinderFactory::class.java, finderFactory)
@@ -230,7 +232,7 @@ class KotlinCoreEnvironment private constructor(
private val allJavaFiles: List<File> private val allJavaFiles: List<File>
get() = configuration.javaSourceRoots get() = configuration.javaSourceRoots
.mapNotNull(this::findLocalDirectory) .mapNotNull(this::findLocalFile)
.flatMap { it.javaFiles } .flatMap { it.javaFiles }
.map { File(it.canonicalPath) } .map { File(it.canonicalPath) }
@@ -337,13 +339,9 @@ class KotlinCoreEnvironment private constructor(
fun tryUpdateClasspath(files: Iterable<File>): List<File>? = updateClasspath(files.map(::JvmClasspathRoot)) fun tryUpdateClasspath(files: Iterable<File>): List<File>? = updateClasspath(files.map(::JvmClasspathRoot))
fun contentRootToVirtualFile(root: JvmContentRoot): VirtualFile? { fun contentRootToVirtualFile(root: JvmContentRoot): VirtualFile? {
when (root) { return when (root) {
is JvmClasspathRoot -> { is JvmClasspathRoot -> if (root.file.isFile) findJarRoot(root) else findLocalFile(root)
return if (root.file.isFile) findJarRoot(root) else findLocalDirectory(root) is JavaSourceRoot -> findLocalFile(root)
}
is JavaSourceRoot -> {
return if (root.file.isDirectory) findLocalDirectory(root) else null
}
else -> throw IllegalStateException("Unexpected root: $root") else -> throw IllegalStateException("Unexpected root: $root")
} }
} }
@@ -352,9 +350,9 @@ class KotlinCoreEnvironment private constructor(
fun findJarFile(path: String) = applicationEnvironment.jarFileSystem.findFileByPath(path) fun findJarFile(path: String) = applicationEnvironment.jarFileSystem.findFileByPath(path)
private fun findLocalDirectory(root: JvmContentRoot): VirtualFile? { private fun findLocalFile(root: JvmContentRoot): VirtualFile? {
val path = root.file val path = root.file
val localFile = findLocalDirectory(path.absolutePath) val localFile = findLocalFile(path.absolutePath)
if (localFile == null) { if (localFile == null) {
report(STRONG_WARNING, "Classpath entry points to a non-existent location: $path") report(STRONG_WARNING, "Classpath entry points to a non-existent location: $path")
return null return null
@@ -362,9 +360,6 @@ class KotlinCoreEnvironment private constructor(
return localFile return localFile
} }
internal fun findLocalDirectory(absolutePath: String): VirtualFile? =
applicationEnvironment.localFileSystem.findFileByPath(absolutePath)
private fun findJarRoot(root: JvmClasspathRoot): VirtualFile? = private fun findJarRoot(root: JvmClasspathRoot): VirtualFile? =
applicationEnvironment.jarFileSystem.findFileByPath("${root.file}${URLUtil.JAR_SEPARATOR}") applicationEnvironment.jarFileSystem.findFileByPath("${root.file}${URLUtil.JAR_SEPARATOR}")
@@ -362,7 +362,7 @@ object KotlinToJVMBytecodeCompiler {
override fun analyze(): AnalysisResult { override fun analyze(): AnalysisResult {
val project = environment.project val project = environment.project
val moduleOutputs = environment.configuration.get(JVMConfigurationKeys.MODULES)?.mapNotNull { module -> val moduleOutputs = environment.configuration.get(JVMConfigurationKeys.MODULES)?.mapNotNull { module ->
environment.findLocalDirectory(module.getOutputDirectory()) environment.findLocalFile(module.getOutputDirectory())
}.orEmpty() }.orEmpty()
val sourcesOnly = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, environment.getSourceFiles()) val sourcesOnly = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(project, environment.getSourceFiles())
// To support partial and incremental compilation, we add the scope which contains binaries from output directories // To support partial and incremental compilation, we add the scope which contains binaries from output directories
@@ -0,0 +1,115 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.cli.jvm.index
import com.intellij.lang.java.lexer.JavaLexer
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.pom.java.LanguageLevel
import com.intellij.psi.impl.source.tree.ElementType
import com.intellij.psi.tree.IElementType
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
class SingleJavaFileRootsIndex(private val roots: List<JavaRoot>) {
init {
for ((file) in roots) {
assert(!file.isDirectory) { "Should not be a directory: $file" }
}
}
private val classIdsInRoots = ArrayList<List<ClassId>>(roots.size)
fun findJavaSourceClass(classId: ClassId): VirtualFile? =
roots.indices
.find { index -> classId in getClassIdsForRootAt(index) }
?.let { index -> roots[index].file }
fun findJavaSourceClasses(packageFqName: FqName): List<ClassId> =
roots.indices.flatMap(this::getClassIdsForRootAt).filter { root -> root.packageFqName == packageFqName }
private fun getClassIdsForRootAt(index: Int): List<ClassId> {
for (i in classIdsInRoots.size..index) {
classIdsInRoots.add(JavaSourceClassIdReader(roots[i].file).readClassIds())
}
return classIdsInRoots[index]
}
/**
* Given a .java file, [readClassIds] uses lexer to determine which classes are declared in that file
*/
private class JavaSourceClassIdReader(file: VirtualFile) {
private val lexer = JavaLexer(LanguageLevel.JDK_1_9).apply {
start(String(file.contentsToByteArray()))
}
private var braceBalance = 0
private fun at(type: IElementType): Boolean = lexer.tokenType == type
private fun end(): Boolean = lexer.tokenType == null
private fun advance() {
when {
at(ElementType.LBRACE) -> braceBalance++
at(ElementType.RBRACE) -> braceBalance--
}
lexer.advance()
}
private fun tokenText(): String = lexer.tokenText
private fun atClass(): Boolean =
braceBalance == 0 && lexer.tokenType in CLASS_KEYWORDS
fun readClassIds(): List<ClassId> {
var packageFqName = FqName.ROOT
while (!end() && !at(ElementType.PACKAGE_KEYWORD) && !atClass()) {
advance()
}
if (at(ElementType.PACKAGE_KEYWORD)) {
val packageName = StringBuilder()
while (!end() && !at(ElementType.SEMICOLON)) {
if (at(ElementType.IDENTIFIER) || at(ElementType.DOT)) {
packageName.append(tokenText())
}
advance()
}
packageFqName = FqName(packageName.toString())
}
val result = ArrayList<ClassId>(1)
while (true) {
while (!end() && !atClass()) {
advance()
}
if (end()) break
while (!end() && !at(ElementType.IDENTIFIER)) {
advance()
}
if (end()) break
result.add(ClassId(packageFqName, Name.identifier(tokenText())))
}
return result
}
companion object {
private val CLASS_KEYWORDS = setOf(ElementType.CLASS_KEYWORD, ElementType.INTERFACE_KEYWORD, ElementType.ENUM_KEYWORD)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
$TESTDATA_DIR$/singleJavaFileRoots/test.kt
$TESTDATA_DIR$/singleJavaFileRoots/DefaultPackage.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/A.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/B.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/ext/SeveralClasses.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/ClassClass.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/InterfaceClass.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/EnumClass.java
$TESTDATA_DIR$/singleJavaFileRoots/lib/classKinds/AnnotationClass.java
-d
$TEMP_DIR$
+13
View File
@@ -0,0 +1,13 @@
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:7:9: error: cannot access class 'C'. Check your module classpath for missing or conflicting dependencies
B().c()
^
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:8:5: error: unresolved reference: C
C().a()
^
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:12:5: error: cannot access '<init>': it is public/*package*/ in 'PackageLocal1'
PackageLocal1()
^
compiler/testData/cli/jvm/singleJavaFileRoots/test.kt:13:5: error: cannot access '<init>': it is public/*package*/ in 'PackageLocal2'
PackageLocal2()
^
COMPILATION_ERROR
@@ -0,0 +1 @@
public class DefaultPackage {}
@@ -0,0 +1,5 @@
package lib;
public class A {
public B b() {}
}
@@ -0,0 +1,5 @@
package lib;
public class B {
public C c() {}
}
@@ -0,0 +1,5 @@
package lib;
public class C {
public A a() {}
}
@@ -0,0 +1,5 @@
package lib.classKinds;
public @interface AnnotationClass {
EnumClass value();
}
@@ -0,0 +1,3 @@
package lib.classKinds;
public class ClassClass {}
@@ -0,0 +1,3 @@
package lib.classKinds;
public enum EnumClass { ENTRY }
@@ -0,0 +1,3 @@
package lib.classKinds;
public interface InterfaceClass {}
@@ -0,0 +1,6 @@
package lib. /* comment */ ext;
class
PackageLocal1 {}
class /* comment */ PackageLocal2 {}
+21
View File
@@ -0,0 +1,21 @@
import lib.*
import lib.ext.*
import lib.classKinds.*
fun main(args: Array<String>) {
A().b()
B().c()
C().a()
DefaultPackage()
PackageLocal1()
PackageLocal2()
}
@AnnotationClass(EnumClass.ENTRY)
fun classKinds(
c: ClassClass,
i: InterfaceClass,
e: EnumClass
) {}
@@ -61,7 +61,7 @@ public abstract class AbstractTopLevelMembersInvocationTest extends AbstractByte
getTestRootDisposable(), getTestRootDisposable(),
KotlinTestUtils.newConfiguration( KotlinTestUtils.newConfiguration(
ConfigurationKind.JDK_ONLY, TestJdkKind.MOCK_JDK, ConfigurationKind.JDK_ONLY, TestJdkKind.MOCK_JDK,
CollectionsKt.plus(classPath, KotlinTestUtils.getAnnotationsJar()), classPath CollectionsKt.plus(classPath, KotlinTestUtils.getAnnotationsJar()), Collections.emptyList()
), ),
EnvironmentConfigFiles.JVM_CONFIG_FILES); EnvironmentConfigFiles.JVM_CONFIG_FILES);
@@ -332,6 +332,12 @@ public class CliTestGenerated extends AbstractCliTest {
doJvmTest(fileName); doJvmTest(fileName);
} }
@TestMetadata("singleJavaFileRoots.args")
public void testSingleJavaFileRoots() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/jvm/singleJavaFileRoots.args");
doJvmTest(fileName);
}
@TestMetadata("suppressAllWarningsJvm.args") @TestMetadata("suppressAllWarningsJvm.args")
public void testSuppressAllWarningsJvm() throws Exception { public void testSuppressAllWarningsJvm() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/jvm/suppressAllWarningsJvm.args"); String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/cli/jvm/suppressAllWarningsJvm.args");
@@ -26,6 +26,7 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.cli.jvm.index.JavaRoot
import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndexImpl import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndexImpl
import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
@@ -202,6 +203,7 @@ class KotlinCliJavaFileManagerTest : KotlinTestWithEnvironment() {
val root = environment.contentRootToVirtualFile(JavaSourceRoot(javaFilesDir!!, null))!! val root = environment.contentRootToVirtualFile(JavaSourceRoot(javaFilesDir!!, null))!!
coreJavaFileManager.initialize( coreJavaFileManager.initialize(
JvmDependenciesIndexImpl(listOf(JavaRoot(root, JavaRoot.RootType.SOURCE))), JvmDependenciesIndexImpl(listOf(JavaRoot(root, JavaRoot.RootType.SOURCE))),
SingleJavaFileRootsIndex(emptyList()),
useFastClassFilesReading = true useFastClassFilesReading = true
) )