Version 1.4.86

Cherry pick: New implementation of -keeppackagenames
CL: https://r8-review.googlesource.com/c/r8/+/36944

Cherry pick: Reproduce b/130135768: keeppackagenames with asterisks
CL: https://r8-review.googlesource.com/c/r8/+/36860

Bug: 130135768
Change-Id: I8a340afe4811ec3f8826956c99b94346f4c2c8b4
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index f7bc09f..99999c6 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.85";
+  public static final String LABEL = "1.4.86";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 9368a79..9a8513b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -335,6 +335,14 @@
     return descriptor.toString();
   }
 
+  public String toBinaryName() {
+    String descriptor = toDescriptorString();
+    assert descriptor.length() > 1
+        && descriptor.charAt(0) == 'L'
+        && descriptor.charAt(descriptor.length() - 1) == ';';
+    return descriptor.substring(1, descriptor.length() - 1);
+  }
+
   @Override
   public String toSourceString() {
     if (toStringCache == null) {
@@ -682,4 +690,8 @@
     }
     return lubType;
   }
+
+  public String getPackageName() {
+    return DescriptorUtils.getPackageNameFromBinaryName(toBinaryName());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index d7426fc..2634ca4 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -21,11 +21,11 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.ProguardPackageNameList;
 import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
-import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableMap;
@@ -44,7 +44,6 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final AppInfoWithLiveness appInfo;
-  private final Reporter reporter;
   private final PackageObfuscationMode packageObfuscationMode;
   private final boolean isAccessModificationAllowed;
   private final Set<String> noObfuscationPrefixes = Sets.newHashSet();
@@ -58,7 +57,6 @@
   private final boolean keepInnerClassStructure;
 
   private final Set<DexType> noObfuscationTypes;
-  private final Set<DexType> keepPackageName;
 
   private final Namespace topLevelState;
 
@@ -66,7 +64,6 @@
     this.appView = appView;
     this.appInfo = appView.appInfo();
     InternalOptions options = appView.options();
-    this.reporter = options.reporter;
     this.packageObfuscationMode = options.getProguardConfiguration().getPackageObfuscationMode();
     this.isAccessModificationAllowed =
         options.getProguardConfiguration().isAccessModificationAllowed();
@@ -74,11 +71,7 @@
     this.classDictionary = options.getProguardConfiguration().getClassObfuscationDictionary();
     this.keepInnerClassStructure = options.getProguardConfiguration().getKeepAttributes().signature;
     this.noObfuscationTypes =
-        DexReference.filterDexType(rootSet.noObfuscation.stream())
-            .collect(Collectors.toSet());
-    this.keepPackageName =
-        DexReference.filterDexType(rootSet.keepPackageName.stream())
-            .collect(Collectors.toSet());
+        DexReference.filterDexType(rootSet.noObfuscation.stream()).collect(Collectors.toSet());
 
     // Initialize top-level naming state.
     topLevelState = new Namespace(
@@ -257,8 +250,9 @@
     String packageName = getPackageBinaryNameFromJavaType(type.getPackageDescriptor());
     // Check whether the given class should be kept.
     // or check whether the given class belongs to a package that is kept for another class.
-    if (keepPackageName.contains(type)
-        || noObfuscationPrefixes.contains(packageName)) {
+    ProguardPackageNameList keepPackageNames =
+        appView.options().getProguardConfiguration().getKeepPackageNamesPatterns();
+    if (noObfuscationPrefixes.contains(packageName) || keepPackageNames.matches(type)) {
       return states.computeIfAbsent(packageName, Namespace::new);
     }
     Namespace state = topLevelState;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index a767c86..ec1d9ba 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -42,6 +42,8 @@
     private boolean verbose;
     private String renameSourceFileAttribute;
     private final List<String> keepAttributePatterns = new ArrayList<>();
+    private final ProguardPackageNameList.Builder keepPackageNamesPatterns =
+        ProguardPackageNameList.builder();
     private final ProguardClassFilter.Builder dontWarnPatterns = ProguardClassFilter.builder();
     private final ProguardClassFilter.Builder dontNotePatterns = ProguardClassFilter.builder();
     protected final Set<ProguardConfigurationRule> rules = Sets.newLinkedHashSet();
@@ -181,6 +183,10 @@
       this.rules.add(rule);
     }
 
+    public void addKeepPackageNamesPattern(boolean isNegated, ProguardPackageMatcher pattern) {
+      keepPackageNamesPatterns.addPackageName(isNegated, pattern);
+    }
+
     public void addDontWarnPattern(ProguardClassNameList pattern) {
       dontWarnPatterns.addPattern(pattern);
     }
@@ -292,43 +298,45 @@
     }
 
     public ProguardConfiguration buildRaw() {
-      ProguardConfiguration configuration = new ProguardConfiguration(
-          String.join(System.lineSeparator(), parsedConfiguration),
-          dexItemFactory,
-          injars,
-          libraryjars,
-          packageObfuscationMode,
-          packagePrefix,
-          allowAccessModification,
-          ignoreWarnings,
-          optimizing,
-          obfuscating,
-          shrinking,
-          printConfiguration,
-          printConfigurationFile,
-          printUsage,
-          printUsageFile,
-          printMapping,
-          printMappingFile,
-          applyMappingFile,
-          verbose,
-          renameSourceFileAttribute,
-          ProguardKeepAttributes.fromPatterns(keepAttributePatterns),
-          dontWarnPatterns.build(),
-          dontNotePatterns.build(),
-          rules,
-          printSeeds,
-          seedFile,
-          overloadAggressively,
-          DictionaryReader.readAllNames(obfuscationDictionary, reporter),
-          DictionaryReader.readAllNames(classObfuscationDictionary, reporter),
-          DictionaryReader.readAllNames(packageObfuscationDictionary, reporter),
-          useUniqueClassMemberNames,
-          keepParameterNames,
-          adaptClassStrings.build(),
-          adaptResourceFilenames.build(),
-          adaptResourceFileContents.build(),
-          keepDirectories.build());
+      ProguardConfiguration configuration =
+          new ProguardConfiguration(
+              String.join(System.lineSeparator(), parsedConfiguration),
+              dexItemFactory,
+              injars,
+              libraryjars,
+              packageObfuscationMode,
+              packagePrefix,
+              allowAccessModification,
+              ignoreWarnings,
+              optimizing,
+              obfuscating,
+              shrinking,
+              printConfiguration,
+              printConfigurationFile,
+              printUsage,
+              printUsageFile,
+              printMapping,
+              printMappingFile,
+              applyMappingFile,
+              verbose,
+              renameSourceFileAttribute,
+              ProguardKeepAttributes.fromPatterns(keepAttributePatterns),
+              keepPackageNamesPatterns.build(),
+              dontWarnPatterns.build(),
+              dontNotePatterns.build(),
+              rules,
+              printSeeds,
+              seedFile,
+              overloadAggressively,
+              DictionaryReader.readAllNames(obfuscationDictionary, reporter),
+              DictionaryReader.readAllNames(classObfuscationDictionary, reporter),
+              DictionaryReader.readAllNames(packageObfuscationDictionary, reporter),
+              useUniqueClassMemberNames,
+              keepParameterNames,
+              adaptClassStrings.build(),
+              adaptResourceFilenames.build(),
+              adaptResourceFileContents.build(),
+              keepDirectories.build());
 
       reporter.failIfPendingErrors();
 
@@ -380,6 +388,7 @@
   private final boolean verbose;
   private final String renameSourceFileAttribute;
   private final ProguardKeepAttributes keepAttributes;
+  private final ProguardPackageNameList keepPackageNamesPatterns;
   private final ProguardClassFilter dontWarnPatterns;
   private final ProguardClassFilter dontNotePatterns;
   protected final ImmutableList<ProguardConfigurationRule> rules;
@@ -418,6 +427,7 @@
       boolean verbose,
       String renameSourceFileAttribute,
       ProguardKeepAttributes keepAttributes,
+      ProguardPackageNameList keepPackageNamesPatterns,
       ProguardClassFilter dontWarnPatterns,
       ProguardClassFilter dontNotePatterns,
       Set<ProguardConfigurationRule> rules,
@@ -454,6 +464,7 @@
     this.verbose = verbose;
     this.renameSourceFileAttribute = renameSourceFileAttribute;
     this.keepAttributes = keepAttributes;
+    this.keepPackageNamesPatterns = keepPackageNamesPatterns;
     this.dontWarnPatterns = dontWarnPatterns;
     this.dontNotePatterns = dontNotePatterns;
     this.rules = ImmutableList.copyOf(rules);
@@ -567,6 +578,10 @@
     return keepAttributes;
   }
 
+  public ProguardPackageNameList getKeepPackageNamesPatterns() {
+    return keepPackageNamesPatterns;
+  }
+
   public ProguardClassFilter getDontWarnPatterns() {
     return dontWarnPatterns;
   }
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 991c763..10d3695 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -38,6 +38,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -178,6 +179,7 @@
   }
 
   private enum IdentifierType {
+    PACKAGE_NAME,
     CLASS_NAME,
     ANY
   }
