diff --git a/.idea/libraries/java_decompiler_plugin.xml b/.idea/libraries/java_decompiler_plugin.xml new file mode 100644 index 00000000000..85bbc24dfdd --- /dev/null +++ b/.idea/libraries/java_decompiler_plugin.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md index d8d9598b32d..a696e21e10c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -25,6 +25,7 @@ New features: - [KT-11704](https://youtrack.jetbrains.com/issue/KT-11704) Support file path references inside of Kotlin string literals - [KT-12092](https://youtrack.jetbrains.com/issue/KT-12092) Implement bean references in @Qualifier annotations - [KT-12175](https://youtrack.jetbrains.com/issue/KT-12175) Don't enforce empty line after one-line constructor +- Added "Decompile" button to Kotlin bytecode toolwindow Issues fixed: diff --git a/idea/idea.iml b/idea/idea.iml index d1133b53984..7b5ac27bb8f 100644 --- a/idea/idea.iml +++ b/idea/idea.iml @@ -51,6 +51,7 @@ + diff --git a/idea/src/META-INF/decompiler.xml b/idea/src/META-INF/decompiler.xml new file mode 100644 index 00000000000..e0d38be40b2 --- /dev/null +++ b/idea/src/META-INF/decompiler.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 108426074ca..9d4c6c91e98 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -18,6 +18,7 @@ Coverage com.intellij.java-i18n org.intellij.intelliLang + org.jetbrains.java.decompiler diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinBytecodeToolWindow.java b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinBytecodeToolWindow.java index ea41eb465af..a9c54938510 100644 --- a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinBytecodeToolWindow.java +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinBytecodeToolWindow.java @@ -19,6 +19,7 @@ package org.jetbrains.kotlin.idea.internal; import com.intellij.ide.highlighter.JavaFileType; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; @@ -26,12 +27,14 @@ import com.intellij.openapi.editor.ScrollType; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.ToolWindow; import com.intellij.util.Alarm; import org.jetbrains.annotations.NotNull; +import org.jetbrains.java.decompiler.IdeaLogger; import org.jetbrains.kotlin.backend.common.output.OutputFile; import org.jetbrains.kotlin.backend.common.output.OutputFileCollection; import org.jetbrains.kotlin.codegen.ClassBuilderFactories; @@ -54,12 +57,16 @@ import org.jetbrains.kotlin.utils.StringsKt; import javax.swing.*; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; import java.util.List; public class KotlinBytecodeToolWindow extends JPanel implements Disposable { + private final Logger LOG = Logger.getInstance(KotlinBytecodeToolWindow.class); + private static final int UPDATE_DELAY = 1000; private static final String DEFAULT_TEXT = "/*\n" + "Generated bytecode for Kotlin source file.\n" + @@ -152,8 +159,9 @@ public class KotlinBytecodeToolWindow extends JPanel implements Disposable { private final JCheckBox enableInline; private final JCheckBox enableOptimization; private final JCheckBox enableAssertions; + private final JButton decompile; - public KotlinBytecodeToolWindow(Project project, ToolWindow toolWindow) { + public KotlinBytecodeToolWindow(final Project project, ToolWindow toolWindow) { super(new BorderLayout()); myProject = project; this.toolWindow = toolWindow; @@ -165,10 +173,31 @@ public class KotlinBytecodeToolWindow extends JPanel implements Disposable { JPanel optionPanel = new JPanel(new FlowLayout()); add(optionPanel, BorderLayout.NORTH); + decompile = new JButton("Decompile"); + if (KotlinDecompilerService.Companion.getInstance() != null) { + optionPanel.add(decompile); + decompile.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Location location = Location.fromEditor(FileEditorManager.getInstance(myProject).getSelectedTextEditor(), myProject); + KtFile file = location.getKFile(); + if (file != null) { + try { + KotlinDecompilerAdapterKt.showDecompiledCode(file); + } + catch (IdeaLogger.InternalException ex) { + LOG.info(ex); + Messages.showErrorDialog(myProject, "Failed to decompile " + file.getName() + ": " + ex, "Kotlin Bytecode Decompiler"); + } + } + } + }); + } + /*TODO: try to extract default parameter from compiler options*/ - enableInline = new JCheckBox("Enable inline", true); - enableOptimization = new JCheckBox("Enable optimization", true); - enableAssertions = new JCheckBox("Enable assertions", true); + enableInline = new JCheckBox("Inline", true); + enableOptimization = new JCheckBox("Optimization", true); + enableAssertions = new JCheckBox("Assertions", true); optionPanel.add(enableInline); optionPanel.add(enableOptimization); optionPanel.add(enableAssertions); @@ -193,50 +222,7 @@ public class KotlinBytecodeToolWindow extends JPanel implements Disposable { ) { GenerationState state; try { - ResolutionFacade resolutionFacade = ResolutionUtils.getResolutionFacade(jetFile); - - BindingContext bindingContextForFile = resolutionFacade.analyzeFullyAndGetResult(Collections.singletonList(jetFile)).getBindingContext(); - - kotlin.Pair> result = DebuggerUtils.INSTANCE.analyzeInlinedFunctions( - resolutionFacade, bindingContextForFile, jetFile, !enableInline - ); - - BindingContext bindingContext = result.getFirst(); - List toProcess = result.getSecond(); - - GenerationState.GenerateClassFilter generateClassFilter = new GenerationState.GenerateClassFilter() { - - @Override - public boolean shouldGeneratePackagePart(@NotNull KtFile file) { - return file == jetFile; - } - - @Override - public boolean shouldAnnotateClass(@NotNull KtClassOrObject processingClassOrObject) { - return true; - } - - @Override - public boolean shouldGenerateClass(@NotNull KtClassOrObject processingClassOrObject) { - return processingClassOrObject.getContainingKtFile() == jetFile; - } - - @Override - public boolean shouldGenerateScript(@NotNull KtScript script) { - return script.getContainingKtFile() == jetFile; - } - }; - - state = new GenerationState(jetFile.getProject(), ClassBuilderFactories.TEST, - resolutionFacade.getModuleDescriptor(), bindingContext, - toProcess, - !enableAssertions, - !enableAssertions, - generateClassFilter, - !enableInline, - !enableOptimization, - /*useTypeTableInSerializer=*/false); - KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION); + state = compileSingleFile(jetFile, enableInline, enableAssertions, enableOptimization); } catch (ProcessCanceledException e) { throw e; @@ -273,6 +259,60 @@ public class KotlinBytecodeToolWindow extends JPanel implements Disposable { return answer.toString(); } + @NotNull + public static GenerationState compileSingleFile( + final KtFile ktFile, + boolean enableInline, + boolean enableAssertions, + boolean enableOptimization + ) { + ResolutionFacade resolutionFacade = ResolutionUtils.getResolutionFacade(ktFile); + + BindingContext bindingContextForFile = resolutionFacade.analyzeFullyAndGetResult(Collections.singletonList(ktFile)).getBindingContext(); + + kotlin.Pair> result = DebuggerUtils.INSTANCE.analyzeInlinedFunctions( + resolutionFacade, bindingContextForFile, ktFile, !enableInline + ); + + BindingContext bindingContext = result.getFirst(); + List toProcess = result.getSecond(); + + GenerationState.GenerateClassFilter generateClassFilter = new GenerationState.GenerateClassFilter() { + + @Override + public boolean shouldGeneratePackagePart(@NotNull KtFile file) { + return file == ktFile; + } + + @Override + public boolean shouldAnnotateClass(@NotNull KtClassOrObject processingClassOrObject) { + return true; + } + + @Override + public boolean shouldGenerateClass(@NotNull KtClassOrObject processingClassOrObject) { + return processingClassOrObject.getContainingKtFile() == ktFile; + } + + @Override + public boolean shouldGenerateScript(@NotNull KtScript script) { + return script.getContainingKtFile() == ktFile; + } + }; + + GenerationState state = new GenerationState(ktFile.getProject(), ClassBuilderFactories.TEST, + resolutionFacade.getModuleDescriptor(), bindingContext, + toProcess, + !enableAssertions, + !enableAssertions, + generateClassFilter, + !enableInline, + !enableOptimization, + /*useTypeTableInSerializer=*/false); + KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION); + return state; + } + private static Pair mapLines(String text, int startLine, int endLine) { int byteCodeLine = 0; int byteCodeStartLine = -1; diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt new file mode 100644 index 00000000000..65d7d0b4cb5 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerAdapter.kt @@ -0,0 +1,59 @@ +/* + * 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.internal + +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.WritingAccessProvider +import com.intellij.openapi.vfs.ex.dummy.DummyFileSystem +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.psi.KtFile + +fun showDecompiledCode(sourceFile: KtFile) { + val decompilerService = KotlinDecompilerService.getInstance() ?: return + val decompiledCode = decompilerService.decompile(sourceFile) + + runWriteAction { + val root = getOrCreateDummyRoot() + val decompiledFileName = FileUtil.getNameWithoutExtension(sourceFile.name) + ".decompiled.java" + val result = DummyFileSystem.getInstance().createChildFile(null, root, decompiledFileName) + VfsUtil.saveText(result, decompiledCode) + + OpenFileDescriptor(sourceFile.project, result).navigate(true) + } +} + +val KOTLIN_DECOMPILED_FOLDER = "kotlinDecompiled" +val KOTLIN_DECOMPILED_ROOT = "dummy://$KOTLIN_DECOMPILED_FOLDER" + +fun getOrCreateDummyRoot(): VirtualFile = + VirtualFileManager.getInstance().refreshAndFindFileByUrl(KOTLIN_DECOMPILED_ROOT) ?: + DummyFileSystem.getInstance().createRoot(KOTLIN_DECOMPILED_FOLDER) + +class DecompiledFileWritingAccessProvider : WritingAccessProvider() { + override fun isPotentiallyWritable(file: VirtualFile): Boolean { + if (file.fileSystem is DummyFileSystem && file.parent.name == KOTLIN_DECOMPILED_FOLDER) { + return false + } + return true + } + + override fun requestWriting(vararg files: VirtualFile): Collection = emptyList() +} diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt new file mode 100644 index 00000000000..f1e99e0e56e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerService.kt @@ -0,0 +1,31 @@ +/* + * 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.internal + +import com.intellij.openapi.components.ServiceManager +import org.jetbrains.kotlin.psi.KtFile + +interface KotlinDecompilerService { + fun decompile(file: KtFile): String + + companion object { + fun getInstance(): KotlinDecompilerService? { + return ServiceManager.getService(KotlinDecompilerService::class.java) + } + } +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt new file mode 100644 index 00000000000..f533dd7e685 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/internal/KotlinDecompilerServiceImpl.kt @@ -0,0 +1,92 @@ +/* + * 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.internal + +import com.intellij.openapi.util.io.FileUtilRt +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.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) + + val bytecodeMap = hashMapOf() + generationState.factory.asList().filter { FileUtilRt.extensionEquals(it.relativePath, "class") }.forEach { + bytecodeMap[File("/${it.relativePath}").absoluteFile] = it.asByteArray() + } + + val bytecodeProvider = KotlinBytecodeProvider(bytecodeMap) + val resultSaver = KotlinResultSaver() + val options = hashMapOf( + IFernflowerPreferences.REMOVE_BRIDGE to "0" + ) + val decompiler = BaseDecompiler(bytecodeProvider, resultSaver, options, IdeaLogger()) + for (path in bytecodeMap.keys) { + decompiler.addSpace(path, true) + } + decompiler.decompileContext() + return resultSaver.resultText + } + + class KotlinBytecodeProvider(val bytecodeMap: Map) : IBytecodeProvider { + override fun getBytecode(externalPath: String?, internalPath: String?): ByteArray? { + return bytecodeMap[File(externalPath)] + } + } + + class KotlinResultSaver : IResultSaver { + private val decompiledText = mutableMapOf() + + val resultText: String + get() { + decompiledText.values.singleOrNull()?.let { return it } + return buildString { + for ((filename, content) in decompiledText) { + appendln("// $filename") + append(content) + } + } + } + + override fun saveFolder(path: String?) { } + + override fun closeArchive(path: String?, archiveName: String?) { } + + override fun copyEntry(source: String?, path: String?, archiveName: String?, entry: String?) { } + + override fun createArchive(path: String?, archiveName: String?, manifest: Manifest?) { } + + override fun saveClassFile(path: String?, qualifiedName: String?, entryName: String?, content: String?, mapping: IntArray?) { + if (entryName != null && content != null) { + decompiledText[entryName] = content + } + } + + override fun copyFile(source: String?, path: String?, entryName: String?) { } + + override fun saveClassEntry(path: String?, archiveName: String?, qualifiedName: String?, entryName: String?, content: String?) { } + + override fun saveDirEntry(path: String?, archiveName: String?, entryName: String?) { } + + } +} \ No newline at end of file