From d76f293dc9d0e2897f3e44c3955c24a7923fa687 Mon Sep 17 00:00:00 2001 From: "Pavel V. Talanov" Date: Tue, 17 Mar 2015 15:46:04 +0300 Subject: [PATCH] Speed up finding kotlin binaries and java files in CLI Change VirtualFileFinder api to accept ClassId and adjust usages Introduce KotlinCoreProjectEnvironment to create special KotlinCliJavaFileManager which shares cache with CliVirtualFileFinder Truly distinguish between java source roots and binary roots: don't search for source files in classpath and vica versa Implement JvmDependenciesIndex --- .../codegen/inline/InlineCodegenUtil.java | 12 +- .../kotlin/codegen/inline/MethodInliner.java | 9 +- .../jvm/compiler/CliVirtualFileFinder.java | 79 ------ .../cli/jvm/compiler/CliVirtualFileFinder.kt | 34 +++ ...ry.java => CliVirtualFileFinderFactory.kt} | 23 +- .../cli/jvm/compiler/JvmDependenciesIndex.kt | 251 ++++++++++++++++++ .../compiler/KotlinCliJavaFileManagerImpl.kt | 160 +++++++++++ .../cli/jvm/compiler/KotlinCoreEnvironment.kt | 23 +- ...h.java => KotlinCoreProjectEnvironment.kt} | 33 +-- .../kotlin/load/java/JavaClassFinderImpl.java | 6 +- .../kotlin/load/kotlin/VirtualFileFinder.java | 4 +- .../kotlin/VirtualFileKotlinClassFinder.kt | 2 +- .../resolve/jvm/KotlinCliJavaFileManager.kt | 26 ++ .../resolve/jvm/KotlinJavaPsiFacade.java | 37 +-- .../cli/jvm/KotlinCliJavaFileManagerTest.kt | 176 ++++++++++++ .../navigation/DecompiledNavigationUtils.java | 39 ++- .../vfilefinder/IDEVirtualFileFinder.java | 13 +- .../IDEVirtualFileFinderFactory.java | 10 +- .../stubBuilder/ClsStubConsistencyTest.kt | 13 +- .../DecompiledTextConsistencyTest.kt | 3 +- 20 files changed, 755 insertions(+), 198 deletions(-) delete mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.java create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.kt rename compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/{CliVirtualFileFinderFactory.java => CliVirtualFileFinderFactory.kt} (53%) create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmDependenciesIndex.kt create mode 100644 compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt rename compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/{ClassPath.java => KotlinCoreProjectEnvironment.kt} (51%) create mode 100644 compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinCliJavaFileManager.kt create mode 100644 compiler/tests/org/jetbrains/kotlin/cli/jvm/KotlinCliJavaFileManagerTest.kt diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/InlineCodegenUtil.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/InlineCodegenUtil.java index b7bf3d6635e..156e0c236f3 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/InlineCodegenUtil.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/InlineCodegenUtil.java @@ -44,6 +44,7 @@ import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils; import org.jetbrains.kotlin.resolve.DescriptorUtils; import org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilPackage; import org.jetbrains.kotlin.resolve.jvm.AsmTypes; +import org.jetbrains.kotlin.resolve.jvm.JvmClassName; import org.jetbrains.kotlin.serialization.ProtoBuf; import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedSimpleFunctionDescriptor; import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf; @@ -59,6 +60,7 @@ import java.io.StringWriter; import java.util.Arrays; import java.util.ListIterator; +import static kotlin.KotlinPackage.substringAfterLast; import static org.jetbrains.kotlin.resolve.DescriptorUtils.getFqName; import static org.jetbrains.kotlin.resolve.DescriptorUtils.isTrait; @@ -150,7 +152,7 @@ public class InlineCodegenUtil { @NotNull public static VirtualFile getVirtualFileForCallable(@NotNull ClassId containerClassId, @NotNull GenerationState state) { VirtualFileFinder fileFinder = VirtualFileFinder.SERVICE.getInstance(state.getProject()); - VirtualFile file = fileFinder.findVirtualFileWithHeader(containerClassId.asSingleFqName()); + VirtualFile file = fileFinder.findVirtualFileWithHeader(containerClassId); if (file == null) { throw new IllegalStateException("Couldn't find declaration file for " + containerClassId); } @@ -177,9 +179,13 @@ public class InlineCodegenUtil { } @Nullable - public static VirtualFile findVirtualFile(@NotNull Project project, @NotNull String internalName) { + public static VirtualFile findVirtualFile(@NotNull Project project, @NotNull String internalClassName) { + FqName packageFqName = JvmClassName.byInternalName(internalClassName).getPackageFqName(); + String classNameWithDollars = substringAfterLast(internalClassName, "/", internalClassName); VirtualFileFinder fileFinder = VirtualFileFinder.SERVICE.getInstance(project); - return fileFinder.findVirtualFileWithHeader(new FqName(internalName.replace('/', '.'))); + //TODO: we cannot construct proper classId at this point, we need to read InnerClasses info from class file + // we construct valid.package.name/RelativeClassNameAsSingleName that should work in compiler, but fails for inner classes in IDE + return fileFinder.findVirtualFileWithHeader(new ClassId(packageFqName, Name.identifier(classNameWithDollars))); } //TODO: navigate to inner classes diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MethodInliner.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MethodInliner.java index 33e26a11e2e..b70ea736e8e 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MethodInliner.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MethodInliner.java @@ -25,11 +25,14 @@ import org.jetbrains.kotlin.codegen.StackValue; import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicMethods; import org.jetbrains.kotlin.codegen.state.JetTypeMapper; import org.jetbrains.kotlin.load.java.JvmAnnotationNames.KotlinSyntheticClass; -import org.jetbrains.kotlin.resolve.jvm.JvmClassName; -import org.jetbrains.kotlin.load.kotlin.PackageClassUtils; import org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache; import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinaryClass; -import org.jetbrains.org.objectweb.asm.*; +import org.jetbrains.kotlin.load.kotlin.PackageClassUtils; +import org.jetbrains.kotlin.resolve.jvm.JvmClassName; +import org.jetbrains.org.objectweb.asm.Label; +import org.jetbrains.org.objectweb.asm.MethodVisitor; +import org.jetbrains.org.objectweb.asm.Opcodes; +import org.jetbrains.org.objectweb.asm.Type; import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter; import org.jetbrains.org.objectweb.asm.commons.Method; import org.jetbrains.org.objectweb.asm.commons.RemappingMethodAdapter; diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.java deleted file mode 100644 index 5033784e4e1..00000000000 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2010-2015 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.compiler; - -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache; -import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder; -import org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClassFinder; -import org.jetbrains.kotlin.name.FqName; - -public class CliVirtualFileFinder extends VirtualFileKotlinClassFinder implements VirtualFileFinder { - - @NotNull - private final ClassPath classPath; - - public CliVirtualFileFinder(@NotNull ClassPath path) { - classPath = path; - } - - @Nullable - @Override - public VirtualFile findVirtualFileWithHeader(@NotNull FqName className) { - for (VirtualFile root : classPath) { - VirtualFile fileInRoot = findFileInRoot(className.asString(), root, '.'); - //NOTE: currently we use VirtualFileFinder to find Kotlin binaries only - if (fileInRoot != null && KotlinBinaryClassCache.getKotlinBinaryClass(fileInRoot) != null) { - return fileInRoot; - } - } - return null; - } - - //NOTE: copied with some changes from CoreJavaFileManager - @Nullable - private static VirtualFile findFileInRoot(@NotNull String qName, @NotNull VirtualFile root, char separator) { - String pathRest = qName; - VirtualFile cur = root; - - while (true) { - int dot = pathRest.indexOf(separator); - if (dot < 0) break; - - String pathComponent = pathRest.substring(0, dot); - VirtualFile child = cur.findChild(pathComponent); - - if (child == null) break; - pathRest = pathRest.substring(dot + 1); - cur = child; - } - - String className = pathRest.replace('.', '$'); - VirtualFile vFile = cur.findChild(className + ".class"); - if (vFile != null) { - if (!vFile.isValid()) { - //TODO: log - return null; - } - return vFile; - } - return null; - } - -} diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.kt new file mode 100644 index 00000000000..a1b89e1e560 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinder.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2015 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.compiler + +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder +import org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClassFinder +import org.jetbrains.kotlin.name.ClassId + +public class CliVirtualFileFinder(private val index: JvmDependenciesIndex) : VirtualFileKotlinClassFinder(), VirtualFileFinder { + + override fun findVirtualFileWithHeader(classId: ClassId): VirtualFile? { + val classFileName = classId.getRelativeClassName().asString().replace('.', '$') + return index.findClass(classId, acceptedRootTypes = JavaRoot.OnlyBinary) { dir, _ -> + dir.findChild("$classFileName.class")?.let { + if (it.isValid()) it else null + } + } + } +} diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.kt similarity index 53% rename from compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.java rename to compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.kt index 747ebc0d41b..7f63250c0ea 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CliVirtualFileFinderFactory.kt @@ -14,23 +14,14 @@ * limitations under the License. */ -package org.jetbrains.kotlin.cli.jvm.compiler; +package org.jetbrains.kotlin.cli.jvm.compiler -import com.intellij.psi.search.GlobalSearchScope; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder; -import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory; +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder +import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory -public final class CliVirtualFileFinderFactory implements VirtualFileFinderFactory { - private final ClassPath classPath; - - public CliVirtualFileFinderFactory(@NotNull ClassPath classpath) { - this.classPath = classpath; - } - - @NotNull - @Override - public VirtualFileFinder create(@NotNull GlobalSearchScope scope) { - return new CliVirtualFileFinder(classPath); +public class CliVirtualFileFinderFactory(private val index: JvmDependenciesIndex) : VirtualFileFinderFactory { + override fun create(scope: GlobalSearchScope): VirtualFileFinder { + return CliVirtualFileFinder(index) } } diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmDependenciesIndex.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmDependenciesIndex.kt new file mode 100644 index 00000000000..a7930d7d9e0 --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmDependenciesIndex.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2010-2015 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.compiler + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.containers.IntArrayList +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import java.util.ArrayList +import java.util.EnumSet +import java.util.HashMap +import kotlin.properties.Delegates + +public data class JavaRoot(public val file: VirtualFile, public val type: JavaRoot.RootType) { + public enum class RootType { + SOURCE + BINARY + } + + companion object RootTypes { + public val OnlyBinary: Set = EnumSet.of(RootType.BINARY) + public val SourceAndBinary: Set = EnumSet.of(RootType.BINARY, RootType.SOURCE) + } +} + +// speeds up finding files/classes in classpath/java source roots +// NOT THREADSAFE, needs to be adapted/removed if we want compiler to be multithreaded +// the main idea of this class is for each package to store roots which contains it to avoid excessive file system traversal +public class JvmDependenciesIndex(_roots: List) { + + //these fields are computed based on _roots passed to constructor which are filled in later + private val roots: List by Delegates.lazy { _roots.toList() } + + private val maxIndex: Int + get() = roots.size() + + // each "Cache" object corresponds to a package + private class Cache { + private val innerPackageCaches = HashMap() + + fun get(name: String) = innerPackageCaches.getOrPut(name) { Cache() } + + // indices of roots that are known to contain this package + // if this list contains [1, 3, 5] then roots with indices 1, 3 and 5 are known to contain this package, 2 and 4 are known not to (no information about roots 6 or higher) + // if this list contains maxIndex that means that all roots containing this package are known + val rootIndices = IntArrayList() + } + + // root "Cache" object corresponds to DefaultPackage which exists in every root + private val rootCache: Cache by Delegates.lazy { + with(Cache()) { + roots.indices.forEach { + rootIndices.add(it) + } + rootIndices.add(maxIndex) + rootIndices.trimToSize() + this + } + } + + // holds the request and the result last time we searched for class + // helps improve several scenarios, LazyJavaResolverContext.findClassInJava being the most important + private var lastClassSearch: Pair? = null + + + // findClassGivenDirectory MUST check whether the class with this classId exists in given package + public fun findClass( + classId: ClassId, + acceptedRootTypes: Set = JavaRoot.SourceAndBinary, + findClassGivenDirectory: (VirtualFile, JavaRoot.RootType) -> T? + ): T? { + return search(FindClassRequest(classId, acceptedRootTypes)) { dir, rootType -> + val found = findClassGivenDirectory(dir, rootType) + HandleResult(found, continueSearch = found == null) + } + } + + public fun traverseDirectoriesInPackage( + packageFqName: FqName, + acceptedRootTypes: Set = JavaRoot.SourceAndBinary, + continueSearch: (VirtualFile, JavaRoot.RootType) -> Boolean + ) { + search(TraverseRequest(packageFqName, acceptedRootTypes)) { dir, rootType -> + HandleResult(Unit, continueSearch(dir, rootType)) + } + } + + private data class HandleResult(val result: T?, val continueSearch: Boolean) + + private fun search( + request: SearchRequest, + handler: (VirtualFile, JavaRoot.RootType) -> HandleResult + ): T? { + + // default to searching with given parameters + fun doSearch() = doSearch(request, handler) + + // make a decision based on information saved from last class search + if (request !is FindClassRequest || lastClassSearch == null) { + return doSearch() + } + val (cachedRequest, cachedResult) = lastClassSearch!! + if (cachedRequest.classId != request.classId) { + return doSearch() + } + when (cachedResult) { + is SearchResult.NotFound -> { + val limitedRootTypes = request.acceptedRootTypes.toHashSet() + limitedRootTypes.removeAll(cachedRequest.acceptedRootTypes) + if (limitedRootTypes.isEmpty()) { + return null + } + else { + return doSearch(FindClassRequest(request.classId, limitedRootTypes), handler) + } + } + is SearchResult.Found -> { + if (cachedRequest.acceptedRootTypes == request.acceptedRootTypes) { + return handler(cachedResult.packageDirectory, cachedResult.root.type).result + } + } + } + + return doSearch() + } + + private fun doSearch(request: SearchRequest, handler: (VirtualFile, JavaRoot.RootType) -> HandleResult): T? { + val findClassRequest = request as? FindClassRequest + + fun found(packageDirectory: VirtualFile, root: JavaRoot, result: T): T { + if (findClassRequest != null) { + lastClassSearch = Pair(findClassRequest, SearchResult.Found(packageDirectory, root)) + } + return result + } + + fun notFound(): T? { + if (findClassRequest != null) { + lastClassSearch = Pair(findClassRequest, SearchResult.NotFound) + } + return null + } + + fun handle(root: JavaRoot, targetDirInRoot: VirtualFile): T? { + if (root.type in request.acceptedRootTypes) { + val (result, shouldContinue) = handler(targetDirInRoot, root.type) + if (!shouldContinue) { + return result + } + } + return null + } + + // a list of package sub names, ["org", "jb", "kotlin"] + val packagesPath = request.packageFqName.pathSegments().map { it.getIdentifier() } + // a list of caches corresponding to packages, [default, "org", "org.jb", "org.jb.kotlin"] + val caches = cachesPath(packagesPath) + + var processedRootsUpTo = -1 + // traverse caches starting from last, which contains most specific information + for (cacheIndex in caches.indices.reversed()) { + val cache = caches[cacheIndex] + for (i in cache.rootIndices.size().indices) { + val rootIndex = cache.rootIndices[i] + if (rootIndex <= processedRootsUpTo) continue // roots with those indices have been processed by now + + val directoryInRoot = travelPath(rootIndex, packagesPath, cacheIndex, caches) ?: continue + val root = roots[rootIndex] + val result = handle(root, directoryInRoot) + if (result != null) { + return found(directoryInRoot, root, result) + } + } + processedRootsUpTo = cache.rootIndices.lastOrNull() ?: processedRootsUpTo + } + return notFound() + } + + // try to find a target directory corresponding to package represented by packagesPath in a given root reprenting by index + // possibly filling "Cache" objects with new information + private fun travelPath(rootIndex: Int, packagesPath: List, fillCachesAfter: Int, cachesPath: List): VirtualFile? { + if (rootIndex >= maxIndex) { + for (i in (fillCachesAfter + 1)..cachesPath.size() - 1) { + // we all know roots that contain this package by now + cachesPath[i].rootIndices.add(maxIndex) + cachesPath[i].rootIndices.trimToSize() + } + return null + } + + var currentFile = roots[rootIndex].file + for (pathIndex in packagesPath.indices) { + val subPackageName = packagesPath[pathIndex] + currentFile = currentFile.findChild(subPackageName) ?: return null + val correspondingCacheIndex = pathIndex + 1 + if (correspondingCacheIndex > fillCachesAfter) { + // subPackageName exists in this root + cachesPath[correspondingCacheIndex].rootIndices.add(rootIndex) + } + } + return currentFile + } + + private fun cachesPath(path: List): List { + val caches = ArrayList() + caches.add(rootCache) + var currentCache = rootCache + for (subPackageName in path) { + currentCache = currentCache[subPackageName] + caches.add(currentCache) + } + return caches + } + + private data class FindClassRequest(val classId: ClassId, override val acceptedRootTypes: Set) : SearchRequest { + override val packageFqName: FqName + get() = classId.getPackageFqName() + } + + private data class TraverseRequest( + override val packageFqName: FqName, + override val acceptedRootTypes: Set + ) : SearchRequest + + private trait SearchRequest { + val packageFqName: FqName + val acceptedRootTypes: Set + } + + private trait SearchResult { + class Found(val packageDirectory: VirtualFile, val root: JavaRoot) : SearchResult + + object NotFound : SearchResult + } +} + +private fun IntArrayList.lastOrNull() = if (isEmpty()) null else get(size() - 1) \ No newline at end of file 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 new file mode 100644 index 00000000000..b546aaf0f0d --- /dev/null +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2010-2015 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.compiler + +import com.intellij.core.CoreJavaFileManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiPackage +import com.intellij.psi.impl.file.PsiPackageImpl +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.KotlinCliJavaFileManager +import java.util.ArrayList +import kotlin.properties.Delegates + +public class KotlinCliJavaFileManagerImpl(private val myPsiManager: PsiManager) +: CoreJavaFileManager(myPsiManager), KotlinCliJavaFileManager { + + private var index: JvmDependenciesIndex by Delegates.notNull() + + public fun initIndex(packagesCache: JvmDependenciesIndex) { + this.index = packagesCache + } + + public override fun findClass(classId: ClassId, searchScope: GlobalSearchScope): PsiClass? { + val classNameWithInnerClasses = classId.getRelativeClassName().asString() + return index.findClass(classId) { dir, type -> + findClassGivenPackage(searchScope, dir, classNameWithInnerClasses, type) + } + } + + override fun findClass(qName: String, scope: GlobalSearchScope): PsiClass? { + // this method is called from IDEA to resolve dependencies in Java code + // which supposedly shouldn't have errors so the dependencies exist in general + // Most classes are top level classes so we will try to find them fast + // but we must sometimes fallback to support finding inner/nested classes + return qName.toSafeTopLevelClassId()?.let { classId -> findClass(classId, scope) } + ?: super.findClass(qName, scope) + } + + override fun findClasses(qName: String, scope: GlobalSearchScope): Array { + val classIdAsTopLevelClass = qName.toSafeTopLevelClassId() ?: return super.findClasses(qName, scope) + + val result = ArrayList() + val classNameWithInnerClasses = classIdAsTopLevelClass.getRelativeClassName().asString() + index.traverseDirectoriesInPackage(classIdAsTopLevelClass.getPackageFqName()) { dir, rootType -> + val psiClass = findClassGivenPackage(scope, dir, classNameWithInnerClasses, rootType) + if (psiClass != null) { + result.add(psiClass) + } + // traverse all + true + } + if (result.isEmpty()) { + return super.findClasses(qName, scope) + } + return result.toArray(arrayOfNulls(result.size())) + } + + override fun findPackage(packageName: String): PsiPackage? { + var found = false + index.traverseDirectoriesInPackage(FqName(packageName)) { _, __ -> + found = true + //abort on first found + false + } + if (found) { + return PsiPackageImpl(myPsiManager, packageName) + } + return null + } + + private fun findClassGivenPackage( + scope: GlobalSearchScope, packageDir: VirtualFile, + classNameWithInnerClasses: String, rootType: JavaRoot.RootType + ): PsiClass? { + val topLevelClassName = classNameWithInnerClasses.substringBefore('.') + + val vFile = when (rootType) { + JavaRoot.RootType.BINARY -> packageDir.findChild("$topLevelClassName.class") + JavaRoot.RootType.SOURCE -> packageDir.findChild("$topLevelClassName.java") + } ?: return null + + if (!vFile.isValid()) { + LOG.error("Invalid child of valid parent: ${vFile.getPath()}; ${packageDir.isValid()} path=${packageDir.getPath()}") + return null + } + if (vFile !in scope) { + return null + } + + val file = myPsiManager.findFile(vFile) as? PsiClassOwner ?: return null + return findClassInPsiFile(classNameWithInnerClasses, file) + } + + companion object { + private val LOG = Logger.getInstance(javaClass()) + + private fun findClassInPsiFile(classNameWithInnerClassesDotSeparated: String, file: PsiClassOwner): PsiClass? { + for (topLevelClass in file.getClasses()) { + val candidate = findClassByTopLevelClass(classNameWithInnerClassesDotSeparated, topLevelClass) + if (candidate != null) { + return candidate + } + } + return null + } + + private fun findClassByTopLevelClass(className: String, topLevelClass: PsiClass): PsiClass? { + if (className.indexOf('.') < 0) { + return if (className == topLevelClass.getName()) topLevelClass else null + } + + val segments = StringUtil.split(className, ".").iterator() + if (!segments.hasNext() || segments.next() != topLevelClass.getName()) { + return null + } + var curClass = topLevelClass + while (segments.hasNext()) { + val innerClassName = segments.next() + val innerClass = curClass.findInnerClassByName(innerClassName, false) + if (innerClass == null) { + return null + } + curClass = innerClass + } + return curClass + } + } +} + +// a sad workaround to avoid throwing exception when called from inside IDEA code +private fun String.toSafeTopLevelClassId(): ClassId? = try { + ClassId.topLevel(FqName(this)) +} +catch (e: IllegalArgumentException) { + null +} +catch (e: AssertionError) { + null +} diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt index 975bcfcf555..765de41918e 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt @@ -59,12 +59,14 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.WARNING -import org.jetbrains.kotlin.cli.jvm.config.* +import org.jetbrains.kotlin.cli.jvm.config.JVMConfigurationKeys +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.cli.jvm.config.JvmContentRoot import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.ContentRoot import org.jetbrains.kotlin.config.KotlinSourceRoot import org.jetbrains.kotlin.extensions.ExternalDeclarationsProvider import org.jetbrains.kotlin.idea.JetFileType @@ -89,13 +91,13 @@ public class KotlinCoreEnvironment private( configuration: CompilerConfiguration ) { - private val projectEnvironment: JavaCoreProjectEnvironment = object : JavaCoreProjectEnvironment(parentDisposable, applicationEnvironment) { + private val projectEnvironment: JavaCoreProjectEnvironment = object : KotlinCoreProjectEnvironment(parentDisposable, applicationEnvironment) { override fun preregisterServices() { registerProjectExtensionPoints(Extensions.getArea(getProject())) } } private val sourceFiles = ArrayList() - private val classPath = ClassPath() + private val javaRoots = ArrayList() private val annotationsManager: CoreExternalAnnotationsManager @@ -114,6 +116,9 @@ public class KotlinCoreEnvironment private( registerProjectServices(projectEnvironment) fillClasspath(configuration) + val fileManager = ServiceManager.getService(project, javaClass()) + val index = JvmDependenciesIndex(javaRoots) + (fileManager as KotlinCliJavaFileManagerImpl).initIndex(index) for (path in configuration.getList(JVMConfigurationKeys.ANNOTATIONS_PATH_KEY)) { addExternalAnnotationsRoot(path) @@ -130,7 +135,7 @@ public class KotlinCoreEnvironment private( JetScriptDefinitionProvider.getInstance(project).addScriptDefinitions(configuration.getList(CommonConfigurationKeys.SCRIPT_DEFINITIONS_KEY)) - project.registerService(javaClass(), CliVirtualFileFinderFactory(classPath)) + project.registerService(javaClass(), CliVirtualFileFinderFactory(index)) ExternalDeclarationsProvider.registerExtensionPoint(project) ExpressionCodegenExtension.registerExtensionPoint(project) @@ -163,7 +168,12 @@ public class KotlinCoreEnvironment private( val virtualFile = contentRootToVirtualFile(javaRoot) ?: continue projectEnvironment.addSourcesToClasspath(virtualFile) - classPath.add(virtualFile) + val rootType = when (javaRoot) { + is JavaSourceRoot -> JavaRoot.RootType.SOURCE + is JvmClasspathRoot -> JavaRoot.RootType.BINARY + else -> throw IllegalStateException() + } + javaRoots.add(JavaRoot(virtualFile, rootType)) } } @@ -339,7 +349,6 @@ public class KotlinCoreEnvironment private( platformStatic public fun registerApplicationServices(applicationEnvironment: JavaCoreApplicationEnvironment) { with(applicationEnvironment) { registerFileType(JetFileType.INSTANCE, "kt") - registerFileType(JetFileType.INSTANCE, "ktm") registerFileType(JetFileType.INSTANCE, JetParserDefinition.STD_SCRIPT_SUFFIX) registerParserDefinition(JetParserDefinition()) getApplication().registerService(javaClass(), KotlinBinaryClassCache()) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/ClassPath.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreProjectEnvironment.kt similarity index 51% rename from compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/ClassPath.java rename to compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreProjectEnvironment.kt index 8dead5f9379..7243f21335e 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/ClassPath.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreProjectEnvironment.kt @@ -14,27 +14,16 @@ * limitations under the License. */ -package org.jetbrains.kotlin.cli.jvm.compiler; +package org.jetbrains.kotlin.cli.jvm.compiler -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.NotNull; +import com.intellij.core.JavaCoreApplicationEnvironment +import com.intellij.core.JavaCoreProjectEnvironment +import com.intellij.openapi.Disposable +import com.intellij.psi.PsiManager -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public final class ClassPath implements Iterable { - - @NotNull - private final List roots = new ArrayList(); - - @NotNull - @Override - public Iterator iterator() { - return roots.iterator(); - } - - public void add(@NotNull VirtualFile root) { - roots.add(root); - } -} +open class KotlinCoreProjectEnvironment( + disposable: Disposable, + applicationEnvironment: JavaCoreApplicationEnvironment +) : JavaCoreProjectEnvironment(disposable, applicationEnvironment) { + override fun createCoreFileManager() = KotlinCliJavaFileManagerImpl(PsiManager.getInstance(getProject())) +} \ No newline at end of file diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/load/java/JavaClassFinderImpl.java b/compiler/frontend.java/src/org/jetbrains/kotlin/load/java/JavaClassFinderImpl.java index 51bcaabda62..790862131af 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/load/java/JavaClassFinderImpl.java +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/load/java/JavaClassFinderImpl.java @@ -86,13 +86,11 @@ public class JavaClassFinderImpl implements JavaClassFinder { @Nullable @Override public JavaClass findClass(@NotNull ClassId classId) { - FqName fqName = classId.asSingleFqName(); - - PsiClass psiClass = javaFacade.findClass(fqName.asString(), javaSearchScope); + PsiClass psiClass = javaFacade.findClass(classId, javaSearchScope); if (psiClass == null) return null; JavaClassImpl javaClass = new JavaClassImpl(psiClass); - + FqName fqName = classId.asSingleFqName(); if (!fqName.equals(javaClass.getFqName())) { throw new IllegalStateException("Requested " + fqName + ", got " + javaClass.getFqName()); } diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileFinder.java b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileFinder.java index c75bbe47f11..ad88ad1bc14 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileFinder.java +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileFinder.java @@ -22,7 +22,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.search.GlobalSearchScope; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.name.FqName; +import org.jetbrains.kotlin.name.ClassId; public interface VirtualFileFinder extends KotlinClassFinder { class SERVICE { @@ -33,5 +33,5 @@ public interface VirtualFileFinder extends KotlinClassFinder { } @Nullable - VirtualFile findVirtualFileWithHeader(@NotNull FqName className); + VirtualFile findVirtualFileWithHeader(@NotNull ClassId className); } diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileKotlinClassFinder.kt b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileKotlinClassFinder.kt index ba9c097c922..0fdecc74d35 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileKotlinClassFinder.kt +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/load/kotlin/VirtualFileKotlinClassFinder.kt @@ -23,7 +23,7 @@ import org.jetbrains.kotlin.utils.sure public abstract class VirtualFileKotlinClassFinder : VirtualFileFinder { override fun findKotlinClass(classId: ClassId): KotlinJvmBinaryClass? { - val file = findVirtualFileWithHeader(classId.asSingleFqName()) ?: return null + val file = findVirtualFileWithHeader(classId) ?: return null return KotlinBinaryClassCache.getKotlinBinaryClass(file) } diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinCliJavaFileManager.kt b/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinCliJavaFileManager.kt new file mode 100644 index 00000000000..cecce802338 --- /dev/null +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinCliJavaFileManager.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2015 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.resolve.jvm + +import com.intellij.psi.PsiClass +import com.intellij.psi.impl.file.impl.JavaFileManager +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.name.ClassId + +public trait KotlinCliJavaFileManager : JavaFileManager { + public fun findClass(classId: ClassId, searchScope: GlobalSearchScope): PsiClass? +} \ No newline at end of file diff --git a/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinJavaPsiFacade.java b/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinJavaPsiFacade.java index 2472da1a081..7c5b1dee811 100644 --- a/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinJavaPsiFacade.java +++ b/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/KotlinJavaPsiFacade.java @@ -16,7 +16,6 @@ package org.jetbrains.kotlin.resolve.jvm; -import com.intellij.core.CoreJavaFileManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.project.DumbAware; @@ -44,6 +43,7 @@ import com.intellij.util.messages.MessageBus; import kotlin.Function1; import kotlin.KotlinPackage; import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.name.ClassId; import java.util.ArrayList; import java.util.Arrays; @@ -87,9 +87,11 @@ public class KotlinJavaPsiFacade { }); } - public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) { + public PsiClass findClass(@NotNull ClassId classId, @NotNull GlobalSearchScope scope) { ProgressIndicatorProvider.checkCanceled(); // We hope this method is being called often enough to cancel daemon processes smoothly + String qualifiedName = classId.asSingleFqName().asString(); + if (shouldUseSlowResolve()) { PsiClass[] classes = findClassesInDumbMode(qualifiedName, scope); if (classes.length != 0) { @@ -99,8 +101,14 @@ public class KotlinJavaPsiFacade { } for (KotlinPsiElementFinderWrapper finder : finders()) { - PsiClass aClass = finder.findClass(qualifiedName, scope); - if (aClass != null) return aClass; + if (finder instanceof KotlinPsiElementFinderImpl) { + PsiClass aClass = ((KotlinPsiElementFinderImpl) finder).findClass(classId, scope); + if (aClass != null) return aClass; + } + else { + PsiClass aClass = finder.findClass(qualifiedName, scope); + if (aClass != null) return aClass; + } } return null; @@ -286,14 +294,14 @@ public class KotlinJavaPsiFacade { static class KotlinPsiElementFinderImpl implements KotlinPsiElementFinderWrapper, DumbAware { private final JavaFileManager javaFileManager; - private final boolean isCoreJavaFileManager; + private final boolean isCliFileManager; private final PsiManager psiManager; private final PackageIndex packageIndex; public KotlinPsiElementFinderImpl(Project project) { this.javaFileManager = findJavaFileManager(project); - this.isCoreJavaFileManager = javaFileManager instanceof CoreJavaFileManager; + this.isCliFileManager = javaFileManager instanceof KotlinCliJavaFileManager; this.packageIndex = PackageIndex.getInstance(project); this.psiManager = PsiManager.getInstance(project); @@ -312,20 +320,19 @@ public class KotlinJavaPsiFacade { @Override public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) { - PsiClass aClass = javaFileManager.findClass(qualifiedName, scope); - if (aClass != null) { - //TODO: (module refactoring) CoreJavaFileManager should check scope - if (!isCoreJavaFileManager || scope.contains(aClass.getContainingFile().getOriginalFile().getVirtualFile())) { - return aClass; - } - } + return javaFileManager.findClass(qualifiedName, scope); + } - return null; + public PsiClass findClass(@NotNull ClassId classId, @NotNull GlobalSearchScope scope) { + if (isCliFileManager) { + return ((KotlinCliJavaFileManager) javaFileManager).findClass(classId, scope); + } + return findClass(classId.asSingleFqName().asString(), scope); } @Override public PsiPackage findPackage(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) { - if (isCoreJavaFileManager) { + if (isCliFileManager) { return javaFileManager.findPackage(qualifiedName); } diff --git a/compiler/tests/org/jetbrains/kotlin/cli/jvm/KotlinCliJavaFileManagerTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/jvm/KotlinCliJavaFileManagerTest.kt new file mode 100644 index 00000000000..8d29e870505 --- /dev/null +++ b/compiler/tests/org/jetbrains/kotlin/cli/jvm/KotlinCliJavaFileManagerTest.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2000-2013 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 + +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.testFramework.PlatformTestCase +import com.intellij.testFramework.PsiTestCase +import com.intellij.testFramework.PsiTestUtil +import junit.framework.TestCase +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.cli.jvm.compiler.JavaRoot +import org.jetbrains.kotlin.cli.jvm.compiler.JvmDependenciesIndex +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCliJavaFileManagerImpl +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +//Partial copy of CoreJavaFileManagerTest +public class KotlinCliJavaFileManagerTest : PsiTestCase() { + public fun testCommon() { + val manager = configureManager("package foo;\n\n" + "public class TopLevel {\n" + "public class Inner {\n" + " public class Inner {}\n" + "}\n" + "\n" + "}", "TopLevel") + + assertCanFind(manager, "foo", "TopLevel") + assertCanFind(manager, "foo", "TopLevel.Inner") + assertCanFind(manager, "foo", "TopLevel.Inner.Inner") + + assertCannotFind(manager, "foo", "TopLevel\$Inner.Inner") + assertCannotFind(manager, "foo", "TopLevel.Inner\$Inner") + assertCannotFind(manager, "foo", "TopLevel.Inner.Inner.Inner") + } + + public fun testInnerClassesWithDollars() { + val manager = configureManager("package foo;\n\n" + "public class TopLevel {\n" + + "public class I\$nner {" + " public class I\$nner{}" + " public class \$Inner{}" + " public class In\$ne\$r\${}" + " public class Inner\$\${}" + " public class \$\$\$\$\${}" + "}\n" + "public class Inner\$ {" + " public class I\$nner{}" + " public class \$Inner{}" + " public class In\$ne\$r\${}" + " public class Inner\$\${}" + " public class \$\$\$\$\${}" + "}\n" + "public class In\$ner\$\$ {" + " public class I\$nner{}" + " public class \$Inner{}" + " public class In\$ne\$r\${}" + " public class Inner\$\${}" + " public class \$\$\$\$\${}" + "}\n" + "\n" + "}", "TopLevel") + + assertCanFind(manager, "foo", "TopLevel") + + assertCanFind(manager, "foo", "TopLevel.I\$nner") + assertCanFind(manager, "foo", "TopLevel.I\$nner.I\$nner") + assertCanFind(manager, "foo", "TopLevel.I\$nner.\$Inner") + assertCanFind(manager, "foo", "TopLevel.I\$nner.In\$ne\$r\$") + assertCanFind(manager, "foo", "TopLevel.I\$nner.Inner\$\$") + assertCanFind(manager, "foo", "TopLevel.I\$nner.\$\$\$\$\$") + + assertCannotFind(manager, "foo", "TopLevel.I.nner.\$\$\$\$\$") + + assertCanFind(manager, "foo", "TopLevel.Inner\$") + assertCanFind(manager, "foo", "TopLevel.Inner\$.I\$nner") + assertCanFind(manager, "foo", "TopLevel.Inner\$.\$Inner") + assertCanFind(manager, "foo", "TopLevel.Inner\$.In\$ne\$r\$") + assertCanFind(manager, "foo", "TopLevel.Inner\$.Inner\$\$") + assertCanFind(manager, "foo", "TopLevel.Inner\$.\$\$\$\$\$") + + assertCannotFind(manager, "foo", "TopLevel.Inner..\$\$\$\$\$") + + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$") + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$.I\$nner") + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$.\$Inner") + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$.In\$ne\$r\$") + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$.Inner\$\$") + assertCanFind(manager, "foo", "TopLevel.In\$ner\$\$.\$\$\$\$\$") + + assertCannotFind(manager, "foo", "TopLevel.In.ner\$\$.\$\$\$\$\$") + } + + public fun testTopLevelClassesWithDollars() { + val inTheMiddle = configureManager("package foo;\n\n public class Top\$Level {}", "Top\$Level") + assertCanFind(inTheMiddle, "foo", "Top\$Level") + + val doubleAtTheEnd = configureManager("package foo;\n\n public class TopLevel\$\$ {}", "TopLevel\$\$") + assertCanFind(doubleAtTheEnd, "foo", "TopLevel\$\$") + + val multiple = configureManager("package foo;\n\n public class Top\$Lev\$el\$ {}", "Top\$Lev\$el\$") + assertCanFind(multiple, "foo", "Top\$Lev\$el\$") + assertCannotFind(multiple, "foo", "Top.Lev\$el\$") + + val twoBucks = configureManager("package foo;\n\n public class \$\$ {}", "\$\$") + assertCanFind(twoBucks, "foo", "\$\$") + } + + throws(javaClass()) + public fun testTopLevelClassWithDollarsAndInners() { + val manager = configureManager("package foo;\n\n" + "public class Top\$Level\$\$ {\n" + + "public class I\$nner {" + " public class I\$nner{}" + " public class In\$ne\$r\${}" + " public class Inner\$\$\$\$\${}" + " public class \$Inner{}" + " public class \${}" + " public class \$\$\$\$\${}" + "}\n" + "public class Inner {" + " public class Inner{}" + "}\n" + "\n" + "}", "Top\$Level\$\$") + + assertCanFind(manager, "foo", "Top\$Level\$\$") + + assertCanFind(manager, "foo", "Top\$Level\$\$.Inner") + assertCanFind(manager, "foo", "Top\$Level\$\$.Inner.Inner") + + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.I\$nner") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.In\$ne\$r\$") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.Inner\$\$\$\$\$") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.\$Inner") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.\$") + assertCanFind(manager, "foo", "Top\$Level\$\$.I\$nner.\$\$\$\$\$") + + assertCannotFind(manager, "foo", "Top.Level\$\$.I\$nner.\$\$\$\$\$") + } + + public fun testDoNotThrowOnMalformedInput() { + val fileWithEmptyName = configureManager("package foo;\n\n public class Top\$Level {}", "") + val allScope = GlobalSearchScope.allScope(getProject()) + fileWithEmptyName.findClass("foo.", allScope) + fileWithEmptyName.findClass(".", allScope) + fileWithEmptyName.findClass("..", allScope) + fileWithEmptyName.findClass(".foo", allScope) + } + + public fun testSeveralClassesInOneFile() { + val manager = configureManager("package foo;\n\n" + "public class One {}\n" + "class Two {}\n" + "class Three {}", "One") + + assertCanFind(manager, "foo", "One") + + //NOTE: this is unsupported + assertCannotFind(manager, "foo", "Two") + assertCannotFind(manager, "foo", "Three") + } + + public fun testScopeCheck() { + val manager = configureManager("package foo;\n\n" + "public class Test {}\n", "Test") + + TestCase.assertNotNull("Should find class in all scope", manager.findClass("foo.Test", GlobalSearchScope.allScope(getProject()))) + TestCase.assertNull("Should not find class in empty scope", manager.findClass("foo.Test", GlobalSearchScope.EMPTY_SCOPE)) + } + + private fun configureManager(Language("JAVA") text: String, className: String): KotlinCliJavaFileManagerImpl { + val root = PsiTestUtil.createTestProjectStructure(myProject, myModule, PlatformTestCase.myFilesToDelete) + val pkg = root.createChildDirectory(this, "foo") + val dir = myPsiManager.findDirectory(pkg) + TestCase.assertNotNull(dir) + dir.add(PsiFileFactory.getInstance(getProject()).createFileFromText(className + ".java", JavaFileType.INSTANCE, text)) + val coreJavaFileManagerExt = KotlinCliJavaFileManagerImpl(myPsiManager) + coreJavaFileManagerExt.initIndex(JvmDependenciesIndex(listOf(JavaRoot(root, JavaRoot.RootType.SOURCE)))) + coreJavaFileManagerExt.addToClasspath(root) + return coreJavaFileManagerExt + } + + private fun assertCanFind(manager: KotlinCliJavaFileManagerImpl, packageFQName: String, classFqName: String) { + val allScope = GlobalSearchScope.allScope(getProject()) + + val classId = ClassId(FqName(packageFQName), FqName(classFqName), false) + val stringRequest = classId.asSingleFqName().asString() + + val foundByClassId = manager.findClass(classId, allScope) + val foundByString = manager.findClass(stringRequest, allScope) + + TestCase.assertNotNull("Could not find: $classId", foundByClassId) + TestCase.assertNotNull("Could not find: $stringRequest", foundByString) + + TestCase.assertEquals(foundByClassId, foundByString) + TestCase.assertEquals("Found ${foundByClassId!!.getQualifiedName()} instead of $packageFQName", packageFQName + "." + classFqName, + foundByClassId.getQualifiedName()) + } + + private fun assertCannotFind(manager: KotlinCliJavaFileManagerImpl, packageFQName: String, classFqName: String) { + val classId = ClassId(FqName(packageFQName), FqName(classFqName), false) + val foundClass = manager.findClass(classId, GlobalSearchScope.allScope(getProject())) + TestCase.assertNull("Found, but shouldn't have: $classId", foundClass) + } +} diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/decompiler/navigation/DecompiledNavigationUtils.java b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/decompiler/navigation/DecompiledNavigationUtils.java index 5e922d69196..7d060bb5f5e 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/decompiler/navigation/DecompiledNavigationUtils.java +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/decompiler/navigation/DecompiledNavigationUtils.java @@ -17,27 +17,30 @@ package org.jetbrains.kotlin.idea.decompiler.navigation; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.*; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.search.GlobalSearchScope; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.descriptors.*; +import org.jetbrains.kotlin.descriptors.ClassDescriptor; +import org.jetbrains.kotlin.descriptors.ClassOrPackageFragmentDescriptor; +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor; +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor; import org.jetbrains.kotlin.idea.caches.resolve.JsProjectDetector; import org.jetbrains.kotlin.idea.decompiler.DecompilerPackage; import org.jetbrains.kotlin.idea.decompiler.KotlinClsFileBase; import org.jetbrains.kotlin.idea.stubindex.JetSourceFilterScope; -import org.jetbrains.kotlin.load.kotlin.PackageClassUtils; import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder; import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory; -import org.jetbrains.kotlin.name.FqName; -import org.jetbrains.kotlin.name.FqNameUnsafe; +import org.jetbrains.kotlin.name.ClassId; import org.jetbrains.kotlin.psi.JetDeclaration; import org.jetbrains.kotlin.resolve.DescriptorUtils; +import org.jetbrains.kotlin.types.ErrorUtils; import org.jetbrains.kotlin.types.expressions.ExpressionTypingUtils; -import static org.jetbrains.kotlin.resolve.DescriptorUtils.getFqName; +import static org.jetbrains.kotlin.load.kotlin.PackageClassUtils.getPackageClassId; +import static org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilPackage.getClassId; public final class DecompiledNavigationUtils { @@ -76,34 +79,30 @@ public final class DecompiledNavigationUtils { @NotNull Project project, @NotNull DeclarationDescriptor referencedDescriptor ) { - FqName containerFqName = getContainerFqName(referencedDescriptor); - if (containerFqName == null) { - return null; - } + if (ErrorUtils.isError(referencedDescriptor)) return null; + + ClassId containerClassId = getContainerClassId(referencedDescriptor); + if (containerClassId == null) return null; + GlobalSearchScope scopeToSearchIn = JetSourceFilterScope.kotlinSourceAndClassFiles(GlobalSearchScope.allScope(project), project); VirtualFileFinder fileFinder = VirtualFileFinderFactory.SERVICE.getInstance(project).create(scopeToSearchIn); - VirtualFile virtualFile = fileFinder.findVirtualFileWithHeader(containerFqName); - if (virtualFile == null) { - return null; - } - return virtualFile; + return fileFinder.findVirtualFileWithHeader(containerClassId); } //TODO: navigate to inner classes @Nullable - private static FqName getContainerFqName(@NotNull DeclarationDescriptor referencedDescriptor) { + private static ClassId getContainerClassId(@NotNull DeclarationDescriptor referencedDescriptor) { ClassOrPackageFragmentDescriptor containerDescriptor = DescriptorUtils.getParentOfType(referencedDescriptor, ClassOrPackageFragmentDescriptor.class, false); if (containerDescriptor instanceof PackageFragmentDescriptor) { - return PackageClassUtils.getPackageClassFqName(((PackageFragmentDescriptor) containerDescriptor).getFqName()); + return getPackageClassId(((PackageFragmentDescriptor) containerDescriptor).getFqName()); } if (containerDescriptor instanceof ClassDescriptor) { if (containerDescriptor.getContainingDeclaration() instanceof ClassDescriptor || ExpressionTypingUtils.isLocal(containerDescriptor.getContainingDeclaration(), containerDescriptor)) { - return getContainerFqName(containerDescriptor.getContainingDeclaration()); + return getContainerClassId(containerDescriptor.getContainingDeclaration()); } - FqNameUnsafe fqNameUnsafe = getFqName(containerDescriptor); - return fqNameUnsafe.isSafe() ? fqNameUnsafe.toSafe() : null; + return getClassId((ClassDescriptor) containerDescriptor); } return null; } diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinder.java b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinder.java index 0076856bf37..8222aa4259e 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinder.java +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinder.java @@ -17,7 +17,6 @@ package org.jetbrains.kotlin.idea.vfilefinder; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; @@ -25,7 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder; import org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClassFinder; -import org.jetbrains.kotlin.name.FqName; +import org.jetbrains.kotlin.name.ClassId; import java.util.Collection; @@ -33,11 +32,9 @@ public final class IDEVirtualFileFinder extends VirtualFileKotlinClassFinder imp private static final Logger LOG = Logger.getInstance(IDEVirtualFileFinder.class); - @NotNull private final Project project; @NotNull private final GlobalSearchScope scope; - public IDEVirtualFileFinder(@NotNull Project project, @NotNull GlobalSearchScope scope) { - this.project = project; + public IDEVirtualFileFinder(@NotNull GlobalSearchScope scope) { this.scope = scope; if (scope != GlobalSearchScope.EMPTY_SCOPE && scope.getProject() == null) { @@ -47,13 +44,13 @@ public final class IDEVirtualFileFinder extends VirtualFileKotlinClassFinder imp @Nullable @Override - public VirtualFile findVirtualFileWithHeader(@NotNull FqName className) { - Collection files = FileBasedIndex.getInstance().getContainingFiles(KotlinClassFileIndex.KEY, className, scope); + public VirtualFile findVirtualFileWithHeader(@NotNull ClassId classId) { + Collection files = FileBasedIndex.getInstance().getContainingFiles(KotlinClassFileIndex.KEY, classId.asSingleFqName(), scope); if (files.isEmpty()) { return null; } if (files.size() > 1) { - LOG.warn("There are " + files.size() + " classes with same fqName: " + className + " found."); + LOG.warn("There are " + files.size() + " classes with same fqName: " + classId + " found."); } return files.iterator().next(); } diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinderFactory.java b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinderFactory.java index 45c9fae276d..abf71dbf561 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinderFactory.java +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/vfilefinder/IDEVirtualFileFinderFactory.java @@ -16,7 +16,6 @@ package org.jetbrains.kotlin.idea.vfilefinder; -import com.intellij.openapi.project.Project; import com.intellij.psi.search.GlobalSearchScope; import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.load.kotlin.VirtualFileFinder; @@ -24,16 +23,9 @@ import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory; public class IDEVirtualFileFinderFactory implements VirtualFileFinderFactory { - @NotNull - private final Project project; - - public IDEVirtualFileFinderFactory(@NotNull Project project) { - this.project = project; - } - @NotNull @Override public VirtualFileFinder create(@NotNull GlobalSearchScope scope) { - return new IDEVirtualFileFinder(project, scope); + return new IDEVirtualFileFinder(scope); } } diff --git a/idea/tests/org/jetbrains/kotlin/idea/decompiler/stubBuilder/ClsStubConsistencyTest.kt b/idea/tests/org/jetbrains/kotlin/idea/decompiler/stubBuilder/ClsStubConsistencyTest.kt index 5362b85660c..85ce88941d1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/decompiler/stubBuilder/ClsStubConsistencyTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/decompiler/stubBuilder/ClsStubConsistencyTest.kt @@ -16,14 +16,14 @@ package org.jetbrains.kotlin.idea.decompiler.stubBuilder +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.indexing.FileContentImpl +import org.jetbrains.kotlin.idea.decompiler.textBuilder.buildDecompiledText import org.jetbrains.kotlin.idea.test.JetLightCodeInsightFixtureTestCase -import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.idea.test.JetWithJdkAndRuntimeLightProjectDescriptor import org.jetbrains.kotlin.load.kotlin.PackageClassUtils import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory -import com.intellij.psi.search.GlobalSearchScope -import org.jetbrains.kotlin.idea.decompiler.textBuilder.buildDecompiledText -import org.jetbrains.kotlin.idea.test.JetWithJdkAndRuntimeLightProjectDescriptor -import com.intellij.util.indexing.FileContentImpl +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.JetPsiFactory import org.jetbrains.kotlin.psi.stubs.elements.JetFileStubBuilder import org.junit.Assert @@ -34,9 +34,8 @@ public class ClsStubConsistencyTest : JetLightCodeInsightFixtureTestCase() { public fun testConsistencyForKotlinPackage() { val project = getProject() - val packageClassFqName = PackageClassUtils.getPackageClassFqName(STANDARD_LIBRARY_FQNAME) val virtualFileFinder = VirtualFileFinderFactory.SERVICE.getInstance(project).create(GlobalSearchScope.allScope(project)) - val kotlinPackageFile = virtualFileFinder.findVirtualFileWithHeader(packageClassFqName)!! + val kotlinPackageFile = virtualFileFinder.findVirtualFileWithHeader(PackageClassUtils.getPackageClassId(STANDARD_LIBRARY_FQNAME))!! val decompiledText = buildDecompiledText(kotlinPackageFile).text val clsStub = KotlinClsStubBuilder().buildFileStub(FileContentImpl.createByFile(kotlinPackageFile))!! diff --git a/idea/tests/org/jetbrains/kotlin/idea/decompiler/textBuilder/DecompiledTextConsistencyTest.kt b/idea/tests/org/jetbrains/kotlin/idea/decompiler/textBuilder/DecompiledTextConsistencyTest.kt index c78bd073ef9..af1ba615022 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/decompiler/textBuilder/DecompiledTextConsistencyTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/decompiler/textBuilder/DecompiledTextConsistencyTest.kt @@ -44,9 +44,8 @@ public class DecompiledTextConsistencyTest : JetLightCodeInsightFixtureTestCase( public fun testConsistencyWithJavaDescriptorResolver() { val project = getProject() - val packageClassFqName = PackageClassUtils.getPackageClassFqName(STANDARD_LIBRARY_FQNAME) val virtualFileFinder = VirtualFileFinderFactory.SERVICE.getInstance(project).create(GlobalSearchScope.allScope(project)) - val kotlinPackageFile = virtualFileFinder.findVirtualFileWithHeader(packageClassFqName)!! + val kotlinPackageFile = virtualFileFinder.findVirtualFileWithHeader(PackageClassUtils.getPackageClassId(STANDARD_LIBRARY_FQNAME))!! val projectBasedText = buildDecompiledText(kotlinPackageFile, ProjectBasedResolverForDecompiler(project)).text val deserializedText = buildDecompiledText(kotlinPackageFile).text Assert.assertEquals(projectBasedText, deserializedText)