blob: 36c9ba536862ab5069af582c919b94055b92c7d4 [file] [log] [blame]
// 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) {
if (parameters.isCfRuntime()) {
diagnostics.assertNoErrors();
diagnostics.assertWarningsMatch(cfD8NotSupportedDiagnostic);
} else {
diagnostics.assertOnlyInfos();
}
// The reporting of invalid debug info issues three info items:
diagnostics.assertInfosMatch(
ImmutableList.of(
diagnosticMessage(containsString("Stripped invalid locals information")),
diagnosticMessage(containsString(message)),
diagnosticMessage(containsString("sign of using an outdated Java toolchain"))));
}
private void assertNoMessages(TestDiagnosticMessages diagnostics) {
if (parameters.isCfRuntime()) {
diagnostics.assertOnlyWarnings();
diagnostics.assertWarningsMatch(cfD8NotSupportedDiagnostic);
} else {
diagnostics.assertNoMessages();
}
}
// 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 {
assertNoMessages(diagnostics);
}
})
.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 {
assertNoMessages(diagnostics);
}
})
.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(this::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());
});
}
}