Add support for negated names in member rules

RELNOTES: Support for negating member name patterns

This extend the configuration language such that it is possible to match
on negated member name patterns.

Example: It is now possible to match all methods that don't end in
"ForTesting" using the following rule:

-keepclassmembers class com.example.MyClass {
  *** !*ForTesting(...);
}

Member name patterns can also be negated in the precondition of -if
rules. If a negated member name pattern contains wildcards, such
wildcards cannot be back-referenced in the -if consequent rule.

Bug: b/484870212
Change-Id: I87f23a2238404747917d8e6a02545bc0a7f740f8
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 f99f630..1be2931 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -889,7 +889,7 @@
             Lists.newArrayList(
                 ProguardMemberRule.builder()
                     .setName(
-                        IdentifierPatternWithWildcards.withoutWildcards(
+                        new IdentifierPatternWithWildcardsAndNegation(
                             Constants.INSTANCE_INITIALIZER_NAME))
                     .setRuleType(ProguardMemberType.INIT)
                     .setArguments(Collections.emptyList())
@@ -999,7 +999,19 @@
                     + "(only seen " + patterns.size() + " at this point).",
                 origin, getPosition()));
           }
-          backReference.setReference(patterns.get(backReference.referenceIndex - 1));
+          ProguardWildcard.Pattern referencedPattern =
+              patterns.get(backReference.referenceIndex - 1);
+          if (referencedPattern.isNonReferenceable()) {
+            throw reporter.fatalError(
+                new StringDiagnostic(
+                    "Wildcard <"
+                        + backReference.referenceIndex
+                        + "> is invalid "
+                        + "(cannot reference negated matches).",
+                    origin,
+                    getPosition()));
+          }
+          backReference.setReference(referencedPattern);
         } else {
           assert wildcard.isPattern();
           if (!backReferenceStarted) {
@@ -1367,19 +1379,19 @@
         ruleBuilder.setRuleType(ProguardMemberType.ALL_FIELDS);
       } else if (acceptString("<init>")) {
         ruleBuilder.setRuleType(ProguardMemberType.INIT);
-        ruleBuilder.setName(IdentifierPatternWithWildcards.withoutWildcards("<init>"));
+        ruleBuilder.setName(new IdentifierPatternWithWildcardsAndNegation("<init>"));
         ruleBuilder.setArguments(parseArgumentList(allowValueSpecification, ruleBuilder));
       } else if (acceptString("<clinit>")) {
         ruleBuilder.setRuleType(ProguardMemberType.CLINIT);
-        ruleBuilder.setName(IdentifierPatternWithWildcards.withoutWildcards("<clinit>"));
+        ruleBuilder.setName(new IdentifierPatternWithWildcardsAndNegation("<clinit>"));
         ruleBuilder.setArguments(parseArgumentList(allowValueSpecification, ruleBuilder));
       } else {
         TextPosition firstStart = getPosition();
-        IdentifierPatternWithWildcards first =
-            acceptIdentifierWithBackreference(IdentifierType.ANY);
+        IdentifierPatternWithWildcardsAndNegation first =
+            acceptIdentifierWithBackreference(IdentifierType.ANY, false);
         if (first != null) {
           skipWhitespace();
-          if (first.pattern.equals("*") && hasNextChar(';')) {
+          if (first.getStringPattern().equals("*") && hasNextChar(';')) {
             ruleBuilder.setRuleType(ProguardMemberType.ALL);
           } else {
             // No return type present, only method name, most likely constructors.
@@ -1392,49 +1404,52 @@
             } else {
               if (acceptString("<init>")) {
                 ProguardTypeMatcher typeMatcher =
-                    ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory);
+                    ProguardTypeMatcher.create(
+                        first.getNonNegatedPattern(), ClassOrType.TYPE, dexItemFactory);
                 if (!typeMatcher.hasSpecificType() || !typeMatcher.getSpecificType().isVoidType()) {
                   throw parseError("Expected [access-flag]* void <init>");
                 }
                 ruleBuilder.setRuleType(ProguardMemberType.INIT);
-                ruleBuilder.setName(IdentifierPatternWithWildcards.withoutWildcards("<init>"));
+                ruleBuilder.setName(new IdentifierPatternWithWildcardsAndNegation("<init>"));
                 ruleBuilder.setTypeMatcher(typeMatcher);
                 ruleBuilder.setArguments(parseArgumentList(allowValueSpecification, ruleBuilder));
               } else if (acceptString("<clinit>")) {
                 ProguardTypeMatcher typeMatcher =
-                    ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory);
+                    ProguardTypeMatcher.create(
+                        first.getNonNegatedPattern(), ClassOrType.TYPE, dexItemFactory);
                 if (!typeMatcher.hasSpecificType() || !typeMatcher.getSpecificType().isVoidType()) {
                   throw parseError("Expected [access-flag]* void <clinit>");
                 }
                 ruleBuilder.setRuleType(ProguardMemberType.CLINIT);
-                ruleBuilder.setName(IdentifierPatternWithWildcards.withoutWildcards("<clinit>"));
+                ruleBuilder.setName(new IdentifierPatternWithWildcardsAndNegation("<clinit>"));
                 ruleBuilder.setTypeMatcher(typeMatcher);
                 ruleBuilder.setArguments(parseArgumentList(allowValueSpecification, ruleBuilder));
               } else {
                 TextPosition secondStart = getPosition();
-                IdentifierPatternWithWildcards second =
-                    acceptIdentifierWithBackreference(IdentifierType.ANY);
+                IdentifierPatternWithWildcardsAndNegation second =
+                    acceptIdentifierWithBackreference(IdentifierType.ANY, true);
                 if (second != null) {
                   skipWhitespace();
                   if (hasNextChar('(')) {
                     // Parsing legitimate constructor patters is already done, so angular brackets
                     // can't appear, except for legitimate back references.
-                    if (!second.hasBackreference() || second.hasUnusualCharacters()) {
+                    if (!second.pattern.hasBackreference()
+                        || second.pattern.hasUnusualCharacters()) {
                       checkConstructorPattern(second, secondStart);
                     }
                     ruleBuilder.setRuleType(ProguardMemberType.METHOD);
                     ruleBuilder.setName(second);
-                    ruleBuilder
-                        .setTypeMatcher(
-                            ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory));
+                    ruleBuilder.setTypeMatcher(
+                        ProguardTypeMatcher.create(
+                            first.getNonNegatedPattern(), ClassOrType.TYPE, dexItemFactory));
                     ruleBuilder.setArguments(
                         parseArgumentList(allowValueSpecification, ruleBuilder));
                   } else {
                     ruleBuilder.setRuleType(ProguardMemberType.FIELD);
                     ruleBuilder.setName(second);
-                    ruleBuilder
-                        .setTypeMatcher(
-                            ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory));
+                    ruleBuilder.setTypeMatcher(
+                        ProguardTypeMatcher.create(
+                            first.getNonNegatedPattern(), ClassOrType.TYPE, dexItemFactory));
                   }
                   // Parse "return ..." if present.
                   ruleBuilder.setReturnValue(
@@ -1513,16 +1528,16 @@
     }
 
     private void checkConstructorPattern(
-        IdentifierPatternWithWildcards pattern, TextPosition position) {
-      if (pattern.pattern.equals("<clinit>")) {
+        IdentifierPatternWithWildcardsAndNegation pattern, TextPosition position) {
+      if (pattern.getStringPattern().equals("<clinit>")) {
         reporter.warning(
             new StringDiagnostic("Member rule for <clinit> has no effect.", origin, position));
         return;
       }
-      if (pattern.pattern.contains("<")) {
+      if (pattern.getStringPattern().contains("<")) {
         throw parseError("Unexpected character '<' in method name. "
             + "The character '<' is only allowed in the method name '<init>'.", position);
-      } else if (pattern.pattern.contains(">")) {
+      } else if (pattern.getStringPattern().contains(">")) {
         throw parseError("Unexpected character '>' in method name. "
             + "The character '>' is only allowed in the method name '<init>'.", position);
       }
@@ -2016,16 +2031,6 @@
       return acceptString(this::isClassName);
     }
 
-    private IdentifierPatternWithWildcards acceptIdentifierWithBackreference(IdentifierType kind) {
-      IdentifierPatternWithWildcardsAndNegation pattern =
-          acceptIdentifierWithBackreference(kind, false);
-      if (pattern == null) {
-        return null;
-      }
-      assert !pattern.negated;
-      return pattern.patternWithWildcards;
-    }
-
     private IdentifierPatternWithWildcardsAndNegation acceptIdentifierWithBackreference(
         IdentifierType kind, boolean allowNegation) {
       ImmutableList.Builder<ProguardWildcard> wildcardsCollector = ImmutableList.builder();
@@ -2080,7 +2085,8 @@
             // only '*', '**', and '***' are allowed.
             // E.g., '****' should be regarded as two separate wildcards (e.g., '***' and '*')
             if (asteriskCount >= 3) {
-              wildcardsCollector.add(new ProguardWildcard.Pattern(currentAsterisks.toString()));
+              wildcardsCollector.add(
+                  ProguardWildcard.Pattern.create(currentAsterisks.toString(), negated));
               currentAsterisks = new StringBuilder();
               asteriskCount = 0;
             }
@@ -2089,7 +2095,8 @@
             end += Character.charCount(current);
             continue;
           } else {
-            wildcardsCollector.add(new ProguardWildcard.Pattern(currentAsterisks.toString()));
+            wildcardsCollector.add(
+                ProguardWildcard.Pattern.create(currentAsterisks.toString(), negated));
             currentAsterisks = null;
             asteriskCount = 0;
           }
@@ -2104,11 +2111,13 @@
             asteriskCount = 1;
           } else {
             // For member names, regard '**' or '***' as separate single-asterisk wildcards.
-            wildcardsCollector.add(new ProguardWildcard.Pattern(String.valueOf((char) current)));
+            wildcardsCollector.add(
+                ProguardWildcard.Pattern.create(String.valueOf((char) current), negated));
           }
           end += Character.charCount(current);
         } else if (current == '?' || current == '%') {
-          wildcardsCollector.add(new ProguardWildcard.Pattern(String.valueOf((char) current)));
+          wildcardsCollector.add(
+              ProguardWildcard.Pattern.create(String.valueOf((char) current), negated));
           end += Character.charCount(current);
         } else if (kind == IdentifierType.PACKAGE_NAME
             ? isPackageName(current)
@@ -2130,7 +2139,8 @@
       }
       position = quoted ? end + 1 : end;
       if (currentAsterisks != null) {
-        wildcardsCollector.add(new ProguardWildcard.Pattern(currentAsterisks.toString()));
+        wildcardsCollector.add(
+            ProguardWildcard.Pattern.create(currentAsterisks.toString(), negated));
       }
       if (kind == IdentifierType.CLASS_NAME && currentBackreference != null) {
         // Proguard 6 reports this error message, so try to be compatible.
@@ -2266,7 +2276,7 @@
       IdentifierPatternWithWildcardsAndNegation name = parseClassName(true);
       builder.addClassName(
           name.negated,
-          ProguardTypeMatcher.create(name.patternWithWildcards, ClassOrType.CLASS, dexItemFactory));
+          ProguardTypeMatcher.create(name.pattern, ClassOrType.CLASS, dexItemFactory));
     }
 
     private ProguardClassNameList parseClassNames() {
@@ -2292,7 +2302,7 @@
     private IdentifierPatternWithWildcards parseClassName() {
       IdentifierPatternWithWildcardsAndNegation name = parseClassName(false);
       assert !name.negated;
-      return name.patternWithWildcards;
+      return name.pattern;
     }
 
     private IdentifierPatternWithWildcardsAndNegation parseClassName(boolean allowNegation) {
@@ -2442,14 +2452,14 @@
       this.wildcards = wildcards;
     }
 
-    static IdentifierPatternWithWildcards init() {
-      return withoutWildcards("<init>");
-    }
-
     public static IdentifierPatternWithWildcards withoutWildcards(String pattern) {
       return new IdentifierPatternWithWildcards(pattern, ImmutableList.of());
     }
 
+    public List<ProguardWildcard> getWildcards() {
+      return wildcards;
+    }
+
     boolean isMatchAllNames() {
       return pattern.equals("*");
     }
@@ -2482,15 +2492,40 @@
     }
   }
 
-  static class IdentifierPatternWithWildcardsAndNegation {
-    final IdentifierPatternWithWildcards patternWithWildcards;
+  public static class IdentifierPatternWithWildcardsAndNegation {
+    final IdentifierPatternWithWildcards pattern;
     final boolean negated;
 
+    IdentifierPatternWithWildcardsAndNegation(String pattern) {
+      this(pattern, Collections.emptyList());
+    }
+
+    IdentifierPatternWithWildcardsAndNegation(String pattern, List<ProguardWildcard> wildcards) {
+      this(pattern, wildcards, false);
+    }
+
     IdentifierPatternWithWildcardsAndNegation(
         String pattern, List<ProguardWildcard> wildcards, boolean negated) {
-      patternWithWildcards = new IdentifierPatternWithWildcards(pattern, wildcards);
+      this.pattern = new IdentifierPatternWithWildcards(pattern, wildcards);
       this.negated = negated;
     }
+
+    public IdentifierPatternWithWildcards getNonNegatedPattern() {
+      assert !negated;
+      return pattern;
+    }
+
+    String getStringPattern() {
+      return pattern.pattern;
+    }
+
+    List<ProguardWildcard> getWildcards() {
+      return pattern.getWildcards();
+    }
+
+    boolean isNegated() {
+      return negated;
+    }
   }
 
   static class IncludeWorkItem {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
index 60b7ffbf..91f9fc8 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationUtils.java
@@ -8,7 +8,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcardsAndNegation;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.LongInterval;
@@ -77,7 +77,7 @@
                     .setAccessFlags(publicStaticFinalFlags)
                     .setRuleType(ProguardMemberType.FIELD)
                     .setTypeMatcher(ProguardTypeMatcher.create(factory.intType))
-                    .setName(IdentifierPatternWithWildcards.withoutWildcards("SDK_INT"))
+                    .setName(new IdentifierPatternWithWildcardsAndNegation("SDK_INT"))
                     .setReturnValue(
                         new ProguardMemberRuleValue(
                             new LongInterval(apiLevel.getLevel(), Integer.MAX_VALUE)))
@@ -157,7 +157,7 @@
             Collections.singletonList(
                 ProguardMemberRule.builder()
                     .setRuleType(ProguardMemberType.INIT)
-                    .setName(IdentifierPatternWithWildcards.init())
+                    .setName(new IdentifierPatternWithWildcardsAndNegation("<init>"))
                     .setArguments(Collections.emptyList())
                     .setTypeMatcher(ProguardTypeMatcher.create(factory.voidType))
                     .build()))
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
index 740b2eb..4bb37f5 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardMemberRule.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcardsAndNegation;
 import com.android.tools.r8.shaking.rootset.RootSetBuilder;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ObjectUtils;
@@ -80,8 +80,8 @@
       return this;
     }
 
-    public Builder setName(IdentifierPatternWithWildcards identifierPatternWithWildcards) {
-      this.name = ProguardNameMatcher.create(identifierPatternWithWildcards);
+    public Builder setName(IdentifierPatternWithWildcardsAndNegation name) {
+      this.name = ProguardNameMatcher.create(name);
       return this;
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardNameMatcher.java b/src/main/java/com/android/tools/r8/shaking/ProguardNameMatcher.java
index ecd43b1..f4eea02 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardNameMatcher.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardNameMatcher.java
@@ -6,8 +6,10 @@
 import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcardsAndNegation;
 import com.android.tools.r8.shaking.ProguardWildcard.BackReference;
 import com.android.tools.r8.shaking.ProguardWildcard.Pattern;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.IterableUtils;
 import java.util.Collections;
 import java.util.List;
@@ -21,15 +23,17 @@
   private ProguardNameMatcher() {
   }
 
-  public static ProguardNameMatcher create(
-      IdentifierPatternWithWildcards identifierPatternWithWildcards) {
-    if (identifierPatternWithWildcards.isMatchAllNames()) {
-      return MATCH_ALL_NAMES;
-    } else if (identifierPatternWithWildcards.wildcards.isEmpty()) {
-      return new MatchSpecificName(identifierPatternWithWildcards.pattern);
-    } else {
-      return new MatchNamePattern(identifierPatternWithWildcards);
+  public static ProguardNameMatcher create(IdentifierPatternWithWildcardsAndNegation pattern) {
+    if (!pattern.isNegated()) {
+      IdentifierPatternWithWildcards nonNegatedPattern = pattern.getNonNegatedPattern();
+      if (nonNegatedPattern.isMatchAllNames()) {
+        return MATCH_ALL_NAMES;
+      }
+      if (nonNegatedPattern.getWildcards().isEmpty()) {
+        return new MatchSpecificName(pattern.getStringPattern());
+      }
     }
+    return new MatchNamePattern(pattern);
   }
 
   private static boolean matchFieldOrMethodNameImpl(
@@ -160,15 +164,21 @@
 
     private final String pattern;
     private final List<ProguardWildcard> wildcards;
+    private final boolean negated;
 
-    MatchNamePattern(IdentifierPatternWithWildcards identifierPatternWithWildcards) {
-      this.pattern = identifierPatternWithWildcards.pattern;
-      this.wildcards = identifierPatternWithWildcards.wildcards;
+    MatchNamePattern(IdentifierPatternWithWildcardsAndNegation pattern) {
+      this(pattern.getStringPattern(), pattern.getWildcards(), pattern.isNegated());
+    }
+
+    MatchNamePattern(String pattern, List<ProguardWildcard> wildcards, boolean negated) {
+      this.pattern = pattern;
+      this.wildcards = wildcards;
+      this.negated = negated;
     }
 
     @Override
     public boolean matches(String name) {
-      boolean matched = matchFieldOrMethodNameImpl(pattern, 0, name, 0, wildcards, 0);
+      boolean matched = matchFieldOrMethodNameImpl(pattern, 0, name, 0, wildcards, 0) != negated;
       if (!matched) {
         wildcards.forEach(ProguardWildcard::clearCaptured);
       }
@@ -185,14 +195,12 @@
     protected MatchNamePattern materialize() {
       List<ProguardWildcard> materializedWildcards =
           wildcards.stream().map(ProguardWildcard::materialize).collect(Collectors.toList());
-      IdentifierPatternWithWildcards identifierPatternWithMaterializedWildcards =
-          new IdentifierPatternWithWildcards(pattern, materializedWildcards);
-      return new MatchNamePattern(identifierPatternWithMaterializedWildcards);
+      return new MatchNamePattern(pattern, materializedWildcards, negated);
     }
 
     @Override
     public String toString() {
-      return pattern;
+      return (negated ? "!" : "") + pattern;
     }
 
     @Override
@@ -200,12 +208,16 @@
       if (this == o) {
         return true;
       }
-      return o instanceof MatchNamePattern && pattern.equals(((MatchNamePattern) o).pattern);
+      if (o == null || !(o instanceof MatchNamePattern)) {
+        return false;
+      }
+      MatchNamePattern other = (MatchNamePattern) o;
+      return pattern.equals(other.pattern) && negated == other.negated;
     }
 
     @Override
     public int hashCode() {
-      return pattern.hashCode();
+      return (pattern.hashCode() << 1) | BooleanUtils.intValue(negated);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardWildcard.java b/src/main/java/com/android/tools/r8/shaking/ProguardWildcard.java
index a672c96..814c502 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardWildcard.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardWildcard.java
@@ -36,6 +36,10 @@
       this.pattern = pattern;
     }
 
+    static Pattern create(String pattern, boolean isNonReferenceable) {
+      return isNonReferenceable ? new NonReferenceablePattern(pattern) : new Pattern(pattern);
+    }
+
     @Override
     synchronized void setCaptured(String captured) {
       this.captured = captured;
@@ -71,12 +75,28 @@
       return this;
     }
 
+    boolean isNonReferenceable() {
+      return false;
+    }
+
     @Override
     public String toString() {
       return pattern;
     }
   }
 
+  static class NonReferenceablePattern extends Pattern {
+
+    NonReferenceablePattern(String pattern) {
+      super(pattern);
+    }
+
+    @Override
+    boolean isNonReferenceable() {
+      return true;
+    }
+  }
+
   public static class BackReference extends ProguardWildcard {
     // Back-reference is not referable, hence the other type, Pattern, here.
     Pattern reference;
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepRuleNegatedNameTest.java b/src/test/java/com/android/tools/r8/shaking/KeepRuleNegatedNameTest.java
new file mode 100644
index 0000000..05762b3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/KeepRuleNegatedNameTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) 2026, 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.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
+import static com.android.tools.r8.utils.codeinspector.AssertUtils.assertFailsCompilation;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KeepRuleNegatedNameTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(Backend.DEX)
+        .addInnerClasses(getClass())
+        .addKeepRules("-keep class " + Main.class.getTypeName() + " {", "  void !foo();", "}")
+        .setMinApi(AndroidApiLevel.N)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClass = inspector.clazz(Main.class);
+              assertThat(mainClass, isPresent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("foo"), isAbsent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("bar"), isPresent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("baz"), isPresent());
+            });
+  }
+
+  @Test
+  public void testIfRule() throws Exception {
+    testForR8(Backend.DEX)
+        .addInnerClasses(getClass())
+        .addKeepRules(
+            // Keep foo.
+            "-keep class " + Main.class.getTypeName() + " {",
+            "  void foo();",
+            "}",
+            // If there exists a method not ending in "ar", then keep baz().
+            "-if class * {",
+            "  void !*ar();",
+            "}",
+            "-keep class <1> {",
+            "  void baz();",
+            "}")
+        .setMinApi(AndroidApiLevel.N)
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClass = inspector.clazz(Main.class);
+              assertThat(mainClass, isPresent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("foo"), isPresent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("bar"), isAbsent());
+              assertThat(mainClass.uniqueMethodWithOriginalName("baz"), isPresent());
+            });
+  }
+
+  @Test
+  public void testIfRuleWithBackReference() throws Exception {
+    assertFailsCompilation(
+        () ->
+            testForR8(Backend.DEX)
+                .addInnerClasses(getClass())
+                .addKeepRules(
+                    // Keep foo.
+                    "-keep class " + Main.class.getTypeName() + " {",
+                    "  void foo();",
+                    "}",
+                    // If there exists a method not ending in "ar", then keep what's by the
+                    // wildcard?!
+                    "-if class * {",
+                    "  void !*ar();",
+                    "}",
+                    "-keep class <1> {",
+                    "  void <2>();",
+                    "}")
+                .setMinApi(AndroidApiLevel.N)
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics
+                            .assertOnlyErrors()
+                            .assertErrorsMatch(
+                                allOf(
+                                    diagnosticType(StringDiagnostic.class),
+                                    diagnosticMessage(
+                                        equalTo(
+                                            "Wildcard <2> is invalid (cannot reference negated"
+                                                + " matches)."))))));
+  }
+
+  static class Main {
+
+    public static void foo() {}
+
+    public static void bar() {}
+
+    public static void baz() {}
+  }
+}
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 50465ca..f21e321 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -37,7 +37,7 @@
 import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.shaking.ProguardClassNameList.SingleClassNameList;
 import com.android.tools.r8.shaking.ProguardConfiguration.ProcessKotlinNullChecks;
