diff --git a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsClassFinder.kt b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsClassFinder.kt new file mode 100644 index 00000000000..6d995a19482 --- /dev/null +++ b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsClassFinder.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.psi.stubs.file.builder + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.ClassFileViewProvider +import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinaryClass +import org.jetbrains.kotlin.load.kotlin.findKotlinClass +import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.Name + +object ClsClassFinder { + fun findMultifileClassParts(file: VirtualFile, classId: ClassId, partNames: List): List { + val packageFqName = classId.packageFqName + val partsFinder = DirectoryBasedClassFinder(file.parent!!, packageFqName) + + return partNames.mapNotNull { + partsFinder.findKotlinClass(ClassId(packageFqName, Name.identifier(it.substringAfterLast('/')))) + } + } + + /** + * Checks if this file is a compiled "internal" Kotlin class, i.e. a Kotlin class (not necessarily ABI-compatible with the current plugin) + * which should NOT be decompiled (and, as a result, shown under the library in the Project view, be searchable via Find class, etc.) + */ + fun isKotlinInternalCompiledFile(file: VirtualFile, fileContent: ByteArray? = null): Boolean { + // Don't crash on invalid files (EA-97751) + if (!file.isValid || fileContent?.size == 0 || !file.exists()) { + return false + } + + val clsKotlinBinaryClassCache = ClsKotlinBinaryClassCache.getInstance() + + if (!clsKotlinBinaryClassCache.isKotlinJvmCompiledFile(file, fileContent)) { + return false + } + + val innerClass = + try { + if (fileContent == null) { + ClassFileViewProvider.isInnerClass(file) + } else { + ClassFileViewProvider.isInnerClass(file, fileContent) + } + } catch (exception: Exception) { + Logger + .getInstance("org.jetbrains.kotlin.analysis.decompiler.stub.file.ClsClassFinder.isKotlinInternalCompiledFile") + .debug(file.path, exception) + + return false + } + + if (innerClass) { + return true + } + + val header = clsKotlinBinaryClassCache.getKotlinBinaryClassHeaderData(file, fileContent) ?: return false + if (header.classId.isLocal) return true + + return header.kind == KotlinClassHeader.Kind.SYNTHETIC_CLASS || + header.kind == KotlinClassHeader.Kind.MULTIFILE_CLASS_PART + } +} \ No newline at end of file diff --git a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsKotlinBinaryClassCache.kt b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsKotlinBinaryClassCache.kt new file mode 100644 index 00000000000..f633bd143bc --- /dev/null +++ b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/ClsKotlinBinaryClassCache.kt @@ -0,0 +1,132 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.jetbrains.kotlin.psi.stubs.file.builder + +import com.intellij.ide.highlighter.JavaClassFileType +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileWithId +import com.intellij.reference.SoftReference +import org.jetbrains.kotlin.deserialization.common.jvm.impl.KotlinBinaryClassCache +import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinaryClass +import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader +import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMetadataVersion +import org.jetbrains.kotlin.name.ClassId + +class ClsKotlinBinaryClassCache { + class KotlinBinaryClassHeaderData( + val classId: ClassId, + val kind: KotlinClassHeader.Kind, + val metadataVersion: JvmMetadataVersion, + val partNamesIfMultifileFacade: List, + val packageName: String? + ) + + data class KotlinBinaryData(val isKotlinBinary: Boolean, val timestamp: Long, val headerData: KotlinBinaryClassHeaderData?) + + /** + * Checks if this file is a compiled Kotlin class file (not necessarily ABI-compatible with the current plugin) + */ + fun isKotlinJvmCompiledFile(file: VirtualFile, fileContent: ByteArray? = null): Boolean { + if (file.extension != JavaClassFileType.INSTANCE!!.defaultExtension) { + return false + } + + getKotlinBinaryFromCache(file)?.let { + return it.isKotlinBinary + } + return kotlinJvmBinaryClass(file, fileContent) != null + } + + fun getKotlinBinaryClass(file: VirtualFile, fileContent: ByteArray? = null): KotlinJvmBinaryClass? { + val cached = getKotlinBinaryFromCache(file) + if (cached != null && !cached.isKotlinBinary) { + return null + } + + return kotlinJvmBinaryClass(file, fileContent) + } + + private fun kotlinJvmBinaryClass( + file: VirtualFile, + fileContent: ByteArray? + ): KotlinJvmBinaryClass? { + val kotlinBinaryClass = KotlinBinaryClassCache.getKotlinBinaryClassOrClassFileContent(file, fileContent)?.toKotlinJvmBinaryClass() + + val isKotlinBinaryClass = kotlinBinaryClass != null + if (file is VirtualFileWithId) { + attributeService.writeBooleanAttribute(KOTLIN_IS_COMPILED_FILE_ATTRIBUTE, file, isKotlinBinaryClass) + } + + if (isKotlinBinaryClass) { + val headerInfo = createHeaderInfo(kotlinBinaryClass!!) + file.putUserData(KOTLIN_BINARY_DATA_KEY, SoftReference(KotlinBinaryData(isKotlinBinaryClass, file.timeStamp, headerInfo))) + } + + return kotlinBinaryClass + } + + fun getKotlinBinaryClassHeaderData(file: VirtualFile, fileContent: ByteArray? = null): KotlinBinaryClassHeaderData? { + getKotlinBinaryFromCache(file)?.let { cached -> + if (!cached.isKotlinBinary) { + return null + } + if (cached.headerData != null) { + return cached.headerData + } + } + + val kotlinBinaryClass = kotlinJvmBinaryClass(file, fileContent) ?: return null + return createHeaderInfo(kotlinBinaryClass) + } + + private val attributeService = ApplicationManager.getApplication().getService(FileAttributeService::class.java) + + private fun createHeaderInfo(kotlinBinaryClass: KotlinJvmBinaryClass): KotlinBinaryClassHeaderData { + val classId = kotlinBinaryClass.classId + + return kotlinBinaryClass.classHeader.toLightHeader(classId) + } + + private fun KotlinClassHeader.toLightHeader(classId: ClassId) = + KotlinBinaryClassHeaderData( + classId, kind, metadataVersion, multifilePartNames, packageName + ) + + private val KOTLIN_IS_COMPILED_FILE_ATTRIBUTE: String = "kotlin-is-binary-compiled".apply { + attributeService.register(this, 2) + } + + private val KOTLIN_BINARY_DATA_KEY = Key.create>(KOTLIN_IS_COMPILED_FILE_ATTRIBUTE) + + private fun getKotlinBinaryFromCache(file: VirtualFile): KotlinBinaryData? { + val userData = file.getUserData(KOTLIN_BINARY_DATA_KEY)?.get() + if (userData != null && userData.timestamp == file.timeStamp) { + return userData + } + + val isKotlinBinaryAttribute = if (file is VirtualFileWithId) { + attributeService.readBooleanAttribute(KOTLIN_IS_COMPILED_FILE_ATTRIBUTE, file) + } else { + null + } + + if (isKotlinBinaryAttribute != null) { + val isKotlinBinary = isKotlinBinaryAttribute.value + val kotlinBinaryData = KotlinBinaryData(isKotlinBinary, file.timeStamp, null) + if (isKotlinBinary) { + file.putUserData(KOTLIN_BINARY_DATA_KEY, SoftReference(kotlinBinaryData)) + } + + return kotlinBinaryData + } + + return null + } + + companion object { + fun getInstance(): ClsKotlinBinaryClassCache = + ApplicationManager.getApplication().getService(ClsKotlinBinaryClassCache::class.java) + } +} diff --git a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/DirectoryBasedClassFinder.kt b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/DirectoryBasedClassFinder.kt index dd3f3bfacec..66e1a7d6759 100644 --- a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/DirectoryBasedClassFinder.kt +++ b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/DirectoryBasedClassFinder.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.kotlin.load.java.structure.JavaClass import org.jetbrains.kotlin.load.java.structure.classId import org.jetbrains.kotlin.load.kotlin.KotlinClassFinder +import org.jetbrains.kotlin.load.kotlin.KotlinClassFinder.Result.KotlinClass import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import java.io.InputStream @@ -26,7 +27,7 @@ class DirectoryBasedClassFinder( val targetName = classId.relativeClassName.pathSegments().joinToString("$", postfix = ".class") val virtualFile = packageDirectory.findChild(targetName) if (virtualFile != null && isKotlinWithCompatibleAbiVersion(virtualFile)) { - return IDEKotlinBinaryClassCache.getInstance().getKotlinBinaryClass(virtualFile)?.let(::KotlinClass) + return ClsKotlinBinaryClassCache.getInstance().getKotlinBinaryClass(virtualFile)?.let(::KotlinClass) } return null } @@ -45,10 +46,10 @@ class DirectoryBasedClassFinder( * Checks if this file is a compiled Kotlin class file ABI-compatible with the current plugin */ private fun isKotlinWithCompatibleAbiVersion(file: VirtualFile): Boolean { - val ideKotlinBinaryClassCache = IDEKotlinBinaryClassCache.getInstance() - if (!ideKotlinBinaryClassCache.isKotlinJvmCompiledFile(file)) return false + val clsKotlinBinaryClassCache = ClsKotlinBinaryClassCache.getInstance() + if (!clsKotlinBinaryClassCache.isKotlinJvmCompiledFile(file)) return false - val kotlinClass = ideKotlinBinaryClassCache.getKotlinBinaryClassHeaderData(file) + val kotlinClass = clsKotlinBinaryClassCache.getKotlinBinaryClassHeaderData(file) return kotlinClass != null && kotlinClass.metadataVersion.isCompatible() } diff --git a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/FileAttributeService.kt b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/FileAttributeService.kt new file mode 100644 index 00000000000..9dc407474fc --- /dev/null +++ b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/FileAttributeService.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.psi.stubs.file.builder + +import com.intellij.openapi.vfs.VirtualFile +import java.io.DataInput +import java.io.DataOutput + +data class CachedAttributeData(val value: T, val timeStamp: Long) + +interface FileAttributeService { + fun register(id: String, version: Int, fixedSize: Boolean = true) {} + + fun > writeEnumAttribute(id: String, file: VirtualFile, value: T): CachedAttributeData = + CachedAttributeData(value, timeStamp = file.timeStamp) + + fun > readEnumAttribute(id: String, file: VirtualFile, klass: Class): CachedAttributeData? = null + + fun writeBooleanAttribute(id: String, file: VirtualFile, value: Boolean): CachedAttributeData = + CachedAttributeData(value, timeStamp = file.timeStamp) + + fun readBooleanAttribute(id: String, file: VirtualFile): CachedAttributeData? = null + + fun write(file: VirtualFile, id: String, value: T, writeValueFun: (DataOutput, T) -> Unit): CachedAttributeData + + fun read(file: VirtualFile, id: String, readValueFun: (DataInput) -> T): CachedAttributeData? +} diff --git a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/KotlinClsStubBuilder.kt b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/KotlinClsStubBuilder.kt index f837c640c9a..e3c71b61c7e 100644 --- a/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/KotlinClsStubBuilder.kt +++ b/compiler/psi/cls-psi-file-stub-builder/src/org/jetbrains/kotlin/psi/stubs/file/builder/KotlinClsStubBuilder.kt @@ -13,6 +13,7 @@ import com.intellij.psi.stubs.PsiFileStub import com.intellij.util.indexing.FileContent import org.jetbrains.kotlin.SpecialJvmAnnotations import org.jetbrains.kotlin.descriptors.SourceElement +import org.jetbrains.kotlin.deserialization.common.jvm.impl.VirtualFileKotlinClass import org.jetbrains.kotlin.load.kotlin.* import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.metadata.ProtoBuf @@ -37,7 +38,7 @@ open class KotlinClsStubBuilder : ClsStubBuilder() { override fun buildFileStub(content: FileContent): PsiFileStub<*>? { val virtualFile = content.file - if (isKotlinInternalCompiledFile(virtualFile, content.content)) { + if (ClsClassFinder.isKotlinInternalCompiledFile(virtualFile, content.content)) { return null } @@ -51,7 +52,7 @@ open class KotlinClsStubBuilder : ClsStubBuilder() { } private fun doBuildFileStub(file: VirtualFile, fileContent: ByteArray): PsiFileStub? { - val kotlinClass: KotlinJvmBinaryClass = IDEKotlinBinaryClassCache.getInstance().getKotlinBinaryClass(file, fileContent) + val kotlinClass = ClsKotlinBinaryClassCache.getInstance().getKotlinBinaryClass(file, fileContent) ?: error("Can't find binary class for Kotlin file: $file") val header = kotlinClass.classHeader val classId = kotlinClass.classId @@ -63,7 +64,7 @@ open class KotlinClsStubBuilder : ClsStubBuilder() { val components = createStubBuilderComponents(file, packageFqName, fileContent) if (header.kind == KotlinClassHeader.Kind.MULTIFILE_CLASS) { - val partFiles = findMultifileClassParts(file, classId, header.multifilePartNames) + val partFiles = ClsClassFinder.findMultifileClassParts(file, classId, header.multifilePartNames) return createMultifileClassStub(header, partFiles, classId.asSingleFqName(), components) }