Merge "Add parsing of more Proguard options"
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 2a31604a..48ea17d 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -54,6 +54,10 @@
     private Origin keepParameterNamesOptionOrigin;
     private Position keepParameterNamesOptionPosition;
     private final ProguardClassFilter.Builder adaptClassStrings = ProguardClassFilter.builder();
+    private final ProguardPathFilter.Builder adaptResourceFilenames = ProguardPathFilter.builder();
+    private final ProguardPathFilter.Builder adaptResourceFilecontents =
+        ProguardPathFilter.builder();
+    private final ProguardPathFilter.Builder keepDirectories = ProguardPathFilter.builder();
     private boolean forceProguardCompatibility = false;
     private boolean overloadAggressively;
 
@@ -222,6 +226,18 @@
       adaptClassStrings.addPattern(pattern);
     }
 
+    public void addAdaptResourceFilenames(ProguardPathList pattern) {
+      adaptResourceFilenames.addPattern(pattern);
+    }
+
+    public void addAdaptResourceFilecontents(ProguardPathList pattern) {
+      adaptResourceFilecontents.addPattern(pattern);
+    }
+
+    public void addKeepDirectories(ProguardPathList pattern) {
+      keepDirectories.addPattern(pattern);
+    }
+
     public void setForceProguardCompatibility(boolean forceProguardCompatibility) {
       this.forceProguardCompatibility = forceProguardCompatibility;
     }
@@ -264,7 +280,10 @@
           DictionaryReader.readAllNames(packageObfuscationDictionary, reporter),
           useUniqueClassMemberNames,
           keepParameterNames,
-          adaptClassStrings.build());
+          adaptClassStrings.build(),
+          adaptResourceFilenames.build(),
+          adaptResourceFilecontents.build(),
+          keepDirectories.build());
 
       reporter.failIfPendingErrors();
 
@@ -330,6 +349,9 @@
   private final boolean useUniqueClassMemberNames;
   private final boolean keepParameterNames;
   private final ProguardClassFilter adaptClassStrings;
