Make the D8 command line allow diplicate classes for classpath

For each --classpath argument (either directory or archive) a search
will be preformed for classes in the order of the arguments. The first
class found will be returned.

Bug: 109832071
Change-Id: I6be3b3f2781f0582443fd02a2c4efc631d26776d
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index 9e5d36a..93c0126 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -3,16 +3,79 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.isArchive;
+
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
 
 public class D8CommandParser extends BaseCompilerCommandParser {
 
+  static class OrderedClassFileResourceProvider implements ClassFileResourceProvider {
+    static class Builder {
+      private final ImmutableList.Builder<ClassFileResourceProvider> builder =
+          ImmutableList.builder();
+      boolean empty = true;
+
+      OrderedClassFileResourceProvider build() {
+        return new OrderedClassFileResourceProvider(builder.build());
+      }
+
+      Builder addClassFileResourceProvider(ClassFileResourceProvider provider) {
+        builder.add(provider);
+        empty = false;
+        return this;
+      }
+
+      boolean isEmpty() {
+        return empty;
+      }
+    }
+
+    final List<ClassFileResourceProvider> providers;
+    final Set<String> descriptors = Sets.newHashSet();
+
+    private OrderedClassFileResourceProvider(ImmutableList<ClassFileResourceProvider> providers) {
+      this.providers = providers;
+      // Collect all descriptors that can be provided.
+      this.providers.forEach(provider -> this.descriptors.addAll(provider.getClassDescriptors()));
+    }
+
+    static Builder builder() {
+      return new Builder();
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return descriptors;
+    }
+
+    @Override
+    public ProgramResource getProgramResource(String descriptor) {
+      // Search the providers in order. Return the program resource from the first provider that
+      // can provide it.
+      for (ClassFileResourceProvider provider : providers) {
+        if (provider.getClassDescriptors().contains(descriptor)) {
+          return provider.getProgramResource(descriptor);
+        }
+      }
+      return null;
+    }
+  }
+
   public static void main(String[] args) throws CompilationFailedException {
     D8Command command = parse(args, Origin.root()).build();
     if (command.isPrintHelp()) {
@@ -76,6 +139,8 @@
     Path outputPath = null;
     OutputMode outputMode = null;
     boolean hasDefinedApiLevel = false;
+    OrderedClassFileResourceProvider.Builder classpathBuilder =
+        OrderedClassFileResourceProvider.builder();
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder);
     try {
       for (int i = 0; i < expandedArgs.length; i++) {
@@ -115,7 +180,22 @@
         } else if (arg.equals("--lib")) {
           builder.addLibraryFiles(Paths.get(expandedArgs[++i]));
         } else if (arg.equals("--classpath")) {
-          builder.addClasspathFiles(Paths.get(expandedArgs[++i]));
+          Path file = Paths.get(expandedArgs[++i]);
+          try {
+            if (!Files.exists(file)) {
+              throw new NoSuchFileException(file.toString());
+            }
+            if (isArchive(file)) {
+              classpathBuilder.addClassFileResourceProvider(new ArchiveClassFileProvider(file));
+            } else if (Files.isDirectory(file)) {
+              classpathBuilder.addClassFileResourceProvider(
+                  DirectoryClassFileProvider.fromDirectory(file));
+            } else {
+              throw new CompilationError("Unsupported classpath file type", new PathOrigin(file));
+            }
+          } catch (IOException e) {
+            builder.error(new ExceptionDiagnostic(e, new PathOrigin(file)));
+          }
         } else if (arg.equals("--main-dex-list")) {
           builder.addMainDexListFiles(Paths.get(expandedArgs[++i]));
         } else if (arg.equals("--optimize-multidex-for-linearalloc")) {
@@ -140,6 +220,9 @@
           builder.addProgramFiles(Paths.get(arg));
         }
       }
+      if (!classpathBuilder.isEmpty()) {
+        builder.addClasspathResourceProvider(classpathBuilder.build());
+      }
       if (compilationMode != null) {
         builder.setMode(compilationMode);
       }
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index 1b846a5..0c278f7 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -6,13 +6,13 @@
 import static com.android.tools.r8.R8CommandTest.getOutputPath;
 import static com.android.tools.r8.ToolHelper.EXAMPLES_BUILD_DIR;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
-import static java.nio.file.StandardOpenOption.CREATE_NEW;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import com.android.sdklib.AndroidVersion;
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
@@ -24,7 +24,6 @@
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.List;
@@ -263,13 +262,37 @@
         tmpClassesDir.toString());
     AndroidApp inputApp = ToolHelper.getApp(command);
     assertEquals(1, inputApp.getClasspathResourceProviders().size());
+    OrderedClassFileResourceProvider classpathProvider =
+        (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+    assertEquals(1, classpathProvider.providers.size());
     assertTrue(Files.isSameFile(tmpClassesDir,
-        ((DirectoryClassFileProvider) inputApp.getClasspathResourceProviders().get(0)).getRoot()));
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
     assertEquals(1, inputApp.getLibraryResourceProviders().size());
     assertTrue(Files.isSameFile(tmpClassesDir,
         ((DirectoryClassFileProvider) inputApp.getLibraryResourceProviders().get(0)).getRoot()));
   }
 
+  @Test
+  public void folderClasspathMultiple() throws Throwable {
+    Path inputFile =
+        Paths.get(ToolHelper.EXAMPLES_ANDROID_N_BUILD_DIR, "interfacemethods" + JAR_EXTENSION);
+    Path tmpClassesDir1 = temp.newFolder().toPath();
+    Path tmpClassesDir2 = temp.newFolder().toPath();
+    ZipUtils.unzip(inputFile.toString(), tmpClassesDir1.toFile());
+    ZipUtils.unzip(inputFile.toString(), tmpClassesDir2.toFile());
+    D8Command command = parse("--classpath", tmpClassesDir1.toString(), "--classpath",
+        tmpClassesDir2.toString());
+    AndroidApp inputApp = ToolHelper.getApp(command);
+    assertEquals(1, inputApp.getClasspathResourceProviders().size());
+    OrderedClassFileResourceProvider classpathProvider =
+        (OrderedClassFileResourceProvider) inputApp.getClasspathResourceProviders().get(0);
+    assertEquals(2, classpathProvider.providers.size());
+    assertTrue(Files.isSameFile(tmpClassesDir1,
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(0)).getRoot()));
+    assertTrue(Files.isSameFile(tmpClassesDir2,
+        ((DirectoryClassFileProvider) classpathProvider.providers.get(1)).getRoot()));
+  }
+
   @Test(expected = CompilationFailedException.class)
   public void classFolderProgram() throws Throwable {
     Path inputFile =
diff --git a/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
new file mode 100644
index 0000000..dc497c4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/OrderedClassFileResourceProviderTest.java
@@ -0,0 +1,113 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8CommandParser.OrderedClassFileResourceProvider;
+import com.android.tools.r8.origin.Origin;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+
+public class OrderedClassFileResourceProviderTest extends TestBase {
+  class SimpleClassFileResourceProvider implements ClassFileResourceProvider {
+
+    private final Set<String> descriptors;
+    private final ProgramResource fixedProgramResource;
+
+    SimpleClassFileResourceProvider(int id, Set<String> descriptors) {
+      this.descriptors = descriptors;
+      this.fixedProgramResource = new SimpleProgramResource(id);
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return descriptors;
+    }
+
+    @Override
+    public ProgramResource getProgramResource(String descriptor) {
+      return fixedProgramResource;
+    }
+  }
+
+  class SimpleProgramResource implements ProgramResource {
+
+    private final Origin origin;
+
+    SimpleProgramResource(int id) {
+      origin = new SimpleOrigin(id);
+    }
+
+    @Override
+    public Kind getKind() {
+      return null;
+    }
+
+    @Override
+    public InputStream getByteStream() throws ResourceException {
+      return null;
+    }
+
+    @Override
+    public Set<String> getClassDescriptors() {
+      return null;
+    }
+
+    @Override
+    public Origin getOrigin() {
+      return origin;
+    }
+  }
+
+  public class SimpleOrigin extends Origin {
+
+    private final int id;
+
+    private SimpleOrigin(int index) {
+      super(root());
+      this.id = index;
+    }
+
+    int getId() {
+      return id;
+    }
+
+    @Override
+    public String part() {
+      return "Test";
+    }
+  }
+
+  @Test
+  public void test() {
+    OrderedClassFileResourceProvider.Builder builder = OrderedClassFileResourceProvider.builder();
+    builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(1, ImmutableSet.of(
+        "L/a/a/a", "L/a/a/b", "L/a/a/c"
+    )));
+    builder.addClassFileResourceProvider(new SimpleClassFileResourceProvider(2, ImmutableSet.of(
+        "L/a/a/b", "L/a/a/c", "L/a/a/d"
+        )));
+    ClassFileResourceProvider provider = builder.build();
+    assertEquals(
+        ImmutableSet.of("L/a/a/a", "L/a/a/b", "L/a/a/c", "L/a/a/d"),
+        provider.getClassDescriptors());
+
+    Map<String, Integer> expectations = ImmutableMap.of(
+        "L/a/a/a", 1,
+        "L/a/a/b", 1,
+        "L/a/a/c", 1,
+        "L/a/a/d", 2
+    );
+    expectations.forEach((descriptor, id) ->
+        assertEquals(
+            (int) id,
+            ((SimpleOrigin) provider.getProgramResource(descriptor).getOrigin()).getId()));
+  }
+}