| // 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; |
| |
| import static junit.framework.TestCase.assertFalse; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.graph.AppInfo; |
| import com.android.tools.r8.graph.DexMethod; |
| 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 com.android.tools.r8.ir.optimize.string.StringOptimizer; |
| 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; |
| |
| public class EscapeAnalysisForNameReflectionTest extends AnalysisTestBase { |
| |
| public EscapeAnalysisForNameReflectionTest() throws Exception { |
| super(TestClass.class.getTypeName(), |
| TestClass.class, Helper.class, NamingInterface.class); |
| } |
| |
| @Test |
| public void testEscapeViaReturn() throws Exception { |
| buildAndCheckIR("escapeViaReturn", checkEscapingName(appInfo, true, instr -> { |
| return instr.isReturn(); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaThrow() throws Exception { |
| buildAndCheckIR("escapeViaThrow", code -> { |
| NewInstance e = getMatchingInstruction(code, Instruction::isNewInstance); |
| assertNotNull(e); |
| Value v = e.outValue(); |
| assertNotNull(v); |
| Set<Instruction> escapingInstructions = EscapeAnalysis.escape(code, v); |
| assertTrue( |
| StringOptimizer.hasPotentialReadOutside(appInfo, code.method, escapingInstructions)); |
| assertTrue(escapingInstructions.stream().allMatch(instr -> { |
| return instr.isThrow() |
| || (instr.isInvokeDirect() |
| && instr.asInvokeDirect().getInvokedMethod().name.toString().equals("<init>")); |
| })); |
| }); |
| } |
| |
| @Test |
| public void testEscapeViaStaticPut() throws Exception { |
| buildAndCheckIR("escapeViaStaticPut", checkEscapingName(appInfo, true, instr -> { |
| return instr.isStaticPut(); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaInstancePut() throws Exception { |
| buildAndCheckIR("escapeViaInstancePut", checkEscapingName(appInfo, true, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString() |
| .equals("namingInterfaceConsumer"); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaArrayPut() throws Exception { |
| buildAndCheckIR("escapeViaArrayPut", checkEscapingName(appInfo, true, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString() |
| .equals("namingInterfacesConsumer"); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaArrayArgumentPut() throws Exception { |
| buildAndCheckIR("escapeViaArrayArgumentPut", checkEscapingName(appInfo, true, instr -> { |
| return instr.isArrayPut(); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaListPut() throws Exception { |
| buildAndCheckIR("escapeViaListPut", checkEscapingName(appInfo, false, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("add"); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaListArgumentPut() throws Exception { |
| buildAndCheckIR("escapeViaListArgumentPut", checkEscapingName(appInfo, false, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("add"); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaArrayGet() throws Exception { |
| buildAndCheckIR("escapeViaArrayGet", checkEscapingName(appInfo, true, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString() |
| .equals("namingInterfaceConsumer"); |
| })); |
| } |
| |
| @Test |
| public void testHandlePhiAndAlias() throws Exception { |
| buildAndCheckIR("handlePhiAndAlias", checkEscapingName(appInfo, true, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("stringConsumer"); |
| })); |
| } |
| |
| @Test |
| public void testToString() throws Exception { |
| buildAndCheckIR("toString", checkEscapingName(appInfo, false, instr -> { |
| return instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("toString"); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaRecursion() throws Exception { |
| buildAndCheckIR("escapeViaRecursion", checkEscapingName(appInfo, false, instr -> { |
| return instr.isInvokeStatic(); |
| })); |
| } |
| |
| @Test |
| public void testEscapeViaLoopAndBoxing() throws Exception { |
| buildAndCheckIR("escapeViaLoopAndBoxing", checkEscapingName(appInfo, true, instr -> { |
| return instr.isReturn() |
| || (instr.isInvokeMethod() |
| && instr.asInvokeMethod().getInvokedMethod().name.toString().equals("toString")); |
| })); |
| } |
| |
| private static Consumer<IRCode> checkEscapingName( |
| AppInfo appInfo, |
| boolean expectedHeuristicResult, |
| Predicate<Instruction> instructionTester) { |
| return code -> { |
| InvokeVirtual simpleNameCall = getSimpleNameCall(code); |
| assertNotNull(simpleNameCall); |
| Value v = simpleNameCall.outValue(); |
| assertNotNull(v); |
| Set<Instruction> escapingInstructions = EscapeAnalysis.escape(code, v); |
| assertEquals( |
| expectedHeuristicResult, |
| StringOptimizer.hasPotentialReadOutside(appInfo, code.method, escapingInstructions)); |
| if (instructionTester == null) { |
| // Implicitly expecting the absence of escaping points. |
| assertTrue(escapingInstructions.isEmpty()); |
| } else { |
| // Otherwise, test all escaping instructions. |
| assertFalse(escapingInstructions.isEmpty()); |
| assertTrue(escapingInstructions.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 { |
| 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() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| Helper.namingInterfaceConsumer(instance); |
| } |
| |
| public static void escapeViaArrayPut() { |
| TestClass instance = new TestClass(); |
| instance.id = instance.getClass().getSimpleName(); |
| NamingInterface[] array = new NamingInterface[1]; |
| array[0] = instance; |
| Helper.namingInterfacesConsumer(array); |
| } |
| |
| public static void escapeViaArrayArgumentPut(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; |
| } |
| } |
| } |