-import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcardsAndNegation;
 import com.android.tools.r8.shaking.ProguardTypeMatcher.MatchSpecificType;
 import com.android.tools.r8.shaking.constructor.InitMatchingTest;
 import com.android.tools.r8.utils.AbortException;
@@ -234,8 +234,9 @@
     assertEquals("some.library.Class", rule.getInheritanceClassName().toString());
     ProguardMemberRule memberRule = rule.getMemberRules().iterator().next();
     assertTrue(memberRule.getAccessFlags().isProtected());
-    assertEquals(ProguardNameMatcher.create(
-        IdentifierPatternWithWildcards.withoutWildcards("getContents")), memberRule.getName());
+    assertEquals(
+        ProguardNameMatcher.create(new IdentifierPatternWithWildcardsAndNegation("getContents")),
+        memberRule.getName());
     assertEquals("java.lang.Object[][]", memberRule.getType().toString());
     assertEquals(ProguardMemberType.METHOD, memberRule.getRuleType());
     assertEquals(0, memberRule.getArguments().size());
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardNameMatchingTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardNameMatchingTest.java
index 5163bbd..673ba91 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardNameMatchingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardNameMatchingTest.java
@@ -7,7 +7,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcards;
+import com.android.tools.r8.shaking.ProguardConfigurationParser.IdentifierPatternWithWildcardsAndNegation;
 import com.android.tools.r8.shaking.ProguardTypeMatcher.ClassOrType;
 import com.android.tools.r8.shaking.ProguardWildcard.BackReference;
 import com.android.tools.r8.shaking.ProguardWildcard.Pattern;