@@ -232,8 +234,7 @@
       } else if (acceptString("keepattributes")) {
         parseKeepAttributes();
       } else if (acceptString("keeppackagenames")) {
-        ProguardKeepPackageNamesRule rule = parseKeepPackageNamesRule(optionStart);
-        configurationBuilder.addRule(rule);
+        parsePackageFilter(configurationBuilder::addKeepPackageNamesPattern);
       } else if (acceptString("keepparameternames")) {
         configurationBuilder.setKeepParameterNames(true, origin, getPosition(optionStart));
       } else if (acceptString("checkdiscard")) {
@@ -610,17 +611,6 @@
       return keepRuleBuilder.build();
     }
 
-    private ProguardKeepPackageNamesRule parseKeepPackageNamesRule(Position start)
-        throws ProguardRuleParserException {
-      ProguardKeepPackageNamesRule.Builder keepRuleBuilder =
-          ProguardKeepPackageNamesRule.builder().setOrigin(origin).setStart(start);
-      keepRuleBuilder.setClassNames(parseClassNames());
-      Position end = getPosition();
-      keepRuleBuilder.setSource(getSourceSnippet(contents, start, end));
-      keepRuleBuilder.setEnd(end);
-      return keepRuleBuilder.build();
-    }
-
     private ProguardCheckDiscardRule parseCheckDiscardRule(Position start)
         throws ProguardRuleParserException {
       ProguardCheckDiscardRule.Builder keepRuleBuilder = ProguardCheckDiscardRule.builder()
@@ -1441,6 +1431,13 @@
                 || codePoint == '['
                 || codePoint == ']';
 
+    private final Predicate<Integer> PACKAGE_NAME_PREDICATE =
+        codePoint ->
+            IdentifierUtils.isDexIdentifierPart(codePoint)
+                || codePoint == '.'
+                || codePoint == '*'
+                || codePoint == '?';
+
     private String acceptClassName() {
       return acceptString(CLASS_NAME_PREDICATE);
     }
@@ -1539,9 +1536,11 @@
         } else if (current == '?' || current == '%') {
           wildcardsCollector.add(new ProguardWildcard.Pattern(String.valueOf((char) current)));
           end += Character.charCount(current);
-        } else if (CLASS_NAME_PREDICATE.test(current) || current == '>') {
+        } else if (kind == IdentifierType.PACKAGE_NAME
+            ? PACKAGE_NAME_PREDICATE.test(current)
+            : (CLASS_NAME_PREDICATE.test(current) || current == '>')) {
           end += Character.charCount(current);
-        } else if (current == '<') {
+        } else if (kind != IdentifierType.PACKAGE_NAME && current == '<') {
           currentBackreference = new StringBuilder();
           end += Character.charCount(current);
         } else {
@@ -1651,6 +1650,25 @@
       }
     }
 
