blob: 3e4078bac00b2d02e1d7a898abf9600a590bf850 [file] [log] [blame]
// 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.naming;
import static com.android.tools.r8.utils.DescriptorUtils.javaTypeToDescriptorIfValidJavaType;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexDefinitionSupplier;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMember;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.code.ArrayPut;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CheckCast;
import com.android.tools.r8.ir.code.ConstantValueUtils;
import com.android.tools.r8.ir.code.DexItemBasedConstString;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.NewArrayEmpty;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.naming.identifiernamestring.IdentifierNameStringLookupResult;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class IdentifierNameStringUtils {
/**
* Checks if the given {@param method} is a reflection method in Java.
*
* @param dexItemFactory where pre-defined descriptors are retrieved
* @param method to test
* @return {@code true} if the given {@param method} is a reflection method in Java.
*/
public static boolean isReflectionMethod(DexItemFactory dexItemFactory, DexMethod method) {
// So, why is this simply not like:
// return dexItemFactory.classMethods.isReflectiveClassLookup(method)
// || dexItemFactory.classMethods.isReflectiveMemberLookup(method)
// || dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(method);
// ?
// That is because the counter part of other shrinkers supports users' own reflective methods
// whose signature matches with reflection methods in Java. Hence, explicit signature matching.
// See tests {@link IdentifierMinifierTest#test2_rule3},
// {@link IdentifierNameStringMarkerTest#reflective_field_singleUseOperand_renamed}, or
// {@link IdentifierNameStringMarkerTest#reflective_method_singleUseOperand_renamed}.
//
// For java.lang.Class:
// (String) -> java.lang.Class | java.lang.reflect.Field
// (String, boolean, ClassLoader) -> java.lang.Class
// (String, Class[]) -> java.lang.reflect.Method
// For java.util.concurrent.atomic.Atomic(Integer|Long)FieldUpdater:
// (Class, String) -> $holderType
// For java.util.concurrent.atomic.AtomicReferenceFieldUpdater:
// (Class, Class, String) -> $holderType
// For any other types:
// (Class, String) -> java.lang.reflect.Field
// (Class, String, Class[]) -> java.lang.reflect.Method
int arity = method.getArity();
if (method.holder == dexItemFactory.classType) {
// Virtual methods of java.lang.Class, such as getField, getMethod, etc.
if (arity == 0 || arity > 3) {
return false;
}
if (arity == 1) {
if (method.proto.returnType != dexItemFactory.classType
&& method.proto.returnType != dexItemFactory.fieldType) {
return false;
}
} else if (arity == 2) {
if (method.proto.returnType != dexItemFactory.methodType) {
return false;
}
} else {
if (method.proto.returnType != dexItemFactory.classType) {
return false;
}
}
if (method.proto.parameters.values[0] != dexItemFactory.stringType) {
return false;
}
if (arity == 2) {
if (method.proto.parameters.values[1] != dexItemFactory.classArrayType) {
return false;
}
}
if (arity == 3) {
if (method.proto.parameters.values[1] != dexItemFactory.booleanType
&& method.proto.parameters.values[2] != dexItemFactory.classLoaderType) {
return false;
}
}
} else if (
method.holder.descriptor == dexItemFactory.intFieldUpdaterDescriptor
|| method.holder.descriptor == dexItemFactory.longFieldUpdaterDescriptor) {
// Atomic(Integer|Long)FieldUpdater->newUpdater(Class, String)AtomicFieldUpdater
if (arity != 2) {
return false;
}
if (method.proto.returnType != method.holder) {
return false;
}
if (method.proto.parameters.values[0] != dexItemFactory.classType) {
return false;
}
if (method.proto.parameters.values[1] != dexItemFactory.stringType) {
return false;
}
} else if (method.holder.descriptor == dexItemFactory.referenceFieldUpdaterDescriptor) {
// AtomicReferenceFieldUpdater->newUpdater(Class, Class, String)AtomicFieldUpdater
if (arity != 3) {
return false;
}
if (method.proto.returnType != method.holder) {
return false;
}
if (method.proto.parameters.values[0] != dexItemFactory.classType) {
return false;
}
if (method.proto.parameters.values[1] != dexItemFactory.classType) {
return false;
}
if (method.proto.parameters.values[2] != dexItemFactory.stringType) {
return false;
}
} else {
// Methods whose first argument is of java.lang.Class type.
if (arity != 2 && arity != 3) {
return false;
}
if (arity == 2) {
if (method.proto.returnType != dexItemFactory.fieldType) {
return false;
}
} else {
if (method.proto.returnType != dexItemFactory.methodType) {
return false;
}
}
if (method.proto.parameters.values[0] != dexItemFactory.classType) {
return false;
}
if (method.proto.parameters.values[1] != dexItemFactory.stringType) {
return false;
}
if (arity == 3) {
if (method.proto.parameters.values[2] != dexItemFactory.classArrayType) {
return false;
}
}
}
return true;
}
/**
* Returns true if the given invoke instruction is calling `boolean java.lang.String.equals(
* java.lang.String)`, and one of the arguments is defined by an invoke-instruction that calls
* `java.lang.String java.lang.Class.getName()`.
*/
static boolean isClassNameComparison(InvokeMethod invoke, DexItemFactory dexItemFactory) {
return invoke.isInvokeVirtual()
&& isClassNameComparison(invoke.asInvokeVirtual(), dexItemFactory);
}
static boolean isClassNameComparison(InvokeVirtual invoke, DexItemFactory dexItemFactory) {
return invoke.getInvokedMethod() == dexItemFactory.stringMembers.equals
&& (isClassNameValue(invoke.getReceiver(), dexItemFactory)
|| isClassNameValue(invoke.inValues().get(1), dexItemFactory));
}
public static boolean isClassNameValue(Value value, DexItemFactory dexItemFactory) {
Value root = value.getAliasedValue();
if (!root.isDefinedByInstructionSatisfying(Instruction::isInvokeVirtual)) {
return false;
}
InvokeVirtual invoke = root.definition.asInvokeVirtual();
return dexItemFactory.classMethods.isReflectiveNameLookup(invoke.getInvokedMethod());
}
/**
* Returns a {@link DexReference} if one of the arguments to the invoke instruction is a constant
* string that corresponds to either a class or member name (i.e., an identifier).
*
* @param definitions {@link DexDefinitionSupplier} that gives access to {@link DexItemFactory}.
* @param invoke {@link InvokeMethod} that is expected to have an identifier in its arguments.
* @return {@link DexReference} corresponding to the first constant string argument that matches a
* class or member name, or {@code null} if no such constant was found.
*/
public static IdentifierNameStringLookupResult<?> identifyIdentifier(
InvokeMethod invoke, DexDefinitionSupplier definitions, ProgramMethod context) {
DexItemFactory dexItemFactory = definitions.dexItemFactory();
List<Value> ins = invoke.arguments();
// The only static calls: Class#forName,
// which receive either (String) or (String, boolean, ClassLoader) as ins.
if (invoke.isInvokeStatic()) {
InvokeStatic invokeStatic = invoke.asInvokeStatic();
if (dexItemFactory.classMethods.isReflectiveClassLookup(invokeStatic.getInvokedMethod())) {
return IdentifierNameStringLookupResult.fromClassForName(
ConstantValueUtils.getDexTypeFromClassForName(invokeStatic, definitions));
}
}
if (invoke.isInvokeVirtual()) {
InvokeVirtual invokeVirtual = invoke.asInvokeVirtual();
if (isClassNameComparison(invokeVirtual, dexItemFactory)) {
int argumentIndex = getPositionOfFirstConstString(invokeVirtual);
if (argumentIndex >= 0) {
return IdentifierNameStringLookupResult.fromClassNameComparison(
inferTypeFromConstStringValue(
definitions, invokeVirtual.inValues().get(argumentIndex)));
}
}
}
// All the other cases receive either (Class, String) or (Class, String, Class[]) as ins.
if (ins.size() == 1) {
return null;
}
boolean isReferenceFieldUpdater =
invoke.getReturnType().descriptor == dexItemFactory.referenceFieldUpdaterDescriptor;
int positionOfIdentifier = isReferenceFieldUpdater ? 2 : 1;
Value in = ins.get(positionOfIdentifier);
if (in.isConstString()) {
Value classValue = ins.get(0);
if (!classValue.isConstClass()) {
return null;
}
DexType holderType = classValue.getConstInstruction().asConstClass().getValue();
if (holderType.isArrayType()) {
// None of the fields or methods of an array type will be renamed, since they are all
// declared in the library. Hence there is no need to handle this case.
return null;
}
DexClass holder = definitions.definitionFor(holderType, context);
if (holder == null) {
return null;
}
DexString dexString = in.getConstInstruction().asConstString().getValue();
int numOfParams = ins.size();
if (isReferenceFieldUpdater) {
Value fieldTypeValue = ins.get(1);
if (!fieldTypeValue.isConstClass()) {
return null;
}
DexType fieldType = fieldTypeValue.getConstInstruction().asConstClass().getValue();
return IdentifierNameStringLookupResult.fromUncategorized(
inferFieldInHolder(holder, dexString.toString(), fieldType));
}
if (numOfParams == 2) {
return IdentifierNameStringLookupResult.fromUncategorized(
inferFieldInHolder(holder, dexString.toString(), null));
}
assert numOfParams == 3;
DexTypeList arguments = retrieveDexTypeListFromClassList(invoke, ins.get(2), dexItemFactory);
if (arguments == null) {
return null;
}
return IdentifierNameStringLookupResult.fromUncategorized(
inferMethodInHolder(holder, dexString.toString(), arguments));
}
if (in.isDexItemBasedConstString()) {
DexItemBasedConstString constString = in.getConstInstruction().asDexItemBasedConstString();
if (constString.getItem().isDexType()) {
return IdentifierNameStringLookupResult.fromDexTypeBasedConstString(
constString.getItem().asDexType());
}
return IdentifierNameStringLookupResult.fromDexMemberBasedConstString(
constString.getItem().asDexMember());
}
return null;
}
static int getPositionOfFirstConstString(Instruction instruction) {
List<Value> inValues = instruction.inValues();
for (int i = 0; i < inValues.size(); i++) {
Value value = inValues.get(i).getAliasedValue();
if (value.isConstString() || value.isDexItemBasedConstString()) {
return i;
}
}
return -1;
}
static DexReference inferMemberOrTypeFromNameString(
AppView<AppInfoWithLiveness> appView, DexString dexString) {
// "fully.qualified.ClassName.fieldOrMethodName"
// "fully.qualified.ClassName#fieldOrMethodName"
DexMember<?, ?> itemBasedString = inferMemberFromNameString(appView, dexString);
if (itemBasedString == null) {
// "fully.qualified.ClassName"
return inferTypeFromNameString(appView, dexString);
}
return itemBasedString;
}
public static DexType inferTypeFromNameString(
DexDefinitionSupplier definitions, DexString dexString) {
String maybeDescriptor = javaTypeToDescriptorIfValidJavaType(dexString.toString());
if (maybeDescriptor != null) {
return definitions.dexItemFactory().createType(maybeDescriptor);
}
return null;
}
public static DexType inferTypeFromConstStringValue(
DexDefinitionSupplier definitions, Value value) {
Value root = value.getAliasedValue();
assert !root.isPhi();
assert root.isConstString() || root.isDexItemBasedConstString();
if (root.isConstString()) {
return inferTypeFromNameString(definitions, root.definition.asConstString().getValue());
}
if (root.isDexItemBasedConstString()) {
DexReference reference = root.definition.asDexItemBasedConstString().getItem();
if (reference.isDexType()) {
return reference.asDexType();
}
}
return null;
}
private static DexMember<?, ?> inferMemberFromNameString(
AppView<AppInfoWithLiveness> appView, DexString dexString) {
String identifier = dexString.toString();
String typeIdentifier = null;
String memberIdentifier = null;
String[] items = identifier.split("#");
// "x#y#z"
if (items.length > 2) {
return null;
}
// "fully.qualified.ClassName#fieldOrMethodName"
if (items.length == 2) {
typeIdentifier = items[0];
memberIdentifier = items[1];
} else {
int lastDot = identifier.lastIndexOf(".");
// "fully.qualified.ClassName.fieldOrMethodName"
if (0 < lastDot && lastDot < identifier.length() - 1) {
typeIdentifier = identifier.substring(0, lastDot);
memberIdentifier = identifier.substring(lastDot + 1);
}
}
if (typeIdentifier == null) {
return null;
}
String maybeDescriptor = javaTypeToDescriptorIfValidJavaType(typeIdentifier);
if (maybeDescriptor == null) {
return null;
}
DexType type = appView.dexItemFactory().createType(maybeDescriptor);
// TODO(b/150736225): Should we move the identification of identifiers into the initial tracing?
DexClass holder = appView.appInfo().definitionForWithoutExistenceAssert(type);
if (holder == null) {
return null;
}
DexMember<?, ?> itemBasedString = inferFieldInHolder(holder, memberIdentifier, null);
if (itemBasedString == null) {
itemBasedString = inferMethodNameInHolder(holder, memberIdentifier);
}
return itemBasedString;
}
private static DexField inferFieldInHolder(DexClass holder, String name, DexType fieldType) {
for (DexEncodedField encodedField : holder.fields()) {
if (encodedField.getReference().name.toString().equals(name)
&& (fieldType == null || encodedField.getReference().type == fieldType)) {
return encodedField.getReference();
}
}
return null;
}
private static DexMethod inferMethodNameInHolder(DexClass holder, String name) {
for (DexEncodedMethod encodedMethod : holder.methods()) {
if (encodedMethod.getReference().name.toString().equals(name)) {
return encodedMethod.getReference();
}
}
return null;
}
private static DexMethod inferMethodInHolder(
DexClass holder, String name, DexTypeList arguments) {
assert arguments != null;
for (DexEncodedMethod encodedMethod : holder.methods()) {
if (encodedMethod.getReference().name.toString().equals(name)
&& encodedMethod.getReference().proto.parameters.equals(arguments)) {
return encodedMethod.getReference();
}
}
return null;
}
private static DexType getTypeFromConstClassOrBoxedPrimitive(
Value value, DexItemFactory factory) {
if (value.isPhi()) {
return null;
}
if (value.isConstant() && value.getConstInstruction().isConstClass()) {
return value.getConstInstruction().asConstClass().getValue();
}
if (value.definition.isStaticGet()) {
return factory.primitiveTypesBoxedTypeFields.boxedFieldTypeToPrimitiveType(
value.definition.asStaticGet().getField());
}
return null;
}
// Perform a conservative evaluation of an array content of dex type values from its construction
// until its use at a given instruction.
private static DexType[] evaluateTypeArrayContentFromConstructionToUse(
NewArrayEmpty newArray,
List<CheckCast> aliases,
int size,
Instruction user,
DexItemFactory factory) {
DexType[] values = new DexType[size];
int remaining = size;
Set<Instruction> users = Sets.newIdentityHashSet();
users.addAll(newArray.outValue().uniqueUsers());
for (CheckCast alias : aliases) {
users.addAll(alias.outValue().uniqueUsers());
}
// Follow the path from the array construction to the requested use collecting the constants
// put into the array. Conservatively bail out if the content of the array cannot be statically
// computed.
BasicBlock block = newArray.getBlock();
InstructionIterator iterator = block.iterator();
iterator.nextUntil(i -> i == newArray);
do {
while (iterator.hasNext()) {
Instruction instruction = iterator.next();
// Ignore instructions which do not use the array.
if (!users.contains(instruction)) {
continue;
}
if (instruction == user) {
// Return the array content if all elements are known when hitting the user for which
// the content was requested.
return remaining == 0 ? values : null;
}
// Any other kinds of use besides array-put mean that the array escapes and its content
// could be altered.
if (!instruction.isArrayPut()) {
if (instruction.isCheckCast() && aliases.contains(instruction.asCheckCast())) {
continue;
}
values = new DexType[size];
remaining = size;
continue;
}
ArrayPut arrayPut = instruction.asArrayPut();
if (!arrayPut.index().isConstNumber()) {
return null;
}
int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
if (index < 0 || index >= values.length) {
return null;
}
DexType type = getTypeFromConstClassOrBoxedPrimitive(arrayPut.value(), factory);
if (type == null) {
return null;
}
// Allow several writes to the same array element.
if (values[index] == null) {
remaining--;
}
values[index] = type;
}
if (!block.exit().isGoto()) {
return null;
}
block = block.exit().asGoto().getTarget();
// Don't allow any other control flow into the sequence of blocks filling the array from
// construction to requested use. This will also includes loopback and guarantee that
// this will terminate without marking visited blocks.
if (block.getPredecessors().size() != 1) {
return null;
}
iterator = block.iterator();
} while (iterator != null);
return null;
}
/**
* Visits all {@link ArrayPut}'s with the given {@param classListValue} as array and {@link Class}
* as value. Then collects all corresponding {@link DexType}s so as to determine reflective cases.
*
* @param invoke the instruction that invokes a reflective method with -identifiernamestring rule
* @param classListValue the register that holds an array of {@link Class}'s
* @return a list of {@link DexType} that corresponds to const class in {@param classListValue}
*/
private static DexTypeList retrieveDexTypeListFromClassList(
InvokeMethod invoke, Value classListValue, DexItemFactory factory) {
// The code
// A.class.getMethod("m", String.class, String.class)
// results in the following Java byte code from javac:
//
// LDC LA;.class
// LDC "m"
// ICONST_2
// ANEWARRAY java/lang/Class
// DUP
// ICONST_0
// LDC Ljava/lang/String;.class
// AASTORE
// DUP
// ICONST_1
// LDC Ljava/lang/String;.class
// AASTORE
// INVOKEVIRTUAL java/lang/Class.getMethod \
// (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
// Besides the code pattern above this supports a series of check-cast instructions. e.g.:
//
// A.class.getMethod("name", (Class<?>[]) new Class<?>[]{String.class})
List<CheckCast> aliases = new ArrayList<>();
if (!classListValue.isPhi()
&& classListValue.definition.isCheckCast()
&& classListValue.definition.asCheckCast().getType() == factory.classArrayType) {
while (!classListValue.isPhi() && classListValue.definition.isCheckCast()) {
aliases.add(classListValue.definition.asCheckCast());
classListValue = classListValue.definition.asCheckCast().object();
}
}
if (classListValue.isPhi()) {
return null;
}
// A null argument list is an empty argument list
if (classListValue.isZero()) {
return DexTypeList.empty();
}
// Make sure this Value refers to a new array.
if (!classListValue.definition.isNewArrayEmpty()
|| !classListValue.definition.asNewArrayEmpty().size().isConstant()) {
return null;
}
int size =
classListValue
.definition
.asNewArrayEmpty()
.size()
.getConstInstruction()
.asConstNumber()
.getIntValue();
if (size == 0) {
return DexTypeList.empty();
}
DexType[] arrayContent =
evaluateTypeArrayContentFromConstructionToUse(
classListValue.definition.asNewArrayEmpty(), aliases, size, invoke, factory);
if (arrayContent == null) {
return null;
}
return new DexTypeList(arrayContent);
}
}