diff --git a/ChangeLog.md b/ChangeLog.md index c21737dd018..9a9eb8b9066 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -213,6 +213,7 @@ ###### New features - Added "Decompile" button to Kotlin bytecode toolwindow +- [`KT-11657`](https://youtrack.jetbrains.com/issue/KT-11657) Allow viewing decompiled Java source code for Kotlin-compiled classes - [`KT-11704`](https://youtrack.jetbrains.com/issue/KT-11704) Support file path references inside of Kotlin string literals ###### Issues fixed: diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 14912791bff..0fa0d686741 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -99,6 +99,11 @@ + + + + @@ -600,6 +605,8 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/DecompileKotlinToJavaAction.kt b/idea/src/org/jetbrains/kotlin/idea/actions/DecompileKotlinToJavaAction.kt new file mode 100644 index 00000000000..12aa9669a76 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/actions/DecompileKotlinToJavaAction.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2016 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.idea.actions + +import com.intellij.codeInsight.AttachSourcesProvider +import com.intellij.ide.highlighter.JavaClassFileType +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.roots.LibraryOrderEntry +import com.intellij.openapi.util.ActionCallback +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.idea.internal.showDecompiledCode +import org.jetbrains.kotlin.psi.KtFile + +class DecompileKotlinToJavaAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + val binaryFile = getBinaryKotlinFile(e) ?: return + + showDecompiledCode(binaryFile) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getBinaryKotlinFile(e) != null + } + + private fun getBinaryKotlinFile(e: AnActionEvent): KtFile? { + val file = e.getData(CommonDataKeys.PSI_FILE) as? KtFile ?: return null + if (!file.canBeDecompiledToJava()) return null + + return file + } + +} + +fun KtFile.canBeDecompiledToJava() = isCompiled && virtualFile?.fileType == JavaClassFileType.INSTANCE + +// Add action to "Attach sources" notification panel +class DecompileKotlinToJavaActionProvider() : AttachSourcesProvider { + override fun getActions(orderEntries: MutableList, psiFile: PsiFile): Collection { + if (psiFile !is KtFile || !psiFile.canBeDecompiledToJava()) return emptyList() + + return listOf(object : AttachSourcesProvider.AttachSourcesAction { + override fun getName() = "Decompile to Java" + + override fun perform(orderEntriesContainingFile: List?): ActionCallback { + showDecompiledCode(psiFile) + return ActionCallback.DONE + } + + override fun getBusyText() = "Kotlin Classfile" + }) + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt index 65d7d0b4cb5..c3f2ddcaf9f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt @@ -17,6 +17,7 @@ package org.jetbrains.kotlin.idea.internal import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile @@ -29,6 +30,10 @@ import org.jetbrains.kotlin.psi.KtFile fun showDecompiledCode(sourceFile: KtFile) { val decompilerService = KotlinDecompilerService.getInstance() ?: return val decompiledCode = decompilerService.decompile(sourceFile) + if (decompiledCode == null) { + Messages.showErrorDialog("Cannot decompile ${sourceFile.name}", "Decompiler error") + return + } runWriteAction { val root = getOrCreateDummyRoot() diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt index f1e99e0e56e..d4d1b4a324c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt @@ -20,7 +20,7 @@ import com.intellij.openapi.components.ServiceManager import org.jetbrains.kotlin.psi.KtFile interface KotlinDecompilerService { - fun decompile(file: KtFile): String + fun decompile(file: KtFile): String? companion object { fun getInstance(): KotlinDecompilerService? { diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt index f533dd7e685..602b9497a43 100644 --- a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt @@ -16,30 +16,39 @@ package org.jetbrains.kotlin.idea.internal +import com.intellij.openapi.fileTypes.StdFileTypes +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.java.decompiler.IdeaLogger import org.jetbrains.java.decompiler.main.decompiler.BaseDecompiler import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences import org.jetbrains.java.decompiler.main.extern.IResultSaver +import org.jetbrains.kotlin.idea.actions.canBeDecompiledToJava import org.jetbrains.kotlin.psi.KtFile import java.io.File import java.util.jar.Manifest class KotlinDecompilerServiceImpl : KotlinDecompilerService { - override fun decompile(file: KtFile): String { - val generationState = KotlinBytecodeToolWindow.compileSingleFile(file, true, true, true) + override fun decompile(file: KtFile): String? { - val bytecodeMap = hashMapOf() - generationState.factory.asList().filter { FileUtilRt.extensionEquals(it.relativePath, "class") }.forEach { - bytecodeMap[File("/${it.relativePath}").absoluteFile] = it.asByteArray() + val bytecodeMap = when { + file.canBeDecompiledToJava() -> bytecodeMapForExistingClassfile(file.virtualFile) + !file.isCompiled -> bytecodeMapForSourceFile(file) + else -> return null } - - val bytecodeProvider = KotlinBytecodeProvider(bytecodeMap) val resultSaver = KotlinResultSaver() val options = hashMapOf( - IFernflowerPreferences.REMOVE_BRIDGE to "0" + IFernflowerPreferences.REMOVE_BRIDGE to "0" ) + + val bytecodeProvider = IBytecodeProvider { + externalPath, internalPath -> + val path = File(FileUtil.toSystemIndependentName(externalPath)) + bytecodeMap[path]?.invoke() + } + val decompiler = BaseDecompiler(bytecodeProvider, resultSaver, options, IdeaLogger()) for (path in bytecodeMap.keys) { decompiler.addSpace(path, true) @@ -48,12 +57,29 @@ class KotlinDecompilerServiceImpl : KotlinDecompilerService { return resultSaver.resultText } - class KotlinBytecodeProvider(val bytecodeMap: Map) : IBytecodeProvider { - override fun getBytecode(externalPath: String?, internalPath: String?): ByteArray? { - return bytecodeMap[File(externalPath)] + fun bytecodeMapForExistingClassfile(file: VirtualFile): Map ByteArray> { + val mask = "${file.nameWithoutExtension}$" + val files = + mapOf(file.path to file) + + file.parent.children.filter { + it.nameWithoutExtension.startsWith(mask) && it.fileType === StdFileTypes.CLASS + }.map { it.path to it } + + return files.entries.associate { + Pair(File(it.key), { it.value.contentsToByteArray() }) } } + fun bytecodeMapForSourceFile(file: KtFile): Map ByteArray> { + val generationState = KotlinBytecodeToolWindow.compileSingleFile(file, true, true, true) + + val bytecodeMap = hashMapOf ByteArray>() + generationState.factory.asList().filter { FileUtilRt.extensionEquals(it.relativePath, "class") }.forEach { + bytecodeMap[File("/${it.relativePath}").absoluteFile] = { it.asByteArray() } + } + return bytecodeMap + } + class KotlinResultSaver : IResultSaver { private val decompiledText = mutableMapOf()