+    private void parsePackageFilter(BiConsumer<Boolean, ProguardPackageMatcher> consumer)
+        throws ProguardRuleParserException {
+      skipWhitespace();
+      if (isOptionalArgumentGiven()) {
+        do {
+          IdentifierPatternWithWildcardsAndNegation name =
+              acceptIdentifierWithBackreference(IdentifierType.PACKAGE_NAME, true);
+          if (name == null) {
+            throw parseError("Package name expected");
+          }
+          consumer.accept(
+              name.negated, new ProguardPackageMatcher(name.patternWithWildcards.pattern));
+          skipWhitespace();
+        } while (acceptChar(','));
+      } else {
+        consumer.accept(false, new ProguardPackageMatcher("**"));
+      }
+    }
+
     private void parseClassFilter(Consumer<ProguardClassNameList> consumer)
         throws ProguardRuleParserException {
       skipWhitespace();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepPackageNamesRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepPackageNamesRule.java
deleted file mode 100644
index 767ecf7..0000000
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepPackageNamesRule.java
+++ /dev/null
@@ -1,59 +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.shaking;
-
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.position.Position;
-import java.util.List;
-
-public class ProguardKeepPackageNamesRule extends ProguardConfigurationRule {
-
-  public static class Builder
-      extends ProguardConfigurationRule.Builder<ProguardKeepPackageNamesRule, Builder> {
-
-    private Builder() {
-      super();
-    }
-
-    @Override
-    public Builder self() {
-      return this;
-    }
-
-    @Override
-    public ProguardKeepPackageNamesRule build() {
-      return new ProguardKeepPackageNamesRule(origin, getPosition(), source, classAnnotation,
-          classAccessFlags, negatedClassAccessFlags, classTypeNegated, classType, classNames,
-          inheritanceAnnotation, inheritanceClassName, inheritanceIsExtends, memberRules);
-    }
-  }
-
-  private ProguardKeepPackageNamesRule(
-      Origin origin,
-      Position position,
-      String source,
-      ProguardTypeMatcher classAnnotation,
-      ProguardAccessFlags classAccessFlags,
-      ProguardAccessFlags negatedClassAccessFlags,
-      boolean classTypeNegated,
-      ProguardClassType classType,
-      ProguardClassNameList classNames,
-      ProguardTypeMatcher inheritanceAnnotation,
-      ProguardTypeMatcher inheritanceClassName,
-      boolean inheritanceIsExtends,
-      List<ProguardMemberRule> memberRules) {
-    super(origin, position, source, classAnnotation, classAccessFlags, negatedClassAccessFlags,
-        classTypeNegated, classType, classNames, inheritanceAnnotation, inheritanceClassName,
-        inheritanceIsExtends, memberRules);
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  @Override
-  String typeString() {
-    return "keeppackagenames";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardPackageMatcher.java b/src/main/java/com/android/tools/r8/shaking/ProguardPackageMatcher.java
new file mode 100644
index 0000000..48c4703
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardPackageMatcher.java
@@ -0,0 +1,96 @@
+// 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;
+
+import com.android.tools.r8.graph.DexType;
+
+public class ProguardPackageMatcher {
+  private final String pattern;
+
+  public ProguardPackageMatcher(String pattern) {
+    this.pattern = pattern;
+  }
+
+  public boolean matches(DexType type) {
+    return matchPackageNameImpl(pattern, 0, type.getPackageName(), 0);
+  }
+
+  private static boolean matchPackageNameImpl(
+      String pattern, int patternIndex, String name, int nameIndex) {
+    for (int i = patternIndex; i < pattern.length(); i++) {
+      char patternChar = pattern.charAt(i);
+      switch (patternChar) {
+        case '*':
+          int nextPatternIndex = i + 1;
+          // Check for **.
+          boolean includeSeparators =
+              pattern.length() > (nextPatternIndex) && pattern.charAt(nextPatternIndex) == '*';
+          if (includeSeparators) {
+            nextPatternIndex += 1;
+          }
+
+          // Fast cases for the common case where a pattern ends with '*' or '**'.
+          if (nextPatternIndex == pattern.length()) {
+            if (includeSeparators) {
+              return true;
+            }
+            boolean hasSeparators = containsSeparatorsStartingAt(name, nameIndex);
+            return !hasSeparators;
+          }
+
+          // Match the rest of the pattern against the (non-empty) rest of the class name.
+          for (int nextNameIndex = nameIndex; nextNameIndex < name.length(); nextNameIndex++) {
+            if (!includeSeparators) {
+              // Stop at the first separator for just *.
+              if (name.charAt(nextNameIndex) == '.') {
+                return matchPackageNameImpl(pattern, nextPatternIndex, name, nextNameIndex);
+              }
+            }
+            if (matchPackageNameImpl(pattern, nextPatternIndex, name, nextNameIndex)) {
+              return true;
+            }
+          }
+
+          // Finally, check the case where the '*' or '**' eats all of the package name.
+          return matchPackageNameImpl(pattern, nextPatternIndex, name, name.length());
+
+        case '?':
+          if (nameIndex == name.length() || name.charAt(nameIndex) == '.') {
+            return false;
+          }
+          nameIndex++;
+          break;
+
+        default:
+          if (nameIndex == name.length() || patternChar != name.charAt(nameIndex++)) {
+            return false;
+          }
+          break;
+      }
+    }
+    return nameIndex == name.length();
+  }
+
+  private static boolean containsSeparatorsStartingAt(String className, int nameIndex) {
+    return className.indexOf('.', nameIndex) != -1;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof ProguardPackageMatcher)) {
+      return false;
+    }
+    ProguardPackageMatcher other = (ProguardPackageMatcher) o;
+    return pattern.equals(other.pattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return pattern.hashCode();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardPackageNameList.java b/src/main/java/com/android/tools/r8/shaking/ProguardPackageNameList.java
new file mode 100644
index 0000000..6d45459
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardPackageNameList.java
@@ -0,0 +1,107 @@
+// 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;
+
+import com.android.tools.r8.graph.DexType;
+import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
+import it.unimi.dsi.fastutil.objects.ObjectIterator;
+
+public class ProguardPackageNameList {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+
+    /** Map used to store pairs of patterns and whether they are negated. */
+    private final Object2BooleanMap<ProguardPackageMatcher> matchers =
+        new Object2BooleanArrayMap<>();
+
+    private Builder() {}
+
+    public ProguardPackageNameList.Builder addPackageName(
+        boolean isNegated, ProguardPackageMatcher className) {
+      matchers.put(className, isNegated);
+      return this;
+    }
+
+    ProguardPackageNameList build() {
+      return new ProguardPackageNameList(matchers);
+    }
+  }
+
+  private final Object2BooleanMap<ProguardPackageMatcher> packageNames;
+
+  private ProguardPackageNameList(Object2BooleanMap<ProguardPackageMatcher> pacakgeNames) {
+    this.packageNames = pacakgeNames;
+  }
+
+  public void writeTo(StringBuilder builder) {
+    boolean first = true;
+    for (Object2BooleanMap.Entry<ProguardPackageMatcher> packageName :
+        packageNames.object2BooleanEntrySet()) {
+      if (!first) {
+        builder.append(',');
+      }
+      if (packageName.getBooleanValue()) {
+        builder.append('!');
+      }
+      builder.append(packageName.getKey().toString());
+      first = false;
+    }
+  }
+
+  public boolean matches(DexType type) {
+    for (Object2BooleanMap.Entry<ProguardPackageMatcher> packageName :
+        packageNames.object2BooleanEntrySet()) {
+      if (packageName.getKey().matches(type)) {
+        // If we match a negation, abort as non-match. If we match a positive, return true.
+        return !packageName.getBooleanValue();
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof ProguardPackageNameList)) {
+      return false;
+    }
+    ProguardPackageNameList other = (ProguardPackageNameList) o;
+    if (packageNames.size() != other.packageNames.size()) {
+      return false;
+    }
+    ObjectIterator<Object2BooleanMap.Entry<ProguardPackageMatcher>> i1 =
+        packageNames.object2BooleanEntrySet().iterator();
+    ObjectIterator<Object2BooleanMap.Entry<ProguardPackageMatcher>> i2 =
+        other.packageNames.object2BooleanEntrySet().iterator();
+    while (i1.hasNext()) {
+      Object2BooleanMap.Entry<ProguardPackageMatcher> e1 = i1.next();
+      Object2BooleanMap.Entry<ProguardPackageMatcher> e2 = i2.next();
+      if (!e1.equals(e2)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int hash = 0;
+    ObjectIterator<Object2BooleanMap.Entry<ProguardPackageMatcher>> iterator =
+        packageNames.object2BooleanEntrySet().iterator();
+    while (iterator.hasNext()) {
+      Object2BooleanMap.Entry<ProguardPackageMatcher> packageMatcher = iterator.next();
+      hash = hash * (packageMatcher.getBooleanValue() ? 1 : 2);
+      hash = hash * 13 + packageMatcher.getKey().hashCode();
+    }
+    return hash;
+  }
+}
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 a0c9735..4ce20ba 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -69,7 +69,6 @@
   private final Set<DexReference> noOptimization = Sets.newIdentityHashSet();
   private final Set<DexReference> noObfuscation = Sets.newIdentityHashSet();
   private final LinkedHashMap<DexReference, DexReference> reasonAsked = new LinkedHashMap<>();
-  private final Set<DexReference> keepPackageName = Sets.newIdentityHashSet();
   private final Set<ProguardConfigurationRule> rulesThatUseExtendsOrImplementsWrong =
       Sets.newIdentityHashSet();
   private final Set<DexReference> checkDiscarded = Sets.newIdentityHashSet();
@@ -179,8 +178,7 @@
           markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
           markMatchingMethods(clazz, memberKeepRules, rule, preconditionSupplier);
         }
-      } else if (rule instanceof ProguardWhyAreYouKeepingRule
-          || rule instanceof ProguardKeepPackageNamesRule) {
+      } else if (rule instanceof ProguardWhyAreYouKeepingRule) {
         markClass(clazz, rule);
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null, true);
         markMatchingVisibleFields(clazz, memberKeepRules, rule, null, true);
@@ -264,7 +262,6 @@
         noOptimization,
         noObfuscation,
         ImmutableList.copyOf(reasonAsked.values()),
-        keepPackageName,
         checkDiscarded,
         alwaysInline,
         forceInline,
@@ -946,8 +943,6 @@
       noSideEffects.put(item.toReference(), rule);
     } else if (context instanceof ProguardWhyAreYouKeepingRule) {
       reasonAsked.computeIfAbsent(item.toReference(), i -> i);
-    } else if (context instanceof ProguardKeepPackageNamesRule) {
-      keepPackageName.add(item.toReference());
     } else if (context instanceof ProguardAssumeValuesRule) {
       assumedValues.put(item.toReference(), rule);
     } else if (context instanceof ProguardCheckDiscardRule) {
@@ -1011,7 +1006,6 @@
     public final Set<DexReference> noOptimization;
     public final Set<DexReference> noObfuscation;
     public final ImmutableList<DexReference> reasonAsked;
-    public final Set<DexReference> keepPackageName;
     public final Set<DexReference> checkDiscarded;
     public final Set<DexMethod> alwaysInline;
     public final Set<DexMethod> forceInline;
@@ -1032,7 +1026,6 @@
         Set<DexReference> noOptimization,
         Set<DexReference> noObfuscation,
         ImmutableList<DexReference> reasonAsked,
-        Set<DexReference> keepPackageName,
         Set<DexReference> checkDiscarded,
         Set<DexMethod> alwaysInline,
         Set<DexMethod> forceInline,
@@ -1050,7 +1043,6 @@
       this.noOptimization = noOptimization;
       this.noObfuscation = noObfuscation;
       this.reasonAsked = reasonAsked;
-      this.keepPackageName = Collections.unmodifiableSet(keepPackageName);
       this.checkDiscarded = Collections.unmodifiableSet(checkDiscarded);
       this.alwaysInline = Collections.unmodifiableSet(alwaysInline);
       this.forceInline = Collections.unmodifiableSet(forceInline);
@@ -1071,7 +1063,6 @@
       this.noOptimization = lense.rewriteMutableReferencesConservatively(previous.noOptimization);
       this.noObfuscation = lense.rewriteMutableReferencesConservatively(previous.noObfuscation);
       this.reasonAsked = lense.rewriteReferencesConservatively(previous.reasonAsked);
-      this.keepPackageName = lense.rewriteReferencesConservatively(previous.keepPackageName);
       this.checkDiscarded = lense.rewriteReferencesConservatively(previous.checkDiscarded);
       this.alwaysInline = lense.rewriteMethodsConservatively(previous.alwaysInline);
       this.forceInline = lense.rewriteMethodsConservatively(previous.forceInline);
@@ -1310,7 +1301,6 @@
       builder.append("\nnoOptimization: " + noOptimization.size());
       builder.append("\nnoObfuscation: " + noObfuscation.size());
       builder.append("\nreasonAsked: " + reasonAsked.size());
-      builder.append("\nkeepPackageName: " + keepPackageName.size());
       builder.append("\ncheckDiscarded: " + checkDiscarded.size());
       builder.append("\nnoSideEffects: " + noSideEffects.size());
       builder.append("\nassumedValues: " + assumedValues.size());
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 62b2b65..0047eec 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -64,6 +64,10 @@
     return addKeepRules("-keep class ** { *; }");
   }
 
+  public T addKeepAndMinifyAllClassesRule() {
+    return addKeepRules("-keep,allowobfuscation class ** { *; }");
+  }
+
   public T addKeepAllInterfacesRule() {
     return addKeepRules("-keep interface ** { *; }");
   }
diff --git a/src/test/java/com/android/tools/r8/naming/KeepPackageNamesTest.java b/src/test/java/com/android/tools/r8/naming/KeepPackageNamesTest.java
new file mode 100644
index 0000000..e57c596
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/KeepPackageNamesTest.java
@@ -0,0 +1,103 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.naming;
+
+import static com.android.tools.r8.utils.DescriptorUtils.getPackageNameFromDescriptor;
+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.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.naming.keeppackagenames.Top;
+import com.android.tools.r8.naming.keeppackagenames.sub.SubClass;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class KeepPackageNamesTest extends TestBase {
+  private static final String PACKAGE_NAME = Top.class.getPackage().getName();
+  private static final List<Class<?>> CLASSES = ImmutableList.of(Top.class, SubClass.class);
+
+  enum TestConfig {
+    SINGLE_ASTERISK,
+    DOUBLE_ASTERISKS;
+
+    public String getKeepRule() {
+      switch (this) {
+        case SINGLE_ASTERISK:
+          return "-keeppackagenames com.android.tools.r8.naming.keeppackage*";
+        case DOUBLE_ASTERISKS:
+          return "-keeppackagenames com.android.tools.r8.naming.keeppackage**";
+      }
+      throw new Unreachable();
+    }
+
+    public void inspect(CodeInspector inspector) {
+      ClassSubject top = inspector.clazz(Top.class);
+      assertThat(top, isPresent());
+      assertThat(top, isRenamed());
+      assertEquals(
+          getPackageNameFromDescriptor(top.getOriginalDescriptor()),
+          getPackageNameFromDescriptor(top.getFinalDescriptor()));
+
+      ClassSubject sub = inspector.clazz(SubClass.class);
+      assertThat(sub, isPresent());
+      assertThat(sub, isRenamed());
+      switch (this) {
+        case SINGLE_ASTERISK:
+          assertNotEquals(
+              getPackageNameFromDescriptor(sub.getOriginalDescriptor()),
+              getPackageNameFromDescriptor(sub.getFinalDescriptor()));
+          assertThat(
+              getPackageNameFromDescriptor(sub.getFinalDescriptor()), containsString(PACKAGE_NAME));
+          break;
+        case DOUBLE_ASTERISKS:
+          assertEquals(
+              getPackageNameFromDescriptor(sub.getOriginalDescriptor()),
+              getPackageNameFromDescriptor(sub.getFinalDescriptor()));
+          break;
+      }
+    }
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static Object[] parameters() {
+    return TestConfig.values();
+  }
+
+  private final TestConfig config;
+
+  public KeepPackageNamesTest(TestConfig config) {
+    this.config = config;
+  }
+
+  @Test
+  public void testProguard() throws Exception {
+    testForProguard()
+        .addProgramClasses(CLASSES)
+        .addKeepAndMinifyAllClassesRule()
+        .addKeepRules(config.getKeepRule())
+        .compile()
+        .inspect(config::inspect);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(Backend.DEX)
+        .addProgramClasses(CLASSES)
+        .addKeepAndMinifyAllClassesRule()
+        .addKeepRules(config.getKeepRule())
+        .compile()
+        .inspect(config::inspect);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/keeppackagenames/Top.java b/src/test/java/com/android/tools/r8/naming/keeppackagenames/Top.java
new file mode 100644
index 0000000..0d156b4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/keeppackagenames/Top.java
@@ -0,0 +1,6 @@
+// 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.naming.keeppackagenames;
+
+public class Top {}
diff --git a/src/test/java/com/android/tools/r8/naming/keeppackagenames/sub/SubClass.java b/src/test/java/com/android/tools/r8/naming/keeppackagenames/sub/SubClass.java
new file mode 100644
index 0000000..2497a53
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/keeppackagenames/sub/SubClass.java
@@ -0,0 +1,6 @@
+// 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.naming.keeppackagenames.sub;
+
+public class SubClass {}
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 d82aef4..7516576 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1304,6 +1304,51 @@
     testKeepattributes(expected, config);
   }
 
+  private void testKeeppackagenames(ProguardPackageNameList expected, String config) {
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(new DexItemFactory(), reporter);
+    parser.parse(createConfigurationForTesting(ImmutableList.of(config)));
+    verifyParserEndsCleanly();
+    assertEquals(expected, parser.getConfigRawForTesting().getKeepPackageNamesPatterns());
+  }
+
+  @Test
+  public void parseKeeppackagenames() {
+    ProguardPackageNameList xxxYYY =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("xxx"))
+            .addPackageName(false, new ProguardPackageMatcher("yyy"))
+            .build();
+    testKeeppackagenames(xxxYYY, "-keeppackagenames xxx,yyy");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames xxx, yyy");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames xxx ,yyy");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames xxx   ,   yyy");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames       xxx   ,   yyy     ");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames       xxx   ,   yyy     \n");
+    testKeeppackagenames(xxxYYY, "-keeppackagenames \"xxx\",\"yyy\"");
+
+    testKeeppackagenames(
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.**"))
+            .addPackageName(false, new ProguardPackageMatcher("org.*"))
+            .build(),
+        "-keeppackagenames com.**, org.*");
+
+    testKeeppackagenames(
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("c?m.**"))
+            .addPackageName(false, new ProguardPackageMatcher("?r?.*"))
+            .build(),
+        "-keeppackagenames c?m.**, ?r?.*");
+
+    testKeeppackagenames(
+        ProguardPackageNameList.builder()
+            .addPackageName(true, new ProguardPackageMatcher("c?m.**"))
+            .addPackageName(true, new ProguardPackageMatcher("?r?.*"))
+            .build(),
+        "-keeppackagenames !c?m.**, !?r?.*");
+  }
+
   @Test
   public void parseInvalidKeepattributes() throws Exception {
     try {
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardPackageNameMatcherTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardPackageNameMatcherTest.java
new file mode 100644
index 0000000..d730c34
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardPackageNameMatcherTest.java
@@ -0,0 +1,146 @@
+// 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;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import org.junit.Test;
+
+public class ProguardPackageNameMatcherTest {
+  private DexItemFactory dexItemFactory = new DexItemFactory();
+
+  private boolean matches(ProguardPackageNameList matcher, String packageName) {
+    return matcher.matches(dexItemFactory.createType("L" + packageName.replace('.', '/') + "/A;"));
+  }
+
+  @Test
+  public void testSimple() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertFalse(matches(matcher, "com.exampl"));
+    assertFalse(matches(matcher, "com.example.a"));
+  }
+
+  @Test
+  public void testSingleEnd() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.example*"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "com.example1"));
+    assertTrue(matches(matcher, "com.example2"));
+    assertFalse(matches(matcher, "com.exampl"));
+    assertFalse(matches(matcher, "com.example.a"));
+  }
+
+  @Test
+  public void testSingleBeginning() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("*.example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "org.example"));
+    assertFalse(matches(matcher, "com.example1"));
+    assertFalse(matches(matcher, "org.example1"));
+    assertFalse(matches(matcher, "com.exampl"));
+    assertFalse(matches(matcher, "com.example.a"));
+  }
+
+  @Test
+  public void testDoubleEnd() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.example**"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "com.example1"));
+    assertTrue(matches(matcher, "com.example2"));
+    assertTrue(matches(matcher, "com.example.a"));
+    assertTrue(matches(matcher, "com.example.a.a"));
+    assertFalse(matches(matcher, "com.exampl"));
+  }
+
+  @Test
+  public void testDoubleBeginning() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("**example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "org.example"));
+    assertTrue(matches(matcher, "com.a.example"));
+    assertTrue(matches(matcher, "com.a.a.example"));
+    assertTrue(matches(matcher, "comexample"));
+    assertFalse(matches(matcher, "com.example1"));
+  }
+
+  @Test
+  public void testQuestionMark() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.e?ample"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "com.eyample"));
+    assertFalse(matches(matcher, "com.example1"));
+    assertFalse(matches(matcher, "com.example.a"));
+  }
+
+  @Test
+  public void testList() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("com.example"))
+            .addPackageName(false, new ProguardPackageMatcher("org.example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "org.example"));
+    assertFalse(matches(matcher, "com.example1"));
+    assertFalse(matches(matcher, "com.example.a"));
+    assertFalse(matches(matcher, "org.example1"));
+    assertFalse(matches(matcher, "org.example.a"));
+  }
+
+  @Test
+  public void testListNegation() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(true, new ProguardPackageMatcher("!org.example"))
+            .addPackageName(false, new ProguardPackageMatcher("*.example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    assertTrue(matches(matcher, "org.example"));
+  }
+
+  @Test
+  public void testListNegationNotMatched() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(false, new ProguardPackageMatcher("*.example"))
+            .addPackageName(true, new ProguardPackageMatcher("!org.example"))
+            .build();
+    assertTrue(matches(matcher, "com.example"));
+    // Negations only stops attempts on subsequent names.
+    assertTrue(matches(matcher, "org.example"));
+  }
+
+  @Test
+  public void testNegateAll() {
+    ProguardPackageNameList matcher =
+        ProguardPackageNameList.builder()
+            .addPackageName(true, new ProguardPackageMatcher("!**"))
+            .build();
+    assertFalse(matches(matcher, "com"));
+    assertFalse(matches(matcher, "com.example"));
+    assertFalse(matches(matcher, "com.example.a"));
+  }
+}