| // 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.utils.codeinspector; |
| |
| import static com.android.tools.r8.KotlinTestBase.METADATA_TYPE; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import com.android.tools.r8.TestRuntime.CfRuntime; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.ToolHelper.ProcessResult; |
| import com.android.tools.r8.graph.ClassAccessFlags; |
| import com.android.tools.r8.graph.DexAnnotation; |
| import com.android.tools.r8.graph.DexClass; |
| 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.DexMethod; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexProto; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.kotlin.KotlinClassMetadataReader; |
| import com.android.tools.r8.naming.ClassNamingForNameMapper; |
| import com.android.tools.r8.naming.MemberNaming; |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.naming.MemberNaming.Signature; |
| import com.android.tools.r8.naming.mappinginformation.MappingInformation; |
| import com.android.tools.r8.naming.signature.GenericSignatureParser; |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.references.FieldReference; |
| import com.android.tools.r8.references.Reference; |
| import com.android.tools.r8.references.TypeReference; |
| import com.android.tools.r8.retrace.RetraceClassElement; |
| import com.android.tools.r8.retrace.RetraceClassResult; |
| import com.android.tools.r8.retrace.RetraceTypeResult; |
| import com.android.tools.r8.retrace.RetracedFieldReference; |
| import com.android.tools.r8.retrace.Retracer; |
| import com.android.tools.r8.synthesis.SyntheticItemsTestUtils; |
| import com.android.tools.r8.utils.DescriptorUtils; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.android.tools.r8.utils.ZipUtils; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector.MappingWrapper; |
| import com.google.common.collect.Sets; |
| import java.io.File; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| import kotlinx.metadata.jvm.KotlinClassMetadata; |
| import org.junit.rules.TemporaryFolder; |
| |
| public class FoundClassSubject extends ClassSubject { |
| |
| private final DexClass dexClass; |
| private final MappingWrapper mapping; |
| |
| FoundClassSubject( |
| CodeInspector codeInspector, |
| DexClass dexClass, |
| MappingWrapper mapping, |
| ClassReference reference) { |
| super(codeInspector, reference); |
| this.dexClass = dexClass; |
| this.mapping = mapping; |
| } |
| |
| @Override |
| public boolean isPresent() { |
| return true; |
| } |
| |
| @Override |
| public void forAllMethods(Consumer<FoundMethodSubject> inspection) { |
| CodeInspector.forAll( |
| dexClass.directMethods(), |
| (encoded, clazz) -> new FoundMethodSubject(codeInspector, encoded, clazz), |
| this, |
| inspection); |
| forAllVirtualMethods(inspection); |
| } |
| |
| @Override |
| public void forAllVirtualMethods(Consumer<FoundMethodSubject> inspection) { |
| CodeInspector.forAll( |
| dexClass.virtualMethods(), |
| (encoded, clazz) -> new FoundMethodSubject(codeInspector, encoded, clazz), |
| this, |
| inspection); |
| } |
| |
| @Override |
| public MethodSubject method(String returnType, String name, List<String> parameters) { |
| DexType[] parameterTypes = new DexType[parameters.size()]; |
| for (int i = 0; i < parameters.size(); i++) { |
| parameterTypes[i] = |
| codeInspector.toDexType(codeInspector.getObfuscatedTypeName(parameters.get(i))); |
| } |
| DexProto proto = |
| codeInspector.dexItemFactory.createProto( |
| codeInspector.toDexType(codeInspector.getObfuscatedTypeName(returnType)), parameterTypes); |
| if (getNaming() != null) { |
| Signature signature = |
| new MethodSignature(name, returnType, parameters.toArray(StringUtils.EMPTY_ARRAY)); |
| MemberNaming methodNaming = getNaming().lookupByOriginalSignature(signature); |
| if (methodNaming != null) { |
| name = methodNaming.getRenamedName(); |
| } |
| } |
| DexMethod dexMethod = |
| codeInspector.dexItemFactory.createMethod( |
| dexClass.type, proto, codeInspector.dexItemFactory.createString(name)); |
| DexEncodedMethod encoded = findMethod(dexClass.directMethods(), dexMethod); |
| if (encoded == null) { |
| encoded = findMethod(dexClass.virtualMethods(), dexMethod); |
| } |
| return encoded == null |
| ? new AbsentMethodSubject() |
| : new FoundMethodSubject(codeInspector, encoded, this); |
| } |
| |
| private DexEncodedMethod findMethod(Iterable<DexEncodedMethod> methods, DexMethod dexMethod) { |
| for (DexEncodedMethod method : methods) { |
| if (method.getReference().equals(dexMethod)) { |
| return method; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public MethodSubject uniqueMethodThatMatches(Predicate<FoundMethodSubject> predicate) { |
| MethodSubject methodSubject = null; |
| for (FoundMethodSubject candidate : allMethods(predicate)) { |
| assert methodSubject == null; |
| methodSubject = candidate; |
| } |
| return methodSubject != null ? methodSubject : new AbsentMethodSubject(); |
| } |
| |
| @Override |
| public MethodSubject uniqueMethodWithName(String name) { |
| MethodSubject methodSubject = null; |
| for (FoundMethodSubject candidate : allMethods()) { |
| if (candidate.getOriginalName(false).equals(name)) { |
| assert methodSubject == null; |
| methodSubject = candidate; |
| } |
| } |
| return methodSubject != null ? methodSubject : new AbsentMethodSubject(); |
| } |
| |
| @Override |
| public MethodSubject uniqueMethodWithFinalName(String name) { |
| MethodSubject methodSubject = null; |
| for (FoundMethodSubject candidate : allMethods()) { |
| if (candidate.getFinalName().equals(name)) { |
| assert methodSubject == null; |
| methodSubject = candidate; |
| } |
| } |
| return methodSubject != null ? methodSubject : new AbsentMethodSubject(); |
| } |
| |
| @Override |
| public void forAllFields(Consumer<FoundFieldSubject> inspection) { |
| forAllInstanceFields(inspection); |
| forAllStaticFields(inspection); |
| } |
| |
| @Override |
| public void forAllInstanceFields(Consumer<FoundFieldSubject> inspection) { |
| CodeInspector.forAll( |
| dexClass.instanceFields(), |
| (dexField, clazz) -> new FoundFieldSubject(codeInspector, dexField, clazz), |
| this, |
| inspection); |
| } |
| |
| @Override |
| public void forAllStaticFields(Consumer<FoundFieldSubject> inspection) { |
| CodeInspector.forAll( |
| dexClass.staticFields(), |
| (dexField, clazz) -> new FoundFieldSubject(codeInspector, dexField, clazz), |
| this, |
| inspection); |
| } |
| |
| @Override |
| public FieldSubject field(String type, String name) { |
| String obfuscatedType = codeInspector.getObfuscatedTypeName(type); |
| MemberNaming fieldNaming = null; |
| if (getNaming() != null) { |
| fieldNaming = getNaming().lookupByOriginalSignature(new FieldSignature(name, type)); |
| } |
| String obfuscatedName = fieldNaming == null ? name : fieldNaming.getRenamedName(); |
| |
| DexField field = |
| codeInspector.dexItemFactory.createField( |
| dexClass.type, |
| codeInspector.toDexType(obfuscatedType), |
| codeInspector.dexItemFactory.createString(obfuscatedName)); |
| DexEncodedField encoded = findField(dexClass.staticFields(), field); |
| if (encoded == null) { |
| encoded = findField(dexClass.instanceFields(), field); |
| } |
| return encoded == null |
| ? new AbsentFieldSubject() |
| : new FoundFieldSubject(codeInspector, encoded, this); |
| } |
| |
| @Override |
| public FieldSubject uniqueFieldWithName(String name) { |
| return uniqueFieldWithName(name, null); |
| } |
| |
| // TODO(b/169882658): This should be removed when we have identity mappings for ambiguous cases. |
| public FieldSubject uniqueFieldWithName(String name, TypeReference originalType) { |
| Retracer retracer = codeInspector.retrace(); |
| Set<FoundFieldSubject> candidates = Sets.newIdentityHashSet(); |
| Set<FoundFieldSubject> sameTypeCandidates = Sets.newIdentityHashSet(); |
| for (FoundFieldSubject candidate : allFields()) { |
| FieldReference fieldReference = candidate.getDexField().asFieldReference(); |
| // TODO(b/169882658): This if should be removed completely. |
| if (candidate.getFinalName().equals(name)) { |
| candidates.add(candidate); |
| if (isNullOrEqual(originalType, fieldReference.getFieldType())) { |
| sameTypeCandidates.add(candidate); |
| } |
| } |
| retracer |
| .retraceField(fieldReference) |
| .forEach( |
| element -> { |
| RetracedFieldReference field = element.getField(); |
| if (!element.isUnknown() && field.getFieldName().equals(name)) { |
| candidates.add(candidate); |
| // TODO(b/169953605): There should not be a need for mapping the final type. |
| TypeReference fieldOriginalType = originalType; |
| if (fieldOriginalType == null) { |
| RetraceTypeResult retraceTypeResult = |
| retracer.retraceType(fieldReference.getFieldType()); |
| assert !retraceTypeResult.isAmbiguous(); |
| fieldOriginalType = |
| retraceTypeResult.stream().iterator().next().getType().getTypeReference(); |
| } |
| if (isNullOrEqual(fieldOriginalType, field.asKnown().getFieldType())) { |
| sameTypeCandidates.add(candidate); |
| } |
| } |
| }); |
| } |
| assert candidates.size() >= sameTypeCandidates.size(); |
| // If we have any merged types we cannot rely on sameTypeCandidates, so we look in all |
| // candidates first. |
| if (candidates.size() == 1) { |
| return candidates.iterator().next(); |
| } |
| return sameTypeCandidates.size() == 1 |
| ? sameTypeCandidates.iterator().next() |
| : new AbsentFieldSubject(); |
| } |
| |
| private boolean isNullOrEqual(TypeReference original, TypeReference rewritten) { |
| return original == null || original.equals(rewritten); |
| } |
| |
| @Override |
| public FieldSubject uniqueFieldWithFinalName(String name) { |
| FieldSubject fieldSubject = null; |
| for (FoundFieldSubject candidate : allFields()) { |
| if (candidate.getFinalName().equals(name)) { |
| assert fieldSubject == null; |
| fieldSubject = candidate; |
| } |
| } |
| return fieldSubject != null ? fieldSubject : new AbsentFieldSubject(); |
| } |
| |
| @Override |
| public FoundClassSubject asFoundClassSubject() { |
| return this; |
| } |
| |
| @Override |
| public boolean isAbstract() { |
| return dexClass.accessFlags.isAbstract(); |
| } |
| |
| @Override |
| public boolean isImplementing(ClassSubject subject) { |
| assertTrue(subject.isPresent()); |
| for (DexType itf : getDexProgramClass().interfaces) { |
| if (itf.toSourceString().equals(subject.getFinalName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean isImplementing(Class<?> clazz) { |
| return isImplementing(clazz.getTypeName()); |
| } |
| |
| @Override |
| public boolean isImplementing(String javaTypeName) { |
| for (DexType itf : getDexProgramClass().interfaces) { |
| if (itf.toSourceString().equals(javaTypeName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean isAnnotation() { |
| return dexClass.accessFlags.isAnnotation(); |
| } |
| |
| private DexEncodedField findField(List<DexEncodedField> fields, DexField dexField) { |
| for (DexEncodedField field : fields) { |
| if (field.getReference().equals(dexField)) { |
| return field; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public DexProgramClass getDexProgramClass() { |
| assert dexClass.isProgramClass(); |
| return dexClass.asProgramClass(); |
| } |
| |
| public ClassSubject getSuperClass() { |
| return codeInspector.clazz(dexClass.superType.toSourceString()); |
| } |
| |
| @Override |
| public AnnotationSubject annotation(String name) { |
| // Ensure we don't check for annotations represented as attributes. |
| assert !name.endsWith("EnclosingClass") |
| && !name.endsWith("EnclosingMethod") |
| && !name.endsWith("InnerClass"); |
| DexAnnotation annotation = codeInspector.findAnnotation(name, dexClass.annotations()); |
| return annotation == null |
| ? new AbsentAnnotationSubject() |
| : new FoundAnnotationSubject(annotation); |
| } |
| |
| @Override |
| public ClassAccessFlags getAccessFlags() { |
| return getDexProgramClass().getAccessFlags(); |
| } |
| |
| @Override |
| public String getOriginalName() { |
| if (getNaming() != null) { |
| return getNaming().originalName; |
| } else { |
| return getFinalName(); |
| } |
| } |
| |
| @Override |
| public String getOriginalDescriptor() { |
| if (getNaming() != null) { |
| return DescriptorUtils.javaTypeToDescriptor(getNaming().originalName); |
| } else { |
| return getFinalDescriptor(); |
| } |
| } |
| |
| @Override |
| public String getOriginalBinaryName() { |
| return DescriptorUtils.getBinaryNameFromDescriptor(getOriginalDescriptor()); |
| } |
| |
| public DexType getOriginalDexType(DexItemFactory dexItemFactory) { |
| return dexItemFactory.createType(getOriginalDescriptor()); |
| } |
| |
| @Override |
| public ClassReference getOriginalReference() { |
| return Reference.classFromDescriptor(getOriginalDescriptor()); |
| } |
| |
| @Override |
| public ClassReference getFinalReference() { |
| return Reference.classFromDescriptor(getFinalDescriptor()); |
| } |
| |
| @Override |
| public String getFinalName() { |
| return DescriptorUtils.descriptorToJavaType(getFinalDescriptor()); |
| } |
| |
| @Override |
| public String getFinalDescriptor() { |
| return dexClass.type.descriptor.toString(); |
| } |
| |
| @Override |
| public String getFinalBinaryName() { |
| return DescriptorUtils.getBinaryNameFromDescriptor(getFinalDescriptor()); |
| } |
| |
| @Override |
| public boolean isRenamed() { |
| return getNaming() != null && !getFinalDescriptor().equals(getOriginalDescriptor()); |
| } |
| |
| @Override |
| public boolean isCompilerSynthesized() { |
| for (MappingInformation info : mapping.getAdditionalMappings()) { |
| if (info.isCompilerSynthesizedMappingInformation()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean isLocalClass() { |
| return dexClass.isLocalClass(); |
| } |
| |
| @Override |
| public boolean isMemberClass() { |
| return dexClass.isMemberClass(); |
| } |
| |
| @Override |
| public boolean isAnonymousClass() { |
| return dexClass.isAnonymousClass(); |
| } |
| |
| @Override |
| public boolean isSynthesizedJavaLambdaClass() { |
| // TODO(141287349): Make this precise based on the map input. |
| return SyntheticItemsTestUtils.isExternalLambda(getOriginalReference()) |
| || SyntheticItemsTestUtils.isExternalLambda(getFinalReference()); |
| } |
| |
| @Override |
| public DexMethod getFinalEnclosingMethod() { |
| return dexClass.getEnclosingMethodAttribute().getEnclosingMethod(); |
| } |
| |
| @Override |
| public String getOriginalSignatureAttribute() { |
| return codeInspector.getOriginalSignatureAttribute( |
| dexClass.getClassSignature().toString(), GenericSignatureParser::parseClassSignature); |
| } |
| |
| @Override |
| public String getFinalSignatureAttribute() { |
| return dexClass.getClassSignature().toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = codeInspector.hashCode(); |
| result = 31 * result + dexClass.hashCode(); |
| result = 31 * result + (getNaming() != null ? getNaming().hashCode() : 0); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other == null || other.getClass() != this.getClass()) { |
| return false; |
| } |
| FoundClassSubject otherSubject = (FoundClassSubject) other; |
| return codeInspector == otherSubject.codeInspector |
| && dexClass == otherSubject.dexClass |
| && getNaming() == otherSubject.getNaming(); |
| } |
| |
| @Override |
| public String toString() { |
| return dexClass.toSourceString(); |
| } |
| |
| public TypeSubject asTypeSubject() { |
| return new TypeSubject(codeInspector, getDexProgramClass().type); |
| } |
| |
| @Override |
| public KmClassSubject getKmClass() { |
| AnnotationSubject annotationSubject = annotation(METADATA_TYPE); |
| if (!annotationSubject.isPresent()) { |
| return new AbsentKmClassSubject(); |
| } |
| KotlinClassMetadata metadata = |
| KotlinClassMetadataReader.toKotlinClassMetadata( |
| codeInspector.getFactory().kotlin, annotationSubject.getAnnotation()); |
| assertTrue(metadata instanceof KotlinClassMetadata.Class); |
| KotlinClassMetadata.Class kClass = (KotlinClassMetadata.Class) metadata; |
| return new FoundKmClassSubject(codeInspector, getDexProgramClass(), kClass.toKmClass()); |
| } |
| |
| @Override |
| public KmPackageSubject getKmPackage() { |
| AnnotationSubject annotationSubject = annotation(METADATA_TYPE); |
| if (!annotationSubject.isPresent()) { |
| return new AbsentKmPackageSubject(); |
| } |
| KotlinClassMetadata metadata = |
| KotlinClassMetadataReader.toKotlinClassMetadata( |
| codeInspector.getFactory().kotlin, annotationSubject.getAnnotation()); |
| assertTrue(metadata instanceof KotlinClassMetadata.FileFacade |
| || metadata instanceof KotlinClassMetadata.MultiFileClassPart); |
| if (metadata instanceof KotlinClassMetadata.FileFacade) { |
| KotlinClassMetadata.FileFacade kFile = (KotlinClassMetadata.FileFacade) metadata; |
| return new FoundKmPackageSubject(codeInspector, getDexProgramClass(), kFile.toKmPackage()); |
| } else { |
| KotlinClassMetadata.MultiFileClassPart kPart = |
| (KotlinClassMetadata.MultiFileClassPart) metadata; |
| return new FoundKmPackageSubject(codeInspector, getDexProgramClass(), kPart.toKmPackage()); |
| } |
| } |
| |
| @Override |
| public KotlinClassMetadata getKotlinClassMetadata() { |
| AnnotationSubject annotationSubject = annotation(METADATA_TYPE); |
| if (!annotationSubject.isPresent()) { |
| return null; |
| } |
| return KotlinClassMetadataReader.toKotlinClassMetadata( |
| codeInspector.getFactory().kotlin, annotationSubject.getAnnotation()); |
| } |
| |
| public RetraceClassResult retrace() { |
| assertTrue(mapping.getNaming() != null); |
| return codeInspector |
| .getRetracer() |
| .retraceClass(Reference.classFromTypeName(mapping.getNaming().renamedName)); |
| } |
| |
| public RetraceClassElement retraceUnique() { |
| RetraceClassResult result = retrace(); |
| if (result.isAmbiguous()) { |
| fail("Expected unique retrace of " + this + ", got ambiguous: " + result); |
| } |
| Optional<RetraceClassElement> first = result.stream().findFirst(); |
| if (!first.isPresent()) { |
| fail("Expected unique retrace of " + this + ", got empty result"); |
| } |
| return first.get(); |
| } |
| |
| @Override |
| public ClassNamingForNameMapper getNaming() { |
| return mapping.getNaming(); |
| } |
| |
| @Override |
| public String disassembleUsingJavap(boolean verbose) throws Exception { |
| assert dexClass.origin != null; |
| List<String> command = new ArrayList<>(); |
| command.add( |
| CfRuntime.getCheckedInJdk9().getJavaHome().resolve("bin").resolve("javap").toString()); |
| if (verbose) { |
| command.add("-v"); |
| command.add("-c"); |
| command.add("-p"); |
| } |
| command.add("-cp"); |
| List<String> parts = dexClass.origin.parts(); |
| assert parts.size() == 2; |
| command.add(parts.get(0)); |
| command.add(parts.get(1).replace(".class", "")); |
| ProcessResult processResult = ToolHelper.runProcess(new ProcessBuilder(command)); |
| assert processResult.exitCode == 0; |
| System.out.println(processResult.stdout); |
| return processResult.stdout; |
| } |
| |
| @Override |
| public String asmify(TemporaryFolder tempFolder, boolean debug) throws Exception { |
| assert dexClass.origin != null; |
| List<String> parts = dexClass.origin.parts(); |
| assert parts.size() == 2; |
| String directory = parts.get(0); |
| String fileName = parts.get(1); |
| if (directory.endsWith(".jar") || directory.endsWith(".zip")) { |
| File tempOut = tempFolder.newFolder(); |
| ZipUtils.unzip(directory, tempOut); |
| directory = tempOut.getAbsolutePath(); |
| } |
| List<String> command = new ArrayList<>(); |
| command.add( |
| CfRuntime.getCheckedInJdk9().getJavaHome().resolve("bin").resolve("java").toString()); |
| command.add("-cp"); |
| command.add(ToolHelper.ASM_JAR + ":" + ToolHelper.ASM_UTIL_JAR); |
| command.add("org.objectweb.asm.util.ASMifier"); |
| if (!debug) { |
| command.add("-debug"); |
| } |
| command.add(Paths.get(directory, fileName).toString()); |
| ProcessResult processResult = ToolHelper.runProcess(new ProcessBuilder(command)); |
| assert processResult.exitCode == 0; |
| System.out.println(processResult.stdout); |
| return processResult.stdout; |
| } |
| } |