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
This commit is contained in:
Nikolay Krasko
2014-10-17 15:30:03 +04:00
parent 5555e28bfb
commit 43eb94726c
10 changed files with 162 additions and 101 deletions
@@ -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<SLRUCache<VirtualFile, Ref<VirtualFileKotlinClass>>> cache =
new ThreadLocal<SLRUCache<VirtualFile, Ref<VirtualFileKotlinClass>>>() {
public VirtualFileKotlinClass cache(VirtualFile file, VirtualFileKotlinClass aClass) {
virtualFile = file;
virtualFileKotlinClass = aClass;
modificationStamp = file.getModificationStamp();
return aClass;
}
}
private final ThreadLocal<RequestCache> cache =
new ThreadLocal<RequestCache>() {
@Override
protected SLRUCache<VirtualFile, Ref<VirtualFileKotlinClass>> initialValue() {
return new SLRUCache<VirtualFile, Ref<VirtualFileKotlinClass>>(2, 2) {
@NotNull
@Override
@SuppressWarnings("deprecation")
public Ref<VirtualFileKotlinClass> 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
@@ -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<T: Any>(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
}
}
}
@@ -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
}
@@ -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)
}
}
}
@@ -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<JetDeclaration>())
}
else {
null
}
}
}
override fun onContentReload() {
super<ClsFileImpl>.onContentReload()
synchronized (mirrorLock) {
decompiledText.drop()
mirrorFile.drop()
}
return PsiTreeUtil.findElementOfClassAtRange(decompiledFile, range.getStartOffset(), range.getEndOffset(), javaClass<JetDeclaration>())
}
TestOnly
fun getRenderedDescriptorsToRange(): Map<String, TextRange> {
return provider.decompiledText.renderedDescriptorsToRange
return decompiledText.get().renderedDescriptorsToRange
}
override fun getTextLength() = decompiledFile.getTextLength()
override fun getText() = decompiledFile.getText()
}
@@ -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"
@@ -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}")
}
}
@@ -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
+1 -2
View File
@@ -397,8 +397,7 @@
<stubIndex implementation="org.jetbrains.jet.plugin.stubindex.JetTopLevelObjectShortNameIndex"/>
<psi.classFileDecompiler implementation="org.jetbrains.jet.plugin.decompiler.JetClassFileDecompiler"/>
<psi.classFileDecompiler implementation="org.jetbrains.jet.plugin.decompiler.JetDecompilerForWrongAbiVersion"/>
<fileBasedIndex implementation="org.jetbrains.jet.plugin.versions.KotlinAbiVersionIndex"/>
<fileBasedIndex implementation="org.jetbrains.jet.plugin.vfilefinder.KotlinClassFileIndex"/>
@@ -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))
}
}