Add warnings to Proguard parser for unusual types and field names

An unusual type or field name is one containing characters < and/or > which are
not parsed as a back reference.

Bug: 110021323
Change-Id: Ibbe59e32f6797f4f9af392d8397791dbba245cbd
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 c4276dc..1de3f17 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -873,6 +873,7 @@
         ruleBuilder.setName(IdentifierPatternWithWildcards.withoutWildcards("<init>"));
         ruleBuilder.setArguments(parseArgumentList());
       } else {
+        TextPosition firstStart = getPosition();
         IdentifierPatternWithWildcards first =
             acceptIdentifierWithBackreference(IdentifierType.ANY);
         if (first != null) {
@@ -885,6 +886,7 @@
               ruleBuilder.setName(first);
               ruleBuilder.setArguments(parseArgumentList());
             } else {
+              TextPosition secondStart = getPosition();
               IdentifierPatternWithWildcards second =
                   acceptIdentifierWithBackreference(IdentifierType.ANY);
               if (second != null) {
@@ -897,6 +899,12 @@
                           ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory));
                   ruleBuilder.setArguments(parseArgumentList());
                 } else {
+                  if (first.hasUnusualCharacters()) {
+                    warnUnusualCharacters("type", first.pattern, "field", firstStart);
+                  }
+                  if (second.hasUnusualCharacters()) {
+                    warnUnusualCharacters("field name", second.pattern, "field", secondStart);
+                  }
                   ruleBuilder.setRuleType(ProguardMemberType.FIELD);
                   ruleBuilder.setName(second);
                   ruleBuilder
@@ -1490,6 +1498,16 @@
           "Option -" + optionName + " overrides -" + victim, origin, getPosition(start)));
     }
 
+    private void warnUnusualCharacters(
+        String kind, String pattern, String ruleType, TextPosition start) {
+      reporter.warning(new StringDiagnostic(
+          "The " + kind + " \"" + pattern + "\" is used in a " + ruleType + " rule. The "
+              + "characters in this " + kind + " are legal for the JVM, "
+              + "but unlikely to originate from a source language. "
+              + "Maybe this is not the rule you are looking for.",
+          origin, getPosition(start)));
+    }
+
     private void failPartiallyImplementedOption(String optionName, TextPosition start) {
       throw reporter.fatalError(new StringDiagnostic(
           "Option " + optionName + " currently not supported", origin, getPosition(start)));
@@ -1528,5 +1546,25 @@
     boolean isMatchAllNames() {
       return pattern.equals("*");
     }
+
+    boolean hasUnusualCharacters() {
+      if (pattern.contains("<") || pattern.contains(">")) {
+        int angleStartCount = 0;
+        int angleEndCount = 0;
+        for (int i = 0; i < pattern.length(); i++) {
+          char c = pattern.charAt(i);
+          if (c == '<') {
+            angleStartCount++;
+          }
+          if (c == '>') {
+            angleEndCount++;
+          }
+        }
+        // Check that start/end angles are matched, and *only* used for well-formed wildcard
+        // backreferences (e.g. '<1>', but not '<<1>>', '<<*>>' or '>1<').
+        return !(angleStartCount == angleEndCount && angleStartCount == wildcards.size());
+      }
+      return false;
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index 2f45ae2..d79aaf0 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -116,9 +116,14 @@
         messageParts);
   }
 
+  public static Diagnostic checkDiagnostics(List<Diagnostic> diagnostics, int index, Path path,
+      int lineStart, int columnStart, String... messageParts) {
+    return checkDiagnostic(diagnostics.get(index), path, lineStart, columnStart, messageParts);
+  }
+
   public static Diagnostic checkDiagnostics(List<Diagnostic> diagnostics, Path path,
       int lineStart, int columnStart, String... messageParts) {
     assertEquals(1, diagnostics.size());
-    return checkDiagnostic(diagnostics.get(0), path, lineStart, columnStart, messageParts);
+    return checkDiagnostics(diagnostics, 0, path, lineStart, columnStart, messageParts);
   }
 }
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 f96dc45..f484bc8 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1485,7 +1485,7 @@
     ProguardConfigurationParser parser =
         new ProguardConfigurationParser(new DexItemFactory(), reporter);
     parser.parse(proguardConfig);
-    verifyParserEndsCleanly();
+    checkDiagnostics(handler.warnings, proguardConfig, 3, 7, "The field name \"id<<*>>\" is");
 
     verifyWithProguard6(proguardConfig);
   }
@@ -1731,7 +1731,7 @@
     ProguardConfigurationParser parser =
         new ProguardConfigurationParser(new DexItemFactory(), reporter);
     parser.parse(proguardConfig);
-    verifyParserEndsCleanly();
+    checkDiagnostics(handler.warnings, proguardConfig, 2, 5, "The field name \"<fields>\" is");
     verifyWithProguard(proguardConfig);
   }
 
@@ -1753,6 +1753,26 @@
     verifyWithProguard(proguardConfig);
   }
 
+  @Test
+  public void parse_regress110021323() throws Exception {
+    Path proguardConfig = writeTextToTempFile(
+      "-keepclassmembernames class A {",
+      "  <public methods>;",
+      "  <public fields>;",
+      "}"
+    );
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(new DexItemFactory(), reporter);
+    parser.parse(proguardConfig);
+    assertEquals(4, handler.warnings.size());
+    checkDiagnostics(handler.warnings, 0, proguardConfig, 2, 3, "The type \"<public\" is");
+    checkDiagnostics(handler.warnings, 1, proguardConfig, 2, 11, "The field name \"methods>\" is");
+    checkDiagnostics(handler.warnings, 2, proguardConfig, 3, 3, "The type \"<public\" is");
+    checkDiagnostics(handler.warnings, 3, proguardConfig, 3, 11, "The field name \"fields>\" is");
+
+    verifyWithProguard(proguardConfig);
+  }
+
   public void testNotSupported(String option) {
     try {
       reset();