+  private final ProguardPathFilter adaptResourceFilenames;
+  private final ProguardPathFilter adaptResourceFilecontents;
+  private final ProguardPathFilter keepDirectories;
 
   private ProguardConfiguration(
       String parsedConfiguration,
@@ -363,7 +385,10 @@
       ImmutableList<String> packageObfuscationDictionary,
       boolean useUniqueClassMemberNames,
       boolean keepParameterNames,
-      ProguardClassFilter adaptClassStrings) {
+      ProguardClassFilter adaptClassStrings,
+      ProguardPathFilter adaptResourceFilenames,
+      ProguardPathFilter adaptResourceFilecontents,
+      ProguardPathFilter keepDirectories) {
     this.parsedConfiguration = parsedConfiguration;
     this.dexItemFactory = factory;
     this.injars = ImmutableList.copyOf(injars);
@@ -396,6 +421,9 @@
     this.useUniqueClassMemberNames = useUniqueClassMemberNames;
     this.keepParameterNames = keepParameterNames;
     this.adaptClassStrings = adaptClassStrings;
+    this.adaptResourceFilenames = adaptResourceFilenames;
+    this.adaptResourceFilecontents = adaptResourceFilecontents;
+    this.keepDirectories = keepDirectories;
   }
 
   /**
@@ -530,6 +558,18 @@
     return adaptClassStrings;
   }
 
+  public ProguardPathFilter getAdaptResourceFilenames() {
+    return adaptResourceFilenames;
+  }
+
+  public ProguardPathFilter getAdaptResourceFilecontents() {
+    return adaptResourceFilecontents;
+  }
+
+  public ProguardPathFilter getKeepDirectories() {
+    return keepDirectories;
+  }
+
   public static ProguardConfiguration defaultConfiguration(DexItemFactory dexItemFactory,
       Reporter reporter) {
     return builder(dexItemFactory, reporter).build();
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 3d426dd..dd7bb60 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -50,7 +50,6 @@
       "target");
 
   private static final List<String> IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS = ImmutableList.of(
-      "keepdirectories",
       "runtype",
       "laststageoutput");
 
@@ -72,8 +71,7 @@
   private static final List<String> WARNED_SINGLE_ARG_OPTIONS = ImmutableList.of(
       // TODO -outjars (http://b/37137994) and -adaptresourcefilecontents (http://b/37139570)
       // should be reported as errors, not just as warnings!
-      "outjars",
-      "adaptresourcefilecontents");
+      "outjars");
 
   private static final List<String> WARNED_FLAG_OPTIONS = ImmutableList.of(
       // TODO(b/73707846): add support -addconfigurationdebugging
@@ -217,6 +215,13 @@
       } else if (acceptString("checkdiscard")) {
         ProguardCheckDiscardRule rule = parseCheckDiscardRule();
         configurationBuilder.addRule(rule);
+      } else if (acceptString("keepdirectories")) {
+        skipWhitespace();
+        if (isOptionalArgumentGiven()) {
+          configurationBuilder.addKeepDirectories(parsePathFilter());
+        } else {
+          configurationBuilder.addKeepDirectories(ProguardPathList.emptyList());
+        }
       } else if (acceptString("keep")) {
         ProguardKeepRule rule = parseKeepRule();
         configurationBuilder.addRule(rule);
@@ -345,6 +350,20 @@
           configurationBuilder.addAdaptClassStringsPattern(
               ProguardClassNameList.singletonList(ProguardTypeMatcher.defaultAllMatcher()));
         }
+      } else if (acceptString("adaptresourcefilenames")) {
+        skipWhitespace();
+        if (isOptionalArgumentGiven()) {
+          configurationBuilder.addAdaptResourceFilenames(parsePathFilter());
+        } else {
+          configurationBuilder.addAdaptResourceFilenames(ProguardPathList.emptyList());
+        }
+      } else if (acceptString("adaptresourcefilecontents")) {
+        skipWhitespace();
+        if (isOptionalArgumentGiven()) {
+          configurationBuilder.addAdaptResourceFilecontents(parsePathFilter());
+        } else {
+          configurationBuilder.addAdaptResourceFilecontents(ProguardPathList.emptyList());
+        }
       } else if (acceptString("identifiernamestring")) {
         configurationBuilder.addRule(parseIdentifierNameStringRule());
       } else if (acceptString("if")) {
@@ -1292,6 +1311,34 @@
       return name;
     }
 
+    private boolean pathFilterMatcher(Integer character) {
+      return character != ',' && !Character.isWhitespace(character);
+    }
+
+    private ProguardPathList parsePathFilter() throws ProguardRuleParserException {
+      ProguardPathList.Builder builder = ProguardPathList.builder();
+      skipWhitespace();
+      boolean negated = acceptChar('!');
+      String fileFilter = acceptString(this::pathFilterMatcher);
+      if (fileFilter == null) {
+        throw parseError("Path filter expected");
+      }
+      builder.addFileName(negated, fileFilter);
+      skipWhitespace();
+      while (acceptChar(',')) {
+        skipWhitespace();
+        negated = acceptChar('!');
+        skipWhitespace();
+        fileFilter = acceptString(this::pathFilterMatcher);
+        if (fileFilter == null) {
+          throw parseError("Path filter expected");
+        }
+        builder.addFileName(negated, fileFilter);
+        skipWhitespace();
+      }
+      return builder.build();
+    }
+
     private String snippetForPosition() {
       // TODO(ager): really should deal with \r as well to get column right.
       String[] lines = contents.split("\n", -1);  // -1 to get trailing empty lines represented.
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardPathFilter.java b/src/main/java/com/android/tools/r8/shaking/ProguardPathFilter.java
new file mode 100644
index 0000000..cb2e117
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardPathFilter.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import com.google.common.collect.ImmutableList;
+
+public class ProguardPathFilter {
+  private final ImmutableList<ProguardPathList> patterns;
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private final ImmutableList.Builder<ProguardPathList> patterns = ImmutableList.builder();
+
+    private Builder() {
+    }
+
+    public Builder addPattern(ProguardPathList pattern) {
+      patterns.add(pattern);
+      return this;
+    }
+
+    ProguardPathFilter build() {
+      return new ProguardPathFilter(patterns.build());
+    }
+  }
+
+  private ProguardPathFilter(ImmutableList<ProguardPathList> patterns) {
+    if (patterns.isEmpty()) {
+      this.patterns = ImmutableList.of(ProguardPathList.emptyList());
+    } else {
+      this.patterns = patterns;
+    }
+  }
+
+  public boolean matches(String path) {
+    for (ProguardPathList pattern : patterns) {
+      if (pattern.matches(path)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardPathList.java b/src/main/java/com/android/tools/r8/shaking/ProguardPathList.java
new file mode 100644
index 0000000..6766128
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardPathList.java
@@ -0,0 +1,128 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.shaking;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+public abstract class ProguardPathList {
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static ProguardPathList emptyList() {
+    return new EmptyPathList();
+  }
+
+  abstract boolean matches(String path);
+
+  public static class Builder {
+
+    private final ImmutableList.Builder<FileNameMatcher> matchers = ImmutableList.builder();
+
+    private Builder() {
+    }
+
+    public Builder addFileName(boolean isNegated, String path) {
+      matchers.add(new FileNameMatcher(isNegated, path));
+      return this;
+    }
+
+    ProguardPathList build() {
+      List<FileNameMatcher> matchers = this.matchers.build();
+      if (matchers. size() > 0) {
+        return new PathList(matchers);
+      } else {
+        return emptyList();
+      }
+    }
+  }
+
+  private static class FileNameMatcher {
+    public final boolean negated;
+    public final String pattern;
+
+    FileNameMatcher(boolean negated, String pattern) {
+      this.negated = negated;
+      this.pattern = pattern;
+    }
+
+    private boolean match(String path) {
+      return matchImpl(pattern, 0, path, 0);
+    }
+
+    private boolean matchImpl(String pattern, int patternIndex, String path, int pathIndex) {
+      for (int i = patternIndex; i < pattern.length(); i++) {
+        char patternChar = pattern.charAt(i);
+        switch (patternChar) {
+          case '*':
+            boolean includeSeparators = pattern.length() > (i + 1) && pattern.charAt(i + 1) == '*';
+            int nextPatternIndex = i + (includeSeparators ? 2 : 1);
+            // Fast cases for the common case where a pattern ends with '**' or '*'.
+            if (nextPatternIndex == pattern.length()) {
+              return includeSeparators || !containsSeparatorsStartingAt(path, pathIndex);
+            }
+            // Match the rest of the pattern against the (non-empty) rest of the class name.
+            for (int nextPathIndex = pathIndex; nextPathIndex < path.length(); nextPathIndex++) {
+              if (!includeSeparators && path.charAt(nextPathIndex) == '/') {
+                return matchImpl(pattern, nextPatternIndex, path, nextPathIndex);
+              }
+              if (matchImpl(pattern, nextPatternIndex, path, nextPathIndex)) {
+                return true;
+              }
+            }
+            break;
+          case '?':
+            if (pathIndex == path.length() || path.charAt(pathIndex++) == '/') {
+              return false;
+            }
+            break;
+          default:
+            if (pathIndex == path.length() || patternChar != path.charAt(pathIndex++)) {
+              return false;
+            }
+            break;
+        }
+      }
+      return pathIndex == path.length();
+    }
+
+    private boolean containsSeparatorsStartingAt(String path, int pathIndex) {
+      return path.indexOf('/', pathIndex) != -1;
+    }
+
+  }
+
+  private static class PathList extends ProguardPathList {
+    private final List<FileNameMatcher> matchers;
+
+    private PathList(List<FileNameMatcher> matchers) {
+      this.matchers = matchers;
+    }
+
+    @Override
+    boolean matches(String path) {
+      for (FileNameMatcher matcher : matchers) {
+        if (matcher.match(path)) {
+          // If we match a negation, abort as non-match. If we match a positive, return true.
+          return !matcher.negated;
+        }
+      }
+      return false;
+    }
+  }
+
+  private static class EmptyPathList extends ProguardPathList {
+
+    private EmptyPathList() {
+    }
+
+    @Override
+    boolean matches(String path) {
+      return true;
+    }
+  }
+}
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 f26e48d..9e9f80e 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.position.TextRange;
@@ -157,11 +158,13 @@
 
   private Reporter reporter;
   private KeepingDiagnosticHandler handler;
+  private ProguardConfigurationParser parser;
 
   @Before
-  public void setup() {
+  public void reset() {
     handler = new KeepingDiagnosticHandler();
     reporter = new Reporter(handler);
+    parser = new ProguardConfigurationParser(new DexItemFactory(), reporter);
   }
 
   @Test
@@ -1175,6 +1178,146 @@
     }
   }
 
+  private void checkFileFilterMatchAnything(ProguardPathFilter filter) {
+    assertTrue(filter.matches("x"));
+    assertTrue(filter.matches("x/y"));
+    assertTrue(filter.matches("x/y/x"));
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_noArguments1() {
+    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
+        "-adaptresourcefilenames",
+        "-adaptresourcefilecontents",
+        "-keepdirectories"
+    ));
+    checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
+    checkFileFilterMatchAnything(config.getAdaptResourceFilecontents());
+    checkFileFilterMatchAnything(config.getKeepDirectories());
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_noArguments2() {
+    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
+        "-keepdirectories",
+        "-adaptresourcefilenames",
+        "-adaptresourcefilecontents"
+    ));
+    checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
+    checkFileFilterMatchAnything(config.getAdaptResourceFilecontents());
+    checkFileFilterMatchAnything(config.getKeepDirectories());
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_noArguments3() {
+    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
+        "-adaptresourcefilecontents",
+        "-keepdirectories",
+        "-adaptresourcefilenames"
+    ));
+    checkFileFilterMatchAnything(config.getAdaptResourceFilenames());
+    checkFileFilterMatchAnything(config.getAdaptResourceFilecontents());
+    checkFileFilterMatchAnything(config.getKeepDirectories());
+  }
+
+  private String FILE_FILTER_SINGLE = "xxx/*";
+
+  private void checkFileFilterSingle(ProguardPathFilter filter) {
+    assertTrue(filter.matches("xxx/x"));
+    assertTrue(filter.matches("xxx/"));
+    assertFalse(filter.matches("xxx/yyy/z"));
+    assertFalse(filter.matches("xxx"));
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_singleArgument() {
+    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
+        "-adaptresourcefilenames " + FILE_FILTER_SINGLE,
+        "-adaptresourcefilecontents " + FILE_FILTER_SINGLE,
+        "-keepdirectories " + FILE_FILTER_SINGLE
+    ));
+    checkFileFilterSingle(config.getAdaptResourceFilenames());
+    checkFileFilterSingle(config.getAdaptResourceFilecontents());
+    checkFileFilterSingle(config.getKeepDirectories());
+  }
+
+  private String FILE_FILTER_MULTIPLE =
+      "xxx/*, !**.gif  ,images/**  ,  com/myapp/**/*.xml,com/mylib/*/*.xml";
+
+  private void checkFileFilterMultiple(ProguardPathFilter filter) {
+    assertTrue(filter.matches("xxx/x"));
+    assertTrue(filter.matches("xxx/x.gif"));
+    assertTrue(filter.matches("images/x.jpg"));
+    assertTrue(filter.matches("images/xxx/x.jpg"));
+    assertTrue(filter.matches("com/myapp/package1/x.xml"));
+    assertTrue(filter.matches("com/myapp/package1/package2/x.xml"));
+    assertTrue(filter.matches("com/mylib/package1/x.xml"));
+    assertFalse(filter.matches("x.gif"));
+    assertFalse(filter.matches("images/x.gif"));
+    assertFalse(filter.matches("images/xxx/y.gif"));
+    assertFalse(filter.matches("images/xxx/yyy/z.gif"));
+    assertFalse(filter.matches("com/myapp/package1/x.jpg"));
+    assertFalse(filter.matches("com/myapp/package1/package2/x.jpg"));
+    assertFalse(filter.matches("com/mylib/package1/package2/x.xml"));
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_multipleArgument() {
+    ProguardConfiguration config = parseAndVerifyParserEndsCleanly(ImmutableList.of(
+        "-adaptresourcefilenames " + FILE_FILTER_MULTIPLE,
+        "-adaptresourcefilecontents " + FILE_FILTER_MULTIPLE,
+        "-keepdirectories " + FILE_FILTER_MULTIPLE
+    ));
+    checkFileFilterMultiple(config.getAdaptResourceFilenames());
+    checkFileFilterMultiple(config.getAdaptResourceFilecontents());
+    checkFileFilterMultiple(config.getKeepDirectories());
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_leadingComma() {
+    List<String> options = ImmutableList.of(
+        "-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
+    for (String option : options) {
+      try {
+        reset();
+        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " ,")));
+        fail("Expect to fail due to the lack of path filter.");
+      } catch (AbortException e) {
+        checkDiagnostic(handler.errors, null, 1, option.length() + 2, "Path filter expected");
+      }
+    }
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_emptyListElement() {
+    List<String> options = ImmutableList.of(
+        "-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
+    for (String option : options) {
+      try {
+        reset();
+        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,,yyy")));
+        fail("Expect to fail due to the lack of path filter.");
+      } catch (AbortException e) {
+        checkDiagnostic(handler.errors, null, 1, option.length() + 6, "Path filter expected");
+      }
+    }
+  }
+
+  @Test
+  public void parse_adaptresourcexxx_keepdirectories_trailingComma() {
+    List<String> options = ImmutableList.of(
+        "-adaptresourcefilenames", "-adaptresourcefilecontents", "-keepdirectories");
+    for (String option : options) {
+      try {
+        reset();
+        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " xxx,")));
+        fail("Expect to fail due to the lack of path filter.");
+      } catch (AbortException e) {
+        checkDiagnostic(handler.errors, null, 1, option.length() + 6, "Path filter expected");
+      }
+    }
+  }
+
   @Test
   public void parse_if() throws Exception {
     Path proguardConfig = writeTextToTempFile(
@@ -1459,6 +1602,12 @@
     verifyWithProguard(proguardConfig);
   }
 
+  private ProguardConfiguration parseAndVerifyParserEndsCleanly(List<String> config) {
+    parser.parse(createConfigurationForTesting(config));
+    verifyParserEndsCleanly();
+    return parser.getConfig();
+  }
+
   private void verifyParserEndsCleanly() {
     assertEquals(0, handler.infos.size());
     assertEquals(0, handler.warnings.size());
@@ -1469,7 +1618,11 @@
       int columnStart, String... messageParts) {
     assertEquals(1, diagnostics.size());
     Diagnostic diagnostic = diagnostics.get(0);
-    assertEquals(path, ((PathOrigin) diagnostic.getOrigin()).getPath());
+    if (path != null) {
+      assertEquals(path, ((PathOrigin) diagnostic.getOrigin()).getPath());
+    } else {
+      assertSame(Origin.unknown(), diagnostic.getOrigin());
+    }
     TextPosition position;
     if (diagnostic.getPosition() instanceof TextRange) {
       position = ((TextRange) diagnostic.getPosition()).getStart();