Merge "-printusage part II: print dead code."
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 3c408cb..af67b41 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -173,10 +173,11 @@
CompilationResult output =
new CompilationResult(
- new ApplicationWriter(app, appInfo, options, NamingLens.getIdentityLens(), null)
- .write(null, executor),
- app,
- appInfo);
+ new ApplicationWriter(
+ app, appInfo, options, null, NamingLens.getIdentityLens(), null)
+ .write(null, executor),
+ app,
+ appInfo);
options.printWarnings();
return output;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6216b8f..4867b54 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -37,6 +37,7 @@
import com.android.tools.r8.shaking.TreePruner;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.CfgPrinter;
+import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.PackageDistribution;
import com.android.tools.r8.utils.ThreadUtils;
@@ -74,13 +75,15 @@
ExecutorService executorService,
DexApplication application,
AppInfo appInfo,
+ byte[] deadCode,
NamingLens namingLens,
byte[] proguardSeedsData,
PackageDistribution packageDistribution,
InternalOptions options)
throws ExecutionException {
try {
- return new ApplicationWriter(application, appInfo, options, namingLens, proguardSeedsData)
+ return new ApplicationWriter(
+ application, appInfo, options, deadCode, namingLens, proguardSeedsData)
.write(packageDistribution, executorService);
} catch (IOException e) {
throw new RuntimeException("Cannot write dex application", e);
@@ -180,7 +183,7 @@
}
}
- static CompilationResult runForTesting(
+ private static CompilationResult runForTesting(
AndroidApp app,
InternalOptions options,
ExecutorService executor)
@@ -337,6 +340,7 @@
executorService,
application,
appInfo,
+ application.deadCode,
namingLens,
proguardSeedsData,
packageDistribution,
@@ -386,42 +390,46 @@
if (options.printMapping && !options.skipMinification) {
assert outputApp.hasProguardMap();
try (Closer closer = Closer.create()) {
- OutputStream mapOut = openPathWithDefault(
+ OutputStream mapOut = FileUtils.openPathWithDefault(
closer,
options.printMappingFile,
- System.out);
+ System.out,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeProguardMap(closer, mapOut);
}
}
if (options.printSeeds) {
assert outputApp.hasProguardSeeds();
try (Closer closer = Closer.create()) {
- OutputStream seedsOut = openPathWithDefault(closer, options.seedsFile, System.out);
+ OutputStream seedsOut = FileUtils.openPathWithDefault(
+ closer,
+ options.seedsFile,
+ System.out,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeProguardSeeds(closer, seedsOut);
}
}
if (options.printMainDexList && outputApp.hasMainDexList()) {
try (Closer closer = Closer.create()) {
OutputStream mainDexOut =
- openPathWithDefault(closer, options.printMainDexListFile, System.out);
+ FileUtils.openPathWithDefault(
+ closer,
+ options.printMainDexListFile,
+ System.out,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
outputApp.writeMainDexList(closer, mainDexOut);
}
}
- }
-
- private static OutputStream openPathWithDefault(Closer closer,
- Path file,
- PrintStream defaultOutput) throws IOException {
- OutputStream mapOut;
- if (file == null) {
- mapOut = defaultOutput;
- } else {
- mapOut =
- Files.newOutputStream(
- file, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
- closer.register(mapOut);
+ if (options.printUsage && outputApp.hasDeadCode()) {
+ try (Closer closer = Closer.create()) {
+ OutputStream deadCodeOut = FileUtils.openPathWithDefault(
+ closer,
+ options.printUsageFile,
+ System.out,
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ outputApp.writeDeadCode(closer, deadCodeOut);
+ }
}
- return mapOut;
}
/**
diff --git a/src/main/java/com/android/tools/r8/bisect/Bisect.java b/src/main/java/com/android/tools/r8/bisect/Bisect.java
index d7b23c5..4fe3773 100644
--- a/src/main/java/com/android/tools/r8/bisect/Bisect.java
+++ b/src/main/java/com/android/tools/r8/bisect/Bisect.java
@@ -177,7 +177,7 @@
throws IOException, ExecutionException {
InternalOptions options = new InternalOptions();
AppInfo appInfo = new AppInfo(app);
- ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null);
+ ApplicationWriter writer = new ApplicationWriter(app, appInfo, options, null, null, null);
AndroidApp outApp = writer.write(null, executor);
outApp.writeToDirectory(output, OutputMode.Indexed);
}
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 39f741f..c9da10b 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -42,6 +42,7 @@
public final DexApplication application;
public final AppInfo appInfo;
+ public final byte[] deadCode;
public final NamingLens namingLens;
public final byte[] proguardSeedsData;
public final InternalOptions options;
@@ -107,6 +108,7 @@
DexApplication application,
AppInfo appInfo,
InternalOptions options,
+ byte[] deadCode,
NamingLens namingLens,
byte[] proguardSeedsData) {
assert application != null;
@@ -114,6 +116,7 @@
this.appInfo = appInfo;
assert options != null;
this.options = options;
+ this.deadCode = deadCode;
this.namingLens = namingLens;
this.proguardSeedsData = proguardSeedsData;
}
@@ -171,6 +174,9 @@
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while waiting for future.", e);
}
+ if (deadCode != null) {
+ builder.setDeadCode(deadCode);
+ }
// Write the proguard map file after writing the dex files, as the map writer traverses
// the DexProgramClass structures, which are destructively updated during dex file writing.
byte[] proguardMapResult = writeProguardMapFile();
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 2c9366c..0d2edcb 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -15,6 +15,7 @@
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
+import com.google.common.primitives.Bytes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
@@ -38,6 +39,7 @@
private LibraryClassCollection libraryClasses;
public final ImmutableSet<DexType> mainDexList;
+ public final byte[] deadCode;
private final ClassNameMapper proguardMap;
@@ -55,6 +57,7 @@
ClasspathClassCollection classpathClasses,
LibraryClassCollection libraryClasses,
ImmutableSet<DexType> mainDexList,
+ byte[] deadCode,
DexItemFactory dexItemFactory,
DexString highestSortingString,
Timing timing) {
@@ -64,6 +67,7 @@
this.classpathClasses = classpathClasses;
this.libraryClasses = libraryClasses;
this.mainDexList = mainDexList;
+ this.deadCode = deadCode;
this.dexItemFactory = dexItemFactory;
this.highestSortingString = highestSortingString;
this.timing = timing;
@@ -305,16 +309,18 @@
private LibraryClassCollection libraryClasses;
public final DexItemFactory dexItemFactory;
- public ClassNameMapper proguardMap;
+ ClassNameMapper proguardMap;
private final Timing timing;
- public DexString highestSortingString;
+ DexString highestSortingString;
+ private byte[] deadCode;
private final Set<DexType> mainDexList = Sets.newIdentityHashSet();
public Builder(DexItemFactory dexItemFactory, Timing timing) {
this.programClasses = new ArrayList<>();
this.dexItemFactory = dexItemFactory;
this.timing = timing;
+ this.deadCode = null;
this.classpathClasses = null;
this.libraryClasses = null;
}
@@ -328,6 +334,7 @@
highestSortingString = application.highestSortingString;
dexItemFactory = application.dexItemFactory;
mainDexList.addAll(application.mainDexList);
+ deadCode = application.deadCode;
}
public synchronized Builder setProguardMap(ClassNameMapper proguardMap) {
@@ -343,6 +350,19 @@
return this;
}
+ public Builder appendDeadCode(byte[] deadCodeAtAnotherRound) {
+ if (deadCodeAtAnotherRound == null) {
+ return this;
+ }
+ if (this.deadCode == null) {
+ this.deadCode = deadCodeAtAnotherRound;
+ return this;
+ }
+ // Concatenate existing byte[] and the given byte[].
+ this.deadCode = Bytes.concat(this.deadCode, deadCodeAtAnotherRound);
+ return this;
+ }
+
public synchronized Builder setHighestSortingString(DexString value) {
highestSortingString = value;
return this;
@@ -389,6 +409,7 @@
classpathClasses,
libraryClasses,
ImmutableSet.copyOf(mainDexList),
+ deadCode,
dexItemFactory,
highestSortingString,
timing);
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 3b78e1b..cc29125 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -159,8 +159,6 @@
if (isOptionalArgumentGiven()) {
configurationBuilder.setPrintUsageFile(parseFileName());
}
- // TODO(b/36799826): once fully implemented, no longer necessary to warn.
- System.out.println("WARNING: Ignoring option: -printusage");
} else if (acceptString("verbose")) {
configurationBuilder.setVerbose(true);
} else if (acceptString("ignorewarnings")) {
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index 3968c9a..2a5c9e0 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -13,6 +13,7 @@
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -22,25 +23,27 @@
private DexApplication application;
private final AppInfoWithLiveness appInfo;
private final InternalOptions options;
+ private UsagePrinter usagePrinter;
public TreePruner(
DexApplication application, AppInfoWithLiveness appInfo, InternalOptions options) {
this.application = application;
this.appInfo = appInfo;
this.options = options;
+ this.usagePrinter = options.printUsage ? new UsagePrinter() : UsagePrinter.DONT_PRINT;
}
- public DexApplication run() {
+ public DexApplication run() throws IOException {
application.timing.begin("Pruning application...");
if (options.debugKeepRules && !options.skipMinification) {
System.out.println(
- "NOTE: Debugging keep rules on a minified build might yield broken builds, as\n" +
- " minifcation also depends on the used keep rules. We recommend using\n" +
- " --skip-minification.");
+ "NOTE: Debugging keep rules on a minified build might yield broken builds, as\n"
+ + " minification also depends on the used keep rules. We recommend using\n"
+ + " --skip-minification.");
}
DexApplication result;
try {
- result = removeUnused(application).build();
+ result = removeUnused(application).appendDeadCode(usagePrinter.toByteArray()).build();
} finally {
application.timing.end();
}
@@ -60,6 +63,7 @@
if (Log.ENABLED) {
Log.debug(getClass(), "Removing class: " + clazz);
}
+ usagePrinter.printUnusedClass(clazz);
} else {
newClasses.add(clazz);
if (!appInfo.instantiatedTypes.contains(clazz.type) &&
@@ -78,10 +82,12 @@
}
// The class is used and must be kept. Remove the unused fields and methods from
// the class.
+ usagePrinter.visiting(clazz);
clazz.directMethods = reachableMethods(clazz.directMethods(), clazz);
clazz.virtualMethods = reachableMethods(clazz.virtualMethods(), clazz);
clazz.instanceFields = reachableFields(clazz.instanceFields());
clazz.staticFields = reachableFields(clazz.staticFields());
+ usagePrinter.visited();
}
}
return newClasses;
@@ -122,18 +128,18 @@
reachableMethods.add(methods[i]);
}
for (int i = firstUnreachable; i < methods.length; i++) {
+ DexEncodedMethod method = methods[i];
if (appInfo.liveMethods.contains(methods[i].getKey())) {
- reachableMethods.add(methods[i]);
- } else if (options.debugKeepRules && isDefaultConstructor(methods[i])) {
+ reachableMethods.add(method);
+ } else if (options.debugKeepRules && isDefaultConstructor(method)) {
// Keep the method but rewrite its body, if it has one.
reachableMethods.add(methods[i].accessFlags.isAbstract()
- ? methods[i]
- : methods[i].toMethodThatLogsError(application.dexItemFactory));
- } else if (appInfo.targetedMethods.contains(methods[i].getKey())) {
+ ? method
+ : method.toMethodThatLogsError(application.dexItemFactory));
+ } else if (appInfo.targetedMethods.contains(method.getKey())) {
if (Log.ENABLED) {
- Log.debug(getClass(), "Making method %s abstract.", methods[i].method);
+ Log.debug(getClass(), "Making method %s abstract.", method.method);
}
- DexEncodedMethod method = methods[i];
// Final classes cannot be abstract, so we have to keep the method in that case.
// Also some other kinds of methods cannot be abstract, so keep them around.
boolean allowAbstract = clazz.accessFlags.isAbstract()
@@ -144,10 +150,13 @@
// By construction, private and static methods cannot be reachable but non-live.
assert !method.accessFlags.isPrivate() && !method.accessFlags.isStatic();
reachableMethods.add(allowAbstract
- ? methods[i].toAbstractMethod()
- : methods[i].toEmptyThrowingMethod());
- } else if (Log.ENABLED) {
- Log.debug(getClass(), "Removing method %s.", methods[i].method);
+ ? method.toAbstractMethod()
+ : method.toEmptyThrowingMethod());
+ } else {
+ if (Log.ENABLED) {
+ Log.debug(getClass(), "Removing method %s.", method.method);
+ }
+ usagePrinter.printUnusedMethod(method);
}
}
return reachableMethods.toArray(new DexEncodedMethod[reachableMethods.size()]);
@@ -155,21 +164,27 @@
private DexEncodedField[] reachableFields(DexEncodedField[] fields) {
int firstUnreachable = firstUnreachableIndex(fields, appInfo.liveFields);
+ // Return the original array if all fields are used.
if (firstUnreachable == -1) {
return fields;
}
if (Log.ENABLED) {
Log.debug(getClass(), "Removing field: " + fields[firstUnreachable]);
}
+ usagePrinter.printUnusedField(fields[firstUnreachable]);
ArrayList<DexEncodedField> reachableFields = new ArrayList<>(fields.length);
for (int i = 0; i < firstUnreachable; i++) {
reachableFields.add(fields[i]);
}
for (int i = firstUnreachable + 1; i < fields.length; i++) {
- if (appInfo.liveFields.contains(fields[i].getKey())) {
- reachableFields.add(fields[i]);
- } else if (Log.ENABLED) {
- Log.debug(getClass(), "Removing field: " + fields[i]);
+ DexEncodedField field = fields[i];
+ if (appInfo.liveFields.contains(field.getKey())) {
+ reachableFields.add(field);
+ } else {
+ if (Log.ENABLED) {
+ Log.debug(getClass(), "Removing field: " + field);
+ }
+ usagePrinter.printUnusedField(field);
}
}
return reachableFields.toArray(new DexEncodedField[reachableFields.size()]);
diff --git a/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java b/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
new file mode 100644
index 0000000..7e41671
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
@@ -0,0 +1,119 @@
+// 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.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import java.nio.charset.StandardCharsets;
+
+class UsagePrinter {
+ private static final String INDENT = " ";
+
+ static final UsagePrinter DONT_PRINT = new NoOpUsagePrinter();
+
+ private final StringBuilder writer;
+ private DexProgramClass enclosingClazz = null;
+ private boolean clazzPrefixPrinted = false;
+
+ UsagePrinter() {
+ writer = new StringBuilder();
+ }
+
+ byte[] toByteArray() {
+ return writer.toString().getBytes(StandardCharsets.UTF_8);
+ }
+
+ void printUnusedClass(DexProgramClass clazz) {
+ writer.append(clazz.toSourceString());
+ writer.append('\n');
+ }
+
+ // Visiting methods and fields of the given clazz.
+ void visiting(DexProgramClass clazz) {
+ assert enclosingClazz == null;
+ enclosingClazz = clazz;
+ }
+
+ // Visited methods and fields of the top at the clazz stack.
+ void visited() {
+ enclosingClazz = null;
+ clazzPrefixPrinted = false;
+ }
+
+ private void printClazzPrefixIfNecessary() {
+ assert enclosingClazz != null;
+ if (!clazzPrefixPrinted) {
+ writer.append(enclosingClazz.toSourceString());
+ writer.append('\n');
+ clazzPrefixPrinted = true;
+ }
+ }
+
+ void printUnusedMethod(DexEncodedMethod method) {
+ printClazzPrefixIfNecessary();
+ writer.append(INDENT);
+ String accessFlags = method.accessFlags.toString();
+ if (!accessFlags.isEmpty()) {
+ writer.append(accessFlags).append(' ');
+ }
+ writer.append(method.method.proto.returnType.toSourceString()).append(' ');
+ writer.append(method.method.name.toSourceString());
+ writer.append('(');
+ for (int i = 0; i < method.method.proto.parameters.values.length; i++) {
+ if (i != 0) {
+ writer.append(',');
+ }
+ writer.append(method.method.proto.parameters.values[i].toSourceString());
+ }
+ writer.append(')');
+ writer.append('\n');
+ }
+
+ void printUnusedField(DexEncodedField field) {
+ printClazzPrefixIfNecessary();
+ writer.append(INDENT);
+ String accessFlags = field.accessFlags.toString();
+ if (!accessFlags.isEmpty()) {
+ writer.append(accessFlags).append(' ');
+ }
+ writer.append(field.field.type.toSourceString()).append(" ");
+ writer.append(field.field.name.toSourceString());
+ writer.append('\n');
+ }
+
+ // Empty implementation to silently ignore printing dead code.
+ private static class NoOpUsagePrinter extends UsagePrinter {
+
+ @Override
+ byte[] toByteArray() {
+ return null;
+ }
+
+ @Override
+ void printUnusedClass(DexProgramClass clazz) {
+ // Intentionally left empty.
+ }
+
+ @Override
+ void visiting(DexProgramClass clazz) {
+ // Intentionally left empty.
+ }
+
+ @Override
+ void visited() {
+ // Intentionally left empty.
+ }
+
+ @Override
+ void printUnusedMethod(DexEncodedMethod method) {
+ // Intentionally left empty.
+ }
+
+ @Override
+ void printUnusedField(DexEncodedField field) {
+ // Intentionally left empty.
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index 3877f1a..15ca87b 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -52,6 +52,7 @@
private final ImmutableList<Resource> programResources;
private final ImmutableList<ClassFileResourceProvider> classpathResourceProviders;
private final ImmutableList<ClassFileResourceProvider> libraryResourceProviders;
+ private final Resource deadCode;
private final Resource proguardMap;
private final Resource proguardSeeds;
private final Resource packageDistribution;
@@ -62,6 +63,7 @@
ImmutableList<Resource> programResources,
ImmutableList<ClassFileResourceProvider> classpathResourceProviders,
ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
+ Resource deadCode,
Resource proguardMap,
Resource proguardSeeds,
Resource packageDistribution,
@@ -69,6 +71,7 @@
this.programResources = programResources;
this.classpathResourceProviders = classpathResourceProviders;
this.libraryResourceProviders = libraryResourceProviders;
+ this.deadCode = deadCode;
this.proguardMap = proguardMap;
this.proguardSeeds = proguardSeeds;
this.packageDistribution = packageDistribution;
@@ -169,6 +172,20 @@
}
/**
+ * True if the dead-code resource exists.
+ */
+ public boolean hasDeadCode() {
+ return deadCode != null;
+ }
+
+ /**
+ * Get the input stream of the dead-code resource if exists.
+ */
+ public InputStream getDeadCode(Closer closer) throws IOException {
+ return deadCode == null ? null : deadCode.getStream(closer);
+ }
+
+ /**
* True if the proguard-map resource exists.
*/
public boolean hasProguardMap() {
@@ -337,6 +354,12 @@
out.write(ByteStreams.toByteArray(input));
}
+ public void writeDeadCode(Closer closer, OutputStream out) throws IOException {
+ InputStream input = getDeadCode(closer);
+ assert input != null;
+ out.write(ByteStreams.toByteArray(input));
+ }
+
/**
* Builder interface for constructing an AndroidApp.
*/
@@ -345,6 +368,7 @@
private final List<Resource> programResources = new ArrayList<>();
private final List<ClassFileResourceProvider> classpathResourceProviders = new ArrayList<>();
private final List<ClassFileResourceProvider> libraryResourceProviders = new ArrayList<>();
+ private Resource deadCode;
private Resource proguardMap;
private Resource proguardSeeds;
private Resource packageDistribution;
@@ -359,6 +383,7 @@
programResources.addAll(app.programResources);
classpathResourceProviders.addAll(app.classpathResourceProviders);
libraryResourceProviders.addAll(app.libraryResourceProviders);
+ deadCode = app.deadCode;
proguardMap = app.proguardMap;
proguardSeeds = app.proguardSeeds;
packageDistribution = app.packageDistribution;
@@ -499,6 +524,14 @@
}
/**
+ * Set dead-code data.
+ */
+ public Builder setDeadCode(byte[] content) {
+ deadCode = content == null ? null : Resource.fromBytes(null, content);
+ return this;
+ }
+
+ /**
* Set proguard-map file.
*/
public Builder setProguardMapFile(Path file) {
@@ -561,6 +594,7 @@
ImmutableList.copyOf(programResources),
ImmutableList.copyOf(classpathResourceProviders),
ImmutableList.copyOf(libraryResourceProviders),
+ deadCode,
proguardMap,
proguardSeeds,
packageDistribution,
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 16f41cc..5c5a776 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -4,11 +4,16 @@
package com.android.tools.r8.utils;
import com.android.tools.r8.CompilationException;
+import com.google.common.io.Closer;
import java.io.BufferedReader;
import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -79,10 +84,27 @@
if (!isJarOrZip && !(Files.exists(path) && Files.isDirectory(path))) {
throw new CompilationException(
"Invalid output: "
- + path +
- "\nOutput must be a .zip or .jar archive or an existing directory");
+ + path
+ + "\nOutput must be a .zip or .jar archive or an existing directory");
}
}
return path;
}
+
+ public static OutputStream openPathWithDefault(
+ Closer closer,
+ Path file,
+ PrintStream defaultOutput,
+ OpenOption... openOptions)
+ throws IOException {
+ OutputStream mapOut;
+ if (file == null) {
+ mapOut = defaultOutput;
+ } else {
+ mapOut = Files.newOutputStream(file, openOptions);
+ closer.register(mapOut);
+ }
+ return mapOut;
+ }
+
}
diff --git a/src/test/examples/shaking1/keep-rules-printusage.txt b/src/test/examples/shaking1/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking1/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking12/keep-rules-printusage.txt b/src/test/examples/shaking12/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking12/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking2/keep-rules-printusage.txt b/src/test/examples/shaking2/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking2/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking4/keep-rules-printusage.txt b/src/test/examples/shaking4/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking4/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking8/keep-rules-printusage.txt b/src/test/examples/shaking8/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking8/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/examples/shaking9/keep-rules-printusage.txt b/src/test/examples/shaking9/keep-rules-printusage.txt
new file mode 100644
index 0000000..5db2d89
--- /dev/null
+++ b/src/test/examples/shaking9/keep-rules-printusage.txt
@@ -0,0 +1,8 @@
+# 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.
+
+@keep-rules.txt
+
+-dontobfuscate
+-printusage
diff --git a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
index 6ceb912..e5bbad6 100644
--- a/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
+++ b/src/test/java/com/android/tools/r8/jasmin/JasminTestBase.java
@@ -85,6 +85,7 @@
Executors.newSingleThreadExecutor(),
app,
info,
+ null,
NamingLens.getIdentityLens(),
null,
null,
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index 416a266..001883f 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -440,7 +440,7 @@
DexApplication application = builder.build();
AppInfoWithSubtyping appInfo = new AppInfoWithSubtyping(application);
ApplicationWriter writer =
- new ApplicationWriter(application, appInfo, options, NamingLens.getIdentityLens(), null);
+ new ApplicationWriter(application, appInfo, options, null, NamingLens.getIdentityLens(), null);
ExecutorService executor = ThreadUtils.getExecutorService(options);
try {
return writer.write(null, executor);
diff --git a/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
new file mode 100644
index 0000000..47c6330
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/PrintUsageTest.java
@@ -0,0 +1,255 @@
+// 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 static com.android.tools.r8.shaking.TreeShakingTest.getTestOptionalParameter;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationException;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.shaking.PrintUsageTest.PrintUsageInspector.ClassSubject;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PrintUsageTest {
+ private static final String ANDROID_JAR = ToolHelper.getDefaultAndroidJar();
+ private static final String PRINT_USAGE_FILE_SUFFIX = "-print-usage.txt";
+
+ private final String test;
+ private final String programFile;
+ private final List<String> keepRulesFiles;
+ private final Consumer<PrintUsageInspector> inspection;
+
+ @Rule
+ public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+
+ public PrintUsageTest(
+ String test,
+ List<String> keepRulesFiles,
+ Consumer<PrintUsageInspector> inspection) {
+ this.test = test;
+ this.programFile = ToolHelper.EXAMPLES_BUILD_DIR + test + ".jar";
+ this.keepRulesFiles = keepRulesFiles;
+ this.inspection = inspection;
+ }
+
+ @Before
+ public void runR8andGetPrintUsage()
+ throws IOException, ProguardRuleParserException, ExecutionException, CompilationException {
+ Path out = temp.getRoot().toPath();
+ R8Command command =
+ R8Command.builder()
+ .setOutputPath(out)
+ .addProgramFiles(Paths.get(programFile))
+ .addProguardConfigurationFiles(ListUtils.map(keepRulesFiles, Paths::get))
+ .addLibraryFiles(Paths.get(ANDROID_JAR))
+ .build();
+ ToolHelper.runR8(command, options -> {
+ options.printUsage = true;
+ options.printUsageFile = out.resolve(test + PRINT_USAGE_FILE_SUFFIX);
+ });
+ }
+
+ @Test
+ public void printUsageTest() throws IOException, ExecutionException {
+ Path out = temp.getRoot().toPath();
+ Path printUsageFile = out.resolve(test + PRINT_USAGE_FILE_SUFFIX);
+ if (inspection != null) {
+ PrintUsageInspector inspector = new PrintUsageInspector(printUsageFile);
+ inspection.accept(inspector);
+ }
+ }
+
+ @Parameters(name = "test: {0} keep: {1}")
+ public static Collection<Object[]> data() {
+ List<String> tests = Arrays.asList(
+ "shaking1", "shaking2", "shaking4", "shaking8", "shaking9", "shaking12");
+
+ Map<String, Consumer<PrintUsageInspector>> inspections = new HashMap<>();
+ inspections.put("shaking1:keep-rules-printusage.txt", PrintUsageTest::inspectShaking1);
+ inspections.put("shaking2:keep-rules-printusage.txt", PrintUsageTest::inspectShaking2);
+ inspections.put("shaking4:keep-rules-printusage.txt", PrintUsageTest::inspectShaking4);
+ inspections.put("shaking8:keep-rules-printusage.txt", PrintUsageTest::inspectShaking8);
+ inspections.put("shaking9:keep-rules-printusage.txt", PrintUsageTest::inspectShaking9);
+ inspections.put("shaking12:keep-rules-printusage.txt", PrintUsageTest::inspectShaking12);
+
+ List<Object[]> testCases = new ArrayList<>();
+ Set<String> usedInspections = new HashSet<>();
+ for (String test : tests) {
+ File[] keepFiles = new File(ToolHelper.EXAMPLES_DIR + test)
+ .listFiles(file -> file.isFile() && file.getName().endsWith(".txt"));
+ for (File keepFile : keepFiles) {
+ String keepName = keepFile.getName();
+ Consumer<PrintUsageInspector> inspection =
+ getTestOptionalParameter(inspections, usedInspections, test, keepName);
+ if (inspection != null) {
+ testCases.add(new Object[]{test, ImmutableList.of(keepFile.getPath()), inspection});
+ }
+ }
+ }
+ assert usedInspections.size() == inspections.size();
+ return testCases;
+ }
+
+ private static void inspectShaking1(PrintUsageInspector inspector) {
+ assertTrue(inspector.clazz("shaking1.Unused").isPresent());
+ assertFalse(inspector.clazz("shaking1.Used").isPresent());
+ }
+
+ private static void inspectShaking2(PrintUsageInspector inspector) {
+ Optional<ClassSubject> staticFields = inspector.clazz("shaking2.StaticFields");
+ assertTrue(staticFields.isPresent());
+ assertTrue(staticFields.get().field("int", "completelyUnused"));
+ assertTrue(staticFields.get().field("int", "unused"));
+ Optional<ClassSubject> subClass1 = inspector.clazz("shaking2.SubClass1");
+ assertTrue(subClass1.isPresent());
+ assertTrue(subClass1.get().method("void", "unusedVirtualMethod", Collections.emptyList()));
+ Optional<ClassSubject> superClass = inspector.clazz("shaking2.SuperClass");
+ assertTrue(superClass.isPresent());
+ assertTrue(superClass.get().method("void", "unusedStaticMethod", Collections.emptyList()));
+ }
+
+ private static void inspectShaking4(PrintUsageInspector inspector) {
+ assertTrue(inspector.clazz("shaking4.Interface").isPresent());
+ }
+
+ private static void inspectShaking8(PrintUsageInspector inspector) {
+ Optional<ClassSubject> thing = inspector.clazz("shaking8.Thing");
+ assertTrue(thing.isPresent());
+ assertTrue(thing.get().field("int", "aField"));
+ assertFalse(inspector.clazz("shaking8.OtherThing").isPresent());
+ assertTrue(inspector.clazz("shaking8.YetAnotherThing").isPresent());
+ }
+
+ private static void inspectShaking9(PrintUsageInspector inspector) {
+ assertFalse(inspector.clazz("shaking9.Superclass").isPresent());
+ Optional<ClassSubject> subClass = inspector.clazz("shaking9.Subclass");
+ assertTrue(subClass.isPresent());
+ assertTrue(subClass.get().method("void", "aMethod", Collections.emptyList()));
+ }
+
+ private static void inspectShaking12(PrintUsageInspector inspector) {
+ assertFalse(inspector.clazz("shaking12.PeopleClass").isPresent());
+ Optional<ClassSubject> animal = inspector.clazz("shaking12.AnimalClass");
+ assertTrue(animal.isPresent());
+ assertTrue(animal.get().method("java.lang.String", "getName", Collections.emptyList()));
+ }
+
+ static class PrintUsageInspector {
+ private Map<String, ClassSubject> printedUsage;
+
+ PrintUsageInspector(Path printUsageFile) throws IOException {
+ printedUsage = new HashMap<>();
+ try (Stream<String> lines = Files.lines(printUsageFile)) {
+ lines.forEach(line -> {
+ if (line.startsWith(" ")) {
+ if (line.contains("(") && line.contains(")")) {
+ readMethod(line);
+ } else {
+ readField(line);
+ }
+ } else {
+ readClazz(line);
+ }
+ });
+ }
+ }
+
+ private ClassSubject lastClazz = null;
+
+ private void readClazz(String line) {
+ if (printedUsage.containsKey(line)) {
+ lastClazz = printedUsage.get(line);
+ } else {
+ lastClazz = new ClassSubject();
+ printedUsage.put(line, lastClazz);
+ }
+ }
+
+ private void readMethod(String line) {
+ assert lastClazz != null;
+ lastClazz.putMethod(line);
+ }
+
+ private void readField(String line) {
+ assert lastClazz != null;
+ lastClazz.putField(line);
+ }
+
+ public Optional<ClassSubject> clazz(String name) {
+ if (printedUsage.containsKey(name)) {
+ return Optional.of(printedUsage.get(name));
+ }
+ return Optional.empty();
+ }
+
+ static class ClassSubject {
+ private Set<String> methods;
+ private Set<String> fields;
+
+ public ClassSubject() {
+ methods = new HashSet<>();
+ fields = new HashSet<>();
+ }
+
+ void putMethod(String line) {
+ String[] tokens = line.split(" ");
+ assert tokens.length >= 2;
+ methods.add(tokens[tokens.length - 2] + " " + tokens[tokens.length - 1]);
+ }
+
+ void putField(String line) {
+ String[] tokens = line.split(" ");
+ assert tokens.length >= 2;
+ fields.add(tokens[tokens.length - 2] + " " + tokens[tokens.length - 1]);
+ }
+
+ public boolean method(String returnType, String name, List<String> parameters) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(returnType).append(" ").append(name);
+ builder.append("(");
+ for (int i = 0; i < parameters.size(); i++) {
+ if (i != 0) {
+ builder.append(",");
+ }
+ builder.append(parameters.get(i));
+ }
+ builder.append(")");
+ return methods.contains(builder.toString());
+ }
+
+ public boolean field(String type, String name) {
+ return fields.contains(type + " " + name);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index 55a9fc0..5b94e6b 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -727,7 +727,7 @@
}
}
- private static <T> T getTestOptionalParameter(
+ static <T> T getTestOptionalParameter(
Map<String, T> specifications, Set<String> usedSpecifications, String test,
String keepName) {
T parameter = specifications.get(test);
diff --git a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
index 71ee09f..9a12d3e 100644
--- a/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
+++ b/src/test/java/com/android/tools/r8/smali/SmaliTestBase.java
@@ -500,6 +500,7 @@
Executors.newSingleThreadExecutor(),
application,
appInfo,
+ null,
NamingLens.getIdentityLens(),
null,
null,