| // Copyright (c) 2019, 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.ir.conversion; |
| |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| |
| import com.android.tools.r8.NeverInline; |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.Value; |
| import com.android.tools.r8.utils.BooleanUtils; |
| import com.android.tools.r8.utils.InternalOptions; |
| import com.android.tools.r8.utils.Reporter; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Streams; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class StringSwitchConversionFromIfTest extends TestBase { |
| |
| private final boolean enableStringSwitchConversion; |
| private final TestParameters parameters; |
| |
| @Parameterized.Parameters(name = "{1}, enable string switch conversion: {0}") |
| public static List<Object[]> data() { |
| return buildParameters( |
| BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build()); |
| } |
| |
| public StringSwitchConversionFromIfTest( |
| boolean enableStringSwitchConversion, TestParameters parameters) { |
| this.enableStringSwitchConversion = enableStringSwitchConversion; |
| this.parameters = parameters; |
| } |
| |
| @Test |
| public void test() throws Exception { |
| testForR8(parameters.getBackend()) |
| .addInnerClasses(StringSwitchConversionFromIfTest.class) |
| .addKeepMainRule(TestClass.class) |
| .addOptionsModification( |
| options -> options.enableStringSwitchConversion = enableStringSwitchConversion) |
| .enableInliningAnnotations() |
| // TODO(b/135560746): Add support for treating the keys of a string-switch instruction as an |
| // identifier name string. |
| .noMinification() |
| .setMinApi(parameters.getApiLevel()) |
| .compile() |
| .inspect( |
| inspector -> { |
| ClassSubject classSubject = inspector.clazz(TestClass.class); |
| assertThat(classSubject, isPresent()); |
| |
| List<String> methodNames = |
| ImmutableList.of( |
| "test", |
| "testWithDeadStringIdComparisons", |
| "testWithMultipleHashCodeInvocations", |
| "testWithSwitch", |
| "testNullCheckIsPreserved"); |
| for (String methodName : methodNames) { |
| MethodSubject methodSubject = classSubject.uniqueMethodWithName(methodName); |
| assertThat(methodSubject, isPresent()); |
| |
| DexItemFactory dexItemFactory = new DexItemFactory(); |
| InternalOptions options = new InternalOptions(dexItemFactory, new Reporter()); |
| options.enableStringSwitchConversion = enableStringSwitchConversion; |
| |
| IRCode code = methodSubject.buildIR(options); |
| List<Value> hashCodeValues = |
| Streams.stream(code.instructions()) |
| .filter(instruction -> isInvokeStringHashCode(instruction, dexItemFactory)) |
| .map(Instruction::asInvokeVirtual) |
| .map(Instruction::outValue) |
| .collect(Collectors.toList()); |
| |
| // There should be at least one call to `int String.hashCode()`, since the |
| // instruction may throw. This holds even if the string-switch instruction is |
| // compiled to a sequence of `if (x.equals("..."))` instructions that do not even |
| // use the hash code. |
| assertNotEquals(0, hashCodeValues.size()); |
| } |
| }) |
| .run(parameters.getRuntime(), TestClass.class) |
| .assertSuccessWithOutputLines("Hello world!", "Caught NPE"); |
| } |
| |
| @Test |
| public void testCorrectnessOfConstants() { |
| assertEquals(A.class.getTypeName(), TestClass.NAME_1); |
| assertEquals(A.class.getTypeName().hashCode(), TestClass.HASH_1); |
| assertEquals(B.class.getTypeName(), TestClass.NAME_2); |
| assertEquals(B.class.getTypeName().hashCode(), TestClass.HASH_2); |
| } |
| |
| private static boolean isInvokeStringHashCode( |
| Instruction instruction, DexItemFactory dexItemFactory) { |
| // Intentionally using toSourceString() because `instruction.getInvokedMethod()` belongs to |
| // another factory than the given `dexItemFactory`. |
| String signature = dexItemFactory.stringMembers.hashCode.toSourceString(); |
| return instruction.isInvokeVirtual() |
| && instruction.asInvokeVirtual().getInvokedMethod().toSourceString().equals(signature); |
| } |
| |
| static class TestClass { |
| |
| private static final String NAME_1 = |
| "com.android.tools.r8.ir.conversion.StringSwitchConversionFromIfTest$A"; |
| private static final int HASH_1 = -1340982899; |
| |
| private static final String NAME_2 = |
| "com.android.tools.r8.ir.conversion.StringSwitchConversionFromIfTest$B"; |
| private static final int HASH_2 = -1340982898; |
| |
| public static void main(String[] args) { |
| test(toNullableClassName(A.class)); |
| test(toNullableClassName(B.class)); |
| testWithDeadStringIdComparisons(toNullableClassName(A.class)); |
| testWithDeadStringIdComparisons(toNullableClassName(B.class)); |
| testWithMultipleHashCodeInvocations(toNullableClassName(A.class)); |
| testWithMultipleHashCodeInvocations(toNullableClassName(B.class)); |
| testWithSwitch(toNullableClassName(A.class)); |
| testWithSwitch(toNullableClassName(B.class)); |
| |
| try { |
| testNullCheckIsPreserved(System.currentTimeMillis() >= 0 ? null : A.class.getName()); |
| } catch (NullPointerException e) { |
| System.out.println("Caught NPE"); |
| } |
| } |
| |
| static String toNullableClassName(Class<?> clazz) { |
| return System.currentTimeMillis() > 0 ? clazz.getName() : null; |
| } |
| |
| @NeverInline |
| private static void test(String className) { |
| int hashCode = className.hashCode(); |
| int result = -1; |
| if (hashCode == HASH_1) { |
| if (className.equals(NAME_1)) { |
| result = 0; |
| } |
| } else if (hashCode == HASH_2) { |
| if (className.equals(NAME_2)) { |
| result = 1; |
| } |
| } |
| if (result == 0) { |
| System.out.print("H"); |
| } else if (result == 1) { |
| System.out.print("e"); |
| } |
| } |
| |
| @NeverInline |
| private static void testWithDeadStringIdComparisons(String className) { |
| int hashCode = className.hashCode(); |
| int result = -1; |
| if (hashCode == HASH_1) { |
| if (className.equals(NAME_1)) { |
| result = 0; |
| } |
| } else if (hashCode == HASH_2) { |
| if (className.equals(NAME_2)) { |
| result = 1; |
| } |
| } |
| if (result == 0) { |
| System.out.print("l"); |
| } else { |
| switch (result) { |
| case 0: |
| System.out.println("Unexpected"); |
| break; |
| default: |
| if (result == 0) { |
| System.out.print("Unexpected"); |
| } else if (result == 1) { |
| System.out.print("l"); |
| } else if (result == 0) { |
| System.out.print("Unexpected"); |
| } else if (result == 1) { |
| System.out.print("Unexpected"); |
| } else { |
| switch (result) { |
| case 0: |
| System.out.println("Unexpected"); |
| break; |
| case 1: |
| System.out.println("Unexpected"); |
| break; |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| @NeverInline |
| private static void testWithMultipleHashCodeInvocations(String className) { |
| int result = -1; |
| if (className.hashCode() == HASH_1) { |
| if (className.equals(NAME_1)) { |
| result = 0; |
| } |
| } else if (className.hashCode() == HASH_2) { |
| if (className.equals(NAME_2)) { |
| result = 1; |
| } |
| } |
| if (result == 0) { |
| System.out.print("o"); |
| } else if (result == 1) { |
| System.out.print(" "); |
| } |
| } |
| |
| @NeverInline |
| private static void testWithSwitch(String className) { |
| int result = -1; |
| if (className.hashCode() == HASH_1) { |
| if (className.equals(NAME_1)) { |
| result = 0; |
| } |
| } else if (className.hashCode() == HASH_2) { |
| if (className.equals(NAME_2)) { |
| result = 1; |
| } |
| } |
| switch (result) { |
| case 0: |
| System.out.print("world"); |
| break; |
| case 1: |
| System.out.println("!"); |
| break; |
| default: |
| // Ignore. |
| } |
| } |
| |
| @NeverInline |
| private static void testNullCheckIsPreserved(String className) { |
| switch (className) { |
| case "A": |
| System.out.println("Unexpected (A)"); |
| break; |
| case "B": |
| System.out.println("Unexpected (B)"); |
| break; |
| case "C": |
| System.out.println("Unexpected (C)"); |
| break; |
| default: |
| System.out.println("Unexpected (default)"); |
| } |
| } |
| } |
| |
| static class A {} |
| |
| static class B {} |
| } |