@@ -27,7 +27,9 @@
 
   private static boolean matchTypeName(String typeName, String pattern) {
     return ProguardTypeMatcher.create(
-        toIdentifierPatternWithWildCards(pattern, false), ClassOrType.TYPE, dexItemFactory)
+            toIdentifierPatternWithWildcards(pattern, false).getNonNegatedPattern(),
+            ClassOrType.TYPE,
+            dexItemFactory)
         .matches(dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptor(typeName)));
   }
 
@@ -42,10 +44,12 @@
       for (String pattern : patterns) {
         boolean isNegated = pattern.startsWith("!");
         String actualPattern = isNegated ? pattern.substring(1) : pattern;
-        listBuilder.addClassName(isNegated,
+        listBuilder.addClassName(
+            isNegated,
             ProguardTypeMatcher.create(
-                toIdentifierPatternWithWildCards(actualPattern, false),
-                ClassOrType.CLASS, dexItemFactory));
+                toIdentifierPatternWithWildcards(actualPattern, false).getNonNegatedPattern(),
+                ClassOrType.CLASS,
+                dexItemFactory));
       }
       builder.addPattern(listBuilder.build());
     }
@@ -55,7 +59,7 @@
 
   private static boolean matchMemberName(String pattern, String memberName) {
     ProguardNameMatcher nameMatcher =
-        ProguardNameMatcher.create(toIdentifierPatternWithWildCards(pattern, true));
+        ProguardNameMatcher.create(toIdentifierPatternWithWildcards(pattern, true));
     return nameMatcher.matches(memberName);
   }
 
@@ -178,7 +182,7 @@
     assertFalse(matchMemberName("*foo<1>", "barfoobaz"));
   }
 
-  private static IdentifierPatternWithWildcards toIdentifierPatternWithWildCards(
+  private static IdentifierPatternWithWildcardsAndNegation toIdentifierPatternWithWildcards(
       String pattern, boolean isForNameMatcher) {
     ImmutableList.Builder<ProguardWildcard> builder = ImmutableList.builder();
     String allPattern = "";
@@ -222,7 +226,7 @@
     }
     List<ProguardWildcard> wildcards = builder.build();
     linkBackReferences(wildcards);
-    return new IdentifierPatternWithWildcards(pattern, wildcards);
+    return new IdentifierPatternWithWildcardsAndNegation(pattern, wildcards);
   }
 
   private static void linkBackReferences(Iterable<ProguardWildcard> wildcards) {