Discard init with wildcards.
If the current member rule is constructor pattern, make sure it does not
have wildcard patterns, since only "<init>" is allowed.
Bug: 130710288
Change-Id: I42822dd8c4702019389975e788d963b06f360edd
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 1d9de37..492f1f5 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -1045,6 +1045,8 @@
ruleBuilder.setRuleType(ProguardMemberType.ALL);
} else {
if (hasNextChar('(')) {
+ // Parsing "<init>" is already done, and thus neither '<' nor '>' can appear.
+ checkConstructorPattern(first, firstStart);
ruleBuilder.setRuleType(ProguardMemberType.CONSTRUCTOR);
ruleBuilder.setName(first);
ruleBuilder.setArguments(parseArgumentList());
@@ -1055,6 +1057,10 @@
if (second != null) {
skipWhitespace();
if (hasNextChar('(')) {
+ // Neither '<' nor '>' can appear except for "[access-flag]* void <init>(...)".
+ if (!first.pattern.contains("void") || !second.pattern.equals("<init>")) {
+ checkConstructorPattern(second, secondStart);
+ }
ruleBuilder.setRuleType(ProguardMemberType.METHOD);
ruleBuilder.setName(second);
ruleBuilder
@@ -1140,6 +1146,18 @@
}
}
+ private void checkConstructorPattern(
+ IdentifierPatternWithWildcards pattern, TextPosition position)
+ throws ProguardRuleParserException {
+ if (pattern.pattern.contains("<")) {
+ throw parseError("Unexpected character '<' in method name. "
+ + "The character '<' is only allowed in the method name '<init>'.", position);
+ } else if (pattern.pattern.contains(">")) {
+ throw parseError("Unexpected character '>' in method name. "
+ + "The character '>' is only allowed in the method name '<init>'.", position);
+ }
+ }
+
private List<ProguardTypeMatcher> parseArgumentList() throws ProguardRuleParserException {
List<ProguardTypeMatcher> arguments = new ArrayList<>();
skipWhitespace();
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 62aaf50..d0fb723 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -5,6 +5,7 @@
import static com.android.tools.r8.DiagnosticsChecker.checkDiagnostics;
import static com.android.tools.r8.shaking.ProguardConfigurationSourceStrings.createConfigurationForTesting;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -12,7 +13,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -27,6 +27,7 @@
import com.android.tools.r8.position.Position;
import com.android.tools.r8.position.TextRange;
import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.constructor.InitMatchingTest;
import com.android.tools.r8.utils.AbortException;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
@@ -2381,6 +2382,12 @@
return parser.getConfig();
}
+ private ProguardConfiguration parseAndVerifyParserEndsCleanly(Path config) {
+ parser.parse(config);
+ verifyParserEndsCleanly();
+ return parser.getConfig();
+ }
+
private void verifyParserEndsCleanly() {
assertEquals(0, handler.infos.size());
assertEquals(0, handler.warnings.size());
@@ -2538,4 +2545,44 @@
+ "The negation character can only be used to negate access flags");
}
}
+
+ @Test
+ public void b130710288() throws Exception {
+ // "[[access-flag]* void] <init>" is the only valid format.
+ for (String initName : ImmutableList.of("<init>", "void <init>", "public void <init>")) {
+ Path initConfig = writeTextToTempFile(
+ "-keep class **.MyClass {",
+ " " + initName + "(...);",
+ "}");
+ parseAndVerifyParserEndsCleanly(initConfig);
+ }
+
+ for (String initName : InitMatchingTest.INIT_NAMES) {
+ // Tested above.
+ if (initName.contains("<init>")) {
+ continue;
+ }
+ reset();
+ Path proguardConfig = writeTextToTempFile(
+ "-keep class **.MyClass {",
+ " " + initName + "(...);",
+ "}");
+ try {
+ parser.parse(proguardConfig);
+ fail("Expect to fail due to unsupported constructor name pattern.");
+ } catch (AbortException e) {
+ int column = initName.contains("void") ? initName.indexOf("void") + 8 : 3;
+ checkDiagnostics(
+ handler.errors, proguardConfig, 2, column, "Unexpected character", "method name");
+ }
+ // For some exceptional cases, Proguard accepts the rules but fails with an empty jar message.
+ if (initName.contains("<init>")
+ || initName.contains("<clinit>")
+ || initName.contains("void")) {
+ continue;
+ }
+ verifyFailWithProguard6(
+ proguardConfig, "Expecting type and name instead of just '" + initName + "'");
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/r8/shaking/constructor/InitMatchingTest.java b/src/test/java/com/android/tools/r8/shaking/constructor/InitMatchingTest.java
new file mode 100644
index 0000000..4db8a60
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/constructor/InitMatchingTest.java
@@ -0,0 +1,136 @@
+// 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.constructor;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.ProguardTestCompileResult;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+class InitMatchingTestClass {
+ // Trivial <clinit>
+ static {
+ }
+ int field;
+ public InitMatchingTestClass(int arg) {
+ field = arg;
+ }
+}
+
+@RunWith(Parameterized.class)
+public class InitMatchingTest extends TestBase {
+ public static final List<String> INIT_NAMES = ImmutableList.of(
+ "<clinit>", "<init>", "<*init>", "<in*>", "<in*", "*init>",
+ "void <clinit>", "void <init>", "void <*init>", "void <in*>", "void <in*", "void *init>",
+ "public void <init>", "public void <*init>"
+ );
+
+ @Parameterized.Parameters(name = "{0} \"{1}\"")
+ public static Collection<Object[]> data() {
+ return buildParameters(ToolHelper.getBackends(), INIT_NAMES);
+ }
+
+ private final Backend backend;
+ private final String initName;
+
+ public InitMatchingTest(Backend backend, String initName) {
+ this.backend = backend;
+ this.initName = initName;
+ }
+
+ private String createKeepRule() {
+ return "-keep class * { " + initName + "(...); }";
+ }
+
+ @Test
+ public void testProguard() throws Exception {
+ assumeTrue(backend == Backend.CF);
+ ProguardTestCompileResult result;
+ try {
+ result =
+ testForProguard()
+ .addProgramClasses(InitMatchingTestClass.class)
+ .addKeepRules(createKeepRule())
+ .compile();
+ } catch (CompilationFailedException e) {
+ assertNotEquals("<init>", initName);
+ if (initName.equals("void <in*")) {
+ assertThat(e.getMessage(), containsString("Missing closing angular bracket"));
+ } else {
+ assertThat(e.getMessage(),
+ containsString("Expecting type and name instead of just '" + initName + "'"));
+ }
+ return;
+ }
+ result.inspect(this::inspectProguard);
+ }
+
+ private void inspectProguard(CodeInspector inspector) {
+ ClassSubject classSubject = inspector.clazz(InitMatchingTestClass.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject init = classSubject.init(ImmutableList.of("int"));
+ if (initName.equals("void <clinit>")) {
+ assertThat(init, not(isPresent()));
+ } else {
+ assertThat(init, isPresent());
+ }
+ MethodSubject clinit = classSubject.clinit();
+ if (initName.equals("void <clinit>")
+ || initName.equals("void <*init>")
+ || initName.equals("void *init>")) {
+ assertThat(clinit, isPresent());
+ } else {
+ assertThat(clinit, not(isPresent()));
+ }
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ R8TestCompileResult result;
+ try {
+ result =
+ testForR8(backend)
+ .addProgramClasses(InitMatchingTestClass.class)
+ .addKeepRules(createKeepRule())
+ .compile();
+ } catch (CompilationFailedException e) {
+ assertNotEquals("<init>", initName);
+ assertThat(e.getCause().getMessage(),
+ containsString("Unexpected character '" + (initName.contains("<") ? "<" : ">") + "'"));
+ assertThat(e.getCause().getMessage(),
+ containsString("only allowed in the method name '<init>'"));
+ return;
+ }
+ result
+ .assertNoMessages()
+ .inspect(this::inspectR8);
+ }
+
+ private void inspectR8(CodeInspector inspector) {
+ ClassSubject classSubject = inspector.clazz(InitMatchingTestClass.class);
+ assertThat(classSubject, isPresent());
+ MethodSubject init = classSubject.init(ImmutableList.of("int"));
+ assertThat(init, isPresent());
+ MethodSubject clinit = classSubject.clinit();
+ assertThat(clinit, not(isPresent()));
+ }
+
+}