/*
* 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.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;
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;
import org.jetbrains.kotlin.codegen.CompilationErrorHandler;
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade;
import org.jetbrains.kotlin.codegen.state.GenerationState;
import org.jetbrains.kotlin.config.*;
import org.jetbrains.kotlin.diagnostics.Diagnostic;
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils;
import org.jetbrains.kotlin.idea.project.PlatformKt;
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade;
import org.jetbrains.kotlin.idea.util.InfinitePeriodicalTask;
import org.jetbrains.kotlin.idea.util.LongRunningReadTask;
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil;
import org.jetbrains.kotlin.psi.KtClassOrObject;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.psi.KtScript;
import org.jetbrains.kotlin.resolve.BindingContext;
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" +
"No Kotlin source file is opened.\n" +
"*/";
private class UpdateBytecodeToolWindowTask extends LongRunningReadTask<Location, String> {
@Override
protected Location prepareRequestInfo() {
if (!toolWindow.isVisible()) {
return null;
}
Location location = Location.fromEditor(FileEditorManager.getInstance(myProject).getSelectedTextEditor(), myProject);
if (location.getEditor() == null) {
return null;
}
KtFile file = location.getKFile();
if (file == null || !ProjectRootsUtil.isInProjectSource(file)) {
return null;
}
return location;
}
@NotNull
@Override
protected Location cloneRequestInfo(@NotNull Location location) {
Location newLocation = super.cloneRequestInfo(location);
assert location.equals(newLocation) : "cloneRequestInfo should generate same location object";
return newLocation;
}
@Override
protected void hideResultOnInvalidLocation() {
setText(DEFAULT_TEXT);
}
@NotNull
@Override
protected String processRequest(@NotNull Location location) {
KtFile ktFile = location.getKFile();
assert ktFile != null;
CompilerConfiguration configuration = new CompilerConfiguration();
if (!enableInline.isSelected()) {
configuration.put(CommonConfigurationKeys.DISABLE_INLINE, true);
}
if (!enableAssertions.isSelected()) {
configuration.put(JVMConfigurationKeys.DISABLE_CALL_ASSERTIONS, true);
configuration.put(JVMConfigurationKeys.DISABLE_PARAM_ASSERTIONS, true);
}
if (!enableOptimization.isSelected()) {
configuration.put(JVMConfigurationKeys.DISABLE_OPTIMIZATION, true);
}
if (jvm8Target.isSelected()) {
configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8);
}
if (ir.isSelected()) {
configuration.put(JVMConfigurationKeys.IR, true);
}
CommonConfigurationKeysKt.setLanguageVersionSettings(configuration, PlatformKt.getLanguageVersionSettings(ktFile));
return getBytecodeForFile(ktFile, configuration);
}
@Override
protected void onResultReady(@NotNull Location requestInfo, String resultText) {
Editor editor = requestInfo.getEditor();
assert editor != null;
if (resultText == null) {
return;
}
setText(resultText);
int fileStartOffset = requestInfo.getStartOffset();
int fileEndOffset = requestInfo.getEndOffset();
Document document = editor.getDocument();
int startLine = document.getLineNumber(fileStartOffset);
int endLine = document.getLineNumber(fileEndOffset);
if (endLine > startLine && fileEndOffset > 0 && document.getCharsSequence().charAt(fileEndOffset - 1) == '\n') {
endLine--;
}
Document byteCodeDocument = myEditor.getDocument();
Pair<Integer, Integer> linesRange = mapLines(byteCodeDocument.getText(), startLine, endLine);
int endSelectionLineIndex = Math.min(linesRange.second + 1, byteCodeDocument.getLineCount());
int startOffset = byteCodeDocument.getLineStartOffset(linesRange.first);
int endOffset = Math.min(byteCodeDocument.getLineStartOffset(endSelectionLineIndex), byteCodeDocument.getTextLength());
myEditor.getCaretModel().moveToOffset(endOffset);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
myEditor.getCaretModel().moveToOffset(startOffset);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
myEditor.getSelectionModel().setSelection(startOffset, endOffset);
}
}
private final Editor myEditor;
private final Project myProject;
private final ToolWindow toolWindow;
private final JCheckBox enableInline;
private final JCheckBox enableOptimization;
private final JCheckBox enableAssertions;
private final JButton decompile;
private final JCheckBox jvm8Target;
private final JCheckBox ir;
public KotlinBytecodeToolWindow(Project project, ToolWindow toolWindow) {
super(new BorderLayout());
myProject = project;
this.toolWindow = toolWindow;
myEditor = EditorFactory.getInstance().createEditor(
EditorFactory.getInstance().createDocument(""), project, JavaFileType.INSTANCE, true);
add(myEditor.getComponent());
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("Inline", true);
enableOptimization = new JCheckBox("Optimization", true);
enableAssertions = new JCheckBox("Assertions", true);
jvm8Target = new JCheckBox("JVM 8 target", false);
ir = new JCheckBox("IR", false);
optionPanel.add(enableInline);
optionPanel.add(enableOptimization);
optionPanel.add(enableAssertions);
optionPanel.add(ir);
optionPanel.add(jvm8Target);
new InfinitePeriodicalTask(UPDATE_DELAY, Alarm.ThreadToUse.SWING_THREAD, this, new Computable<LongRunningReadTask>() {
@Override
public LongRunningReadTask compute() {
return new UpdateBytecodeToolWindowTask();
}
}).start();
setText(DEFAULT_TEXT);
}
// public for tests
@NotNull
public static String getBytecodeForFile(@NotNull KtFile ktFile, @NotNull CompilerConfiguration configuration) {
GenerationState state;
try {
state = compileSingleFile(ktFile, configuration);
}
catch (ProcessCanceledException e) {
throw e;
}
catch (Exception e) {
return printStackTraceToString(e);
}
StringBuilder answer = new StringBuilder();
Collection<Diagnostic> diagnostics = state.getCollectedExtraJvmDiagnostics().all();
if (!diagnostics.isEmpty()) {
answer.append("// Backend Errors: \n");
answer.append("// ================\n");
for (Diagnostic diagnostic : diagnostics) {
answer.append("// Error at ")
.append(diagnostic.getPsiFile().getName())
.append(StringsKt.join(diagnostic.getTextRanges(), ","))
.append(": ")
.append(DefaultErrorMessages.render(diagnostic))
.append("\n");
}
answer.append("// ================\n\n");
}
OutputFileCollection outputFiles = state.getFactory();
for (OutputFile outputFile : outputFiles.asList()) {
answer.append("// ================");
answer.append(outputFile.getRelativePath());
answer.append(" =================\n");
answer.append(outputFile.asText()).append("\n\n");
}
return answer.toString();
}
@NotNull
public static GenerationState compileSingleFile(
@NotNull final KtFile ktFile,
@NotNull CompilerConfiguration configuration
) {
ResolutionFacade resolutionFacade = ResolutionUtils.getResolutionFacade(ktFile);
BindingContext bindingContextForFile = resolutionFacade.analyzeFullyAndGetResult(Collections.singletonList(ktFile)).getBindingContext();
kotlin.Pair<BindingContext, List<KtFile>> result = DebuggerUtils.INSTANCE.analyzeInlinedFunctions(
resolutionFacade, ktFile, configuration.getBoolean(CommonConfigurationKeys.DISABLE_INLINE),
bindingContextForFile
);
BindingContext bindingContext = result.getFirst();
List<KtFile> 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,
configuration, generateClassFilter
);
KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION);
return state;
}
private static Pair<Integer, Integer> mapLines(String text, int startLine, int endLine) {
int byteCodeLine = 0;
int byteCodeStartLine = -1;
int byteCodeEndLine = -1;
List<Integer> lines = new ArrayList<Integer>();
for (String line : text.split("\n")) {
line = line.trim();
if (line.startsWith("LINENUMBER")) {
int ktLineNum = new Scanner(line.substring("LINENUMBER".length())).nextInt() - 1;
lines.add(ktLineNum);
}
}
Collections.sort(lines);
for (Integer line : lines) {
if (line >= startLine) {
startLine = line;
break;
}
}
for (String line : text.split("\n")) {
line = line.trim();
if (line.startsWith("LINENUMBER")) {
int ktLineNum = new Scanner(line.substring("LINENUMBER".length())).nextInt() - 1;
if (byteCodeStartLine < 0 && ktLineNum == startLine) {
byteCodeStartLine = byteCodeLine;
}
if (byteCodeStartLine > 0 && ktLineNum > endLine) {
byteCodeEndLine = byteCodeLine - 1;
break;
}
}
if (byteCodeStartLine >= 0 && (line.startsWith("MAXSTACK") || line.startsWith("LOCALVARIABLE") || line.isEmpty())) {
byteCodeEndLine = byteCodeLine - 1;
break;
}
byteCodeLine++;
}
if (byteCodeStartLine == -1 || byteCodeEndLine == -1) {
return new Pair<Integer, Integer>(0, 0);
}
else {
return new Pair<Integer, Integer>(byteCodeStartLine, byteCodeEndLine);
}
}
private static String printStackTraceToString(Throwable e) {
StringWriter out = new StringWriter(1024);
try (PrintWriter printWriter = new PrintWriter(out)) {
e.printStackTrace(printWriter);
return out.toString().replace("\r", "");
}
}
private void setText(@NotNull final String resultText) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
myEditor.getDocument().setText(StringUtil.convertLineSeparators(resultText));
}
});
}
@Override
public void dispose() {
EditorFactory.getInstance().releaseEditor(myEditor);
}
}