| // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8.jasmin; |
| |
| import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage; |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.TestDiagnosticMessages; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.jasmin.JasminBuilder.ClassFileVersion; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.utils.BooleanUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.google.common.collect.ImmutableList; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| |
| @RunWith(Parameterized.class) |
| public class InvalidDebugInfoTests extends TestBase { |
| |
| @Parameters(name = "{0}, strict:{1}") |
| public static List<Object[]> data() { |
| return buildParameters( |
| getTestParameters().withAllRuntimesAndApiLevels().enableApiLevelsForCf().build(), |
| BooleanUtils.values()); |
| } |
| |
| private final TestParameters parameters; |
| private final boolean strict; |
| |
| public InvalidDebugInfoTests(TestParameters parameters, boolean strict) { |
| this.parameters = parameters; |
| this.strict = strict; |
| } |
| |
| private void optionsModification(InternalOptions options) { |
| options.invalidDebugInfoStrict = strict; |
| options.testing.forceIRForCfToCfDesugar = true; |
| } |
| |
| private void assertInvalidTypeMessage(TestDiagnosticMessages diagnostics) { |
| assertInvalidInfoMessages(diagnostics, "Attempt to define local of type"); |
| } |
| |
| private void assertUninitializedLocalMessage(TestDiagnosticMessages diagnostics) { |
| assertInvalidInfoMessages(diagnostics, "Local refers to uninitialized register"); |
| } |
| |
| private void assertInvalidInfoMessages(TestDiagnosticMessages diagnostics, String message) { |
| // The reporting of invalid debug info issues three info items: |
| diagnostics.assertOnlyInfos(); |
| diagnostics.assertInfosMatch( |
| ImmutableList.of( |
| diagnosticMessage(containsString("Stripped invalid locals information")), |
| diagnosticMessage(containsString(message)), |
| diagnosticMessage(containsString("sign of using an outdated Java toolchain")))); |
| } |
| |
| // This is a regression test for invalid live-ranges of locals generated by some old Java |
| // compilers. The issue is that a local slot may have been initialized outside the live-scope of |
| // the variable and then the subsequent live-scope of the variable extends beyond its actual |
| // liveness. In the example below the variable 'y' is initialized outside its range (it is thus |
| // associated with the local 'x' (the SSA value is unaffected by the istore). Finally the 'return' |
| // forces a read of all supposedly live variables before exiting. Here the attempt to read 'y' |
| // will actually be a read of 'x'. |
| @Test |
| public void testInvalidInfoThrow() throws Exception { |
| JasminBuilder builder = new JasminBuilder(ClassFileVersion.JDK_1_4); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| |
| clazz.addStaticMethod("foo", ImmutableList.of("I"), "V", |
| ".limit stack 3", |
| ".limit locals 4", |
| ".var 0 is x I from LabelInit to LabelExit", |
| ".var 1 is y I from LabelLocalStart to LabelExit", |
| ".var 2 is e Ljava/lang/Exception; from LabelCatchStart to LabelCatchEnd", |
| // var 3 is the jsr address |
| "LabelInit:", |
| "LabelTryStart:", |
| " ldc 84", |
| " iload 0", |
| " dup", |
| " istore 1", // init local[1] to value of local[0] (eg, 'x' since 'y' is not live yet). |
| " idiv", |
| " istore 0", |
| "LabelLocalStart:", |
| " jsr LabelPrint", |
| " goto LabelExit", |
| "LabelTryEnd:", |
| "LabelCatchStart:", |
| " astore 2", |
| " jsr LabelPrint", |
| " return", // y is not actually live here. |
| "LabelCatchEnd:", |
| "LabelPrint:", |
| " astore 3", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " iload 0", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| " ret 3", |
| "LabelExit:", |
| " return", |
| ".catch java/lang/Exception from LabelTryStart to LabelTryEnd using LabelCatchStart" |
| ); |
| |
| clazz.addMainMethod( |
| ".limit stack 1", |
| ".limit locals 1", |
| " ldc 2", |
| " invokestatic Test/foo(I)V", |
| " ldc 0", |
| " invokestatic Test/foo(I)V", |
| " return"); |
| |
| String expected = "42" + ToolHelper.LINE_SEPARATOR + "0" + ToolHelper.LINE_SEPARATOR; |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .setMinApi(parameters.getApiLevel()) |
| .addOptionsModification(this::optionsModification) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| if (strict) { |
| assertUninitializedLocalMessage(diagnostics); |
| } else { |
| diagnostics.assertNoMessages(); |
| } |
| }) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| |
| // Regression test to check that we properly add UninitializedLocal SSA values for methods that |
| // have arguments without local info. To witness this bug, we also need "invalid" debug info, eg, |
| // in this test the scope of "y" (local 2) spans the exceptional edge in which it is not live. |
| @Test |
| public void testInvalidInfoBug37722432() throws Exception { |
| JasminBuilder builder = new JasminBuilder(ClassFileVersion.JDK_1_4); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| |
| clazz.addStaticMethod("foo", ImmutableList.of("I", "I"), "V", |
| ".limit stack 2", |
| ".limit locals 3", |
| ".var 0 is x I from LabelInit to LabelExit", |
| // Synthesized arg (no local info) |
| ".var 2 is y I from LabelLocalStart to LabelExit", |
| ".catch java/lang/Exception from LabelInit to LabelCatch using LabelCatch", |
| "LabelInit:", // Start of try block targets catch with a state excluding 'y'. |
| " ldc 84", |
| " iload 0", |
| " idiv", |
| " istore 2", |
| "LabelLocalStart:", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " iload 2", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| " return", |
| "LabelCatch:", // Catch target appears to include 'y' but actually does not. |
| " pop", |
| " return", |
| "LabelExit:" |
| ); |
| |
| clazz.addMainMethod( |
| ".limit stack 2", |
| ".limit locals 1", |
| " ldc 2", |
| " ldc 2", |
| " invokestatic Test/foo(II)V", |
| " ldc 0", |
| " ldc 0", |
| " invokestatic Test/foo(II)V", |
| " return"); |
| |
| String expected = "42" + ToolHelper.LINE_SEPARATOR; |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .addOptionsModification(this::optionsModification) |
| .setMinApi(parameters.getApiLevel()) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| if (strict) { |
| assertUninitializedLocalMessage(diagnostics); |
| } else { |
| diagnostics.assertNoMessages(); |
| } |
| }) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| |
| @Test |
| public void invalidInfoBug63412730_onWrite() throws Throwable { |
| JasminBuilder builder = new JasminBuilder(); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V", |
| ".limit stack 3", |
| ".limit locals 2", |
| ".var 1 is i I from LI to End", |
| ".var 0 is f F from LF to End", |
| "Init:", |
| " ldc 42", |
| " istore 0", |
| ".line 1", |
| "LI:", |
| " ldc 7.5", |
| " fstore 1", |
| ".line 2", |
| "LF:", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " dup", |
| " iload 0", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| " fload 1", |
| " invokevirtual java/io/PrintStream/println(F)V", |
| ".line 3", |
| " return", |
| "End:"); |
| |
| clazz.addMainMethod( |
| ".limit stack 1", |
| ".limit locals 1", |
| " invokestatic Test/bar()V", |
| " return"); |
| |
| String expected = StringUtils.lines("42", "7.5"); |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .setMinApi(parameters.getApiLevel()) |
| .addOptionsModification(this::optionsModification) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| if (strict) { |
| assertUninitializedLocalMessage(diagnostics); |
| } else { |
| assertInvalidTypeMessage(diagnostics); |
| } |
| }) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected) |
| .inspect( |
| inspector -> |
| assertFalse(inspector.clazz(clazz.name).method(method).hasLocalVariableTable())); |
| } |
| |
| @Test |
| public void invalidInfoBug63412730_onRead() throws Throwable { |
| JasminBuilder builder = new JasminBuilder(); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V", |
| ".limit stack 3", |
| ".limit locals 2", |
| ".var 1 is i I from Locals to End", |
| ".var 0 is f F from Locals to End", |
| "Init:", |
| " ldc 42", |
| " istore 0", |
| ".line 1", |
| " ldc 7.5", |
| " fstore 1", |
| ".line 2", |
| "Locals:", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " dup", |
| " iload 0", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| " fload 1", |
| " invokevirtual java/io/PrintStream/println(F)V", |
| ".line 3", |
| " return", |
| "End:"); |
| |
| clazz.addMainMethod( |
| ".limit stack 1", |
| ".limit locals 1", |
| " invokestatic Test/bar()V", |
| " return"); |
| |
| String expected = StringUtils.lines("42", "7.5"); |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .addOptionsModification(this::optionsModification) |
| .setMinApi(parameters.getApiLevel()) |
| .compileWithExpectedDiagnostics(this::assertInvalidTypeMessage) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected) |
| .inspect( |
| inspector -> |
| assertFalse(inspector.clazz(clazz.name).method(method).hasLocalVariableTable())); |
| } |
| |
| @Test |
| public void invalidInfoBug63412730_onMove() throws Throwable { |
| JasminBuilder builder = new JasminBuilder(ClassFileVersion.JDK_1_4); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V", |
| ".limit stack 3", |
| ".limit locals 2", |
| ".var 1 is i I from LI to End", |
| ".var 0 is j F from LJ to End", |
| "Init:", |
| " ldc 42", |
| " istore 0", |
| ".line 1", |
| "LI:", |
| " ldc 0", |
| " ldc 0", |
| " ifeq LZ", |
| " ldc 75", |
| " istore 1", |
| ".line 2", |
| "LJ:", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " iload 1", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| ".line 3", |
| " return", |
| "LZ:", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " iload 0", |
| " invokevirtual java/io/PrintStream/println(I)V", |
| ".line 4", |
| " return", |
| "End:"); |
| |
| clazz.addMainMethod( |
| ".limit stack 1", |
| ".limit locals 1", |
| " invokestatic Test/bar()V", |
| " return"); |
| |
| String expected = StringUtils.lines("42"); |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .addOptionsModification(this::optionsModification) |
| .setMinApi(parameters.getApiLevel()) |
| .compileWithExpectedDiagnostics( |
| diagnostics -> { |
| if (strict) { |
| assertUninitializedLocalMessage(diagnostics); |
| } else { |
| assertInvalidTypeMessage(diagnostics); |
| } |
| }) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected) |
| .inspect( |
| inspector -> |
| assertFalse(inspector.clazz(clazz.name).method(method).hasLocalVariableTable())); |
| } |
| |
| @Test |
| public void invalidInfoBug63412730_onPop() throws Throwable { |
| JasminBuilder builder = new JasminBuilder(); |
| JasminBuilder.ClassBuilder clazz = builder.addClass("Test"); |
| MethodSignature method = clazz.addStaticMethod("bar", ImmutableList.of(), "V", |
| ".limit stack 3", |
| ".limit locals 2", |
| ".var 1 is a [Ljava/lang/Object; from Locals to End", |
| ".var 0 is o LObject; from Locals to End", |
| "Init:", |
| " ldc 1", |
| " anewarray java/lang/Object", |
| " astore 0", |
| ".line 1", |
| " new java/lang/Integer", |
| " dup", |
| " ldc 42", |
| " invokespecial java/lang/Integer/<init>(I)V", |
| " astore 1", |
| ".line 2", |
| "Locals:", |
| " aload 0", |
| " ldc 0", |
| " aload 1", |
| " aastore", |
| ".line 3", |
| " getstatic java/lang/System/out Ljava/io/PrintStream;", |
| " aload 0", |
| " ldc 0", |
| " aaload", |
| " invokevirtual java/io/PrintStream/println(Ljava/lang/Object;)V", |
| ".line 4", |
| " return", |
| "End:"); |
| |
| clazz.addMainMethod( |
| ".limit stack 1", |
| ".limit locals 1", |
| " invokestatic Test/bar()V", |
| " return"); |
| |
| String expected = StringUtils.lines("42"); |
| if (parameters.isCfRuntime()) { |
| testForJvm() |
| .addProgramClassFileData(builder.buildClasses()) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected); |
| } |
| testForD8(parameters.getBackend()) |
| .addProgramClassFileData(builder.buildClasses()) |
| .addOptionsModification(this::optionsModification) |
| .setMinApi(parameters.getApiLevel()) |
| .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertNoMessages) |
| .run(parameters.getRuntime(), clazz.name) |
| .assertSuccessWithOutput(expected) |
| .inspect( |
| inspector -> { |
| // Note: This code is actually invalid debug info, but we do not reject it because |
| // both types are reference types. If we change that we should update this test. |
| assertTrue(inspector.clazz(clazz.name).method(method).hasLocalVariableTable()); |
| }); |
| } |
| } |