From 65ea14169ffc82e8efcf989353b584012748fb29 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 4 Dec 2024 17:20:47 +0100 Subject: [PATCH] [BytecodeViewer] IDEA-364201 new Java bytecode viewer (cherry picked from commit e65e45e753abafa9014a3a8fcd483f03df23d82b) IJ-MR-151769 GitOrigin-RevId: 781d88b5e0280d29bcb9e1d361f1b192880910ae --- .../intellij.java.byteCodeViewer.iml | 2 + .../resources/META-INF/plugin.xml | 14 +- .../messages/BytecodeViewerBundle.properties | 8 + .../JavaByteCodeViewerBundle.properties | 14 - .../ByteCodeViewerComponent.java | 147 --- .../byteCodeViewer/ByteCodeViewerManager.java | 294 ------ .../byteCodeViewer/BytecodeLineMapping.kt | 110 +++ .../BytecodeToolWindowPanel.java | 478 ++++++++++ ...rBundle.java => BytecodeViewerBundle.java} | 8 +- .../byteCodeViewer/BytecodeViewerManager.java | 155 ++++ .../byteCodeViewer/BytecodeViewerUtil.kt | 80 ++ .../byteCodeViewer/ShowByteCodeAction.java | 186 ---- .../byteCodeViewer/ShowBytecodeAction.java | 74 ++ .../byteCodeViewer/BytecodeLineMappingTest.kt | 863 ++++++++++++++++++ .../java/decompiler/SingleClassesTest.java | 3 +- .../testData/classes/pkg/TestHelloWorld.class | Bin 0 -> 437 bytes .../testData/results/TestHelloWorld.dec | 25 + .../testData/src/pkg/TestHelloWorld.java | 7 + 18 files changed, 1817 insertions(+), 651 deletions(-) create mode 100644 plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties delete mode 100644 plugins/ByteCodeViewer/resources/messages/JavaByteCodeViewerBundle.properties delete mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerComponent.java delete mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerManager.java create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeLineMapping.kt create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.java rename plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/{JavaByteCodeViewerBundle.java => BytecodeViewerBundle.java} (80%) create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerManager.java create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerUtil.kt delete mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowByteCodeAction.java create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.java create mode 100644 plugins/ByteCodeViewer/test/com/intellij/byteCodeViewer/BytecodeLineMappingTest.kt create mode 100644 plugins/java-decompiler/engine/testData/classes/pkg/TestHelloWorld.class create mode 100644 plugins/java-decompiler/engine/testData/results/TestHelloWorld.dec create mode 100644 plugins/java-decompiler/engine/testData/src/pkg/TestHelloWorld.java diff --git a/plugins/ByteCodeViewer/intellij.java.byteCodeViewer.iml b/plugins/ByteCodeViewer/intellij.java.byteCodeViewer.iml index e1c0ad513c15..4b7a3ad9b215 100644 --- a/plugins/ByteCodeViewer/intellij.java.byteCodeViewer.iml +++ b/plugins/ByteCodeViewer/intellij.java.byteCodeViewer.iml @@ -4,6 +4,7 @@ + @@ -19,6 +20,7 @@ + diff --git a/plugins/ByteCodeViewer/resources/META-INF/plugin.xml b/plugins/ByteCodeViewer/resources/META-INF/plugin.xml index bfd780025c10..cad2bfeefd8e 100644 --- a/plugins/ByteCodeViewer/resources/META-INF/plugin.xml +++ b/plugins/ByteCodeViewer/resources/META-INF/plugin.xml @@ -3,9 +3,13 @@ ByteCodeViewer Other Tools - - -To view the bytecode, select a class file and open View | Show Bytecode.]]> + + To view the bytecode, select a .java or .class file and open View | Show Bytecode. +
+ If you select a .java file, make sure that you've compiled it before triggering "Show Bytecode" action. + ]]>
JetBrains @@ -21,10 +25,10 @@ To view the bytecode, select a class file and open View | Show Bytecode.] - + - messages.JavaByteCodeViewerBundle + messages.BytecodeViewerBundle diff --git a/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties new file mode 100644 index 000000000000..939fc894106a --- /dev/null +++ b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties @@ -0,0 +1,8 @@ +action.BytecodeViewer.text=Show Bytecode +bytecode.class.in.selection.message=The selection should contain a class +bytecode.not.found.title=Bytecode not found +class.file.may.be.out.of.date=Class File May Be Outdated +loading.bytecode=Loading bytecode\u2026 +checking.if.bytecode.is.up.to.date=Checking if bytecode is up to date +bytecode.for.class=Bytecode for class ''{0}'' +open.java.file.to.see.bytecode=Open a Java source file or class file to see the corresponding bytecode. diff --git a/plugins/ByteCodeViewer/resources/messages/JavaByteCodeViewerBundle.properties b/plugins/ByteCodeViewer/resources/messages/JavaByteCodeViewerBundle.properties deleted file mode 100644 index 085cd529cc4c..000000000000 --- a/plugins/ByteCodeViewer/resources/messages/JavaByteCodeViewerBundle.properties +++ /dev/null @@ -1,14 +0,0 @@ -show.bytecode.tool.window.title=Byte Code Viewer -action.ByteCodeViewer.text=Show Bytecode -bytecode.class.in.selection.message=The selection should contain a class -bytecode.not.found.message=Unable to Find Class to Show Bytecode -bytecode.not.found.title=Bytecode not Found -bytecode.parser.failure.message=Unable to parse class file for ''{0}''. -class.file.may.be.out.of.date=Class File May Be Out-of-Date -looking.for.bytecode.progress=Looking for Bytecode\u2026 -no.bytecode.found=No bytecode found -no.bytecode.found.for=No bytecode found for {0} -popup.title.element.bytecode={0} Bytecode -show.bytecode.for.current.element.action.description=Show bytecode for current element automatically -show.bytecode.for.current.element.action.name=Auto Show Bytecode for Selected Element -show.bytecode.restore.popup.action.description=Restore bytecode popup behavior \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerComponent.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerComponent.java deleted file mode 100644 index bf58cd03c3d4..000000000000 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerComponent.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2000-2015 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 com.intellij.byteCodeViewer; - -import com.intellij.execution.filters.LineNumbersMapping; -import com.intellij.ide.highlighter.JavaFileType; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.editor.*; -import com.intellij.openapi.editor.colors.EditorColorsManager; -import com.intellij.openapi.editor.ex.EditorEx; -import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory; -import com.intellij.openapi.editor.impl.EditorFactoryImpl; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileTypes.SyntaxHighlighter; -import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; -import com.intellij.psi.util.PsiUtilCore; -import com.intellij.util.DocumentUtil; -import org.jetbrains.annotations.NonNls; - -import javax.swing.*; -import java.awt.*; - -/** - * @author anna - */ -public class ByteCodeViewerComponent extends JPanel implements Disposable { - - private final Editor myEditor; - - public ByteCodeViewerComponent(Project project) { - super(new BorderLayout()); - final EditorFactory factory = EditorFactory.getInstance(); - final Document doc = ((EditorFactoryImpl)factory).createDocument("", true, false); - doc.setReadOnly(true); - myEditor = factory.createEditor(doc, project); - EditorHighlighterFactory editorHighlighterFactory = EditorHighlighterFactory.getInstance(); - final SyntaxHighlighter syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(JavaFileType.INSTANCE, project, null); - ((EditorEx)myEditor).setHighlighter(editorHighlighterFactory.createEditorHighlighter(syntaxHighlighter, EditorColorsManager.getInstance().getGlobalScheme())); - ((EditorEx)myEditor).setCaretVisible(true); - - final EditorSettings settings = myEditor.getSettings(); - settings.setLineMarkerAreaShown(false); - settings.setIndentGuidesShown(false); - settings.setLineNumbersShown(false); - settings.setFoldingOutlineShown(false); - - myEditor.setBorder(null); - add(myEditor.getComponent(), BorderLayout.CENTER); - } - - public void setText(final String bytecode) { - setText(bytecode, 0); - } - - public void setText(@NonNls final String bytecode, PsiElement element, int lineNumber) { - int offset = -1; - VirtualFile file = PsiUtilCore.getVirtualFile(element); - if (file != null) { - final Document document = FileDocumentManager.getInstance().getDocument(file); - if (document != null) { - if (lineNumber == -1) { - lineNumber = document.getLineNumber(element.getTextOffset()); - } - - // Use 1-based line numbers from here: - lineNumber++; - - LineNumbersMapping mapping = file.getUserData(LineNumbersMapping.LINE_NUMBERS_MAPPING_KEY); - if (mapping != null) { - for (int l = lineNumber; l <= document.getLineCount(); l++) { - int bytecodeLine = mapping.sourceToBytecode(l); - if (bytecodeLine != -1) { - offset = findLineNumber(bytecode, bytecodeLine); - if (offset != -1) { - break; - } - } - } - } - else { - for (int l = lineNumber; l <= document.getLineCount(); l++) { - offset = findLineNumber(bytecode, l); - if (offset != -1) { - break; - } - } - } - } - } - setText(bytecode, Math.max(0, offset)); - } - - private static int findLineNumber(@NonNls String bytecode, int l) { - int idx = bytecode.indexOf("\n LINENUMBER " + l + " "); - if (idx != -1) { - // shift by one to skip '\n' - return idx + 1; - } else { - return -1; - } - } - - public void setText(final String bytecode, final int offset) { - DocumentUtil.writeInRunUndoTransparentAction(() -> { - Document fragmentDoc = myEditor.getDocument(); - fragmentDoc.setReadOnly(false); - try { - fragmentDoc.replaceString(0, fragmentDoc.getTextLength(), bytecode); - } - finally { - fragmentDoc.setReadOnly(true); - } - - myEditor.getCaretModel().moveToOffset(offset); - myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); - }); - } - - public String getText() { - return myEditor.getDocument().getText(); - } - - public JComponent getEditorComponent() { - return myEditor.getContentComponent(); - } - - @Override - public void dispose() { - EditorFactory.getInstance().releaseEditor(myEditor); - } -} diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerManager.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerManager.java deleted file mode 100644 index c2eebe933e0a..000000000000 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ByteCodeViewerManager.java +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.byteCodeViewer; - -import com.intellij.codeInsight.documentation.DockablePopupManager; -import com.intellij.ide.util.JavaAnonymousClassesHelper; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.extensions.ExtensionPointName; -import com.intellij.openapi.fileTypes.FileTypeRegistry; -import com.intellij.openapi.fileTypes.StdFileTypes; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.CompilerModuleExtension; -import com.intellij.openapi.roots.ProjectFileIndex; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; -import com.intellij.psi.presentation.java.SymbolPresentationUtil; -import com.intellij.psi.util.ClassUtil; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.util.PsiUtil; -import com.intellij.ui.content.Content; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.org.objectweb.asm.ClassReader; -import org.jetbrains.org.objectweb.asm.util.Textifier; -import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor; - -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.Queue; - -/** - * @author anna - */ -@Service(Service.Level.PROJECT) -public final class ByteCodeViewerManager extends DockablePopupManager { - private static final ExtensionPointName CLASS_SEARCHER_EP = ExtensionPointName.create("ByteCodeViewer.classSearcher"); - - private static final Logger LOG = Logger.getInstance(ByteCodeViewerManager.class); - - private static final @NonNls String TOOLWINDOW_ID = "Byte Code Viewer"; - private static final @NonNls String SHOW_BYTECODE_IN_TOOL_WINDOW = "BYTE_CODE_TOOL_WINDOW"; - private static final @NonNls String BYTECODE_AUTO_UPDATE_ENABLED = "BYTE_CODE_AUTO_UPDATE_ENABLED"; - - public static ByteCodeViewerManager getInstance(Project project) { - return project.getService(ByteCodeViewerManager.class); - } - - public ByteCodeViewerManager(Project project) { - super(project); - } - - @Override - public String getShowInToolWindowProperty() { - return SHOW_BYTECODE_IN_TOOL_WINDOW; - } - - @Override - public String getAutoUpdateEnabledProperty() { - return BYTECODE_AUTO_UPDATE_ENABLED; - } - - @Override - protected String getToolwindowId() { - return TOOLWINDOW_ID; - } - - @Override - protected String getToolwindowTitle() { - return JavaByteCodeViewerBundle.message("show.bytecode.tool.window.title"); - } - - @Override - protected String getAutoUpdateTitle() { - return JavaByteCodeViewerBundle.message("show.bytecode.for.current.element.action.name"); - } - - @Override - protected String getAutoUpdateDescription() { - return JavaByteCodeViewerBundle.message("show.bytecode.for.current.element.action.description"); - } - - @Override - protected String getRestorePopupDescription() { - return JavaByteCodeViewerBundle.message("show.bytecode.restore.popup.action.description"); - } - - @Override - protected ByteCodeViewerComponent createComponent() { - return new ByteCodeViewerComponent(myProject); - } - - @Override - @Nullable - protected String getTitle(PsiElement element) { - PsiClass aClass = getContainingClass(element); - if (aClass == null) return null; - return SymbolPresentationUtil.getSymbolPresentableText(aClass); - } - - private void updateByteCode(PsiElement element, ByteCodeViewerComponent component, Content content) { - updateByteCode(element, -1, component, content, getByteCode(element)); - } - - private void updateByteCode(PsiElement element, int lineNumber, ByteCodeViewerComponent component, Content content, String byteCode) { - if (!StringUtil.isEmpty(byteCode)) { - component.setText(byteCode, element, lineNumber); - } - else { - PsiElement presentableElement = getContainingClass(element); - if (presentableElement == null) { - presentableElement = element.getContainingFile(); - if (presentableElement == null && element instanceof PsiNamedElement) { - presentableElement = element; - } - if (presentableElement == null) { - component.setText(JavaByteCodeViewerBundle.message("no.bytecode.found")); - return; - } - } - component.setText( - JavaByteCodeViewerBundle.message("no.bytecode.found.for", SymbolPresentationUtil.getSymbolPresentableText(presentableElement))); - } - content.setDisplayName(getTitle(element)); - } - - @Override - protected void doUpdateComponent(@NotNull PsiElement element, PsiElement originalElement, ByteCodeViewerComponent component) { - Content content = myToolWindow.getContentManager().getSelectedContent(); - if (content != null) { - updateByteCode(element, component, content); - } - } - - @Override - protected void doUpdateComponent(Editor editor, PsiFile psiFile) { - Content content = myToolWindow.getContentManager().getSelectedContent(); - if (content == null) { - return; - } - - ByteCodeViewerComponent component = (ByteCodeViewerComponent)content.getComponent(); - PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset()); - if (element != null) { - updateByteCode(element, component, content); - } - } - - @Override - protected void doUpdateComponent(@NotNull PsiElement element) { - doUpdateComponent(element, -1, getByteCode(element)); - } - - void doUpdateComponent(@NotNull PsiElement element, int lineNumber, final String newText) { - Content content = myToolWindow.getContentManager().getSelectedContent(); - if (content != null) { - updateByteCode(element, lineNumber, (ByteCodeViewerComponent)content.getComponent(), content, newText); - } - } - - @Nullable - public static String getByteCode(@NotNull PsiElement psiElement) { - PsiClass containingClass = getContainingClass(psiElement); - if (containingClass != null) { - try { - byte[] bytes = loadClassFileBytes(containingClass); - if (bytes != null) { - StringWriter writer = new StringWriter(); - try (PrintWriter printWriter = new PrintWriter(writer)) { - new ClassReader(bytes).accept(new TraceClassVisitor(null, new Textifier(), printWriter), 0); - } - return writer.toString(); - } - } - catch (IOException e) { - LOG.error(e); - } - } - return null; - } - - private static byte[] loadClassFileBytes(PsiClass aClass) throws IOException { - String jvmClassName = getJVMClassName(aClass); - if (jvmClassName != null) { - PsiClass fileClass = aClass; - while (PsiUtil.isLocalOrAnonymousClass(fileClass)) { - PsiClass containingClass = PsiTreeUtil.getParentOfType(fileClass, PsiClass.class); - if (containingClass != null) { - fileClass = containingClass; - } - } - VirtualFile file = fileClass.getOriginalElement().getContainingFile().getVirtualFile(); - if (file != null) { - ProjectFileIndex index = ProjectFileIndex.getInstance(aClass.getProject()); - if (FileTypeRegistry.getInstance().isFileOfType(file, StdFileTypes.CLASS)) { - // compiled class; looking for the right .class file (inner class 'A.B' is "contained" in 'A.class', but we need 'A$B.class') - String classFileName = StringUtil.getShortName(jvmClassName) + ".class"; - if (index.isInLibraryClasses(file)) { - VirtualFile classFile = file.getParent().findChild(classFileName); - if (classFile != null) { - return classFile.contentsToByteArray(false); - } - } - else { - File classFile = new File(file.getParent().getPath(), classFileName); - if (classFile.isFile()) { - return FileUtil.loadFileBytes(classFile); - } - } - } - else { - // source code; looking for a .class file in compiler output - Module module = index.getModuleForFile(file); - if (module != null) { - CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module); - if (extension != null) { - boolean inTests = index.isInTestSourceContent(file); - VirtualFile classRoot = inTests ? extension.getCompilerOutputPathForTests() : extension.getCompilerOutputPath(); - if (classRoot != null) { - String relativePath = jvmClassName.replace('.', '/') + ".class"; - File classFile = new File(classRoot.getPath(), relativePath); - if (classFile.exists()) { - return FileUtil.loadFileBytes(classFile); - } - } - } - } - } - } - } - - return null; - } - - private static String getJVMClassName(PsiClass aClass) { - if (!(aClass instanceof PsiAnonymousClass)) { - return ClassUtil.getJVMClassName(aClass); - } - - PsiClass containingClass = PsiTreeUtil.getParentOfType(aClass, PsiClass.class); - if (containingClass != null) { - return getJVMClassName(containingClass) + JavaAnonymousClassesHelper.getName((PsiAnonymousClass)aClass); - } - - return null; - } - - @Nullable - public static PsiClass getContainingClass(@NotNull PsiElement psiElement) { - for (ClassSearcher searcher : CLASS_SEARCHER_EP.getExtensionList()) { - PsiClass aClass = searcher.findClass(psiElement); - if (aClass != null) { - return aClass; - } - } - - PsiClass containingClass = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class, false); - while (containingClass instanceof PsiTypeParameter) { - containingClass = PsiTreeUtil.getParentOfType(containingClass, PsiClass.class); - } - - if (containingClass == null) { - PsiFile containingFile = psiElement.getContainingFile(); - if (containingFile instanceof PsiClassOwner) { - TextRange textRange = psiElement.getTextRange(); - PsiClass result = null; - Queue queue = new ArrayDeque<>(Arrays.asList(((PsiClassOwner)containingFile).getClasses())); - while (!queue.isEmpty()) { - PsiClass c = queue.remove(); - PsiElement navigationElement = c.getNavigationElement(); - TextRange classRange = navigationElement != null ? navigationElement.getTextRange() : null; - if (classRange != null && classRange.contains(textRange)) { - result = c; - queue.clear(); - queue.addAll(Arrays.asList(c.getInnerClasses())); - } - } - return result; - } - return null; - } - - return containingClass; - } -} \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeLineMapping.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeLineMapping.kt new file mode 100644 index 000000000000..13a93de24f57 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeLineMapping.kt @@ -0,0 +1,110 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer + +import java.util.* + + +/** + * This method the following debugging information from `bytecode`: + * - `LINENUMBER` + * - `LOCALVARIABLE` + * + * ### Why is this needed? + * + * Ideally, we would use `ClassReader#SKIP_DEBUG` flag, but it has problematic behavior when it comes to labels. + * + * When parsing bytecode with ASM's `ClassReader`, we want to set `ClassReader#SKIP_DEBUG`, because it's actually not part of bytecode. + * Unfortunately, `ClassReader#SKIP_DEBUG` also removes labels from the bytecode in most cases. + * This is bad because labels are often targets of conditional jumps. + * Also, we do want to display labels. + * They're useful. + * + * // TODO(bartekpacia): Create issue on https://gitlab.ow2.org/asm/asm + */ +internal fun removeDebugInfo(bytecodeWithDebugInfo: String): String = bytecodeWithDebugInfo.lines() + .filter { line -> !isDebugLine(line.trim()) } + .joinToString("\n") + + +/** + * Maps the line numbers from the provided bytecode to the source code line numbers within a specified range. + * + * @param bytecodeWithDebugInfo The Java bytecode in ASM format, with debugging information included (see `ClassReader#SKIP_DEBUG`) + * @param sourceStartLine The starting line number in the source code to map from. + * @param sourceEndLine The ending line number in the source code to map to. + * @return A pair where the first element is the start line number in the bytecode, and the second element is the end line number in the bytecode. Returns (0, 0) if no valid mapping + * is found. + */ +internal fun mapLines(bytecodeWithDebugInfo: String, sourceStartLine: Int, sourceEndLine: Int, stripDebugInfo: Boolean = false): IntRange { + var sourceStartLine = sourceStartLine // + 1 // editor selection is 0-indexed + var currentBytecodeLine = 0 + var bytecodeStartLine = -1 + var bytecodeEndLine = -1 + + val lines = arrayListOf() + for (line in bytecodeWithDebugInfo.split("\n").dropLastWhile { it.isEmpty() }.map { line -> line.trim { it <= ' ' } }) { + if (line.startsWith("LINENUMBER")) { + // `line` is e.g. "LINENUMBER 3 L0" or "LINENUMBER 6 L1", but we are only interested in the 3 or 6, respectively. + val ktLineNum = Scanner(line.substring("LINENUMBER".length)).nextInt() - 1 + lines.add(ktLineNum) + } + } + lines.sort() + + for (line in lines) { + if (line >= sourceStartLine) { + sourceStartLine = line + break + } + } + + var linesToSkipBeforeStartLine = 0 + var linesToSkipBeforeEndLine = 0 + + for (line in bytecodeWithDebugInfo.split("\n").dropLastWhile { it.isEmpty() }.map { line -> line.trim { it <= ' ' } }) { + if (bytecodeEndLine < 0 && isDebugLine(line)) { + linesToSkipBeforeEndLine++ + } + + if (bytecodeStartLine < 0 && isDebugLine(line)) { + linesToSkipBeforeStartLine++ + } + + if (line.startsWith("LINENUMBER")) { + val ktLineNum = Scanner(line.substring("LINENUMBER".length)).nextInt() - 1 + + if (bytecodeStartLine < 0 && ktLineNum == sourceStartLine) { + bytecodeStartLine = currentBytecodeLine + } + + if (bytecodeStartLine > 0 && ktLineNum > sourceEndLine) { + bytecodeEndLine = currentBytecodeLine - 1 + break + } + } + + if (bytecodeStartLine >= 0 && (line.startsWith("MAXSTACK") || line.startsWith("LOCALVARIABLE") || line.isEmpty())) { + // We have reached the end of the method body + bytecodeEndLine = currentBytecodeLine - 1 + break + } + + currentBytecodeLine++ + } + + if (stripDebugInfo) { + bytecodeStartLine -= linesToSkipBeforeStartLine + bytecodeEndLine -= linesToSkipBeforeEndLine + } + + return if (bytecodeStartLine == -1 || bytecodeEndLine == -1) IntRange(0, 0) else IntRange(bytecodeStartLine, bytecodeEndLine) +} + +/** + * Returns true if `line` is considered to be part of debug info, i.e. not actual bytecode. + */ +private fun isDebugLine(line: String): Boolean { + if (line.startsWith("LINENUMBER")) return true + if (line.startsWith("LOCALVARIABLE")) return true + return false +} diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.java new file mode 100644 index 000000000000..c5c43d614669 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.java @@ -0,0 +1,478 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer; + +import com.intellij.ide.highlighter.JavaClassFileType; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.application.WriteAction; +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.editor.event.CaretEvent; +import com.intellij.openapi.editor.event.CaretListener; +import com.intellij.openapi.editor.event.EditorEventMulticaster; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.NlsSafe; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiUtilBase; +import com.intellij.ui.JBColor; +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread; +import com.intellij.util.concurrency.annotations.RequiresEdt; +import com.intellij.util.concurrency.annotations.RequiresWriteLock; +import com.intellij.util.messages.MessageBus; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.util.progress.CancellationUtil; +import kotlin.Pair; +import kotlin.ranges.IntRange; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static com.intellij.byteCodeViewer.BytecodeLineMappingKt.mapLines; +import static com.intellij.byteCodeViewer.BytecodeViewerUtilKt.*; + +final class BytecodeToolWindowPanel extends JPanel implements Disposable { + public static final String TOOL_WINDOW_ID = "Java Bytecode"; + + private static final Logger LOG = Logger.getInstance(BytecodeToolWindowPanel.class); + private static final Key BYTECODE_WITH_DEBUG_INFO = Key.create("BYTECODE_WITH_DEBUG_INFO"); + + private static final String DEFAULT_TEXT = BytecodeViewerBundle.message("open.java.file.to.see.bytecode"); + + private final Project project; + /// The tool window that this panel is displayed in + private final ToolWindow toolWindow; + private final Document bytecodeDocument; + private final Editor bytecodeEditor; + private final JLabel classNameLabel; + private final JLabel errorLabel; + + private @Nullable String currentlyDisplayedClassFQN; + private @Nullable VirtualFile currentlyFocusedSourceFile; + + private @Nullable LoadBytecodeTask existingLoadBytecodeTask; + private @Nullable UpdateBytecodeStatusTask existingUpdateBytecodeStatusTask; + + BytecodeToolWindowPanel(Project project, ToolWindow toolWindow, Editor initialSourceEditor) { + super(new BorderLayout()); + this.project = project; + this.toolWindow = toolWindow; + + bytecodeDocument = EditorFactory.getInstance().createDocument(""); + // TODO(bartekpacia): JavaClassFileType doesn't seem right, because its 'isBinary()' method returns true. + // The text we display in the editor is actually a human-readable representation of the bytecode (as returned by ASM ClassReader). + bytecodeEditor = EditorFactory.getInstance().createEditor(bytecodeDocument, project, JavaClassFileType.INSTANCE, true); + classNameLabel = new JLabel(); + errorLabel = new JLabel(); + + currentlyFocusedSourceFile = initialSourceEditor.getVirtualFile(); + + setUpContent(); + WriteAction.run(() -> setBytecodeText(null, DEFAULT_TEXT)); + setUpListeners(); + + LOG.trace("Scheduled loading bytecode because the initial tool window setup occurred"); + queueLoadBytecodeTask(() -> updateBytecodeSelection(initialSourceEditor)); + } + + private void setUpContent() { + bytecodeEditor.setBorder(null); + add(bytecodeEditor.getComponent()); + + JPanel optionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 12, 8)); + + optionPanel.add(classNameLabel); + + errorLabel.setForeground(JBColor.YELLOW); + optionPanel.add(errorLabel); + + add(optionPanel, BorderLayout.NORTH); + } + + private void setUpListeners() { + MessageBus messageBus = project.getMessageBus(); + MessageBusConnection messageBusConnection = messageBus.connect(this); + messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() { + + @Override + public void selectionChanged(@NotNull FileEditorManagerEvent event) { + if (!toolWindow.isVisible()) return; + + currentlyFocusedSourceFile = event.getNewFile(); + + final VirtualFile newFile = event.getNewFile(); + if (newFile == null) return; + if (!isValidFileType(newFile.getFileType())) return; + + final FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(newFile); + if (!(fileEditor instanceof TextEditor textEditor)) return; + final Editor sourceEditor = textEditor.getEditor(); + + queueLoadBytecodeTask(() -> updateBytecodeSelection(sourceEditor)); + LOG.trace("Scheduled loading bytecode because listener fired: FileEditorManagerListener.selectionChanged()"); + } + }); + + EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster(); + + multicaster.addCaretListener(new CaretListener() { + @Override + public void caretPositionChanged(@NotNull CaretEvent event) { + if (!toolWindow.isVisible()) return; + + LOG.trace("Will update bytecode selection because listener fired: CaretListener.caretPositionChanged()"); + updateBytecodeSelection(event.getEditor()); + } + }, this); + } + + /// Update only text selection ranges. Do not read bytecode again. + /// + /// @param sourceEditor an editor that displays Java code (either real source Java or decompiled Java). If not, this method does nothing. + @RequiresEdt + private void updateBytecodeSelection(Editor sourceEditor) { + if (sourceEditor.getCaretModel().getCaretCount() != 1) return; + + final VirtualFile virtualFile = sourceEditor.getVirtualFile(); + if (virtualFile == null) return; + if (virtualFile.getFileType() != JavaFileType.INSTANCE) { + // Only update ranges when viewing the source code. + LOG.trace("updateBytecodeSelection: file displayed in editor is not Java source, but " + virtualFile.getFileType().getName()); + return; + } + + final PsiElement selectedPsiElement = getPsiElement(project, sourceEditor); + if (selectedPsiElement == null) { + LOG.trace("Tried to update displayed bytecode but the selectedPsiElement is null"); + return; + } + final PsiClass containingClass = BytecodeViewerManager.getContainingClass(selectedPsiElement); + if (containingClass == null) { + LOG.trace("Tried to update displayed bytecode but the selectedPsiElement (" + selectedPsiElement + ") has no containing class"); + showWarning(BytecodeViewerBundle.message("bytecode.not.found.title"), + BytecodeViewerBundle.message("bytecode.class.in.selection.message")); + return; + } + if (!Objects.equals(containingClass.getQualifiedName(), currentlyDisplayedClassFQN)) { + // This is required to correctly handle different classes being present in a single Java file + LOG.trace("Scheduled loading bytecode because the cursor is now located inside class " + + containingClass.getQualifiedName() + + ", which is different from previously displayed class " + + currentlyDisplayedClassFQN); + queueLoadBytecodeTask(null); + return; + } + + currentlyDisplayedClassFQN = containingClass.getQualifiedName(); + queueUpdateBytecodeStatusTask(); + + final int sourceStartOffset = sourceEditor.getCaretModel().getCurrentCaret().getSelectionStart(); + final int sourceEndOffset = sourceEditor.getCaretModel().getCurrentCaret().getSelectionEnd(); + final Document sourceDocument = sourceEditor.getDocument(); + + final int sourceStartLine = sourceDocument.getLineNumber(sourceStartOffset); + int sourceEndLine = sourceDocument.getLineNumber(sourceEndOffset); + if (sourceEndLine > sourceStartLine && sourceEndOffset > 0 && sourceDocument.getCharsSequence().charAt(sourceEndOffset - 1) == '\n') { + sourceEndLine--; + } + + final String bytecodeWithDebugInfo = bytecodeDocument.getUserData(BYTECODE_WITH_DEBUG_INFO); + if (bytecodeWithDebugInfo == null) { + LOG.warn("Bytecode with debug information is null. Ensure the bytecode has been generated correctly."); + return; + } + + final var linesRange = mapLines(bytecodeWithDebugInfo, sourceStartLine, sourceEndLine, true); + + if (linesRange.equals(new IntRange(0, 0)) || linesRange.getFirst() < 0 || linesRange.getLast() < 0) { + bytecodeEditor.getSelectionModel().removeSelection(); + return; + } + + final int endSelectionLineIndex = Math.min(linesRange.getLast() + 1, bytecodeDocument.getLineCount()); + + final int startOffset = bytecodeDocument.getLineStartOffset(linesRange.getFirst()); + final int endOffset = Math.min(bytecodeDocument.getLineEndOffset(endSelectionLineIndex), bytecodeDocument.getTextLength()); + + if (bytecodeDocument.getTextLength() <= startOffset || bytecodeDocument.getTextLength() <= endOffset) { + return; + } + + bytecodeEditor.getCaretModel().moveToOffset(endOffset); + bytecodeEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + + bytecodeEditor.getCaretModel().moveToOffset(startOffset); + bytecodeEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + + bytecodeEditor.getSelectionModel().setSelection(startOffset, endOffset); + + LOG.trace("updated bytecode selection to lines: (" + linesRange.getFirst() + 1 + ", " + endSelectionLineIndex + 1 + ")"); + } + + private void queueUpdateBytecodeStatusTask() { + if (currentlyFocusedSourceFile == null) return; + + Consumer<@NotNull Boolean> onUpToDateCheckDone = (Boolean isUpToDate) -> { + ApplicationManager.getApplication().invokeLater(() -> { + if (!isUpToDate) { + setErrorText(BytecodeViewerBundle.message("class.file.may.be.out.of.date")); + } + else { + setErrorText(null); + } + }); + }; + + // If a new task was scheduled, we want to cancel the previous one. + if (existingUpdateBytecodeStatusTask != null) { + if (existingUpdateBytecodeStatusTask.isRunning()) { + existingUpdateBytecodeStatusTask.cancel(); + } + existingUpdateBytecodeStatusTask = null; + } + existingUpdateBytecodeStatusTask = new UpdateBytecodeStatusTask(project, currentlyFocusedSourceFile, onUpToDateCheckDone); + existingUpdateBytecodeStatusTask.queue(); + } + + /// Update the contents of the whole editor in the tool window, including reading bytecode again from the currently opened file. + @RequiresEdt + private void queueLoadBytecodeTask(@RequiresWriteLock @RequiresEdt @Nullable Runnable onAfterBytecodeLoaded) { + final Consumer onClassUpdated = (PsiClass newClass) -> { + ApplicationManager.getApplication().invokeLater(() -> { + currentlyDisplayedClassFQN = newClass.getQualifiedName(); + setClassName(newClass.getName()); + }); + }; + + final Consumer onNewBytecodeLoaded = (Bytecode bytecode) -> { + ApplicationManager.getApplication().invokeLater(() -> { + WriteAction.run(() -> { + setBytecodeText(bytecode.withDebugInfo(), bytecode.withoutDebugInfo()); + if (onAfterBytecodeLoaded != null) { + onAfterBytecodeLoaded.run(); + } + }); + }); + }; + + final BiConsumer<@NotNull String, @NotNull String> onWarningShown = (String title, String message) -> { + ApplicationManager.getApplication().invokeLater(() -> { + showWarning(title, message); + }); + }; + + // If a new task was scheduled to update bytecode, we want to cancel the previous one. + if (existingLoadBytecodeTask != null) { + if (existingLoadBytecodeTask.isRunning()) { + LOG.trace("queueLoadBytecodeTask(): canceling existing LoadBytecodeTask " + existingLoadBytecodeTask.hashCode()); + existingLoadBytecodeTask.cancel(); + } + existingLoadBytecodeTask = null; + } + existingLoadBytecodeTask = new LoadBytecodeTask(project, onNewBytecodeLoaded, onClassUpdated, onWarningShown); + existingLoadBytecodeTask.queue(); + } + + private void showWarning(@NotNull String title, @NotNull String message) { + ToolWindowManager.getInstance(project).notifyByBalloon(TOOL_WINDOW_ID, MessageType.WARNING, title + "\n" + message); + } + + @RequiresEdt + @RequiresWriteLock + private void setBytecodeText(@Nullable String bytecodeWithDebugInfo, @NotNull String bytecodeWithoutDebugInfo) { + bytecodeEditor.getDocument().putUserData(BYTECODE_WITH_DEBUG_INFO, bytecodeWithDebugInfo); + bytecodeEditor.getDocument().setText(StringUtil.convertLineSeparators(bytecodeWithoutDebugInfo)); + } + + @RequiresEdt + private void setErrorText(@Nls @Nullable String errorText) { + errorLabel.setText(errorText); + errorLabel.setVisible(errorText != null); + } + + @RequiresEdt + private void setClassName(@NlsSafe String className) { + classNameLabel.setText(BytecodeViewerBundle.message("bytecode.for.class", className)); + classNameLabel.setVisible(className != null); + } + + @Override + public void dispose() { + EditorFactory.getInstance().releaseEditor(bytecodeEditor); + } +} + + +final class LoadBytecodeTask extends Task.Backgroundable { + private static final Logger LOG = Logger.getInstance(LoadBytecodeTask.class); + + private final @NotNull Consumer<@NotNull Bytecode> onBytecodeUpdated; + private final @NotNull Consumer<@NotNull PsiClass> onClassNameUpdated; + private final @NotNull BiConsumer<@NotNull String, @NotNull String> onWarningShown; + + private @Nullable ProgressIndicator myProgressIndicator; + private @Nullable Bytecode myBytecode; + + LoadBytecodeTask(@NotNull Project project, + @RequiresEdt @NotNull Consumer<@NotNull Bytecode> onBytecodeUpdated, + @RequiresBackgroundThread @NotNull Consumer<@NotNull PsiClass> onClassUpdated, + @NotNull BiConsumer<@NotNull String, @NotNull String> onWarningShown) { + super(project, BytecodeViewerBundle.message("loading.bytecode"), true); + this.onBytecodeUpdated = onBytecodeUpdated; + this.onClassNameUpdated = onClassUpdated; + this.onWarningShown = onWarningShown; + } + + public void cancel() { + LOG.trace("canceled"); + if (myProgressIndicator != null) { + myProgressIndicator.cancel(); + } + } + + public boolean isRunning() { + return myProgressIndicator != null && myProgressIndicator.isRunning(); + } + + @RequiresBackgroundThread + @Override + public void run(@NotNull ProgressIndicator indicator) { + if (myProject == null) return; + myProgressIndicator = indicator; + myBytecode = ReadAction.computeCancellable(() -> { + final Editor selectedEditor = FileEditorManager.getInstance(myProject).getSelectedTextEditor(); + if (selectedEditor == null) { + LOG.warn("Tried to show Java bytecode but selectedEditor is null"); + return null; + } + + final PsiFile psiFileInEditor = PsiUtilBase.getPsiFileInEditor(selectedEditor, myProject); + if (psiFileInEditor == null) { + LOG.warn("Tried to update displayed bytecode but psiFileInEditor is null"); + return null; + } + if (!isValidFileType(psiFileInEditor.getFileType())) { + LOG.warn("Tried to update displayed bytecode for invalid file type " + psiFileInEditor.getFileType().getName()); + return null; + } + + final PsiElement selectedPsiElement = getPsiElement(myProject, selectedEditor); + if (selectedPsiElement == null) { + LOG.warn("Tried to update displayed bytecode but the selectedPsiElement is null"); + return null; + } + + final PsiClass containingClass = BytecodeViewerManager.getContainingClass(selectedPsiElement); + if (containingClass == null) { + LOG.trace("Tried to update displayed bytecode but the selectedPsiElement (" + selectedPsiElement + ") has no containing class"); + onWarningShown.accept(BytecodeViewerBundle.message("bytecode.not.found.title"), + BytecodeViewerBundle.message("bytecode.class.in.selection.message")); + return null; + } + + onClassNameUpdated.accept(containingClass); + + //CancellationUtil.sleepCancellable(1000); // Uncomment if you want to make sure we continue to not freeze the IDE + + final Pair bytecodeVariants = getByteCodeVariants(selectedPsiElement); + if (bytecodeVariants == null) { + LOG.warn("Tried to update displayed bytecode but bytecode is null. selectedPsiElement: " + selectedPsiElement); + return null; + } + + return new Bytecode(bytecodeVariants.getFirst(), bytecodeVariants.getSecond()); + }); + } + + @RequiresEdt + @Override + public void onSuccess() { + LOG.trace("onSuccess(): bytecode != null? " + (myBytecode != null)); + if (myBytecode != null) { + onBytecodeUpdated.accept(myBytecode); + } + } + + @RequiresEdt + @Override + public void onCancel() { + LOG.warn("task was canceled, task title: " + getTitle() + "task text: " + myProgressIndicator.getText()); + } +} + +final class UpdateBytecodeStatusTask extends Task.Backgroundable { + private static final Logger LOG = Logger.getInstance(UpdateBytecodeStatusTask.class); + + private final @NotNull VirtualFile myVirtualFile; + private final @NotNull Consumer onUpToDateCheckDone; + + private @Nullable ProgressIndicator myProgressIndicator; + + UpdateBytecodeStatusTask(@NotNull Project project, @NotNull VirtualFile virtualFile, @NotNull Consumer onUpToDateCheckDone) { + super(project, BytecodeViewerBundle.message("checking.if.bytecode.is.up.to.date"), true); + this.myVirtualFile = virtualFile; + this.onUpToDateCheckDone = onUpToDateCheckDone; + } + + public void cancel() { + if (myProgressIndicator != null) { + myProgressIndicator.cancel(); + } + } + + public boolean isRunning() { + return myProgressIndicator != null && myProgressIndicator.isRunning(); + } + + @RequiresBackgroundThread + @Override + public void run(@NotNull ProgressIndicator indicator) { + if (myProject == null) return; + LOG.trace("run() started!"); + if (!myVirtualFile.isValid()) { + LOG.trace("run() canceled because file" + myVirtualFile + " is invalid"); + return; + } + + myProgressIndicator = indicator; + + CancellationUtil.sleepCancellable(1000); // Poor man's event debouncing + + final boolean isInContent = ReadAction.computeCancellable(() -> { + return ProjectRootManager.getInstance(myProject).getFileIndex().isInContent(myVirtualFile); + }); + if (!isInContent) { + LOG.trace("run() returns early because file " + myVirtualFile + " is not in the project's content"); + return; + } + + final boolean isUpToDate = !isMarkedForCompilation(myProject, myVirtualFile); + LOG.trace("up-to-date check for file " + myVirtualFile + " finished, is up to date: " + isUpToDate); + onUpToDateCheckDone.accept(isUpToDate); + } +} + +record Bytecode(@NotNull String withDebugInfo, @NotNull String withoutDebugInfo) { +} diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/JavaByteCodeViewerBundle.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerBundle.java similarity index 80% rename from plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/JavaByteCodeViewerBundle.java rename to plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerBundle.java index b96842eb74a4..89364788ec5c 100644 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/JavaByteCodeViewerBundle.java +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerBundle.java @@ -7,11 +7,11 @@ import org.jetbrains.annotations.PropertyKey; import java.util.function.Supplier; -public final class JavaByteCodeViewerBundle { - private static final @NonNls String BUNDLE = "messages.JavaByteCodeViewerBundle"; - private static final DynamicBundle INSTANCE = new DynamicBundle(JavaByteCodeViewerBundle.class, BUNDLE); +public final class BytecodeViewerBundle { + private static final @NonNls String BUNDLE = "messages.BytecodeViewerBundle"; + private static final DynamicBundle INSTANCE = new DynamicBundle(BytecodeViewerBundle.class, BUNDLE); - private JavaByteCodeViewerBundle() { + private BytecodeViewerBundle() { } public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerManager.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerManager.java new file mode 100644 index 000000000000..8267a85e0bc2 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerManager.java @@ -0,0 +1,155 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer; + +import com.intellij.ide.highlighter.JavaClassFileType; +import com.intellij.ide.util.JavaAnonymousClassesHelper; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.fileTypes.FileTypeRegistry; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.CompilerModuleExtension; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.util.ClassUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Queue; + +/** + * @author anna + */ +@Service(Service.Level.PROJECT) +public final class BytecodeViewerManager { + private static final ExtensionPointName CLASS_SEARCHER_EP = ExtensionPointName.create("ByteCodeViewer.classSearcher"); + + private static final Logger LOG = Logger.getInstance(BytecodeViewerManager.class); + + public static BytecodeViewerManager getInstance(Project project) { + return project.getService(BytecodeViewerManager.class); + } + + public static byte[] loadClassFileBytes(PsiClass aClass) throws IOException { + String jvmClassName = getJVMClassName(aClass); + if (jvmClassName != null) { + PsiClass fileClass = aClass; + while (PsiUtil.isLocalOrAnonymousClass(fileClass)) { + PsiClass containingClass = PsiTreeUtil.getParentOfType(fileClass, PsiClass.class); + if (containingClass != null) { + fileClass = containingClass; + } + } + VirtualFile file = fileClass.getOriginalElement().getContainingFile().getVirtualFile(); + if (file != null) { + ProjectFileIndex index = ProjectFileIndex.getInstance(aClass.getProject()); + if (FileTypeRegistry.getInstance().isFileOfType(file, JavaClassFileType.INSTANCE)) { + // compiled class; looking for the right .class file (inner class 'A.B' is "contained" in 'A.class', but we need 'A$B.class') + String classFileName = StringUtil.getShortName(jvmClassName) + ".class"; + if (index.isInLibraryClasses(file)) { + VirtualFile classFile = file.getParent().findChild(classFileName); + if (classFile != null) { + LOG.trace("loadClassFileBytes(" + aClass + "): found class file " + classFile.getPath()); + return classFile.contentsToByteArray(false); + } + } + else { + File classFile = new File(file.getParent().getPath(), classFileName); + if (classFile.isFile()) { + LOG.trace("loadClassFileBytes(" + aClass + "): found class file " + classFile.getPath()); + return FileUtil.loadFileBytes(classFile); + } + } + } + else { + // source code; looking for a .class file in compiler output + Module module = index.getModuleForFile(file); + if (module != null) { + CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module); + if (extension != null) { + boolean inTests = index.isInTestSourceContent(file); + VirtualFile classRoot = inTests ? extension.getCompilerOutputPathForTests() : extension.getCompilerOutputPath(); + if (classRoot != null) { + String relativePath = jvmClassName.replace('.', '/') + ".class"; + File classFile = new File(classRoot.getPath(), relativePath); + if (classFile.exists()) { + LOG.trace("loadClassFileBytes(" + + aClass + + "): found class file " + + classFile.getPath() + + " compiled from source file " + + file.getPath()); + return FileUtil.loadFileBytes(classFile); + } + } + } + } + } + } + } + + return null; + } + + private static String getJVMClassName(PsiClass aClass) { + if (!(aClass instanceof PsiAnonymousClass)) { + return ClassUtil.getJVMClassName(aClass); + } + + PsiClass containingClass = PsiTreeUtil.getParentOfType(aClass, PsiClass.class); + if (containingClass != null) { + return getJVMClassName(containingClass) + JavaAnonymousClassesHelper.getName((PsiAnonymousClass)aClass); + } + + return null; + } + + @Nullable + public static PsiClass getContainingClass(@NotNull PsiElement psiElement) { + for (ClassSearcher searcher : CLASS_SEARCHER_EP.getExtensionList()) { + PsiClass aClass = searcher.findClass(psiElement); + if (aClass != null) { + return aClass; + } + } + + PsiClass containingClass = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class, false); + while (containingClass instanceof PsiTypeParameter) { + containingClass = PsiTreeUtil.getParentOfType(containingClass, PsiClass.class); + } + + if (containingClass == null) { + PsiFile containingFile = psiElement.getContainingFile(); + if (containingFile instanceof PsiClassOwner) { + TextRange textRange = psiElement.getTextRange(); + PsiClass result = null; + Queue queue = new ArrayDeque<>(Arrays.asList(((PsiClassOwner)containingFile).getClasses())); + while (!queue.isEmpty()) { + PsiClass c = queue.remove(); + PsiElement navigationElement = c.getNavigationElement(); + TextRange classRange = navigationElement != null ? navigationElement.getTextRange() : null; + if (classRange != null && classRange.contains(textRange)) { + result = c; + queue.clear(); + queue.addAll(Arrays.asList(c.getInnerClasses())); + } + } + return result; + } + return null; + } + + return containingClass; + } +} diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerUtil.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerUtil.kt new file mode 100644 index 000000000000..28231e8b7111 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerUtil.kt @@ -0,0 +1,80 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer + +import com.intellij.ide.highlighter.JavaClassFileType +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.openapi.compiler.CompilerManager +import com.intellij.openapi.diagnostic.fileLogger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil +import com.intellij.psi.util.PsiUtilBase +import org.jetbrains.annotations.Contract +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.util.Textifier +import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor +import java.io.IOException +import java.io.PrintWriter +import java.io.StringWriter + +private val LOG = fileLogger() + +@Contract("null -> false") +internal fun isValidFileType(fileType: FileType?): Boolean { + return fileType === JavaClassFileType.INSTANCE || fileType === JavaFileType.INSTANCE +} + +internal fun isMarkedForCompilation(project: Project, virtualFile: VirtualFile?): Boolean { + val compilerManager = CompilerManager.getInstance(project) + val compileScope = compilerManager.createFilesCompileScope(arrayOf(virtualFile)) + return !compilerManager.isUpToDate(compileScope) +} + +internal fun getPsiElement(project: Project, editor: Editor): PsiElement? { + fun findElementInFile(psiFile: PsiFile?, editor: Editor): PsiElement? { + return psiFile?.findElementAt(editor.getCaretModel().offset) + } + + val file = PsiUtilBase.getPsiFileInEditor(editor, project) + val injectedEditor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file) + var psiElement = findElementInFile(PsiUtilBase.getPsiFileInEditor(injectedEditor, project), injectedEditor) + + if (file != null && psiElement == null) { + psiElement = findElementInFile(file, editor) + } + + return psiElement +} + +/** + * Retrieves the bytecode representation of the class containing the provided PSI element. + * + * @return a Pair where the first value is a bytecode with debug info included, and the second value is without any debug info + */ +internal fun getByteCodeVariants(psiElement: PsiElement): Pair? { + val containingClass = BytecodeViewerManager.getContainingClass(psiElement) + if (containingClass != null) { + try { + val bytes = BytecodeViewerManager.loadClassFileBytes(containingClass) + if (bytes != null) { + val withDebugInfoWriter = StringWriter() + PrintWriter(withDebugInfoWriter).use { printWriter -> + val textifier = Textifier() + val classVisitor: ClassVisitor = TraceClassVisitor(null, textifier, printWriter) + ClassReader(bytes).accept(classVisitor, ClassReader.SKIP_FRAMES) + } + val noDebugInfo = removeDebugInfo(withDebugInfoWriter.toString()) + return Pair(withDebugInfoWriter.toString(), noDebugInfo) + } + } + catch (e: IOException) { + LOG.error(e) + } + } + return null +} diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowByteCodeAction.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowByteCodeAction.java deleted file mode 100644 index 18611454eb1b..000000000000 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowByteCodeAction.java +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.byteCodeViewer; - -import com.intellij.codeInsight.lookup.LookupManager; -import com.intellij.icons.AllIcons; -import com.intellij.openapi.actionSystem.*; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.compiler.CompilerManager; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.colors.EditorColors; -import com.intellij.openapi.editor.colors.EditorColorsManager; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ProjectRootManager; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.ui.popup.JBPopup; -import com.intellij.openapi.ui.popup.JBPopupFactory; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; -import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; -import com.intellij.psi.util.PsiUtilBase; -import com.intellij.psi.util.PsiUtilCore; -import com.intellij.ui.awt.RelativePoint; -import com.intellij.util.Processor; -import com.intellij.util.ui.JBEmptyBorder; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; - -final class ShowByteCodeAction extends AnAction { - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } - - @Override - public void update(@NotNull AnActionEvent e) { - e.getPresentation().setEnabled(false); - e.getPresentation().setIcon(AllIcons.Actions.Preview); - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project != null) { - final PsiElement psiElement = getPsiElement(e.getDataContext(), project, e.getData(CommonDataKeys.EDITOR)); - if (psiElement != null) { - if (psiElement.getContainingFile() instanceof PsiClassOwner) { - e.getPresentation().setEnabled(true); - } - } - } - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - final DataContext dataContext = e.getDataContext(); - final Project project = e.getProject(); - if (project == null) return; - final Editor editor = e.getData(CommonDataKeys.EDITOR); - - final PsiElement psiElement = getPsiElement(dataContext, project, editor); - if (psiElement == null) return; - - // Some PSI elements could be multiline. Try to be precise about the line we were invoked at. - int lineNumber = editor != null ? editor.getCaretModel().getLogicalPosition().line : -1; - - if (ByteCodeViewerManager.getContainingClass(psiElement) == null) { - Messages.showWarningDialog(project, JavaByteCodeViewerBundle.message("bytecode.class.in.selection.message"), - JavaByteCodeViewerBundle.message("bytecode.not.found.message")); - return; - } - - final String psiElementTitle = ByteCodeViewerManager.getInstance(project).getTitle(psiElement); - - final VirtualFile virtualFile = PsiUtilCore.getVirtualFile(psiElement); - if (virtualFile == null) return; - - final RelativePoint bestPopupLocation = JBPopupFactory.getInstance().guessBestPopupLocation(dataContext); - - final SmartPsiElementPointer element = SmartPointerManager.getInstance(project).createSmartPsiElementPointer(psiElement); - ProgressManager.getInstance().run(new Task.Backgroundable(project, JavaByteCodeViewerBundle.message("looking.for.bytecode.progress")) { - private String myByteCode; - private @Nls String myErrorTitle; - - @Override - public void run(@NotNull ProgressIndicator indicator) { - if (ReadAction.compute(() -> ProjectRootManager.getInstance(project).getFileIndex().isInContent(virtualFile)) && - isMarkedForCompilation(project, virtualFile)) { - myErrorTitle = JavaByteCodeViewerBundle.message("class.file.may.be.out.of.date"); - } - myByteCode = ReadAction.compute(() -> { - PsiElement targetElement = element.getElement(); - return targetElement != null ? ByteCodeViewerManager.getByteCode(targetElement) : null; - }); - } - - @Override - public void onSuccess() { - if (project.isDisposed()) return; - - final PsiElement targetElement = element.getElement(); - if (targetElement == null) return; - - final ByteCodeViewerManager codeViewerManager = ByteCodeViewerManager.getInstance(project); - if (codeViewerManager.hasActiveDockedDocWindow()) { - codeViewerManager.doUpdateComponent(targetElement, lineNumber, myByteCode); - } - else { - if (myByteCode == null) { - Messages.showErrorDialog(project, JavaByteCodeViewerBundle.message("bytecode.parser.failure.message", psiElementTitle), - JavaByteCodeViewerBundle.message("bytecode.not.found.title")); - return; - } - - final ByteCodeViewerComponent component = new ByteCodeViewerComponent(project); - component.setText(myByteCode, targetElement, lineNumber); - Processor pinCallback = popup -> { - codeViewerManager.recreateToolWindow(targetElement, targetElement); - popup.cancel(); - return false; - }; - - if (myErrorTitle != null) { - JLabel errorLabel = new JLabel(myErrorTitle); - Color color = EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.NOTIFICATION_BACKGROUND); - if (color != null) { - errorLabel.setBorder(new JBEmptyBorder(2)); - errorLabel.setBackground(color); - errorLabel.setOpaque(true); - } - component.add(errorLabel, BorderLayout.NORTH); - } - - final JBPopup popup = JBPopupFactory.getInstance().createComponentPopupBuilder(component, component.getEditorComponent()) - .setProject(project) - .setDimensionServiceKey(project, ShowByteCodeAction.class.getName(), false) - .setResizable(true) - .setMovable(true) - .setRequestFocus(LookupManager.getActiveLookup(editor) == null) - .setTitle(JavaByteCodeViewerBundle.message("popup.title.element.bytecode", psiElementTitle)) - .setCouldPin(pinCallback) - .createPopup(); - Disposer.register(popup, component); - - if (editor != null && editor.getComponent().isShowing()) { - popup.showInBestPositionFor(editor); - } else { - popup.show(bestPopupLocation); - } - } - } - }); - } - - private static boolean isMarkedForCompilation(Project project, VirtualFile virtualFile) { - final CompilerManager compilerManager = CompilerManager.getInstance(project); - return !compilerManager.isUpToDate(compilerManager.createFilesCompileScope(new VirtualFile[]{virtualFile})); - } - - @Nullable - private static PsiElement getPsiElement(DataContext dataContext, Project project, @Nullable Editor editor) { - PsiElement psiElement; - if (editor == null) { - psiElement = dataContext.getData(CommonDataKeys.PSI_ELEMENT); - } - else { - final PsiFile file = PsiUtilBase.getPsiFileInEditor(editor, project); - final Editor injectedEditor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file); - psiElement = findElementInFile(PsiUtilBase.getPsiFileInEditor(injectedEditor, project), injectedEditor); - - if (file != null && psiElement == null) { - psiElement = findElementInFile(file, editor); - } - } - - return psiElement; - } - - private static PsiElement findElementInFile(@Nullable PsiFile psiFile, @NotNull Editor editor) { - return psiFile != null ? psiFile.findElementAt(editor.getCaretModel().getOffset()) : null; - } -} \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.java b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.java new file mode 100644 index 000000000000..9a6c02644139 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.java @@ -0,0 +1,74 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.RegisterToolWindowTaskBuilder; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowAnchor; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.psi.PsiFile; +import com.intellij.ui.content.ContentFactory; +import com.intellij.ui.content.ContentManager; +import kotlin.Unit; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.byteCodeViewer.BytecodeViewerUtilKt.isValidFileType; + +final class ShowBytecodeAction extends AnAction { + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent event) { + FileType fileType = null; + final PsiFile file = event.getData(CommonDataKeys.PSI_FILE); + if (file != null) { + fileType = file.getFileType(); + } + + event.getPresentation().setEnabled(event.getProject() != null && isValidFileType(fileType)); + event.getPresentation().setIcon(AllIcons.FileTypes.JavaClass); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + final Project project = event.getProject(); + if (project == null) return; + + final VirtualFile virtualFile = event.getData(CommonDataKeys.VIRTUAL_FILE); + final Editor editor = event.getData(CommonDataKeys.EDITOR); + if (virtualFile == null || editor == null) return; + + final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + ToolWindow toolWindow = toolWindowManager.getToolWindow(BytecodeToolWindowPanel.TOOL_WINDOW_ID); + if (toolWindow == null) { + toolWindow = toolWindowManager.registerToolWindow( + BytecodeToolWindowPanel.TOOL_WINDOW_ID, + (RegisterToolWindowTaskBuilder builder) -> { + builder.icon = AllIcons.FileTypes.JavaClass; + builder.anchor = ToolWindowAnchor.RIGHT; + builder.hideOnEmptyContent = false; + builder.canCloseContent = false; + return Unit.INSTANCE; + } + ); + final ContentFactory contentFactory = ContentFactory.getInstance(); + final ContentManager contentManager = toolWindow.getContentManager(); + final BytecodeToolWindowPanel panel = new BytecodeToolWindowPanel(project, toolWindow, editor); + contentManager.addContent(contentFactory.createContent(panel, "", false)); + } + + toolWindow.activate(null); + } +} diff --git a/plugins/ByteCodeViewer/test/com/intellij/byteCodeViewer/BytecodeLineMappingTest.kt b/plugins/ByteCodeViewer/test/com/intellij/byteCodeViewer/BytecodeLineMappingTest.kt new file mode 100644 index 000000000000..dc293bda1a3b --- /dev/null +++ b/plugins/ByteCodeViewer/test/com/intellij/byteCodeViewer/BytecodeLineMappingTest.kt @@ -0,0 +1,863 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.byteCodeViewer + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class BytecodeLineMappingTest : BasePlatformTestCase() { + private fun bootstrapAndGetSelection(text: String): Pair { + myFixture.configureByText(getTestName(false) + ".java", text) + val sourceDocument = myFixture.editor.document + + val sourceStartOffset = myFixture.editor.selectionModel.selectionStart + val sourceEndOffset = myFixture.editor.selectionModel.selectionEnd + + val startLine = sourceDocument.getLineNumber(sourceStartOffset) + val endLine = sourceDocument.getLineNumber(sourceEndOffset) + return startLine to endLine + } + + private fun doTest( + source: String, + bytecodeWithDebugInfo: String, + bytecodeWithoutDebugInfo: String, + expectedBytecodeSelection: String, + stripDebugInfo: Boolean = false, + ) { + val (startLine, endLine) = bootstrapAndGetSelection(source) + + var linesRange = mapLines( + bytecodeWithDebugInfo = bytecodeWithDebugInfo, + sourceStartLine = startLine, + sourceEndLine = endLine, + stripDebugInfo = stripDebugInfo, + ) + val bytecodeSelectionStart = linesRange.first + var bytecodeSelectionEnd = linesRange.last + bytecodeSelectionEnd++ // because most substring() functions use exclusive indexing for "end" + bytecodeSelectionEnd++ // because string operations are 0-indexed but the editor is 1-indexed + + assertEquals(expectedBytecodeSelection, (if (stripDebugInfo) bytecodeWithoutDebugInfo else bytecodeWithDebugInfo).lines().subList(bytecodeSelectionStart, bytecodeSelectionEnd).joinToString("\n")) + } + + fun `test removeDebugInfo`() { + val actualStrippedBytecode = removeDebugInfo(sampleBytecode_1) + val expectedStrippedBytecode = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple1/Main { + | + | // compiled from: Main.java + | + | // access flags 0x1 + | public ()V + | L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x9 + | public static main([Ljava/lang/String;)V + | L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | RETURN + | L2 + | MAXSTACK = 2 + | MAXLOCALS = 1 + |} + """.trimMargin("|") + + assertEquals(expectedStrippedBytecode, actualStrippedBytecode) + } + + fun `test simple 1 method body start`() { + + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world");I + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | LINENUMBER 5 L0 + """.trimMargin("|")) + } + + fun `test simple 1 method body - line selected`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | LINENUMBER 5 L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | LINENUMBER 6 L1 + """.trimMargin("|")) + } + + fun `test simple 1 method body - from brace to brace`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | LINENUMBER 5 L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | LINENUMBER 6 L1 + | RETURN + | L2 + | LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 + """.trimMargin("|")) + } + + fun `test simple 1 method body end`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | LINENUMBER 6 L1 + | RETURN + | L2 + | LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 + """.trimMargin("|")) + } + + fun `test simple 1 class`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | LINENUMBER 3 L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | LOCALVARIABLE this Lsimple1/Main; L0 L1 0 + """.trimMargin("|")) + } + + fun `test simple 2 - single field selected`() { + val source = """ + package simple2; + + public class Main { + public static String name = "Charlie"; + public static Object obj = new Object(); + + public static void main(String[] args) { + System.out.println("hello world"); + int argCount = args.length; + System.out.println("there are " + argCount + " args"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_2, sampleBytecode_2_noDebugInfo, """ + | LINENUMBER 4 L0 + | LDC "Charlie" + | PUTSTATIC simple2/Main.name : Ljava/lang/String; + | L1 + | LINENUMBER 5 L1 + """.trimMargin("|")) + } + + fun `test simple 2 - two fields selected`() { + val source = """ + package simple2; + + public class Main { + public static String name = "Charlie"; + public static Object obj = new Object(); + + public static void main(String[] args) { + System.out.println("hello world"); + int argCount = args.length; + System.out.println("there are " + argCount + " args"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_2, sampleBytecode_2_noDebugInfo, """ + | LINENUMBER 4 L0 + | LDC "Charlie" + | PUTSTATIC simple2/Main.name : Ljava/lang/String; + | L1 + | LINENUMBER 5 L1 + | NEW java/lang/Object + | DUP + | INVOKESPECIAL java/lang/Object. ()V + | PUTSTATIC simple2/Main.obj : Ljava/lang/Object; + | RETURN + | MAXSTACK = 2 + """.trimMargin("|")) + } + + fun `test simple 2 method body - two lines selected`() { + val source = """ + package simple2; + + public class Main { + public static String name = "Charlie"; + public static Object obj = new Object(); + + public static void main(String[] args) { + System.out.println("hello world"); + int argCount = args.length; + System.out.println("there are " + argCount + " args"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_2, sampleBytecode_2_noDebugInfo, """ + | LINENUMBER 8 L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | LINENUMBER 9 L1 + | ALOAD 0 + | ARRAYLENGTH + | ISTORE 1 + | L2 + | LINENUMBER 10 L2 + """.trimMargin("|")) + } + + fun `test (strip debug info) simple 1 method body - single line selected`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + """.trimMargin("|"), stripDebugInfo = true) + } + + fun `test (strip debug info) simple 1 method body - from brace to brace`() { + val source = """ + package simple1; + + public class Main { + public static void main(String[] args) { + System.out.println("hello world"); + } + } + """.trimIndent() + + doTest(source, sampleBytecode_1, sampleBytecode_1_noDebugInfo, """ + | L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | RETURN + | L2 + """.trimMargin("|"), stripDebugInfo = true) + } + + fun `test (strip debug info) simple 4 - works fine in presence of jumps (1)`() { + val source = """ + package simple4; + + public class Main { + String method1(boolean value) { + if (value == true) { + return "baz"; + } + return "baz"; + } + + String method2(boolean value) { + if (value == Boolean.TRUE) { + return "bar"; + } + return "baz"; + } + + String method3(boolean value) { + if (value == Boolean.FALSE) { + return "bar"; + } + return "baz"; + } + + String method(boolean value) { + if (Boolean.TRUE.equals(returnsBool(value))) { + return "foo"; + } + return "baz"; + } + + public Boolean returnsBool(boolean value) { + return Math.random() > 0.5; + } + } + """.trimIndent() + + doTest(source, sampleBytecode_3, sampleBytecode_3_noDebugInfo, """ + | L0 + | ILOAD 1 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + | IF_ICMPNE L1 + | L2 + | LDC "bar" + | ARETURN + | L1 + """.trimMargin("|"), stripDebugInfo = true) + } + + fun `test (strip debug info) simple 4 - works fine in presence of jumps (2)`() { + val source = """ + package simple4; + + public class Main { + String method1(boolean value) { + if (value == true) { + return "baz"; + } + return "baz"; + } + + String method2(boolean value) { + if (value == Boolean.TRUE) { + return "bar"; + } + return "baz"; + } + + String method3(boolean value) { + if (value == Boolean.FALSE) { + return "bar"; + } + return "baz"; + } + + String method(boolean value) { + if (Boolean.TRUE.equals(returnsBool(value))) { + return "foo"; + } + return "baz"; + } + + public Boolean returnsBool(boolean value) { + return Math.random() > 0.5; + } + } + """.trimIndent() + + doTest(source, sampleBytecode_3, sampleBytecode_3_noDebugInfo, """ + | L0 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | ALOAD 0 + | ILOAD 1 + | INVOKEVIRTUAL simple4/Main.returnsBool (Z)Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.equals (Ljava/lang/Object;)Z + | IFEQ L1 + | L2 + | LDC "foo" + | ARETURN + | L1 + """.trimMargin("|"), stripDebugInfo = true) + } + + /** + * Bytecode was compiled using javac 23. + * + * Human-readable sample bytecode was read with ASM ClassReader, with flags applied: SKIP_FRAMES + * + * NOT-TRUE: Human-readable sample bytecode without debug info was read with ASM ClassReader, with flags applied: SKIP_FRAMES, SKIP_DEBUG + * Actually, it was created with [BytecodeLineMapping.removeDebugInfo]. + */ + companion object Fixtures { + + private val sampleBytecode_1 = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple1/Main { + | + | // compiled from: Main.java + | + | // access flags 0x1 + | public ()V + | L0 + | LINENUMBER 3 L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | LOCALVARIABLE this Lsimple1/Main; L0 L1 0 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x9 + | public static main([Ljava/lang/String;)V + | L0 + | LINENUMBER 5 L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | LINENUMBER 6 L1 + | RETURN + | L2 + | LOCALVARIABLE args [Ljava/lang/String; L0 L2 0 + | MAXSTACK = 2 + | MAXLOCALS = 1 + |} + """.trimMargin("|") + + private val sampleBytecode_1_noDebugInfo = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple1/Main { + | + | // compiled from: Main.java + | + | // access flags 0x1 + | public ()V + | L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x9 + | public static main([Ljava/lang/String;)V + | L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | RETURN + | L2 + | MAXSTACK = 2 + | MAXLOCALS = 1 + |} + """.trimMargin("|") + + private val sampleBytecode_2 = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple2/Main { + | + | // compiled from: Main.java + | // access flags 0x19 + | public final static INNERCLASS java/lang/invoke/MethodHandles${'$'}Lookup java/lang/invoke/MethodHandles Lookup + | + | // access flags 0x9 + | public static Ljava/lang/String; name + | + | // access flags 0x9 + | public static Ljava/lang/Object; obj + | + | // access flags 0x1 + | public ()V + | L0 + | LINENUMBER 3 L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | LOCALVARIABLE this Lsimple2/Main; L0 L1 0 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x9 + | public static main([Ljava/lang/String;)V + | L0 + | LINENUMBER 8 L0 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L1 + | LINENUMBER 9 L1 + | ALOAD 0 + | ARRAYLENGTH + | ISTORE 1 + | L2 + | LINENUMBER 10 L2 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | ILOAD 1 + | INVOKEDYNAMIC makeConcatWithConstants(I)Ljava/lang/String; [ + | // handle kind 0x6 : INVOKESTATIC + | java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles${'$'}Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; + | // arguments: + | "there are \u0001 args" + | ] + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | L3 + | LINENUMBER 11 L3 + | RETURN + | L4 + | LOCALVARIABLE args [Ljava/lang/String; L0 L4 0 + | LOCALVARIABLE argCount I L2 L4 1 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x8 + | static ()V + | L0 + | LINENUMBER 4 L0 + | LDC "Charlie" + | PUTSTATIC simple2/Main.name : Ljava/lang/String; + | L1 + | LINENUMBER 5 L1 + | NEW java/lang/Object + | DUP + | INVOKESPECIAL java/lang/Object. ()V + | PUTSTATIC simple2/Main.obj : Ljava/lang/Object; + | RETURN + | MAXSTACK = 2 + | MAXLOCALS = 0 + |} + """.trimMargin("|") + + private val sampleBytecode_2_noDebugInfo = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple2/Main { + | + | // access flags 0x19 + | public final static INNERCLASS java/lang/invoke/MethodHandles${'$'}Lookup java/lang/invoke/MethodHandles Lookup + | + | // access flags 0x9 + | public static Ljava/lang/String; name + | + | // access flags 0x9 + | public static Ljava/lang/Object; obj + | + | // access flags 0x1 + | public ()V + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x9 + | public static main([Ljava/lang/String;)V + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | LDC "hello world" + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | ALOAD 0 + | ARRAYLENGTH + | ISTORE 1 + | GETSTATIC java/lang/System.out : Ljava/io/PrintStream; + | ILOAD 1 + | INVOKEDYNAMIC makeConcatWithConstants(I)Ljava/lang/String; [ + | // handle kind 0x6 : INVOKESTATIC + | java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles${'$'}Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; + | // arguments: + | "there are \u0001 args" + | ] + | INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V + | RETURN + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x8 + | static ()V + | LDC "Charlie" + | PUTSTATIC simple2/Main.name : Ljava/lang/String; + | NEW java/lang/Object + | DUP + | INVOKESPECIAL java/lang/Object. ()V + | PUTSTATIC simple2/Main.obj : Ljava/lang/Object; + | RETURN + | MAXSTACK = 2 + | MAXLOCALS = 0 + |} + """.trimMargin("|") + + private val sampleBytecode_3 = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple4/Main { + | + | // compiled from: Main.java + | + | // access flags 0x1 + | public ()V + | L0 + | LINENUMBER 3 L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | LOCALVARIABLE this Lsimple4/Main; L0 L1 0 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x0 + | method1(Z)Ljava/lang/String; + | L0 + | LINENUMBER 5 L0 + | ILOAD 1 + | ICONST_1 + | IF_ICMPNE L1 + | L2 + | LINENUMBER 6 L2 + | LDC "baz" + | ARETURN + | L1 + | LINENUMBER 8 L1 + | LDC "baz" + | ARETURN + | L3 + | LOCALVARIABLE this Lsimple4/Main; L0 L3 0 + | LOCALVARIABLE value Z L0 L3 1 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method2(Z)Ljava/lang/String; + | L0 + | LINENUMBER 12 L0 + | ILOAD 1 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + | IF_ICMPNE L1 + | L2 + | LINENUMBER 13 L2 + | LDC "bar" + | ARETURN + | L1 + | LINENUMBER 15 L1 + | LDC "baz" + | ARETURN + | L3 + | LOCALVARIABLE this Lsimple4/Main; L0 L3 0 + | LOCALVARIABLE value Z L0 L3 1 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method3(Z)Ljava/lang/String; + | L0 + | LINENUMBER 19 L0 + | ILOAD 1 + | GETSTATIC java/lang/Boolean.FALSE : Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + | IF_ICMPNE L1 + | L2 + | LINENUMBER 20 L2 + | LDC "bar" + | ARETURN + | L1 + | LINENUMBER 22 L1 + | LDC "baz" + | ARETURN + | L3 + | LOCALVARIABLE this Lsimple4/Main; L0 L3 0 + | LOCALVARIABLE value Z L0 L3 1 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method(Z)Ljava/lang/String; + | L0 + | LINENUMBER 26 L0 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | ALOAD 0 + | ILOAD 1 + | INVOKEVIRTUAL simple4/Main.returnsBool (Z)Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.equals (Ljava/lang/Object;)Z + | IFEQ L1 + | L2 + | LINENUMBER 27 L2 + | LDC "foo" + | ARETURN + | L1 + | LINENUMBER 29 L1 + | LDC "baz" + | ARETURN + | L3 + | LOCALVARIABLE this Lsimple4/Main; L0 L3 0 + | LOCALVARIABLE value Z L0 L3 1 + | MAXSTACK = 3 + | MAXLOCALS = 2 + | + | // access flags 0x1 + | public returnsBool(Z)Ljava/lang/Boolean; + | L0 + | LINENUMBER 33 L0 + | INVOKESTATIC java/lang/Math.random ()D + | LDC 0.5 + | DCMPL + | IFLE L1 + | ICONST_1 + | GOTO L2 + | L1 + | ICONST_0 + | L2 + | INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; + | ARETURN + | L3 + | LOCALVARIABLE this Lsimple4/Main; L0 L3 0 + | LOCALVARIABLE value Z L0 L3 1 + | MAXSTACK = 4 + | MAXLOCALS = 2 + |} + """.trimMargin("|") + + private val sampleBytecode_3_noDebugInfo = """ + |// class version 67.0 (67) + |// access flags 0x21 + |public class simple4/Main { + | + | // compiled from: Main.java + | + | // access flags 0x1 + | public ()V + | L0 + | ALOAD 0 + | INVOKESPECIAL java/lang/Object. ()V + | RETURN + | L1 + | MAXSTACK = 1 + | MAXLOCALS = 1 + | + | // access flags 0x0 + | method1(Z)Ljava/lang/String; + | L0 + | ILOAD 1 + | ICONST_1 + | IF_ICMPNE L1 + | L2 + | LDC "baz" + | ARETURN + | L1 + | LDC "baz" + | ARETURN + | L3 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method2(Z)Ljava/lang/String; + | L0 + | ILOAD 1 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + | IF_ICMPNE L1 + | L2 + | LDC "bar" + | ARETURN + | L1 + | LDC "baz" + | ARETURN + | L3 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method3(Z)Ljava/lang/String; + | L0 + | ILOAD 1 + | GETSTATIC java/lang/Boolean.FALSE : Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + | IF_ICMPNE L1 + | L2 + | LDC "bar" + | ARETURN + | L1 + | LDC "baz" + | ARETURN + | L3 + | MAXSTACK = 2 + | MAXLOCALS = 2 + | + | // access flags 0x0 + | method(Z)Ljava/lang/String; + | L0 + | GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean; + | ALOAD 0 + | ILOAD 1 + | INVOKEVIRTUAL simple4/Main.returnsBool (Z)Ljava/lang/Boolean; + | INVOKEVIRTUAL java/lang/Boolean.equals (Ljava/lang/Object;)Z + | IFEQ L1 + | L2 + | LDC "foo" + | ARETURN + | L1 + | LDC "baz" + | ARETURN + | L3 + | MAXSTACK = 3 + | MAXLOCALS = 2 + | + | // access flags 0x1 + | public returnsBool(Z)Ljava/lang/Boolean; + | L0 + | INVOKESTATIC java/lang/Math.random ()D + | LDC 0.5 + | DCMPL + | IFLE L1 + | ICONST_1 + | GOTO L2 + | L1 + | ICONST_0 + | L2 + | INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; + | ARETURN + | L3 + | MAXSTACK = 4 + | MAXLOCALS = 2 + |} + """.trimMargin("|") + } +} diff --git a/plugins/java-decompiler/engine/test/org/jetbrains/java/decompiler/SingleClassesTest.java b/plugins/java-decompiler/engine/test/org/jetbrains/java/decompiler/SingleClassesTest.java index 011bbb5bdbf4..d395814222d9 100644 --- a/plugins/java-decompiler/engine/test/org/jetbrains/java/decompiler/SingleClassesTest.java +++ b/plugins/java-decompiler/engine/test/org/jetbrains/java/decompiler/SingleClassesTest.java @@ -33,6 +33,7 @@ public class SingleClassesTest extends SingleClassesTestBase { ); } + @Test public void testHelloWorld() { doTest("pkg/TestHelloWorld"); } @Test public void testGenerics() { doTest("pkg/TestGenerics"); } @Test public void testEnhancedForLoops() { doTest("pkg/TestEnhancedForLoops"); } @Test public void testPrimitiveNarrowing() { doTest("pkg/TestPrimitiveNarrowing"); } @@ -255,4 +256,4 @@ public class SingleClassesTest extends SingleClassesTestBase { @Test public void testTryCatchFinallyMismatched() { doTest("pkg/TestTryCatchFinallyMismatched"); } @Test public void testNestedCalls() { doTest("pkg/TestNestedCalls"); } @Test public void testBreakpointsContextProvider() { doTest("com/intellij/tasks/context/java/BreakpointsContextProvider"); } -} +} \ No newline at end of file diff --git a/plugins/java-decompiler/engine/testData/classes/pkg/TestHelloWorld.class b/plugins/java-decompiler/engine/testData/classes/pkg/TestHelloWorld.class new file mode 100644 index 0000000000000000000000000000000000000000..46b044a80b11d37e109b3be3a0ccdf18bee7e05a GIT binary patch literal 437 zcmZvZ%TB^T6o&s(3Z5_d4gEN9Zzgjw%?Ooq8O!W~ zQ0n%^CaN$EST<^~SLEKyG7}N2lAKMgVF8kfdz;EQ^RiUVRH1Gq~Xj$ z16u|h8%=Bznrlo1W1$dX%+&55wwRBZG7kE^v4J+hSv&^rNG!7}MZZ&Uf<8!Qf>0mI zSX}4PM5H5s67r26`I33> u(+|)q7ld7o>KQ1(#2)s!R??Hg3Yc#&oQ-$X|K;jen8C|d#sjW39R31n6I@gP literal 0 HcmV?d00001 diff --git a/plugins/java-decompiler/engine/testData/results/TestHelloWorld.dec b/plugins/java-decompiler/engine/testData/results/TestHelloWorld.dec new file mode 100644 index 000000000000..b2d507596b73 --- /dev/null +++ b/plugins/java-decompiler/engine/testData/results/TestHelloWorld.dec @@ -0,0 +1,25 @@ +package pkg; + +public class TestHelloWorld { + public static void main(String[] var0) { + System.out.println("hello world");// 5 + }// 6 +} + +class 'pkg/TestHelloWorld' { + method 'main ([Ljava/lang/String;)V' { + 0 4 + 1 4 + 2 4 + 3 4 + 4 4 + 5 4 + 6 4 + 7 4 + 8 5 + } +} + +Lines mapping: +5 <-> 5 +6 <-> 6 \ No newline at end of file diff --git a/plugins/java-decompiler/engine/testData/src/pkg/TestHelloWorld.java b/plugins/java-decompiler/engine/testData/src/pkg/TestHelloWorld.java new file mode 100644 index 000000000000..de48103984e1 --- /dev/null +++ b/plugins/java-decompiler/engine/testData/src/pkg/TestHelloWorld.java @@ -0,0 +1,7 @@ +package pkg; + +public class TestHelloWorld { + public static void main(String[] args) { + System.out.println("hello world"); + } +}