Merge "Reland "Add library licenses for kotlinx-metadata-jvm and its dependencies.""
diff --git a/build.gradle b/build.gradle
index 7720cf3..7f41caa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,10 @@
'-Xep:MissingDefault:ERROR',
'-Xep:MultipleTopLevelClasses:ERROR',
'-Xep:NarrowingCompoundAssignment:ERROR',
- '-Xep:BoxedPrimitiveConstructor:ERROR']
+ '-Xep:BoxedPrimitiveConstructor:ERROR',
+ '-Xep:LogicalAssignment:ERROR',
+ '-Xep:FloatCast:ERROR',
+ '-Xep:ReturnValueIgnored:ERROR']
apply from: 'copyAdditionalJctfCommonFiles.gradle'
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
index b3d59fb..a6389da 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -12,15 +12,15 @@
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
import com.google.common.io.ByteStreams;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
@@ -60,7 +60,7 @@
assert isArchive(archive);
origin = new PathOrigin(archive);
try {
- zipFile = new ZipFile(archive.toFile());
+ zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8);
} catch (IOException e) {
if (!Files.exists(archive)) {
throw new NoSuchFileException(archive.toString());
@@ -72,7 +72,7 @@
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
- if (FileUtils.isClassFile(Paths.get(name)) && include.test(name)) {
+ if (ZipUtils.isClassFile(name) && include.test(name)) {
descriptors.add(DescriptorUtils.guessTypeDescriptor(name));
}
}
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
index baac9bc..1d6e678 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -3,21 +3,18 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
-
import com.android.tools.r8.ProgramResource.Kind;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.origin.ArchiveEntryOrigin;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ZipUtils;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -38,16 +35,15 @@
}
public static boolean includeClassFileEntries(String entry) {
- return isClassFile(Paths.get(entry));
+ return ZipUtils.isClassFile(entry);
}
public static boolean includeDexEntries(String entry) {
- return isDexFile(Paths.get(entry));
+ return ZipUtils.isDexFile(entry);
}
public static boolean includeClassFileOrDexEntries(String entry) {
- Path path = Paths.get(entry);
- return isClassFile(path) || isDexFile(path);
+ return ZipUtils.isClassFile(entry) || ZipUtils.isDexFile(entry);
}
private final Origin origin;
@@ -60,7 +56,10 @@
public static ArchiveProgramResourceProvider fromArchive(
Path archive, Predicate<String> include) {
- return fromSupplier(new PathOrigin(archive), () -> new ZipFile(archive.toFile()), include);
+ return fromSupplier(
+ new PathOrigin(archive),
+ () -> new ZipFile(archive.toFile(), StandardCharsets.UTF_8),
+ include);
}
public static ArchiveProgramResourceProvider fromSupplier(
@@ -93,14 +92,13 @@
ZipEntry entry = entries.nextElement();
try (InputStream stream = zipFile.getInputStream(entry)) {
String name = entry.getName();
- Path path = Paths.get(name);
- Origin entryOrigin = new ArchiveEntryOrigin(entry.getName(), origin);
+ Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
if (include.test(name)) {
- if (FileUtils.isDexFile(path)) {
+ if (ZipUtils.isDexFile(name)) {
dexResources.add(
ProgramResource.fromBytes(
entryOrigin, Kind.DEX, ByteStreams.toByteArray(stream), null));
- } else if (isClassFile(path)) {
+ } else if (ZipUtils.isClassFile(name)) {
String descriptor = DescriptorUtils.guessTypeDescriptor(name);
classResources.add(
ProgramResource.fromBytes(
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index 9e5d36a..93c0126 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -3,16 +3,79 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.FlagFile;
import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
public class D8CommandParser extends BaseCompilerCommandParser {
+ static class OrderedClassFileResourceProvider implements ClassFileResourceProvider {
+ static class Builder {
+ private final ImmutableList.Builder<ClassFileResourceProvider> builder =
+ ImmutableList.builder();
+ boolean empty = true;
+
+ OrderedClassFileResourceProvider build() {
+ return new OrderedClassFileResourceProvider(builder.build());
+ }
+
+ Builder addClassFileResourceProvider(ClassFileResourceProvider provider) {
+ builder.add(provider);
+ empty = false;
+ return this;
+ }
+
+ boolean isEmpty() {
+ return empty;
+ }
+ }
+
+ final List<ClassFileResourceProvider> providers;
+ final Set<String> descriptors = Sets.newHashSet();
+
+ private OrderedClassFileResourceProvider(ImmutableList<ClassFileResourceProvider> providers) {
+ this.providers = providers;
+ // Collect all descriptors that can be provided.
+ this.providers.forEach(provider -> this.descriptors.addAll(provider.getClassDescriptors()));
+ }
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public Set<String> getClassDescriptors() {
+ return descriptors;
+ }
+
+ @Override
+ public ProgramResource getProgramResource(String descriptor) {
+ // Search the providers in order. Return the program resource from the first provider that
+ // can provide it.
+ for (ClassFileResourceProvider provider : providers) {
+ if (provider.getClassDescriptors().contains(descriptor)) {
+ return provider.getProgramResource(descriptor);
+ }
+ }
+ return null;
+ }
+ }
+
public static void main(String[] args) throws CompilationFailedException {
D8Command command = parse(args, Origin.root()).build();
if (command.isPrintHelp()) {
@@ -76,6 +139,8 @@
Path outputPath = null;
OutputMode outputMode = null;
boolean hasDefinedApiLevel = false;
+ OrderedClassFileResourceProvider.Builder classpathBuilder =
+ OrderedClassFileResourceProvider.builder();
String[] expandedArgs = FlagFile.expandFlagFiles(args, builder);
try {
for (int i = 0; i < expandedArgs.length; i++) {
@@ -115,7 +180,22 @@
} else if (arg.equals("--lib")) {
builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
} else if (arg.equals("--classpath")) {
- builder.addClasspathFiles(Paths.get(expandedArgs[++i]));
+ Path file = Paths.get(expandedArgs[++i]);
+ try {
+ if (!Files.exists(file)) {
+ throw new NoSuchFileException(file.toString());
+ }
+ if (isArchive(file)) {
+ classpathBuilder.addClassFileResourceProvider(new ArchiveClassFileProvider(file));
+ } else if (Files.isDirectory(file)) {
+ classpathBuilder.addClassFileResourceProvider(
+ DirectoryClassFileProvider.fromDirectory(file));
+ } else {
+ throw new CompilationError("Unsupported classpath file type", new PathOrigin(file));
+ }
+ } catch (IOException e) {
+ builder.error(new ExceptionDiagnostic(e, new PathOrigin(file)));
+ }
} else if (arg.equals("--main-dex-list")) {
builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
} else if (arg.equals("--optimize-multidex-for-linearalloc")) {
@@ -140,6 +220,9 @@
builder.addProgramFiles(Paths.get(arg));
}
}
+ if (!classpathBuilder.isEmpty()) {
+ builder.addClasspathResourceProvider(classpathBuilder.build());
+ }
if (compilationMode != null) {
builder.setMode(compilationMode);
}
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index 9b1599f..7b17ec8 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -14,7 +14,6 @@
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -33,6 +32,7 @@
private Path proguardMapFile = null;
private boolean useSmali = false;
private boolean allInfo = false;
+ private boolean useIr;
@Override
Builder self() {
@@ -63,6 +63,11 @@
return this;
}
+ public Builder setUseIr(boolean useIr) {
+ this.useIr = useIr;
+ return this;
+ }
+
@Override
protected DisassembleCommand makeCommand() {
// If printing versions ignore everything else.
@@ -74,24 +79,26 @@
getOutputPath(),
proguardMapFile == null ? null : StringResource.fromFile(proguardMapFile),
allInfo,
- useSmali);
+ useSmali,
+ useIr);
}
}
- static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of(
- "Usage: disasm [options] <input-files>",
- " where <input-files> are dex files",
- " and options are:",
- " --all # Include all information in disassembly.",
- " --smali # Disassemble using smali syntax.",
- " --pg-map <file> # Proguard map <file> for mapping names.",
- " --output # Specify a file or directory to write to.",
- " --version # Print the version of r8.",
- " --help # Print this message."));
-
+ static final String USAGE_MESSAGE =
+ "Usage: disasm [options] <input-files>\n"
+ + " where <input-files> are dex files\n"
+ + " and options are:\n"
+ + " --all # Include all information in disassembly.\n"
+ + " --smali # Disassemble using smali syntax.\n"
+ + " --ir # Print IR before and after optimization.\n"
+ + " --pg-map <file> # Proguard map <file> for mapping names.\n"
+ + " --output # Specify a file or directory to write to.\n"
+ + " --version # Print the version of r8.\n"
+ + " --help # Print this message.";
private final boolean allInfo;
private final boolean useSmali;
+ private final boolean useIr;
public static Builder builder() {
return new Builder();
@@ -112,10 +119,12 @@
builder.setPrintHelp(true);
} else if (arg.equals("--version")) {
builder.setPrintVersion(true);
- } else if (arg.equals("--all")) {
+ } else if (arg.equals("--all")) {
builder.setAllInfo(true);
} else if (arg.equals("--smali")) {
builder.setUseSmali(true);
+ } else if (arg.equals("--ir")) {
+ builder.setUseIr(true);
} else if (arg.equals("--pg-map")) {
builder.setProguardMapFile(Paths.get(args[++i]));
} else if (arg.equals("--output")) {
@@ -132,13 +141,18 @@
}
private DisassembleCommand(
- AndroidApp inputApp, Path outputPath, StringResource proguardMap,
- boolean allInfo, boolean useSmali) {
+ AndroidApp inputApp,
+ Path outputPath,
+ StringResource proguardMap,
+ boolean allInfo,
+ boolean useSmali,
+ boolean useIr) {
super(inputApp);
this.outputPath = outputPath;
this.proguardMap = proguardMap;
this.allInfo = allInfo;
this.useSmali = useSmali;
+ this.useIr = useIr;
}
private DisassembleCommand(boolean printHelp, boolean printVersion) {
@@ -147,6 +161,7 @@
proguardMap = null;
allInfo = false;
useSmali = false;
+ useIr = false;
}
public Path getOutputPath() {
@@ -157,6 +172,10 @@
return useSmali;
}
+ public boolean useIr() {
+ return useIr;
+ }
+
@Override
InternalOptions getInternalOptions() {
InternalOptions internal = new InternalOptions();
@@ -190,9 +209,10 @@
try {
DexApplication application =
new ApplicationReader(app, options, timing).read(command.proguardMap, executor);
- DexByteCodeWriter writer = command.useSmali()
- ? new SmaliWriter(application, options)
- : new AssemblyWriter(application, options, command.allInfo);
+ DexByteCodeWriter writer =
+ command.useSmali()
+ ? new SmaliWriter(application, options)
+ : new AssemblyWriter(application, options, command.allInfo, command.useIr());
if (command.getOutputPath() != null) {
writer.write(command.getOutputPath());
} else {
diff --git a/src/main/java/com/android/tools/r8/JarSizeCompare.java b/src/main/java/com/android/tools/r8/JarSizeCompare.java
new file mode 100644
index 0000000..aa16b1f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/JarSizeCompare.java
@@ -0,0 +1,482 @@
+// 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;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexApplication;
+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.DexProgramClass;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.naming.ClassNamingForNameMapper;
+import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * JarSizeCompare outputs the class, method, field sizes of the given JAR files. For each input, a
+ * ProGuard map can be passed that is used to resolve minified names.
+ *
+ * <p>By default, only shows methods where R8's DEX output is 5 or more instructions larger than
+ * ProGuard+D8's output. Pass {@code --threshold 0} to display all methods.
+ */
+public class JarSizeCompare {
+
+ private static final String USAGE =
+ "Arguments:\n"
+ + " [--threshold <threshold>]\n"
+ + " [--lib <lib.jar>]\n"
+ + " --input <name1> <input1.jar> [<map1.txt>]\n"
+ + " --input <name2> <input2.jar> [<map2.txt>] ...\n"
+ + "\n"
+ + "JarSizeCompare outputs the class, method, field sizes of the given JAR files.\n"
+ + "For each input, a ProGuard map can be passed that is used to resolve minified names.\n";
+
+ private static final ImmutableMap<String, String> R8_RELOCATIONS =
+ ImmutableMap.<String, String>builder()
+ .put("com.google.common", "com.android.tools.r8.com.google.common")
+ .put("com.google.gson", "com.android.tools.r8.com.google.gson")
+ .put("com.google.thirdparty", "com.android.tools.r8.com.google.thirdparty")
+ .put("joptsimple", "com.android.tools.r8.joptsimple")
+ .put("org.apache.commons", "com.android.tools.r8.org.apache.commons")
+ .put("org.objectweb.asm", "com.android.tools.r8.org.objectweb.asm")
+ .put("it.unimi.dsi.fastutil", "com.android.tools.r8.it.unimi.dsi.fastutil")
+ .build();
+
+ private final List<Path> libraries;
+ private final List<InputParameter> inputParameters;
+ private final int threshold;
+ private final InternalOptions options;
+ private int pgIndex;
+ private int r8Index;
+
+ private JarSizeCompare(
+ List<Path> libraries, List<InputParameter> inputParameters, int threshold) {
+ this.libraries = libraries;
+ this.inputParameters = inputParameters;
+ this.threshold = threshold;
+ options = new InternalOptions();
+ options.enableCfFrontend = true;
+ }
+
+ public void run() throws Exception {
+ List<String> names = new ArrayList<>();
+ List<InputApplication> inputApplicationList = new ArrayList<>();
+ Timing timing = new Timing("JarSizeCompare");
+ for (InputParameter inputParameter : inputParameters) {
+ AndroidApp inputApp = inputParameter.getInputApp(libraries);
+ DexApplication input = inputParameter.getReader(options, inputApp, timing);
+ AndroidAppConsumers appConsumer = new AndroidAppConsumers();
+ D8.run(
+ D8Command.builder(inputApp)
+ .setMinApiLevel(AndroidApiLevel.P.getLevel())
+ .setProgramConsumer(appConsumer.wrapDexIndexedConsumer(null))
+ .build());
+ DexApplication d8Input = inputParameter.getReader(options, appConsumer.build(), timing);
+ InputApplication inputApplication =
+ new InputApplication(input, translateClassNames(input, input.classes()));
+ InputApplication d8Classes =
+ new InputApplication(input, translateClassNames(input, d8Input.classes()));
+ names.add(inputParameter.name + "-input");
+ inputApplicationList.add(inputApplication);
+ names.add(inputParameter.name + "-d8");
+ inputApplicationList.add(d8Classes);
+ }
+ if (threshold != 0) {
+ pgIndex = names.indexOf("pg-d8");
+ r8Index = names.indexOf("r8-d8");
+ }
+ Map<String, InputClass[]> inputClasses = new HashMap<>();
+ for (int i = 0; i < names.size(); i++) {
+ InputApplication classes = inputApplicationList.get(i);
+ for (String className : classes.getClasses()) {
+ inputClasses.computeIfAbsent(className, k -> new InputClass[names.size()])[i] =
+ classes.getInputClass(className);
+ }
+ }
+ for (Entry<String, Map<String, InputClass[]>> library : byLibrary(inputClasses)) {
+ System.out.println("");
+ System.out.println(Strings.repeat("=", 100));
+ String commonPrefix = getCommonPrefix(library.getValue().keySet());
+ if (library.getKey().isEmpty()) {
+ System.out.println("PROGRAM (" + commonPrefix + ")");
+ } else {
+ System.out.println("LIBRARY: " + library.getKey() + " (" + commonPrefix + ")");
+ }
+ printLibrary(library.getValue(), commonPrefix);
+ }
+ }
+
+ private Map<String, DexProgramClass> translateClassNames(
+ DexApplication input, List<DexProgramClass> classes) {
+ Map<String, DexProgramClass> result = new HashMap<>();
+ ClassNameMapper classNameMapper = input.getProguardMap();
+ for (DexProgramClass programClass : classes) {
+ ClassNamingForNameMapper classNaming;
+ if (classNameMapper == null) {
+ classNaming = null;
+ } else {
+ classNaming = classNameMapper.getClassNaming(programClass.type);
+ }
+ String type =
+ classNaming == null ? programClass.type.toSourceString() : classNaming.originalName;
+ result.put(type, programClass);
+ }
+ return result;
+ }
+
+ private String getCommonPrefix(Set<String> classes) {
+ if (classes.size() <= 1) {
+ return "";
+ }
+ String commonPrefix = null;
+ for (String clazz : classes) {
+ if (clazz.equals("r8.GeneratedOutlineSupport")) {
+ continue;
+ }
+ if (commonPrefix == null) {
+ commonPrefix = clazz;
+ } else {
+ int i = 0;
+ while (i < clazz.length()
+ && i < commonPrefix.length()
+ && clazz.charAt(i) == commonPrefix.charAt(i)) {
+ i++;
+ }
+ commonPrefix = commonPrefix.substring(0, i);
+ }
+ }
+ return commonPrefix;
+ }
+
+ private void printLibrary(Map<String, InputClass[]> classMap, String commonPrefix) {
+ List<Entry<String, InputClass[]>> classes = new ArrayList<>(classMap.entrySet());
+ classes.sort(Comparator.comparing(Entry::getKey));
+ for (Entry<String, InputClass[]> clazz : classes) {
+ printClass(
+ clazz.getKey().substring(commonPrefix.length()), new ClassCompare(clazz.getValue()));
+ }
+ }
+
+ private void printClass(String name, ClassCompare inputClasses) {
+ List<MethodSignature> methods = getMethods(inputClasses);
+ List<FieldSignature> fields = getFields(inputClasses);
+ if (methods.isEmpty() && fields.isEmpty()) {
+ return;
+ }
+ System.out.println(name);
+ for (MethodSignature sig : methods) {
+ printSignature(getMethodString(sig), inputClasses.sizes(sig));
+ }
+ for (FieldSignature sig : fields) {
+ printSignature(getFieldString(sig), inputClasses.sizes(sig));
+ }
+ }
+
+ private String getMethodString(MethodSignature sig) {
+ StringBuilder builder = new StringBuilder().append('(');
+ for (int i = 0; i < sig.parameters.length; i++) {
+ builder.append(DescriptorUtils.javaTypeToShorty(sig.parameters[i]));
+ }
+ builder.append(')').append(DescriptorUtils.javaTypeToShorty(sig.type)).append(' ');
+ return builder.append(sig.name).toString();
+ }
+
+ private String getFieldString(FieldSignature sig) {
+ return DescriptorUtils.javaTypeToShorty(sig.type) + ' ' + sig.name;
+ }
+
+ private void printSignature(String key, int[] sizes) {
+ System.out.print(padItem(key));
+ for (int size : sizes) {
+ System.out.print(padValue(size));
+ }
+ System.out.print('\n');
+ }
+
+ private List<MethodSignature> getMethods(ClassCompare inputClasses) {
+ List<MethodSignature> methods = new ArrayList<>();
+ for (MethodSignature methodSignature : inputClasses.getMethods()) {
+ if (threshold == 0 || methodExceedsThreshold(inputClasses, methodSignature)) {
+ methods.add(methodSignature);
+ }
+ }
+ return methods;
+ }
+
+ private boolean methodExceedsThreshold(
+ ClassCompare inputClasses, MethodSignature methodSignature) {
+ assert threshold > 0;
+ assert pgIndex != r8Index;
+ int pgSize = inputClasses.size(methodSignature, pgIndex);
+ int r8Size = inputClasses.size(methodSignature, r8Index);
+ return pgSize != -1 && r8Size != -1 && pgSize + threshold <= r8Size;
+ }
+
+ private List<FieldSignature> getFields(ClassCompare inputClasses) {
+ return threshold == 0 ? inputClasses.getFields() : Collections.emptyList();
+ }
+
+ private String padItem(String s) {
+ return String.format("%-52s", s);
+ }
+
+ private String padValue(int v) {
+ return String.format("%8s", v == -1 ? "---" : v);
+ }
+
+ private List<Map.Entry<String, Map<String, InputClass[]>>> byLibrary(
+ Map<String, InputClass[]> inputClasses) {
+ Map<String, Map<String, InputClass[]>> byLibrary = new HashMap<>();
+ for (Entry<String, InputClass[]> entry : inputClasses.entrySet()) {
+ Map<String, InputClass[]> library =
+ byLibrary.computeIfAbsent(getLibraryName(entry.getKey()), k -> new HashMap<>());
+ library.put(entry.getKey(), entry.getValue());
+ }
+ List<Entry<String, Map<String, InputClass[]>>> list = new ArrayList<>(byLibrary.entrySet());
+ list.sort(Comparator.comparing(Entry::getKey));
+ return list;
+ }
+
+ private String getLibraryName(String className) {
+ for (Entry<String, String> relocation : R8_RELOCATIONS.entrySet()) {
+ if (className.startsWith(relocation.getValue())) {
+ return relocation.getKey();
+ }
+ }
+ return "";
+ }
+
+ static class InputParameter {
+
+ private final String name;
+ private final Path jar;
+ private final Path map;
+
+ InputParameter(String name, Path jar, Path map) {
+ this.name = name;
+ this.jar = jar;
+ this.map = map;
+ }
+
+ DexApplication getReader(InternalOptions options, AndroidApp inputApp, Timing timing)
+ throws Exception {
+ ApplicationReader applicationReader = new ApplicationReader(inputApp, options, timing);
+ return applicationReader.read(map == null ? null : StringResource.fromFile(map)).toDirect();
+ }
+
+ AndroidApp getInputApp(List<Path> libraries) throws Exception {
+ return AndroidApp.builder().addLibraryFiles(libraries).addProgramFiles(jar).build();
+ }
+ }
+
+ static class InputApplication {
+
+ private final DexApplication dexApplication;
+ private final Map<String, DexProgramClass> classMap;
+
+ private InputApplication(DexApplication dexApplication, Map<String, DexProgramClass> classMap) {
+ this.dexApplication = dexApplication;
+ this.classMap = classMap;
+ }
+
+ public Set<String> getClasses() {
+ return classMap.keySet();
+ }
+
+ private InputClass getInputClass(String type) {
+ DexProgramClass inputClass = classMap.get(type);
+ ClassNameMapper proguardMap = dexApplication.getProguardMap();
+ return new InputClass(inputClass, proguardMap);
+ }
+ }
+
+ static class InputClass {
+ private final DexProgramClass programClass;
+ private final ClassNameMapper proguardMap;
+
+ InputClass(DexClass dexClass, ClassNameMapper proguardMap) {
+ this.programClass = dexClass == null ? null : dexClass.asProgramClass();
+ this.proguardMap = proguardMap;
+ }
+
+ void forEachMethod(BiConsumer<MethodSignature, DexEncodedMethod> consumer) {
+ if (programClass == null) {
+ return;
+ }
+ programClass.forEachMethod(
+ dexEncodedMethod -> {
+ MethodSignature originalSignature =
+ proguardMap == null
+ ? null
+ : ((MethodSignature) proguardMap.originalSignatureOf(dexEncodedMethod.method));
+ MethodSignature signature = MethodSignature.fromDexMethod(dexEncodedMethod.method);
+ consumer.accept(
+ originalSignature == null ? signature : originalSignature, dexEncodedMethod);
+ });
+ }
+
+ void forEachField(BiConsumer<FieldSignature, DexEncodedField> consumer) {
+ if (programClass == null) {
+ return;
+ }
+ programClass.forEachField(
+ dexEncodedField -> {
+ FieldSignature originalSignature =
+ proguardMap == null ? null : proguardMap.originalSignatureOf(dexEncodedField.field);
+ FieldSignature signature = FieldSignature.fromDexField(dexEncodedField.field);
+ consumer.accept(
+ originalSignature == null ? signature : originalSignature, dexEncodedField);
+ });
+ }
+ }
+
+ private static class ClassCompare {
+ final Map<MethodSignature, DexEncodedMethod[]> methods = new HashMap<>();
+ final Map<FieldSignature, DexEncodedField[]> fields = new HashMap<>();
+ final int classes;
+
+ ClassCompare(InputClass[] inputs) {
+ for (int i = 0; i < inputs.length; i++) {
+ InputClass inputClass = inputs[i];
+ int finalI = i;
+ if (inputClass == null) {
+ continue;
+ }
+ inputClass.forEachMethod(
+ (sig, m) ->
+ methods.computeIfAbsent(sig, o -> new DexEncodedMethod[inputs.length])[finalI] = m);
+ inputClass.forEachField(
+ (sig, f) ->
+ fields.computeIfAbsent(sig, o -> new DexEncodedField[inputs.length])[finalI] = f);
+ }
+ classes = inputs.length;
+ }
+
+ List<MethodSignature> getMethods() {
+ List<MethodSignature> methods = new ArrayList<>(this.methods.keySet());
+ methods.sort(Comparator.comparing(MethodSignature::toString));
+ return methods;
+ }
+
+ List<FieldSignature> getFields() {
+ List<FieldSignature> fields = new ArrayList<>(this.fields.keySet());
+ fields.sort(Comparator.comparing(FieldSignature::toString));
+ return fields;
+ }
+
+ int size(MethodSignature method, int classIndex) {
+ DexEncodedMethod dexEncodedMethod = methods.get(method)[classIndex];
+ if (dexEncodedMethod == null) {
+ return -1;
+ }
+ Code code = dexEncodedMethod.getCode();
+ if (code == null) {
+ return 0;
+ }
+ if (code.isCfCode()) {
+ return code.asCfCode().getInstructions().size();
+ }
+ if (code.isDexCode()) {
+ return code.asDexCode().instructions.length;
+ }
+ throw new Unreachable();
+ }
+
+ int[] sizes(MethodSignature method) {
+ int[] result = new int[classes];
+ for (int i = 0; i < classes; i++) {
+ result[i] = size(method, i);
+ }
+ return result;
+ }
+
+ int size(FieldSignature field, int classIndex) {
+ return fields.get(field)[classIndex] == null ? -1 : 1;
+ }
+
+ int[] sizes(FieldSignature field) {
+ int[] result = new int[classes];
+ for (int i = 0; i < classes; i++) {
+ result[i] = size(field, i);
+ }
+ return result;
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ JarSizeCompare program = JarSizeCompare.parse(args);
+ if (program == null) {
+ System.out.println(USAGE);
+ } else {
+ program.run();
+ }
+ }
+
+ public static JarSizeCompare parse(String[] args) {
+ int i = 0;
+ int threshold = 0;
+ List<Path> libraries = new ArrayList<>();
+ List<InputParameter> inputs = new ArrayList<>();
+ Set<String> names = new HashSet<>();
+ while (i < args.length) {
+ if (args[i].equals("--threshold") && i + 1 < args.length) {
+ threshold = Integer.parseInt(args[i + 1]);
+ i += 2;
+ } else if (args[i].equals("--lib") && i + 1 < args.length) {
+ libraries.add(Paths.get(args[i + 1]));
+ i += 2;
+ } else if (args[i].equals("--input") && i + 2 < args.length) {
+ String name = args[i + 1];
+ Path jar = Paths.get(args[i + 2]);
+ Path map = null;
+ if (i + 3 < args.length && !args[i + 3].startsWith("-")) {
+ map = Paths.get(args[i + 3]);
+ i += 4;
+ } else {
+ i += 3;
+ }
+ inputs.add(new InputParameter(name, jar, map));
+ if (!names.add(name)) {
+ System.out.println("Duplicate name: " + name);
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ if (inputs.size() < 2) {
+ return null;
+ }
+ if (threshold != 0 && (!names.contains("r8") || !names.contains("pg"))) {
+ System.out.println(
+ "You must either specify names \"pg\" and \"r8\" for input files "
+ + "or use \"--threshold 0\".");
+ return null;
+ }
+ return new JarSizeCompare(libraries, inputs, threshold);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/PrintSeeds.java b/src/main/java/com/android/tools/r8/PrintSeeds.java
index b1bc7ca..782c91d 100644
--- a/src/main/java/com/android/tools/r8/PrintSeeds.java
+++ b/src/main/java/com/android/tools/r8/PrintSeeds.java
@@ -20,17 +20,32 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+/**
+ * PrintSeeds prints the classes, interfaces, methods and fields selected by a given ProGuard
+ * configuration <pg-conf.txt> when compiling a given program <r8.jar> alongside a given
+ * library <rt.jar>.
+ *
+ * <p>The output format is identical to what is printed when {@code -printseeds} is specified in
+ * <pg-conf.txt>, but running PrintSeeds can be faster than running R8 with {@code
+ * -printseeds}. See also the {@link PrintUses} program in R8.
+ */
public class PrintSeeds {
private static final String USAGE =
"Arguments: <rt.jar> <r8.jar> <pg-conf.txt>\n"
+ "\n"
+ "PrintSeeds prints the classes, interfaces, methods and fields selected by\n"
- + "<pg-conf.txt> when compiling <r8.jar> alongside <rt.jar>.";
+ + "<pg-conf.txt> when compiling <r8.jar> alongside <rt.jar>.\n"
+ + "\n"
+ + "The output format is identical to what is printed when -printseeds is specified in\n"
+ + "<pg-conf.txt>, but running PrintSeeds can be faster than running R8 with \n"
+ + "-printseeds. See also the "
+ + PrintUses.class.getSimpleName()
+ + " program in R8.";
public static void main(String[] args) throws Exception {
if (args.length != 3) {
- System.out.println(USAGE);
+ System.out.println(USAGE.replace("\n", System.lineSeparator()));
System.exit(1);
}
Path rtJar = Paths.get(args[0]);
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
index 0e69de6..c762182 100644
--- a/src/main/java/com/android/tools/r8/PrintUses.java
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -31,6 +31,17 @@
import java.util.Map;
import java.util.Set;
+/**
+ * PrintUses prints the classes, interfaces, methods and fields used by a given program
+ * <sample.jar>, restricted to classes and interfaces in a given library <r8.jar> that
+ * are not in <sample.jar>.
+ *
+ * <p>The output is in the same format as what is printed when specifying {@code -printseeds} in a
+ * ProGuard configuration file. See also the {@link PrintSeeds} program in R8.
+ *
+ * <p>Note that this tool is not related to the {@code -printusage} option of ProGuard configuration
+ * files.
+ */
public class PrintUses {
private static final String USAGE =
@@ -38,7 +49,12 @@
+ "\n"
+ "PrintUses prints the classes, interfaces, methods and fields used by <sample.jar>,\n"
+ "restricted to classes and interfaces in <r8.jar> that are not in <sample.jar>.\n"
- + "<rt.jar> and <r8.jar> should point to libraries used by <sample.jar>.";
+ + "<rt.jar> and <r8.jar> should point to libraries used by <sample.jar>.\n"
+ + "\n"
+ + "The output is in the same format as what is printed when specifying -printseeds in\n"
+ + "a ProGuard configuration file. See also the "
+ + PrintSeeds.class.getSimpleName()
+ + " program in R8.";
private final Set<String> descriptors;
private final PrintStream out;
@@ -191,7 +207,7 @@
public static void main(String[] args) throws Exception {
if (args.length != 3) {
- System.out.println(USAGE);
+ System.out.println(USAGE.replace("\n", System.lineSeparator()));
return;
}
AndroidApp.Builder builder = AndroidApp.builder();
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index e42d8b0..d587efe 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -11,7 +11,6 @@
import com.android.tools.r8.dex.Marker.Tool;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
-import com.android.tools.r8.graph.ClassAndMemberPublicizer;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
@@ -28,6 +27,7 @@
import com.android.tools.r8.naming.ProguardMapSupplier;
import com.android.tools.r8.naming.SeedMapper;
import com.android.tools.r8.naming.SourceFileRewriter;
+import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
import com.android.tools.r8.optimize.MemberRebindingAnalysis;
import com.android.tools.r8.optimize.VisibilityBridgeRemover;
import com.android.tools.r8.origin.CommandLineOrigin;
@@ -42,8 +42,8 @@
import com.android.tools.r8.shaking.ReasonPrinter;
import com.android.tools.r8.shaking.RootSetBuilder;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
-import com.android.tools.r8.shaking.SimpleClassMerger;
import com.android.tools.r8.shaking.TreePruner;
+import com.android.tools.r8.shaking.VerticalClassMerger;
import com.android.tools.r8.shaking.protolite.ProtoLiteExtension;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
@@ -335,8 +335,8 @@
// Class merging requires inlining.
if (options.enableClassMerging && options.enableInlining) {
timing.begin("ClassMerger");
- SimpleClassMerger classMerger = new SimpleClassMerger(application,
- appInfo.withLiveness(), graphLense, timing);
+ VerticalClassMerger classMerger =
+ new VerticalClassMerger(application, appInfo.withLiveness(), graphLense, timing);
graphLense = classMerger.run();
timing.end();
@@ -344,11 +344,11 @@
.prunedCopyFrom(application, classMerger.getRemovedClasses());
}
if (options.proguardConfiguration.hasApplyMappingFile()) {
- SeedMapper seedMapper = SeedMapper.seedMapperFromFile(
- options.proguardConfiguration.getApplyMappingFile());
+ SeedMapper seedMapper =
+ SeedMapper.seedMapperFromFile(options.proguardConfiguration.getApplyMappingFile());
timing.begin("apply-mapping");
- graphLense = new ProguardMapApplier(appInfo.withLiveness(), graphLense, seedMapper)
- .run(timing);
+ graphLense =
+ new ProguardMapApplier(appInfo.withLiveness(), graphLense, seedMapper).run(timing);
timing.end();
}
application = application.asDirect().rewrittenWithLense(graphLense);
diff --git a/src/main/java/com/android/tools/r8/SwissArmyKnife.java b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
index 28e1f93..f54233b 100644
--- a/src/main/java/com/android/tools/r8/SwissArmyKnife.java
+++ b/src/main/java/com/android/tools/r8/SwissArmyKnife.java
@@ -61,6 +61,9 @@
case "jardiff":
JarDiff.main(shift(args));
break;
+ case "jarsizecompare":
+ JarSizeCompare.main(shift(args));
+ break;
case "maindex":
GenerateMainDexList.main(shift(args));
break;
diff --git a/src/main/java/com/android/tools/r8/UsageInformationConsumer.java b/src/main/java/com/android/tools/r8/UsageInformationConsumer.java
deleted file mode 100644
index a572908..0000000
--- a/src/main/java/com/android/tools/r8/UsageInformationConsumer.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (c) 2017, 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;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
-import com.android.tools.r8.utils.ExceptionDiagnostic;
-import com.android.tools.r8.utils.FileUtils;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.file.Path;
-
-/**
- * Interface for receiving usage feedback from R8.
- *
- * The data is in the format defined for Proguard's <code>-printusage</code> flag. The information
- * will be produced if a consumer is provided. A consumer is automatically setup when running R8
- * with the Proguard <code>-printusage</code> flag set.
- */
-public interface UsageInformationConsumer {
-
- /**
- * Callback to receive the usage-information data.
- *
- * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
- * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
- * then the compiler guaranties to exit with an error.
- *
- * @param data UTF-8 encoded usage information.
- * @param handler Diagnostics handler for reporting.
- */
- void acceptUsageInformation(byte[] data, DiagnosticsHandler handler);
-
- static EmptyConsumer emptyConsumer() {
- return EmptyConsumer.EMPTY_CONSUMER;
- }
-
- /** Empty consumer to request usage information but ignore the result. */
- class EmptyConsumer implements UsageInformationConsumer {
-
- private static final EmptyConsumer EMPTY_CONSUMER = new EmptyConsumer();
-
- private EmptyConsumer() {}
-
- @Override
- public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
- // Ignore content.
- }
- }
-
- /** Forwarding consumer to delegate to an optional existing consumer. */
- class ForwardingConsumer implements UsageInformationConsumer {
-
- private final UsageInformationConsumer consumer;
-
- /** @param consumer Consumer to forward to, if null, nothing will be forwarded. */
- public ForwardingConsumer(UsageInformationConsumer consumer) {
- this.consumer = consumer;
- }
-
- @Override
- public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
- if (consumer != null) {
- consumer.acceptUsageInformation(data, handler);
- }
- }
- }
-
- /** File consumer to write contents to a file-system file. */
- class FileConsumer extends ForwardingConsumer {
-
- private final Path outputPath;
-
- /** Consumer that writes to {@param outputPath}. */
- public FileConsumer(Path outputPath) {
- this(outputPath, null);
- }
-
- /** Consumer that forwards to {@param consumer} and also writes to {@param outputPath}. */
- public FileConsumer(Path outputPath, UsageInformationConsumer consumer) {
- super(consumer);
- this.outputPath = outputPath;
- }
-
- @Override
- public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
- super.acceptUsageInformation(data, handler);
- try {
- FileUtils.writeToFile(outputPath, null, data);
- } catch (IOException e) {
- Origin origin = new PathOrigin(outputPath);
- handler.error(new ExceptionDiagnostic(e, origin));
- }
- }
- }
-
- /**
- * Stream consumer to write contents to an output stream.
- *
- * <p>Note: No close events are given to this stream so it should either be a permanent stream or
- * the closing needs to happen outside of the compilation itself. If the stream is not one of the
- * standard streams, i.e., System.out or System.err, you should likely implement yor own consumer.
- */
- class StreamConsumer extends ForwardingConsumer {
-
- private final Origin origin;
- private final OutputStream outputStream;
-
- /** Consumer that writes to {@param outputStream}. */
- public StreamConsumer(Origin origin, OutputStream outputStream) {
- this(origin, outputStream, null);
- }
-
- /** Consumer that forwards to {@param consumer} and also writes to {@param outputStream}. */
- public StreamConsumer(
- Origin origin, OutputStream outputStream, UsageInformationConsumer consumer) {
- super(consumer);
- this.origin = origin;
- this.outputStream = outputStream;
- }
-
- @Override
- public void acceptUsageInformation(byte[] data, DiagnosticsHandler handler) {
- super.acceptUsageInformation(data, handler);
- try {
- outputStream.write(data);
- } catch (IOException e) {
- handler.error(new ExceptionDiagnostic(e, origin));
- }
- }
- }
-}
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index fb3b067..b775fd3 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
// This field is accessed from release scripts using simple pattern matching.
// Therefore, changing this field could break our release scripts.
- public static final String LABEL = "1.2.21-dev";
+ public static final String LABEL = "1.3.0-dev";
private Version() {
}
diff --git a/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java b/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
index 1336f95..807509a 100644
--- a/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
+++ b/src/main/java/com/android/tools/r8/benchmarks/FrameworkIncrementalDexingBenchmark.java
@@ -22,7 +22,6 @@
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.origin.PathOrigin;
import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableMap;
@@ -61,7 +60,7 @@
archive.toString(),
(entry, stream) -> {
String name = entry.getName();
- if (FileUtils.isClassFile(Paths.get(name))) {
+ if (ZipUtils.isClassFile(name)) {
String descriptor = DescriptorUtils.guessTypeDescriptor(name);
builder.put(descriptor, ByteStreams.toByteArray(stream));
}
diff --git a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
index 90b863c..2621979 100644
--- a/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
+++ b/src/main/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilder.java
@@ -17,6 +17,7 @@
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -127,7 +128,7 @@
List<ZipEntry> toDex = new ArrayList<>();
- try (ZipFile zipFile = new ZipFile(input)) {
+ try (ZipFile zipFile = new ZipFile(input, StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index f512a34..7d2907a 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -3,13 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.compatdx;
-import static com.android.tools.r8.utils.FileUtils.isApkFile;
-import static com.android.tools.r8.utils.FileUtils.isArchive;
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
-import static com.android.tools.r8.utils.FileUtils.isJarFile;
-import static com.android.tools.r8.utils.FileUtils.isZipFile;
-
import com.android.tools.r8.CompatDxHelper;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.CompilationMode;
@@ -28,12 +21,14 @@
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -474,7 +469,7 @@
}
if (singleDexFile) {
return new SingleDexFileConsumer(
- isDexFile(output)
+ FileUtils.isDexFile(output)
? new NamedDexFileConsumer(output)
: createDexConsumer(output, inputs, keepClasses));
}
@@ -485,13 +480,13 @@
Path output, List<Path> inputs, boolean keepClasses)
throws DxUsageMessage {
if (keepClasses) {
- if (!isArchive(output)) {
+ if (!FileUtils.isArchive(output)) {
throw new DxCompatOptions.DxUsageMessage(
"Output must be an archive when --keep-classes is set.");
}
return new DexKeepClassesConsumer(output, inputs);
}
- return isArchive(output)
+ return FileUtils.isArchive(output)
? new DexIndexedConsumer.ArchiveConsumer(output)
: new DexIndexedConsumer.DirectoryConsumer(output);
}
@@ -571,12 +566,12 @@
private void writeZipWithClasses(DiagnosticsHandler handler) throws IOException {
// For each input archive file, add all class files within.
for (Path input : inputs) {
- if (isArchive(input)) {
- try (ZipFile zipFile = new ZipFile(input.toFile())) {
+ if (FileUtils.isArchive(input)) {
+ try (ZipFile zipFile = new ZipFile(input.toFile(), StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
- if (isClassFile(Paths.get(entry.getName()))) {
+ if (ZipUtils.isClassFile(entry.getName())) {
try (InputStream entryStream = zipFile.getInputStream(entry)) {
outputBuilder.addFile(
entry.getName(), ByteStreams.toByteArray(entryStream), handler);
@@ -603,11 +598,11 @@
return;
}
Path path = file.toPath();
- if (isZipFile(path) || isJarFile(path) || isClassFile(path)) {
+ if (FileUtils.isZipFile(path) || FileUtils.isJarFile(path) || FileUtils.isClassFile(path)) {
files.add(path);
return;
}
- if (isApkFile(path)) {
+ if (FileUtils.isApkFile(path)) {
throw new Unimplemented("apk files not yet supported");
}
}
diff --git a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
index 89abadd..23dbfbf 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -3,9 +3,15 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.graph;
+import com.android.tools.r8.ClassFileConsumer;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.conversion.OptimizationFeedbackIgnore;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
+import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
import java.io.PrintStream;
public class AssemblyWriter extends DexByteCodeWriter {
@@ -13,12 +19,29 @@
private final boolean writeAllClassInfo;
private final boolean writeFields;
private final boolean writeAnnotations;
+ private final boolean writeIR;
+ private final AppInfoWithSubtyping appInfo;
+ private final Timing timing = new Timing("AssemblyWriter");
+ private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
- public AssemblyWriter(DexApplication application, InternalOptions options, boolean allInfo) {
+ public AssemblyWriter(
+ DexApplication application, InternalOptions options, boolean allInfo, boolean writeIR) {
super(application, options);
this.writeAllClassInfo = allInfo;
this.writeFields = allInfo;
this.writeAnnotations = allInfo;
+ this.writeIR = writeIR;
+ if (writeIR) {
+ this.appInfo = new AppInfoWithSubtyping(application.toDirect());
+ if (options.programConsumer == null) {
+ // Use class-file backend, since the CF frontend for testing does not support desugaring of
+ // synchronized methods for the DEX backend (b/109789541).
+ options.programConsumer = ClassFileConsumer.emptyConsumer();
+ }
+ options.outline.enabled = false;
+ } else {
+ this.appInfo = null;
+ }
}
@Override
@@ -88,10 +111,21 @@
ps.println();
Code code = method.getCode();
if (code != null) {
- ps.println(code.toString(method, naming));
+ if (writeIR) {
+ writeIR(method, ps);
+ } else {
+ ps.println(code.toString(method, naming));
+ }
}
}
+ private void writeIR(DexEncodedMethod method, PrintStream ps) {
+ CfgPrinter printer = new CfgPrinter();
+ new IRConverter(appInfo, options, timing, printer)
+ .processMethod(method, ignoreOptimizationFeedback, null, null, null);
+ ps.println(printer.toString());
+ }
+
private void writeAnnotations(DexAnnotationSet annotations, PrintStream ps) {
if (writeAnnotations) {
if (!annotations.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
new file mode 100644
index 0000000..7f31098
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
@@ -0,0 +1,63 @@
+// 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.graph;
+
+public class DefaultUseRegistry extends UseRegistry {
+
+ @Override
+ public boolean registerInvokeVirtual(DexMethod method) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInvokeDirect(DexMethod method) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInvokeStatic(DexMethod method) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInvokeInterface(DexMethod method) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInvokeSuper(DexMethod method) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInstanceFieldWrite(DexField field) {
+ return true;
+ }
+
+ @Override
+ public boolean registerInstanceFieldRead(DexField field) {
+ return true;
+ }
+
+ @Override
+ public boolean registerNewInstance(DexType type) {
+ return true;
+ }
+
+ @Override
+ public boolean registerStaticFieldRead(DexField field) {
+ return true;
+ }
+
+ @Override
+ public boolean registerStaticFieldWrite(DexField field) {
+ return true;
+ }
+
+ @Override
+ public boolean registerTypeReference(DexType type) {
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 23139c5..2c188da 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -10,6 +10,7 @@
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.ThrowingConsumer;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.Iterators;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@@ -96,6 +97,16 @@
}
}
+ public Iterable<DexEncodedField> fields() {
+ return () ->
+ Iterators.concat(Iterators.forArray(instanceFields), Iterators.forArray(staticFields));
+ }
+
+ public Iterable<DexEncodedMethod> methods() {
+ return () ->
+ Iterators.concat(Iterators.forArray(directMethods), Iterators.forArray(virtualMethods));
+ }
+
@Override
void collectMixedSectionItems(MixedSectionCollection mixedItems) {
throw new Unreachable();
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 831edbe..026c2b5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -790,6 +790,7 @@
private Code code;
private CompilationState compilationState = CompilationState.NOT_PROCESSED;
private OptimizationInfo optimizationInfo = DefaultOptimizationInfo.DEFAULT;
+ private final int classFileVersion;
private Builder(DexEncodedMethod from) {
// Copy all the mutable state of a DexEncodedMethod here.
@@ -800,6 +801,7 @@
code = from.code;
compilationState = from.compilationState;
optimizationInfo = from.optimizationInfo.copy();
+ classFileVersion = from.classFileVersion;
}
public void setMethod(DexMethod method) {
@@ -816,7 +818,8 @@
assert annotations != null;
assert parameterAnnotations != null;
DexEncodedMethod result =
- new DexEncodedMethod(method, accessFlags, annotations, parameterAnnotations, code);
+ new DexEncodedMethod(
+ method, accessFlags, annotations, parameterAnnotations, code, classFileVersion);
result.compilationState = compilationState;
result.optimizationInfo = optimizationInfo;
return result;
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index 06ea28d..cd3c093 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.dex.IndexedItemCollection;
import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.code.Invoke.Type;
import com.android.tools.r8.naming.NamingLens;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
@@ -159,6 +160,26 @@
public boolean isInvokeConstructor() {
return this == MethodHandleType.INVOKE_CONSTRUCTOR;
}
+
+ public Type toInvokeType() {
+ assert isMethodType();
+ switch (this) {
+ case INVOKE_STATIC:
+ return Type.STATIC;
+ case INVOKE_INSTANCE:
+ return Type.VIRTUAL;
+ case INVOKE_CONSTRUCTOR:
+ return Type.DIRECT;
+ case INVOKE_DIRECT:
+ return Type.DIRECT;
+ case INVOKE_INTERFACE:
+ return Type.INTERFACE;
+ case INVOKE_SUPER:
+ return Type.SUPER;
+ default:
+ throw new Unreachable("DexMethodHandle with unexpected type: " + this);
+ }
+ }
}
public MethodHandleType type;
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index ed99d25..2310891 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -557,7 +557,7 @@
@Override
public int hashCode() {
- return (int) value * 19;
+ return (int) (value * 19);
}
@Override
@@ -615,7 +615,7 @@
@Override
public int hashCode() {
- return (int) value * 29;
+ return (int) (value * 29);
}
@Override
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index 8d5f0d0..e4f1c8c 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -69,7 +69,6 @@
}
public DirectMappedDexApplication rewrittenWithLense(GraphLense graphLense) {
- assert graphLense.isContextFree();
assert mappingIsValid(graphLense, programClasses.getAllTypes());
assert mappingIsValid(graphLense, libraryClasses.keySet());
// As a side effect, this will rebuild the program classes and library classes maps.
@@ -81,7 +80,7 @@
// (e.g. relinking a type) or it might encode a type that was renamed, in which case the
// original type will point to a definition that was renamed.
for (DexType type : types) {
- DexType renamed = graphLense.lookupType(type, null);
+ DexType renamed = graphLense.lookupType(type);
if (renamed != type) {
if (definitionFor(type).type != renamed && definitionFor(renamed) == null) {
return false;
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index ed4106e..096de5a 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -3,8 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.graph;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
+import java.util.Set;
/**
* A GraphLense implements a virtual view on top of the graph, used to delay global rewrites until
@@ -59,13 +63,33 @@
return new Builder();
}
- public abstract DexType lookupType(DexType type, DexEncodedMethod context);
+ public abstract DexType lookupType(DexType type);
- public abstract DexMethod lookupMethod(DexMethod method, DexEncodedMethod context);
+ // This overload can be used when the graph lense is known to be context insensitive.
+ public DexMethod lookupMethod(DexMethod method) {
+ assert isContextFreeForMethod(method);
+ return lookupMethod(method, null, null);
+ }
- public abstract DexField lookupField(DexField field, DexEncodedMethod context);
+ public abstract DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type);
- public abstract boolean isContextFree();
+ // Context sensitive graph lenses should override this method.
+ public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+ assert isContextFreeForMethod(method);
+ DexMethod result = lookupMethod(method);
+ if (result != null) {
+ return ImmutableSet.of(result);
+ }
+ return ImmutableSet.of();
+ }
+
+ public abstract DexField lookupField(DexField field);
+
+ public abstract boolean isContextFreeForMethods();
+
+ public boolean isContextFreeForMethod(DexMethod method) {
+ return isContextFreeForMethods();
+ }
public static GraphLense getIdentityLense() {
return new IdentityGraphLense();
@@ -78,22 +102,22 @@
private static class IdentityGraphLense extends GraphLense {
@Override
- public DexType lookupType(DexType type, DexEncodedMethod context) {
+ public DexType lookupType(DexType type) {
return type;
}
@Override
- public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
+ public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
return method;
}
@Override
- public DexField lookupField(DexField field, DexEncodedMethod context) {
+ public DexField lookupField(DexField field) {
return field;
}
@Override
- public boolean isContextFree() {
+ public boolean isContextFreeForMethods() {
return true;
}
}
@@ -118,14 +142,14 @@
}
@Override
- public DexType lookupType(DexType type, DexEncodedMethod context) {
+ public DexType lookupType(DexType type) {
if (type.isArrayType()) {
synchronized (this) {
// This block need to be synchronized due to arrayTypeCache.
DexType result = arrayTypeCache.get(type);
if (result == null) {
DexType baseType = type.toBaseType(dexItemFactory);
- DexType newType = lookupType(baseType, context);
+ DexType newType = lookupType(baseType);
if (baseType == newType) {
result = type;
} else {
@@ -136,25 +160,39 @@
return result;
}
}
- DexType previous = previousLense.lookupType(type, context);
+ DexType previous = previousLense.lookupType(type);
return typeMap.getOrDefault(previous, previous);
}
@Override
- public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
- DexMethod previous = previousLense.lookupMethod(method, context);
+ public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+ DexMethod previous = previousLense.lookupMethod(method, context, type);
return methodMap.getOrDefault(previous, previous);
}
@Override
- public DexField lookupField(DexField field, DexEncodedMethod context) {
- DexField previous = previousLense.lookupField(field, context);
+ public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+ Set<DexMethod> result = new HashSet<>();
+ for (DexMethod previous : previousLense.lookupMethodInAllContexts(method)) {
+ result.add(methodMap.getOrDefault(previous, previous));
+ }
+ return result;
+ }
+
+ @Override
+ public DexField lookupField(DexField field) {
+ DexField previous = previousLense.lookupField(field);
return fieldMap.getOrDefault(previous, previous);
}
@Override
- public boolean isContextFree() {
- return previousLense.isContextFree();
+ public boolean isContextFreeForMethods() {
+ return previousLense.isContextFreeForMethods();
+ }
+
+ @Override
+ public boolean isContextFreeForMethod(DexMethod method) {
+ return previousLense.isContextFreeForMethod(method);
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 113d522..819c6f2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -6,10 +6,11 @@
import static com.android.tools.r8.ir.code.IRCode.INSTRUCTION_NUMBER_DELTA;
import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DebugLocalInfo;
import com.android.tools.r8.graph.DebugLocalInfo.PrintLevel;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.ir.conversion.DexBuilder;
import com.android.tools.r8.ir.conversion.IRBuilder;
import com.android.tools.r8.utils.CfgPrinter;
@@ -20,10 +21,13 @@
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -173,7 +177,7 @@
public void removeSuccessor(BasicBlock block) {
int index = successors.indexOf(block);
assert index >= 0 : "removeSuccessor did not find the successor to remove";
- removeSuccessorsByIndex(Collections.singletonList(index));
+ removeSuccessorsByIndex(new IntArrayList(new int[] {index}));
}
public void removePredecessor(BasicBlock block) {
@@ -328,7 +332,7 @@
assert false : "replaceSuccessor did not find the predecessor to replace";
}
- public void removeSuccessorsByIndex(List<Integer> successorsToRemove) {
+ public void removeSuccessorsByIndex(IntList successorsToRemove) {
if (successorsToRemove.isEmpty()) {
return;
}
@@ -463,7 +467,7 @@
return instruction;
}
}
- throw new Unreachable();
+ return null;
}
public void clearUserInfo() {
@@ -678,6 +682,44 @@
catchHandlers = new CatchHandlers<>(guards, successorIndexes);
}
+ // Due to class merging, it is possible that two exception classes have been merged into one.
+ // This function renames the guards according to the given graph lense.
+ public void renameGuardsInCatchHandlers(GraphLense graphLense) {
+ assert hasCatchHandlers();
+ List<DexType> newGuards = new ArrayList<>(catchHandlers.getGuards().size());
+ for (DexType guard : catchHandlers.getGuards()) {
+ // The type may have changed due to class merging.
+ newGuards.add(graphLense.lookupType(guard));
+ }
+ this.catchHandlers = new CatchHandlers<>(newGuards, catchHandlers.getAllTargets());
+ }
+
+ public boolean consistentCatchHandlers() {
+ // Check that catch handlers are always the first successors of a block.
+ if (hasCatchHandlers()) {
+ assert exit().isGoto() || exit().isThrow();
+ CatchHandlers<Integer> catchHandlers = getCatchHandlersWithSuccessorIndexes();
+ // If there is a catch-all guard it must be the last.
+ List<DexType> guards = catchHandlers.getGuards();
+ int lastGuardIndex = guards.size() - 1;
+ for (int i = 0; i < guards.size(); i++) {
+ assert guards.get(i) != DexItemFactory.catchAllType || i == lastGuardIndex;
+ }
+ // Check that all successors except maybe the last are catch successors.
+ List<Integer> sortedHandlerIndices = new ArrayList<>(catchHandlers.getAllTargets());
+ sortedHandlerIndices.sort(Comparator.naturalOrder());
+ int firstIndex = sortedHandlerIndices.get(0);
+ int lastIndex = sortedHandlerIndices.get(sortedHandlerIndices.size() - 1);
+ assert firstIndex == 0;
+ assert lastIndex < sortedHandlerIndices.size();
+ int lastSuccessorIndex = getSuccessors().size() - 1;
+ assert lastIndex == lastSuccessorIndex // All successors are catch successors.
+ || lastIndex == lastSuccessorIndex - 1; // All but one successors are catch successors.
+ assert lastIndex == lastSuccessorIndex || !exit().isThrow();
+ }
+ return true;
+ }
+
public void clearCurrentDefinitions() {
currentDefinitions = null;
for (Phi phi : getPhis()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index a5d3e4a..c832991 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -5,8 +5,6 @@
import com.android.tools.r8.graph.DebugLocalInfo;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.ImmutableList;
@@ -473,28 +471,7 @@
private boolean consistentCatchHandlers() {
for (BasicBlock block : blocks) {
- // Check that catch handlers are always the first successors of a block.
- if (block.hasCatchHandlers()) {
- assert block.exit().isGoto() || block.exit().isThrow();
- CatchHandlers<Integer> catchHandlers = block.getCatchHandlersWithSuccessorIndexes();
- // If there is a catch-all guard it must be the last.
- List<DexType> guards = catchHandlers.getGuards();
- int lastGuardIndex = guards.size() - 1;
- for (int i = 0; i < guards.size(); i++) {
- assert guards.get(i) != DexItemFactory.catchAllType || i == lastGuardIndex;
- }
- // Check that all successors except maybe the last are catch successors.
- List<Integer> sortedHandlerIndices = new ArrayList<>(catchHandlers.getAllTargets());
- sortedHandlerIndices.sort(Comparator.naturalOrder());
- int firstIndex = sortedHandlerIndices.get(0);
- int lastIndex = sortedHandlerIndices.get(sortedHandlerIndices.size() - 1);
- assert firstIndex == 0;
- assert lastIndex < sortedHandlerIndices.size();
- int lastSuccessorIndex = block.getSuccessors().size() - 1;
- assert lastIndex == lastSuccessorIndex // All successors are catch successors.
- || lastIndex == lastSuccessorIndex - 1; // All but one successors are catch successors.
- assert lastIndex == lastSuccessorIndex || !block.exit().isThrow();
- }
+ assert block.consistentCatchHandlers();
}
return true;
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 8330e06..21b8dc4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -363,7 +363,7 @@
private void processInvoke(Type type, DexMethod method) {
DexEncodedMethod source = caller.method;
- method = graphLense.lookupMethod(method, source);
+ method = graphLense.lookupMethod(method, source, type);
DexEncodedMethod definition = appInfo.lookup(type, method, source.method.holder);
if (definition != null) {
assert !source.accessFlags.isBridge() || definition != caller.method;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index e798fd2..de86cb4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -22,6 +22,7 @@
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.ir.code.Argument;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CatchHandlers;
@@ -118,13 +119,16 @@
}
public CfCode build(
- CodeRewriter rewriter, InternalOptions options, AppInfoWithSubtyping appInfo) {
+ CodeRewriter rewriter,
+ GraphLense graphLense,
+ InternalOptions options,
+ AppInfoWithSubtyping appInfo) {
computeInitializers();
types = new TypeVerificationHelper(code, factory, appInfo).computeVerificationTypes();
splitExceptionalBlocks();
LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, types);
loadStoreHelper.insertLoadsAndStores();
- DeadCodeRemover.removeDeadCode(code, rewriter, options);
+ DeadCodeRemover.removeDeadCode(code, rewriter, graphLense, options);
removeUnneededLoadsAndStores();
registerAllocator = new CfRegisterAllocator(code, options);
registerAllocator.allocateRegisters();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
index ca79b0f..309e1d0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfSourceCode.java
@@ -367,6 +367,13 @@
}
setLocalVariableLists();
readEndingLocals(builder);
+ if (currentBlockInfo != null && instruction.canThrow()) {
+ Snapshot exceptionTransfer =
+ state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
+ for (int target : currentBlockInfo.exceptionalSuccessors) {
+ recordStateForTarget(target, exceptionTransfer);
+ }
+ }
if (isControlFlow(instruction)) {
ensureDebugValueLivenessControl(builder);
instruction.buildIR(builder, state, this);
@@ -376,13 +383,6 @@
}
state.clear();
} else {
- if (currentBlockInfo != null && instruction.canThrow()) {
- Snapshot exceptionTransfer =
- state.getSnapshot().exceptionTransfer(builder.getFactory().throwableType);
- for (int target : currentBlockInfo.exceptionalSuccessors) {
- recordStateForTarget(target, exceptionTransfer);
- }
- }
instruction.buildIR(builder, state, this);
ensureDebugValueLiveness(builder);
if (builder.getCFG().containsKey(currentInstructionIndex + 1)) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 81dc436..7d7e5f3 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -46,6 +46,7 @@
import com.android.tools.r8.ir.optimize.NonNullTracker;
import com.android.tools.r8.ir.optimize.Outliner;
import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -140,7 +141,7 @@
this.memberValuePropagation =
options.enableValuePropagation ?
new MemberValuePropagation(appInfo.withLiveness()) : null;
- this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping());
+ this.lensCodeRewriter = new LensCodeRewriter(graphLense, appInfo.withSubtyping(), options);
if (appInfo.hasLiveness()) {
// When disabling the pruner here, also disable the ProtoLiteExtension in R8.java.
this.protoLiteRewriter =
@@ -482,7 +483,7 @@
// StringBuilder/StringBuffer method invocations, and removeDeadCode() to remove
// unused out-values.
codeRewriter.rewriteMoveResult(code);
- DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+ DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
consumer.accept(code, method);
return null;
}));
@@ -686,6 +687,7 @@
codeRewriter.rewriteSwitch(code);
codeRewriter.processMethodsNeverReturningNormally(code);
codeRewriter.simplifyIf(code, typeEnvironment);
+ new RedundantFieldLoadElimination(code).run();
if (options.testing.invertConditionals) {
invertConditionalsForTesting(code);
@@ -705,7 +707,7 @@
// Dead code removal. Performed after simplifications to remove code that becomes dead
// as a result of those simplifications. The following optimizations could reveal more
// dead code which is removed right before register allocation in performRegisterAllocation.
- DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+ DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
assert code.isConsistentSSA();
if (options.enableDesugaring && enableTryWithResourcesDesugaring()) {
@@ -776,7 +778,7 @@
private void finalizeToCf(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
assert !method.getCode().isDexCode();
CfBuilder builder = new CfBuilder(method, code, options.itemFactory);
- CfCode result = builder.build(codeRewriter, options, appInfo.withSubtyping());
+ CfCode result = builder.build(codeRewriter, graphLense, options, appInfo.withSubtyping());
method.setCode(result);
markProcessed(method, code, feedback);
}
@@ -818,7 +820,7 @@
private RegisterAllocator performRegisterAllocation(IRCode code, DexEncodedMethod method) {
// Always perform dead code elimination before register allocation. The register allocator
// does not allow dead code (to make sure that we do not waste registers for unneeded values).
- DeadCodeRemover.removeDeadCode(code, codeRewriter, options);
+ DeadCodeRemover.removeDeadCode(code, codeRewriter, graphLense, options);
materializeInstructionBeforeLongOperationsWorkaround(code, options);
LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
registerAllocator.allocateRegisters(options.debug);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index cdb419f..0e4be8b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -34,6 +34,7 @@
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.InternalOptions;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Collectors;
@@ -42,10 +43,13 @@
private final GraphLense graphLense;
private final AppInfoWithSubtyping appInfo;
+ private final InternalOptions options;
- public LensCodeRewriter(GraphLense graphLense, AppInfoWithSubtyping appInfo) {
+ public LensCodeRewriter(
+ GraphLense graphLense, AppInfoWithSubtyping appInfo, InternalOptions options) {
this.graphLense = graphLense;
this.appInfo = appInfo;
+ this.options = options;
}
private Value makeOutValue(Instruction insn, IRCode code) {
@@ -96,8 +100,8 @@
if (!invokedHolder.isClassType()) {
continue;
}
- DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method);
- Invoke.Type invokeType = getInvokeType(invoke, actualTarget, invokedMethod);
+ DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method, invoke.getType());
+ Invoke.Type invokeType = getInvokeType(invoke, actualTarget, invokedMethod, method);
if (actualTarget != invokedMethod || invoke.getType() != invokeType) {
Invoke newInvoke = Invoke.create(invokeType, actualTarget, null,
invoke.outValue(), invoke.inValues());
@@ -107,10 +111,11 @@
&& newInvoke.outValue() != null) {
Value newValue = code.createValue(newInvoke.outType(), invoke.getLocalInfo());
newInvoke.outValue().replaceUsers(newValue);
- CheckCast cast = new CheckCast(
- newValue,
- newInvoke.outValue(),
- graphLense.lookupType(invokedMethod.proto.returnType, method));
+ CheckCast cast =
+ new CheckCast(
+ newValue,
+ newInvoke.outValue(),
+ graphLense.lookupType(invokedMethod.proto.returnType));
cast.setPosition(current.getPosition());
iterator.add(cast);
// If the current block has catch handlers split the check cast into its own block.
@@ -123,7 +128,7 @@
} else if (current.isInstanceGet()) {
InstanceGet instanceGet = current.asInstanceGet();
DexField field = instanceGet.getField();
- DexField actualField = graphLense.lookupField(field, method);
+ DexField actualField = graphLense.lookupField(field);
if (actualField != field) {
InstanceGet newInstanceGet =
new InstanceGet(
@@ -133,7 +138,7 @@
} else if (current.isInstancePut()) {
InstancePut instancePut = current.asInstancePut();
DexField field = instancePut.getField();
- DexField actualField = graphLense.lookupField(field, method);
+ DexField actualField = graphLense.lookupField(field);
if (actualField != field) {
InstancePut newInstancePut =
new InstancePut(
@@ -143,7 +148,7 @@
} else if (current.isStaticGet()) {
StaticGet staticGet = current.asStaticGet();
DexField field = staticGet.getField();
- DexField actualField = graphLense.lookupField(field, method);
+ DexField actualField = graphLense.lookupField(field);
if (actualField != field) {
StaticGet newStaticGet =
new StaticGet(staticGet.getType(), staticGet.dest(), actualField);
@@ -152,7 +157,7 @@
} else if (current.isStaticPut()) {
StaticPut staticPut = current.asStaticPut();
DexField field = staticPut.getField();
- DexField actualField = graphLense.lookupField(field, method);
+ DexField actualField = graphLense.lookupField(field);
if (actualField != field) {
StaticPut newStaticPut =
new StaticPut(staticPut.getType(), staticPut.inValue(), actualField);
@@ -160,7 +165,7 @@
}
} else if (current.isCheckCast()) {
CheckCast checkCast = current.asCheckCast();
- DexType newType = graphLense.lookupType(checkCast.getType(), method);
+ DexType newType = graphLense.lookupType(checkCast.getType());
if (newType != checkCast.getType()) {
CheckCast newCheckCast =
new CheckCast(makeOutValue(checkCast, code), checkCast.object(), newType);
@@ -168,14 +173,14 @@
}
} else if (current.isConstClass()) {
ConstClass constClass = current.asConstClass();
- DexType newType = graphLense.lookupType(constClass.getValue(), method);
+ DexType newType = graphLense.lookupType(constClass.getValue());
if (newType != constClass.getValue()) {
ConstClass newConstClass = new ConstClass(makeOutValue(constClass, code), newType);
iterator.replaceCurrentInstruction(newConstClass);
}
} else if (current.isInstanceOf()) {
InstanceOf instanceOf = current.asInstanceOf();
- DexType newType = graphLense.lookupType(instanceOf.type(), method);
+ DexType newType = graphLense.lookupType(instanceOf.type());
if (newType != instanceOf.type()) {
InstanceOf newInstanceOf = new InstanceOf(makeOutValue(instanceOf, code),
instanceOf.value(), newType);
@@ -183,7 +188,7 @@
}
} else if (current.isInvokeNewArray()) {
InvokeNewArray newArray = current.asInvokeNewArray();
- DexType newType = graphLense.lookupType(newArray.getArrayType(), method);
+ DexType newType = graphLense.lookupType(newArray.getArrayType());
if (newType != newArray.getArrayType()) {
InvokeNewArray newNewArray = new InvokeNewArray(newType, makeOutValue(newArray, code),
newArray.inValues());
@@ -191,7 +196,7 @@
}
} else if (current.isNewArrayEmpty()) {
NewArrayEmpty newArrayEmpty = current.asNewArrayEmpty();
- DexType newType = graphLense.lookupType(newArrayEmpty.type, method);
+ DexType newType = graphLense.lookupType(newArrayEmpty.type);
if (newType != newArrayEmpty.type) {
NewArrayEmpty newNewArray = new NewArrayEmpty(makeOutValue(newArrayEmpty, code),
newArrayEmpty.size(), newType);
@@ -199,7 +204,7 @@
}
} else if (current.isNewInstance()) {
NewInstance newInstance= current.asNewInstance();
- DexType newClazz = graphLense.lookupType(newInstance.clazz, method);
+ DexType newClazz = graphLense.lookupType(newInstance.clazz);
if (newClazz != newInstance.clazz) {
NewInstance newNewInstance =
new NewInstance(newClazz, makeOutValue(newInstance, code));
@@ -215,7 +220,8 @@
DexEncodedMethod method, DexMethodHandle methodHandle) {
if (methodHandle.isMethodHandle()) {
DexMethod invokedMethod = methodHandle.asMethod();
- DexMethod actualTarget = graphLense.lookupMethod(invokedMethod, method);
+ DexMethod actualTarget =
+ graphLense.lookupMethod(invokedMethod, method, methodHandle.type.toInvokeType());
if (actualTarget != invokedMethod) {
DexClass clazz = appInfo.definitionFor(actualTarget.holder);
MethodHandleType newType = methodHandle.type;
@@ -229,7 +235,7 @@
}
} else {
DexField field = methodHandle.asField();
- DexField actualField = graphLense.lookupField(field, method);
+ DexField actualField = graphLense.lookupField(field);
if (actualField != field) {
return new DexMethodHandle(methodHandle.type, actualField);
}
@@ -240,7 +246,8 @@
private Type getInvokeType(
InvokeMethod invoke,
DexMethod actualTarget,
- DexMethod originalTarget) {
+ DexMethod originalTarget,
+ DexEncodedMethod invocationContext) {
// We might move methods from interfaces to classes and vice versa. So we have to support
// fixing the invoke kind, yet only if it was correct to start with.
if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
@@ -248,23 +255,42 @@
DexClass newTargetClass = appInfo.definitionFor(actualTarget.holder);
if (newTargetClass == null) {
return invoke.getType();
- } else {
- DexClass originalTargetClass = appInfo.definitionFor(originalTarget.holder);
- if (originalTargetClass != null
- && (originalTargetClass.isInterface() ^ (invoke.getType() == Type.INTERFACE))) {
- // The invoke was wrong to start with, so we keep it wrong. This is to ensure we get
- // the IncompatibleClassChangeError the original invoke would have triggered.
- return newTargetClass.accessFlags.isInterface()
- ? Type.VIRTUAL
- : Type.INTERFACE;
- } else {
- return newTargetClass.accessFlags.isInterface()
- ? Type.INTERFACE
- : Type.VIRTUAL;
+ }
+ DexClass originalTargetClass = appInfo.definitionFor(originalTarget.holder);
+ if (originalTargetClass != null
+ && (originalTargetClass.isInterface() ^ (invoke.getType() == Type.INTERFACE))) {
+ // The invoke was wrong to start with, so we keep it wrong. This is to ensure we get
+ // the IncompatibleClassChangeError the original invoke would have triggered.
+ return newTargetClass.accessFlags.isInterface() ? Type.VIRTUAL : Type.INTERFACE;
+ }
+ return newTargetClass.accessFlags.isInterface() ? Type.INTERFACE : Type.VIRTUAL;
+ }
+ if (options.enableClassMerging && invoke.isInvokeSuper()) {
+ if (actualTarget.getHolder() == invocationContext.method.getHolder()) {
+ DexClass targetClass = appInfo.definitionFor(actualTarget.holder);
+ if (targetClass == null) {
+ return invoke.getType();
+ }
+
+ // If the super class A of the enclosing class B (i.e., invocationContext.method.holder)
+ // has been merged into B during vertical class merging, and this invoke-super instruction
+ // was resolving to a method in A, then the target method has been changed to a direct
+ // method and moved into B, so that we need to use an invoke-direct instruction instead of
+ // invoke-super.
+ //
+ // At this point, we have an invoke-super instruction where the static target is the
+ // enclosing class. However, such an instruction could occur even if a subclass has never
+ // been merged into the enclosing class. Therefore, to determine if vertical class merging
+ // has been applied, we look if there is a direct method with the right signature, and only
+ // return Type.DIRECT in that case.
+ DexEncodedMethod method = targetClass.lookupDirectMethod(actualTarget);
+ if (method != null) {
+ // The target method has been moved from the super class into the sub class during class
+ // merging such that we now need to use an invoke-direct instruction.
+ return Type.DIRECT;
}
}
- } else {
- return invoke.getType();
}
+ return invoke.getType();
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index 531989e..8e587b9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.optimize;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CatchHandlers;
import com.android.tools.r8.ir.code.IRCode;
@@ -11,15 +13,19 @@
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.utils.InternalOptions;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
+import java.util.List;
import java.util.Queue;
+import java.util.Set;
public class DeadCodeRemover {
public static void removeDeadCode(
- IRCode code, CodeRewriter codeRewriter, InternalOptions options) {
- removeUnneededCatchHandlers(code);
+ IRCode code, CodeRewriter codeRewriter, GraphLense graphLense, InternalOptions options) {
+ removeUnneededCatchHandlers(code, graphLense, options);
Queue<BasicBlock> worklist = new LinkedList<>();
worklist.addAll(code.blocks);
for (BasicBlock block = worklist.poll(); block != null; block = worklist.poll()) {
@@ -98,15 +104,49 @@
}
}
- private static void removeUnneededCatchHandlers(IRCode code) {
+ private static void removeUnneededCatchHandlers(
+ IRCode code, GraphLense graphLense, InternalOptions options) {
for (BasicBlock block : code.blocks) {
- if (block.hasCatchHandlers() && !block.canThrow()) {
- CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
- for (BasicBlock target : handlers.getUniqueTargets()) {
- target.unlinkCatchHandler();
+ if (block.hasCatchHandlers()) {
+ if (block.canThrow()) {
+ if (options.enableClassMerging) {
+ // Handle the case where an exception class has been merged into its sub class.
+ block.renameGuardsInCatchHandlers(graphLense);
+ unlinkDeadCatchHandlers(block, graphLense);
+ }
+ } else {
+ CatchHandlers<BasicBlock> handlers = block.getCatchHandlers();
+ for (BasicBlock target : handlers.getUniqueTargets()) {
+ target.unlinkCatchHandler();
+ }
}
}
}
code.removeUnreachableBlocks();
}
+
+ // Due to class merging, it is possible that two exception classes have been merged into one. This
+ // function removes catch handlers where the guards ended up being the same as a previous one.
+ private static void unlinkDeadCatchHandlers(BasicBlock block, GraphLense graphLense) {
+ assert block.hasCatchHandlers();
+ CatchHandlers<BasicBlock> catchHandlers = block.getCatchHandlers();
+ List<DexType> guards = catchHandlers.getGuards();
+ List<BasicBlock> targets = catchHandlers.getAllTargets();
+
+ Set<DexType> previouslySeenGuards = new HashSet<>();
+ List<BasicBlock> deadCatchHandlers = new ArrayList<>();
+ for (int i = 0; i < guards.size(); i++) {
+ // The type may have changed due to class merging.
+ DexType guard = graphLense.lookupType(guards.get(i));
+ boolean guardSeenBefore = !previouslySeenGuards.add(guard);
+ if (guardSeenBefore) {
+ deadCatchHandlers.add(targets.get(i));
+ }
+ }
+ // Remove the guards that are guaranteed to be dead.
+ for (BasicBlock deadCatchHandler : deadCatchHandlers) {
+ deadCatchHandler.unlinkCatchHandler();
+ }
+ assert block.consistentCatchHandlers();
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 25cd969..029b169 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -271,7 +271,7 @@
Origin origin = appInfo.originFor(target.method.holder);
IRCode code = target.buildInliningIR(appInfo, options, generator, callerPosition, origin);
if (!target.isProcessed()) {
- new LensCodeRewriter(graphLense, appInfo).rewrite(code, target);
+ new LensCodeRewriter(graphLense, appInfo, options).rewrite(code, target);
}
return code;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
new file mode 100644
index 0000000..24012b1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -0,0 +1,193 @@
+// 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.optimize;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Eliminate redundant field loads.
+ *
+ * <p>Simple algorithm that goes through all blocks in one pass in dominator order and propagates
+ * active field sets across control-flow edges where the target has only one predecessor.
+ */
+// TODO(ager): Evaluate speed/size for computing active field sets in a fixed-point computation.
+public class RedundantFieldLoadElimination {
+
+ private final IRCode code;
+ private final DominatorTree dominatorTree;
+
+ // Maps keeping track of fields that have an already loaded value at basic block entry.
+ private final HashMap<BasicBlock, HashMap<FieldAndObject, Instruction>>
+ activeInstanceFieldsAtEntry = new HashMap<>();
+ private final HashMap<BasicBlock, HashMap<DexField, Instruction>> activeStaticFieldsAtEntry =
+ new HashMap<>();
+
+ // Maps keeping track of fields with already loaded values for the current block during
+ // elimination.
+ private HashMap<FieldAndObject, Instruction> activeInstanceFields;
+ private HashMap<DexField, Instruction> activeStaticFields;
+
+ public RedundantFieldLoadElimination(IRCode code) {
+ this.code = code;
+ dominatorTree = new DominatorTree(code);
+ }
+
+ private static class FieldAndObject {
+ private final DexField field;
+ private final Value object;
+
+ private FieldAndObject(DexField field, Value receiver) {
+ this.field = field;
+ this.object = receiver;
+ }
+
+ @Override
+ public int hashCode() {
+ return field.hashCode() * 7 + object.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof FieldAndObject)) {
+ return false;
+ }
+ FieldAndObject o = (FieldAndObject) other;
+ return o.object == object && o.field == field;
+ }
+ }
+
+ public void run() {
+ for (BasicBlock block : dominatorTree.getSortedBlocks()) {
+ activeInstanceFields =
+ activeInstanceFieldsAtEntry.containsKey(block)
+ ? activeInstanceFieldsAtEntry.get(block)
+ : new HashMap<>();
+ activeStaticFields =
+ activeStaticFieldsAtEntry.containsKey(block)
+ ? activeStaticFieldsAtEntry.get(block)
+ : new HashMap<>();
+ InstructionListIterator it = block.listIterator();
+ while (it.hasNext()) {
+ Instruction instruction = it.next();
+ if (instruction.isFieldInstruction()) {
+ DexField field = instruction.asFieldInstruction().getField();
+ if (instruction.isInstancePut() || instruction.isStaticPut()) {
+ killActiveFields(instruction.asFieldInstruction());
+ } else if (instruction.isInstanceGet() && !instruction.outValue().hasLocalInfo()) {
+ Value object = instruction.asInstanceGet().object();
+ FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+ if (activeInstanceFields.containsKey(fieldAndObject)) {
+ Instruction active = activeInstanceFields.get(fieldAndObject);
+ eliminateRedundantRead(it, instruction, active);
+ } else {
+ activeInstanceFields.put(fieldAndObject, instruction);
+ }
+ } else if (instruction.isStaticGet() && !instruction.outValue().hasLocalInfo()) {
+ if (activeStaticFields.containsKey(field)) {
+ Instruction active = activeStaticFields.get(field);
+ eliminateRedundantRead(it, instruction, active);
+ } else {
+ // A field get on a different class can cause <clinit> to run and change static
+ // field values.
+ killActiveFields(instruction.asFieldInstruction());
+ activeStaticFields.put(field, instruction);
+ }
+ }
+ }
+ if (instruction.isMonitor() || instruction.isInvokeMethod()) {
+ activeInstanceFields.clear();
+ activeStaticFields.clear();
+ }
+ }
+ propagateActiveFieldsFrom(block);
+ }
+ assert code.isConsistentSSA();
+ }
+
+ private void propagateActiveFieldsFrom(BasicBlock block) {
+ for (BasicBlock successor : block.getSuccessors()) {
+ // Allow propagation across exceptional edges, just be careful not to propagate if the
+ // throwing instruction is a field instruction.
+ if (successor.getPredecessors().size() == 1) {
+ if (block.hasCatchSuccessor(successor)) {
+ Instruction exceptionalExit = block.exceptionalExit();
+ if (exceptionalExit != null && exceptionalExit.isFieldInstruction()) {
+ killActiveFieldsForExceptionalExit(exceptionalExit.asFieldInstruction());
+ }
+ }
+ assert !activeInstanceFieldsAtEntry.containsKey(successor);
+ activeInstanceFieldsAtEntry.put(successor, new HashMap<>(activeInstanceFields));
+ assert !activeStaticFieldsAtEntry.containsKey(successor);
+ activeStaticFieldsAtEntry.put(successor, new HashMap<>(activeStaticFields));
+ }
+ }
+ }
+
+ private void killActiveFields(FieldInstruction instruction) {
+ DexField field = instruction.getField();
+ if (instruction.isInstancePut()) {
+ // Remove all the field/object pairs that refer to this field to make sure
+ // that we are conservative.
+ List<FieldAndObject> keysToRemove = new ArrayList<>();
+ for (FieldAndObject key : activeInstanceFields.keySet()) {
+ if (key.field == field) {
+ keysToRemove.add(key);
+ }
+ }
+ keysToRemove.forEach((k) -> activeInstanceFields.remove(k));
+ } else if (instruction.isInstanceGet()) {
+ Value object = instruction.asInstanceGet().object();
+ FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+ activeInstanceFields.remove(fieldAndObject);
+ } else if (instruction.isStaticPut()) {
+ if (field.clazz != code.method.method.holder) {
+ // Accessing a static field on a different object could cause <clinit> to run which
+ // could modify any static field on any other object.
+ activeStaticFields.clear();
+ } else {
+ activeStaticFields.remove(field);
+ }
+ } else if (instruction.isStaticGet()) {
+ if (field.clazz != code.method.method.holder) {
+ // Accessing a static field on a different object could cause <clinit> to run which
+ // could modify any static field on any other object.
+ activeStaticFields.clear();
+ }
+ }
+ }
+
+ // If a field get instruction throws an exception it did not have an effect on the
+ // value of the field. Therefore, when propagating across exceptional edges for a
+ // field get instruction we have to exclude that field from the set of known
+ // field values.
+ private void killActiveFieldsForExceptionalExit(FieldInstruction instruction) {
+ DexField field = instruction.getField();
+ if (instruction.isInstanceGet()) {
+ Value object = instruction.asInstanceGet().object();
+ FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+ activeInstanceFields.remove(fieldAndObject);
+ } else if (instruction.isStaticGet()) {
+ activeStaticFields.remove(field);
+ }
+ }
+
+ private void eliminateRedundantRead(
+ InstructionListIterator it, Instruction redundant, Instruction active) {
+ redundant.outValue().replaceUsers(active.outValue());
+ it.removeOrReplaceByDebugLocalRead();
+ active.outValue().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index d2c7701..a9df6a3 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -195,7 +195,8 @@
transformBridgeMethod();
}
computeNeedsRegister();
- insertArgumentMoves();
+ constrainArgumentIntervals();
+ insertRangeInvokeMoves();
ImmutableList<BasicBlock> blocks = computeLivenessInformation();
// First attempt to allocate register allowing argument reuse. This will fail if spilling
// is required or if we end up using more than 16 registers.
@@ -2598,11 +2599,14 @@
}
}
- private void insertArgumentMoves() {
+ private void constrainArgumentIntervals() {
// Record the constraint that incoming arguments are in consecutive registers.
List<Value> arguments = code.collectArguments();
createArgumentLiveIntervals(arguments);
linkArgumentValuesAndIntervals(arguments);
+ }
+
+ private void insertRangeInvokeMoves() {
for (BasicBlock block : code.blocks) {
InstructionListIterator it = block.listIterator();
while (it.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
index 0ba49d3..6ea7d60 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/ForwardMethodSourceCode.java
@@ -45,6 +45,7 @@
assert checkSignatures();
switch (invokeType) {
+ case DIRECT:
case STATIC:
case SUPER:
case INTERFACE:
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 56e381b..01fff01 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -8,9 +8,11 @@
import static com.android.tools.r8.utils.DescriptorUtils.getPackageBinaryNameFromJavaType;
import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationSet;
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.DexItem;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
@@ -18,16 +20,20 @@
import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.naming.signature.GenericSignatureAction;
import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
+import java.lang.reflect.GenericSignatureFormatError;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -40,6 +46,7 @@
private final AppInfoWithLiveness appInfo;
private final RootSet rootSet;
+ private final Reporter reporter;
private final PackageObfuscationMode packageObfuscationMode;
private final boolean isAccessModificationAllowed;
private final Set<String> noObfuscationPrefixes = Sets.newHashSet();
@@ -66,6 +73,7 @@
InternalOptions options) {
this.appInfo = appInfo;
this.rootSet = rootSet;
+ this.reporter = options.reporter;
this.packageObfuscationMode = options.proguardConfiguration.getPackageObfuscationMode();
this.isAccessModificationAllowed = options.proguardConfiguration.isAccessModificationAllowed();
this.packageDictionary = options.proguardConfiguration.getPackageObfuscationDictionary();
@@ -152,27 +160,77 @@
}
}
+ private void parseError(DexItem item, Origin origin, GenericSignatureFormatError e) {
+ StringBuilder message = new StringBuilder("Invalid signature for ");
+ if (item instanceof DexClass) {
+ message.append("class ");
+ message.append(((DexClass) item).getType().toSourceString());
+ } else if (item instanceof DexEncodedField) {
+ message.append("field ");
+ message.append(item.toSourceString());
+ } else {
+ assert item instanceof DexEncodedMethod;
+ message.append("method ");
+ message.append(item.toSourceString());
+ }
+ message.append(".\n");
+ message.append(e.getMessage());
+ reporter.warning(new StringDiagnostic(message.toString(), origin));
+ }
+
private void renameTypesInGenericSignatures() {
for (DexClass clazz : appInfo.classes()) {
- rewriteGenericSignatures(clazz.annotations.annotations,
- genericSignatureParser::parseClassSignature);
- clazz.forEachField(field -> rewriteGenericSignatures(
- field.annotations.annotations, genericSignatureParser::parseFieldSignature));
- clazz.forEachMethod(method -> rewriteGenericSignatures(
- method.annotations.annotations, genericSignatureParser::parseMethodSignature));
+ clazz.annotations = rewriteGenericSignatures(clazz.annotations,
+ genericSignatureParser::parseClassSignature,
+ e -> parseError(clazz, clazz.getOrigin(), e));
+ clazz.forEachField(field ->
+ field.annotations = rewriteGenericSignatures(
+ field.annotations, genericSignatureParser::parseFieldSignature,
+ e -> parseError(field, clazz.getOrigin(), e)));
+ clazz.forEachMethod(method ->
+ method.annotations = rewriteGenericSignatures(
+ method.annotations, genericSignatureParser::parseMethodSignature,
+ e -> parseError(method, clazz.getOrigin(), e)));
}
}
- private void rewriteGenericSignatures(DexAnnotation[] annotations, Consumer<String> parser) {
- for (int i = 0; i < annotations.length; i++) {
- DexAnnotation annotation = annotations[i];
+ private DexAnnotationSet rewriteGenericSignatures(
+ DexAnnotationSet annotations,
+ Consumer<String> parser,
+ Consumer<GenericSignatureFormatError> parseError) {
+ // There can be no more than one signature annotation in an annotation set.
+ final int VALID = -1;
+ int invalid = VALID;
+ for (int i = 0; i < annotations.annotations.length && invalid == VALID; i++) {
+ DexAnnotation annotation = annotations.annotations[i];
if (DexAnnotation.isSignatureAnnotation(annotation, appInfo.dexItemFactory)) {
- parser.accept(DexAnnotation.getSignature(annotation));
- annotations[i] = DexAnnotation.createSignatureAnnotation(
- genericSignatureRewriter.getRenamedSignature(),
- appInfo.dexItemFactory);
+ try {
+ parser.accept(DexAnnotation.getSignature(annotation));
+ annotations.annotations[i] = DexAnnotation.createSignatureAnnotation(
+ genericSignatureRewriter.getRenamedSignature(),
+ appInfo.dexItemFactory);
+ } catch (GenericSignatureFormatError e) {
+ parseError.accept(e);
+ invalid = i;
+ }
}
}
+
+ // Return the rewritten signatures if it was valid and could be rewritten.
+ if (invalid == VALID) {
+ return annotations;
+ }
+ // Remove invalid signature if found.
+ DexAnnotation[] prunedAnnotations =
+ new DexAnnotation[annotations.annotations.length - 1];
+ int dest = 0;
+ for (int i = 0; i < annotations.annotations.length; i++) {
+ if (i != invalid) {
+ prunedAnnotations[dest++] = annotations.annotations[i];
+ }
+ }
+ assert dest == prunedAnnotations.length;
+ return new DexAnnotationSet(prunedAnnotations);
}
/**
@@ -450,12 +508,18 @@
String enclosingRenamedBinaryName =
getClassBinaryNameFromDescriptor(
renaming.getOrDefault(enclosingType, enclosingType.descriptor).toString());
- String renamed =
- getClassBinaryNameFromDescriptor(
- renaming.getOrDefault(type, type.descriptor).toString());
- assert renamed.startsWith(enclosingRenamedBinaryName + Minifier.INNER_CLASS_SEPARATOR);
- String outName = renamed.substring(enclosingRenamedBinaryName.length() + 1);
- renamedSignature.append(outName);
+ DexString renamedDescriptor = renaming.get(type);
+ if (renamedDescriptor != null) {
+ // Pick the renamed inner class from the fully renamed binary name.
+ String fullRenamedBinaryName =
+ getClassBinaryNameFromDescriptor(renamedDescriptor.toString());
+ renamedSignature.append(
+ fullRenamedBinaryName.substring(enclosingRenamedBinaryName.length() + 1));
+ } else {
+ // Did not find the class - keep the inner class name as is.
+ // TODO(110085899): Warn about missing classes in signatures?
+ renamedSignature.append(name);
+ }
return type;
}
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
index 521229e..7dfd840 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapApplier.java
@@ -37,6 +37,7 @@
AppInfoWithLiveness appInfo,
GraphLense previousLense,
SeedMapper seedMapper) {
+ assert previousLense.isContextFreeForMethods();
this.appInfo = appInfo;
this.previousLense = previousLense;
this.seedMapper = seedMapper;
@@ -44,7 +45,7 @@
public GraphLense run(Timing timing) {
timing.begin("from-pg-map-to-lense");
- GraphLense lenseFromMap = new MapToLenseConverter().run(previousLense);
+ GraphLense lenseFromMap = new MapToLenseConverter().run();
timing.end();
timing.begin("fix-types-in-programs");
GraphLense typeFixedLense = new TreeFixer(lenseFromMap).run();
@@ -61,7 +62,7 @@
lenseBuilder = new ConflictFreeBuilder();
}
- private GraphLense run(GraphLense previousLense) {
+ private GraphLense run() {
// To handle inherited yet undefined methods in library classes, we are traversing types in
// a subtyping order. That also helps us detect conflicted mappings in a diamond case:
// LibItfA#foo -> a, LibItfB#foo -> b, while PrgA implements LibItfA and LibItfB.
@@ -308,7 +309,7 @@
}
for (int i = 0; i < methods.length; i++) {
DexEncodedMethod encodedMethod = methods[i];
- DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method, encodedMethod);
+ DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method);
DexType newHolderType = substituteType(appliedMethod.holder, encodedMethod);
DexProto newProto = substituteTypesIn(appliedMethod.proto, encodedMethod);
DexMethod newMethod;
@@ -331,7 +332,7 @@
}
for (int i = 0; i < fields.length; i++) {
DexEncodedField encodedField = fields[i];
- DexField appliedField = appliedLense.lookupField(encodedField.field, null);
+ DexField appliedField = appliedLense.lookupField(encodedField.field);
DexType newHolderType = substituteType(appliedField.clazz, null);
DexType newFieldType = substituteType(appliedField.type, null);
DexField newField;
@@ -427,7 +428,7 @@
return type.replaceBaseType(fixed, appInfo.dexItemFactory);
}
}
- return appliedLense.lookupType(type, context);
+ return appliedLense.lookupType(type);
}
}
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
index 5b549a3..c1f6a7d 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureParser.java
@@ -84,7 +84,7 @@
throw e;
} catch (Throwable t) {
Error e = new GenericSignatureFormatError(
- "Unknown error parsing generic signature: " + t.getMessage());
+ "Unknown error parsing class signature: " + t.getMessage());
e.addSuppressed(t);
throw e;
}
@@ -100,7 +100,7 @@
throw e;
} catch (Throwable t) {
Error e = new GenericSignatureFormatError(
- "Unknown error parsing generic signature: " + t.getMessage());
+ "Unknown error parsing method signature: " + t.getMessage());
e.addSuppressed(t);
throw e;
}
@@ -112,14 +112,14 @@
setInput(signature);
parseFieldTypeSignature();
actions.stop();
- } catch (GenericSignatureFormatError e) {
- throw e;
- } catch (Throwable t) {
- Error e = new GenericSignatureFormatError(
- "Unknown error parsing generic signature: " + t.getMessage());
- e.addSuppressed(t);
- throw e;
- }
+ } catch (GenericSignatureFormatError e) {
+ throw e;
+ } catch (Throwable t) {
+ Error e = new GenericSignatureFormatError(
+ "Unknown error parsing field signature: " + t.getMessage());
+ e.addSuppressed(t);
+ throw e;
+ }
}
private void setInput(String input) {
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
index c68122c..56089f4 100644
--- a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
@@ -43,16 +44,17 @@
InvokeKind kind = targetExtractor.getKind();
if (target != null && target.getArity() == method.method.getArity()) {
assert !method.accessFlags.isPrivate() && !method.accessFlags.isConstructor();
- target = lense.lookupMethod(target, method);
if (kind == InvokeKind.STATIC) {
assert method.accessFlags.isStatic();
- DexEncodedMethod targetMethod = appInfo.lookupStaticTarget(target);
+ DexMethod actualTarget = lense.lookupMethod(target, method, Type.STATIC);
+ DexEncodedMethod targetMethod = appInfo.lookupStaticTarget(actualTarget);
if (targetMethod != null) {
addForwarding(method, targetMethod);
}
} else if (kind == InvokeKind.VIRTUAL) {
// TODO(herhut): Add support for bridges with multiple targets.
- DexEncodedMethod targetMethod = appInfo.lookupSingleVirtualTarget(target);
+ DexMethod actualTarget = lense.lookupMethod(target, method, Type.VIRTUAL);
+ DexEncodedMethod targetMethod = appInfo.lookupSingleVirtualTarget(actualTarget);
if (targetMethod != null) {
addForwarding(method, targetMethod);
}
@@ -86,31 +88,29 @@
}
@Override
- public DexType lookupType(DexType type, DexEncodedMethod context) {
- return previousLense.lookupType(type, context);
+ public DexType lookupType(DexType type) {
+ return previousLense.lookupType(type);
}
@Override
- public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context) {
- DexMethod previous = previousLense.lookupMethod(method, context);
+ public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+ DexMethod previous = previousLense.lookupMethod(method, context, type);
DexMethod bridge = bridgeTargetToBridgeMap.get(previous);
// Do not forward calls from a bridge method to itself while the bridge method is still
// a bridge.
- if (bridge == null
- || (context.accessFlags.isBridge() && bridge == context.method)) {
+ if (bridge == null || (context.accessFlags.isBridge() && bridge == context.method)) {
return previous;
- } else {
- return bridge;
}
+ return bridge;
}
@Override
- public DexField lookupField(DexField field, DexEncodedMethod context) {
- return previousLense.lookupField(field, context);
+ public DexField lookupField(DexField field) {
+ return previousLense.lookupField(field);
}
@Override
- public boolean isContextFree() {
+ public boolean isContextFreeForMethods() {
return false;
}
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
similarity index 87%
rename from src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java
rename to src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
index 7a13348..297dd57 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAndMemberPublicizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/ClassAndMemberPublicizer.java
@@ -1,7 +1,13 @@
// Copyright (c) 2016, 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.graph;
+package com.android.tools.r8.optimize;
+
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.MethodAccessFlags;
public final class ClassAndMemberPublicizer {
private final DexItemFactory factory;
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 488cf2f..fc408e3 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -29,7 +29,7 @@
private final GraphLense.Builder builder = GraphLense.builder();
public MemberRebindingAnalysis(AppInfoWithLiveness appInfo, GraphLense lense) {
- assert lense.isContextFree();
+ assert lense.isContextFreeForMethods();
this.appInfo = appInfo;
this.lense = lense;
}
@@ -115,7 +115,7 @@
private void computeMethodRebinding(Set<DexMethod> methods,
Function<DexMethod, DexEncodedMethod> lookupTarget) {
for (DexMethod method : methods) {
- method = lense.lookupMethod(method, null);
+ method = lense.lookupMethod(method);
// We can safely ignore array types, as the corresponding methods are defined in a library.
if (!method.getHolder().isClassType()) {
continue;
@@ -188,7 +188,7 @@
BiFunction<DexClass, DexField, DexEncodedField> lookupTargetOnClass) {
for (Map.Entry<DexField, Set<DexEncodedMethod>> entry : fields.entrySet()) {
DexField field = entry.getKey();
- field = lense.lookupField(field, null);
+ field = lense.lookupField(field);
DexEncodedField target = lookup.apply(field.getHolder(), field);
// Rebind to the lowest library class or program class. Do not rebind accesses to fields that
// are not visible from the access context.
diff --git a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
index 5847a80..ebf110e 100644
--- a/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
+++ b/src/main/java/com/android/tools/r8/optimize/VisibilityBridgeRemover.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MethodAccessFlags;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
import com.google.common.collect.Sets;
@@ -28,7 +29,8 @@
}
private void identifyBridgeMethod(DexEncodedMethod method) {
- if (method.accessFlags.isBridge()) {
+ MethodAccessFlags accessFlags = method.accessFlags;
+ if (accessFlags.isBridge() && !accessFlags.isAbstract()) {
InvokeSingleTargetExtractor targetExtractor = new InvokeSingleTargetExtractor();
method.getCode().registerCodeReferences(targetExtractor);
DexMethod target = targetExtractor.getTarget();
@@ -36,11 +38,11 @@
// javac-generated visibility forward bridge method has same descriptor (name, signature and
// return type).
if (target != null && target.hasSameProtoAndName(method.method)) {
- assert !method.accessFlags.isPrivate() && !method.accessFlags.isConstructor();
+ assert !accessFlags.isPrivate() && !accessFlags.isConstructor();
if (kind == InvokeKind.SUPER) {
// This is a visibility forward, so check for the direct target.
- DexEncodedMethod targetMethod
- = appInfo.resolveMethod(target.getHolder(), target).asSingleTarget();
+ DexEncodedMethod targetMethod =
+ appInfo.resolveMethod(target.getHolder(), target).asSingleTarget();
if (targetMethod != null && targetMethod.accessFlags.isPublic()) {
if (Log.ENABLED) {
Log.info(getClass(), "Removing visibility forwarding %s -> %s", method.method,
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 51feb9d..2b51720 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -71,7 +71,6 @@
import java.util.SortedSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -1639,8 +1638,8 @@
this.liveTypes = rewriteItems(previous.liveTypes, lense::lookupType);
this.instantiatedTypes = rewriteItems(previous.instantiatedTypes, lense::lookupType);
this.instantiatedLambdas = rewriteItems(previous.instantiatedLambdas, lense::lookupType);
- this.targetedMethods = rewriteItems(previous.targetedMethods, lense::lookupMethod);
- this.liveMethods = rewriteItems(previous.liveMethods, lense::lookupMethod);
+ this.targetedMethods = rewriteMethodsConservatively(previous.targetedMethods, lense);
+ this.liveMethods = rewriteMethodsConservatively(previous.liveMethods, lense);
this.liveFields = rewriteItems(previous.liveFields, lense::lookupField);
this.instanceFieldReads =
rewriteKeysWhileMergingValues(previous.instanceFieldReads, lense::lookupField);
@@ -1653,11 +1652,11 @@
this.fieldsRead = rewriteItems(previous.fieldsRead, lense::lookupField);
this.fieldsWritten = rewriteItems(previous.fieldsWritten, lense::lookupField);
this.pinnedItems = rewriteMixedItems(previous.pinnedItems, lense);
- this.virtualInvokes = rewriteItems(previous.virtualInvokes, lense::lookupMethod);
- this.interfaceInvokes = rewriteItems(previous.interfaceInvokes, lense::lookupMethod);
- this.superInvokes = rewriteItems(previous.superInvokes, lense::lookupMethod);
- this.directInvokes = rewriteItems(previous.directInvokes, lense::lookupMethod);
- this.staticInvokes = rewriteItems(previous.staticInvokes, lense::lookupMethod);
+ this.virtualInvokes = rewriteMethodsConservatively(previous.virtualInvokes, lense);
+ this.interfaceInvokes = rewriteMethodsConservatively(previous.interfaceInvokes, lense);
+ this.superInvokes = rewriteMethodsConservatively(previous.superInvokes, lense);
+ this.directInvokes = rewriteMethodsConservatively(previous.directInvokes, lense);
+ this.staticInvokes = rewriteMethodsConservatively(previous.staticInvokes, lense);
this.prunedTypes = rewriteItems(previous.prunedTypes, lense::lookupType);
// TODO(herhut): Migrate these to Descriptors, as well.
assert assertNotModifiedByLense(previous.noSideEffects.keySet(), lense);
@@ -1739,16 +1738,16 @@
for (DexItem item : items) {
if (item instanceof DexClass) {
DexType type = ((DexClass) item).type;
- assert lense.lookupType(type, null) == type;
+ assert lense.lookupType(type) == type;
} else if (item instanceof DexEncodedMethod) {
DexEncodedMethod method = (DexEncodedMethod) item;
// We only allow changes to bridge methods, as these get retargeted even if they
// are kept.
assert method.accessFlags.isBridge()
- || lense.lookupMethod(method.method, null) == method.method;
+ || lense.lookupMethod(method.method) == method.method;
} else if (item instanceof DexEncodedField) {
DexField field = ((DexEncodedField) item).field;
- assert lense.lookupField(field, null) == field;
+ assert lense.lookupField(field) == field;
} else {
assert false;
}
@@ -1793,31 +1792,54 @@
return builder.build();
}
+ private static ImmutableSortedSet<DexMethod> rewriteMethodsConservatively(
+ Set<DexMethod> original, GraphLense lense) {
+ ImmutableSortedSet.Builder<DexMethod> builder =
+ new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
+ if (lense.isContextFreeForMethods()) {
+ for (DexMethod item : original) {
+ builder.add(lense.lookupMethod(item));
+ }
+ } else {
+ for (DexMethod item : original) {
+ // Avoid using lookupMethodInAllContexts when possible.
+ if (lense.isContextFreeForMethod(item)) {
+ builder.add(lense.lookupMethod(item));
+ } else {
+ // The lense is context sensitive, but we do not have the context here. Therefore, we
+ // conservatively look up the method in all contexts.
+ builder.addAll(lense.lookupMethodInAllContexts(item));
+ }
+ }
+ }
+ return builder.build();
+ }
+
private static <T extends PresortedComparable<T>> ImmutableSortedSet<T> rewriteItems(
- Set<T> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+ Set<T> original, Function<T, T> rewrite) {
ImmutableSortedSet.Builder<T> builder =
new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
for (T item : original) {
- builder.add(rewrite.apply(item, null));
+ builder.add(rewrite.apply(item));
}
return builder.build();
}
private static <T extends PresortedComparable<T>, S> ImmutableMap<T, S> rewriteKeys(
- Map<T, S> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+ Map<T, S> original, Function<T, T> rewrite) {
ImmutableMap.Builder<T, S> builder = new ImmutableMap.Builder<>();
for (T item : original.keySet()) {
- builder.put(rewrite.apply(item, null), original.get(item));
+ builder.put(rewrite.apply(item), original.get(item));
}
return builder.build();
}
- private static <T extends PresortedComparable<T>, S> Map<T, Set<S>>
- rewriteKeysWhileMergingValues(
- Map<T, Set<S>> original, BiFunction<T, DexEncodedMethod, T> rewrite) {
+ private static <T extends PresortedComparable<T>, S>
+ Map<T, Set<S>> rewriteKeysWhileMergingValues(
+ Map<T, Set<S>> original, Function<T, T> rewrite) {
Map<T, Set<S>> result = new IdentityHashMap<>();
for (T item : original.keySet()) {
- T rewrittenKey = rewrite.apply(item, null);
+ T rewrittenKey = rewrite.apply(item);
result.computeIfAbsent(rewrittenKey, k -> Sets.newIdentityHashSet())
.addAll(original.get(item));
}
@@ -1830,11 +1852,11 @@
for (DexItem item : original) {
// TODO(b/67934123) There should be a common interface to perform the dispatch.
if (item instanceof DexType) {
- builder.add(lense.lookupType((DexType) item, null));
+ builder.add(lense.lookupType((DexType) item));
} else if (item instanceof DexMethod) {
- builder.add(lense.lookupMethod((DexMethod) item, null));
+ builder.add(lense.lookupMethod((DexMethod) item));
} else if (item instanceof DexField) {
- builder.add(lense.lookupField((DexField) item, null));
+ builder.add(lense.lookupField((DexField) item));
} else {
throw new Unreachable();
}
@@ -1893,7 +1915,6 @@
public AppInfoWithLiveness rewrittenWithLense(DirectMappedDexApplication application,
GraphLense lense) {
- assert lense.isContextFree();
return new AppInfoWithLiveness(this, application, lense);
}
diff --git a/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java b/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
index 1eb11e9..c67af00 100644
--- a/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
+++ b/src/main/java/com/android/tools/r8/shaking/FilteredClassPath.java
@@ -46,14 +46,14 @@
return path;
}
- public boolean matchesFile(Path file) {
+ public boolean matchesFile(String name) {
if (isUnfiltered()) {
return true;
}
boolean isNegated = false;
for (String pattern : pattern) {
isNegated = pattern.charAt(0) == '!';
- boolean matches = matchAgainstFileName(file.toString(), 0, pattern, isNegated ? 1 : 0);
+ boolean matches = matchAgainstFileName(name, 0, pattern, isNegated ? 1 : 0);
if (matches) {
return !isNegated;
}
@@ -63,7 +63,7 @@
}
private boolean containsFileSeparator(String string) {
- return string.indexOf(File.separatorChar) != -1;
+ return string.indexOf('/') != -1;
}
private boolean matchAgainstFileName(String fileName, int namePos, String pattern,
@@ -95,7 +95,7 @@
}
} else {
for (int i = namePos; i < fileName.length(); i++) {
- if (!includeFileSeparators && fileName.charAt(i) == File.separatorChar) {
+ if (!includeFileSeparators && fileName.charAt(i) == '/') {
return false;
}
if (matchAgainstFileName(fileName, i, pattern, patternPos + 1)) {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index ed9cbdb..c4276dc 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -812,45 +812,45 @@
skipWhitespace();
switch (peekChar()) {
case 'a':
- if (found = acceptString("abstract")) {
+ if ((found = acceptString("abstract"))) {
flags.setAbstract();
}
break;
case 'f':
- if (found = acceptString("final")) {
+ if ((found = acceptString("final"))) {
flags.setFinal();
}
break;
case 'n':
- if (found = acceptString("native")) {
+ if ((found = acceptString("native"))) {
flags.setNative();
}
break;
case 'p':
- if (found = acceptString("public")) {
+ if ((found = acceptString("public"))) {
flags.setPublic();
- } else if (found = acceptString("private")) {
+ } else if ((found = acceptString("private"))) {
flags.setPrivate();
- } else if (found = acceptString("protected")) {
+ } else if ((found = acceptString("protected"))) {
flags.setProtected();
}
break;
case 's':
- if (found = acceptString("synchronized")) {
+ if ((found = acceptString("synchronized"))) {
flags.setSynchronized();
- } else if (found = acceptString("static")) {
+ } else if ((found = acceptString("static"))) {
flags.setStatic();
- } else if (found = acceptString("strictfp")) {
+ } else if ((found = acceptString("strictfp"))) {
flags.setStrict();
}
break;
case 't':
- if (found = acceptString("transient")) {
+ if ((found = acceptString("transient"))) {
flags.setTransient();
}
break;
case 'v':
- if (found = acceptString("volatile")) {
+ if ((found = acceptString("volatile"))) {
flags.setVolatile();
}
break;
diff --git a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java b/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
deleted file mode 100644
index a80e758..0000000
--- a/src/main/java/com/android/tools/r8/shaking/SimpleClassMerger.java
+++ /dev/null
@@ -1,667 +0,0 @@
-// Copyright (c) 2017, 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.shaking;
-
-import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.DexApplication;
-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.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexProto;
-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.GraphLense;
-import com.android.tools.r8.graph.GraphLense.Builder;
-import com.android.tools.r8.graph.KeyedDexItem;
-import com.android.tools.r8.graph.PresortedComparable;
-import com.android.tools.r8.logging.Log;
-import com.android.tools.r8.optimize.InvokeSingleTargetExtractor;
-import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
-import com.android.tools.r8.utils.FieldSignatureEquivalence;
-import com.android.tools.r8.utils.MethodSignatureEquivalence;
-import com.android.tools.r8.utils.Timing;
-import com.google.common.base.Equivalence;
-import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.Iterators;
-import it.unimi.dsi.fastutil.ints.Int2IntMap;
-import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.BiFunction;
-import java.util.stream.Collectors;
-
-/**
- * Merges Supertypes with a single implementation into their single subtype.
- * <p>
- * A common use-case for this is to merge an interface into its single implementation.
- * <p>
- * The class merger only fixes the structure of the graph but leaves the actual instructions
- * untouched. Fixup of instructions is deferred via a {@link GraphLense} to the Ir building phase.
- */
-public class SimpleClassMerger {
-
- private final DexApplication application;
- private final AppInfoWithLiveness appInfo;
- private final GraphLense graphLense;
- private final GraphLense.Builder renamedMembersLense = GraphLense.builder();
- private final Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
- private final Timing timing;
- private Collection<DexMethod> invokes;
- private int numberOfMerges = 0;
-
- public SimpleClassMerger(DexApplication application, AppInfoWithLiveness appInfo,
- GraphLense graphLense, Timing timing) {
- this.application = application;
- this.appInfo = appInfo;
- this.graphLense = graphLense;
- this.timing = timing;
- }
-
- private boolean isMergeCandidate(DexProgramClass clazz) {
- // We can merge program classes if they are not instantiated, have a single subtype
- // and we do not have to keep them.
- return !clazz.isLibraryClass()
- && !appInfo.instantiatedTypes.contains(clazz.type)
- && !appInfo.isPinned(clazz.type)
- && clazz.type.getSingleSubtype() != null;
- }
-
- private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
- Equivalence<DexMethod> equivalence) {
- DexClass definition = appInfo.definitionFor(method.holder);
- if (definition != null && definition.isProgramClass()) {
- set.add(equivalence.wrap(method));
- }
- }
-
- private Collection<DexMethod> getInvokes() {
- if (invokes == null) {
- // Collect all reachable methods that are not within a library class. Those defined on
- // library classes are known not to have program classes in their signature.
- // Also filter methods that only use types from library classes in their signatures. We
- // know that those won't conflict.
- Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
- Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
- appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
- invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
- .collect(Collectors.toList());
- }
- return invokes;
- }
-
- private boolean isProgramClass(DexType type) {
- if (type.isArrayType()) {
- type = type.toBaseType(appInfo.dexItemFactory);
- }
- if (type.isClassType()) {
- DexClass clazz = appInfo.definitionFor(type);
- if (clazz != null && clazz.isProgramClass()) {
- return true;
- }
- }
- return false;
- }
-
- private boolean removeNonProgram(DexMethod dexMethod) {
- for (DexType type : dexMethod.proto.parameters.values) {
- if (isProgramClass(type)) {
- return true;
- }
- }
- return isProgramClass(dexMethod.proto.returnType);
- }
-
- public GraphLense run() {
- timing.begin("merge");
- GraphLense mergingGraphLense = mergeClasses(graphLense);
- timing.end();
- timing.begin("fixup");
- GraphLense result = new TreeFixer().fixupTypeReferences(mergingGraphLense);
- timing.end();
- return result;
- }
-
- private GraphLense mergeClasses(GraphLense graphLense) {
- for (DexProgramClass clazz : application.classes()) {
- if (isMergeCandidate(clazz)) {
- DexClass targetClass = appInfo.definitionFor(clazz.type.getSingleSubtype());
- if (appInfo.isPinned(targetClass.type)) {
- // We have to keep the target class intact, so we cannot merge it.
- continue;
- }
- if (mergedClasses.containsKey(targetClass.type)) {
- // TODO(herhut): Traverse top-down.
- continue;
- }
- if (clazz.hasClassInitializer() && targetClass.hasClassInitializer()) {
- // TODO(herhut): Handle class initializers.
- if (Log.ENABLED) {
- Log.info(getClass(), "Cannot merge %s into %s due to static initializers.",
- clazz.toSourceString(), targetClass.toSourceString());
- }
- continue;
- }
- // Guard against the case where we have two methods that may get the same signature
- // if we replace types. This is rare, so we approximate and err on the safe side here.
- if (new CollisionDetector(clazz.type, targetClass.type, getInvokes(), mergedClasses)
- .mayCollide()) {
- if (Log.ENABLED) {
- Log.info(getClass(), "Cannot merge %s into %s due to conflict.", clazz.toSourceString(),
- targetClass.toSourceString());
- }
- continue;
- }
- boolean merged = new ClassMerger(clazz, targetClass).merge();
- if (Log.ENABLED) {
- if (merged) {
- numberOfMerges++;
- Log.info(getClass(), "Merged class %s into %s.", clazz.toSourceString(),
- targetClass.toSourceString());
- } else {
- Log.info(getClass(), "Aborted merge for class %s into %s.",
- clazz.toSourceString(), targetClass.toSourceString());
- }
- }
- }
- }
- if (Log.ENABLED) {
- Log.debug(getClass(), "Merged %d classes.", numberOfMerges);
- }
- return renamedMembersLense.build(application.dexItemFactory, graphLense);
- }
-
- private class ClassMerger {
-
- private static final String CONSTRUCTOR_NAME = "constructor";
-
- private final DexClass source;
- private final DexClass target;
- private final Map<DexEncodedMethod, DexEncodedMethod> deferredRenamings = new HashMap<>();
- private boolean abortMerge = false;
-
- private ClassMerger(DexClass source, DexClass target) {
- this.source = source;
- this.target = target;
- }
-
- public boolean merge() {
- if (source.getEnclosingMethod() != null || !source.getInnerClasses().isEmpty()
- || target.getEnclosingMethod() != null || !target.getInnerClasses().isEmpty()) {
- // TODO(herhut): Consider supporting merging of inner-class attributes.
- return false;
- }
- // Merge the class [clazz] into [targetClass] by adding all methods to
- // targetClass that are not currently contained.
- // Step 1: Merge methods
- Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
- addAll(existingMethods, target.directMethods(), MethodSignatureEquivalence.get());
- addAll(existingMethods, target.virtualMethods(), MethodSignatureEquivalence.get());
- Collection<DexEncodedMethod> mergedDirectMethods = mergeItems(
- Iterators.transform(Iterators.forArray(source.directMethods()), this::renameConstructors),
- target.directMethods(),
- MethodSignatureEquivalence.get(),
- existingMethods,
- this::renameMethod
- );
- Iterator<DexEncodedMethod> methods = Iterators.forArray(source.virtualMethods());
- if (source.accessFlags.isInterface()) {
- // If merging an interface, only merge methods that are not otherwise defined in the
- // target class.
- methods = Iterators.transform(methods, this::filterShadowedInterfaceMethods);
- }
- Collection<DexEncodedMethod> mergedVirtualMethods = mergeItems(
- methods,
- target.virtualMethods(),
- MethodSignatureEquivalence.get(),
- existingMethods,
- this::abortOnNonAbstract);
- if (abortMerge) {
- return false;
- }
- // Step 2: Merge fields
- Set<Wrapper<DexField>> existingFields = new HashSet<>();
- addAll(existingFields, target.instanceFields(), FieldSignatureEquivalence.get());
- addAll(existingFields, target.staticFields(), FieldSignatureEquivalence.get());
- Collection<DexEncodedField> mergedStaticFields = mergeItems(
- Iterators.forArray(source.staticFields()),
- target.staticFields(),
- FieldSignatureEquivalence.get(),
- existingFields,
- this::renameField);
- Collection<DexEncodedField> mergedInstanceFields = mergeItems(
- Iterators.forArray(source.instanceFields()),
- target.instanceFields(),
- FieldSignatureEquivalence.get(),
- existingFields,
- this::renameField);
- // Step 3: Merge interfaces
- Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
- // Now destructively update the class.
- // Step 1: Update supertype or fix interfaces.
- if (source.isInterface()) {
- interfaces.remove(source.type);
- } else {
- assert !target.isInterface();
- target.superType = source.superType;
- }
- target.interfaces = interfaces.isEmpty()
- ? DexTypeList.empty()
- : new DexTypeList(interfaces.toArray(new DexType[interfaces.size()]));
- // Step 2: replace fields and methods.
- target.setDirectMethods(mergedDirectMethods
- .toArray(new DexEncodedMethod[mergedDirectMethods.size()]));
- target.setVirtualMethods(mergedVirtualMethods
- .toArray(new DexEncodedMethod[mergedVirtualMethods.size()]));
- target.setStaticFields(mergedStaticFields
- .toArray(new DexEncodedField[mergedStaticFields.size()]));
- target.setInstanceFields(mergedInstanceFields
- .toArray(new DexEncodedField[mergedInstanceFields.size()]));
- // Step 3: Unlink old class to ease tree shaking.
- source.superType = application.dexItemFactory.objectType;
- source.setDirectMethods(null);
- source.setVirtualMethods(null);
- source.setInstanceFields(null);
- source.setStaticFields(null);
- source.interfaces = DexTypeList.empty();
- // Step 4: Record merging.
- mergedClasses.put(source.type, target.type);
- // Step 5: Make deferred renamings final.
- deferredRenamings.forEach((from, to) -> renamedMembersLense.map(from.method, to.method));
- return true;
- }
-
- private DexEncodedMethod filterShadowedInterfaceMethods(DexEncodedMethod m) {
- DexEncodedMethod actual = appInfo.resolveMethod(target.type, m.method).asSingleTarget();
- assert actual != null;
- if (actual != m) {
- // We will drop a method here, so record it as a potential renaming.
- deferredRenamings.put(m, actual);
- return null;
- }
- // We will keep the method, so the class better be abstract.
- assert target.accessFlags.isAbstract();
- return m;
- }
-
- private <T extends KeyedDexItem<S>, S extends PresortedComparable<S>> void addAll(
- Collection<Wrapper<S>> collection, T[] items, Equivalence<S> equivalence) {
- for (T item : items) {
- collection.add(equivalence.wrap(item.getKey()));
- }
- }
-
- private <T> Set<T> mergeArrays(T[] one, T[] other) {
- Set<T> merged = new LinkedHashSet<>();
- Collections.addAll(merged, one);
- Collections.addAll(merged, other);
- return merged;
- }
-
- private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> Collection<S> mergeItems(
- Iterator<S> fromItems,
- S[] toItems,
- Equivalence<T> equivalence,
- Set<Wrapper<T>> existing,
- BiFunction<S, S, S> onConflict) {
- HashMap<Wrapper<T>, S> methods = new HashMap<>();
- // First add everything from the target class. These items are not preprocessed.
- for (S item : toItems) {
- methods.put(equivalence.wrap(item.getKey()), item);
- }
- // Now add the new methods, resolving shadowing.
- addNonShadowed(fromItems, methods, equivalence, existing, onConflict);
- return methods.values();
- }
-
- private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> void addNonShadowed(
- Iterator<S> items,
- HashMap<Wrapper<T>, S> map,
- Equivalence<T> equivalence,
- Set<Wrapper<T>> existing,
- BiFunction<S, S, S> onConflict) {
- while (items.hasNext()) {
- S item = items.next();
- if (item == null) {
- // This item was filtered out by a preprocessing.
- continue;
- }
- Wrapper<T> wrapped = equivalence.wrap(item.getKey());
- if (existing.contains(wrapped)) {
- S resolved = onConflict.apply(map.get(wrapped), item);
- wrapped = equivalence.wrap(resolved.getKey());
- map.put(wrapped, resolved);
- } else {
- map.put(wrapped, item);
- }
- }
- }
-
- private DexString makeMergedName(String nameString, DexType holder) {
- return application.dexItemFactory
- .createString(nameString + "$" + holder.toSourceString().replace('.', '$'));
- }
-
- private DexEncodedMethod abortOnNonAbstract(DexEncodedMethod existing,
- DexEncodedMethod method) {
- if (existing == null) {
- // This is a conflict between a static and virtual method. Abort.
- abortMerge = true;
- return method;
- }
- // Ignore if we merge in an abstract method or if we override a bridge method that would
- // bridge to the superclasses method.
- if (method.accessFlags.isAbstract()) {
- // We make a method disappear here, so record the renaming so that calls to the previous
- // target get forwarded properly.
- deferredRenamings.put(method, existing);
- return existing;
- } else if (existing.accessFlags.isBridge()) {
- InvokeSingleTargetExtractor extractor = new InvokeSingleTargetExtractor();
- existing.getCode().registerCodeReferences(extractor);
- if (extractor.getTarget() != method.method) {
- abortMerge = true;
- }
- return method;
- } else {
- abortMerge = true;
- return existing;
- }
- }
-
- private DexEncodedMethod renameConstructors(DexEncodedMethod method) {
- // Only rename instance initializers.
- if (!method.isInstanceInitializer()) {
- return method;
- }
- DexType holder = method.method.holder;
- DexEncodedMethod result = method
- .toRenamedMethod(makeMergedName(CONSTRUCTOR_NAME, holder), application.dexItemFactory);
- result.markForceInline();
- deferredRenamings.put(method, result);
- // Renamed constructors turn into ordinary private functions. They can be private, as
- // they are only references from their direct subclass, which they were merged into.
- result.accessFlags.unsetConstructor();
- result.accessFlags.unsetPublic();
- result.accessFlags.unsetProtected();
- result.accessFlags.setPrivate();
- return result;
- }
-
- private DexEncodedMethod renameMethod(DexEncodedMethod existing, DexEncodedMethod method) {
- // We cannot handle renaming static initializers yet and constructors should have been
- // renamed already.
- assert !method.accessFlags.isConstructor();
- DexType holder = method.method.holder;
- String name = method.method.name.toSourceString();
- DexEncodedMethod result = method
- .toRenamedMethod(makeMergedName(name, holder), application.dexItemFactory);
- renamedMembersLense.map(method.method, result.method);
- return result;
- }
-
- private DexEncodedField renameField(DexEncodedField existing, DexEncodedField field) {
- DexString oldName = field.field.name;
- DexType holder = field.field.clazz;
- DexEncodedField result = field
- .toRenamedField(makeMergedName(oldName.toSourceString(), holder),
- application.dexItemFactory);
- renamedMembersLense.map(field.field, result.field);
- return result;
- }
- }
-
- private class TreeFixer {
-
- private final Builder lense = GraphLense.builder();
- Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
-
- private GraphLense fixupTypeReferences(GraphLense graphLense) {
- // Globally substitute merged class types in protos and holders.
- for (DexProgramClass clazz : appInfo.classes()) {
- clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
- clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
- clazz.setVirtualMethods(removeDupes(clazz.virtualMethods()));
- clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
- clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
- }
- // Record type renamings so instanceof and checkcast checks are also fixed.
- for (DexType type : mergedClasses.keySet()) {
- DexType fixed = fixupType(type);
- lense.map(type, fixed);
- }
- return lense.build(application.dexItemFactory, graphLense);
- }
-
- private DexEncodedMethod[] removeDupes(DexEncodedMethod[] methods) {
- if (methods == null) {
- return null;
- }
- Map<DexMethod, DexEncodedMethod> filtered = new IdentityHashMap<>();
- for (DexEncodedMethod method : methods) {
- DexEncodedMethod previous = filtered.put(method.method, method);
- if (previous != null) {
- if (!previous.accessFlags.isBridge()) {
- if (!method.accessFlags.isBridge()) {
- throw new CompilationError(
- "Class merging produced invalid result on: " + previous.toSourceString());
- } else {
- filtered.put(previous.method, previous);
- }
- }
- }
- }
- if (filtered.size() == methods.length) {
- return methods;
- }
- return filtered.values().toArray(new DexEncodedMethod[filtered.size()]);
- }
-
- private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
- if (methods == null) {
- return null;
- }
- for (int i = 0; i < methods.length; i++) {
- DexEncodedMethod encodedMethod = methods[i];
- DexMethod method = encodedMethod.method;
- DexProto newProto = getUpdatedProto(method.proto);
- DexType newHolder = fixupType(method.holder);
- DexMethod newMethod = application.dexItemFactory.createMethod(newHolder, newProto,
- method.name);
- if (newMethod != encodedMethod.method) {
- lense.map(encodedMethod.method, newMethod);
- methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
- }
- }
- return methods;
- }
-
- private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
- if (fields == null) {
- return null;
- }
- for (int i = 0; i < fields.length; i++) {
- DexEncodedField encodedField = fields[i];
- DexField field = encodedField.field;
- DexType newType = fixupType(field.type);
- DexType newHolder = fixupType(field.clazz);
- DexField newField = application.dexItemFactory.createField(newHolder, newType, field.name);
- if (newField != encodedField.field) {
- lense.map(encodedField.field, newField);
- fields[i] = encodedField.toTypeSubstitutedField(newField);
- }
- }
- return fields;
- }
-
- private DexProto getUpdatedProto(DexProto proto) {
- DexProto result = protoFixupCache.get(proto);
- if (result == null) {
- DexType returnType = fixupType(proto.returnType);
- DexType[] arguments = fixupTypes(proto.parameters.values);
- result = application.dexItemFactory.createProto(returnType, arguments);
- protoFixupCache.put(proto, result);
- }
- return result;
- }
-
- private DexType fixupType(DexType type) {
- if (type.isArrayType()) {
- DexType base = type.toBaseType(application.dexItemFactory);
- DexType fixed = fixupType(base);
- if (base == fixed) {
- return type;
- } else {
- return type.replaceBaseType(fixed, application.dexItemFactory);
- }
- }
- while (mergedClasses.containsKey(type)) {
- type = mergedClasses.get(type);
- }
- return type;
- }
-
- private DexType[] fixupTypes(DexType[] types) {
- DexType[] result = new DexType[types.length];
- for (int i = 0; i < result.length; i++) {
- result[i] = fixupType(types[i]);
- }
- return result;
- }
- }
-
- private static class CollisionDetector {
-
- private static final int NOT_FOUND = 1 << (Integer.SIZE - 1);
-
- // TODO(herhut): Maybe cache seenPositions for target classes.
- private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
- private final Reference2IntMap<DexProto> targetProtoCache;
- private final Reference2IntMap<DexProto> sourceProtoCache;
- private final DexType source, target;
- private final Collection<DexMethod> invokes;
- private final Map<DexType, DexType> substituions;
-
- private CollisionDetector(DexType source, DexType target, Collection<DexMethod> invokes,
- Map<DexType, DexType> substitutions) {
- this.source = source;
- this.target = target;
- this.invokes = invokes;
- this.substituions = substitutions;
- this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
- this.targetProtoCache.defaultReturnValue(NOT_FOUND);
- this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
- this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
- }
-
- boolean mayCollide() {
- fillSeenPositions(invokes);
- // If the type is not used in methods at all, there cannot be any conflict.
- if (seenPositions.isEmpty()) {
- return false;
- }
- for (DexMethod method : invokes) {
- Int2IntMap positionsMap = seenPositions.get(method.name);
- if (positionsMap != null) {
- int arity = method.getArity();
- int previous = positionsMap.get(arity);
- if (previous != NOT_FOUND) {
- assert previous != 0;
- int positions = computePositionsFor(method.proto, source, sourceProtoCache,
- substituions);
- if ((positions & previous) != 0) {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- private void fillSeenPositions(Collection<DexMethod> invokes) {
- for (DexMethod method : invokes) {
- DexType[] parameters = method.proto.parameters.values;
- int arity = parameters.length;
- int positions = computePositionsFor(method.proto, target, targetProtoCache, substituions);
- if (positions != 0) {
- Int2IntMap positionsMap =
- seenPositions.computeIfAbsent(method.name, k -> {
- Int2IntMap result = new Int2IntOpenHashMap();
- result.defaultReturnValue(NOT_FOUND);
- return result;
- });
- int value = 0;
- int previous = positionsMap.get(arity);
- if (previous != NOT_FOUND) {
- value = previous;
- }
- value |= positions;
- positionsMap.put(arity, value);
- }
- }
-
- }
-
- private int computePositionsFor(DexProto proto, DexType type,
- Reference2IntMap<DexProto> cache, Map<DexType, DexType> substitutions) {
- int result = cache.getInt(proto);
- if (result != NOT_FOUND) {
- return result;
- }
- result = 0;
- int bitsUsed = 0;
- int accumulator = 0;
- for (DexType aType : proto.parameters.values) {
- if (substitutions != null) {
- // Substitute the type with the already merged class to estimate what it will
- // look like.
- while (substitutions.containsKey(aType)) {
- aType = substitutions.get(aType);
- }
- }
- accumulator <<= 1;
- bitsUsed++;
- if (aType == type) {
- accumulator |= 1;
- }
- // Handle overflow on 31 bit boundary.
- if (bitsUsed == Integer.SIZE - 1) {
- result |= accumulator;
- accumulator = 0;
- bitsUsed = 0;
- }
- }
- // We also take the return type into account for potential conflicts.
- DexType returnType = proto.returnType;
- if (substitutions != null) {
- while (substitutions.containsKey(returnType)) {
- returnType = substitutions.get(returnType);
- }
- }
- accumulator <<= 1;
- if (returnType == type) {
- accumulator |= 1;
- }
- result |= accumulator;
- cache.put(proto, result);
- return result;
- }
- }
-
- public Collection<DexType> getRemovedClasses() {
- return Collections.unmodifiableCollection(mergedClasses.keySet());
- }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
new file mode 100644
index 0000000..4b57209
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -0,0 +1,1075 @@
+// Copyright (c) 2017, 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.shaking;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DefaultUseRegistry;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexApplication;
+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.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+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.GraphLense;
+import com.android.tools.r8.graph.GraphLense.Builder;
+import com.android.tools.r8.graph.KeyedDexItem;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.graph.PresortedComparable;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
+import com.android.tools.r8.ir.synthetic.SynthesizedCode;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.optimize.InvokeSingleTargetExtractor;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.utils.FieldSignatureEquivalence;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterators;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+/**
+ * Merges Supertypes with a single implementation into their single subtype.
+ *
+ * <p>A common use-case for this is to merge an interface into its single implementation.
+ *
+ * <p>The class merger only fixes the structure of the graph but leaves the actual instructions
+ * untouched. Fixup of instructions is deferred via a {@link GraphLense} to the IR building phase.
+ */
+public class VerticalClassMerger {
+
+ private final DexApplication application;
+ private final AppInfoWithLiveness appInfo;
+ private final GraphLense graphLense;
+ private final Map<DexType, DexType> mergedClasses = new HashMap<>();
+ private final Timing timing;
+ private Collection<DexMethod> invokes;
+
+ public VerticalClassMerger(
+ DexApplication application,
+ AppInfoWithLiveness appInfo,
+ GraphLense graphLense,
+ Timing timing) {
+ this.application = application;
+ this.appInfo = appInfo;
+ this.graphLense = graphLense;
+ this.timing = timing;
+ }
+
+ // Returns a set of types that must not be merged into other types.
+ private Set<DexType> getPinnedTypes(Iterable<DexProgramClass> classes) {
+ Set<DexType> pinnedTypes = new HashSet<>();
+ for (DexProgramClass clazz : classes) {
+ for (DexEncodedMethod method : clazz.methods()) {
+ // TODO(christofferqa): Remove the invariant that the graph lense should not modify any
+ // methods from the sets alwaysInline and noSideEffects (see use of assertNotModifiedBy-
+ // Lense).
+ if (appInfo.alwaysInline.contains(method) || appInfo.noSideEffects.containsKey(method)) {
+ DexClass other = appInfo.definitionFor(method.method.proto.returnType);
+ if (other != null && other.isProgramClass()) {
+ // If we were to merge [other] into its sub class, then we would implicitly change the
+ // signature of this method, and therefore break the invariant.
+ pinnedTypes.add(other.type);
+ }
+ for (DexType parameterType : method.method.proto.parameters.values) {
+ other = appInfo.definitionFor(parameterType);
+ if (other != null && other.isProgramClass()) {
+ // If we were to merge [other] into its sub class, then we would implicitly change the
+ // signature of this method, and therefore break the invariant.
+ pinnedTypes.add(other.type);
+ }
+ }
+ }
+
+ // Avoid merging two types if this could remove a NoSuchMethodError, as illustrated by the
+ // following example. (Alternatively, it would be possible to merge A and B and rewrite the
+ // "invoke-super A.m" instruction into "invoke-super Object.m" to preserve the error. This
+ // situation should generally not occur in practice, though.)
+ //
+ // class A {}
+ // class B extends A {
+ // public void m() {}
+ // }
+ // class C extends A {
+ // public void m() {
+ // invoke-super "A.m" <- should yield NoSuchMethodError, cannot merge A and B
+ // }
+ // }
+ if (!method.isStaticMethod()) {
+ method.registerCodeReferences(
+ new DefaultUseRegistry() {
+ @Override
+ public boolean registerInvokeSuper(DexMethod target) {
+ DexClass targetClass = appInfo.definitionFor(target.getHolder());
+ if (targetClass != null
+ && targetClass.isProgramClass()
+ && targetClass.lookupVirtualMethod(target) == null) {
+ pinnedTypes.add(target.getHolder());
+ }
+ return true;
+ }
+ });
+ }
+ }
+ }
+ return pinnedTypes;
+ }
+
+ private boolean isMergeCandidate(DexProgramClass clazz, Set<DexType> pinnedTypes) {
+ if (appInfo.instantiatedTypes.contains(clazz.type)
+ || appInfo.isPinned(clazz.type)
+ || pinnedTypes.contains(clazz.type)) {
+ return false;
+ }
+ DexType singleSubtype = clazz.type.getSingleSubtype();
+ if (singleSubtype == null) {
+ // TODO(christofferqa): Even if [clazz] has multiple subtypes, we could still merge it into
+ // its subclass if [clazz] is not live. This should only be done, though, if it does not
+ // lead to members being duplicated.
+ return false;
+ }
+ DexClass targetClass = appInfo.definitionFor(singleSubtype);
+ if (mergeMayLeadToIllegalAccesses(clazz, targetClass)) {
+ return false;
+ }
+ for (DexEncodedField field : clazz.fields()) {
+ if (appInfo.isPinned(field.field)) {
+ return false;
+ }
+ }
+ for (DexEncodedMethod method : clazz.methods()) {
+ if (appInfo.isPinned(method.method)) {
+ return false;
+ }
+ if (method.isInstanceInitializer() && disallowInlining(method)) {
+ // Cannot guarantee that markForceInline() will work.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean mergeMayLeadToIllegalAccesses(DexClass clazz, DexClass singleSubclass) {
+ if (clazz.type.isSamePackage(singleSubclass.type)) {
+ return false;
+ }
+ // TODO(christofferqa): To merge [clazz] into a class from another package we need to ensure:
+ // (A) All accesses to [clazz] and its members from inside the current package of [clazz] will
+ // continue to work. This is guaranteed if [clazz] is public and all members of [clazz] are
+ // either private or public.
+ // (B) All accesses from [clazz] to classes or members from the current package of [clazz] will
+ // continue to work. This is guaranteed if the methods of [clazz] do not access any private
+ // or protected classes or members from the current package of [clazz].
+ return true;
+ }
+
+ private void addProgramMethods(Set<Wrapper<DexMethod>> set, DexMethod method,
+ Equivalence<DexMethod> equivalence) {
+ DexClass definition = appInfo.definitionFor(method.holder);
+ if (definition != null && definition.isProgramClass()) {
+ set.add(equivalence.wrap(method));
+ }
+ }
+
+ private Collection<DexMethod> getInvokes() {
+ if (invokes == null) {
+ // Collect all reachable methods that are not within a library class. Those defined on
+ // library classes are known not to have program classes in their signature.
+ // Also filter methods that only use types from library classes in their signatures. We
+ // know that those won't conflict.
+ Set<Wrapper<DexMethod>> filteredInvokes = new HashSet<>();
+ Equivalence<DexMethod> equivalence = MethodSignatureEquivalence.get();
+ appInfo.targetedMethods.forEach(m -> addProgramMethods(filteredInvokes, m, equivalence));
+ invokes = filteredInvokes.stream().map(Wrapper::get).filter(this::removeNonProgram)
+ .collect(Collectors.toList());
+ }
+ return invokes;
+ }
+
+ private boolean isProgramClass(DexType type) {
+ if (type.isArrayType()) {
+ type = type.toBaseType(appInfo.dexItemFactory);
+ }
+ if (type.isClassType()) {
+ DexClass clazz = appInfo.definitionFor(type);
+ if (clazz != null && clazz.isProgramClass()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean removeNonProgram(DexMethod dexMethod) {
+ for (DexType type : dexMethod.proto.parameters.values) {
+ if (isProgramClass(type)) {
+ return true;
+ }
+ }
+ return isProgramClass(dexMethod.proto.returnType);
+ }
+
+ public GraphLense run() {
+ timing.begin("merge");
+ GraphLense mergingGraphLense = mergeClasses(graphLense);
+ timing.end();
+ timing.begin("fixup");
+ GraphLense result = new TreeFixer().fixupTypeReferences(mergingGraphLense);
+ timing.end();
+ return result;
+ }
+
+ private void addAncestorsToWorklist(
+ DexProgramClass clazz, Deque<DexProgramClass> worklist, Set<DexProgramClass> seenBefore) {
+ if (seenBefore.contains(clazz)) {
+ return;
+ }
+
+ worklist.addFirst(clazz);
+
+ // Add super classes to worklist.
+ if (clazz.superType != null) {
+ DexClass definition = appInfo.definitionFor(clazz.superType);
+ if (definition != null && definition.isProgramClass()) {
+ addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+ }
+ }
+
+ // Add super interfaces to worklist.
+ for (DexType interfaceType : clazz.interfaces.values) {
+ DexClass definition = appInfo.definitionFor(interfaceType);
+ if (definition != null && definition.isProgramClass()) {
+ addAncestorsToWorklist(definition.asProgramClass(), worklist, seenBefore);
+ }
+ }
+ }
+
+ private GraphLense mergeClasses(GraphLense graphLense) {
+ Iterable<DexProgramClass> classes = application.classesWithDeterministicOrder();
+ Deque<DexProgramClass> worklist = new ArrayDeque<>();
+ Set<DexProgramClass> seenBefore = new HashSet<>();
+
+ int numberOfMerges = 0;
+
+ // Types that are pinned (in addition to those where appInfo.isPinned returns true).
+ Set<DexType> pinnedTypes = getPinnedTypes(classes);
+
+ // The resulting graph lense that should be used after class merging.
+ VerticalClassMergerGraphLense.Builder renamedMembersLense =
+ new VerticalClassMergerGraphLense.Builder();
+
+ Iterator<DexProgramClass> classIterator = classes.iterator();
+
+ // Visit the program classes in a top-down order according to the class hierarchy.
+ while (classIterator.hasNext() || !worklist.isEmpty()) {
+ if (worklist.isEmpty()) {
+ // Add the ancestors of this class (including the class itself) to the worklist in such a
+ // way that all super types of the class come before the class itself.
+ addAncestorsToWorklist(classIterator.next(), worklist, seenBefore);
+ if (worklist.isEmpty()) {
+ continue;
+ }
+ }
+
+ DexProgramClass clazz = worklist.removeFirst();
+ if (!seenBefore.add(clazz) || !isMergeCandidate(clazz, pinnedTypes)) {
+ continue;
+ }
+
+ DexClass targetClass = appInfo.definitionFor(clazz.type.getSingleSubtype());
+ assert !mergedClasses.containsKey(targetClass.type);
+ if (appInfo.isPinned(targetClass.type)) {
+ // We have to keep the target class intact, so we cannot merge it.
+ continue;
+ }
+ if (clazz.hasClassInitializer() && targetClass.hasClassInitializer()) {
+ // TODO(herhut): Handle class initializers.
+ if (Log.ENABLED) {
+ Log.info(
+ getClass(),
+ "Cannot merge %s into %s due to static initializers.",
+ clazz.toSourceString(),
+ targetClass.toSourceString());
+ }
+ continue;
+ }
+ // Field resolution first considers the direct interfaces of [targetClass] before it proceeds
+ // to the super class.
+ if (fieldResolutionMayChange(clazz, targetClass)) {
+ continue;
+ }
+ // Guard against the case where we have two methods that may get the same signature
+ // if we replace types. This is rare, so we approximate and err on the safe side here.
+ if (new CollisionDetector(clazz.type, targetClass.type, getInvokes(), mergedClasses)
+ .mayCollide()) {
+ if (Log.ENABLED) {
+ Log.info(
+ getClass(),
+ "Cannot merge %s into %s due to conflict.",
+ clazz.toSourceString(),
+ targetClass.toSourceString());
+ }
+ continue;
+ }
+ ClassMerger merger = new ClassMerger(clazz, targetClass);
+ boolean merged = merger.merge();
+ if (merged) {
+ // Commit the changes to the graph lense.
+ renamedMembersLense.merge(merger.getRenamings());
+ // Do not allow merging the resulting class into its subclass.
+ // TODO(christofferqa): Get rid of this limitation.
+ pinnedTypes.add(targetClass.type);
+ }
+ if (Log.ENABLED) {
+ if (merged) {
+ numberOfMerges++;
+ Log.info(
+ getClass(),
+ "Merged class %s into %s.",
+ clazz.toSourceString(),
+ targetClass.toSourceString());
+ } else {
+ Log.info(
+ getClass(),
+ "Aborted merge for class %s into %s.",
+ clazz.toSourceString(),
+ targetClass.toSourceString());
+ }
+ }
+ }
+ if (Log.ENABLED) {
+ Log.debug(getClass(), "Merged %d classes.", numberOfMerges);
+ }
+ return renamedMembersLense.build(graphLense);
+ }
+
+ private boolean fieldResolutionMayChange(DexClass source, DexClass target) {
+ if (source.type == target.superType) {
+ // If there is a "iget Target.f" or "iput Target.f" instruction in target, and the class
+ // Target implements an interface that declares a static final field f, this should yield an
+ // IncompatibleClassChangeError.
+ // TODO(christofferqa): In the following we only check if a static field from an interface
+ // shadows an instance field from [source]. We could actually check if there is an iget/iput
+ // instruction whose resolution would be affected by the merge. The situation where a static
+ // field shadows an instance field is probably not widespread in practice, though.
+ FieldSignatureEquivalence equivalence = FieldSignatureEquivalence.get();
+ Set<Wrapper<DexField>> staticFieldsInInterfacesOfTarget = new HashSet<>();
+ for (DexType interfaceType : target.interfaces.values) {
+ DexClass clazz = appInfo.definitionFor(interfaceType);
+ for (DexEncodedField staticField : clazz.staticFields()) {
+ staticFieldsInInterfacesOfTarget.add(equivalence.wrap(staticField.field));
+ }
+ }
+ for (DexEncodedField instanceField : source.instanceFields()) {
+ if (staticFieldsInInterfacesOfTarget.contains(equivalence.wrap(instanceField.field))) {
+ // An instruction "iget Target.f" or "iput Target.f" that used to hit a static field in an
+ // interface would now hit an instance field from [source], so that an IncompatibleClass-
+ // ChangeError would no longer be thrown. Abort merge.
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private class ClassMerger {
+
+ private static final String CONSTRUCTOR_NAME = "constructor";
+
+ private final DexClass source;
+ private final DexClass target;
+ private final VerticalClassMergerGraphLense.Builder deferredRenamings =
+ new VerticalClassMergerGraphLense.Builder();
+ private boolean abortMerge = false;
+
+ private ClassMerger(DexClass source, DexClass target) {
+ this.source = source;
+ this.target = target;
+ }
+
+ public boolean merge() {
+ if (source.getEnclosingMethod() != null || !source.getInnerClasses().isEmpty()
+ || target.getEnclosingMethod() != null || !target.getInnerClasses().isEmpty()) {
+ // TODO(herhut): Consider supporting merging of inner-class attributes.
+ return false;
+ }
+ // Merge the class [clazz] into [targetClass] by adding all methods to
+ // targetClass that are not currently contained.
+ // Step 1: Merge methods
+ Set<Wrapper<DexMethod>> existingMethods = new HashSet<>();
+ addAll(existingMethods, target.methods(), MethodSignatureEquivalence.get());
+
+ List<DexEncodedMethod> directMethods = new ArrayList<>();
+ for (DexEncodedMethod directMethod : source.directMethods()) {
+ if (directMethod.isInstanceInitializer()) {
+ directMethods.add(renameConstructor(directMethod));
+ } else {
+ directMethods.add(directMethod);
+ }
+ }
+
+ List<DexEncodedMethod> virtualMethods = new ArrayList<>();
+ for (DexEncodedMethod virtualMethod : source.virtualMethods()) {
+ DexEncodedMethod shadowedBy = findMethodInTarget(virtualMethod);
+ if (shadowedBy != null) {
+ if (virtualMethod.accessFlags.isAbstract()) {
+ // Remove abstract/interface methods that are shadowed.
+ deferredRenamings.map(virtualMethod.method, shadowedBy.method);
+ continue;
+ }
+ } else {
+ // The method is shadowed. If it is abstract, we can simply move it to the subclass.
+ // Non-abstract methods are handled below (they cannot simply be moved to the subclass as
+ // a virtual method, because they might be the target of an invoke-super instruction).
+ if (virtualMethod.accessFlags.isAbstract()) {
+ DexEncodedMethod resultingVirtualMethod =
+ renameMethod(virtualMethod, target.type, false);
+ deferredRenamings.map(virtualMethod.method, resultingVirtualMethod.method);
+ virtualMethods.add(resultingVirtualMethod);
+ continue;
+ }
+ }
+
+ // This virtual method could be called directly from a sub class via an invoke-super
+ // instruction. Therefore, we translate this virtual method into a direct method, such that
+ // relevant invoke-super instructions can be rewritten into invoke-direct instructions.
+ DexEncodedMethod resultingDirectMethod = renameMethod(virtualMethod, target.type, true);
+ makePrivate(resultingDirectMethod);
+ directMethods.add(resultingDirectMethod);
+
+ // Record that invoke-super instructions in the target class should be redirected to the
+ // newly created direct method.
+ redirectSuperCallsInTarget(virtualMethod.method, resultingDirectMethod.method);
+
+ if (shadowedBy == null) {
+ // In addition to the newly added direct method, create a virtual method such that we do
+ // not accidentally remove the method from the interface of this class.
+ // Note that this method is added independently of whether it will actually be used. If
+ // it turns out that the method is never used, it will be removed by the final round
+ // of tree shaking.
+ shadowedBy = buildBridgeMethod(virtualMethod, resultingDirectMethod.method);
+ virtualMethods.add(shadowedBy);
+ }
+
+ deferredRenamings.map(virtualMethod.method, shadowedBy.method);
+ }
+
+ Collection<DexEncodedMethod> mergedDirectMethods =
+ mergeItems(
+ directMethods.iterator(),
+ target.directMethods(),
+ MethodSignatureEquivalence.get(),
+ existingMethods,
+ (existing, method) -> {
+ DexEncodedMethod renamedMethod = renameMethod(method, target.type, true);
+ deferredRenamings.map(method.method, renamedMethod.method);
+ return renamedMethod;
+ });
+ Collection<DexEncodedMethod> mergedVirtualMethods =
+ mergeItems(
+ virtualMethods.iterator(),
+ target.virtualMethods(),
+ MethodSignatureEquivalence.get(),
+ existingMethods,
+ this::abortOnNonAbstract);
+ if (abortMerge) {
+ return false;
+ }
+ // Step 2: Merge fields
+ Set<Wrapper<DexField>> existingFields = new HashSet<>();
+ addAll(existingFields, target.fields(), FieldSignatureEquivalence.get());
+ Collection<DexEncodedField> mergedStaticFields = mergeItems(
+ Iterators.forArray(source.staticFields()),
+ target.staticFields(),
+ FieldSignatureEquivalence.get(),
+ existingFields,
+ this::renameField);
+ Collection<DexEncodedField> mergedInstanceFields = mergeItems(
+ Iterators.forArray(source.instanceFields()),
+ target.instanceFields(),
+ FieldSignatureEquivalence.get(),
+ existingFields,
+ this::renameField);
+ // Step 3: Merge interfaces
+ Set<DexType> interfaces = mergeArrays(target.interfaces.values, source.interfaces.values);
+ // Now destructively update the class.
+ // Step 1: Update supertype or fix interfaces.
+ if (source.isInterface()) {
+ interfaces.remove(source.type);
+ } else {
+ assert !target.isInterface();
+ target.superType = source.superType;
+ }
+ target.interfaces = interfaces.isEmpty()
+ ? DexTypeList.empty()
+ : new DexTypeList(interfaces.toArray(new DexType[interfaces.size()]));
+ // Step 2: replace fields and methods.
+ target.setDirectMethods(mergedDirectMethods
+ .toArray(new DexEncodedMethod[mergedDirectMethods.size()]));
+ target.setVirtualMethods(mergedVirtualMethods
+ .toArray(new DexEncodedMethod[mergedVirtualMethods.size()]));
+ target.setStaticFields(mergedStaticFields
+ .toArray(new DexEncodedField[mergedStaticFields.size()]));
+ target.setInstanceFields(mergedInstanceFields
+ .toArray(new DexEncodedField[mergedInstanceFields.size()]));
+ // Step 3: Unlink old class to ease tree shaking.
+ source.superType = application.dexItemFactory.objectType;
+ source.setDirectMethods(null);
+ source.setVirtualMethods(null);
+ source.setInstanceFields(null);
+ source.setStaticFields(null);
+ source.interfaces = DexTypeList.empty();
+ // Step 4: Record merging.
+ mergedClasses.put(source.type, target.type);
+ return true;
+ }
+
+ public VerticalClassMergerGraphLense.Builder getRenamings() {
+ return deferredRenamings;
+ }
+
+ private void redirectSuperCallsInTarget(DexMethod oldTarget, DexMethod newTarget) {
+ // If we merge class B into class C, and class C contains an invocation super.m(), then it
+ // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
+ // C.m$B denotes the direct method that has been created in C for B.m). In particular, there
+ // might be an instruction "invoke-super A.m()" in C that resolves to B.m at runtime (A is
+ // a superclass of B), which also needs to be rewritten to "invoke-direct C.m$B()".
+ //
+ // We handle this by adding a mapping for [target] and all of its supertypes.
+ DexClass holder = target;
+ while (holder != null && holder.isProgramClass()) {
+ DexMethod signatureInHolder =
+ application.dexItemFactory.createMethod(holder.type, oldTarget.proto, oldTarget.name);
+ // Only rewrite the invoke-super call if it does not lead to a NoSuchMethodError.
+ if (resolutionSucceeds(signatureInHolder)) {
+ deferredRenamings.mapVirtualMethodToDirectInType(
+ signatureInHolder, newTarget, target.type);
+ holder = holder.superType != null ? appInfo.definitionFor(holder.superType) : null;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private boolean resolutionSucceeds(DexMethod targetMethod) {
+ DexClass enclosingClass = appInfo.definitionFor(targetMethod.holder);
+ return enclosingClass != null
+ && (enclosingClass.lookupVirtualMethod(targetMethod) != null
+ || appInfo.lookupSuperTarget(targetMethod, enclosingClass.type) != null);
+ }
+
+ private DexEncodedMethod buildBridgeMethod(
+ DexEncodedMethod signature, DexMethod invocationTarget) {
+ DexType holder = target.type;
+ DexProto proto = invocationTarget.proto;
+ DexString name = signature.method.name;
+ MethodAccessFlags accessFlags = signature.accessFlags.copy();
+ accessFlags.setBridge();
+ accessFlags.setSynthetic();
+ accessFlags.unsetAbstract();
+ return new DexEncodedMethod(
+ application.dexItemFactory.createMethod(holder, proto, name),
+ accessFlags,
+ DexAnnotationSet.empty(),
+ ParameterAnnotationsList.empty(),
+ new SynthesizedCode(
+ new ForwardMethodSourceCode(holder, proto, holder, invocationTarget, Type.DIRECT),
+ registry -> registry.registerInvokeDirect(invocationTarget)));
+ }
+
+ // Returns the method that shadows the given method, or null if method is not shadowed.
+ private DexEncodedMethod findMethodInTarget(DexEncodedMethod method) {
+ DexEncodedMethod actual = appInfo.resolveMethod(target.type, method.method).asSingleTarget();
+ if (actual == null) {
+ // May happen in case of missing classes.
+ abortMerge = true;
+ return null;
+ }
+ if (actual != method) {
+ return actual;
+ }
+ // We will keep the method, so the class better be abstract if there is no implementation.
+ assert !method.accessFlags.isAbstract() || target.accessFlags.isAbstract();
+ return null;
+ }
+
+ private <T extends KeyedDexItem<S>, S extends PresortedComparable<S>> void addAll(
+ Collection<Wrapper<S>> collection, Iterable<T> items, Equivalence<S> equivalence) {
+ for (T item : items) {
+ collection.add(equivalence.wrap(item.getKey()));
+ }
+ }
+
+ private <T> Set<T> mergeArrays(T[] one, T[] other) {
+ Set<T> merged = new LinkedHashSet<>();
+ Collections.addAll(merged, one);
+ Collections.addAll(merged, other);
+ return merged;
+ }
+
+ private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> Collection<S> mergeItems(
+ Iterator<S> fromItems,
+ S[] toItems,
+ Equivalence<T> equivalence,
+ Set<Wrapper<T>> existing,
+ BiFunction<S, S, S> onConflict) {
+ HashMap<Wrapper<T>, S> methods = new HashMap<>();
+ // First add everything from the target class. These items are not preprocessed.
+ for (S item : toItems) {
+ methods.put(equivalence.wrap(item.getKey()), item);
+ }
+ // Now add the new methods, resolving shadowing.
+ addNonShadowed(fromItems, methods, equivalence, existing, onConflict);
+ return methods.values();
+ }
+
+ private <T extends PresortedComparable<T>, S extends KeyedDexItem<T>> void addNonShadowed(
+ Iterator<S> items,
+ HashMap<Wrapper<T>, S> map,
+ Equivalence<T> equivalence,
+ Set<Wrapper<T>> existing,
+ BiFunction<S, S, S> onConflict) {
+ while (items.hasNext()) {
+ S item = items.next();
+ if (item == null) {
+ // This item was filtered out by a preprocessing.
+ continue;
+ }
+ Wrapper<T> wrapped = equivalence.wrap(item.getKey());
+ if (existing.contains(wrapped)) {
+ S resolved = onConflict.apply(map.get(wrapped), item);
+ wrapped = equivalence.wrap(resolved.getKey());
+ map.put(wrapped, resolved);
+ } else {
+ map.put(wrapped, item);
+ }
+ }
+ }
+
+ // Note that names returned by this function are not necessarily unique. However, class merging
+ // is aborted if it turns out that the returned name is not unique.
+ // TODO(christofferqa): Write a test that checks that class merging is aborted if this function
+ // generates a name that is not unique.
+ private DexString getFreshName(String nameString, DexType holder) {
+ String freshName = nameString + "$" + holder.toSourceString().replace('.', '$');
+ return application.dexItemFactory.createString(freshName);
+ }
+
+ private DexEncodedMethod abortOnNonAbstract(DexEncodedMethod existing,
+ DexEncodedMethod method) {
+ // Ignore if we override a bridge method that would bridge to the superclasses method.
+ if (existing != null && existing.accessFlags.isBridge()) {
+ InvokeSingleTargetExtractor extractor = new InvokeSingleTargetExtractor();
+ existing.getCode().registerCodeReferences(extractor);
+ if (extractor.getTarget() == method.method) {
+ return method;
+ }
+ }
+
+ // Abstract shadowed methods are removed prior to calling mergeItems.
+ assert !method.accessFlags.isAbstract();
+
+ // If [existing] is null, there is a conflict between a static and virtual method. Otherwise,
+ // there is a non-abstract method that is shadowed. We translate virtual shadowed methods into
+ // direct methods with a fresh name, so this situation should never happen. We currently get
+ // in this situation if getFreshName returns a name that is not unique, though.
+ abortMerge = true;
+ return method;
+ }
+
+ private DexEncodedMethod renameConstructor(DexEncodedMethod method) {
+ assert method.isInstanceInitializer();
+ DexType holder = method.method.holder;
+ DexEncodedMethod result =
+ method.toRenamedMethod(
+ getFreshName(CONSTRUCTOR_NAME, holder), application.dexItemFactory);
+ result.markForceInline();
+ deferredRenamings.map(method.method, result.method);
+ // Renamed constructors turn into ordinary private functions. They can be private, as
+ // they are only references from their direct subclass, which they were merged into.
+ result.accessFlags.unsetConstructor();
+ makePrivate(result);
+ return result;
+ }
+
+ private DexEncodedMethod renameMethod(
+ DexEncodedMethod method, DexType newHolder, boolean useFreshName) {
+ // We cannot handle renaming static initializers yet and constructors should have been
+ // renamed already.
+ assert !method.accessFlags.isConstructor();
+ DexType oldHolder = method.method.holder;
+ DexString oldName = method.method.name;
+ DexString newName =
+ useFreshName ? getFreshName(oldName.toSourceString(), oldHolder) : oldName;
+ DexMethod newSignature =
+ application.dexItemFactory.createMethod(newHolder, method.method.proto, newName);
+ return method.toTypeSubstitutedMethod(newSignature);
+ }
+
+ private DexEncodedField renameField(DexEncodedField existing, DexEncodedField field) {
+ DexString oldName = field.field.name;
+ DexType oldHolder = field.field.clazz;
+ DexString newName = getFreshName(oldName.toSourceString(), oldHolder);
+ DexField newSignature =
+ application.dexItemFactory.createField(target.type, field.field.type, newName);
+ DexEncodedField result = field.toTypeSubstitutedField(newSignature);
+ deferredRenamings.map(field.field, result.field);
+ return result;
+ }
+ }
+
+ private static void makePrivate(DexEncodedMethod method) {
+ assert !method.accessFlags.isAbstract();
+ method.accessFlags.unsetPublic();
+ method.accessFlags.unsetProtected();
+ method.accessFlags.setPrivate();
+ }
+
+ private class TreeFixer {
+
+ private final Builder lense = GraphLense.builder();
+ Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
+
+ private GraphLense fixupTypeReferences(GraphLense graphLense) {
+ // Globally substitute merged class types in protos and holders.
+ for (DexProgramClass clazz : appInfo.classes()) {
+ clazz.setDirectMethods(substituteTypesIn(clazz.directMethods()));
+ clazz.setVirtualMethods(substituteTypesIn(clazz.virtualMethods()));
+ clazz.setVirtualMethods(removeDupes(clazz.virtualMethods()));
+ clazz.setStaticFields(substituteTypesIn(clazz.staticFields()));
+ clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields()));
+ }
+ // Record type renamings so instanceof and checkcast checks are also fixed.
+ for (DexType type : mergedClasses.keySet()) {
+ DexType fixed = fixupType(type);
+ lense.map(type, fixed);
+ }
+ return lense.build(application.dexItemFactory, graphLense);
+ }
+
+ private DexEncodedMethod[] removeDupes(DexEncodedMethod[] methods) {
+ if (methods == null) {
+ return null;
+ }
+ Map<DexMethod, DexEncodedMethod> filtered = new IdentityHashMap<>();
+ for (DexEncodedMethod method : methods) {
+ DexEncodedMethod previous = filtered.put(method.method, method);
+ if (previous != null) {
+ if (!previous.accessFlags.isBridge()) {
+ if (!method.accessFlags.isBridge()) {
+ throw new CompilationError(
+ "Class merging produced invalid result on: " + previous.toSourceString());
+ } else {
+ filtered.put(previous.method, previous);
+ }
+ }
+ }
+ }
+ if (filtered.size() == methods.length) {
+ return methods;
+ }
+ return filtered.values().toArray(new DexEncodedMethod[filtered.size()]);
+ }
+
+ private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
+ if (methods == null) {
+ return null;
+ }
+ for (int i = 0; i < methods.length; i++) {
+ DexEncodedMethod encodedMethod = methods[i];
+ DexMethod method = encodedMethod.method;
+ DexProto newProto = getUpdatedProto(method.proto);
+ DexType newHolder = fixupType(method.holder);
+ DexMethod newMethod = application.dexItemFactory.createMethod(newHolder, newProto,
+ method.name);
+ if (newMethod != encodedMethod.method) {
+ lense.map(encodedMethod.method, newMethod);
+ methods[i] = encodedMethod.toTypeSubstitutedMethod(newMethod);
+ }
+ }
+ return methods;
+ }
+
+ private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) {
+ if (fields == null) {
+ return null;
+ }
+ for (int i = 0; i < fields.length; i++) {
+ DexEncodedField encodedField = fields[i];
+ DexField field = encodedField.field;
+ DexType newType = fixupType(field.type);
+ DexType newHolder = fixupType(field.clazz);
+ DexField newField = application.dexItemFactory.createField(newHolder, newType, field.name);
+ if (newField != encodedField.field) {
+ lense.map(encodedField.field, newField);
+ fields[i] = encodedField.toTypeSubstitutedField(newField);
+ }
+ }
+ return fields;
+ }
+
+ private DexProto getUpdatedProto(DexProto proto) {
+ DexProto result = protoFixupCache.get(proto);
+ if (result == null) {
+ DexType returnType = fixupType(proto.returnType);
+ DexType[] arguments = fixupTypes(proto.parameters.values);
+ result = application.dexItemFactory.createProto(returnType, arguments);
+ protoFixupCache.put(proto, result);
+ }
+ return result;
+ }
+
+ private DexType fixupType(DexType type) {
+ if (type.isArrayType()) {
+ DexType base = type.toBaseType(application.dexItemFactory);
+ DexType fixed = fixupType(base);
+ if (base == fixed) {
+ return type;
+ } else {
+ return type.replaceBaseType(fixed, application.dexItemFactory);
+ }
+ }
+ while (mergedClasses.containsKey(type)) {
+ type = mergedClasses.get(type);
+ }
+ return type;
+ }
+
+ private DexType[] fixupTypes(DexType[] types) {
+ DexType[] result = new DexType[types.length];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = fixupType(types[i]);
+ }
+ return result;
+ }
+ }
+
+ private static class CollisionDetector {
+
+ private static final int NOT_FOUND = 1 << (Integer.SIZE - 1);
+
+ // TODO(herhut): Maybe cache seenPositions for target classes.
+ private final Map<DexString, Int2IntMap> seenPositions = new IdentityHashMap<>();
+ private final Reference2IntMap<DexProto> targetProtoCache;
+ private final Reference2IntMap<DexProto> sourceProtoCache;
+ private final DexType source, target;
+ private final Collection<DexMethod> invokes;
+ private final Map<DexType, DexType> substituions;
+
+ private CollisionDetector(DexType source, DexType target, Collection<DexMethod> invokes,
+ Map<DexType, DexType> substitutions) {
+ this.source = source;
+ this.target = target;
+ this.invokes = invokes;
+ this.substituions = substitutions;
+ this.targetProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+ this.targetProtoCache.defaultReturnValue(NOT_FOUND);
+ this.sourceProtoCache = new Reference2IntOpenHashMap<>(invokes.size() / 2);
+ this.sourceProtoCache.defaultReturnValue(NOT_FOUND);
+ }
+
+ boolean mayCollide() {
+ fillSeenPositions(invokes);
+ // If the type is not used in methods at all, there cannot be any conflict.
+ if (seenPositions.isEmpty()) {
+ return false;
+ }
+ for (DexMethod method : invokes) {
+ Int2IntMap positionsMap = seenPositions.get(method.name);
+ if (positionsMap != null) {
+ int arity = method.getArity();
+ int previous = positionsMap.get(arity);
+ if (previous != NOT_FOUND) {
+ assert previous != 0;
+ int positions = computePositionsFor(method.proto, source, sourceProtoCache,
+ substituions);
+ if ((positions & previous) != 0) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private void fillSeenPositions(Collection<DexMethod> invokes) {
+ for (DexMethod method : invokes) {
+ DexType[] parameters = method.proto.parameters.values;
+ int arity = parameters.length;
+ int positions = computePositionsFor(method.proto, target, targetProtoCache, substituions);
+ if (positions != 0) {
+ Int2IntMap positionsMap =
+ seenPositions.computeIfAbsent(method.name, k -> {
+ Int2IntMap result = new Int2IntOpenHashMap();
+ result.defaultReturnValue(NOT_FOUND);
+ return result;
+ });
+ int value = 0;
+ int previous = positionsMap.get(arity);
+ if (previous != NOT_FOUND) {
+ value = previous;
+ }
+ value |= positions;
+ positionsMap.put(arity, value);
+ }
+ }
+
+ }
+
+ private int computePositionsFor(DexProto proto, DexType type,
+ Reference2IntMap<DexProto> cache, Map<DexType, DexType> substitutions) {
+ int result = cache.getInt(proto);
+ if (result != NOT_FOUND) {
+ return result;
+ }
+ result = 0;
+ int bitsUsed = 0;
+ int accumulator = 0;
+ for (DexType aType : proto.parameters.values) {
+ if (substitutions != null) {
+ // Substitute the type with the already merged class to estimate what it will
+ // look like.
+ while (substitutions.containsKey(aType)) {
+ aType = substitutions.get(aType);
+ }
+ }
+ accumulator <<= 1;
+ bitsUsed++;
+ if (aType == type) {
+ accumulator |= 1;
+ }
+ // Handle overflow on 31 bit boundary.
+ if (bitsUsed == Integer.SIZE - 1) {
+ result |= accumulator;
+ accumulator = 0;
+ bitsUsed = 0;
+ }
+ }
+ // We also take the return type into account for potential conflicts.
+ DexType returnType = proto.returnType;
+ if (substitutions != null) {
+ while (substitutions.containsKey(returnType)) {
+ returnType = substitutions.get(returnType);
+ }
+ }
+ accumulator <<= 1;
+ if (returnType == type) {
+ accumulator |= 1;
+ }
+ result |= accumulator;
+ cache.put(proto, result);
+ return result;
+ }
+ }
+
+ private static boolean disallowInlining(DexEncodedMethod method) {
+ // TODO(christofferqa): Determine the situations where markForceInline() may fail, and ensure
+ // that we always return true here in these cases.
+ MethodInlineDecision registry = new MethodInlineDecision();
+ method.getCode().registerCodeReferences(registry);
+ return registry.isInliningDisallowed();
+ }
+
+ private static class MethodInlineDecision extends UseRegistry {
+ private boolean disallowInlining = false;
+
+ public boolean isInliningDisallowed() {
+ return disallowInlining;
+ }
+
+ private boolean allowInlining() {
+ return true;
+ }
+
+ private boolean disallowInlining() {
+ disallowInlining = true;
+ return true;
+ }
+
+ @Override
+ public boolean registerInvokeInterface(DexMethod method) {
+ return disallowInlining();
+ }
+
+ @Override
+ public boolean registerInvokeVirtual(DexMethod method) {
+ return disallowInlining();
+ }
+
+ @Override
+ public boolean registerInvokeDirect(DexMethod method) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerInvokeStatic(DexMethod method) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerInvokeSuper(DexMethod method) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerInstanceFieldWrite(DexField field) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerInstanceFieldRead(DexField field) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerNewInstance(DexType type) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerStaticFieldRead(DexField field) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerStaticFieldWrite(DexField field) {
+ return allowInlining();
+ }
+
+ @Override
+ public boolean registerTypeReference(DexType type) {
+ return allowInlining();
+ }
+ }
+
+ public Collection<DexType> getRemovedClasses() {
+ return Collections.unmodifiableCollection(mergedClasses.keySet());
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
new file mode 100644
index 0000000..ea57d1d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLense.java
@@ -0,0 +1,175 @@
+// 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.shaking;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+// This graph lense is instantiated during vertical class merging. The graph lense is context
+// sensitive in the enclosing class of a given invoke *and* the type of the invoke (e.g., invoke-
+// super vs invoke-virtual). This is illustrated by the following example.
+//
+// public class A {
+// public void m() { ... }
+// }
+// public class B extends A {
+// @Override
+// public void m() { invoke-super A.m(); ... }
+//
+// public void m2() { invoke-virtual A.m(); ... }
+// }
+//
+// Vertical class merging will merge class A into class B. Since class B already has a method with
+// the signature "void B.m()", the method A.m will be given a fresh name and moved to class B.
+// During this process, the method corresponding to A.m will be made private such that it can be
+// called via an invoke-direct instruction.
+//
+// For the invocation "invoke-super A.m()" in B.m, this graph lense will return the newly created,
+// private method corresponding to A.m (that is now in B.m with a fresh name), such that the
+// invocation will hit the same implementation as the original super.m() call.
+//
+// For the invocation "invoke-virtual A.m()" in B.m2, this graph lense will return the method B.m.
+public class VerticalClassMergerGraphLense extends GraphLense {
+ private final GraphLense previousLense;
+
+ private final Map<DexField, DexField> fieldMap;
+ private final Map<DexMethod, DexMethod> methodMap;
+ private final Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps;
+
+ public VerticalClassMergerGraphLense(
+ Map<DexField, DexField> fieldMap,
+ Map<DexMethod, DexMethod> methodMap,
+ Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps,
+ GraphLense previousLense) {
+ this.previousLense = previousLense;
+ this.fieldMap = fieldMap;
+ this.methodMap = methodMap;
+ this.contextualVirtualToDirectMethodMaps = contextualVirtualToDirectMethodMaps;
+ }
+
+ @Override
+ public DexType lookupType(DexType type) {
+ return previousLense.lookupType(type);
+ }
+
+ @Override
+ public DexMethod lookupMethod(DexMethod method, DexEncodedMethod context, Type type) {
+ assert isContextFreeForMethod(method) || (context != null && type != null);
+ DexMethod previous = previousLense.lookupMethod(method, context, type);
+ if (type == Type.SUPER) {
+ Map<DexMethod, DexMethod> virtualToDirectMethodMap =
+ contextualVirtualToDirectMethodMaps.get(context.method.holder);
+ if (virtualToDirectMethodMap != null) {
+ DexMethod directMethod = virtualToDirectMethodMap.get(previous);
+ if (directMethod != null) {
+ return directMethod;
+ }
+ }
+ }
+ return methodMap.getOrDefault(previous, previous);
+ }
+
+ @Override
+ public Set<DexMethod> lookupMethodInAllContexts(DexMethod method) {
+ ImmutableSet.Builder<DexMethod> builder = ImmutableSet.builder();
+ for (DexMethod previous : previousLense.lookupMethodInAllContexts(method)) {
+ builder.add(methodMap.getOrDefault(previous, previous));
+ for (Map<DexMethod, DexMethod> virtualToDirectMethodMap :
+ contextualVirtualToDirectMethodMaps.values()) {
+ DexMethod directMethod = virtualToDirectMethodMap.get(previous);
+ if (directMethod != null) {
+ builder.add(directMethod);
+ }
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public DexField lookupField(DexField field) {
+ DexField previous = previousLense.lookupField(field);
+ return fieldMap.getOrDefault(previous, previous);
+ }
+
+ @Override
+ public boolean isContextFreeForMethods() {
+ return contextualVirtualToDirectMethodMaps.isEmpty() && previousLense.isContextFreeForMethods();
+ }
+
+ @Override
+ public boolean isContextFreeForMethod(DexMethod method) {
+ if (!previousLense.isContextFreeForMethod(method)) {
+ return false;
+ }
+ DexMethod previous = previousLense.lookupMethod(method);
+ for (Map<DexMethod, DexMethod> virtualToDirectMethodMap :
+ contextualVirtualToDirectMethodMaps.values()) {
+ if (virtualToDirectMethodMap.containsKey(previous)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static class Builder {
+
+ private final ImmutableMap.Builder<DexField, DexField> fieldMapBuilder = ImmutableMap.builder();
+ private final ImmutableMap.Builder<DexMethod, DexMethod> methodMapBuilder =
+ ImmutableMap.builder();
+ private final Map<DexType, Map<DexMethod, DexMethod>> contextualVirtualToDirectMethodMaps =
+ new HashMap<>();
+
+ public Builder() {}
+
+ public GraphLense build(GraphLense previousLense) {
+ Map<DexField, DexField> fieldMap = fieldMapBuilder.build();
+ Map<DexMethod, DexMethod> methodMap = methodMapBuilder.build();
+ if (fieldMap.isEmpty()
+ && methodMap.isEmpty()
+ && contextualVirtualToDirectMethodMaps.isEmpty()) {
+ return previousLense;
+ }
+ return new VerticalClassMergerGraphLense(
+ fieldMap, methodMap, contextualVirtualToDirectMethodMaps, previousLense);
+ }
+
+ public void map(DexField from, DexField to) {
+ fieldMapBuilder.put(from, to);
+ }
+
+ public void map(DexMethod from, DexMethod to) {
+ methodMapBuilder.put(from, to);
+ }
+
+ public void mapVirtualMethodToDirectInType(DexMethod from, DexMethod to, DexType type) {
+ Map<DexMethod, DexMethod> virtualToDirectMethodMap =
+ contextualVirtualToDirectMethodMaps.computeIfAbsent(type, key -> new HashMap<>());
+ virtualToDirectMethodMap.put(from, to);
+ }
+
+ public void merge(VerticalClassMergerGraphLense.Builder builder) {
+ fieldMapBuilder.putAll(builder.fieldMapBuilder.build());
+ methodMapBuilder.putAll(builder.methodMapBuilder.build());
+ for (DexType context : builder.contextualVirtualToDirectMethodMaps.keySet()) {
+ Map<DexMethod, DexMethod> current = contextualVirtualToDirectMethodMaps.get(context);
+ Map<DexMethod, DexMethod> other = builder.contextualVirtualToDirectMethodMaps.get(context);
+ if (current != null) {
+ current.putAll(other);
+ } else {
+ contextualVirtualToDirectMethodMaps.put(context, other);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index 499ba47..a611105 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -4,8 +4,6 @@
package com.android.tools.r8.utils;
import static com.android.tools.r8.utils.FileUtils.isArchive;
-import static com.android.tools.r8.utils.FileUtils.isClassFile;
-import static com.android.tools.r8.utils.FileUtils.isDexFile;
import com.android.tools.r8.DataDirectoryResource;
import com.android.tools.r8.DataEntryResource;
@@ -22,8 +20,7 @@
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -49,23 +46,22 @@
private List<ProgramResource> readArchive() throws IOException {
List<ProgramResource> dexResources = new ArrayList<>();
List<ProgramResource> classResources = new ArrayList<>();
- try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
+ try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
try (InputStream stream = zipFile.getInputStream(entry)) {
String name = entry.getName();
- Path path = Paths.get(name);
Origin entryOrigin = new ArchiveEntryOrigin(name, origin);
- if (archive.matchesFile(path)) {
- if (isDexFile(path)) {
+ if (archive.matchesFile(name)) {
+ if (ZipUtils.isDexFile(name)) {
if (!ignoreDexInArchive) {
ProgramResource resource =
OneShotByteResource.create(
Kind.DEX, entryOrigin, ByteStreams.toByteArray(stream), null);
dexResources.add(resource);
}
- } else if (isClassFile(path)) {
+ } else if (ZipUtils.isClassFile(name)) {
String descriptor = DescriptorUtils.guessTypeDescriptor(name);
ProgramResource resource =
OneShotByteResource.create(
@@ -106,11 +102,11 @@
@Override
public void accept(Visitor resourceBrowser) throws ResourceException {
- try (ZipFile zipFile = new ZipFile(archive.getPath().toFile())) {
+ try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
- Path name = Paths.get(entry.getName());
+ String name = entry.getName();
if (archive.matchesFile(name) && !isProgramResourceName(name)) {
if (entry.isDirectory()) {
resourceBrowser.visit(DataDirectoryResource.fromZip(zipFile, entry));
@@ -128,7 +124,7 @@
}
}
- private boolean isProgramResourceName(Path name) {
- return isClassFile(name) || (isDexFile(name) && !ignoreDexInArchive);
+ private boolean isProgramResourceName(String name) {
+ return ZipUtils.isClassFile(name) || (ZipUtils.isDexFile(name) && !ignoreDexInArchive);
}
}
diff --git a/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
index f933b42..0c6cc14 100644
--- a/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/FilteredArchiveClassFileProvider.java
@@ -6,12 +6,11 @@
import com.android.tools.r8.ArchiveClassFileProvider;
import com.android.tools.r8.shaking.FilteredClassPath;
import java.io.IOException;
-import java.nio.file.Paths;
// Internal filtered class-file provider.
class FilteredArchiveClassFileProvider extends ArchiveClassFileProvider {
FilteredArchiveClassFileProvider(FilteredClassPath archive) throws IOException {
- super(archive.getPath(), entry -> archive.matchesFile(Paths.get(entry)));
+ super(archive.getPath(), entry -> archive.matchesFile(entry));
}
}
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index b938596..f10ff69 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -20,6 +20,7 @@
import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNaming;
+import com.android.tools.r8.naming.ClassNaming.Builder;
import com.android.tools.r8.naming.MemberNaming;
import com.android.tools.r8.naming.MemberNaming.FieldSignature;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -187,6 +188,22 @@
}
}
+ // We will be remapping positional debug events and collect them as MappedPositions.
+ private static class MappedPosition {
+ private final DexMethod method;
+ private final int originalLine;
+ private final Position caller;
+ private final int obfuscatedLine;
+
+ private MappedPosition(
+ DexMethod method, int originalLine, Position caller, int obfuscatedLine) {
+ this.method = method;
+ this.originalLine = originalLine;
+ this.caller = caller;
+ this.obfuscatedLine = obfuscatedLine;
+ }
+ }
+
public static ClassNameMapper run(
DexApplication application, NamingLens namingLens, boolean identityMapping) {
IdentityHashMap<DexString, List<DexProgramClass>> classesOfFiles = new IdentityHashMap<>();
@@ -199,25 +216,8 @@
continue;
}
- // Group methods by name
IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
- new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
- clazz.forEachMethod(
- method -> {
- // Add method only if renamed or contains positions.
- if (namingLens.lookupName(method.method) != method.method.name
- || doesContainPositions(method)) {
- methodsByName.compute(
- method.method.name,
- (name, methods) -> {
- if (methods == null) {
- methods = new ArrayList<>();
- }
- methods.add(method);
- return methods;
- });
- }
- });
+ groupMethodsByName(namingLens, clazz);
// At this point we don't know if we really need to add this class to the builder.
// It depends on whether any methods/fields are renamed or some methods contain positions.
@@ -230,24 +230,11 @@
DescriptorUtils.descriptorToJavaType(renamedClassName.toString()),
clazz.toString()));
- // We do know we need to create a ClassNaming.Builder if the class itself had been renamed.
- if (!clazz.toString().equals(renamedClassName.toString())) {
- // Not using return value, it's registered in classNameMapperBuilder
- onDemandClassNamingBuilder.get();
- }
+ // If the class is renamed add it to the classNamingBuilder.
+ addClassToClassNaming(clazz, renamedClassName, onDemandClassNamingBuilder);
// First transfer renamed fields to classNamingBuilder.
- clazz.forEachField(
- dexEncodedField -> {
- DexField dexField = dexEncodedField.field;
- DexString renamedName = namingLens.lookupName(dexField);
- if (renamedName != dexField.name) {
- FieldSignature signature =
- new FieldSignature(dexField.name.toString(), dexField.type.toString());
- MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
- onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
- }
- });
+ addFieldsToClassNaming(namingLens, clazz, onDemandClassNamingBuilder);
// Then process the methods.
for (List<DexEncodedMethod> methods : methodsByName.values()) {
@@ -256,47 +243,13 @@
// deterministic behaviour: the algorithm will assign new line numbers in this order.
// Methods with different names can share the same line numbers, that's why they don't
// need to be sorted.
- methods.sort(
- (lhs, rhs) -> {
- // Sort by startline, then DexEncodedMethod.slowCompare.
- // Use startLine = 0 if no debuginfo.
- Code lhsCode = lhs.getCode();
- Code rhsCode = rhs.getCode();
- DexCode lhsDexCode =
- lhsCode == null || !lhsCode.isDexCode() ? null : lhsCode.asDexCode();
- DexCode rhsDexCode =
- rhsCode == null || !rhsCode.isDexCode() ? null : rhsCode.asDexCode();
- DexDebugInfo lhsDebugInfo = lhsDexCode == null ? null : lhsDexCode.getDebugInfo();
- DexDebugInfo rhsDebugInfo = rhsDexCode == null ? null : rhsDexCode.getDebugInfo();
- int lhsStartLine = lhsDebugInfo == null ? 0 : lhsDebugInfo.startLine;
- int rhsStartLine = rhsDebugInfo == null ? 0 : rhsDebugInfo.startLine;
- int startLineDiff = lhsStartLine - rhsStartLine;
- if (startLineDiff != 0) return startLineDiff;
- return DexEncodedMethod.slowCompare(lhs, rhs);
- });
+ sortMethods(methods);
}
PositionRemapper positionRemapper =
identityMapping ? new IdentityPositionRemapper() : new OptimizingPositionRemapper();
for (DexEncodedMethod method : methods) {
-
- // We will be remapping positional debug events and collect them as MappedPositions.
- class MappedPosition {
- private final DexMethod method;
- private final int originalLine;
- private final Position caller;
- private final int obfuscatedLine;
-
- private MappedPosition(
- DexMethod method, int originalLine, Position caller, int obfuscatedLine) {
- this.method = method;
- this.originalLine = originalLine;
- this.caller = caller;
- this.obfuscatedLine = obfuscatedLine;
- }
- }
-
List<MappedPosition> mappedPositions = new ArrayList<>();
if (doesContainPositions(method)) {
@@ -429,6 +382,75 @@
return classNameMapperBuilder.build();
}
+ // Sort by startline, then DexEncodedMethod.slowCompare.
+ // Use startLine = 0 if no debuginfo.
+ private static void sortMethods(List<DexEncodedMethod> methods) {
+ methods.sort(
+ (lhs, rhs) -> {
+ Code lhsCode = lhs.getCode();
+ Code rhsCode = rhs.getCode();
+ DexCode lhsDexCode =
+ lhsCode == null || !lhsCode.isDexCode() ? null : lhsCode.asDexCode();
+ DexCode rhsDexCode =
+ rhsCode == null || !rhsCode.isDexCode() ? null : rhsCode.asDexCode();
+ DexDebugInfo lhsDebugInfo = lhsDexCode == null ? null : lhsDexCode.getDebugInfo();
+ DexDebugInfo rhsDebugInfo = rhsDexCode == null ? null : rhsDexCode.getDebugInfo();
+ int lhsStartLine = lhsDebugInfo == null ? 0 : lhsDebugInfo.startLine;
+ int rhsStartLine = rhsDebugInfo == null ? 0 : rhsDebugInfo.startLine;
+ int startLineDiff = lhsStartLine - rhsStartLine;
+ if (startLineDiff != 0) return startLineDiff;
+ return DexEncodedMethod.slowCompare(lhs, rhs);
+ });
+ }
+
+ @SuppressWarnings("ReturnValueIgnored")
+ private static void addClassToClassNaming(DexProgramClass clazz, DexString renamedClassName,
+ Supplier<Builder> onDemandClassNamingBuilder) {
+ // We do know we need to create a ClassNaming.Builder if the class itself had been renamed.
+ if (!clazz.toString().equals(renamedClassName.toString())) {
+ // Not using return value, it's registered in classNameMapperBuilder
+ onDemandClassNamingBuilder.get();
+ }
+ }
+
+ private static void addFieldsToClassNaming(NamingLens namingLens, DexProgramClass clazz,
+ Supplier<Builder> onDemandClassNamingBuilder) {
+ clazz.forEachField(
+ dexEncodedField -> {
+ DexField dexField = dexEncodedField.field;
+ DexString renamedName = namingLens.lookupName(dexField);
+ if (renamedName != dexField.name) {
+ FieldSignature signature =
+ new FieldSignature(dexField.name.toString(), dexField.type.toString());
+ MemberNaming memberNaming = new MemberNaming(signature, renamedName.toString());
+ onDemandClassNamingBuilder.get().addMemberEntry(memberNaming);
+ }
+ });
+ }
+
+ private static IdentityHashMap<DexString, List<DexEncodedMethod>> groupMethodsByName(
+ NamingLens namingLens, DexProgramClass clazz) {
+ IdentityHashMap<DexString, List<DexEncodedMethod>> methodsByName =
+ new IdentityHashMap<>(clazz.directMethods().length + clazz.virtualMethods().length);
+ clazz.forEachMethod(
+ method -> {
+ // Add method only if renamed or contains positions.
+ if (namingLens.lookupName(method.method) != method.method.name
+ || doesContainPositions(method)) {
+ methodsByName.compute(
+ method.method.name,
+ (name, methods) -> {
+ if (methods == null) {
+ methods = new ArrayList<>();
+ }
+ methods.add(method);
+ return methods;
+ });
+ }
+ });
+ return methodsByName;
+ }
+
private static boolean doesContainPositions(DexEncodedMethod method) {
Code code = method.getCode();
if (code == null || !code.isDexCode()) {
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index 1b032e5..fd50583 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -3,6 +3,10 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.DEX_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.MODULE_INFO_CLASS;
+
import com.android.tools.r8.errors.CompilationError;
import com.google.common.io.ByteStreams;
import java.io.File;
@@ -10,6 +14,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Enumeration;
@@ -27,7 +32,7 @@
}
public static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
- try (ZipFile zipFile = new ZipFile(zipFileStr)) {
+ try (ZipFile zipFile = new ZipFile(zipFileStr, StandardCharsets.UTF_8)) {
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
@@ -91,4 +96,17 @@
stream.write(content);
stream.closeEntry();
}
+
+ public static boolean isDexFile(String entry) {
+ String name = entry.toLowerCase();
+ return name.endsWith(DEX_EXTENSION);
+ }
+
+ public static boolean isClassFile(String entry) {
+ String name = entry.toLowerCase();
+ if (name.endsWith(MODULE_INFO_CLASS)) {
+ return false;
+ }
+ return name.endsWith(CLASS_EXTENSION);
+ }
}
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
index ad31005..23dc7e0 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8ApiUsageSample.java
@@ -22,6 +22,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -387,11 +388,12 @@
for (Path file : files) {
if (isArchive(file)) {
Origin zipOrigin = new PathOrigin(file);
- ZipInputStream zip = new ZipInputStream(Files.newInputStream(file));
+ ZipInputStream zip = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8);
ZipEntry entry;
while (null != (entry = zip.getNextEntry())) {
- if (isClassFile(Paths.get(entry.getName()))) {
- Origin origin = new ArchiveEntryOrigin(entry.getName(), zipOrigin);
+ String name = entry.getName();
+ if (isClassFile(name)) {
+ Origin origin = new ArchiveEntryOrigin(name, zipOrigin);
classfiles.add(new ClassFileContent(origin, readBytes(zip)));
}
}
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
index 7230f55..7bce10a 100644
--- a/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
+++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/R8ApiUsageSample.java
@@ -22,6 +22,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -378,11 +379,12 @@
for (Path file : files) {
if (isArchive(file)) {
Origin zipOrigin = new PathOrigin(file);
- ZipInputStream zip = new ZipInputStream(Files.newInputStream(file));
+ ZipInputStream zip = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8);
ZipEntry entry;
while (null != (entry = zip.getNextEntry())) {
- if (isClassFile(Paths.get(entry.getName()))) {
- Origin origin = new ArchiveEntryOrigin(entry.getName(), zipOrigin);
+ String name = entry.getName();
+ if (isClassFile(name)) {
+ Origin origin = new ArchiveEntryOrigin(name, zipOrigin);
classfiles.add(new ClassFileContent(origin, readBytes(zip)));
}
}
diff --git a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
new file mode 100644
index 0000000..223ff37
--- /dev/null
+++ b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
@@ -0,0 +1,32 @@
+// 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 classmerging;
+
+public class ConflictingInterfaceSignaturesTest {
+
+ public static void main(String[] args) {
+ A a = new InterfaceImpl();
+ a.foo();
+
+ B b = new InterfaceImpl();
+ b.foo();
+ }
+
+ public interface A {
+ void foo();
+ }
+
+ public interface B {
+ void foo();
+ }
+
+ public static final class InterfaceImpl implements A, B {
+
+ @Override
+ public void foo() {
+ System.out.println("In foo on InterfaceImpl");
+ }
+ }
+}
diff --git a/src/test/examples/classmerging/ExceptionTest.java b/src/test/examples/classmerging/ExceptionTest.java
new file mode 100644
index 0000000..28442f3
--- /dev/null
+++ b/src/test/examples/classmerging/ExceptionTest.java
@@ -0,0 +1,56 @@
+// 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 classmerging;
+
+public class ExceptionTest {
+ public static void main(String[] args) {
+ // The following will lead to a catch handler for ExceptionA, which is merged into ExceptionB.
+ try {
+ doSomethingThatMightThrowExceptionB();
+ doSomethingThatMightThrowException2();
+ } catch (ExceptionB exception) {
+ System.out.println("Caught exception: " + exception.getMessage());
+ } catch (ExceptionA exception) {
+ System.out.println("Caught exception: " + exception.getMessage());
+ } catch (Exception2 exception) {
+ System.out.println("Caught exception: " + exception.getMessage());
+ } catch (Exception1 exception) {
+ System.out.println("Caught exception: " + exception.getMessage());
+ }
+ }
+
+ private static void doSomethingThatMightThrowExceptionB() throws ExceptionB {
+ throw new ExceptionB("Ouch!");
+ }
+
+ private static void doSomethingThatMightThrowException2() throws Exception2 {
+ throw new Exception2("Ouch!");
+ }
+
+ // Will be merged into ExceptionB when class merging is enabled.
+ public static class ExceptionA extends Exception {
+ public ExceptionA(String message) {
+ super(message);
+ }
+ }
+
+ public static class ExceptionB extends ExceptionA {
+ public ExceptionB(String message) {
+ super(message);
+ }
+ }
+
+ public static class Exception1 extends Exception {
+ public Exception1(String message) {
+ super(message);
+ }
+ }
+
+ public static class Exception2 extends Exception1 {
+ public Exception2(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/test/examples/classmerging/SimpleInterface.java b/src/test/examples/classmerging/SimpleInterface.java
new file mode 100644
index 0000000..e8ebcab
--- /dev/null
+++ b/src/test/examples/classmerging/SimpleInterface.java
@@ -0,0 +1,9 @@
+// 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 classmerging;
+
+public interface SimpleInterface {
+ void foo();
+}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
new file mode 100644
index 0000000..04b5386
--- /dev/null
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -0,0 +1,16 @@
+// 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 classmerging;
+
+import classmerging.pkg.SimpleInterfaceImplRetriever;
+
+public class SimpleInterfaceAccessTest {
+ public static void main(String[] args) {
+ // It is not possible to merge the interface SimpleInterface into SimpleInterfaceImpl, since
+ // this would lead to an illegal class access here.
+ SimpleInterface obj = SimpleInterfaceImplRetriever.getSimpleInterfaceImpl();
+ obj.foo();
+ }
+}
diff --git a/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java b/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
index c12804c..c09b11a 100644
--- a/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
+++ b/src/test/examples/classmerging/SubClassThatReferencesSuperMethod.java
@@ -7,6 +7,9 @@
@Override
public String referencedMethod() {
- return "From sub: " + super.referencedMethod();
+ System.out.println("In referencedMethod on SubClassThatReferencesSuperMethod");
+ System.out.println("Calling referencedMethod on SuperClassWithReferencedMethod with super");
+ System.out.println("Got: " + super.referencedMethod());
+ return "SubClassThatReferencesSuperMethod.referencedMethod()";
}
}
diff --git a/src/test/examples/classmerging/SuperCallRewritingTest.java b/src/test/examples/classmerging/SuperCallRewritingTest.java
new file mode 100644
index 0000000..7a3d45c
--- /dev/null
+++ b/src/test/examples/classmerging/SuperCallRewritingTest.java
@@ -0,0 +1,12 @@
+// 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 classmerging;
+
+public class SuperCallRewritingTest {
+ public static void main(String[] args) {
+ System.out.println("Calling referencedMethod on SubClassThatReferencesSuperMethod");
+ System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
+ }
+}
diff --git a/src/test/examples/classmerging/SuperClassWithReferencedMethod.java b/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
index 8d4e7b5..3bd5886c84 100644
--- a/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
+++ b/src/test/examples/classmerging/SuperClassWithReferencedMethod.java
@@ -6,6 +6,7 @@
public class SuperClassWithReferencedMethod {
public String referencedMethod() {
- return "From Super";
+ System.out.println("In referencedMethod on SuperClassWithReferencedMethod");
+ return "SuperClassWithReferencedMethod.referencedMethod()";
}
}
diff --git a/src/test/examples/classmerging/TemplateMethodTest.java b/src/test/examples/classmerging/TemplateMethodTest.java
new file mode 100644
index 0000000..ac9de15
--- /dev/null
+++ b/src/test/examples/classmerging/TemplateMethodTest.java
@@ -0,0 +1,31 @@
+// 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 classmerging;
+
+public class TemplateMethodTest {
+
+ public static void main(String[] args) {
+ AbstractClass obj = new AbstractClassImpl();
+ obj.foo();
+ }
+
+ private abstract static class AbstractClass {
+
+ public void foo() {
+ System.out.println("In foo on AbstractClass");
+ bar();
+ }
+
+ protected abstract void bar();
+ }
+
+ public static final class AbstractClassImpl extends AbstractClass {
+
+ @Override
+ public void bar() {
+ System.out.println("In bar on AbstractClassImpl");
+ }
+ }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 1cc5b87..01e35f3 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -7,6 +7,21 @@
-keep public class classmerging.Test {
public static void main(...);
}
+-keep public class classmerging.ConflictingInterfaceSignaturesTest {
+ public static void main(...);
+}
+-keep public class classmerging.ExceptionTest {
+ public static void main(...);
+}
+-keep public class classmerging.SimpleInterfaceAccessTest {
+ public static void main(...);
+}
+-keep public class classmerging.SuperCallRewritingTest {
+ public static void main(...);
+}
+-keep public class classmerging.TemplateMethodTest {
+ public static void main(...);
+}
# TODO(herhut): Consider supporting merging of inner-class attributes.
# -keepattributes *
\ No newline at end of file
diff --git a/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java b/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java
new file mode 100644
index 0000000..5cfa11e
--- /dev/null
+++ b/src/test/examples/classmerging/pkg/SimpleInterfaceImplRetriever.java
@@ -0,0 +1,25 @@
+// 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 classmerging.pkg;
+
+import classmerging.SimpleInterface;
+
+public class SimpleInterfaceImplRetriever {
+
+ public static SimpleInterface getSimpleInterfaceImpl() {
+ return new SimpleInterfaceImpl();
+ }
+
+ // This class is intentionally marked private. It is not possible to merge the interface
+ // SimpleInterface into SimpleInterfaceImpl, since this would lead to an illegal class access
+ // in SimpleInterfaceAccessTest.
+ private static class SimpleInterfaceImpl implements SimpleInterface {
+
+ @Override
+ public void foo() {
+ System.out.println("In foo on SimpleInterfaceImpl");
+ }
+ }
+}
diff --git a/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
new file mode 100644
index 0000000..781d2b4
--- /dev/null
+++ b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
@@ -0,0 +1,57 @@
+// 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 uninitializedfinal;
+
+// Test that leaks an instance before its final field has been initialized to a thread that
+// reads that field. This tests that redundant field load elimination does not eliminate
+// field reads (even of final fields) that cross a monitor operation.
+public class UninitializedFinalFieldLeak {
+
+ public static class PollingThread extends Thread {
+ public int result = 0;
+ UninitializedFinalFieldLeak f;
+
+ PollingThread(UninitializedFinalFieldLeak f) {
+ this.f = f;
+ }
+
+ // Read the field a number of times. Then lock on the object to await field initialization.
+ public void run() {
+ result += f.i;
+ result += f.i;
+ result += f.i;
+ f.threadReadsDone = true;
+ synchronized (f) {
+ result += f.i;
+ }
+ // The right result is 42. Reading the uninitialized 0 three times and then
+ // reading the initialized value. It is safe to remove the two redundant loads
+ // before the monitor operation.
+ System.out.println(result);
+ }
+ }
+
+ public final int i;
+ public volatile boolean threadReadsDone = false;
+
+ public UninitializedFinalFieldLeak() throws InterruptedException {
+ // Leak the object to a thread and start the thread with the lock on the object taken.
+ // Then allow the other thread to run and read the uninitialized field.
+ // Finally, initialize the field and release the lock.
+ PollingThread t = new PollingThread(this);
+ synchronized (this) {
+ t.start();
+ while (!threadReadsDone) {
+ Thread.yield();
+ }
+ i = 42;
+ }
+ t.join();
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+ new UninitializedFinalFieldLeak();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
new file mode 100644
index 0000000..6ceb0a2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ArchiveClassFileProviderTest.java
@@ -0,0 +1,43 @@
+// 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;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class ArchiveClassFileProviderTest {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ public Path createZip() throws IOException {
+ Path tempRoot = temporaryFolder.getRoot().toPath();
+ Path zipFile = tempRoot.resolve("zipfile.zip");
+ ZipOutputStream zipStream =
+ new ZipOutputStream(new FileOutputStream(zipFile.toFile()), StandardCharsets.UTF_8);
+ ZipEntry entry = new ZipEntry("non-ascii:$\u02CF");
+ zipStream.putNextEntry(entry);
+ zipStream.write(10);
+ zipStream.close();
+ return zipFile;
+ }
+
+ @Test
+ public void testSystemLocale() throws IOException, ResourceException {
+ // Set the locale used for Paths to ASCII which will make Path creation fail
+ // for non-ascii names.
+ System.setProperty("sun.jnu.encoding", "ASCII");
+ Path zipFile = createZip();
+ new ArchiveClassFileProvider(zipFile);
+ ArchiveProgramResourceProvider.fromArchive(zipFile).getProgramResources();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index 4e07849..4f749b5 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -85,6 +85,11 @@
}
@Test
+ public void testInlining() throws Exception {
+ runTest("inlining.Inlining");
+ }
+
+ @Test
public void testInstanceVariable() throws Exception {
runTest("instancevariable.InstanceVariable");
}
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index 1b846a5..87453d0 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -6,13 +6,13 @@
import static com.android.tools.r8.R8CommandTest.getOutputPath;
import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR;
import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import com.android.sdklib.AndroidVersion;
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.dex.Marker;
import com.android.tools.r8.dex.Marker.Tool;
@@ -23,8 +23,8 @@
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@@ -263,13 +263,37 @@
tmpClassesDir.toString());
AndroidApp inputApp = ToolHelper.getApp(command);
assertEquals(1, inputApp.getClasspathResourceProviders().size());
+ OrderedClassFileResourceProvider classpathProvider =
+ (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+ assertEquals(1, classpathProvider.providers.size());
assertTrue(Files.isSameFile(tmpClassesDir,
- ((DirectoryClassFileProvider) inputApp.getClasspathResourceProviders().get(0)).getRoot()));
+ ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
assertEquals(1, inputApp.getLibraryResourceProviders().size());
assertTrue(Files.isSameFile(tmpClassesDir,
((DirectoryClassFileProvider) inputApp.getLibraryResourceProviders().get(0)).getRoot()));
}
+ @Test
+ public void folderClasspathMultiple() throws Throwable {
+ Path inputFile =
+ Paths.get(ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR, "interfacemethods" + JAR_EXTENSION);
+ Path tmpClassesDir1 = temp.newFolder().toPath();
+ Path tmpClassesDir2 = temp.newFolder().toPath();
+ ZipUtils.unzip(inputFile.toString(), tmpClassesDir1.toFile());
+ ZipUtils.unzip(inputFile.toString(), tmpClassesDir2.toFile());
+ D8Command command = parse("--classpath", tmpClassesDir1.toString(), "--classpath",
+ tmpClassesDir2.toString());
+ AndroidApp inputApp = ToolHelper.getApp(command);
+ assertEquals(1, inputApp.getClasspathResourceProviders().size());
+ OrderedClassFileResourceProvider classpathProvider =
+ (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+ assertEquals(2, classpathProvider.providers.size());
+ assertTrue(Files.isSameFile(tmpClassesDir1,
+ ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
+ assertTrue(Files.isSameFile(tmpClassesDir2,
+ ((DirectoryClassFileProvider) classpathProvider.providers.get(1)).getRoot()));
+ }
+
@Test(expected = CompilationFailedException.class)
public void classFolderProgram() throws Throwable {
Path inputFile =
@@ -319,7 +343,7 @@
Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar");
ProgramResourceProvider myProvider =
ArchiveProgramResourceProvider.fromSupplier(
- new MyOrigin(), () -> new ZipFile(input.toFile()));
+ new MyOrigin(), () -> new ZipFile(input.toFile(), StandardCharsets.UTF_8));
D8Command command =
D8Command.builder()
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
@@ -464,7 +488,7 @@
.setOutput(emptyZip, OutputMode.DexIndexed)
.build());
assertTrue(Files.exists(emptyZip));
- assertEquals(0, new ZipFile(emptyZip.toFile()).size());
+ assertEquals(0, new ZipFile(emptyZip.toFile(), StandardCharsets.UTF_8).size());
}
private D8Command parse(String... args) throws CompilationFailedException {
diff --git a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
index 258aa43..ee82fc4 100644
--- a/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
+++ b/src/test/java/com/android/tools/r8/JctfTestSpecifications.java
@@ -1920,11 +1920,6 @@
// 1) t03
// java.lang.AssertionError: expected null, but was:<[I@1b32f32>
- .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01",
- match(runtimes(Version.DEFAULT, Version.V7_0_0, Version.V6_0_1, Version.V5_1_1)))
- // 1) t03
- // java.lang.AssertionError: reference is not enqueued after 2 sec
-
.put("lang.StackTraceElement.toString.StackTraceElement_toString_A01",
match(runtimes(Version.DEFAULT)))
// 1) t03
@@ -4959,8 +4954,9 @@
.put("lang.ref.PhantomReference.isEnqueued.PhantomReference_isEnqueued_A01",
match(runtimesUpTo(Version.V4_4_4)))
- .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01",
- match(runtimesUpTo(Version.V4_4_4)))
+ .put("lang.ref.WeakReference.isEnqueued.WeakReference_isEnqueued_A01", any())
+ // 1) t03
+ // java.lang.AssertionError: reference is not enqueued after 2 sec
.put("lang.ref.WeakReference.enqueue.WeakReference_enqueue_A01",
match(runtimesUpTo(Version.V4_4_4)))
diff --git a/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
new file mode 100644
index 0000000..dc497c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
@@ -0,0 +1,113 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+
+public class OrderedClassFileResourceProviderTest extends TestBase {
+ class SimpleClassFileResourceProvider implements ClassFileResourceProvider {
+
+ private final Set<String> descriptors;
+ private final ProgramResource fixedProgramResource;
+
+ SimpleClassFileResourceProvider(int id, Set<String> descriptors) {
+ this.descriptors = descriptors;
+ this.fixedProgramResource = new SimpleProgramResource(id);
+ }
+
+ @Override
+ public Set<String> getClassDescriptors() {
+ return descriptors;
+ }
+
+ @Override
+ public ProgramResource getProgramResource(String descriptor) {
+ return fixedProgramResource;
+ }
+ }
+
+ class SimpleProgramResource implements ProgramResource {
+
+ private final Origin origin;
+
+ SimpleProgramResource(int id) {
+ origin = new SimpleOrigin(id);
+ }
+
+ @Override
+ public Kind getKind() {
+ return null;
+ }
+
+ @Override
+ public InputStream getByteStream() throws ResourceException {
+ return null;
+ }
+
+ @Override
+ public Set<String> getClassDescriptors() {
+ return null;
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return origin;
+ }
+ }
+
+ public class SimpleOrigin extends Origin {
+
+ private final int id;
+
+ private SimpleOrigin(int index) {
+ super(root());
+ this.id = index;
+ }
+
+ int getId() {
+ return id;
+ }
+
+ @Override
+ public String part() {
+ return "Test";
+ }
+ }
+
+ @Test
+ public void test() {
+ OrderedClassFileResourceProvider.Builder builder = OrderedClassFileResourceProvider.builder();
+ builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(1, ImmutableSet.of(
+ "L/a/a/a", "L/a/a/b", "L/a/a/c"
+ )));
+ builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(2, ImmutableSet.of(
+ "L/a/a/b", "L/a/a/c", "L/a/a/d"
+ )));
+ ClassFileResourceProvider provider = builder.build();
+ assertEquals(
+ ImmutableSet.of("L/a/a/a", "L/a/a/b", "L/a/a/c", "L/a/a/d"),
+ provider.getClassDescriptors());
+
+ Map<String, Integer> expectations = ImmutableMap.of(
+ "L/a/a/a", 1,
+ "L/a/a/b", 1,
+ "L/a/a/c", 1,
+ "L/a/a/d", 2
+ );
+ expectations.forEach((descriptor, id) ->
+ assertEquals(
+ (int) id,
+ ((SimpleOrigin) provider.getProgramResource(descriptor).getOrigin()).getId()));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/R8CommandTest.java b/src/test/java/com/android/tools/r8/R8CommandTest.java
index fbd88be..16bbae9 100644
--- a/src/test/java/com/android/tools/r8/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/R8CommandTest.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -459,7 +460,7 @@
.setOutput(emptyZip, OutputMode.DexIndexed)
.build());
assertTrue(Files.exists(emptyZip));
- assertEquals(0, new ZipFile(emptyZip.toFile()).size());
+ assertEquals(0, new ZipFile(emptyZip.toFile(), StandardCharsets.UTF_8).size());
}
private R8Command parse(String... args) throws CompilationFailedException {
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 348dbf1..36d924f 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -41,12 +41,15 @@
"filledarray.FilledArray",
"hello.Hello",
"ifstatements.IfStatements",
+ "inlining.Inlining",
"instancevariable.InstanceVariable",
"instanceofstring.InstanceofString",
"invoke.Invoke",
+ "invokeempty.InvokeEmpty",
"jumbostring.JumboString",
"loadconst.LoadConst",
"loop.UdpServer",
+ "nestedtrycatches.NestedTryCatches",
"newarray.NewArray",
"regalloc.RegAlloc",
"returns.Returns",
@@ -57,9 +60,7 @@
"throwing.Throwing",
"trivial.Trivial",
"trycatch.TryCatch",
- "nestedtrycatches.NestedTryCatches",
"trycatchmany.TryCatchMany",
- "invokeempty.InvokeEmpty",
"regress.Regress",
"regress2.Regress2",
"regress_37726195.Regress",
@@ -80,6 +81,7 @@
"enclosingmethod_proguarded.Main",
"interfaceinlining.Main",
"switchmaps.Switches",
+ "uninitializedfinal.UninitializedFinalFieldLeak",
};
List<String[]> fullTestList = new ArrayList<>(tests.length * 2);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 2c6a72a..b0876c9 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -27,6 +27,7 @@
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -580,7 +581,7 @@
protected DexInspector getMainDexInspector(Path zip)
throws ZipException, IOException, ExecutionException {
- try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+ try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
try (InputStream in =
zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index 182fe80..4701ac3 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -25,6 +25,7 @@
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -256,7 +257,7 @@
protected DexInspector getMainDexInspector(Path zip)
throws ZipException, IOException, ExecutionException {
- try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+ try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
try (InputStream in =
zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 284ed68..9560c1f 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -25,6 +25,7 @@
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -309,7 +310,7 @@
protected DexInspector getMainDexInspector(Path zip)
throws ZipException, IOException, ExecutionException {
- try (ZipFile zipFile = new ZipFile(zip.toFile())) {
+ try (ZipFile zipFile = new ZipFile(zip.toFile(), StandardCharsets.UTF_8)) {
try (InputStream in =
zipFile.getInputStream(zipFile.getEntry(ToolHelper.DEFAULT_DEX_FILENAME))) {
return new DexInspector(
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index fbf2d02..5200a09 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -151,6 +151,11 @@
return builder.build();
}
+ /** Build an AndroidApp from the specified program files. */
+ protected AndroidApp readProgramFiles(Path... programFiles) throws IOException {
+ return AndroidApp.builder().addProgramFiles(programFiles).build();
+ }
+
/**
* Create a temporary JAR file containing the specified test classes.
*/
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 000076e..d5d0558 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -807,7 +807,9 @@
public static R8Command.Builder prepareR8CommandBuilder(
AndroidApp app, ProgramConsumer programConsumer) {
- return R8Command.builder(app).setProgramConsumer(programConsumer);
+ return R8Command.builder(app)
+ .setProgramConsumer(programConsumer)
+ .setProguardMapConsumer(StringConsumer.emptyConsumer());
}
public static AndroidApp runR8(AndroidApp app) throws IOException {
@@ -1413,6 +1415,11 @@
return result.stdout;
}
+ public static ProcessResult runProguardRaw(
+ Path inJar, Path outJar, Path lib, Path config, Path map) throws IOException {
+ return runProguardRaw(getProguardScript(), inJar, outJar, lib, ImmutableList.of(config), map);
+ }
+
public static ProcessResult runProguardRaw(Path inJar, Path outJar, List<Path> config, Path map)
throws IOException {
return runProguardRaw(getProguardScript(), inJar, outJar, config, map);
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java
new file mode 100644
index 0000000..160ad59
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/EmptyBridgeTest.java
@@ -0,0 +1,63 @@
+// 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.bridgeremoval;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+public class EmptyBridgeTest extends TestBase {
+
+ @Test
+ public void test() throws Exception {
+ JasminBuilder jasminBuilder = new JasminBuilder();
+
+ ClassBuilder abs = jasminBuilder.addClass("Abs");
+ abs.setAccess("public abstract");
+ abs.addMethod("public bridge abstract", "emptyBridge", ImmutableList.of(), "V");
+
+ ClassBuilder cls = jasminBuilder.addClass("Main");
+ cls.addVirtualMethod("bogus", ImmutableList.of(), "V",
+ ".limit stack 3",
+ ".limit locals 2",
+ "getstatic java/lang/System/out Ljava/io/PrintStream;",
+ "ldc \"foo\"",
+ "invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
+ "return");
+ cls.addMainMethod(
+ ".limit stack 4",
+ ".limit locals 2",
+ "new " + cls.name,
+ "dup",
+ "invokespecial " + cls.name + "/<init>()V",
+ "astore_0",
+ "aload_0",
+ "invokevirtual " + cls.name + "/bogus()V",
+ "return");
+
+ String absClassName = abs.name;
+ String mainClassName = cls.name;
+ String proguardConfig =
+ "-allowaccessmodification" + System.lineSeparator()
+ + "-keep class " + mainClassName + "{ *; }" + System.lineSeparator()
+ + "-keep class " + absClassName + "{ *; }";
+ AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig);
+
+ DexInspector inspector = new DexInspector(processedApp);
+ ClassSubject classSubject = inspector.clazz(absClassName);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method("void", "emptyBridge", ImmutableList.of());
+ assertThat(methodSubject, isPresent());
+ }
+
+}
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index 6069b4c..da32cce 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -31,7 +31,6 @@
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
-
@RunWith(Parameterized.class)
public class MethodHandleTestRunner extends TestBase {
static final Class<?> CLASS = MethodHandleTest.class;
@@ -169,7 +168,9 @@
Arrays.asList(
"-keep public class com.android.tools.r8.cf.MethodHandleTest {",
" public static void main(...);",
- "}"),
+ "}",
+ // Disallow merging MethodHandleTest$I into MethodHandleTest$Impl
+ "-keep public interface com.android.tools.r8.cf.MethodHandleTest$I"),
Origin.unknown());
}
try {
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index d5d2c39..7016163 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -3,29 +3,48 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.classmerging;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.OutputMode;
import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.shaking.ProguardRuleParserException;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.MoveException;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.smali.SmaliBuilder;
+import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.FoundClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
-import org.junit.Rule;
+import org.junit.Ignore;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-public class ClassMergingTest {
+// TODO(christofferqa): Add tests to check that statically typed invocations on method handles
+// continue to work after class merging. Rewriting of method handles should be carried out by
+// LensCodeRewriter.rewriteDexMethodHandle.
+public class ClassMergingTest extends TestBase {
+ private static final Path CF_DIR =
+ Paths.get(ToolHelper.BUILD_DIR).resolve("classes/examples/classmerging");
private static final Path EXAMPLE_JAR = Paths.get(ToolHelper.EXAMPLES_BUILD_DIR)
.resolve("classmerging.jar");
private static final Path EXAMPLE_KEEP = Paths.get(ToolHelper.EXAMPLES_DIR)
@@ -33,17 +52,14 @@
private static final Path DONT_OPTIMIZE = Paths.get(ToolHelper.EXAMPLES_DIR)
.resolve("classmerging").resolve("keep-rules-dontoptimize.txt");
- @Rule
- public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
-
private void configure(InternalOptions options) {
options.enableClassMerging = true;
options.enableClassInlining = false;
+ options.enableMinification = false;
}
private void runR8(Path proguardConfig, Consumer<InternalOptions> optionsConsumer)
- throws IOException, ProguardRuleParserException, ExecutionException,
- CompilationFailedException {
+ throws IOException, ExecutionException, CompilationFailedException {
ToolHelper.runR8(
R8Command.builder()
.setOutput(Paths.get(temp.getRoot().getCanonicalPath()), OutputMode.DexIndexed)
@@ -73,13 +89,12 @@
assertFalse(inspector.clazz(candidate).isPresent());
}
assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
- assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent());
assertTrue(inspector.clazz("classmerging.SubClass").isPresent());
}
@Test
- public void testClassesShouldNotMerged() throws Exception {
+ public void testClassesHaveNotBeenMerged() throws Exception {
runR8(DONT_OPTIMIZE, null);
for (String candidate : CAN_BE_MERGED) {
assertTrue(inspector.clazz(candidate).isPresent());
@@ -95,9 +110,243 @@
@Test
public void testSuperCallWasDetected() throws Exception {
- runR8(EXAMPLE_KEEP, this::configure);
- assertTrue(inspector.clazz("classmerging.SuperClassWithReferencedMethod").isPresent());
- assertTrue(inspector.clazz("classmerging.SubClassThatReferencesSuperMethod").isPresent());
+ String main = "classmerging.SuperCallRewritingTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("SubClassThatReferencesSuperMethod.class"),
+ CF_DIR.resolve("SuperClassWithReferencedMethod.class"),
+ CF_DIR.resolve("SuperCallRewritingTest.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.SubClassThatReferencesSuperMethod",
+ "classmerging.SuperCallRewritingTest");
+ runTest(main, programFiles, preservedClassNames);
}
+ // When a subclass A has been merged into its subclass B, we rewrite invoke-super calls that hit
+ // methods in A to invoke-direct calls. However, we should be careful not to transform invoke-
+ // super instructions into invoke-direct instructions simply because the static target is a method
+ // in the enclosing class.
+ //
+ // This test hand-crafts an invoke-super instruction in SubClassThatReferencesSuperMethod that
+ // targets SubClassThatReferencesSuperMethod.referencedMethod. When running without class
+ // merging, R8 should not rewrite the invoke-super instruction into invoke-direct.
+ @Test
+ public void testSuperCallNotRewrittenToDirect() throws Exception {
+ String main = "classmerging.SuperCallRewritingTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("SuperClassWithReferencedMethod.class"),
+ CF_DIR.resolve("SuperCallRewritingTest.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.SubClassThatReferencesSuperMethod",
+ "classmerging.SuperClassWithReferencedMethod",
+ "classmerging.SuperCallRewritingTest");
+
+ // Build SubClassThatReferencesMethod.
+ SmaliBuilder smaliBuilder =
+ new SmaliBuilder(
+ "classmerging.SubClassThatReferencesSuperMethod",
+ "classmerging.SuperClassWithReferencedMethod");
+ smaliBuilder.addInitializer(
+ ImmutableList.of(),
+ 0,
+ "invoke-direct {p0}, Lclassmerging/SuperClassWithReferencedMethod;-><init>()V",
+ "return-void");
+ smaliBuilder.addInstanceMethod(
+ "java.lang.String",
+ "referencedMethod",
+ ImmutableList.of(),
+ 2,
+ "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;",
+ "const-string v1, \"In referencedMethod on SubClassThatReferencesSuperMethod\"",
+ "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V",
+ "invoke-super {p0}, Lclassmerging/SubClassThatReferencesSuperMethod;->referencedMethod()Ljava/lang/String;",
+ "move-result-object v1",
+ "return-object v1");
+
+ // Build app.
+ AndroidApp.Builder builder = AndroidApp.builder();
+ builder.addProgramFiles(programFiles);
+ builder.addDexProgramData(smaliBuilder.compile(), Origin.unknown());
+
+ // Run test.
+ runTestOnInput(
+ main,
+ builder.build(),
+ preservedClassNames,
+ // Prevent class merging, such that the generated code would be invalid if we rewrite the
+ // invoke-super instruction into an invoke-direct instruction.
+ getProguardConfig(EXAMPLE_KEEP, "-keep class *"));
+ }
+
+ @Test
+ public void testConflictingInterfaceSignatures() throws Exception {
+ String main = "classmerging.ConflictingInterfaceSignaturesTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("ConflictingInterfaceSignaturesTest.class"),
+ CF_DIR.resolve("ConflictingInterfaceSignaturesTest$A.class"),
+ CF_DIR.resolve("ConflictingInterfaceSignaturesTest$B.class"),
+ CF_DIR.resolve("ConflictingInterfaceSignaturesTest$InterfaceImpl.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.ConflictingInterfaceSignaturesTest",
+ "classmerging.ConflictingInterfaceSignaturesTest$InterfaceImpl");
+ runTest(main, programFiles, preservedClassNames);
+ }
+
+ // If an exception class A is merged into another exception class B, then all exception tables
+ // should be updated, and class A should be removed entirely.
+ @Test
+ public void testExceptionTables() throws Exception {
+ String main = "classmerging.ExceptionTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("ExceptionTest.class"),
+ CF_DIR.resolve("ExceptionTest$ExceptionA.class"),
+ CF_DIR.resolve("ExceptionTest$ExceptionB.class"),
+ CF_DIR.resolve("ExceptionTest$Exception1.class"),
+ CF_DIR.resolve("ExceptionTest$Exception2.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.ExceptionTest",
+ "classmerging.ExceptionTest$ExceptionB",
+ "classmerging.ExceptionTest$Exception2");
+ DexInspector inspector = runTest(main, programFiles, preservedClassNames);
+
+ ClassSubject mainClass = inspector.clazz(main);
+ assertThat(mainClass, isPresent());
+
+ MethodSubject mainMethod =
+ mainClass.method("void", "main", ImmutableList.of("java.lang.String[]"));
+ assertThat(mainMethod, isPresent());
+
+ // Check that the second catch handler has been removed.
+ DexCode code = mainMethod.getMethod().getCode().asDexCode();
+ int numberOfMoveExceptionInstructions = 0;
+ for (Instruction instruction : code.instructions) {
+ if (instruction instanceof MoveException) {
+ numberOfMoveExceptionInstructions++;
+ }
+ }
+ assertEquals(2, numberOfMoveExceptionInstructions);
+ }
+
+ @Test
+ public void testNoIllegalClassAccess() throws Exception {
+ String main = "classmerging.SimpleInterfaceAccessTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("SimpleInterface.class"),
+ CF_DIR.resolve("SimpleInterfaceAccessTest.class"),
+ CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
+ CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class")
+ };
+ // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl
+ // is in a different package and is not public.
+ ImmutableSet<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.SimpleInterface",
+ "classmerging.SimpleInterfaceAccessTest",
+ "classmerging.pkg.SimpleInterfaceImplRetriever",
+ "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+ runTest(main, programFiles, preservedClassNames);
+ }
+
+ @Ignore("b/73958515")
+ @Test
+ public void testNoIllegalClassAccessWithAccessModifications() throws Exception {
+ // If access modifications are allowed then SimpleInterface should be merged into
+ // SimpleInterfaceImpl.
+ String main = "classmerging.SimpleInterfaceAccessTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("SimpleInterface.class"),
+ CF_DIR.resolve("SimpleInterfaceAccessTest.class"),
+ CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
+ CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class")
+ };
+ ImmutableSet<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.SimpleInterfaceAccessTest",
+ "classmerging.pkg.SimpleInterfaceImplRetriever",
+ "classmerging.pkg.SimpleInterfaceImplRetriever$SimpleInterfaceImpl");
+ // Allow access modifications (and prevent SimpleInterfaceImplRetriever from being removed as
+ // a result of inlining).
+ runTest(
+ main,
+ programFiles,
+ preservedClassNames,
+ getProguardConfig(
+ EXAMPLE_KEEP,
+ "-allowaccessmodification",
+ "-keep public class classmerging.pkg.SimpleInterfaceImplRetriever"));
+ }
+
+ @Test
+ public void testTemplateMethodPattern() throws Exception {
+ String main = "classmerging.TemplateMethodTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("TemplateMethodTest.class"),
+ CF_DIR.resolve("TemplateMethodTest$AbstractClass.class"),
+ CF_DIR.resolve("TemplateMethodTest$AbstractClassImpl.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.TemplateMethodTest", "classmerging.TemplateMethodTest$AbstractClassImpl");
+ runTest(main, programFiles, preservedClassNames);
+ }
+
+ private DexInspector runTest(String main, Path[] programFiles, Set<String> preservedClassNames)
+ throws Exception {
+ return runTest(main, programFiles, preservedClassNames, getProguardConfig(EXAMPLE_KEEP));
+ }
+
+ private DexInspector runTest(
+ String main, Path[] programFiles, Set<String> preservedClassNames, String proguardConfig)
+ throws Exception {
+ return runTestOnInput(
+ main, readProgramFiles(programFiles), preservedClassNames, proguardConfig);
+ }
+
+ private DexInspector runTestOnInput(
+ String main, AndroidApp input, Set<String> preservedClassNames, String proguardConfig)
+ throws Exception {
+ AndroidApp output = compileWithR8(input, proguardConfig, this::configure);
+ DexInspector inspector = new DexInspector(output);
+ // Check that all classes in [preservedClassNames] are in fact preserved.
+ for (String className : preservedClassNames) {
+ assertTrue(
+ "Class " + className + " should be present", inspector.clazz(className).isPresent());
+ }
+ // Check that all other classes have been removed.
+ for (FoundClassSubject classSubject : inspector.allClasses()) {
+ String className = classSubject.getDexClass().toSourceString();
+ assertTrue(
+ "Class " + className + " should be absent", preservedClassNames.contains(className));
+ }
+ // Check that the R8-generated code produces the same result as D8-generated code.
+ assertEquals(runOnArt(compileWithD8(input), main), runOnArt(output, main));
+ return inspector;
+ }
+
+ private String getProguardConfig(Path path, String... additionalRules) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ for (String line : Files.readAllLines(path)) {
+ builder.append(line);
+ builder.append(System.lineSeparator());
+ }
+ for (String rule : additionalRules) {
+ builder.append(rule);
+ builder.append(System.lineSeparator());
+ }
+ return builder.toString();
+ }
}
diff --git a/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java b/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
index e49c740..341fb67 100644
--- a/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
+++ b/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java
@@ -15,6 +15,7 @@
import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.HashSet;
@@ -65,7 +66,7 @@
for (String className : CLASS_NAMES) {
expectedNames.add(SUBDIR + "/" + className + ".class.dex");
}
- try (ZipFile zipFile = new ZipFile(outputZip.toFile())) {
+ try (ZipFile zipFile = new ZipFile(outputZip.toFile(), StandardCharsets.UTF_8)) {
for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
ZipEntry ze = e.nextElement();
expectedNames.remove(ze.getName());
diff --git a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
index c55428f..7d181bc 100644
--- a/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
+++ b/src/test/java/com/android/tools/r8/compatdx/CompatDxTests.java
@@ -17,6 +17,7 @@
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -197,8 +198,8 @@
}
private void compareArchiveFiles(Path d8File, Path dxFile) throws IOException {
- ZipFile d8Zip = new ZipFile(d8File.toFile());
- ZipFile dxZip = new ZipFile(dxFile.toFile());
+ ZipFile d8Zip = new ZipFile(d8File.toFile(), StandardCharsets.UTF_8);
+ ZipFile dxZip = new ZipFile(dxFile.toFile(), StandardCharsets.UTF_8);
// TODO(zerny): This should test resource containment too once supported.
Set<String> d8Content = d8Zip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
Set<String> dxContent = dxZip.stream().map(ZipEntry::getName).collect(Collectors.toSet());
diff --git a/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java b/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java
new file mode 100644
index 0000000..2c1479e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/InvokeSpecialTest.java
@@ -0,0 +1,24 @@
+// 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.graph;
+
+import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokespecial.Main;
+import com.android.tools.r8.graph.invokespecial.TestClassDump;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class InvokeSpecialTest extends AsmTestBase {
+
+ @Ignore("b/110175213")
+ @Test
+ public void testInvokeSpecial() throws Exception {
+ ensureSameOutput(
+ Main.class.getCanonicalName(),
+ ToolHelper.getClassAsBytes(Main.class),
+ TestClassDump.dump());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java b/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java
new file mode 100644
index 0000000..deb316d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/Main.java
@@ -0,0 +1,13 @@
+// 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.graph.invokespecial;
+
+public class Main {
+
+ public static void main(String[] args) {
+ TestClass x = new TestClass();
+ x.m(true);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java
new file mode 100644
index 0000000..c08091e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClass.java
@@ -0,0 +1,15 @@
+// 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.graph.invokespecial;
+
+public class TestClass {
+
+ public void m(boolean recurse) {
+ System.out.println(recurse);
+ if (recurse) {
+ m(false);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java
new file mode 100644
index 0000000..ae336834
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/invokespecial/TestClassDump.java
@@ -0,0 +1,67 @@
+// 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.graph.invokespecial;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running ./tools/asmifier.py build/classes/test/com/android/tools/r8/graph/-
+// invokespecial/TestClass.class, and changing the invoke-virtual TestClass.m() instruction to
+// an invoke-special instruction.
+public class TestClassDump implements Opcodes {
+
+ public static byte[] dump() throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+ AnnotationVisitor av0;
+
+ cw.visit(
+ V1_8,
+ ACC_PUBLIC + ACC_SUPER,
+ "com/android/tools/r8/graph/invokespecial/TestClass",
+ null,
+ "java/lang/Object",
+ null);
+
+ {
+ mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(ACC_PUBLIC, "m", "(Z)V", null, null);
+ mv.visitCode();
+ mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Z)V", false);
+ mv.visitVarInsn(ILOAD, 1);
+ Label l0 = new Label();
+ mv.visitJumpInsn(IFEQ, l0);
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitInsn(ICONST_0);
+ // Note: Changed from INVOKEVIRTUAL to INVOKESPECIAL.
+ mv.visitMethodInsn(
+ INVOKESPECIAL, "com/android/tools/r8/graph/invokespecial/TestClass", "m", "(Z)V", false);
+ mv.visitLabel(l0);
+ mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
index eb6e943..ba04454 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminBuilder.java
@@ -195,7 +195,7 @@
return addStaticMethod("main", ImmutableList.of("[Ljava/lang/String;"), "V", lines);
}
- private MethodSignature addMethod(
+ public MethodSignature addMethod(
String access,
String name,
List<String> argumentTypes,
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 74e3d5e..6ebb08b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -83,13 +83,16 @@
.addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
.addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
- private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+ private Consumer<InternalOptions> disableClassInliningAndMerging = o -> {
+ o.enableClassInlining = false;
+ o.enableClassMerging = false;
+ };
@Test
public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_noUseOfProperties");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -112,7 +115,7 @@
public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_usePrivateProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -134,7 +137,7 @@
public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_useProtectedProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -157,7 +160,7 @@
public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_useInternalProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -180,7 +183,7 @@
public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_usePublicProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -203,7 +206,7 @@
public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/MutablePropertyKt",
"mutableProperty_usePrimitiveProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
MUTABLE_PROPERTY_CLASS.getClassName());
@@ -228,7 +231,7 @@
public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
"lateInitProperty_noUseOfProperties");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -252,7 +255,7 @@
public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -275,7 +278,7 @@
public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
"lateInitProperty_useProtectedLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -296,7 +299,7 @@
public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -315,7 +318,7 @@
public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -334,7 +337,7 @@
public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
String mainClass = addMainToClasspath(
"properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -351,7 +354,7 @@
public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject classSubject = checkClassIsKept(dexInspector,
USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -377,7 +380,7 @@
public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector,
"properties.CompanionProperties");
@@ -408,7 +411,7 @@
public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector,
"properties.CompanionProperties");
@@ -442,7 +445,7 @@
public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector,
"properties.CompanionProperties");
@@ -473,7 +476,7 @@
public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
String mainClass = addMainToClasspath(
"properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector,
"properties.CompanionProperties");
@@ -505,7 +508,7 @@
final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
"companionLateInitProperties_usePrivateLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
@@ -536,7 +539,7 @@
final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
"companionLateInitProperties_useInternalLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
@@ -560,7 +563,7 @@
final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
"companionLateInitProperties_usePublicLateInitProp");
- runTest(PACKAGE_NAME, mainClass, disableClassInliner, (app) -> {
+ runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject outerClass = checkClassIsKept(dexInspector, testedClass.getOuterClassName());
ClassSubject companionClass = checkClassIsKept(dexInspector, testedClass.getClassName());
diff --git a/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
index 751ac83..957bc1d 100644
--- a/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
+++ b/src/test/java/com/android/tools/r8/naming/GenericSignatureParserTest.java
@@ -82,7 +82,7 @@
parseSimpleError(
GenericSignatureParser::parseClassSignature,
e -> assertTrue(e.getMessage().startsWith("Expected L at position 1")));
- // TODO(sgjesse): The position 2 reported here is onr off.
+ // TODO(sgjesse): The position 2 reported here is one off.
parseSimpleError(
GenericSignatureParser::parseFieldSignature,
e -> assertTrue(e.getMessage().startsWith("Expected L, [ or T at position 2")));
@@ -104,7 +104,7 @@
fail("Succesfully parsed " + signature.substring(0, i) + " (position " + i +")");
}
} catch (GenericSignatureFormatError e) {
- assertTrue("" + i + " Was: " + e.getMessage(), e.getMessage().contains("at position " + (i + 1)));
+ assertTrue(e.getMessage().contains("at position " + (i + 1)));
}
}
}
@@ -350,7 +350,8 @@
forMethodSignatures(this::parseMethodSignature);
}
- private void failingParseAction(Consumer<GenericSignatureParser<String>> parse)
+ private void failingParseAction(
+ Consumer<GenericSignatureParser<String>> parse, String errorMessageType)
throws Exception {
class ThrowsInParserActionBase<E extends Error> extends ReGenerateGenericSignatureRewriter {
protected Supplier<? extends E> exceptionSupplier;
@@ -452,7 +453,8 @@
assertEquals("ERROR", e.getMessage());
} else {
plainErrorCount++;
- assertEquals("Unknown error parsing generic signature: ERROR", e.getMessage());
+ assertEquals("Unknown error parsing "
+ + errorMessageType + " signature: ERROR", e.getMessage());
}
}
}
@@ -463,10 +465,10 @@
public void failingParseAction() throws Exception {
// These signatures hits all action callbacks.
failingParseAction(parser -> parser.parseClassSignature(
- "<U:Ljava/lang/Object;>LOuter<TT;>.Inner;Ljava/util/List<TU;>;"));
+ "<U:Ljava/lang/Object;>LOuter<TT;>.Inner;Ljava/util/List<TU;>;"), "class");
failingParseAction(
- parser -> parser.parseFieldSignature("LOuter$InnerInterface<TU;>.Inner;"));
+ parser -> parser.parseFieldSignature("LOuter$InnerInterface<TU;>.Inner;"), "field");
failingParseAction(
- parser -> parser.parseMethodSignature("(LOuter$InnerInterface<TU;>.Inner;)V"));
+ parser -> parser.parseMethodSignature("(LOuter$InnerInterface<TU;>.Inner;)V"), "method");
}
}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
new file mode 100644
index 0000000..b9ff7ea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
@@ -0,0 +1,536 @@
+// 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.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+public class MinifierClassSignatureTest extends TestBase {
+ /*
+
+ class Simple {
+ }
+ class Base<T> {
+ }
+ class Outer<T> {
+ class Inner {
+ class InnerInner {
+ }
+ class ExtendsInnerInner extends InnerInner {
+ }
+ }
+ class ExtendsInner extends Inner {
+ }
+ }
+
+ */
+
+ String baseSignature = "<T:Ljava/lang/Object;>Ljava/lang/Object;";
+ String outerSignature = "<T:Ljava/lang/Object;>Ljava/lang/Object;";
+ String extendsInnerSignature = "LOuter<TT;>.Inner;";
+ String extendsInnerInnerSignature = "LOuter<TT;>.Inner.InnerInner;";
+
+ private byte[] dumpSimple(String classSignature) throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ MethodVisitor mv;
+
+ String signature = classSignature;
+ cw.visit(V1_8, ACC_SUPER, "Simple", signature, "java/lang/Object", null);
+
+ {
+ mv = cw.visitMethod(0, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpBase(String classSignature) throws Exception {
+
+ final String javacClassSignature = baseSignature;
+ ClassWriter cw = new ClassWriter(0);
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Base", signature, "java/lang/Object", null);
+
+ {
+ mv = cw.visitMethod(0, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+
+ private byte[] dumpOuter(String classSignature) {
+
+ final String javacClassSignature = outerSignature;
+ ClassWriter cw = new ClassWriter(0);
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Outer", signature, "java/lang/Object", null);
+
+ cw.visitInnerClass("Outer$ExtendsInner", "Outer", "ExtendsInner", 0);
+
+ cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+ {
+ mv = cw.visitMethod(0, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpInner(String classSignature) {
+
+ final String javacClassSignature = null;
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Outer$Inner", signature, "java/lang/Object", null);
+
+ cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+ cw.visitInnerClass("Outer$Inner$ExtendsInnerInner", "Outer$Inner", "ExtendsInnerInner", 0);
+
+ cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LOuter;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LOuter;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Outer$Inner", "this$0", "LOuter;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpExtendsInner(String classSignature) throws Exception {
+
+ final String javacClassSignature = extendsInnerSignature;
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Outer$ExtendsInner", signature, "Outer$Inner", null);
+
+ cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+ cw.visitInnerClass("Outer$ExtendsInner", "Outer", "ExtendsInner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LOuter;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LOuter;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Outer$ExtendsInner", "this$0", "LOuter;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitMethodInsn(INVOKESPECIAL, "Outer$Inner", "<init>", "(LOuter;)V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpInnerInner(String classSignature) throws Exception {
+
+ final String javacClassSignature = null;
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Outer$Inner$InnerInner", signature, "java/lang/Object", null);
+
+ cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+ cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$1", "LOuter$Inner;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LOuter$Inner;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Outer$Inner$InnerInner", "this$1", "LOuter$Inner;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpExtendsInnerInner(String classSignature) throws Exception {
+
+ final String javacClassSignature = extendsInnerInnerSignature;
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ String signature = classSignature != null ? classSignature : javacClassSignature;
+ cw.visit(V1_8, ACC_SUPER, "Outer$Inner$ExtendsInnerInner", signature, "Outer$Inner$InnerInner",
+ null);
+
+ cw.visitInnerClass("Outer$Inner", "Outer", "Inner", 0);
+
+ cw.visitInnerClass("Outer$Inner$InnerInner", "Outer$Inner", "InnerInner", 0);
+
+ cw.visitInnerClass("Outer$Inner$ExtendsInnerInner", "Outer$Inner", "ExtendsInnerInner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$1", "LOuter$Inner;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LOuter$Inner;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Outer$Inner$ExtendsInnerInner", "this$1", "LOuter$Inner;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitMethodInsn(INVOKESPECIAL, "Outer$Inner$InnerInner", "<init>", "(LOuter$Inner;)V",
+ false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ public void runTest(
+ ImmutableMap<String, String> signatures,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspect)
+ throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ DexInspector inspector = new DexInspector(
+ ToolHelper.runR8(R8Command.builder(checker)
+ .addClassProgramData(dumpSimple(signatures.get("Simple")), Origin.unknown())
+ .addClassProgramData(dumpBase(signatures.get("Base")), Origin.unknown())
+ .addClassProgramData(dumpOuter(signatures.get("Outer")), Origin.unknown())
+ .addClassProgramData(dumpInner(signatures.get("Outer$Inner")), Origin.unknown())
+ .addClassProgramData(
+ dumpExtendsInner(signatures.get("Outer$ExtendsInner")), Origin.unknown())
+ .addClassProgramData(
+ dumpInnerInner(signatures.get("Outer$Inner$InnerInner")), Origin.unknown())
+ .addClassProgramData(
+ dumpExtendsInnerInner(
+ signatures.get("Outer$Inner$ExtendsInnerInner")), Origin.unknown())
+ .addProguardConfiguration(ImmutableList.of(
+ "-keepattributes InnerClasses,EnclosingMethod,Signature",
+ "-keep,allowobfuscation class **"
+ ), Origin.unknown())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setProguardMapConsumer(StringConsumer.emptyConsumer())
+ .build()));
+ // All classes are kept, and renamed.
+ assertThat(inspector.clazz("Simple"), isRenamed());
+ assertThat(inspector.clazz("Base"), isRenamed());
+ assertThat(inspector.clazz("Outer"), isRenamed());
+ assertThat(inspector.clazz("Outer$Inner"), isRenamed());
+ assertThat(inspector.clazz("Outer$ExtendsInner"), isRenamed());
+ assertThat(inspector.clazz("Outer$Inner$InnerInner"), isRenamed());
+ assertThat(inspector.clazz("Outer$Inner$ExtendsInnerInner"), isRenamed());
+
+ // Test that classes with have their original signature if the default was provided.
+ if (!signatures.containsKey("Simple")) {
+ assertNull(inspector.clazz("Simple").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Base")) {
+ assertEquals(baseSignature, inspector.clazz("Base").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Outer")) {
+ assertEquals(outerSignature, inspector.clazz("Outer").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Outer$Inner")) {
+ assertNull(inspector.clazz("Outer$Inner").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Outer$ExtendsInner")) {
+ assertEquals(extendsInnerSignature,
+ inspector.clazz("Outer$ExtendsInner").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Outer$Inner$InnerInner")) {
+ assertNull(inspector.clazz("Outer$Inner$InnerInner").getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("Outer$Inner$ExtendsInnerInner")) {
+ assertEquals(extendsInnerInnerSignature,
+ inspector.clazz("Outer$Inner$ExtendsInnerInner").getOriginalSignatureAttribute());
+ }
+
+ diagnostics.accept(checker);
+ inspect.accept(inspector);
+ }
+
+ private void testSingleClass(String name, String signature,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspector)
+ throws Exception {
+ ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+ runTest(signatures, diagnostics, inspector);
+ }
+
+ private void isOriginUnknown(Origin origin) {
+ assertSame(Origin.unknown(), origin);
+ }
+
+ private void noWarnings(DiagnosticsChecker checker) {
+ assertEquals(0, checker.warnings.size());
+ }
+
+ private void noInspection(DexInspector inspector) {
+ }
+
+ private void noSignatureAttribute(ClassSubject clazz) {
+ assertNull(clazz.getFinalSignatureAttribute());
+ assertNull(clazz.getOriginalSignatureAttribute());
+ }
+
+ @Test
+ public void originalJavacSignatures() throws Exception {
+ // Test using the signatures generated by javac.
+ runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+ }
+
+ @Test
+ public void classSignature_empty() throws Exception {
+ testSingleClass("Outer", "", this::noWarnings, inspector -> {
+ ClassSubject outer = inspector.clazz("Outer");
+ assertNull(outer.getFinalSignatureAttribute());
+ assertNull(outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureOuter_valid() throws Exception {
+ // class Outer<T extends Simple> extends Base<T>
+ String signature = "<T:LSimple;>LBase<TT;>;";
+ testSingleClass("Outer", signature, this::noWarnings, inspector -> {
+ ClassSubject outer = inspector.clazz("Outer");
+ ClassSubject simple = inspector.clazz("Simple");
+ ClassSubject base = inspector.clazz("Base");
+ String baseDescriptorWithoutSemicolon =
+ base.getFinalDescriptor().substring(0, base.getFinalDescriptor().length() - 1);
+ String minifiedSignature =
+ "<T:" + simple.getFinalDescriptor() + ">" + baseDescriptorWithoutSemicolon + "<TT;>;";
+ assertEquals(minifiedSignature, outer.getFinalSignatureAttribute());
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureExtendsInner_valid() throws Exception {
+ String signature = "LOuter<TT;>.Inner;";
+ testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+ ClassSubject extendsInner = inspector.clazz("Outer$ExtendsInner");
+ ClassSubject outer = inspector.clazz("Outer");
+ ClassSubject inner = inspector.clazz("Outer$Inner");
+ String outerDescriptorWithoutSemicolon =
+ outer.getFinalDescriptor().substring(0, outer.getFinalDescriptor().length() - 1);
+ String innerFinalDescriptor = inner.getFinalDescriptor();
+ String innerLastPart =
+ innerFinalDescriptor.substring(innerFinalDescriptor.indexOf("$") + 1);
+ String minifiedSignature = outerDescriptorWithoutSemicolon + "<TT;>." + innerLastPart;
+ assertEquals(minifiedSignature, extendsInner.getFinalSignatureAttribute());
+ assertEquals(signature, extendsInner.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureOuter_classNotFound() throws Exception {
+ String signature = "<T:LNotFound;>LAlsoNotFound;";
+ testSingleClass("Outer", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ ClassSubject outer = inspector.clazz("Outer");
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureExtendsInner_innerClassNotFound() throws Exception {
+ String signature = "LOuter<TT;>.NotFound;";
+ testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureExtendsInner_outerAndInnerClassNotFound() throws Exception {
+ String signature = "LNotFound<TT;>.AlsoNotFound;";
+ testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureExtendsInner_nestedInnerClassNotFound() throws Exception {
+ String signature = "LOuter<TT;>.Inner.NotFound;";
+ testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureExtendsInner_multipleMestedInnerClassesNotFound() throws Exception {
+ String signature = "LOuter<TT;>.NotFound.AlsoNotFound;";
+ testSingleClass("Outer$ExtendsInner", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
+ assertEquals(signature, outer.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void classSignatureOuter_invalid() throws Exception {
+ testSingleClass("Outer", "X", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for class Outer", "Expected L at position 1");
+ }, inspector -> noSignatureAttribute(inspector.clazz("Outer")));
+ }
+
+ @Test
+ public void classSignatureOuter_invalidEnd() throws Exception {
+ testSingleClass("Outer", "<L", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for class Outer", "Unexpected end of signature at position 3");
+ }, inspector -> noSignatureAttribute(inspector.clazz("Outer")));
+ }
+
+ @Test
+ public void classSignatureExtendsInner_invalid() throws Exception {
+ testSingleClass("Outer$ExtendsInner", "X", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for class Outer$ExtendsInner", "Expected L at position 1");
+ }, inspector -> noSignatureAttribute(inspector.clazz("Outer$ExtendsInner")));
+ }
+
+ @Test
+ public void classSignatureExtendsInnerInner_invalid() throws Exception {
+ testSingleClass("Outer$Inner$ExtendsInnerInner", "X", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for class Outer$Inner$ExtendsInnerInner",
+ "Expected L at position 1");
+ }, inspector -> noSignatureAttribute(inspector.clazz("Outer$Inner$ExtendsInnerInner")));
+ }
+
+ @Test
+ public void multipleWarnings() throws Exception {
+ runTest(ImmutableMap.of(
+ "Outer", "X",
+ "Outer$ExtendsInner", "X",
+ "Outer$Inner$ExtendsInnerInner", "X"), diagnostics -> {
+ assertEquals(3, diagnostics.warnings.size());
+ }, inspector -> {
+ noSignatureAttribute(inspector.clazz("Outer"));
+ noSignatureAttribute(inspector.clazz("Outer$ExtendsInner"));
+ noSignatureAttribute(inspector.clazz("Outer$Inner$ExtendsInnerInner"));
+ });
+ }
+ @Test
+ public void regress80029761() throws Exception {
+ String signature = "LOuter<TT;>.com/example/Inner;";
+ testSingleClass("Outer$ExtendsInner", signature, diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for class Outer$ExtendsInner", "Expected ; at position 16");
+ }, inspector -> {
+ noSignatureAttribute(inspector.clazz("Outer$ExtendsInner"));
+ });
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java
new file mode 100644
index 0000000..e48ce56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierFieldSignatureTest.java
@@ -0,0 +1,289 @@
+// 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.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.FieldSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+
+public class MinifierFieldSignatureTest extends TestBase {
+ /*
+
+ class Fields<X extends String> {
+ class Inner {
+ }
+ public X anX;
+ public X[] anArrayOfX;
+ public Fields<X> aFieldsOfX;
+ public Fields<X>.Inner aFieldsOfXInner;
+ }
+
+ */
+
+ private String anXSignature = "TX;";
+ private String anArrayOfXSignature = "[TX;";
+ private String aFieldsOfXSignature = "LFields<TX;>;";
+ private String aFieldsOfXInnerSignature = "LFields<TX;>.Inner;";
+
+ public byte[] dumpFields(Map<String, String> signatures) throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+ String signature;
+
+ cw.visit(V1_8, ACC_SUPER, "Fields", "<X:Ljava/lang/String;>Ljava/lang/Object;",
+ "java/lang/Object", null);
+
+ cw.visitInnerClass("Fields$Inner", "Fields", "Inner", 0);
+
+ {
+ signature = signatures.get("anX");
+ signature = signature == null ? anXSignature : signature;
+ fv = cw.visitField(ACC_PUBLIC, "anX", "Ljava/lang/String;", signature, null);
+ fv.visitEnd();
+ }
+ {
+ signature = signatures.get("anArrayOfX");
+ signature = signature == null ? anArrayOfXSignature : signature;
+ fv = cw.visitField(
+ ACC_PUBLIC, "anArrayOfX", "[Ljava/lang/String;", signature, null);
+ fv.visitEnd();
+ }
+ {
+ signature = signatures.get("aFieldsOfX");
+ signature = signature == null ? aFieldsOfXSignature : signature;
+ fv = cw.visitField(ACC_PUBLIC, "aFieldsOfX", "LFields;", signature, null);
+ fv.visitEnd();
+ }
+ {
+ signature = signatures.get("aFieldsOfXInner");
+ signature = signature == null ? aFieldsOfXInnerSignature : signature;
+ fv = cw.visitField(
+ ACC_PUBLIC, "aFieldsOfXInner", "LFields$Inner;", signature, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ public byte[] dumpInner() throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ cw.visit(V1_8, ACC_SUPER, "Fields$Inner", null, "java/lang/Object", null);
+
+ cw.visitInnerClass("Fields$Inner", "Fields", "Inner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LFields;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LFields;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Fields$Inner", "this$0", "LFields;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private FieldSubject lookupAnX(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Fields");
+ return clazz.field("java.lang.String", "anX");
+ }
+
+ private FieldSubject lookupAnArrayOfX(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Fields");
+ return clazz.field("java.lang.String[]", "anArrayOfX");
+ }
+
+ private FieldSubject lookupAFieldsOfX(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Fields");
+ return clazz.field("Fields", "aFieldsOfX");
+ }
+
+ public void runTest(
+ ImmutableMap<String, String> signatures,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspect)
+ throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ DexInspector inspector = new DexInspector(
+ ToolHelper.runR8(R8Command.builder(checker)
+ .addClassProgramData(dumpFields(signatures), Origin.unknown())
+ .addClassProgramData(dumpInner(), Origin.unknown())
+ .addProguardConfiguration(ImmutableList.of(
+ "-keepattributes InnerClasses,EnclosingMethod,Signature",
+ "-keep,allowobfuscation class ** { *; }"
+ ), Origin.unknown())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setProguardMapConsumer(StringConsumer.emptyConsumer())
+ .build()));
+ // All classes are kept, and renamed.
+ ClassSubject clazz = inspector.clazz("Fields");
+ assertThat(clazz, isRenamed());
+ assertThat(inspector.clazz("Fields$Inner"), isRenamed());
+
+ FieldSubject anX = lookupAnX(inspector);
+ FieldSubject anArrayOfX = lookupAnArrayOfX(inspector);
+ FieldSubject aFieldsOfX =lookupAFieldsOfX(inspector);
+ FieldSubject aFieldsOfXInner = clazz.field("Fields$Inner", "aFieldsOfXInner");
+
+ // Check that all fields have been renamed
+ assertThat(anX, isRenamed());
+ assertThat(anArrayOfX, isRenamed());
+ assertThat(aFieldsOfX, isRenamed());
+ assertThat(aFieldsOfXInner, isRenamed());
+
+ //System.out.println(generic.getFinalSignatureAttribute());
+ //System.out.println(generic.getOriginalSignatureAttribute());
+
+ // Test that methods have their original signature if the default was provided.
+ if (!signatures.containsKey("anX")) {
+ assertEquals(anXSignature, anX.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("anArrayOfX")) {
+ assertEquals(anArrayOfXSignature, anArrayOfX.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("aFieldsOfX")) {
+ assertEquals(
+ aFieldsOfXSignature, aFieldsOfX.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("aFieldsOfXInner")) {
+ assertEquals(
+ aFieldsOfXInnerSignature, aFieldsOfXInner.getOriginalSignatureAttribute());
+ }
+
+ diagnostics.accept(checker);
+ inspect.accept(inspector);
+ }
+
+ private void testSingleField(String name, String signature,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspector)
+ throws Exception {
+ ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+ runTest(signatures, diagnostics, inspector);
+ }
+
+ private void isOriginUnknown(Origin origin) {
+ assertSame(Origin.unknown(), origin);
+ }
+
+ private void noWarnings(DiagnosticsChecker checker) {
+ assertEquals(0, checker.warnings.size());
+ }
+
+ private void noInspection(DexInspector inspector) {
+ }
+
+ private void noSignatureAttribute(FieldSubject field) {
+ assertThat(field, isPresent());
+ assertNull(field.getFinalSignatureAttribute());
+ assertNull(field.getOriginalSignatureAttribute());
+ }
+
+ @Test
+ public void originalJavacSignatures() throws Exception {
+ // Test using the signatures generated by javac.
+ runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+ }
+
+ @Test
+ public void signatureEmpty() throws Exception {
+ testSingleField("anX", "", this::noWarnings,
+ inspector -> noSignatureAttribute(lookupAnX(inspector)));
+ }
+
+ @Test
+ public void signatureInvalid() throws Exception {
+ testSingleField("anX", "X", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ // TODO(sgjesse): The position 2 reported here is one off.
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for field",
+ "java.lang.String Fields.anX",
+ "Expected L, [ or T at position 2");
+ }, inspector -> noSignatureAttribute(lookupAnX(inspector)));
+ }
+
+ @Test
+ public void classNotFound() throws Exception {
+ String signature = "LNotFound<TX;>.InnerNotFound.InnerAlsoNotFound;";
+ testSingleField("anX", signature, this::noWarnings, inspector -> {
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ assertEquals(signature, lookupAnX(inspector).getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void multipleWarnings() throws Exception {
+ runTest(ImmutableMap.of(
+ "anX", "X",
+ "anArrayOfX", "X",
+ "aFieldsOfX", "X"
+ ), diagnostics -> {
+ assertEquals(3, diagnostics.warnings.size());
+ }, inspector -> {
+ noSignatureAttribute(lookupAnX(inspector));
+ noSignatureAttribute(lookupAnArrayOfX(inspector));
+ noSignatureAttribute(lookupAFieldsOfX(inspector));
+ });
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java
new file mode 100644
index 0000000..41750ae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/MinifierMethodSignatureTest.java
@@ -0,0 +1,314 @@
+// 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.DexInspectorMatchers.isPresent;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ACONST_NULL;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ARETURN;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.PUTFIELD;
+import static org.objectweb.asm.Opcodes.RETURN;
+import static org.objectweb.asm.Opcodes.V1_8;
+
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.Test;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+public class MinifierMethodSignatureTest extends TestBase {
+ /*
+
+ class Methods<X extends Throwable> {
+ class Inner {
+ }
+ public static <T extends Throwable> T generic(T a, Methods<T>.Inner b) { return null; }
+ public Methods<X>.Inner parameterizedReturn() { return null; }
+ public void parameterizedArguments(X a, Methods<X>.Inner b) { }
+ public void parametrizedThrows() throws X { }
+ }
+
+ */
+
+ private String genericSignature = "<T:Ljava/lang/Throwable;>(TT;LMethods<TT;>.Inner;)TT;";
+ private String parameterizedReturnSignature = "()LMethods<TX;>.Inner;";
+ private String parameterizedArgumentsSignature = "(TX;LMethods<TX;>.Inner;)V";
+ private String parametrizedThrowsSignature = "()V^TX;";
+
+ private byte[] dumpMethods(Map<String, String> signatures) throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ MethodVisitor mv;
+ String signature;
+
+ cw.visit(V1_8, ACC_SUPER, "Methods", "<X:Ljava/lang/Throwable;>Ljava/lang/Object;",
+ "java/lang/Object", null);
+
+ cw.visitInnerClass("Methods$Inner", "Methods", "Inner", 0);
+
+ {
+ mv = cw.visitMethod(0, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ {
+ signature = signatures.get("generic");
+ signature = signature == null ? genericSignature : signature;
+ mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "generic",
+ "(Ljava/lang/Throwable;LMethods$Inner;)Ljava/lang/Throwable;",
+ signature, null);
+ mv.visitCode();
+ mv.visitInsn(ACONST_NULL);
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(1, 2);
+ mv.visitEnd();
+ }
+ {
+ signature = signatures.get("parameterizedReturn");
+ signature = signature == null ? parameterizedReturnSignature : signature;
+ mv = cw.visitMethod(ACC_PUBLIC, "parameterizedReturn", "()LMethods$Inner;",
+ signature, null);
+ mv.visitCode();
+ mv.visitInsn(ACONST_NULL);
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ {
+ signature = signatures.get("parameterizedArguments");
+ signature = signature == null ? parameterizedArgumentsSignature : signature;
+ mv = cw.visitMethod(ACC_PUBLIC, "parameterizedArguments",
+ "(Ljava/lang/Throwable;LMethods$Inner;)V", signature, null);
+ mv.visitCode();
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(0, 3);
+ mv.visitEnd();
+ }
+ {
+ signature = signatures.get("parametrizedThrows");
+ signature = signature == null ? parametrizedThrowsSignature : signature;
+ mv = cw.visitMethod(ACC_PUBLIC, "parametrizedThrows", "()V", signature,
+ new String[] { "java/lang/Throwable" });
+ mv.visitCode();
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(0, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private byte[] dumpInner() throws Exception {
+
+ ClassWriter cw = new ClassWriter(0);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ cw.visit(V1_8, ACC_SUPER, "Methods$Inner", null, "java/lang/Object", null);
+
+ cw.visitInnerClass("Methods$Inner", "Methods", "Inner", 0);
+
+ {
+ fv = cw.visitField(ACC_FINAL + ACC_SYNTHETIC, "this$0", "LMethods;", null, null);
+ fv.visitEnd();
+ }
+ {
+ mv = cw.visitMethod(0, "<init>", "(LMethods;)V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitVarInsn(ALOAD, 1);
+ mv.visitFieldInsn(PUTFIELD, "Methods$Inner", "this$0", "LMethods;");
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(2, 2);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private MethodSubject lookupGeneric(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Methods");
+ return clazz.method(
+ "java.lang.Throwable", "generic", ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+ }
+
+ private MethodSubject lookupParameterizedReturn(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Methods");
+ return clazz.method(
+ "Methods$Inner", "parameterizedReturn", ImmutableList.of());
+ }
+
+ private MethodSubject lookupParameterizedArguments(DexInspector inspector) {
+ ClassSubject clazz = inspector.clazz("Methods");
+ return clazz.method(
+ "void", "parameterizedArguments", ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+ }
+
+ public void runTest(
+ ImmutableMap<String, String> signatures,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspect)
+ throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ DexInspector inspector = new DexInspector(
+ ToolHelper.runR8(R8Command.builder(checker)
+ .addClassProgramData(dumpMethods(signatures), Origin.unknown())
+ .addClassProgramData(dumpInner(), Origin.unknown())
+ .addProguardConfiguration(ImmutableList.of(
+ "-keepattributes InnerClasses,EnclosingMethod,Signature",
+ "-keep,allowobfuscation class ** { *; }"
+ ), Origin.unknown())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setProguardMapConsumer(StringConsumer.emptyConsumer())
+ .build()));
+ // All classes are kept, and renamed.
+ ClassSubject clazz = inspector.clazz("Methods");
+ assertThat(clazz, isRenamed());
+ assertThat(inspector.clazz("Methods$Inner"), isRenamed());
+
+ MethodSubject generic = lookupGeneric(inspector);
+ MethodSubject parameterizedReturn = lookupParameterizedReturn(inspector);
+ MethodSubject parameterizedArguments = lookupParameterizedArguments(inspector);
+ MethodSubject parametrizedThrows =
+ clazz.method("void", "parametrizedThrows", ImmutableList.of());
+
+ // Check that all methods have been renamed
+ assertThat(generic, isRenamed());
+ assertThat(parameterizedReturn, isRenamed());
+ assertThat(parameterizedArguments, isRenamed());
+ assertThat(parametrizedThrows, isRenamed());
+
+ // Test that methods have their original signature if the default was provided.
+ if (!signatures.containsKey("generic")) {
+ assertEquals(genericSignature, generic.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("parameterizedReturn")) {
+ assertEquals(
+ parameterizedReturnSignature, parameterizedReturn.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("parameterizedArguments")) {
+ assertEquals(
+ parameterizedArgumentsSignature, parameterizedArguments.getOriginalSignatureAttribute());
+ }
+ if (!signatures.containsKey("parametrizedThrows")) {
+ assertEquals(
+ parametrizedThrowsSignature, parametrizedThrows.getOriginalSignatureAttribute());
+ }
+
+ diagnostics.accept(checker);
+ inspect.accept(inspector);
+ }
+
+ private void testSingleMethod(String name, String signature,
+ Consumer<DiagnosticsChecker> diagnostics,
+ Consumer<DexInspector> inspector)
+ throws Exception {
+ ImmutableMap<String, String> signatures = ImmutableMap.of(name, signature);
+ runTest(signatures, diagnostics, inspector);
+ }
+
+ private void isOriginUnknown(Origin origin) {
+ assertSame(Origin.unknown(), origin);
+ }
+
+ private void noWarnings(DiagnosticsChecker checker) {
+ assertEquals(0, checker.warnings.size());
+ }
+
+ private void noInspection(DexInspector inspector) {
+ }
+
+ private void noSignatureAttribute(MethodSubject method) {
+ assertThat(method, isPresent());
+ assertNull(method.getFinalSignatureAttribute());
+ assertNull(method.getOriginalSignatureAttribute());
+ }
+
+ @Test
+ public void originalJavacSignatures() throws Exception {
+ // Test using the signatures generated by javac.
+ runTest(ImmutableMap.of(), this::noWarnings, this::noInspection);
+ }
+
+ @Test
+ public void signatureEmpty() throws Exception {
+ testSingleMethod("generic", "", this::noWarnings, inspector -> {
+ noSignatureAttribute(lookupGeneric(inspector));
+ });
+ }
+
+ @Test
+ public void signatureInvalid() throws Exception {
+ testSingleMethod("generic", "X", diagnostics -> {
+ assertEquals(1, diagnostics.warnings.size());
+ DiagnosticsChecker.checkDiagnostic(diagnostics.warnings.get(0), this::isOriginUnknown,
+ "Invalid signature for method",
+ "java.lang.Throwable Methods.generic(java.lang.Throwable, Methods$Inner)",
+ "Expected ( at position 1");
+ }, inspector -> noSignatureAttribute(lookupGeneric(inspector)));
+ }
+
+ @Test
+ public void classNotFound() throws Exception {
+ String signature = "<T:LNotFound;>(TT;LAlsoNotFound<TT;>.InnerNotFound.InnerAlsoNotFound;)TT;";
+ testSingleMethod("generic", signature, this::noWarnings,
+ inspector -> {
+ ClassSubject methods = inspector.clazz("Methods");
+ MethodSubject method =
+ methods.method("java.lang.Throwable", "generic",
+ ImmutableList.of("java.lang.Throwable", "Methods$Inner"));
+ assertThat(inspector.clazz("NotFound"), not(isPresent()));
+ assertEquals(signature, method.getOriginalSignatureAttribute());
+ });
+ }
+
+ @Test
+ public void multipleWarnings() throws Exception {
+ runTest(ImmutableMap.of(
+ "generic", "X",
+ "parameterizedReturn", "X",
+ "parameterizedArguments", "X"
+ ), diagnostics -> {
+ assertEquals(3, diagnostics.warnings.size());
+ }, inspector -> {
+ noSignatureAttribute(lookupGeneric(inspector));
+ noSignatureAttribute(lookupParameterizedReturn(inspector));
+ noSignatureAttribute(lookupParameterizedArguments(inspector));
+ });
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
index d9808c0..f311c15 100644
--- a/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/NamingTestBase.java
@@ -5,9 +5,9 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
-import com.android.tools.r8.graph.ClassAndMemberPublicizer;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.optimize.ClassAndMemberPublicizer;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardRuleParserException;
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
index c82bbc9..9a9ca34 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/B72391662.java
@@ -4,52 +4,303 @@
package com.android.tools.r8.naming.b72391662;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
-import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.VmTestRunner;
import com.android.tools.r8.VmTestRunner.IgnoreIfVmOlderThan;
import com.android.tools.r8.naming.b72391662.subpackage.OtherPackageSuper;
import com.android.tools.r8.naming.b72391662.subpackage.OtherPackageTestClass;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
import com.google.common.collect.ImmutableList;
import java.util.List;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(VmTestRunner.class)
-public class B72391662 extends TestBase {
+public class B72391662 extends ProguardCompatabilityTestBase {
- private void doTest(boolean allowAccessModification, boolean minify) throws Exception {
+ private static final List<Class> CLASSES = ImmutableList.of(
+ TestMain.class, Interface.class, Super.class, TestClass.class,
+ OtherPackageSuper.class, OtherPackageTestClass.class);
+
+ private void doTest_keepAll(
+ Shrinker shrinker,
+ String repackagePrefix,
+ boolean allowAccessModification,
+ boolean minify) throws Exception {
Class mainClass = TestMain.class;
+ String keep = !minify ? "-keep" : "-keep,allowobfuscation";
List<String> config = ImmutableList.of(
- allowAccessModification ?"-allowaccessmodification" : "",
+ "-printmapping",
+ repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+ allowAccessModification ? "-allowaccessmodification" : "",
!minify ? "-dontobfuscate" : "",
"-keep class " + mainClass.getCanonicalName() + " {",
" public void main(java.lang.String[]);",
"}",
- "-keep class " + TestClass.class.getCanonicalName() + " {",
+ keep + " class " + TestClass.class.getCanonicalName() + " {",
" *;",
"}",
- "-keep class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+ keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
" *;",
- "}"
+ "}",
+ "-dontwarn java.lang.invoke.*"
);
- AndroidApp app = readClassesAndAndriodJar(ImmutableList.of(
- mainClass, Interface.class, Super.class, TestClass.class,
- OtherPackageSuper.class, OtherPackageTestClass.class));
- app = compileWithR8(app, String.join(System.lineSeparator(), config));
+ AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+ DexInspector dexInspector =
+ isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+ ClassSubject testClass = dexInspector.clazz(TestClass.class);
+ assertThat(testClass, isPresent());
+
+ // Test the totally unused method.
+ MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+ assertThat(staticMethod, isPresent());
+ assertEquals(minify, staticMethod.isRenamed());
+ if (isR8(shrinker)) {
+ assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
+ } else {
+ assertFalse(staticMethod.getMethod().accessFlags.isPublic());
+ }
+
+ // Test an indirectly referred method.
+ staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+ assertThat(staticMethod, isPresent());
+ assertEquals(minify, staticMethod.isRenamed());
+ boolean publicizeCondition = isR8(shrinker) ? allowAccessModification
+ : minify && repackagePrefix != null && allowAccessModification;
+ assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
}
@Test
@IgnoreIfVmOlderThan(Version.V7_0_0)
- public void test() throws Exception {
- doTest(true, true);
- doTest(true, false);
- doTest(false, true);
- doTest(false, false);
+ public void test_keepAll_R8() throws Exception {
+ doTest_keepAll(Shrinker.R8, "r8", true, true);
+ doTest_keepAll(Shrinker.R8, "r8", true, false);
+ doTest_keepAll(Shrinker.R8, "r8", false, true);
+ doTest_keepAll(Shrinker.R8, "r8", false, false);
+ doTest_keepAll(Shrinker.R8, null, true, true);
+ doTest_keepAll(Shrinker.R8, null, true, false);
+ doTest_keepAll(Shrinker.R8, null, false, true);
+ doTest_keepAll(Shrinker.R8, null, false, false);
+ }
+
+ @Ignore("b/92236970")
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepAll_R8Compat() throws Exception {
+ doTest_keepAll(Shrinker.R8_COMPAT, "rc", true, true);
+ doTest_keepAll(Shrinker.R8_COMPAT, "rc", true, false);
+ doTest_keepAll(Shrinker.R8_COMPAT, "rc", false, true);
+ doTest_keepAll(Shrinker.R8_COMPAT, "rc", false, false);
+ doTest_keepAll(Shrinker.R8_COMPAT, null, true, true);
+ doTest_keepAll(Shrinker.R8_COMPAT, null, true, false);
+ doTest_keepAll(Shrinker.R8_COMPAT, null, false, true);
+ doTest_keepAll(Shrinker.R8_COMPAT, null, false, false);
+ }
+
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepAll_Proguard6() throws Exception {
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+ doTest_keepAll(Shrinker.PROGUARD6_THEN_D8, null, false, false);
+ }
+
+ private void doTest_keepNonPublic(
+ Shrinker shrinker,
+ String repackagePrefix,
+ boolean allowAccessModification,
+ boolean minify) throws Exception {
+ Class mainClass = TestMain.class;
+ String keep = !minify ? "-keep" : "-keep,allowobfuscation";
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+ allowAccessModification ? "-allowaccessmodification" : "",
+ !minify ? "-dontobfuscate" : "",
+ "-keep class " + mainClass.getCanonicalName() + " {",
+ " public void main(java.lang.String[]);",
+ "}",
+ keep + " class " + TestClass.class.getCanonicalName() + " {",
+ " !public <methods>;",
+ "}",
+ keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+ " !public <methods>;",
+ "}",
+ "-dontwarn java.lang.invoke.*"
+ );
+
+ AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
+ assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+ DexInspector dexInspector =
+ isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+ ClassSubject testClass = dexInspector.clazz(TestClass.class);
+ assertThat(testClass, isPresent());
+
+ // Test the totally unused method.
+ MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+ assertThat(staticMethod, isPresent());
+ assertEquals(minify, staticMethod.isRenamed());
+ if (isR8(shrinker)) {
+ assertEquals(allowAccessModification, staticMethod.getMethod().accessFlags.isPublic());
+ } else {
+ assertFalse(staticMethod.getMethod().accessFlags.isPublic());
+ }
+
+ // Test an indirectly referred method.
+ staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+ assertThat(staticMethod, isPresent());
+ assertEquals(minify, staticMethod.isRenamed());
+ boolean publicizeCondition = isR8(shrinker) ? allowAccessModification
+ : minify && repackagePrefix != null && allowAccessModification;
+ assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+ }
+
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepNonPublic_R8() throws Exception {
+ doTest_keepNonPublic(Shrinker.R8, "r8", true, true);
+ doTest_keepNonPublic(Shrinker.R8, "r8", true, false);
+ doTest_keepNonPublic(Shrinker.R8, "r8", false, true);
+ doTest_keepNonPublic(Shrinker.R8, "r8", false, false);
+ doTest_keepNonPublic(Shrinker.R8, null, true, true);
+ doTest_keepNonPublic(Shrinker.R8, null, true, false);
+ doTest_keepNonPublic(Shrinker.R8, null, false, true);
+ doTest_keepNonPublic(Shrinker.R8, null, false, false);
+ }
+
+ @Ignore("b/92236970")
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepNonPublic_R8Compat() throws Exception {
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", true, true);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", true, false);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", false, true);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, "rc", false, false);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, null, true, true);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, null, true, false);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, null, false, true);
+ doTest_keepNonPublic(Shrinker.R8_COMPAT, null, false, false);
+ }
+
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepNonPublic_Proguard6() throws Exception {
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+ doTest_keepNonPublic(Shrinker.PROGUARD6_THEN_D8, null, false, false);
+ }
+
+ private void doTest_keepPublic(
+ Shrinker shrinker,
+ String repackagePrefix,
+ boolean allowAccessModification,
+ boolean minify) throws Exception {
+ Class mainClass = TestMain.class;
+ String keep = !minify ? "-keep" : "-keep,allowobfuscation";
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ repackagePrefix != null ? "-repackageclasses '" + repackagePrefix + "'" : "",
+ allowAccessModification ? "-allowaccessmodification" : "",
+ !minify ? "-dontobfuscate" : "",
+ "-keep class " + mainClass.getCanonicalName() + " {",
+ " public void main(java.lang.String[]);",
+ "}",
+ keep + " class " + TestClass.class.getCanonicalName() + " {",
+ " public <methods>;",
+ "}",
+ keep + " class " + OtherPackageTestClass.class.getCanonicalName() + " {",
+ " public <methods>;",
+ "}",
+ "-dontwarn java.lang.invoke.*"
+ );
+
+ AndroidApp app = runShrinkerRaw(shrinker, CLASSES, config);
+ assertEquals("123451234567\nABC\n", runOnArt(app, mainClass.getCanonicalName()));
+
+ DexInspector dexInspector =
+ isR8(shrinker) ? new DexInspector(app) : new DexInspector(app, proguardMap);
+ ClassSubject testClass = dexInspector.clazz(TestClass.class);
+ assertThat(testClass, isPresent());
+
+ // Test the totally unused method.
+ MethodSubject staticMethod = testClass.method("void", "unused", ImmutableList.of());
+ assertThat(staticMethod, not(isPresent()));
+
+ // Test an indirectly referred method.
+ staticMethod = testClass.method("java.lang.String", "staticMethod", ImmutableList.of());
+ if (isR8(shrinker)) {
+ // Inlined.
+ assertThat(staticMethod, not(isPresent()));
+ } else {
+ assertThat(staticMethod, isPresent());
+ assertEquals(minify, staticMethod.isRenamed());
+ boolean publicizeCondition = minify && repackagePrefix != null && allowAccessModification;
+ assertEquals(publicizeCondition, staticMethod.getMethod().accessFlags.isPublic());
+ }
+ }
+
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepPublic_R8() throws Exception {
+ doTest_keepPublic(Shrinker.R8, "r8", true, true);
+ doTest_keepPublic(Shrinker.R8, "r8", true, false);
+ doTest_keepPublic(Shrinker.R8, "r8", false, true);
+ doTest_keepPublic(Shrinker.R8, "r8", false, false);
+ doTest_keepPublic(Shrinker.R8, null, true, true);
+ doTest_keepPublic(Shrinker.R8, null, true, false);
+ doTest_keepPublic(Shrinker.R8, null, false, true);
+ doTest_keepPublic(Shrinker.R8, null, false, false);
+ }
+
+ @Ignore("b/92236970")
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepPublic_R8Compat() throws Exception {
+ doTest_keepPublic(Shrinker.R8_COMPAT, "rc", true, true);
+ doTest_keepPublic(Shrinker.R8_COMPAT, "rc", true, false);
+ doTest_keepPublic(Shrinker.R8_COMPAT, "rc", false, true);
+ doTest_keepPublic(Shrinker.R8_COMPAT, "rc", false, false);
+ doTest_keepPublic(Shrinker.R8_COMPAT, null, true, true);
+ doTest_keepPublic(Shrinker.R8_COMPAT, null, true, false);
+ doTest_keepPublic(Shrinker.R8_COMPAT, null, false, true);
+ doTest_keepPublic(Shrinker.R8_COMPAT, null, false, false);
+ }
+
+ @Test
+ @IgnoreIfVmOlderThan(Version.V7_0_0)
+ public void test_keepPublic_Proguard6() throws Exception {
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, true);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", true, false);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, true);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, "pg", false, false);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, true, true);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, true, false);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, false, true);
+ doTest_keepPublic(Shrinker.PROGUARD6_THEN_D8, null, false, false);
}
}
diff --git a/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java b/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
index 88efc09..f13700c 100644
--- a/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
+++ b/src/test/java/com/android/tools/r8/naming/b72391662/TestClass.java
@@ -18,6 +18,10 @@
return value;
}
+ static void unused() {
+ System.out.println("This should be discarded unless there is a keep rule.");
+ }
+
static String staticMethod() {
return "1";
}
diff --git a/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java b/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
index f27813e..7c243ae 100644
--- a/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/FilteredClassPathTest.java
@@ -34,16 +34,8 @@
private void testPath(List<String> filters, List<String> positives, List<String> negatives) {
FilteredClassPath path = makeFilteredClassPath(filters);
- Assert.assertTrue(
- positives.stream().map(FilteredClassPathTest::adaptFileSeparator).map(Paths::get)
- .allMatch(path::matchesFile));
- Assert.assertFalse(
- negatives.stream().map(FilteredClassPathTest::adaptFileSeparator).map(Paths::get)
- .allMatch(path::matchesFile));
- }
-
- private static String adaptFileSeparator(String s) {
- return s.replace('/', File.separatorChar);
+ Assert.assertTrue(positives.stream().allMatch(path::matchesFile));
+ Assert.assertFalse(negatives.stream().allMatch(path::matchesFile));
}
private static FilteredClassPath makeFilteredClassPath(List<String> filters) {
@@ -51,9 +43,7 @@
}
private static FilteredClassPath makeFilteredClassPath(Path path, List<String> filters) {
- return new FilteredClassPath(path,
- filters.stream().map(FilteredClassPathTest::adaptFileSeparator)
- .collect(ImmutableList.toImmutableList()));
+ return new FilteredClassPath(path, ImmutableList.copyOf(filters));
}
@Test
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
index 3976f89..10c64e3 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ProguardCompatabilityTestBase.java
@@ -6,24 +6,31 @@
import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
import com.android.tools.r8.CompatProguardCommandBuilder;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.R8Command;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
+import java.util.function.Consumer;
public class ProguardCompatabilityTestBase extends TestBase {
+ protected Path proguardMap;
+
public enum Shrinker {
PROGUARD5,
PROGUARD6,
@@ -32,6 +39,34 @@
R8
}
+ protected static boolean isR8(Shrinker shrinker) {
+ return shrinker == Shrinker.R8_COMPAT || shrinker == Shrinker.R8;
+ }
+
+ protected AndroidApp runShrinkerRaw(
+ Shrinker mode, List<Class> programClasses, List<String> proguadConfigs) throws Exception {
+ return runShrinkerRaw(
+ mode, programClasses, String.join(System.lineSeparator(), proguadConfigs));
+ }
+
+ protected AndroidApp runShrinkerRaw(
+ Shrinker mode, List<Class> programClasses, String proguardConfig) throws Exception {
+ proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+ switch (mode) {
+ case PROGUARD5:
+ return runProguard5Raw(programClasses, proguardConfig, proguardMap);
+ case PROGUARD6:
+ return runProguard6Raw(programClasses, proguardConfig, proguardMap);
+ case PROGUARD6_THEN_D8:
+ return runProguard6AndD8Raw(programClasses, proguardConfig, proguardMap);
+ case R8_COMPAT:
+ return runR8CompatRaw(programClasses, proguardConfig);
+ case R8:
+ return runR8Raw(programClasses, proguardConfig);
+ }
+ throw new IllegalArgumentException("Unknown shrinker: " + mode);
+ }
+
protected DexInspector runShrinker(
Shrinker mode, List<Class> programClasses, List<String> proguadConfigs) throws Exception {
return runShrinker(mode, programClasses, String.join(System.lineSeparator(), proguadConfigs));
@@ -54,21 +89,38 @@
throw new IllegalArgumentException("Unknown shrinker: " + mode);
}
- protected DexInspector runR8(List<Class> programClasses, String proguardConfig) throws Exception {
- AndroidApp app = readClasses(programClasses);
- R8Command.Builder builder = ToolHelper.prepareR8CommandBuilder(app);
- builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
- return new DexInspector(ToolHelper.runR8(builder.build()));
+ protected AndroidApp runR8Raw(List<Class> programClasses, String proguardConfig)
+ throws Exception {
+ return runR8Raw(programClasses, proguardConfig, null);
}
- protected DexInspector runR8Compat(
+ protected AndroidApp runR8Raw(
+ List<Class> programClasses, String proguardConfig, Consumer<InternalOptions> configure)
+ throws Exception {
+ AndroidApp app = readClassesAndAndriodJar(programClasses);
+ R8Command.Builder builder = ToolHelper.prepareR8CommandBuilder(app);
+ builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
+ return ToolHelper.runR8(builder.build(), configure);
+ }
+
+ protected DexInspector runR8(List<Class> programClasses, String proguardConfig) throws Exception {
+ return new DexInspector(runR8Raw(programClasses, proguardConfig));
+ }
+
+ protected AndroidApp runR8CompatRaw(
List<Class> programClasses, String proguardConfig) throws Exception {
CompatProguardCommandBuilder builder = new CompatProguardCommandBuilder(true);
builder.addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown());
programClasses.forEach(
clazz -> builder.addProgramFiles(ToolHelper.getClassFileForTestClass(clazz)));
+ builder.addLibraryFiles(ToolHelper.getDefaultAndroidJar());
builder.setProgramConsumer(DexIndexedConsumer.emptyConsumer());
- return new DexInspector(ToolHelper.runR8(builder.build()));
+ return ToolHelper.runR8(builder.build());
+ }
+
+ protected DexInspector runR8Compat(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ return new DexInspector(runR8CompatRaw(programClasses, proguardConfig));
}
protected DexInspector runR8CompatKeepingMain(Class mainClass, List<Class> programClasses)
@@ -76,41 +128,79 @@
return runR8Compat(programClasses, keepMainProguardConfiguration(mainClass));
}
- protected DexInspector runProguard5(
- List<Class> programClasses, String proguardConfig) throws Exception {
+ protected AndroidApp runProguard5Raw(
+ List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
Path proguardedJar =
File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
- Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
- ToolHelper.runProguard(
- jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
- return new DexInspector(readJar(proguardedJar), proguardMap);
+ ProcessResult result = ToolHelper.runProguardRaw(
+ jarTestClasses(programClasses),
+ proguardedJar,
+ ToolHelper.getAndroidJar(AndroidApiLevel.N),
+ proguardConfigFile,
+ proguardMap);
+ if (result.exitCode != 0) {
+ fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+ }
+ return readJar(proguardedJar);
+ }
+
+ protected DexInspector runProguard5(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+ return new DexInspector(
+ runProguard5Raw(programClasses, proguardConfig, proguardMap), proguardMap);
+ }
+
+ protected AndroidApp runProguard6Raw(
+ List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
+ Path proguardedJar =
+ File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
+ Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
+ FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
+ ProcessResult result = ToolHelper.runProguard6Raw(
+ jarTestClasses(programClasses),
+ proguardedJar,
+ ToolHelper.getAndroidJar(AndroidApiLevel.N),
+ proguardConfigFile,
+ proguardMap);
+ if (result.exitCode != 0) {
+ fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+ }
+ return readJar(proguardedJar);
}
protected DexInspector runProguard6(
List<Class> programClasses, String proguardConfig) throws Exception {
+ proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+ return new DexInspector(
+ runProguard6Raw(programClasses, proguardConfig, proguardMap), proguardMap);
+ }
+
+ protected AndroidApp runProguard6AndD8Raw(
+ List<Class> programClasses, String proguardConfig, Path proguardMap) throws Exception {
Path proguardedJar =
File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
- Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
- ToolHelper.runProguard6(
- jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
- return new DexInspector(readJar(proguardedJar), proguardMap);
+ ProcessResult result = ToolHelper.runProguard6Raw(
+ jarTestClasses(programClasses),
+ proguardedJar,
+ ToolHelper.getAndroidJar(AndroidApiLevel.N),
+ proguardConfigFile,
+ proguardMap);
+ if (result.exitCode != 0) {
+ fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr);
+ }
+ return ToolHelper.runD8(readJar(proguardedJar));
}
protected DexInspector runProguard6AndD8(
List<Class> programClasses, String proguardConfig) throws Exception {
- Path proguardedJar =
- File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath();
- Path proguardConfigFile = File.createTempFile("proguard", ".config", temp.getRoot()).toPath();
- Path proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
- FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
- ToolHelper.runProguard6(
- jarTestClasses(programClasses), proguardedJar, proguardConfigFile, proguardMap);
- AndroidApp app = ToolHelper.runD8(readJar(proguardedJar));
- return new DexInspector(app, proguardMap);
+ proguardMap = File.createTempFile("proguard", ".map", temp.getRoot()).toPath();
+ return new DexInspector(
+ runProguard6AndD8Raw(programClasses, proguardConfig, proguardMap), proguardMap);
}
protected DexInspector runProguardKeepingMain(Class mainClass, List<Class> programClasses)
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
new file mode 100644
index 0000000..65d1763
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
@@ -0,0 +1,195 @@
+// 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.shaking.ifrule;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class IfOnAccessModifierTest extends ProguardCompatabilityTestBase {
+ private final static List<Class> CLASSES = ImmutableList.of(
+ ClassForIf.class, ClassForSubsequent.class,
+ MainForAccessModifierTest.class);
+
+ private final Shrinker shrinker;
+ private final MethodSignature nonPublicMethod;
+ private final MethodSignature publicMethod;
+
+ public IfOnAccessModifierTest(Shrinker shrinker) {
+ this.shrinker = shrinker;
+ nonPublicMethod = new MethodSignature("nonPublicMethod", "void", ImmutableList.of());
+ publicMethod = new MethodSignature("publicMethod", "void", ImmutableList.of());
+ }
+
+ @Parameters(name = "shrinker: {0}")
+ public static Collection<Object> data() {
+ return ImmutableList.of(Shrinker.PROGUARD6, Shrinker.R8);
+ }
+
+ @Test
+ public void ifOnNonPublic_keepOnPublic() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ "-repackageclasses 'top'",
+ "-allowaccessmodification",
+ "-keep class **.Main* {",
+ " public static void callIfNonPublic();",
+ "}",
+ "-if class **.ClassForIf {",
+ " !public <methods>;",
+ "}",
+ "-keep,allowobfuscation class **.ClassForSubsequent {",
+ " public <methods>;",
+ "}"
+ );
+ DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+ ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, isPresent());
+ assertTrue(methodSubject.getMethod().accessFlags.isPublic());
+
+ classSubject = dexInspector.clazz(ClassForSubsequent.class);
+ if (isR8(shrinker)) {
+ // TODO(b/72109068): ClassForIf#nonPublicMethod becomes public, and -if rule is not applied
+ // at the 2nd tree shaking.
+ assertThat(classSubject, not(isPresent()));
+ return;
+ }
+ assertThat(classSubject, isPresent());
+ methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, isPresent());
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ }
+
+ @Test
+ public void ifOnNonPublic_keepOnNonPublic() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ "-repackageclasses 'top'",
+ "-allowaccessmodification",
+ "-keep class **.Main* {",
+ " public static void callIfNonPublic();",
+ "}",
+ "-if class **.ClassForIf {",
+ " !public <methods>;",
+ "}",
+ "-keep,allowobfuscation class **.ClassForSubsequent {",
+ " !public <methods>;",
+ "}"
+ );
+ DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+ ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, isPresent());
+ assertTrue(methodSubject.getMethod().accessFlags.isPublic());
+
+ classSubject = dexInspector.clazz(ClassForSubsequent.class);
+ if (isR8(shrinker)) {
+ // TODO(b/72109068): ClassForIf#nonPublicMethod becomes public, and -if rule is not applied
+ // at the 2nd tree shaking.
+ assertThat(classSubject, not(isPresent()));
+ return;
+ }
+ assertThat(classSubject, isPresent());
+ methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, isPresent());
+ assertFalse(methodSubject.getMethod().accessFlags.isPublic());
+ }
+
+ @Test
+ public void ifOnPublic_keepOnPublic() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ "-repackageclasses 'top'",
+ "-allowaccessmodification",
+ "-keep class **.Main* {",
+ " public static void callIfPublic();",
+ "}",
+ "-if class **.ClassForIf {",
+ " public <methods>;",
+ "}",
+ "-keep,allowobfuscation class **.ClassForSubsequent {",
+ " public <methods>;",
+ "}"
+ );
+ DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+ ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, isPresent());
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, not(isPresent()));
+
+ classSubject = dexInspector.clazz(ClassForSubsequent.class);
+ assertThat(classSubject, isPresent());
+ methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, isPresent());
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ }
+
+ @Test
+ public void ifOnPublic_keepOnNonPublic() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-printmapping",
+ "-repackageclasses 'top'",
+ "-allowaccessmodification",
+ "-keep class **.Main* {",
+ " public static void callIfPublic();",
+ "}",
+ "-if class **.ClassForIf {",
+ " public <methods>;",
+ "}",
+ "-keep,allowobfuscation class **.ClassForSubsequent {",
+ " !public <methods>;",
+ "}"
+ );
+ DexInspector dexInspector = runShrinker(shrinker, CLASSES, config);
+ ClassSubject classSubject = dexInspector.clazz(ClassForIf.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, isPresent());
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, not(isPresent()));
+
+ classSubject = dexInspector.clazz(ClassForSubsequent.class);
+ assertThat(classSubject, isPresent());
+ methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ methodSubject = classSubject.method(nonPublicMethod);
+ if (isR8(shrinker)) {
+ // TODO(b/72109068): if kept in the 1st tree shaking, should be kept after publicizing.
+ assertThat(methodSubject, not(isPresent()));
+ return;
+ }
+ assertThat(methodSubject, isPresent());
+ assertFalse(methodSubject.getMethod().accessFlags.isPublic());
+ }
+
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java
new file mode 100644
index 0000000..1b5bd75
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTestClasses.java
@@ -0,0 +1,40 @@
+// 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.shaking.ifrule;
+
+class ClassForIf {
+ ClassForIf() {
+ }
+
+ synchronized void nonPublicMethod() {
+ System.out.println("ClassForIf::nonPublicMethod");
+ }
+
+ synchronized public void publicMethod() {
+ System.out.println("ClassForIf::publicMethod");
+ }
+}
+
+class ClassForSubsequent {
+ ClassForSubsequent() {
+ }
+
+ synchronized void nonPublicMethod() {
+ System.out.println("ClassForSubsequent::nonPublicMethod");
+ }
+
+ synchronized public void publicMethod() {
+ System.out.println("ClassForSubsequent::publicMethod");
+ }
+}
+
+class MainForAccessModifierTest {
+ public static void callIfNonPublic() {
+ new ClassForIf().nonPublicMethod();
+ }
+
+ public static void callIfPublic() {
+ new ClassForIf().publicMethod();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
new file mode 100644
index 0000000..14391ee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/inlining/IfRuleWithInlining.java
@@ -0,0 +1,105 @@
+// 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.shaking.ifrule.inlining;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+class A {
+ static public int a() {
+ try {
+ String p = A.class.getPackage().getName();
+ Class.forName(p + ".D");
+ } catch (ClassNotFoundException e) {
+ return 2;
+ }
+ return 1;
+ }
+}
+
+class B {
+ // Depending on inlining option, this method is kept to make inlining of A.a() infeasible.
+ void x() {
+ System.out.print("" + A.a() + A.a() + A.a() + A.a() + A.a() + A.a() + A.a() + A.a());
+ }
+}
+
+class D {
+}
+
+class Main {
+ public static void main(String[] args) {
+ System.out.print("" + A.a());
+ }
+}
+
+@RunWith(Parameterized.class)
+public class IfRuleWithInlining extends ProguardCompatabilityTestBase {
+ private final static List<Class> CLASSES = ImmutableList.of(
+ A.class, B.class, D.class, Main.class);
+
+ private final Shrinker shrinker;
+ private final boolean inlineMethod;
+
+ public IfRuleWithInlining(Shrinker shrinker, boolean inlineMethod) {
+ this.shrinker = shrinker;
+ this.inlineMethod = inlineMethod;
+ }
+
+ @Parameters(name = "shrinker: {0} inlineMethod: {1}")
+ public static Collection<Object[]> data() {
+ // We don't run this on Proguard, as triggering inlining in Proguard is out of our control.
+ return ImmutableList.of(
+ new Object[]{Shrinker.R8, true},
+ new Object[]{Shrinker.R8, false}
+ );
+ }
+
+ private void check(AndroidApp app) throws Exception {
+ DexInspector inspector = new DexInspector(app);
+ ClassSubject clazzA = inspector.clazz(A.class);
+ assertThat(clazzA, isPresent());
+ // A.a might be inlined.
+ assertEquals(!inlineMethod, clazzA.method("int", "a", ImmutableList.of()).isPresent());
+ // TODO(110148109): class D should be present - inlining or not.
+ assertEquals(!inlineMethod, inspector.clazz(D.class).isPresent());
+ ProcessResult result = runOnArtRaw(app, Main.class.getName());
+ assertEquals(0, result.exitCode);
+ // TODO(110148109): Output should be the same - inlining or not.
+ assertEquals(!inlineMethod ? "1" : "2", result.stdout);
+ }
+
+ @Test
+ public void testMergedClassMethodInIfRule() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-keep class **.Main { public static void main(java.lang.String[]); }",
+ inlineMethod
+ ? "-alwaysinline class **.A { int a(); }"
+ : "-keep class **.B { *; }",
+ inlineMethod
+ ? "-checkdiscard class **.A { int a(); }"
+ : "",
+ "-if class **.A { static int a(); }",
+ "-keep class **.D",
+ "-dontobfuscate"
+ );
+
+ check(runShrinkerRaw(shrinker, CLASSES, config));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
new file mode 100644
index 0000000..b484d73
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/IfRuleWithVerticalClassMerging.java
@@ -0,0 +1,156 @@
+// 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.shaking.ifrule.verticalclassmerging;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatabilityTestBase;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+class A {
+ int x = 1;
+ int a() throws ClassNotFoundException {
+ // Class D is expected to be kept - vertical class merging or not. The -if rule say that if
+ // the method A.a is in the output, then class D is needed.
+ String p =getClass().getPackage().getName();
+ Class.forName(p + ".D");
+ return 4;
+ }
+}
+
+class B extends A {
+ int y = 2;
+ int b() {
+ return 5;
+ }
+}
+
+class C extends B {
+ int z = 3;
+ int c() {
+ return 6;
+ }
+}
+
+class D {
+}
+
+class Main {
+ public static void main(String[] args) throws ClassNotFoundException {
+ C c = new C();
+ System.out.print("" + c.x + "" + c.y + "" + c.z + "" + c.a() + "" + c.b() + "" + c.c());
+ }
+}
+
+@RunWith(Parameterized.class)
+public class IfRuleWithVerticalClassMerging extends ProguardCompatabilityTestBase {
+ private final static List<Class> CLASSES = ImmutableList.of(
+ A.class, B.class, C.class, D.class, Main.class);
+
+ private final Shrinker shrinker;
+ private final boolean enableClassMerging;
+
+ public IfRuleWithVerticalClassMerging(Shrinker shrinker, boolean enableClassMerging) {
+ this.shrinker = shrinker;
+ this.enableClassMerging = enableClassMerging;
+ }
+
+ @Parameters(name = "shrinker: {0} classMerging: {1}")
+ public static Collection<Object[]> data() {
+ // We don't run this on Proguard, as Proguard does not merge A into B.
+ return ImmutableList.of(
+ new Object[]{Shrinker.R8, true},
+ new Object[]{Shrinker.R8, false}
+ );
+ }
+
+ private void configure(InternalOptions options) {
+ options.enableClassMerging = enableClassMerging;
+ }
+
+ @Override
+ protected AndroidApp runR8Raw(
+ List<Class> programClasses, String proguardConfig) throws Exception {
+ return super.runR8Raw(programClasses, proguardConfig, this::configure);
+ }
+
+ private void check(AndroidApp app) throws Exception {
+ DexInspector inspector = new DexInspector(app);
+ ClassSubject clazzA = inspector.clazz(A.class);
+ assertEquals(!enableClassMerging, clazzA.isPresent());
+ ClassSubject clazzB = inspector.clazz(B.class);
+ assertThat(clazzB, isPresent());
+ ClassSubject clazzD = inspector.clazz(D.class);
+ // TODO(110141157): Class D should be kept - vertical class merging or not.
+ assertEquals(!enableClassMerging, clazzD.isPresent());
+
+ ProcessResult result = runOnArtRaw(app, Main.class.getName());
+ // TODO(110141157): The code should run - vertical class merging or not.
+ assertEquals(enableClassMerging ? 1 : 0, result.exitCode);
+ if (!enableClassMerging) {
+ assertEquals("123456", result.stdout);
+ }
+ }
+
+ @Test
+ public void testMergedClassInIfRule() throws Exception {
+ // Class C is kept, meaning that it will not be touched.
+ // Class A will be merged into class B.
+ List<String> config = ImmutableList.of(
+ "-keep class **.Main { public static void main(java.lang.String[]); }",
+ "-keep class **.C",
+ "-if class **.A",
+ "-keep class **.D",
+ "-dontobfuscate"
+ );
+
+ check(runShrinkerRaw(shrinker, CLASSES, config));
+ }
+
+ @Test
+ public void testMergedClassFieldInIfRule() throws Exception {
+ // Class C is kept, meaning that it will not be touched.
+ // Class A will be merged into class B.
+ // Main.main access A.x, so that field exists satisfying the if rule.
+ List<String> config = ImmutableList.of(
+ "-keep class **.Main { public static void main(java.lang.String[]); }",
+ "-keep class **.C",
+ "-if class **.A { int x; }",
+ "-keep class **.D",
+ "-dontobfuscate"
+ );
+
+ check(runShrinkerRaw(shrinker, CLASSES, config));
+ }
+
+ @Test
+ public void testMergedClassMethodInIfRule() throws Exception {
+ // Class C is kept, meaning that it will not be touched.
+ // Class A will be merged into class B.
+ // Main.main access A.a(), that method exists satisfying the if rule.
+ List<String> config = ImmutableList.of(
+ "-keep class **.Main { public static void main(java.lang.String[]); }",
+ "-keep class **.C",
+ "-if class **.A { int a(); }",
+ "-keep class **.D",
+ "-dontobfuscate"
+ );
+
+ check(runShrinkerRaw(shrinker, CLASSES, config));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DexInspector.java b/src/test/java/com/android/tools/r8/utils/DexInspector.java
index ace5c9b..fd32ec7 100644
--- a/src/test/java/com/android/tools/r8/utils/DexInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/DexInspector.java
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
+import static org.junit.Assert.assertTrue;
+
import com.android.tools.r8.StringResource;
import com.android.tools.r8.code.Const4;
import com.android.tools.r8.code.ConstString;
@@ -53,6 +55,8 @@
import com.android.tools.r8.code.Throw;
import com.android.tools.r8.dex.ApplicationReader;
import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexAnnotationSet;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexCode;
@@ -65,6 +69,8 @@
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.DexValue.DexValueString;
import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.naming.ClassNameMapper;
import com.android.tools.r8.naming.ClassNamingForNameMapper;
@@ -72,6 +78,8 @@
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.signature.GenericSignatureAction;
+import com.android.tools.r8.naming.signature.GenericSignatureParser;
import com.android.tools.r8.smali.SmaliBuilder;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableList;
@@ -85,6 +93,7 @@
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ExecutionException;
+import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -191,6 +200,50 @@
}
}
+ DexAnnotation findAnnotation(String name, DexAnnotationSet annotations) {
+ for (DexAnnotation annotation : annotations.annotations) {
+ DexType type = annotation.annotation.type;
+ String original = mapping == null ? type.toSourceString() : mapping.originalNameOf(type);
+ if (original.equals(name)) {
+ return annotation;
+ }
+ }
+ return null;
+ }
+
+ public String getFinalSignatureAttribute(DexAnnotationSet annotations) {
+ DexAnnotation annotation =
+ findAnnotation("dalvik.annotation.Signature", annotations);
+ if (annotation == null) {
+ return null;
+ }
+ assert annotation.annotation.elements.length == 1;
+ DexAnnotationElement element = annotation.annotation.elements[0];
+ assert element.value instanceof DexValueArray;
+ StringBuilder builder = new StringBuilder();
+ DexValueArray valueArray = (DexValueArray) element.value;
+ for (DexValue value : valueArray.getValues()) {
+ assertTrue(value instanceof DexValueString);
+ DexValueString s = (DexValueString) value;
+ builder.append(s.getValue());
+ }
+ return builder.toString();
+ }
+
+ public String getOriginalSignatureAttribute(
+ DexAnnotationSet annotations, BiConsumer<GenericSignatureParser, String> parse) {
+ String finalSignature = getFinalSignatureAttribute(annotations);
+ if (finalSignature == null || mapping == null) {
+ return finalSignature;
+ }
+
+ GenericSignatureGenerater rewriter = new GenericSignatureGenerater();
+ GenericSignatureParser<String> parser = new GenericSignatureParser<>(rewriter);
+ parse.accept(parser, finalSignature);
+ return rewriter.getSignature();
+ }
+
+
public ClassSubject clazz(Class clazz) {
return clazz(clazz.getTypeName());
}
@@ -367,6 +420,10 @@
public abstract boolean isLocalClass();
public abstract boolean isAnonymousClass();
+
+ public abstract String getOriginalSignatureAttribute();
+
+ public abstract String getFinalSignatureAttribute();
}
private class AbsentClassSubject extends ClassSubject {
@@ -448,6 +505,16 @@
public boolean isAnonymousClass() {
return false;
}
+
+ @Override
+ public String getOriginalSignatureAttribute() {
+ return null;
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return null;
+ }
}
public class FoundClassSubject extends ClassSubject {
@@ -561,23 +628,12 @@
assert !name.endsWith("EnclosingClass")
&& !name.endsWith("EnclosingMethod")
&& !name.endsWith("InnerClass");
- DexAnnotation annotation = findAnnotation(name);
+ DexAnnotation annotation = findAnnotation(name, dexClass.annotations);
return annotation == null
? new AbsentAnnotationSubject()
: new FoundAnnotationSubject(annotation);
}
- private DexAnnotation findAnnotation(String name) {
- for (DexAnnotation annotation : dexClass.annotations.annotations) {
- DexType type = annotation.annotation.type;
- String original = mapping == null ? type.toSourceString() : mapping.originalNameOf(type);
- if (original.equals(name)) {
- return annotation;
- }
- }
- return null;
- }
-
@Override
public String getOriginalName() {
if (naming != null) {
@@ -641,6 +697,17 @@
}
@Override
+ public String getOriginalSignatureAttribute() {
+ return DexInspector.this.getOriginalSignatureAttribute(
+ dexClass.annotations, GenericSignatureParser::parseClassSignature);
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return DexInspector.this.getFinalSignatureAttribute(dexClass.annotations);
+ }
+
+ @Override
public String toString() {
return dexClass.toSourceString();
}
@@ -677,6 +744,10 @@
public abstract boolean isClassInitializer();
+ public abstract String getOriginalSignatureAttribute();
+
+ public abstract String getFinalSignatureAttribute();
+
public abstract DexEncodedMethod getMethod();
public Iterator<InstructionSubject> iterateInstructions() {
@@ -745,6 +816,16 @@
public Signature getFinalSignature() {
return null;
}
+
+ @Override
+ public String getOriginalSignatureAttribute() {
+ return null;
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return null;
+ }
}
public class FoundMethodSubject extends MethodSubject {
@@ -805,7 +886,31 @@
@Override
public MethodSignature getOriginalSignature() {
MethodSignature signature = getFinalSignature();
- MemberNaming memberNaming = clazz.naming != null ? clazz.naming.lookup(signature) : null;
+ if (clazz.naming == null) {
+ return signature;
+ }
+
+ // Map the parameters and return type to original names. This is needed as the in the
+ // Proguard map the names on the left side are the original names. E.g.
+ //
+ // X -> a
+ // X method(X) -> a
+ //
+ // whereas the final signature is for X.a is "a (a)"
+ String[] OriginalParameters = new String[signature.parameters.length];
+ for (int i = 0; i < OriginalParameters.length; i++) {
+ String obfuscated = signature.parameters[i];
+ String original = originalToObfuscatedMapping.inverse().get(obfuscated);
+ OriginalParameters[i] = original != null ? original : obfuscated;
+ }
+ String obfuscatedReturnType = signature.type;
+ String originalReturnType = originalToObfuscatedMapping.inverse().get(obfuscatedReturnType);
+ String returnType = originalReturnType != null ? originalReturnType : obfuscatedReturnType;
+
+ MethodSignature lookupSignature =
+ new MethodSignature(signature.name, returnType, OriginalParameters);
+
+ MemberNaming memberNaming = clazz.naming.lookup(lookupSignature);
return memberNaming != null
? (MethodSignature) memberNaming.getOriginalSignature()
: signature;
@@ -817,6 +922,17 @@
}
@Override
+ public String getOriginalSignatureAttribute() {
+ return DexInspector.this.getOriginalSignatureAttribute(
+ dexMethod.annotations, GenericSignatureParser::parseMethodSignature);
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return DexInspector.this.getFinalSignatureAttribute(dexMethod.annotations);
+ }
+
+ @Override
public Iterator<InstructionSubject> iterateInstructions() {
return new InstructionIterator(this);
}
@@ -841,6 +957,10 @@
public abstract DexValue getStaticValue();
public abstract boolean isRenamed();
+
+ public abstract String getOriginalSignatureAttribute();
+
+ public abstract String getFinalSignatureAttribute();
}
public class AbsentFieldSubject extends FieldSubject {
@@ -889,6 +1009,16 @@
public DexEncodedField getField() {
return null;
}
+
+ @Override
+ public String getOriginalSignatureAttribute() {
+ return null;
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return null;
+ }
}
public class FoundFieldSubject extends FieldSubject {
@@ -929,7 +1059,24 @@
@Override
public FieldSignature getOriginalSignature() {
FieldSignature signature = getFinalSignature();
- MemberNaming memberNaming = clazz.naming != null ? clazz.naming.lookup(signature) : null;
+ if (clazz.naming == null) {
+ return signature;
+ }
+
+ // Map the type to the original name. This is needed as the in the Proguard map the
+ // names on the left side are the original names. E.g.
+ //
+ // X -> a
+ // X field -> a
+ //
+ // whereas the final signature is for X.a is "a a"
+ String obfuscatedType = signature.type;
+ String originalType = originalToObfuscatedMapping.inverse().get(obfuscatedType);
+ String fieldType = originalType != null ? originalType : obfuscatedType;
+
+ FieldSignature lookupSignature = new FieldSignature(signature.name, fieldType);
+
+ MemberNaming memberNaming = clazz.naming.lookup(lookupSignature);
return memberNaming != null
? (FieldSignature) memberNaming.getOriginalSignature()
: signature;
@@ -956,6 +1103,17 @@
}
@Override
+ public String getOriginalSignatureAttribute() {
+ return DexInspector.this.getOriginalSignatureAttribute(
+ dexField.annotations, GenericSignatureParser::parseFieldSignature);
+ }
+
+ @Override
+ public String getFinalSignatureAttribute() {
+ return DexInspector.this.getFinalSignatureAttribute(dexField.annotations);
+ }
+
+ @Override
public String toString() {
return dexField.toSourceString();
}
@@ -1410,4 +1568,63 @@
return result;
}
}
+
+ // Build the generic signature using the current mapping if any.
+ class GenericSignatureGenerater implements GenericSignatureAction<String> {
+
+ private StringBuilder signature;
+
+ public String getSignature() {
+ return signature.toString();
+ }
+
+ @Override
+ public void parsedSymbol(char symbol) {
+ signature.append(symbol);
+ }
+
+ @Override
+ public void parsedIdentifier(String identifier) {
+ signature.append(identifier);
+ }
+
+ @Override
+ public String parsedTypeName(String name) {
+ String type = name;
+ if (originalToObfuscatedMapping != null) {
+ String original = originalToObfuscatedMapping.inverse().get(name);
+ type = original != null ? original : name;
+ }
+ signature.append(type);
+ return type;
+ }
+
+ @Override
+ public String parsedInnerTypeName(String enclosingType, String name) {
+ String type;
+ if (originalToObfuscatedMapping != null) {
+ // The enclosingType has already been mapped if a mapping is present.
+ String minifiedEnclosing = originalToObfuscatedMapping.get(enclosingType);
+ type = originalToObfuscatedMapping.inverse().get(minifiedEnclosing + "$" + name);
+ if (type != null) {
+ assert type.startsWith(enclosingType + "$");
+ name = type.substring(enclosingType.length() + 1);
+ }
+ } else {
+ type = enclosingType + "$" + name;
+ }
+ signature.append(name);
+ return type;
+ }
+
+ @Override
+ public void start() {
+ signature = new StringBuilder();
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+ }
}
diff --git a/tools/build_r8lib.py b/tools/build_r8lib.py
index c855bc5..4b7473e 100755
--- a/tools/build_r8lib.py
+++ b/tools/build_r8lib.py
@@ -25,16 +25,21 @@
ANDROID_JAR = 'third_party/android_jar/lib-v%s/android.jar' % API_LEVEL
-def build_r8lib():
+def build_r8lib(output_path=None, output_map=None, **kwargs):
+ if output_path is None:
+ output_path = R8LIB_JAR
+ if output_map is None:
+ output_map = R8LIB_MAP_FILE
toolhelper.run(
'r8',
('--release',
'--classfile',
'--lib', utils.RT_JAR,
utils.R8_JAR,
- '--output', R8LIB_JAR,
+ '--output', output_path,
'--pg-conf', utils.R8LIB_KEEP_RULES,
- '--pg-map-output', R8LIB_MAP_FILE))
+ '--pg-map-output', output_map),
+ **kwargs)
def test_d8sample():
diff --git a/tools/r8lib_size_compare.py b/tools/r8lib_size_compare.py
new file mode 100755
index 0000000..84dd1a2
--- /dev/null
+++ b/tools/r8lib_size_compare.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# 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.
+
+'''
+Build r8lib.jar with both R8 and ProGuard and print a size comparison.
+
+By default, inlining is disabled in both R8 and ProGuard to make
+method-by-method comparison much easier. Pass --inlining to enable inlining.
+
+By default, only shows methods where R8's DEX output is 5 or more instructions
+larger than ProGuard+D8's output. Pass --threshold 0 to display all methods.
+'''
+
+import argparse
+import build_r8lib
+import os
+import subprocess
+import toolhelper
+import utils
+
+
+parser = argparse.ArgumentParser(description=__doc__.strip(),
+ formatter_class=argparse.RawTextHelpFormatter)
+parser.add_argument('-t', '--tmpdir',
+ help='Store auxiliary files in given directory')
+parser.add_argument('-i', '--inlining', action='store_true',
+ help='Enable inlining')
+parser.add_argument('--threshold')
+
+R8_RELOCATIONS = [
+ ('com.google.common', 'com.android.tools.r8.com.google.common'),
+ ('com.google.gson', 'com.android.tools.r8.com.google.gson'),
+ ('com.google.thirdparty', 'com.android.tools.r8.com.google.thirdparty'),
+ ('joptsimple', 'com.android.tools.r8.joptsimple'),
+ ('org.apache.commons', 'com.android.tools.r8.org.apache.commons'),
+ ('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm'),
+ ('it.unimi.dsi.fastutil', 'com.android.tools.r8.it.unimi.dsi.fastutil'),
+]
+
+
+def is_output_newer(input, output):
+ if not os.path.exists(output):
+ return False
+ return os.stat(input).st_mtime < os.stat(output).st_mtime
+
+
+def check_call(args, **kwargs):
+ utils.PrintCmd(args)
+ return subprocess.check_call(args, **kwargs)
+
+
+def main(tmpdir=None, inlining=True,
+ run_jarsizecompare=True, threshold=None):
+ if tmpdir is None:
+ with utils.TempDir() as tmpdir:
+ return main(tmpdir, inlining)
+
+ inline_suffix = '-inline' if inlining else '-noinline'
+
+ pg_config = utils.R8LIB_KEEP_RULES
+ r8lib_jar = os.path.join(utils.LIBS, 'r8lib%s.jar' % inline_suffix)
+ r8lib_map = os.path.join(utils.LIBS, 'r8lib%s-map.txt' % inline_suffix)
+ r8lib_args = None
+ if not inlining:
+ r8lib_args = ['-Dcom.android.tools.r8.disableinlining=1']
+ pg_config = os.path.join(tmpdir, 'keep-noinline.txt')
+ with open(pg_config, 'w') as new_config:
+ with open(utils.R8LIB_KEEP_RULES) as old_config:
+ new_config.write(old_config.read().rstrip('\n') +
+ '\n-optimizations !method/inlining/*\n')
+
+ if not is_output_newer(utils.R8_JAR, r8lib_jar):
+ r8lib_memory = os.path.join(tmpdir, 'r8lib%s-memory.txt' % inline_suffix)
+ build_r8lib.build_r8lib(
+ output_path=r8lib_jar, output_map=r8lib_map,
+ extra_args=r8lib_args, track_memory_file=r8lib_memory)
+
+ pg_output = os.path.join(tmpdir, 'r8lib-pg%s.jar' % inline_suffix)
+ pg_memory = os.path.join(tmpdir, 'r8lib-pg%s-memory.txt' % inline_suffix)
+ pg_map = os.path.join(tmpdir, 'r8lib-pg%s-map.txt' % inline_suffix)
+ pg_args = ['tools/track_memory.sh', pg_memory,
+ 'third_party/proguard/proguard6.0.2/bin/proguard.sh',
+ '@' + pg_config,
+ '-lib', utils.RT_JAR,
+ '-injar', utils.R8_JAR,
+ '-printmapping', pg_map,
+ '-outjar', pg_output]
+ for library_name, relocated_package in R8_RELOCATIONS:
+ pg_args.extend(['-dontwarn', relocated_package + '.**',
+ '-dontnote', relocated_package + '.**'])
+ check_call(pg_args)
+ if threshold is None:
+ threshold = 5
+ toolhelper.run('jarsizecompare',
+ ['--threshold', str(threshold),
+ '--lib', utils.RT_JAR,
+ '--input', 'input', utils.R8_JAR,
+ '--input', 'r8', r8lib_jar, r8lib_map,
+ '--input', 'pg', pg_output, pg_map])
+
+
+if __name__ == '__main__':
+ main(**vars(parser.parse_args()))
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index a7f509c..82a2824 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -9,7 +9,7 @@
import utils
def run(tool, args, build=None, debug=True,
- profile=False, track_memory_file=None):
+ profile=False, track_memory_file=None, extra_args=None):
if build is None:
build, args = extract_build_from_args(args)
if build:
@@ -18,6 +18,8 @@
if track_memory_file:
cmd.extend(['tools/track_memory.sh', track_memory_file])
cmd.append('java')
+ if extra_args:
+ cmd.extend(extra_args)
if debug:
cmd.append('-ea')
if profile: