Merge "Add YouTube version 13.37"
diff --git a/src/main/java/com/android/tools/r8/ExtractMarker.java b/src/main/java/com/android/tools/r8/ExtractMarker.java
index 100105f..6fd04b6 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarker.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarker.java
@@ -121,7 +121,7 @@
"Failed to read dex/vdex file `" + programFile + "`: '" + e.getMessage() + "'");
continue;
}
- System.out.print("In file: " + cwd.relativize(programFile));
+ System.out.print("In file: " + cwd.toAbsolutePath().relativize(programFile.toAbsolutePath()));
System.out.println(", " + extractDexSize(programFile) + " bytes:");
for (Marker marker : markers) {
if (marker == null) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index dc94041..78a9d3f 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -362,9 +362,8 @@
mainDexKeepRules = parser.getConfig().getRules();
}
- ProguardConfigurationParser parser = new ProguardConfigurationParser(
- factory, reporter,
- !allowPartiallyImplementedProguardOptions, allowTestProguardOptions);
+ ProguardConfigurationParser parser =
+ new ProguardConfigurationParser(factory, reporter, allowTestProguardOptions);
if (!proguardConfigs.isEmpty()) {
parser.parse(proguardConfigs);
}
diff --git a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
index 6ccacf8..08af875 100644
--- a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
@@ -57,9 +57,9 @@
join = TypeLatticeElement.join(appInfo, join, getLatticeElement(iterator.next()));
}
// All types are reference types so the join is either a class or an array.
- if (join.isClassTypeLatticeElement()) {
+ if (join.isClassType()) {
return join.asClassTypeLatticeElement().getClassType();
- } else if (join.isArrayTypeLatticeElement()) {
+ } else if (join.isArrayType()) {
return join.asArrayTypeLatticeElement().getArrayType();
}
throw new CompilationError("Unexpected join " + join + " of types: " +
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 1ed60c0..150e516 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -138,7 +138,12 @@
DexClass clazz = definitionFor(type);
if (!clazz.isInterface()) {
ResolutionResult methods = resolveMethodOnClass(type, method);
- methods.forEachTarget(result::add);
+ methods.forEachTarget(
+ target -> {
+ if (target.isVirtualMethod()) {
+ result.add(target);
+ }
+ });
}
}
return result;
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 b86c4bd..bbc5922 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -350,6 +350,16 @@
return null;
}
+ @Override
+ public boolean isStatic() {
+ return accessFlags.isStatic();
+ }
+
+ @Override
+ public boolean isStaticMember() {
+ return false;
+ }
+
public DexEncodedMethod getClassInitializer() {
return Arrays.stream(directMethods()).filter(DexEncodedMethod::isClassInitializer).findAny()
.orElse(null);
diff --git a/src/main/java/com/android/tools/r8/graph/DexDefinition.java b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
index c2ccfe3..9127b94 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDefinition.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
@@ -74,4 +74,8 @@
public static Stream<DexEncodedMethod> filterDexEncodedMethod(Stream<DexDefinition> stream) {
return filter(stream, DexDefinition::isDexEncodedMethod, DexDefinition::asDexEncodedMethod);
}
+
+ public abstract boolean isStatic();
+
+ public abstract boolean isStaticMember();
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 115e88a..42cc0ce 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -78,6 +78,16 @@
return this;
}
+ @Override
+ public boolean isStatic() {
+ return accessFlags.isStatic();
+ }
+
+ @Override
+ public boolean isStaticMember() {
+ return isStatic();
+ }
+
public boolean hasAnnotation() {
return !annotations.isEmpty();
}
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 5af5739..a4986a5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -216,11 +216,24 @@
/**
* Returns true if this method can be invoked via invoke-static.
*/
+ // TODO(jsjeon): deprecate this, and use isStatic() instead.
public boolean isStaticMethod() {
checkIfObsolete();
return accessFlags.isStatic();
}
+ @Override
+ public boolean isStatic() {
+ checkIfObsolete();
+ return accessFlags.isStatic();
+ }
+
+ @Override
+ public boolean isStaticMember() {
+ checkIfObsolete();
+ return isStatic();
+ }
+
/**
* Returns true if this method is synthetic.
*/
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index f974aca..0a1ee94 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.graph;
+import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.InvalidDebugInfoException;
import com.android.tools.r8.graph.JarClassFileReader.ReparseContext;
import com.android.tools.r8.ir.code.IRCode;
@@ -241,7 +242,12 @@
private void parseCode(ReparseContext context, boolean useJsrInliner) {
SecondVisitor classVisitor = new SecondVisitor(createCodeLocator(context), useJsrInliner);
- new ClassReader(context.classCache).accept(classVisitor, ClassReader.SKIP_FRAMES);
+ try {
+ new ClassReader(context.classCache).accept(classVisitor, ClassReader.SKIP_FRAMES);
+ } catch (Exception exception) {
+ throw new CompilationError(
+ "Unable to parse method `" + method.toSourceString() + "`", exception);
+ }
}
protected BiFunction<String, String, JarCode> createCodeLocator(ReparseContext context) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java
index 1568531..c8f2b8c 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ArrayTypeLatticeElement.java
@@ -7,42 +7,41 @@
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexType;
-public class ArrayTypeLatticeElement extends TypeLatticeElement {
- private final DexType arrayType;
+public class ArrayTypeLatticeElement extends ReferenceTypeLatticeElement {
- ArrayTypeLatticeElement(DexType arrayType, boolean isNullable) {
- super(isNullable);
- this.arrayType = arrayType;
+ ArrayTypeLatticeElement(DexType type, boolean isNullable) {
+ super(type, isNullable);
+ assert type.isArrayType();
}
public DexType getArrayType() {
- return arrayType;
+ return type;
}
public int getNesting() {
- return arrayType.getNumberOfLeadingSquareBrackets();
+ return type.getNumberOfLeadingSquareBrackets();
}
public DexType getArrayElementType(DexItemFactory factory) {
- return arrayType.toArrayElementType(factory);
+ return type.toArrayElementType(factory);
}
public DexType getArrayBaseType(DexItemFactory factory) {
- return arrayType.toBaseType(factory);
+ return type.toBaseType(factory);
}
@Override
TypeLatticeElement asNullable() {
- return isNullable() ? this : new ArrayTypeLatticeElement(arrayType, true);
+ return isNullable() ? this : new ArrayTypeLatticeElement(type, true);
}
@Override
public TypeLatticeElement asNonNullable() {
- return isNullable() ? new ArrayTypeLatticeElement(arrayType, false) : this;
+ return isNullable() ? new ArrayTypeLatticeElement(type, false) : this;
}
@Override
- public boolean isArrayTypeLatticeElement() {
+ public boolean isArrayType() {
return true;
}
@@ -58,20 +57,7 @@
@Override
public String toString() {
- return isNullableString() + arrayType.toString();
+ return isNullableString() + type.toString();
}
- @Override
- public boolean equals(Object o) {
- if (!super.equals(o)) {
- return false;
- }
- ArrayTypeLatticeElement other = (ArrayTypeLatticeElement) o;
- return arrayType.equals(other.arrayType);
- }
-
- @Override
- public int hashCode() {
- return super.hashCode() * arrayType.hashCode() * 41;
- }
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
index 9144f52..3efc915 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeLatticeElement.java
@@ -5,28 +5,22 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexType;
-import com.google.common.collect.ImmutableSet;
-import java.util.Collections;
import java.util.Set;
-import java.util.stream.Collectors;
-public class ClassTypeLatticeElement extends TypeLatticeElement {
- private final DexType classType;
- private final Set<DexType> interfaces;
+public class ClassTypeLatticeElement extends ReferenceTypeLatticeElement {
ClassTypeLatticeElement(DexType classType, boolean isNullable) {
- this(classType, isNullable, ImmutableSet.of());
+ super(classType, isNullable);
+ assert classType.isClassType();
}
ClassTypeLatticeElement(DexType classType, boolean isNullable, Set<DexType> interfaces) {
- super(isNullable);
+ super(classType, isNullable, interfaces);
assert classType.isClassType();
- this.classType = classType;
- this.interfaces = Collections.unmodifiableSet(interfaces);
}
public DexType getClassType() {
- return classType;
+ return type;
}
Set<DexType> getInterfaces() {
@@ -35,16 +29,16 @@
@Override
TypeLatticeElement asNullable() {
- return isNullable() ? this : new ClassTypeLatticeElement(classType, true, interfaces);
+ return isNullable() ? this : new ClassTypeLatticeElement(type, true, interfaces);
}
@Override
public TypeLatticeElement asNonNullable() {
- return isNullable() ? new ClassTypeLatticeElement(classType, false, interfaces) : this;
+ return isNullable() ? new ClassTypeLatticeElement(type, false, interfaces) : this;
}
@Override
- public boolean isClassTypeLatticeElement() {
+ public boolean isClassType() {
return true;
}
@@ -58,37 +52,4 @@
return objectType(appInfo, true);
}
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append(isNullableString()).append(classType.toString());
- if (!interfaces.isEmpty()) {
- builder.append(" [");
- builder.append(
- interfaces.stream().map(DexType::toString).collect(Collectors.joining(", ")));
- builder.append("]");
- }
- return builder.toString();
- }
-
- @Override
- public boolean equals(Object o) {
- if (!super.equals(o)) {
- return false;
- }
- ClassTypeLatticeElement other = (ClassTypeLatticeElement) o;
- if (!classType.equals(other.classType)) {
- return false;
- }
- if (interfaces.size() != other.interfaces.size()) {
- return false;
- }
- return interfaces.containsAll(other.interfaces);
- }
-
- @Override
- public int hashCode() {
- int prime = (!classType.isUnknown() && classType.isInterface()) ? 3 : 17;
- return super.hashCode() * classType.hashCode() * prime;
- }
}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java
deleted file mode 100644
index efb340f..0000000
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/NullLatticeElement.java
+++ /dev/null
@@ -1,43 +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.ir.analysis.type;
-
-import com.android.tools.r8.graph.AppInfo;
-
-public class NullLatticeElement extends TypeLatticeElement {
- private static final NullLatticeElement INSTANCE = new NullLatticeElement();
-
- private NullLatticeElement() {
- super(true);
- }
-
- @Override
- public boolean mustBeNull() {
- return true;
- }
-
- @Override
- TypeLatticeElement asNullable() {
- return this;
- }
-
- @Override
- public TypeLatticeElement asNonNullable() {
- return BottomTypeLatticeElement.getInstance();
- }
-
- public static NullLatticeElement getInstance() {
- return INSTANCE;
- }
-
- @Override
- public TypeLatticeElement arrayGet(AppInfo appInfo) {
- return this;
- }
-
- @Override
- public String toString() {
- return "NULL";
- }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java
new file mode 100644
index 0000000..f700b15
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java
@@ -0,0 +1,90 @@
+// 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.analysis.type;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ReferenceTypeLatticeElement extends TypeLatticeElement {
+ private static final ReferenceTypeLatticeElement NULL =
+ new ReferenceTypeLatticeElement(DexItemFactory.nullValueType, true);
+
+ final DexType type;
+ final Set<DexType> interfaces;
+
+ ReferenceTypeLatticeElement(DexType type, boolean isNullable) {
+ this(type, isNullable, ImmutableSet.of());
+ }
+
+ ReferenceTypeLatticeElement(DexType type, boolean isNullable, Set<DexType> interfaces) {
+ super(isNullable);
+ this.type = type;
+ this.interfaces = Collections.unmodifiableSet(interfaces);
+ }
+
+ public static ReferenceTypeLatticeElement getNullTypeLatticeElement() {
+ return NULL;
+ }
+
+ @Override
+ public boolean isNull() {
+ return type == DexItemFactory.nullValueType;
+ }
+
+ @Override
+ TypeLatticeElement asNullable() {
+ assert isNull();
+ return this;
+ }
+
+ @Override
+ public boolean isReference() {
+ return true;
+ }
+
+ @Override
+ public TypeLatticeElement arrayGet(AppInfo appInfo) {
+ assert isNull();
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(isNullableString()).append(type.toString());
+ if (!interfaces.isEmpty()) {
+ builder.append(" [");
+ builder.append(
+ interfaces.stream().map(DexType::toString).collect(Collectors.joining(", ")));
+ builder.append("]");
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) {
+ return false;
+ }
+ ReferenceTypeLatticeElement other = (ReferenceTypeLatticeElement) o;
+ if (!type.equals(other.type)) {
+ return false;
+ }
+ if (interfaces.size() != other.interfaces.size()) {
+ return false;
+ }
+ return interfaces.containsAll(other.interfaces);
+ }
+
+ @Override
+ public int hashCode() {
+ int prime = type.isUnknown() ? 3 : (type.isArrayType() ? 7 : 5);
+ return super.hashCode() * type.hashCode() * prime;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
index 90a864c..ab77424 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeAnalysis.java
@@ -150,7 +150,7 @@
AppInfoWithSubtyping appInfo, InvokeMethodWithReceiver invoke) {
DexType receiverType = invoke.getInvokedMethod().getHolder();
TypeLatticeElement lattice = invoke.getReceiver().getTypeLattice();
- if (lattice.isClassTypeLatticeElement()) {
+ if (lattice.isClassType()) {
DexType refinedType = lattice.asClassTypeLatticeElement().getClassType();
if (refinedType.isSubtypeOf(receiverType, appInfo)) {
return refinedType;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
index f9d327f..5fffc8d 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
@@ -33,7 +33,7 @@
return isNullable;
}
- public boolean mustBeNull() {
+ public boolean isNull() {
return false;
}
@@ -50,7 +50,7 @@
* @return {@link TypeLatticeElement} a similar lattice element with nullable flag flipped.
*/
public TypeLatticeElement asNonNullable() {
- throw new Unreachable("Flipping nullable is not allowed in general.");
+ return BottomTypeLatticeElement.getInstance();
}
String isNullableString() {
@@ -76,10 +76,10 @@
if (l1.isTop() || l2.isTop()) {
return TopTypeLatticeElement.getInstance();
}
- if (l1.mustBeNull()) {
+ if (l1.isNull()) {
return l2.asNullable();
}
- if (l2.mustBeNull()) {
+ if (l2.isNull()) {
return l1.asNullable();
}
if (l1.isPrimitive()) {
@@ -98,8 +98,8 @@
return objectType(appInfo, isNullable);
}
// From now on, l1.getClass() == l2.getClass()
- if (l1.isArrayTypeLatticeElement()) {
- assert l2.isArrayTypeLatticeElement();
+ if (l1.isArrayType()) {
+ assert l2.isArrayType();
ArrayTypeLatticeElement a1 = l1.asArrayTypeLatticeElement();
ArrayTypeLatticeElement a2 = l2.asArrayTypeLatticeElement();
// Identical types are the same elements
@@ -136,8 +136,8 @@
DexType arrayTypeLub = appInfo.dexItemFactory.createArrayType(a1Nesting, lub);
return new ArrayTypeLatticeElement(arrayTypeLub, isNullable);
}
- if (l1.isClassTypeLatticeElement()) {
- assert l2.isClassTypeLatticeElement();
+ if (l1.isClassType()) {
+ assert l2.isClassType();
ClassTypeLatticeElement c1 = l1.asClassTypeLatticeElement();
ClassTypeLatticeElement c2 = l2.asClassTypeLatticeElement();
DexType lubType =
@@ -289,7 +289,11 @@
return false;
}
- public boolean isArrayTypeLatticeElement() {
+ public boolean isReference() {
+ return false;
+ }
+
+ public boolean isArrayType() {
return false;
}
@@ -297,7 +301,7 @@
return null;
}
- public boolean isClassTypeLatticeElement() {
+ public boolean isClassType() {
return false;
}
@@ -338,8 +342,8 @@
}
public boolean isPreciseType() {
- return isArrayTypeLatticeElement()
- || isClassTypeLatticeElement()
+ return isArrayType()
+ || isClassType()
|| isInt()
|| isFloat()
|| isLong()
@@ -358,7 +362,7 @@
public static TypeLatticeElement fromDexType(AppInfo appInfo, DexType type, boolean isNullable) {
if (type == DexItemFactory.nullValueType) {
- return NullLatticeElement.getInstance();
+ return ReferenceTypeLatticeElement.getNullTypeLatticeElement();
}
if (type.isPrimitiveType()) {
return PrimitiveTypeLatticeElement.fromDexType(type);
@@ -386,7 +390,7 @@
public TypeLatticeElement checkCast(AppInfo appInfo, DexType castType) {
TypeLatticeElement castTypeLattice = fromDexType(appInfo, castType, isNullable());
// Special case: casting null.
- if (mustBeNull()) {
+ if (isNull()) {
return castTypeLattice;
}
if (lessThanOrEqual(appInfo, this, castTypeLattice)) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 21a3293..6f5fa47 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -26,7 +26,7 @@
import com.android.tools.r8.ir.analysis.type.FloatTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.IntTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.LongTypeLatticeElement;
-import com.android.tools.r8.ir.analysis.type.NullLatticeElement;
+import com.android.tools.r8.ir.analysis.type.ReferenceTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.SingleTypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
import com.android.tools.r8.ir.analysis.type.WideTypeLatticeElement;
@@ -276,13 +276,10 @@
@Override
public TypeLatticeElement evaluate(AppInfo appInfo) {
- if (isZero() && outType().isObject()) {
- return NullLatticeElement.getInstance();
- }
// TODO(b/72693244): IR builder should know the type and assign a proper type lattice.
switch (outType()) {
case OBJECT:
- return NullLatticeElement.getInstance();
+ return ReferenceTypeLatticeElement.getNullTypeLatticeElement();
case INT:
return IntTypeLatticeElement.getInstance();
case FLOAT:
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 19bf655..96cc33a 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
@@ -688,27 +688,17 @@
}
public ConstNumber createIntConstant(int value) {
- return new ConstNumber(createValue(ValueType.INT), value);
- }
-
- public ConstNumber createTrue() {
- return new ConstNumber(createValue(ValueType.INT), 1);
- }
-
- public ConstNumber createFalse() {
- return new ConstNumber(createValue(ValueType.INT), 0);
+ Value out = createValue(ValueType.INT);
+ return new ConstNumber(out, value);
}
public final int getHighestBlockNumber() {
return blocks.stream().max(Comparator.comparingInt(BasicBlock::getNumber)).get().getNumber();
}
- public Instruction createConstNull(Instruction from) {
- return new ConstNumber(createValue(from.outType()), 0);
- }
-
public ConstNumber createConstNull() {
- return new ConstNumber(createValue(ValueType.OBJECT), 0);
+ Value out = createValue(ValueType.OBJECT);
+ return new ConstNumber(out, 0);
}
public boolean doAllThrowingInstructionsHavePositions() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NonNull.java b/src/main/java/com/android/tools/r8/ir/code/NonNull.java
index 539d943..b2942d9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NonNull.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NonNull.java
@@ -91,19 +91,7 @@
@Override
public TypeLatticeElement evaluate(AppInfo appInfo) {
- TypeLatticeElement l = src().getTypeLatticeRaw();
- // Flipping the nullability bit for reference type is the main use case.
- if (l.isClassTypeLatticeElement() || l.isArrayTypeLatticeElement()) {
- return l.asNonNullable();
- }
- // non_null_rcv <- non-null NULL ?!
- // The chances are that the in is phi, and the only available operand is null. If another
- // operand is, say, class A, phi's type is nullable A, and the out value of this instruction
- // would be non-null A. Until that phi is saturated, we will ignore the current null.
- if (l.mustBeNull()) {
- return l.asNonNullable();
- }
- return l;
+ return src().getTypeLatticeRaw().asNonNullable();
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index cad7504..194558e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -1334,7 +1334,7 @@
if (current.isInvokeMethod()) {
InvokeMethod invoke = current.asInvokeMethod();
if (invoke.getInvokedMethod() == dexItemFactory.classMethods.desiredAssertionStatus) {
- iterator.replaceCurrentInstruction(code.createFalse());
+ iterator.replaceCurrentInstruction(code.createIntConstant(0));
}
} else if (current.isStaticPut()) {
StaticPut staticPut = current.asStaticPut();
@@ -1344,7 +1344,7 @@
} else if (current.isStaticGet()) {
StaticGet staticGet = current.asStaticGet();
if (staticGet.getField().name == dexItemFactory.assertionsDisabled) {
- iterator.replaceCurrentInstruction(code.createTrue());
+ iterator.replaceCurrentInstruction(code.createIntConstant(1));
}
}
}
@@ -1644,7 +1644,7 @@
TypeLatticeElement.fromDexType(appInfo, castType, inTypeLattice.isNullable());
// Special case: null cast, e.g., getMethod(..., (Class[]) null);
// This cast should be kept no matter what.
- if (inTypeLattice.mustBeNull()) {
+ if (inTypeLattice.isNull()) {
assert outTypeLattice.equals(castTypeLattice);
continue;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 3bb6efd..9baa647 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.ir.optimize.classinliner;
import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo.ResolutionResult;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
@@ -248,7 +249,8 @@
// Eligible virtual method call on the instance as a receiver.
if (user.isInvokeVirtual() || user.isInvokeInterface()) {
- InliningInfo inliningInfo = isEligibleDirectMethodCall(user.asInvokeMethodWithReceiver());
+ InliningInfo inliningInfo =
+ isEligibleDirectVirtualMethodCall(user.asInvokeMethodWithReceiver());
if (inliningInfo != null) {
methodCallsOnInstance.put(user.asInvokeMethodWithReceiver(), inliningInfo);
continue;
@@ -504,24 +506,39 @@
? new InliningInfo(definition, eligibleClass) : null;
}
- private InliningInfo isEligibleDirectMethodCall(InvokeMethodWithReceiver invoke) {
+ private InliningInfo isEligibleDirectVirtualMethodCall(InvokeMethodWithReceiver invoke) {
if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
return null; // Instance passed as an argument.
}
- return isEligibleMethodCall(!invoke.getBlock().hasCatchHandlers(), invoke.getInvokedMethod(),
- eligibility -> !eligibility.returnsReceiver ||
- invoke.outValue() == null || invoke.outValue().numberOfAllUsers() == 0);
+ return isEligibleVirtualMethodCall(
+ !invoke.getBlock().hasCatchHandlers(),
+ invoke.getInvokedMethod(),
+ eligibility ->
+ !eligibility.returnsReceiver
+ || invoke.outValue() == null
+ || invoke.outValue().numberOfAllUsers() == 0);
}
- private InliningInfo isEligibleIndirectMethodCall(DexMethod callee) {
- return isEligibleMethodCall(false, callee, eligibility -> !eligibility.returnsReceiver);
+ private InliningInfo isEligibleIndirectVirtualMethodCall(DexMethod callee) {
+ return isEligibleVirtualMethodCall(false, callee, eligibility -> !eligibility.returnsReceiver);
}
- private InliningInfo isEligibleMethodCall(boolean allowMethodsWithoutNormalReturns,
- DexMethod callee, Predicate<ClassInlinerEligibility> eligibilityAcceptanceCheck) {
+ private InliningInfo isEligibleVirtualMethodCall(
+ boolean allowMethodsWithoutNormalReturns,
+ DexMethod callee,
+ Predicate<ClassInlinerEligibility> eligibilityAcceptanceCheck) {
+ // We should not inline a method if the invocation has type interface or virtual and the
+ // signature of the invocation resolves to a private or static method.
+ ResolutionResult resolutionResult = appInfo.resolveMethod(callee.holder, callee);
+ if (resolutionResult.hasSingleTarget()
+ && !resolutionResult.asSingleTarget().isVirtualMethod()) {
+ return null;
+ }
DexEncodedMethod singleTarget = findSingleTarget(callee, false);
- if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
+ if (singleTarget == null
+ || !singleTarget.isVirtualMethod()
+ || isProcessedConcurrently.test(singleTarget)) {
return null;
}
if (method == singleTarget) {
@@ -662,7 +679,7 @@
}
// Is the method called indirectly still eligible?
- InliningInfo potentialInliningInfo = isEligibleIndirectMethodCall(call.getSecond());
+ InliningInfo potentialInliningInfo = isEligibleIndirectVirtualMethodCall(call.getSecond());
if (potentialInliningInfo == null) {
return false;
}
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 41551e7..7a0aacb 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -272,6 +272,24 @@
}
}
+ private void enqueueHolderIfDependentNonStaticMember(
+ DexClass holder, Map<DexDefinition, ProguardKeepRule> dependentItems) {
+ // Check if any dependent members are not static, and in that case enqueue the class as well.
+ // Having a dependent rule like -keepclassmembers with non static items indicates that class
+ // instances will be present even if tracing do not find any instantiation. See b/115867670.
+ for (Entry<DexDefinition, ProguardKeepRule> entry : dependentItems.entrySet()) {
+ DexDefinition dependentItem = entry.getKey();
+ if (dependentItem.isDexClass()) {
+ continue;
+ }
+ if (!dependentItem.isStaticMember()) {
+ enqueueRootItem(holder, entry.getValue());
+ // Enough to enqueue the known holder once.
+ break;
+ }
+ }
+ }
+
//
// Things to do with registering events. This is essentially the interface for byte-code
// traversals.
@@ -633,16 +651,6 @@
// Actual actions performed.
//
- static private boolean isStaticMember(DexDefinition definition) {
- if (definition.isDexEncodedMethod()) {
- return (definition.asDexEncodedMethod()).accessFlags.isStatic();
- }
- if (definition.isDexEncodedField()) {
- return (definition.asDexEncodedField()).accessFlags.isStatic();
- }
- return false;
- }
-
private void markTypeAsLive(DexType type) {
assert type.isClassType();
if (liveTypes.add(type)) {
@@ -686,16 +694,8 @@
annotations.forEach(this::handleAnnotationOfLiveType);
}
- // Check if any dependent members are not static, and in that case enqueue the class as well.
- // Having a dependent rule like -keepclassmembers with non static items indicates that class
- // instances will be present even if tracing do not find any instantiation. See b/115867670.
Map<DexDefinition, ProguardKeepRule> dependentItems = rootSet.getDependentItems(holder);
- for (Entry<DexDefinition, ProguardKeepRule> entry : dependentItems.entrySet()) {
- if (!isStaticMember(entry.getKey())) {
- enqueueRootItem(holder, entry.getValue());
- break;
- }
- }
+ enqueueHolderIfDependentNonStaticMember(holder, dependentItems);
// Add all dependent members to the workqueue.
enqueueRootItems(dependentItems);
}
@@ -1136,10 +1136,10 @@
while (!worklist.isEmpty()) {
DexType current = worklist.pollFirst();
DexClass currentHolder = appInfo.definitionFor(current);
- // If this class shadows the virtual, abort the search. Note, according to JVM spec,
- // shadowing is independent of whether a method is public or private.
+ // If this class overrides the virtual, abort the search. Note that, according to
+ // the JVM spec, private methods cannot override a virtual method.
if (currentHolder == null
- || currentHolder.lookupMethod(encodedMethod.method) != null) {
+ || currentHolder.lookupVirtualMethod(encodedMethod.method) != null) {
continue;
}
if (instantiatedTypes.contains(current)) {
@@ -1298,6 +1298,16 @@
enqueueRootItems(consequentRootSet.noShrinking);
rootSet.noOptimization.addAll(consequentRootSet.noOptimization);
rootSet.noObfuscation.addAll(consequentRootSet.noObfuscation);
+ rootSet.addDependentItems(consequentRootSet.dependentNoShrinking);
+ // Check if any newly dependent members are not static, and in that case find the holder
+ // and enqueue it as well. This is -if version of workaround for b/115867670.
+ consequentRootSet.dependentNoShrinking.forEach((precondition, dependentItems) -> {
+ if (precondition.isDexClass()) {
+ enqueueHolderIfDependentNonStaticMember(precondition.asDexClass(), dependentItems);
+ }
+ // Add all dependent members to the workqueue.
+ enqueueRootItems(dependentItems);
+ });
if (!workList.isEmpty()) {
continue;
}
@@ -2191,8 +2201,8 @@
}
for (DexType subtype : type.allExtendsSubtypes()) {
DexClass clazz = definitionFor(subtype);
- DexEncodedMethod target = clazz.lookupMethod(method);
- if (target != null) {
+ DexEncodedMethod target = clazz.lookupVirtualMethod(method);
+ if (target != null && !target.isPrivateMethod()) {
// We found a method on this class. If this class is not abstract it is a runtime
// reachable override and hence a conflict.
if (!clazz.accessFlags.isAbstract()) {
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 a49c15c..faf5317 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -49,7 +49,6 @@
private final DexItemFactory dexItemFactory;
private final Reporter reporter;
- private final boolean failOnPartiallyImplementedOptions;
private final boolean allowTestOptions;
private static final List<String> IGNORED_SINGLE_ARG_OPTIONS = ImmutableList.of(
@@ -102,17 +101,15 @@
public ProguardConfigurationParser(
DexItemFactory dexItemFactory, Reporter reporter) {
- this(dexItemFactory, reporter, true, false);
+ this(dexItemFactory, reporter, false);
}
public ProguardConfigurationParser(
- DexItemFactory dexItemFactory, Reporter reporter, boolean failOnPartiallyImplementedOptions,
- boolean allowTestOptions) {
+ DexItemFactory dexItemFactory, Reporter reporter, boolean allowTestOptions) {
this.dexItemFactory = dexItemFactory;
configurationBuilder = ProguardConfiguration.builder(dexItemFactory, reporter);
this.reporter = reporter;
- this.failOnPartiallyImplementedOptions = failOnPartiallyImplementedOptions;
this.allowTestOptions = allowTestOptions;
}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
index 54a3f2f..6d6d943 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardIfRule.java
@@ -72,7 +72,7 @@
Origin.unknown(),
Position.UNKNOWN,
null,
- getClassAnnotation(),
+ getClassAnnotation() == null ? null : getClassAnnotation().materialize(),
getClassAccessFlags(),
getNegatedClassAccessFlags(),
getClassTypeNegated(),
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRule.java
index 3c1fb2d..dd2bd33 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepRule.java
@@ -63,7 +63,7 @@
Origin.unknown(),
Position.UNKNOWN,
null,
- getClassAnnotation(),
+ getClassAnnotation() == null ? null : getClassAnnotation().materialize(),
getClassAccessFlags(),
getNegatedClassAccessFlags(),
getClassTypeNegated(),
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 43227d7..30def9e 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.utils.StringDiagnostic;
import com.android.tools.r8.utils.ThreadUtils;
import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import java.io.PrintStream;
import java.util.ArrayList;
@@ -34,10 +35,12 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -148,16 +151,13 @@
DexClass clazz,
ProguardConfigurationRule rule,
ProguardIfRule ifRule) {
- if (rule.getClassType().matches(clazz) == rule.getClassTypeNegated()) {
+ if (!satisfyClassType(rule, clazz)) {
return;
}
- if (!rule.getClassAccessFlags().containsAll(clazz.accessFlags)) {
+ if (!satisfyAccessFlag(rule, clazz)) {
return;
}
- if (!rule.getNegatedClassAccessFlags().containsNone(clazz.accessFlags)) {
- return;
- }
- if (!containsAnnotation(rule.getClassAnnotation(), clazz.annotations)) {
+ if (!satisfyAnnotation(rule, clazz)) {
return;
}
// In principle it should make a difference whether the user specified in a class
@@ -202,13 +202,14 @@
if (rule.getClassNames().matches(clazz.type)) {
Collection<ProguardMemberRule> memberKeepRules = rule.getMemberRules();
+ Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier;
if (rule instanceof ProguardKeepRule) {
switch (((ProguardKeepRule) rule).getType()) {
case KEEP_CLASS_MEMBERS: {
- // If we're handling -if consequent part, that means precondition already met.
- DexClass precondition = ifRule != null ? null : clazz;
- markMatchingVisibleMethods(clazz, memberKeepRules, rule, precondition);
- markMatchingFields(clazz, memberKeepRules, rule, precondition);
+ // Members mentioned at -keepclassmembers always depend on their holder.
+ preconditionSupplier = ImmutableMap.of((definition -> true), clazz);
+ markMatchingVisibleMethods(clazz, memberKeepRules, rule, preconditionSupplier);
+ markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
break;
}
case KEEP_CLASSES_WITH_MEMBERS: {
@@ -219,20 +220,33 @@
}
case KEEP: {
markClass(clazz, rule);
- markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
- markMatchingFields(clazz, memberKeepRules, rule, null);
+ preconditionSupplier = new HashMap<>();
+ if (ifRule != null) {
+ // Static members in -keep are pinned no matter what.
+ preconditionSupplier.put(DexDefinition::isStaticMember, null);
+ // Instance members may need to be kept even though the holder is not instantiated.
+ preconditionSupplier.put(definition -> !definition.isStaticMember(), clazz);
+ } else {
+ // Members mentioned at -keep should always be pinned as long as that -keep rule is
+ // not triggered conditionally.
+ preconditionSupplier.put((definition -> true), null);
+ }
+ markMatchingVisibleMethods(clazz, memberKeepRules, rule, preconditionSupplier);
+ markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
break;
}
case CONDITIONAL:
- assert rule instanceof ProguardIfRule;
throw new Unreachable("-if rule will be evaluated separately, not here.");
}
+ } else if (rule instanceof ProguardIfRule) {
+ throw new Unreachable("-if rule will be evaluated separately, not here.");
} else if (rule instanceof ProguardCheckDiscardRule) {
if (memberKeepRules.isEmpty()) {
markClass(clazz, rule);
} else {
- markMatchingFields(clazz, memberKeepRules, rule, clazz);
- markMatchingMethods(clazz, memberKeepRules, rule, clazz);
+ preconditionSupplier = ImmutableMap.of((definition -> true), clazz);
+ markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
+ markMatchingMethods(clazz, memberKeepRules, rule, preconditionSupplier);
}
} else if (rule instanceof ProguardWhyAreYouKeepingRule
|| rule instanceof ProguardKeepPackageNamesRule) {
@@ -344,6 +358,19 @@
// -keep rule may vary (due to back references). So, we need to try all pairs of -if rule
// and live types.
for (DexType currentLiveType : liveTypes) {
+ DexClass currentLiveClass = appInfo.definitionFor(currentLiveType);
+ if (currentLiveClass == null) {
+ continue;
+ }
+ if (!satisfyClassType(rule, currentLiveClass)) {
+ continue;
+ }
+ if (!satisfyAccessFlag(rule, currentLiveClass)) {
+ continue;
+ }
+ if (!satisfyAnnotation(rule, currentLiveClass)) {
+ continue;
+ }
if (ifRule.hasInheritanceClassName()) {
if (!satisfyInheritanceRule(currentLiveType, definitionForWithLiveTypes, ifRule)) {
// Try another live type since the current one doesn't satisfy the inheritance rule.
@@ -406,20 +433,44 @@
} finally {
application.timing.end();
}
- return new ConsequentRootSet(noShrinking, noOptimization, noObfuscation);
+ return new ConsequentRootSet(noShrinking, noOptimization, noObfuscation, dependentNoShrinking);
+ }
+
+ private static DexDefinition testAndGetPrecondition(
+ DexDefinition definition, Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier) {
+ if (preconditionSupplier == null) {
+ return null;
+ }
+ DexDefinition precondition = null;
+ boolean conditionEverMatched = false;
+ for (Entry<Predicate<DexDefinition>, DexDefinition> entry : preconditionSupplier.entrySet()) {
+ if (entry.getKey().test(definition)) {
+ precondition = entry.getValue();
+ conditionEverMatched = true;
+ break;
+ }
+ }
+ // If precondition-supplier is given, there should be at least one predicate that holds.
+ // Actually, there should be only one predicate as we break the loop when it is found.
+ assert conditionEverMatched;
+ return precondition;
}
private void markMatchingVisibleMethods(
DexClass clazz,
Collection<ProguardMemberRule> memberKeepRules,
ProguardConfigurationRule rule,
- DexClass onlyIfClassKept) {
+ Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier) {
Set<Wrapper<DexMethod>> methodsMarked = new HashSet<>();
- Arrays.stream(clazz.directMethods()).forEach(method ->
- markMethod(method, memberKeepRules, methodsMarked, rule, onlyIfClassKept));
+ Arrays.stream(clazz.directMethods()).forEach(method -> {
+ DexDefinition precondition = testAndGetPrecondition(method, preconditionSupplier);
+ markMethod(method, memberKeepRules, methodsMarked, rule, precondition);
+ });
while (clazz != null) {
- Arrays.stream(clazz.virtualMethods()).forEach(method ->
- markMethod(method, memberKeepRules, methodsMarked, rule, onlyIfClassKept));
+ Arrays.stream(clazz.virtualMethods()).forEach(method -> {
+ DexDefinition precondition = testAndGetPrecondition(method, preconditionSupplier);
+ markMethod(method, memberKeepRules, methodsMarked, rule, precondition);
+ });
clazz = clazz.superType == null ? null : application.definitionFor(clazz.superType);
}
}
@@ -428,19 +479,22 @@
DexClass clazz,
Collection<ProguardMemberRule> memberKeepRules,
ProguardConfigurationRule rule,
- DexClass onlyIfClassKept) {
- Arrays.stream(clazz.directMethods()).forEach(method ->
- markMethod(method, memberKeepRules, null, rule, onlyIfClassKept));
- Arrays.stream(clazz.virtualMethods()).forEach(method ->
- markMethod(method, memberKeepRules, null, rule, onlyIfClassKept));
+ Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier) {
+ clazz.forEachMethod(method -> {
+ DexDefinition precondition = testAndGetPrecondition(method, preconditionSupplier);
+ markMethod(method, memberKeepRules, null, rule, precondition);
+ });
}
private void markMatchingFields(
DexClass clazz,
Collection<ProguardMemberRule> memberKeepRules,
ProguardConfigurationRule rule,
- DexClass onlyIfClassKept) {
- clazz.forEachField(field -> markField(field, memberKeepRules, rule, onlyIfClassKept));
+ Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier) {
+ clazz.forEachField(field -> {
+ DexDefinition precondition = testAndGetPrecondition(field, preconditionSupplier);
+ markField(field, memberKeepRules, rule, precondition);
+ });
}
// TODO(67934426): Test this code.
@@ -496,6 +550,19 @@
out.close();
}
+ private static boolean satisfyClassType(ProguardConfigurationRule rule, DexClass clazz) {
+ return rule.getClassType().matches(clazz) != rule.getClassTypeNegated();
+ }
+
+ private static boolean satisfyAccessFlag(ProguardConfigurationRule rule, DexClass clazz) {
+ return rule.getClassAccessFlags().containsAll(clazz.accessFlags)
+ && rule.getNegatedClassAccessFlags().containsNone(clazz.accessFlags);
+ }
+
+ private static boolean satisfyAnnotation(ProguardConfigurationRule rule, DexClass clazz) {
+ return containsAnnotation(rule.getClassAnnotation(), clazz.annotations);
+ }
+
private boolean satisfyInheritanceRule(
DexType type,
Function<DexType, DexClass> definitionFor,
@@ -794,6 +861,15 @@
this.ifRules = Collections.unmodifiableSet(ifRules);
}
+ // Add dependent items that depend on -if rules.
+ void addDependentItems(
+ Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentItems) {
+ dependentItems.forEach((def, dependence) -> {
+ dependentNoShrinking.computeIfAbsent(def, x -> new IdentityHashMap<>())
+ .putAll(dependence);
+ });
+ }
+
Map<DexDefinition, ProguardKeepRule> getDependentItems(DexDefinition item) {
return Collections
.unmodifiableMap(dependentNoShrinking.getOrDefault(item, Collections.emptyMap()));
@@ -831,14 +907,17 @@
final Map<DexDefinition, ProguardKeepRule> noShrinking;
final Set<DexDefinition> noOptimization;
final Set<DexDefinition> noObfuscation;
+ final Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking;
private ConsequentRootSet(
Map<DexDefinition, ProguardKeepRule> noShrinking,
Set<DexDefinition> noOptimization,
- Set<DexDefinition> noObfuscation) {
+ Set<DexDefinition> noObfuscation,
+ Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking) {
this.noShrinking = Collections.unmodifiableMap(noShrinking);
this.noOptimization = Collections.unmodifiableSet(noOptimization);
this.noObfuscation = Collections.unmodifiableSet(noObfuscation);
+ this.dependentNoShrinking = Collections.unmodifiableMap(dependentNoShrinking);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 783e133..43f8d20 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -678,6 +678,17 @@
}
private boolean methodResolutionMayChange(DexClass source, DexClass target) {
+ for (DexEncodedMethod virtualSourceMethod : source.virtualMethods()) {
+ DexEncodedMethod directTargetMethod = target.lookupDirectMethod(virtualSourceMethod.method);
+ if (directTargetMethod != null) {
+ // A private method shadows a virtual method. This situation is rare, since it is not
+ // allowed by javac. Therefore, we just give up in this case. (In principle, it would be
+ // possible to rename the private method in the subclass, and then move the virtual method
+ // to the subclass without changing its name.)
+ return true;
+ }
+ }
+
// When merging an interface into a class, all instructions on the form "invoke-interface
// [source].m" are changed into "invoke-virtual [target].m". We need to abort the merge if this
// transformation could hide IncompatibleClassChangeErrors.
@@ -936,9 +947,10 @@
assert !target.isInterface();
target.superType = source.superType;
}
- target.interfaces = interfaces.isEmpty()
- ? DexTypeList.empty()
- : new DexTypeList(interfaces.toArray(new DexType[interfaces.size()]));
+ target.interfaces =
+ interfaces.isEmpty()
+ ? DexTypeList.empty()
+ : new DexTypeList(interfaces.toArray(new DexType[0]));
// Step 2: replace fields and methods.
target.setDirectMethods(mergedDirectMethods);
target.setVirtualMethods(mergedVirtualMethods);
@@ -1095,6 +1107,7 @@
}
DexEncodedMethod actual = resolutionResult.asSingleTarget();
if (actual != method) {
+ assert actual.isVirtualMethod() == method.isVirtualMethod();
return actual;
}
// We will keep the method, so the class better be abstract if there is no implementation.
@@ -1325,7 +1338,7 @@
if (filtered.size() == methods.length) {
return methods;
}
- return filtered.values().toArray(new DexEncodedMethod[filtered.size()]);
+ return filtered.values().toArray(DexEncodedMethod.EMPTY_ARRAY);
}
private DexEncodedMethod[] substituteTypesIn(DexEncodedMethod[] methods) {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
index 11bdfd3..0ea8731 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/NullabilityTest.java
@@ -68,7 +68,7 @@
if (l.isPrimitive()) {
return;
}
- assertTrue(l.isClassTypeLatticeElement());
+ assertTrue(l.isClassType());
ClassTypeLatticeElement lattice = l.asClassTypeLatticeElement();
// Receiver
if (lattice.getClassType().equals(receiverType)) {
@@ -156,13 +156,13 @@
ArrayGet.class, fromDexType(appInfo, appInfo.dexItemFactory.stringType, true),
NewInstance.class, fromDexType(appInfo, assertionErrorType, false));
forEachOutValue(irCode, (v, l) -> {
- if (l.isArrayTypeLatticeElement()) {
+ if (l.isArrayType()) {
ArrayTypeLatticeElement lattice = l.asArrayTypeLatticeElement();
assertEquals(
appInfo.dexItemFactory.stringType,
lattice.getArrayElementType(appInfo.dexItemFactory));
assertEquals(v.definition.isArgument(), l.isNullable());
- } else if (l.isClassTypeLatticeElement()) {
+ } else if (l.isClassType()) {
verifyClassTypeLattice(expectedLattices, mainClass, v, l);
}
});
@@ -182,13 +182,13 @@
ArrayGet.class, fromDexType(appInfo, appInfo.dexItemFactory.stringType, true),
NewInstance.class, fromDexType(appInfo, assertionErrorType, false));
forEachOutValue(irCode, (v, l) -> {
- if (l.isArrayTypeLatticeElement()) {
+ if (l.isArrayType()) {
ArrayTypeLatticeElement lattice = l.asArrayTypeLatticeElement();
assertEquals(
appInfo.dexItemFactory.stringType,
lattice.getArrayElementType(appInfo.dexItemFactory));
assertEquals(v.definition.isArgument(), l.isNullable());
- } else if (l.isClassTypeLatticeElement()) {
+ } else if (l.isClassType()) {
verifyClassTypeLattice(expectedLattices, mainClass, v, l);
}
});
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
index a2f8d3e..75cef4d 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeAnalysisTest.java
@@ -58,7 +58,8 @@
@RunWith(Parameterized.class)
public class TypeAnalysisTest extends SmaliTestBase {
private static final InternalOptions TEST_OPTIONS = new InternalOptions();
- private static final TypeLatticeElement NULL = NullLatticeElement.getInstance();
+ private static final TypeLatticeElement NULL =
+ ReferenceTypeLatticeElement.getNullTypeLatticeElement();
private static final TypeLatticeElement SINGLE = SingleTypeLatticeElement.getInstance();
private static final TypeLatticeElement INT = IntTypeLatticeElement.getInstance();
private static final TypeLatticeElement LONG = LongTypeLatticeElement.getInstance();
@@ -187,7 +188,7 @@
final Value finalArray = array;
forEachOutValue(irCode, (v, l) -> {
if (v == finalArray) {
- assertTrue(l.isArrayTypeLatticeElement());
+ assertTrue(l.isArrayType());
ArrayTypeLatticeElement lattice = l.asArrayTypeLatticeElement();
assertTrue(lattice.getArrayType().isPrimitiveArrayType());
assertEquals(1, lattice.getNesting());
@@ -220,7 +221,7 @@
final Value finalArray = array;
forEachOutValue(irCode, (v, l) -> {
if (v == finalArray) {
- assertTrue(l.isArrayTypeLatticeElement());
+ assertTrue(l.isArrayType());
ArrayTypeLatticeElement lattice = l.asArrayTypeLatticeElement();
assertTrue(lattice.getArrayType().isPrimitiveArrayType());
assertEquals(1, lattice.getNesting());
@@ -241,7 +242,7 @@
TypeAnalysis analysis = new TypeAnalysis(appInfo, loop2);
analysis.widening(loop2, irCode);
forEachOutValue(irCode, (v, l) -> {
- if (l.isClassTypeLatticeElement()) {
+ if (l.isClassType()) {
ClassTypeLatticeElement lattice = l.asClassTypeLatticeElement();
assertEquals("Ljava/io/PrintStream;", lattice.getClassType().toDescriptorString());
// TODO(b/70795205): Can be refined by using control-flow info.
@@ -262,7 +263,7 @@
TypeAnalysis analysis = new TypeAnalysis(appInfo, test2);
analysis.widening(test2, irCode);
forEachOutValue(irCode, (v, l) -> {
- if (l.isClassTypeLatticeElement()) {
+ if (l.isClassType()) {
ClassTypeLatticeElement lattice = l.asClassTypeLatticeElement();
assertEquals("Ljava/lang/Throwable;", lattice.getClassType().toDescriptorString());
assertFalse(l.isNullable());
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
index 15d5221..eb2c54b 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/TypeLatticeTest.java
@@ -405,7 +405,7 @@
array(2, factory.objectType),
array(1, factory.objectType)));
assertTrue(strictlyLessThan(
- NullLatticeElement.getInstance(),
+ ReferenceTypeLatticeElement.getNullTypeLatticeElement(),
array(1, factory.classType)));
}
diff --git a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
index 23c8933..fbf4e68 100644
--- a/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
+++ b/src/test/java/com/android/tools/r8/resolution/SingleTargetLookupTest.java
@@ -23,7 +23,6 @@
import com.android.tools.r8.resolution.singletarget.one.SubSubClassTwo;
import com.android.tools.r8.resolution.singletarget.three.ThirdAbstractTopClass;
import com.android.tools.r8.resolution.singletarget.three.ThirdSubClassOne;
-import com.android.tools.r8.resolution.singletarget.three.ThirdSubClassTwo;
import com.android.tools.r8.resolution.singletarget.three.ThirdSubClassTwoDump;
import com.android.tools.r8.resolution.singletarget.two.OtherAbstractSubClassOne;
import com.android.tools.r8.resolution.singletarget.two.OtherAbstractSubClassTwo;
@@ -191,10 +190,9 @@
IrrelevantInterfaceWithDefault.class),
manyTargets("abstractMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class,
ThirdSubClassOne.class),
- manyTargets("instanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class,
- ThirdSubClassTwo.class),
- manyTargets("otherInstanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class,
- ThirdSubClassTwo.class),
+ singleTarget("instanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class),
+ singleTarget(
+ "otherInstanceMethod", ThirdAbstractTopClass.class, ThirdAbstractTopClass.class)
});
}
diff --git a/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
new file mode 100644
index 0000000..2490d41
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/NonVirtualOverrideTest.java
@@ -0,0 +1,267 @@
+// 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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.ir.optimize.Inliner.Reason;
+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.DescriptorUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Path;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NonVirtualOverrideTest extends TestBase {
+
+ private static Class<?> main = NonVirtualOverrideTestClass.class;
+ private static Class<?> A = NonVirtualOverrideTestClass.A.class;
+ private static Class<?> B = NonVirtualOverrideTestClass.B.class;
+ private static Class<?> C = NonVirtualOverrideTestClass.C.class;
+
+ private final Backend backend;
+ private final boolean enableClassInlining;
+ private final boolean enableVerticalClassMerging;
+
+ @Parameterized.Parameters(name = "Backend: {0}, class inlining: {1}, vertical class merging: {2}")
+ public static Collection<Object[]> data() {
+ ImmutableList.Builder<Object[]> builder = ImmutableList.builder();
+ for (Backend backend : Backend.values()) {
+ for (boolean enableClassInlining : ImmutableList.of(true, false)) {
+ for (boolean enableVerticalClassMerging : ImmutableList.of(true, false)) {
+ builder.add(new Object[] {backend, enableClassInlining, enableVerticalClassMerging});
+ }
+ }
+ }
+ return builder.build();
+ }
+
+ public NonVirtualOverrideTest(
+ Backend backend, boolean enableClassInlining, boolean enableVerticalClassMerging) {
+ this.backend = backend;
+ this.enableClassInlining = enableClassInlining;
+ this.enableVerticalClassMerging = enableVerticalClassMerging;
+ }
+
+ @Test
+ public void test() throws Exception {
+ // Construct B such that it inherits from A and shadows method A.m() with a private method.
+ JasminBuilder jasminBuilder = new JasminBuilder();
+ ClassBuilder classBuilder = jasminBuilder.addClass(B.getName(), A.getName());
+ classBuilder.addDefaultConstructor();
+ for (String methodName : ImmutableList.of("m1", "m2")) {
+ classBuilder.addPrivateVirtualMethod(
+ methodName, ImmutableList.of(), "V", jasminCodeForPrinting("In B." + methodName + "()"));
+ }
+ for (String methodName : ImmutableList.of("m3", "m4")) {
+ classBuilder.addStaticMethod(
+ methodName, ImmutableList.of(), "V", jasminCodeForPrinting("In B." + methodName + "()"));
+ }
+
+ AndroidApp input =
+ AndroidApp.builder()
+ .addProgramFiles(
+ ToolHelper.getClassFileForTestClass(main),
+ ToolHelper.getClassFileForTestClass(A),
+ ToolHelper.getClassFileForTestClass(C))
+ .addClassProgramData(jasminBuilder.buildClasses())
+ .build();
+
+ // Run the program using java.
+ String referenceResult;
+ if (backend == Backend.DEX
+ && ToolHelper.getDexVm().getVersion().isOlderThanOrEqual(Version.V7_0_0)
+ && ToolHelper.getDexVm().getVersion().isAtLeast(Version.V5_1_1)) {
+ referenceResult =
+ String.join(
+ System.lineSeparator(),
+ "In A.m1()",
+ "In A.m2()",
+ "In A.m3()",
+ "In A.m4()",
+ "In C.m1()",
+ "In A.m2()",
+ "In C.m3()",
+ "In A.m4()",
+ "In A.m1()", // With Java: Caught IllegalAccessError when calling B.m1()
+ "In A.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3()
+ "In C.m1()", // With Java: Caught IllegalAccessError when calling B.m1()
+ "In C.m3()", // With Java: Caught IncompatibleClassChangeError when calling B.m3()
+ "In C.m1()",
+ "In C.m3()",
+ "");
+ } else {
+ Path referenceJar = temp.getRoot().toPath().resolve("input.jar");
+ ArchiveConsumer inputConsumer = new ArchiveConsumer(referenceJar);
+ for (Class<?> clazz : ImmutableList.of(main, A, C)) {
+ inputConsumer.accept(
+ ByteDataView.of(ToolHelper.getClassAsBytes(clazz)),
+ DescriptorUtils.javaTypeToDescriptor(clazz.getName()),
+ null);
+ }
+ inputConsumer.accept(
+ ByteDataView.of(jasminBuilder.buildClasses().get(0)),
+ DescriptorUtils.javaTypeToDescriptor(B.getName()),
+ null);
+ inputConsumer.finished(null);
+
+ ProcessResult javaResult = ToolHelper.runJava(referenceJar, main.getName());
+ assertEquals(javaResult.exitCode, 0);
+ referenceResult = javaResult.stdout;
+ }
+
+ // Run the program on Art after is has been compiled with R8.
+ AndroidApp compiled =
+ compileWithR8(
+ input,
+ keepMainProguardConfiguration(main),
+ options -> {
+ options.enableClassInlining = enableClassInlining;
+ options.enableVerticalClassMerging = enableVerticalClassMerging;
+ options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE);
+ },
+ backend);
+ assertEquals(referenceResult, runOnVM(compiled, main, backend));
+
+ // Check that B is present and that it doesn't contain the unused private method m2.
+ if (!enableClassInlining && !enableVerticalClassMerging) {
+ CodeInspector inspector = new CodeInspector(compiled);
+ ClassSubject classSubject = inspector.clazz(B.getName());
+ assertThat(classSubject, isRenamed());
+ assertThat(classSubject.method("void", "m1", ImmutableList.of()), isPresent());
+ assertThat(classSubject.method("void", "m2", ImmutableList.of()), not(isPresent()));
+ assertThat(classSubject.method("void", "m3", ImmutableList.of()), isPresent());
+ assertThat(classSubject.method("void", "m4", ImmutableList.of()), not(isPresent()));
+ }
+ }
+
+ private static String[] jasminCodeForPrinting(String message) {
+ return new String[] {
+ ".limit stack 2",
+ ".limit locals 1",
+ "getstatic java/lang/System/out Ljava/io/PrintStream;",
+ "ldc \"" + message + "\"",
+ "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
+ "return"
+ };
+ }
+}
+
+class NonVirtualOverrideTestClass {
+
+ public static void main(String[] args) {
+ A a = new B();
+ a.m1();
+ a.m2();
+ a.m3();
+ a.m4();
+
+ a = new C();
+ a.m1();
+ a.m2();
+ a.m3();
+ a.m4();
+
+ B b = new B();
+ try {
+ b.m1();
+ } catch (IllegalAccessError exception) {
+ System.out.println("Caught IllegalAccessError when calling B.m1()");
+ }
+ try {
+ b.m3();
+ } catch (IncompatibleClassChangeError exception) {
+ System.out.println("Caught IncompatibleClassChangeError when calling B.m3()");
+ }
+
+ try {
+ b = new C();
+ b.m1();
+ } catch (IllegalAccessError exception) {
+ System.out.println("Caught IllegalAccessError when calling B.m1()");
+ }
+ try {
+ b = new C();
+ b.m3();
+ } catch (IncompatibleClassChangeError exception) {
+ System.out.println("Caught IncompatibleClassChangeError when calling B.m3()");
+ }
+
+ C c = new C();
+ c.m1();
+ c.m3();
+ }
+
+ static class A {
+
+ public void m1() {
+ System.out.println("In A.m1()");
+ }
+
+ public void m2() {
+ System.out.println("In A.m2()");
+ }
+
+ public void m3() {
+ System.out.println("In A.m3()");
+ }
+
+ public void m4() {
+ System.out.println("In A.m4()");
+ }
+ }
+
+ static class B extends A {
+
+ // Will be made private with Jasmin. This method is targeted and can therefore not be removed.
+ public void m1() {
+ System.out.println("In B.m1()");
+ }
+
+ // Will be made private with Jasmin. Ends up being dead code because the method is never called.
+ public void m2() {
+ System.out.println("In B.m2()");
+ }
+
+ // Will be made static with Jasmin. This method is targeted and can therefore not be removed.
+ public void m3() {
+ System.out.println("In B.m3()");
+ }
+
+ // Will be made static with Jasmin. Ends up being dead code because the method is never called.
+ public void m4() {
+ System.out.println("In B.m4()");
+ }
+ }
+
+ static class C extends B {
+
+ public void m1() {
+ System.out.println("In C.m1()");
+ }
+
+ public void m3() {
+ System.out.println("In C.m3()");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/PrivateOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/PrivateOverrideTest.java
deleted file mode 100644
index a70d68b..0000000
--- a/src/test/java/com/android/tools/r8/shaking/PrivateOverrideTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-// 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 static org.junit.Assert.assertEquals;
-
-import com.android.tools.r8.ByteDataView;
-import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-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.DescriptorUtils;
-import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-@RunWith(Parameterized.class)
-public class PrivateOverrideTest extends TestBase {
-
- private static Class<?> main = PrivateOverrideTestClass.class;
- private static Class<?> A = PrivateOverrideTestClass.A.class;
- private static Class<?> B = PrivateOverrideTestClass.B.class;
-
- private final Backend backend;
-
- @Rule public ExpectedException thrown = ExpectedException.none();
-
- @Parameterized.Parameters(name = "Backend: {0}")
- public static Collection<Backend> data() {
- return Arrays.asList(Backend.values());
- }
-
- public PrivateOverrideTest(Backend backend) {
- this.backend = backend;
- }
-
- @Test
- public void test() throws Exception {
- // Construct B such that it inherits from A and shadows method A.m() with a private method.
- JasminBuilder jasminBuilder = new JasminBuilder();
- ClassBuilder classBuilder = jasminBuilder.addClass(B.getName(), A.getName());
- classBuilder.addDefaultConstructor();
- classBuilder.addPrivateVirtualMethod(
- "m",
- ImmutableList.of(),
- "V",
- ".limit stack 2",
- ".limit locals 1",
- "getstatic java/lang/System/out Ljava/io/PrintStream;",
- "ldc \"In B.m()\"",
- "invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V",
- "return");
-
- AndroidApp input =
- AndroidApp.builder()
- .addProgramFiles(ToolHelper.getClassFileForTestClass(main))
- .addProgramFiles(ToolHelper.getClassFileForTestClass(A))
- .addClassProgramData(jasminBuilder.buildClasses())
- .build();
-
- // Run the program using java.
- Path referenceJar = temp.getRoot().toPath().resolve("input.jar");
- ArchiveConsumer inputConsumer = new ArchiveConsumer(referenceJar);
- for (Class<?> clazz : ImmutableList.of(main, A)) {
- inputConsumer.accept(
- ByteDataView.of(ToolHelper.getClassAsBytes(clazz)),
- DescriptorUtils.javaTypeToDescriptor(clazz.getName()),
- null);
- }
- inputConsumer.accept(
- ByteDataView.of(jasminBuilder.buildClasses().get(0)),
- DescriptorUtils.javaTypeToDescriptor(B.getName()),
- null);
- inputConsumer.finished(null);
-
- ProcessResult referenceResult = ToolHelper.runJava(referenceJar, main.getName());
- assertEquals(referenceResult.exitCode, 0);
-
- // TODO(b/116093710): Fix tree shaking.
- thrown.expect(AssertionError.class);
- thrown.expectMessage("java.lang.AbstractMethodError");
- thrown.expectMessage("com.android.tools.r8.shaking.PrivateOverrideTestClass$A.m()");
-
- // Run the program on Art after is has been compiled with R8.
- AndroidApp compiled =
- compileWithR8(
- input,
- keepMainProguardConfiguration(main),
- options -> {
- options.enableMinification = false;
- options.enableVerticalClassMerging = false;
- },
- backend);
- assertEquals(referenceResult.stdout, runOnVM(compiled, main, backend));
-
- // TODO(b/116093710): Assert that B.m() is removed by tree pruner.
- }
-}
-
-class PrivateOverrideTestClass {
-
- public static void main(String[] args) {
- A b = new B();
- b.m(); // Since B.m() is made private with Jasmin, this should print "In A.m()".
- }
-
- static class A {
-
- public void m() {
- System.out.println("In A.m()");
- }
- }
-
- static class B extends A {
-
- // Will be made private with Jasmin. Therefore ends up being dead code.
- public void m() {
- System.out.println("In B.m()");
- }
- }
-}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index 3f66921..f8907d6 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -154,17 +154,10 @@
}
@Before
- public void resetAllowPartiallyImplementedOptions() {
- handler = new KeepingDiagnosticHandler();
- reporter = new Reporter(handler);
- parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, false, false);
- }
-
- @Before
public void resetAllowTestOptions() {
handler = new KeepingDiagnosticHandler();
reporter = new Reporter(handler);
- parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, true, true);
+ parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, true);
}
@Test
@@ -901,7 +894,7 @@
@Test
public void parseKeepdirectories() throws Exception {
ProguardConfigurationParser parser =
- new ProguardConfigurationParser(new DexItemFactory(), reporter, false, false);
+ new ProguardConfigurationParser(new DexItemFactory(), reporter, false);
parser.parse(Paths.get(KEEPDIRECTORIES));
verifyParserEndsCleanly();
}
@@ -1317,7 +1310,6 @@
@Test
public void parse_adaptresourcexxx_keepdirectories_noArguments1() {
- resetAllowPartiallyImplementedOptions();
ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
"-adaptresourcefilenames",
"-adaptresourcefilecontents",
@@ -1330,7 +1322,6 @@
@Test
public void parse_adaptresourcexxx_keepdirectories_noArguments2() {
- resetAllowPartiallyImplementedOptions();
ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
"-keepdirectories",
"-adaptresourcefilenames",
@@ -1343,7 +1334,6 @@
@Test
public void parse_adaptresourcexxx_keepdirectories_noArguments3() {
- resetAllowPartiallyImplementedOptions();
ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
"-adaptresourcefilecontents",
"-keepdirectories",
@@ -1365,7 +1355,6 @@
@Test
public void parse_adaptresourcexxx_keepdirectories_singleArgument() {
- resetAllowPartiallyImplementedOptions();
ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
"-adaptresourcefilenames " + FILE_FILTER_SINGLE,
"-adaptresourcefilecontents " + FILE_FILTER_SINGLE,
@@ -1398,7 +1387,6 @@
@Test
public void parse_adaptresourcexxx_keepdirectories_multipleArgument() {
- resetAllowPartiallyImplementedOptions();
ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
"-adaptresourcefilenames " + FILE_FILTER_MULTIPLE,
"-adaptresourcefilecontents " + FILE_FILTER_MULTIPLE,
@@ -1415,7 +1403,7 @@
"-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
for (String option : options) {
try {
- resetAllowPartiallyImplementedOptions();
+ reset();
parser.parse(createConfigurationForTesting(ImmutableList.of(option + " ,")));
fail("Expect to fail due to the lack of path filter.");
} catch (AbortException e) {
@@ -1430,7 +1418,7 @@
"-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
for (String option : options) {
try {
- resetAllowPartiallyImplementedOptions();
+ reset();
parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,,yyy")));
fail("Expect to fail due to the lack of path filter.");
} catch (AbortException e) {
@@ -1445,7 +1433,7 @@
"-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
for (String option : options) {
try {
- resetAllowPartiallyImplementedOptions();
+ reset();
parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,")));
fail("Expect to fail due to the lack of path filter.");
} catch (AbortException e) {
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
index af9cfdf..e6de7cc 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAccessModifierTest.java
@@ -52,6 +52,33 @@
}
@Test
+ public void ifOnPublic_noPublicClassForIfRule() throws Exception {
+ List<String> config = ImmutableList.of(
+ "-repackageclasses 'top'",
+ "-keep class **.Main* {",
+ " public static void callIfNonPublic();",
+ "}",
+ "-if public class **.ClassForIf {",
+ " <methods>;",
+ "}",
+ "-keep,allowobfuscation class **.ClassForSubsequent {",
+ " public <methods>;",
+ "}"
+ );
+ CodeInspector codeInspector = inspectAfterShrinking(shrinker, CLASSES, config);
+ ClassSubject classSubject = codeInspector.clazz(ClassForIf.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject methodSubject = classSubject.method(publicMethod);
+ assertThat(methodSubject, not(isPresent()));
+ methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, isPresent());
+ assertFalse(methodSubject.getMethod().accessFlags.isPublic());
+
+ classSubject = codeInspector.clazz(ClassForSubsequent.class);
+ assertThat(classSubject, not(isPresent()));
+ }
+
+ @Test
public void ifOnNonPublic_keepOnPublic() throws Exception {
List<String> config = ImmutableList.of(
"-printmapping",
@@ -191,12 +218,12 @@
methodSubject = classSubject.method(publicMethod);
assertThat(methodSubject, not(isPresent()));
methodSubject = classSubject.method(nonPublicMethod);
+ assertThat(methodSubject, isPresent());
if (isR8(shrinker)) {
- // TODO(b/72109068): if kept in the 1st tree shaking, should be kept after publicizing.
- assertThat(methodSubject, not(isPresent()));
+ // TODO(b/72109068): if kept in the 1st tree shaking, should not be publicized.
+ assertTrue(methodSubject.getMethod().accessFlags.isPublic());
return;
}
- assertThat(methodSubject, isPresent());
assertFalse(methodSubject.getMethod().accessFlags.isPublic());
}
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAnnotationTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAnnotationTest.java
index ce2992a..00cb62e 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/IfOnAnnotationTest.java
@@ -8,7 +8,6 @@
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -84,7 +83,6 @@
UsedAnnotation.class, UsedAnnotationDependent.class);
}
- @Ignore("b/116092333")
@Test
public void ifOnAnnotation_onDependentClass_withNthWildcard() throws Exception {
List<String> config = ImmutableList.of(
diff --git a/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java b/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
index 30a9de9..bdd8e1a 100644
--- a/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
+++ b/src/test/java/com/android/tools/r8/shaking/keepclassmembers/b115867670/B115867670.java
@@ -18,7 +18,6 @@
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -127,6 +126,20 @@
}
@Test
+ public void testDependentWithKeepClass() throws Exception {
+ runTest(
+ "-keep @" + pkg + ".JsonClass class ** { <fields>; }",
+ this::checkKeepClassMembers);
+ }
+
+ @Test
+ public void testDependentWithKeepClassAllowObfuscation() throws Exception {
+ runTest(
+ "-keep,allowobfuscation @" + pkg + ".JsonClass class ** { <fields>; }",
+ this::checkKeepClassMembersRenamed);
+ }
+
+ @Test
public void testDependentWithKeepClassMembers() throws Exception {
runTest(
"-keepclassmembers @" + pkg + ".JsonClass class ** { <fields>; }",
@@ -141,7 +154,6 @@
}
@Test
- @Ignore("b/116092333")
public void testDependentWithIfKeepClassMembers() throws Exception {
runTest(
"-if @" + pkg + ".JsonClass class * -keepclassmembers class <1> { <fields>; }",
@@ -149,7 +161,6 @@
}
@Test
- @Ignore("b/116092333")
public void testDependentWithIfKeepClassMembersAllowObfuscation() throws Exception {
runTest(
"-if @"
diff --git a/tools/internal_test.py b/tools/internal_test.py
index 0cb9019..16ed783 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -4,6 +4,30 @@
# BSD-style license that can be found in the LICENSE file.
# Run all internal tests, archive result to cloud storage.
+# In the continuous operation flow we have a tester continuously checking
+# a specific cloud storage location for a file with a git hash.
+# If the file is there, the tester will remove the file, and add another
+# file stating that this is now being run. After successfully running,
+# the tester will add yet another file, and remove the last one.
+# Complete flow with states:
+# 1:
+# BOT:
+# Add file READY_FOR_TESTING (contains git hash)
+# Wait until file TESTING_COMPLETE exists (contains git hash)
+# Timeout if no progress for RUN_TIMEOUT
+# Cleanup READY_FOR_TESTING and TESTING
+# 2:
+# TESTER:
+# Replace file READY_FOR_TESTING by TESTING (contains git hash)
+# Run tests for git hash
+# Upload commit specific logs if failures
+# Upload git specific overall status file (failed or succeeded)
+# Replace file TESTING by TESTING_COMPLETE (contains git hash)
+# 3:
+# BOT:
+# Read overall status
+# Delete TESTING_COMPLETE
+# Exit based on status
import optparse
import os
@@ -12,18 +36,36 @@
import time
import utils
-# How often to pull the git repo, in seconds.
-PULL_DELAY = 25
+# How often the bot/tester should check state
+PULL_DELAY = 30
# Command timeout, in seconds.
RUN_TIMEOUT = 3600
+# Add some extra time for the bot, since the tester might not start immediately.
+BOT_RUN_TIMEOUT = 4000
BUCKET = 'r8-test-results'
TEST_RESULT_DIR = 'internal'
+# Magic files
+READY_FOR_TESTING = 'READY_FOR_TESTING'
+TESTING = 'TESTING'
+TESTING_COMPLETE = 'TESTING_COMPLETE'
+
+ALL_MAGIC = [READY_FOR_TESTING, TESTING, TESTING_COMPLETE]
+
+# Log file names
+STDERR = 'stderr'
+STDOUT = 'stdout'
+EXITCODE = 'exitcode'
+TIMED_OUT = 'timed_out'
+
def ParseOptions():
result = optparse.OptionParser()
result.add_option('--continuous',
help='Continuously run internal tests and post results to GCS.',
default=False, action='store_true')
+ result.add_option('--bot',
+ help='Run in bot mode, i.e., scheduling runs.',
+ default=False, action='store_true')
result.add_option('--archive',
help='Post result to GCS, implied by --continuous',
default=False, action='store_true')
@@ -39,22 +81,39 @@
print('Restarting tools/internal_test.py, content changed')
os.execv(sys.argv[0], sys.argv)
-def git_pull():
+def ensure_git_clean():
# Ensure clean git repo.
diff = subprocess.check_output(['git', 'diff'])
if len(diff) > 0:
print('Local modifications to the git repo, exiting')
sys.exit(1)
+
+def git_pull():
+ ensure_git_clean()
subprocess.check_call(['git', 'pull'])
return utils.get_HEAD_sha1()
+def git_checkout(git_hash):
+ ensure_git_clean()
+ # Ensure that we are up to date to get the commit.
+ git_pull()
+ subprocess.check_call(['git', 'checkout', git_hash])
+ return utils.get_HEAD_sha1()
+
+def get_test_result_dir():
+ return os.path.join(BUCKET, TEST_RESULT_DIR)
+
def get_sha_destination(sha):
- return os.path.join(BUCKET, TEST_RESULT_DIR, sha)
+ return os.path.join(get_test_result_dir(), sha)
def archive_status(failed):
gs_destination = 'gs://%s' % get_sha_destination(utils.get_HEAD_sha1())
archive_value('status', gs_destination, failed)
+def get_status(sha):
+ gs_destination = 'gs://%s/status' % get_sha_destination(sha)
+ return utils.cat_file_on_cloud_storage(gs_destination)
+
def archive_file(name, gs_dir, src_file):
gs_file = '%s/%s' % (gs_dir, name)
utils.upload_file_to_cloud_storage(src_file, gs_file, public_read=False)
@@ -68,30 +127,97 @@
def archive_log(stdout, stderr, exitcode, timed_out, cmd):
sha = utils.get_HEAD_sha1()
- cmd_dir = cmd.replace(' ', '_')
+ cmd_dir = cmd.replace(' ', '_').replace('/', '_')
destination = os.path.join(get_sha_destination(sha), cmd_dir)
gs_destination = 'gs://%s' % destination
url = 'https://storage.cloud.google.com/%s' % destination
print('Archiving logs to: %s' % gs_destination)
- archive_value('exitcode', gs_destination, exitcode)
- archive_value('timed_out', gs_destination, timed_out)
- archive_file('stdout', gs_destination, stdout)
- archive_file('stderr', gs_destination, stderr)
+ archive_value(EXITCODE, gs_destination, exitcode)
+ archive_value(TIMED_OUT, gs_destination, timed_out)
+ archive_file(STDOUT, gs_destination, stdout)
+ archive_file(STDERR, gs_destination, stderr)
print('Logs available at: %s' % url)
+def get_magic_file_base_path():
+ return 'gs://%s/magic' % get_test_result_dir()
+
+def get_magic_file_gs_path(name):
+ return '%s/%s' % (get_magic_file_base_path(), name)
+
+def get_magic_file_exists(name):
+ return utils.file_exists_on_cloud_storage(get_magic_file_gs_path(name))
+
+def delete_magic_file(name):
+ utils.delete_file_from_cloud_storage(get_magic_file_gs_path(name))
+
+def put_magic_file(name, sha):
+ archive_value(name, get_magic_file_base_path(), sha)
+
+def get_magic_file_content(name, ignore_errors=False):
+ return utils.cat_file_on_cloud_storage(get_magic_file_gs_path(name),
+ ignore_errors=ignore_errors)
+
+def print_magic_file_state():
+ print('Magic file status:')
+ for magic in ALL_MAGIC:
+ if get_magic_file_exists(magic):
+ content = get_magic_file_content(magic, ignore_errors=True)
+ print('%s content: %s' % (magic, content))
+
+def run_bot():
+ print_magic_file_state()
+ # Ensure that there is nothing currently scheduled (broken/stopped run)
+ for magic in ALL_MAGIC:
+ if get_magic_file_exists(magic):
+ print('ERROR: Synchronizing file %s exists, cleaning up' % magic)
+ delete_magic_file(magic)
+ print_magic_file_state()
+ assert not get_magic_file_exists(READY_FOR_TESTING)
+ git_hash = utils.get_HEAD_sha1()
+ put_magic_file(READY_FOR_TESTING, git_hash)
+ begin = time.time()
+ while True:
+ if time.time() - begin > BOT_RUN_TIMEOUT:
+ print('Timeout exceeded')
+ raise Exception('Bot timeout')
+ if get_magic_file_exists(TESTING_COMPLETE):
+ if get_magic_file_content(TESTING_COMPLETE) == git_hash:
+ break
+ else:
+ raise Exception('Non matching git hashes %s and %s' % (
+ get_magic_file_content(TESTING_COMPLETE), git_hash))
+ print('Still waiting for test result')
+ print_magic_file_state()
+ time.sleep(PULL_DELAY)
+ total_time = time.time()-begin
+ print('Done running test for %s in %ss' % (git_hash, total_time))
+ test_status = get_status(git_hash)
+ delete_magic_file(TESTING_COMPLETE)
+ print('Test status is: %s' % test_status)
+ if test_status != '0':
+ return 1
+
def run_continuously():
# If this script changes, we will restart ourselves
own_content = get_own_file_content()
- git_hash = utils.get_HEAD_sha1()
while True:
restart_if_new_version(own_content)
- print('Running with hash: %s' % git_hash)
- exitcode = run_once(archive=True)
- git_pull()
- while git_pull() == git_hash:
- print('Still on same git hash: %s' % git_hash)
- time.sleep(PULL_DELAY)
- git_hash = utils.get_HEAD_sha1()
+ print_magic_file_state()
+ if get_magic_file_exists(READY_FOR_TESTING):
+ git_hash = get_magic_file_content(READY_FOR_TESTING)
+ checked_out = git_checkout(git_hash)
+ # Sanity check, if this does not succeed stop.
+ if checked_out != git_hash:
+ print('Inconsistent state: %s %s' % (git_hash, checked_out))
+ sys.exit(1)
+ put_magic_file(TESTING, git_hash)
+ delete_magic_file(READY_FOR_TESTING)
+ print('Running with hash: %s' % git_hash)
+ exitcode = run_once(archive=True)
+ print('Running finished with exit code %s' % exitcode)
+ put_magic_file(TESTING_COMPLETE, git_hash)
+ delete_magic_file(TESTING)
+ time.sleep(PULL_DELAY)
def handle_output(archive, stderr, stdout, exitcode, timed_out, cmd):
if archive:
@@ -147,17 +273,21 @@
if execute(cmd, archive):
failed = True
# Ensure that all internal apps compile.
- cmd = ['tools/run_on_app.py', '--run-all', '--out=out']
+ cmd = ['tools/run_on_app.py', '--ignore-java-version','--run-all',
+ '--out=out']
if execute(cmd, archive):
failed = True
archive_status(1 if failed else 0)
+ return failed
def Main():
(options, args) = ParseOptions()
if options.continuous:
run_continuously()
+ elif options.bot:
+ return run_bot()
else:
- run_once(options.archive)
+ return run_once(options.archive)
if __name__ == '__main__':
sys.exit(Main())
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index 0667abe..bdb1df8 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -49,6 +49,10 @@
help='Running on golem, do not build or download',
default=False,
action='store_true')
+ result.add_option('--ignore-java-version',
+ help='Do not check java version',
+ default=False,
+ action='store_true')
result.add_option('--no-libraries',
help='Do not pass in libraries, even if they exist in conf',
default=False,
@@ -115,7 +119,7 @@
# do Bug: #BUG in the commit message of disabling to ensure re-enabling
DISABLED_PERMUTATIONS = [
('youtube', '12.10', 'dex'), # b/116089492
- ('youtube', '12.22', 'deploy') # b/116093710
+ ('gmscore', 'latest', 'deploy') # b/116575775
]
def get_permutations():
@@ -150,8 +154,10 @@
exit(exit_code)
def main(argv):
- utils.check_java_version()
(options, args) = ParseOptions(argv)
+ if not options.ignore_java_version:
+ utils.check_java_version()
+
if options.run_all:
return run_all(options, args)
return run_with_options(options, args)
diff --git a/tools/utils.py b/tools/utils.py
index d96151d..20ba093 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -106,6 +106,27 @@
PrintCmd(cmd)
subprocess.check_call(cmd)
+def delete_file_from_cloud_storage(destination):
+ cmd = ['gsutil.py', 'rm', destination]
+ PrintCmd(cmd)
+ subprocess.check_call(cmd)
+
+def cat_file_on_cloud_storage(destination, ignore_errors=False):
+ cmd = ['gsutil.py', 'cat', destination]
+ PrintCmd(cmd)
+ try:
+ return subprocess.check_output(cmd)
+ except subprocess.CalledProcessError as e:
+ if ignore_errors:
+ return ''
+ else:
+ raise e
+
+def file_exists_on_cloud_storage(destination):
+ cmd = ['gsutil.py', 'ls', destination]
+ PrintCmd(cmd)
+ return subprocess.call(cmd) == 0
+
def download_file_from_cloud_storage(source, destination):
cmd = ['gsutil.py', 'cp', source, destination]
PrintCmd(cmd)