From 43eb94726c3063ffd71f3d2d9760590422d8c888 Mon Sep 17 00:00:00 2001 From: Nikolay Krasko Date: Fri, 17 Oct 2014 15:30:03 +0400 Subject: [PATCH] Make single decompiler be responsible for compiled Kotlin files IDEA caches link between a virtual file and correspondent FileViewProvider. This breaks navigation to decompiled sources for the library that has been just updated because of outdated ABI version. Also IDEA don't allow to change language for decompiled files. #KT-6016 Fixed --- .../kotlin/KotlinBinaryClassCache.java | 47 +++++++++++------- .../src/org/jetbrains/jet/utils/concurrent.kt | 49 +++++++++++++++++++ .../jet/plugin/decompiler/DecompiledUtils.kt | 14 +++--- .../decompiler/JetClassFileViewProvider.kt | 23 ++++----- .../jet/plugin/decompiler/JetClsFile.kt | 46 ++++++++++++----- .../JetDecompilerForWrongAbiVersion.kt | 29 ----------- .../textBuilder/DecompiledTextFactory.kt | 40 ++++++++++----- .../textBuilder/DeserializerForDecompiler.kt | 4 +- idea/src/META-INF/plugin.xml | 3 +- .../DecompiledTextForWrongAbiVersionTest.kt | 8 ++- 10 files changed, 162 insertions(+), 101 deletions(-) create mode 100644 compiler/util/src/org/jetbrains/jet/utils/concurrent.kt delete mode 100644 idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetDecompilerForWrongAbiVersion.kt diff --git a/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/kotlin/KotlinBinaryClassCache.java b/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/kotlin/KotlinBinaryClassCache.java index ad4f7b3f862..e4f69ba299b 100644 --- a/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/kotlin/KotlinBinaryClassCache.java +++ b/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/kotlin/KotlinBinaryClassCache.java @@ -18,30 +18,32 @@ package org.jetbrains.jet.lang.resolve.kotlin; import com.intellij.ide.highlighter.JavaClassFileType; import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.util.Ref; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.containers.SLRUCache; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class KotlinBinaryClassCache implements Disposable { + private static class RequestCache { + VirtualFile virtualFile; + long modificationStamp; + VirtualFileKotlinClass virtualFileKotlinClass; - // This cache must be small: we only query the same file a few times in a row (from different places) - // It's local to each thread: we don't want a single instance to synchronize access on, because VirtualFileKotlinClass.create involves - // reading files from disk and may take some time - private final ThreadLocal>> cache = - new ThreadLocal>>() { + public VirtualFileKotlinClass cache(VirtualFile file, VirtualFileKotlinClass aClass) { + virtualFile = file; + virtualFileKotlinClass = aClass; + modificationStamp = file.getModificationStamp(); + + return aClass; + } + } + + private final ThreadLocal cache = + new ThreadLocal() { @Override - protected SLRUCache> initialValue() { - return new SLRUCache>(2, 2) { - @NotNull - @Override - @SuppressWarnings("deprecation") - public Ref createValue(VirtualFile virtualFile) { - return Ref.create(VirtualFileKotlinClass.OBJECT$.create(virtualFile)); - } - }; + protected RequestCache initialValue() { + return new RequestCache(); } }; @@ -50,7 +52,18 @@ public final class KotlinBinaryClassCache implements Disposable { if (file.getFileType() != JavaClassFileType.INSTANCE) return null; KotlinBinaryClassCache service = ServiceManager.getService(KotlinBinaryClassCache.class); - return service.cache.get().get(file).get(); + RequestCache requestCache = service.cache.get(); + + if (file.getModificationStamp() == requestCache.modificationStamp && file.equals(requestCache.virtualFile)) { + return requestCache.virtualFileKotlinClass; + } + else { + ApplicationManager.getApplication().assertReadAccessAllowed(); + + //noinspection deprecation + VirtualFileKotlinClass aClass = VirtualFileKotlinClass.OBJECT$.create(file); + return requestCache.cache(file, aClass); + } } @Override diff --git a/compiler/util/src/org/jetbrains/jet/utils/concurrent.kt b/compiler/util/src/org/jetbrains/jet/utils/concurrent.kt new file mode 100644 index 00000000000..dfcd14aca18 --- /dev/null +++ b/compiler/util/src/org/jetbrains/jet/utils/concurrent.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2014 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.jet.utils.concurrent.block + +public class LockedClearableLazyValue(val lock: Any, val init: () -> T) { + private volatile var value: T? = null + + public fun get(): T { + val _v1 = value + if (_v1 != null) { + return _v1 + } + + return synchronized(lock) { + val _v2 = value + // Suppress because of https://youtrack.jetbrains.com/issue/KT-6176 + [suppress("UNNECESSARY_NOT_NULL_ASSERTION")] + if (_v2 != null) { + _v2!! + } + else { + val _v3 = init() + this.value = _v3 + _v3 + } + } + } + + fun drop() { + synchronized (lock) { + value = null + } + } +} + diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/DecompiledUtils.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/DecompiledUtils.kt index ccdffff1ff2..02fb042a82c 100644 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/DecompiledUtils.kt +++ b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/DecompiledUtils.kt @@ -24,26 +24,26 @@ import org.jetbrains.jet.lang.resolve.java.JvmAnnotationNames.KotlinSyntheticCla import com.intellij.ide.highlighter.JavaClassFileType public fun isKotlinCompiledFile(file: VirtualFile): Boolean { - if (isKotlinCompiledFileWithIncompatibleAbiVersion(file)) { + if (file.getExtension() != JavaClassFileType.INSTANCE!!.getDefaultExtension()) { return false } - + val header = KotlinBinaryClassCache.getKotlinBinaryClass(file)?.getClassHeader() return header != null && header.syntheticClassKind != KotlinSyntheticClass.Kind.TRAIT_IMPL } -public fun isKotlinCompiledFileWithIncompatibleAbiVersion(file: VirtualFile): Boolean { - if (file.getExtension() != JavaClassFileType.INSTANCE!!.getDefaultExtension()) { - return false - } +public fun isKotlinWithCompatibleAbiVersion(file: VirtualFile): Boolean { + if (!isKotlinCompiledFile(file)) return false + val header = KotlinBinaryClassCache.getKotlinBinaryClass(file)?.getClassHeader() - return header != null && !header.isCompatibleAbiVersion + return header != null && header.isCompatibleAbiVersion } public fun isKotlinInternalCompiledFile(file: VirtualFile): Boolean { if (!isKotlinCompiledFile(file)) { return false } + if (ClassFileViewProvider.isInnerClass(file)) { return true } diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClassFileViewProvider.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClassFileViewProvider.kt index a65641c5719..10ce0875916 100644 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClassFileViewProvider.kt +++ b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClassFileViewProvider.kt @@ -24,30 +24,25 @@ import com.intellij.psi.PsiManager import com.intellij.psi.SingleRootFileViewProvider import org.jetbrains.jet.plugin.JetLanguage import kotlin.properties.Delegates -import org.jetbrains.jet.plugin.decompiler.textBuilder.buildDecompiledText public class JetClassFileViewProvider( manager: PsiManager, - file: VirtualFile, + val file: VirtualFile, physical: Boolean, - val isInternal: Boolean -) : SingleRootFileViewProvider(manager, file, physical, JetLanguage.INSTANCE) { - val decompiledText by Delegates.blockingLazy(this) { - buildDecompiledText(getVirtualFile()) + val isInternal: Boolean) : SingleRootFileViewProvider(manager, file, physical, JetLanguage.INSTANCE) { + + val jetClsFile by Delegates.blockingLazy(this) { + //TODO: check index that file is library file, as in ClassFileViewProvider + if (!isInternal) JetClsFile(this) else null } override fun getContents(): CharSequence { - return if (isInternal) "" else decompiledText.text + return if (!isInternal) jetClsFile!!.getText() else "" } - override fun createFile(project: Project, file: VirtualFile, fileType: FileType): PsiFile? { - //TODO: check index that file is library file, as in ClassFileViewProvider - if (isInternal) return null - - return JetClsFile(this) - } + override fun createFile(project: Project, file: VirtualFile, fileType: FileType): PsiFile? = jetClsFile override fun createCopy(copy: VirtualFile): SingleRootFileViewProvider { return JetClassFileViewProvider(getManager(), copy, false, isInternal) } -} +} \ No newline at end of file diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClsFile.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClsFile.kt index 214c014bfab..b7401e52006 100644 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClsFile.kt +++ b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetClsFile.kt @@ -22,31 +22,53 @@ import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.annotations.TestOnly import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor import org.jetbrains.jet.lang.psi.JetDeclaration -import kotlin.properties.Delegates +import com.intellij.psi.PsiElement +import org.jetbrains.jet.plugin.decompiler.textBuilder.buildDecompiledText import org.jetbrains.jet.plugin.decompiler.textBuilder.descriptorToKey +import org.jetbrains.jet.utils.concurrent.block.LockedClearableLazyValue public class JetClsFile(val provider: JetClassFileViewProvider) : ClsFileImpl(provider) { + private val mirrorLock = Any() - private val decompiledFile by Delegates.blockingLazy(this) { - JetDummyClassFileViewProvider.createJetFile(provider.getManager(), getVirtualFile(), provider.decompiledText.text) + private val mirrorFile = LockedClearableLazyValue(mirrorLock) { + JetDummyClassFileViewProvider.createJetFile( + provider.getManager(), + getVirtualFile(), + decompiledText.get().text)!! } - override fun getDecompiledPsiFile() = decompiledFile + private val decompiledText = LockedClearableLazyValue(mirrorLock) { + buildDecompiledText(getVirtualFile()) + } + + override fun getMirror(): PsiElement? = mirrorFile.get() public fun getDeclarationForDescriptor(descriptor: DeclarationDescriptor): JetDeclaration? { val key = descriptorToKey(descriptor.getOriginal()) - val range = provider.decompiledText.renderedDescriptorsToRange[key] - if (range == null) { - return null + + return synchronized(mirrorLock) { + val range = decompiledText.get().renderedDescriptorsToRange[key] + if (range != null) { + PsiTreeUtil.findElementOfClassAtRange( + mirrorFile.get(), range.getStartOffset(), range.getEndOffset(), javaClass()) + } + else { + null + } + } + } + + override fun onContentReload() { + super.onContentReload() + + synchronized (mirrorLock) { + decompiledText.drop() + mirrorFile.drop() } - return PsiTreeUtil.findElementOfClassAtRange(decompiledFile, range.getStartOffset(), range.getEndOffset(), javaClass()) } TestOnly fun getRenderedDescriptorsToRange(): Map { - return provider.decompiledText.renderedDescriptorsToRange + return decompiledText.get().renderedDescriptorsToRange } - - override fun getTextLength() = decompiledFile.getTextLength() - override fun getText() = decompiledFile.getText() } diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetDecompilerForWrongAbiVersion.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetDecompilerForWrongAbiVersion.kt deleted file mode 100644 index 6ca2526ddc5..00000000000 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/JetDecompilerForWrongAbiVersion.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2010-2014 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.jet.plugin.decompiler - -import com.intellij.psi.compiled.ClassFileDecompilers -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.impl.compiled.ClsFileImpl - -public class JetDecompilerForWrongAbiVersion : ClassFileDecompilers.Light() { - override fun accepts(file: VirtualFile) = isKotlinCompiledFileWithIncompatibleAbiVersion(file) - - override fun getText(file: VirtualFile) = "$INCOMPATIBLE_ABI_VERSION_COMMENT\n${ClsFileImpl.decompile(file)}" -} - -public val INCOMPATIBLE_ABI_VERSION_COMMENT: String = " // This Kotlin file has an incompatible ABI version and is displayed as Java class" diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextFactory.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextFactory.kt index b2ecf9ecc08..3be40ea0508 100644 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextFactory.kt +++ b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextFactory.kt @@ -32,6 +32,19 @@ import org.jetbrains.jet.lang.types.error.MissingDependencyErrorClass import org.jetbrains.jet.lang.resolve.dataClassUtils.isComponentLike import org.jetbrains.jet.plugin.util.IdeDescriptorRenderers import org.jetbrains.jet.lang.types.isFlexible +import org.jetbrains.jet.lang.resolve.java.JvmAbi +import org.jetbrains.jet.lang.resolve.kotlin.header.isCompatiblePackageFacadeKind +import org.jetbrains.jet.lang.resolve.kotlin.header.isCompatibleClassKind + +private val FILE_ABI_VERSION_MARKER: String = "FILE_ABI" +private val CURRENT_ABI_VERSION_MARKER: String = "CURRENT_ABI" + +public val INCOMPATIBLE_ABI_VERSION_GENERAL_COMMENT: String = "// This class file was compiled with different version of Kotlin compiler and can't be decompiled." +public val INCOMPATIBLE_ABI_VERSION_COMMENT: String = + "$INCOMPATIBLE_ABI_VERSION_GENERAL_COMMENT\n" + + "//\n" + + "// Current compiler ABI version is $CURRENT_ABI_VERSION_MARKER\n" + + "// File ABI version is $FILE_ABI_VERSION_MARKER" public fun buildDecompiledText( classFile: VirtualFile, @@ -41,21 +54,22 @@ public fun buildDecompiledText( assert(kotlinClass != null) { "Decompiled data factory shouldn't be called on an unsupported file: " + classFile } val classId = kotlinClass!!.getClassId() val classHeader = kotlinClass.getClassHeader() - val kind = kotlinClass.getClassHeader().kind val packageFqName = classId.getPackageFqName() - - if (!classHeader.isCompatibleAbiVersion) { - throw UnsupportedOperationException("Illegal operation for incompatibel header") - } - return if (kind == KotlinClassHeader.Kind.PACKAGE_FACADE) { - buildDecompiledText(packageFqName, ArrayList(resolver.resolveDeclarationsInPackage(packageFqName))) - } - else if (kind == KotlinClassHeader.Kind.CLASS) { - buildDecompiledText(packageFqName, listOf(resolver.resolveTopLevelClass(classId)).filterNotNull()) - } - else { - throw UnsupportedOperationException("Unknown header kind: " + kind) + return when { + !classHeader.isCompatibleAbiVersion -> { + DecompiledText( + INCOMPATIBLE_ABI_VERSION_COMMENT + .replaceAll(CURRENT_ABI_VERSION_MARKER, JvmAbi.VERSION.toString()) + .replaceAll(FILE_ABI_VERSION_MARKER, classHeader.version.toString()), + mapOf()) + } + classHeader.isCompatiblePackageFacadeKind() -> + buildDecompiledText(packageFqName, ArrayList(resolver.resolveDeclarationsInPackage(packageFqName))) + classHeader.isCompatibleClassKind() -> + buildDecompiledText(packageFqName, listOf(resolver.resolveTopLevelClass(classId)).filterNotNull()) + else -> + throw UnsupportedOperationException("Unknown header kind: ${classHeader.kind} ${classHeader.isCompatibleAbiVersion}") } } diff --git a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DeserializerForDecompiler.kt b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DeserializerForDecompiler.kt index d3184815624..4b61977d861 100644 --- a/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DeserializerForDecompiler.kt +++ b/idea/idea-analysis/src/org/jetbrains/jet/plugin/decompiler/textBuilder/DeserializerForDecompiler.kt @@ -37,7 +37,7 @@ import org.jetbrains.jet.lang.PlatformToKotlinClassMap import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns import org.jetbrains.jet.descriptors.serialization.ClassData import org.jetbrains.jet.lang.descriptors.impl.ModuleDescriptorImpl -import org.jetbrains.jet.plugin.decompiler.isKotlinCompiledFile +import org.jetbrains.jet.plugin.decompiler.isKotlinWithCompatibleAbiVersion public fun DeserializerForDecompiler(classFile: VirtualFile): DeserializerForDecompiler { val kotlinClass = KotlinBinaryClassCache.getKotlinBinaryClass(classFile) @@ -81,7 +81,7 @@ public class DeserializerForDecompiler(val packageDirectory: VirtualFile, val di val segments = DeserializedResolverUtils.kotlinFqNameToJavaFqName(classId.getRelativeClassName()).pathSegments() val targetName = segments.joinToString("$", postfix = ".class") val virtualFile = packageDirectory.findChild(targetName) - if (virtualFile != null && isKotlinCompiledFile(virtualFile)) { + if (virtualFile != null && isKotlinWithCompatibleAbiVersion(virtualFile)) { return KotlinBinaryClassCache.getKotlinBinaryClass(virtualFile) } return null diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 3a7ec8586e5..d6630beaa94 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -397,8 +397,7 @@ - - + diff --git a/idea/tests/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextForWrongAbiVersionTest.kt b/idea/tests/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextForWrongAbiVersionTest.kt index c7fff149d4d..6786a3cfd7e 100644 --- a/idea/tests/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextForWrongAbiVersionTest.kt +++ b/idea/tests/org/jetbrains/jet/plugin/decompiler/textBuilder/DecompiledTextForWrongAbiVersionTest.kt @@ -25,10 +25,9 @@ import com.intellij.psi.PsiCompiledFile import org.jetbrains.jet.plugin.JetJdkAndLibraryProjectDescriptor import java.io.File import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiJavaFile import com.intellij.psi.impl.compiled.ClsFileImpl import org.jetbrains.jet.plugin.decompiler.navigation.NavigateToDecompiledLibraryTest -import org.jetbrains.jet.plugin.decompiler.INCOMPATIBLE_ABI_VERSION_COMMENT +import org.jetbrains.jet.lang.psi.JetFile public class DecompiledTextForWrongAbiVersionTest : JetLightCodeInsightFixtureTestCase() { @@ -50,9 +49,8 @@ public class DecompiledTextForWrongAbiVersionTest : JetLightCodeInsightFixtureTe val psiFile = PsiManager.getInstance(getProject()!!).findFile(file) Assert.assertTrue(psiFile is ClsFileImpl) val decompiledPsiFile = (psiFile as PsiCompiledFile).getDecompiledPsiFile() - Assert.assertTrue(decompiledPsiFile is PsiJavaFile) + Assert.assertTrue(decompiledPsiFile is JetFile) val decompiledText = decompiledPsiFile!!.getText()!! - Assert.assertTrue(decompiledText.contains(INCOMPATIBLE_ABI_VERSION_COMMENT)) - Assert.assertTrue((decompiledPsiFile as PsiJavaFile).getClasses().size == 1) + Assert.assertTrue(decompiledText.contains(INCOMPATIBLE_ABI_VERSION_GENERAL_COMMENT)) } } \ No newline at end of file