// Copyright 2000-2025 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) val 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("|") } }