Introduce nodes for conditional keep rules and link them in the graph.
This CL adds support for -keepclassmembers. Actual if rules will be done in
subsequent CLs.
Bug: 131822600
Change-Id: I784875747527f243ce94d3c202620333dfead359
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/ConditionalKeepRuleGraphNode.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/ConditionalKeepRuleGraphNode.java
new file mode 100644
index 0000000..6b293e4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/ConditionalKeepRuleGraphNode.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2019, 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.experimental.graphinfo;
+
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
+import com.android.tools.r8.shaking.ProguardKeepRule;
+import com.google.common.base.Objects;
+import java.util.Set;
+
+@Keep
+public final class ConditionalKeepRuleGraphNode extends GraphNode {
+
+ private final ProguardKeepRule rule;
+ private final Set<GraphNode> preconditions;
+
+ public ConditionalKeepRuleGraphNode(ProguardKeepRule rule, Set<GraphNode> preconditions) {
+ super(false);
+ assert rule != null;
+ assert preconditions != null;
+ assert !preconditions.isEmpty();
+ this.rule = rule;
+ this.preconditions = preconditions;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConditionalKeepRuleGraphNode)) {
+ return false;
+ }
+ ConditionalKeepRuleGraphNode other = (ConditionalKeepRuleGraphNode) o;
+ return other.rule == rule && other.preconditions.equals(preconditions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(rule, preconditions);
+ }
+
+ public Origin getOrigin() {
+ return rule.getOrigin();
+ }
+
+ public Position getPosition() {
+ return rule.getPosition();
+ }
+
+ public String getContent() {
+ return rule.getSource();
+ }
+
+ public Set<GraphNode> getPreconditions() {
+ return preconditions;
+ }
+
+ /**
+ * Get an identity string determining this keep rule.
+ *
+ * <p>The identity string is typically the source-file (if present) followed by the line number.
+ * {@code <keep-rule-file>:<keep-rule-start-line>:<keep-rule-start-column>}.
+ */
+ @Override
+ public String toString() {
+ return (getOrigin() == Origin.unknown() ? getContent() : getOrigin())
+ + ":"
+ + shortPositionInfo(getPosition());
+ }
+
+ private static String shortPositionInfo(Position position) {
+ if (position instanceof TextRange) {
+ TextPosition start = ((TextRange) position).getStart();
+ return start.getLine() + ":" + start.getColumn();
+ }
+ if (position instanceof TextPosition) {
+ TextPosition start = (TextPosition) position;
+ return start.getLine() + ":" + start.getColumn();
+ }
+ return position.getDescription();
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
index 3e55242..214d103 100644
--- a/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/GraphEdgeInfo.java
@@ -16,6 +16,8 @@
// Prioritized list of edge types.
KeepRule,
CompatibilityRule,
+ ConditionalKeepRule,
+ KeepRulePrecondition,
InstantiatedIn,
InvokedViaSuper,
TargetedBySuper,
@@ -45,7 +47,10 @@
switch (edgeKind()) {
case KeepRule:
case CompatibilityRule:
+ case ConditionalKeepRule:
return "referenced in keep rule";
+ case KeepRulePrecondition:
+ return "satisfied with precondition";
case InstantiatedIn:
return "instantiated in";
case InvokedViaSuper:
diff --git a/src/main/java/com/android/tools/r8/experimental/graphinfo/KeepRuleGraphNode.java b/src/main/java/com/android/tools/r8/experimental/graphinfo/KeepRuleGraphNode.java
index 7eb55a8..5aaf157 100644
--- a/src/main/java/com/android/tools/r8/experimental/graphinfo/KeepRuleGraphNode.java
+++ b/src/main/java/com/android/tools/r8/experimental/graphinfo/KeepRuleGraphNode.java
@@ -10,6 +10,8 @@
import com.android.tools.r8.position.TextRange;
import com.android.tools.r8.shaking.ProguardKeepRule;
+// Note: this could potentially be merged with ConditionalKeepRuleGraphNode
+// and an empty precondition set.
@Keep
public final class KeepRuleGraphNode extends GraphNode {
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 8ae75ff..04b9b53 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -874,12 +874,28 @@
} else {
assert !deferredAnnotations.containsKey(holder.type);
}
- rootSet.forEachDependentStaticMember(holder, appView, this::enqueueRootItem);
+ rootSet.forEachDependentStaticMember(holder, appView, this::enqueueDependentItem);
compatEnqueueHolderIfDependentNonStaticMember(
holder, rootSet.getDependentKeepClassCompatRule(holder.getType()));
}
}
+ private void enqueueDependentItem(
+ DexDefinition precondition, DexDefinition consequent, Set<ProguardKeepRule> reasons) {
+ DexReference preconditionReference = precondition.toReference();
+ if (keptGraphConsumer != null) {
+ GraphNode consequentNode = getGraphNode(consequent.toReference());
+ for (ProguardKeepRule rule : reasons) {
+ registerEdge(
+ consequentNode, KeepReason.dueToConditionalKeepRule(rule, preconditionReference));
+ }
+ }
+ // Note: the reason for keeping is reproted above, so this just uses the first.
+ ProguardKeepRule reason = reasons.iterator().next();
+ internalEnqueueRootItem(
+ consequent, KeepReason.dueToConditionalKeepRule(reason, preconditionReference));
+ }
+
private void processAnnotations(DexDefinition holder, DexAnnotation[] annotations) {
for (DexAnnotation annotation : annotations) {
processAnnotation(holder, annotation);
@@ -2498,7 +2514,15 @@
GraphNode sourceNode = getSourceNode(reason);
// TODO(b/120959039): Make sure we do have edges to nodes deriving library nodes!
if (!sourceNode.isLibraryNode()) {
- keptGraphConsumer.acceptEdge(sourceNode, target, getEdgeInfo(reason));
+ GraphEdgeInfo edgeInfo = getEdgeInfo(reason);
+ keptGraphConsumer.acceptEdge(sourceNode, target, edgeInfo);
+ if (reason.isDueToConditionalKeepRule()) {
+ GraphEdgeInfo conditionEdge = new GraphEdgeInfo(EdgeKind.KeepRulePrecondition);
+ for (DexReference precondition : reason.getPreconditions()) {
+ GraphNode preconditionNode = getGraphNode(precondition);
+ keptGraphConsumer.acceptEdge(preconditionNode, sourceNode, conditionEdge);
+ }
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepReason.java b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
index 4218b569..09f80d1 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepReason.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepReason.java
@@ -3,13 +3,18 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
+import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.experimental.graphinfo.GraphEdgeInfo;
import com.android.tools.r8.experimental.graphinfo.GraphEdgeInfo.EdgeKind;
import com.android.tools.r8.experimental.graphinfo.GraphNode;
import com.android.tools.r8.graph.DexDefinition;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItem;
+import com.android.tools.r8.graph.DexReference;
import com.android.tools.r8.graph.DexType;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
// TODO(herhut): Canonicalize reason objects.
public abstract class KeepReason {
@@ -26,6 +31,10 @@
return new DueToKeepRule(rule);
}
+ static KeepReason dueToConditionalKeepRule(ProguardKeepRule rule, DexReference reference) {
+ return new DueToConditionalKeepRule(rule, reference);
+ }
+
static KeepReason dueToProguardCompatibilityKeepRule(ProguardKeepRule rule) {
return new DueToProguardCompatibilityKeepRule(rule);
}
@@ -74,6 +83,10 @@
return false;
}
+ public boolean isDueToConditionalKeepRule() {
+ return false;
+ }
+
public ProguardKeepRule getProguardKeepRule() {
return null;
}
@@ -90,6 +103,10 @@
return new MethodHandleReferencedFrom(method);
}
+ public Collection<DexReference> getPreconditions() {
+ throw new Unreachable();
+ }
+
private static class DueToKeepRule extends KeepReason {
final ProguardKeepRule keepRule;
@@ -135,6 +152,32 @@
}
}
+ private static class DueToConditionalKeepRule extends DueToKeepRule {
+
+ private final DexReference condition;
+
+ public DueToConditionalKeepRule(ProguardKeepRule rule, DexReference condition) {
+ super(rule);
+ assert condition != null;
+ this.condition = condition;
+ }
+
+ @Override
+ public Set<DexReference> getPreconditions() {
+ return Collections.singleton(condition);
+ }
+
+ @Override
+ public boolean isDueToConditionalKeepRule() {
+ return true;
+ }
+
+ @Override
+ public EdgeKind edgeKind() {
+ return EdgeKind.ConditionalKeepRule;
+ }
+ }
+
private abstract static class BasedOnOtherMethod extends KeepReason {
private final DexEncodedMethod method;
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 819c4ff..3cabdf2 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -27,6 +27,7 @@
import com.android.tools.r8.graph.DirectMappedDexApplication;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.Consumer3;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.MethodSignatureEquivalence;
import com.android.tools.r8.utils.StringDiagnostic;
@@ -1260,13 +1261,15 @@
public void forEachDependentStaticMember(
DexDefinition item,
AppView<?> appView,
- BiConsumer<DexDefinition, Set<ProguardKeepRule>> fn) {
- getDependentItems(item).forEach((reference, reasons) -> {
- DexDefinition definition = appView.definitionFor(reference);
- if (definition != null && !definition.isDexClass() && definition.isStaticMember()) {
- fn.accept(definition, reasons);
- }
- });
+ Consumer3<DexDefinition, DexDefinition, Set<ProguardKeepRule>> fn) {
+ getDependentItems(item)
+ .forEach(
+ (reference, reasons) -> {
+ DexDefinition definition = appView.definitionFor(reference);
+ if (definition != null && !definition.isDexClass() && definition.isStaticMember()) {
+ fn.accept(item, definition, reasons);
+ }
+ });
}
public void forEachDependentNonStaticMember(
diff --git a/src/main/java/com/android/tools/r8/utils/Consumer3.java b/src/main/java/com/android/tools/r8/utils/Consumer3.java
new file mode 100644
index 0000000..029ed3d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/Consumer3.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils;
+
+@FunctionalInterface
+public interface Consumer3<F1, F2, F3> {
+ void accept(F1 f1, F2 f2, F3 f3);
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTest.java
new file mode 100644
index 0000000..9d4c94c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTest.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2019, 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.keptgraph;
+
+import com.android.tools.r8.NeverInline;
+
+public class KeptByKeepClassMembersTest {
+
+ public static void main(String[] args) {
+ bar();
+ }
+
+ @NeverInline
+ static void bar() {
+ System.out.println("called bar");
+ }
+
+ @NeverInline
+ static void baz() {
+ System.out.println("called baz");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTestRunner.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTestRunner.java
new file mode 100644
index 0000000..50e381c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByKeepClassMembersTestRunner.java
@@ -0,0 +1,103 @@
+// Copyright (c) 2019, 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.keptgraph;
+
+import static com.android.tools.r8.references.Reference.classFromClass;
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.android.tools.r8.utils.graphinspector.GraphInspector.QueryNode;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+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 KeptByKeepClassMembersTestRunner extends TestBase {
+
+ private static final Class<?> CLASS = KeptByKeepClassMembersTest.class;
+ private static final String PKG = CLASS.getPackage().getName();
+ private final String EXPECTED = StringUtils.lines("called bar");
+ private final String EXPECTED_WHYAREYOUKEEPING =
+ StringUtils.lines(
+ "void " + PKG + ".KeptByKeepClassMembersTest.baz()",
+ "|- is referenced in keep rule:",
+ "| -keepclassmembers class "
+ + PKG
+ + ".KeptByKeepClassMembersTest { static void baz(); }",
+ "|- is satisfied with precondition:",
+ "| " + PKG + ".KeptByKeepClassMembersTest",
+ "|- is referenced in keep rule:",
+ "| -keep class "
+ + PKG
+ + ".KeptByKeepClassMembersTest"
+ + " { public static void main(java.lang.String[]); }");
+
+ private final TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+ }
+
+ public KeptByKeepClassMembersTestRunner(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testKeptMethod() throws Exception {
+ ClassReference clazz = classFromClass(CLASS);
+ MethodReference mainMethod = methodFromMethod(CLASS.getDeclaredMethod("main", String[].class));
+ MethodReference barMethod = methodFromMethod(CLASS.getDeclaredMethod("bar"));
+ MethodReference bazMethod = methodFromMethod(CLASS.getDeclaredMethod("baz"));
+
+ String keepClassMembersRuleContent =
+ "-keepclassmembers class "
+ + KeptByKeepClassMembersTest.class.getTypeName()
+ + " { static void baz(); }";
+
+ WhyAreYouKeepingConsumer whyAreYouKeepingConsumer = new WhyAreYouKeepingConsumer(null);
+ GraphInspector inspector =
+ testForR8(parameters.getBackend())
+ .enableGraphInspector(whyAreYouKeepingConsumer)
+ .enableInliningAnnotations()
+ .addProgramClasses(CLASS)
+ .addKeepMainRule(KeptByKeepClassMembersTest.class)
+ .addKeepRules(keepClassMembersRuleContent)
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), CLASS)
+ .assertSuccessWithOutput(EXPECTED)
+ .graphInspector();
+
+ // The only root should be the keep annotation rule.
+ assertEquals(1, inspector.getRoots().size());
+ QueryNode keepMainRule = inspector.rule(Origin.unknown(), 1, 1).assertRoot();
+ QueryNode keepClassMembersRule = inspector.rule(keepClassMembersRuleContent).assertNotRoot();
+
+ // Check that the call chain goes from root -> main(unchanged) -> bar(renamed).
+ inspector.method(barMethod).assertRenamed().assertInvokedFrom(mainMethod);
+ inspector.method(mainMethod).assertNotRenamed().assertKeptBy(keepMainRule);
+
+ // Check baz is kept by the dependent rule.
+ inspector.method(bazMethod).assertNotRenamed().assertKeptBy(keepClassMembersRule);
+
+ // Regression for b/131822600, the dependent rule must be satisfied by Bar being kept.
+ keepClassMembersRule.assertSatisfiedBy(inspector.clazz(clazz));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ whyAreYouKeepingConsumer.printWhyAreYouKeeping(bazMethod, new PrintStream(baos));
+ assertEquals(EXPECTED_WHYAREYOUKEEPING, baos.toString());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
index 1d53013..cd77042 100644
--- a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.utils.graphinspector;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -75,6 +76,8 @@
abstract boolean isKeptBy(QueryNode node);
+ abstract boolean isSatisfiedBy(QueryNode node);
+
abstract String getNodeDescription();
protected String errorMessage(String expected, String actual) {
@@ -101,6 +104,11 @@
return this;
}
+ public QueryNode assertNotRoot() {
+ assertFalse(errorMessage("non-root", "root"), isRoot());
+ return this;
+ }
+
public QueryNode assertRenamed() {
assertTrue(errorMessage("renamed", "not-renamed"), isRenamed());
return this;
@@ -153,6 +161,15 @@
isKeptBy(node));
return this;
}
+
+ public QueryNode assertSatisfiedBy(QueryNode node) {
+ assertTrue(
+ "Invalid call to assertSatisfiedBy with: " + node.getNodeDescription(), node.isPresent());
+ assertTrue(
+ errorMessage("satisfied by " + node.getNodeDescription(), "was not satisfied by it"),
+ isSatisfiedBy(node));
+ return this;
+ }
}
private static class AbsentQueryNode extends QueryNode {
@@ -201,6 +218,12 @@
fail("Invalid call to isKeptBy on " + getNodeDescription());
throw new Unreachable();
}
+
+ @Override
+ public boolean isSatisfiedBy(QueryNode node) {
+ fail("Invalid call to isTriggeredBy on " + getNodeDescription());
+ throw new Unreachable();
+ }
}
// Class representing a point in the kept-graph structure.
@@ -281,9 +304,22 @@
return filterSources((source, infos) -> impl.graphNode == source).findFirst().isPresent();
}
+ @Override
+ public boolean isSatisfiedBy(QueryNode node) {
+ assertTrue(
+ "Invalid call to isTriggeredBy on non-keep rule node: " + graphNode,
+ graphNode instanceof KeepRuleGraphNode);
+ if (!(node instanceof QueryNodeImpl)) {
+ return false;
+ }
+ QueryNodeImpl impl = (QueryNodeImpl) node;
+ return filterSources((source, infos) -> impl.graphNode == source).findFirst().isPresent();
+ }
+
private Stream<GraphNode> filterSources(BiPredicate<GraphNode, Set<GraphEdgeInfo>> test) {
Map<GraphNode, Set<GraphEdgeInfo>> sources =
inspector.consumer.getSourcesTargeting(graphNode);
+ assertNotNull("Attempt to iterate sources of apparent root node: " + graphNode, sources);
return sources.entrySet().stream()
.filter(e -> test.test(e.getKey(), e.getValue()))
.map(Entry::getKey);