| // Copyright (c) 2018, 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.analysis.escape; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.TestParameters; |
| import com.android.tools.r8.TestParametersCollection; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.ir.analysis.AnalysisTestBase; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.InvokeVirtual; |
| import com.android.tools.r8.ir.code.NewInstance; |
| import com.android.tools.r8.ir.code.Value; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class EscapeAnalysisForNameReflectionTest extends AnalysisTestBase { |
| |
| @Parameterized.Parameters(name = "{0}") |
| public static TestParametersCollection data() { |
| return getTestParameters().withAllRuntimes().build(); |
| } |
| |
| public EscapeAnalysisForNameReflectionTest(TestParameters parameters) throws Exception { |
| super( |
| parameters, |
| TestClass.class.getTypeName(), TestClass.class, Helper.class, NamingInterface.class); |
| } |
| |
| private static Predicate<Instruction> invokesMethodWithName(String name) { |
| return instruction -> |
| instruction.isInvokeMethod() |
| && instruction.asInvokeMethod().getInvokedMethod().name.toString().equals(name); |
| } |
| |
| @Test |
| public void testEscapeViaReturn() throws Exception { |
| buildAndCheckIR("escapeViaReturn", checkEscapingName(true, Instruction::isReturn)); |
| } |
| |
| @Test |
| public void testEscapeViaThrow() throws Exception { |
| buildAndCheckIR( |
| "escapeViaThrow", |
| code -> { |
| NewInstance e = getMatchingInstruction(code, Instruction::isNewInstance); |
| assertNotNull(e); |
| Value v = e.outValue(); |
| assertNotNull(v); |
| EscapeAnalysis escapeAnalysis = |
| new EscapeAnalysis( |
| appView, |
| StringOptimizerEscapeAnalysisConfigurationForTesting.getInstance()); |
| Set<Instruction> escapeRoutes = escapeAnalysis.computeEscapeRoutes(code, v); |
| assertEquals(2, escapeRoutes.size()); |
| assertTrue( |
| escapeRoutes.stream().allMatch( |
| instr -> |
| instr.isThrow() |
| || (instr.isInvokeDirect() |
| && invokesMethodWithName("<init>").test(instr)))); |
| }); |
| } |
| |
| @Test |
| public void testEscapeViaStaticPut() throws Exception { |
| buildAndCheckIR("escapeViaStaticPut", checkEscapingName(true, Instruction::isStaticPut)); |
| } |
| |
| @Test |
| public void testEscapeViaInstancePut_local() throws Exception { |
| buildAndCheckIR( |
| "escapeViaInstancePut_local", |
| checkEscapingName(true, invokesMethodWithName("namingInterfaceConsumer"))); |
| } |
| |
| @Test |
| public void testEscapeViaArrayPut_local() throws Exception { |
| buildAndCheckIR( |
| "escapeViaArrayPut_local", |
| checkEscapingName(true, invokesMethodWithName("namingInterfacesConsumer"))); |
| } |
| |
| @Test |
| public void testEscapeViaArrayPut_heap() throws Exception { |
| buildAndCheckIR( |
| "escapeViaArrayPut_heap", |
| checkEscapingName( |
| true, |
| i -> i.isArrayPut() || invokesMethodWithName("namingInterfacesConsumer").test(i))); |
| } |
| |
| @Test |
| public void testEscapeViaArrayPut_argument() throws Exception { |
| buildAndCheckIR( |
| "escapeViaArrayPut_argument", |
| checkEscapingName( |
| true, |
| i -> i.isArrayPut() || invokesMethodWithName("namingInterfacesConsumer").test(i))); |
| } |
| |
| @Test |
| public void testEscapeViaListPut() throws Exception { |
| buildAndCheckIR( |
| "escapeViaListPut", |
| checkEscapingName(true, invokesMethodWithName("add"))); |
| } |
| |
| @Test |
| public void testEscapeViaListArgumentPut() throws Exception { |
| buildAndCheckIR( |
| "escapeViaListArgumentPut", |
| checkEscapingName(true, invokesMethodWithName("add"))); |
| } |
| |
| @Test |
| public void testEscapeViaArrayGet() throws Exception { |
| buildAndCheckIR( |
| "escapeViaArrayGet", |
| checkEscapingName(true, invokesMethodWithName("namingInterfaceConsumer"))); |
| } |
| |
| @Test |
| public void testHandlePhiAndAlias() throws Exception { |
| buildAndCheckIR( |
| "handlePhiAndAlias", checkEscapingName(true, invokesMethodWithName("stringConsumer"))); |
| } |
| |
| @Test |
| public void testToString() throws Exception { |
| buildAndCheckIR("toString", checkEscapingName(true, Instruction::isReturn)); |
| } |
| |
| @Test |
| public void testEscapeViaRecursion() throws Exception { |
| buildAndCheckIR("escapeViaRecursion", checkEscapingName(true, Instruction::isReturn)); |
| } |
| |
| @Test |
| public void testEscapeViaLoopAndBoxing() throws Exception { |
| buildAndCheckIR( |
| "escapeViaLoopAndBoxing", |
| checkEscapingName( |
| true, instr -> instr.isReturn() || invokesMethodWithName("toString").test(instr))); |
| } |
| |
| static class StringOptimizerEscapeAnalysisConfigurationForTesting |
| implements EscapeAnalysisConfiguration { |
| |
| private static final StringOptimizerEscapeAnalysisConfigurationForTesting INSTANCE = |
| new StringOptimizerEscapeAnalysisConfigurationForTesting(); |
| |
| private StringOptimizerEscapeAnalysisConfigurationForTesting() {} |
| |
| static StringOptimizerEscapeAnalysisConfigurationForTesting getInstance() { |
| return INSTANCE; |
| } |
| |
| @Override |
| public boolean isLegitimateEscapeRoute( |
| AppView<?> appView, |
| EscapeAnalysis escapeAnalysis, |
| Instruction escapeRoute, |
| DexMethod context) { |
| if (escapeRoute.isReturn() || escapeRoute.isThrow() || escapeRoute.isStaticPut()) { |
| return false; |
| } |
| if (escapeRoute.isInvokeMethod()) { |
| DexMethod invokedMethod = escapeRoute.asInvokeMethod().getInvokedMethod(); |
| // Heuristic: if the call target has the same method name, it could be still local. |
| if (invokedMethod.name == context.name) { |
| return true; |
| } |
| // It's not legitimate during testing, except for recursion calls. |
| return false; |
| } |
| if (escapeRoute.isArrayPut()) { |
| Value array = escapeRoute.asArrayPut().array().getAliasedValue(); |
| return !array.isPhi() && array.definition.isCreatingArray(); |
| } |
| if (escapeRoute.isInstancePut()) { |
| Value instance = escapeRoute.asInstancePut().object().getAliasedValue(); |
| return !instance.isPhi() && instance.definition.isNewInstance(); |
| } |
| // All other cases are not legitimate. |
| return false; |
| } |
| } |
| |
| private Consumer<IRCode> checkEscapingName( |
| boolean expectedHeuristicResult, Predicate<Instruction> instructionTester) { |
| assert instructionTester != null; |
| return code -> { |
| InvokeVirtual simpleNameCall = getSimpleNameCall(code); |
| assertNotNull(simpleNameCall); |
| Value v = simpleNameCall.outValue(); |
| assertNotNull(v); |
| EscapeAnalysis escapeAnalysis = |
| new EscapeAnalysis( |
| appView, StringOptimizerEscapeAnalysisConfigurationForTesting.getInstance()); |
| Set<Instruction> escapeRoutes = escapeAnalysis.computeEscapeRoutes(code, v); |
| assertNotEquals(expectedHeuristicResult, escapeRoutes.isEmpty()); |
| assertTrue(escapeRoutes.stream().allMatch(instructionTester)); |
| }; |
| } |
| |
| private static InvokeVirtual getSimpleNameCall(IRCode code) { |
| return getMatchingInstruction(code, instruction -> { |
| if (instruction.isInvokeVirtual()) { |
| DexMethod invokedMethod = instruction.asInvokeVirtual().getInvokedMethod(); |
| return invokedMethod.holder.toDescriptorString().equals("Ljava/lang/Class;") |
| && invokedMethod.name.toString().equals("getSimpleName"); |
| } |
| return false; |
| }).asInvokeVirtual(); |
| } |
| |
| static class Helper { |
| static String toString(String x, String y) { |
| return x + y; |
| } |
| |
| static void stringConsumer(String str) { |
| System.out.println(str); |
| } |
| |
| static void namingInterfaceConsumer(NamingInterface itf) { |
| System.out.println(itf.getName()); |
| } |
| |
| static void namingInterfacesConsumer(NamingInterface[] interfaces) { |
| for (NamingInterface itf : interfaces) { |
| System.out.println(itf.getName()); |
| } |
| } |
| } |
| |
| interface NamingInterface { |
| default String getName() { |
| return getClass().getName(); |
| } |
| } |
| |
| static class TestClass implements NamingInterface { |
| private static NamingInterface[] ARRAY = new NamingInterface[1]; |
| |
| static String tag; |
| String id; |
| |
| TestClass() { |
| this(new Random().nextInt()); |
| } |
| |
| TestClass(int id) { |
| this(String.valueOf(id)); |
| } |
| |
| TestClass(String id) { |
| this.id = id; |
| } |
| |
| @Override |
| public String getName() { |
| return getClass().getCanonicalName(); |
| } |
| |
| @Override |
| public String toString() { |
| String name = getClass().getSimpleName(); |
| return Helper.toString(name, id); |
| } |
| |
| public static String escapeViaReturn() { |
| return TestClass.class.getSimpleName(); |
| } |
| |
| public static void escapeViaThrow() { |
| RuntimeException e = new RuntimeException(); |
| throw e; |
| } |
| |
| public static void escapeViaStaticPut() { |
| tag = TestClass.class.getSimpleName(); |
| } |
| |
| public static void escapeViaInstancePut_local() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| Helper.namingInterfaceConsumer(instance); |
| } |
| |
| public static void escapeViaArrayPut_local() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| NamingInterface[] array = new NamingInterface[1]; |
| array[0] = instance; |
| Helper.namingInterfacesConsumer(array); |
| } |
| |
| public static void escapeViaArrayPut_heap() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| ARRAY[0] = instance; |
| Helper.namingInterfacesConsumer(ARRAY); |
| } |
| |
| public static void escapeViaArrayPut_argument(NamingInterface[] array) { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| array[0] = instance; |
| } |
| |
| public static void escapeViaListPut() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| List<NamingInterface> list = new ArrayList<>(); |
| list.add(instance); |
| Helper.namingInterfacesConsumer(list.toArray(new NamingInterface[0])); |
| } |
| |
| public static void escapeViaListArgumentPut(List<NamingInterface> list) { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| list.add(instance); |
| } |
| |
| public static void escapeViaArrayGet(boolean flag) { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| NamingInterface[] array = new NamingInterface[2]; |
| array[0] = instance; |
| array[1] = null; |
| // At compile-time, we don't know which one will be taken. |
| // So, conservatively, we should consider that name can be propagated. |
| NamingInterface couldBeNull = flag ? array[0] : array[1]; |
| Helper.namingInterfaceConsumer(couldBeNull); |
| } |
| |
| public static void handlePhiAndAlias(String input) { |
| TestClass instance = new TestClass(); |
| String name = System.currentTimeMillis() > 0 ? instance.getClass().getSimpleName() : input; |
| String alias = name; |
| Helper.stringConsumer(alias); |
| } |
| |
| public static String escapeViaRecursion(String arg, boolean escape) { |
| if (escape) { |
| return arg; |
| } else { |
| String name = TestClass.class.getSimpleName(); |
| return escapeViaRecursion(name, true); |
| } |
| } |
| |
| public static TestClass escapeViaLoopAndBoxing(String arg) { |
| TestClass box = null; |
| while (arg != null) { |
| box = new TestClass(arg); |
| String name = TestClass.class.getSimpleName(); |
| box.id = name; |
| arg = box.toString(); |
| } |
| return box; |
| } |
| } |
| } |