Implement the reduced classpath optimization in turbine

MOE_MIGRATED_REVID=138561608
diff --git a/java/com/google/turbine/deps/Dependencies.java b/java/com/google/turbine/deps/Dependencies.java
index b6aeb6a..110d420 100644
--- a/java/com/google/turbine/deps/Dependencies.java
+++ b/java/com/google/turbine/deps/Dependencies.java
@@ -17,6 +17,10 @@
 package com.google.turbine.deps;
 
 import com.google.common.base.Optional;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.turbine.binder.Binder.BindingResult;
 import com.google.turbine.binder.bound.TypeBoundClass;
@@ -27,6 +31,14 @@
 import com.google.turbine.binder.sym.ClassSymbol;
 import com.google.turbine.lower.Lower.Lowered;
 import com.google.turbine.proto.DepsProto;
+import java.io.BufferedInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.Set;
 
@@ -95,4 +107,44 @@
       addSuperTypes(closure, env, i);
     }
   }
+
+  /**
+   * Filters a transitive classpath to contain only the entries for direct dependencies, and the
+   * types needed to compile those direct deps as reported by jdeps.
+   *
+   * <p>If no direct dependency information is available the full transitive classpath is returned.
+   */
+  public static Collection<String> reduceClasspath(
+      ImmutableList<String> transitiveClasspath,
+      ImmutableMap<String, String> directJarsToTargets,
+      ImmutableList<String> depsArtifacts) {
+    if (directJarsToTargets.isEmpty()) {
+      // the compilation doesn't support strict deps (e.g. proto libraries)
+      return transitiveClasspath;
+    }
+    Set<String> reduced = new HashSet<>(directJarsToTargets.keySet());
+    for (String path : depsArtifacts) {
+      DepsProto.Dependencies.Builder deps = DepsProto.Dependencies.newBuilder();
+      try (InputStream is = new BufferedInputStream(Files.newInputStream(Paths.get(path)))) {
+        deps.mergeFrom(is);
+      } catch (IOException e) {
+        throw new IOError(e);
+      }
+      for (DepsProto.Dependency dep : deps.build().getDependencyList()) {
+        switch (dep.getKind()) {
+          case EXPLICIT:
+          case IMPLICIT:
+            reduced.add(dep.getPath());
+            break;
+          case INCOMPLETE:
+          case UNUSED:
+            break;
+          default:
+            throw new AssertionError(dep.getKind());
+        }
+      }
+    }
+    // preserve the order of entries in the transitive classpath
+    return Collections2.filter(transitiveClasspath, Predicates.in(reduced));
+  }
 }
diff --git a/java/com/google/turbine/main/Main.java b/java/com/google/turbine/main/Main.java
index c6d9bd1..1ba8479 100644
--- a/java/com/google/turbine/main/Main.java
+++ b/java/com/google/turbine/main/Main.java
@@ -41,6 +41,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Enumeration;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -70,11 +71,14 @@
 
     ImmutableList<CompUnit> units = parseAll(options);
 
-    // TODO(cushon): reduce classpath
+    Collection<String> reducedClasspath =
+        Dependencies.reduceClasspath(
+            options.classPath(), options.directJarsToTargets(), options.depsArtifacts());
+
     BindingResult bound =
         Binder.bind(
             units,
-            Iterables.transform(options.classPath(), TO_PATH),
+            Iterables.transform(reducedClasspath, TO_PATH),
             Iterables.transform(options.bootClassPath(), TO_PATH));
 
     // TODO(cushon): parallelize
diff --git a/javatests/com/google/turbine/deps/DependenciesTest.java b/javatests/com/google/turbine/deps/DependenciesTest.java
index 71579f5..91a8440 100644
--- a/javatests/com/google/turbine/deps/DependenciesTest.java
+++ b/javatests/com/google/turbine/deps/DependenciesTest.java
@@ -33,7 +33,9 @@
 import com.google.turbine.parse.Parser;
 import com.google.turbine.proto.DepsProto;
 import com.google.turbine.tree.Tree.CompUnit;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -249,4 +251,61 @@
                   liba, DepsProto.Dependency.Kind.EXPLICIT));
     }
   }
+
+  @Test
+  public void unreducedClasspathTest() throws IOException {
+    ImmutableList<String> classpath =
+        ImmutableList.of(
+            "a.jar", "b.jar", "c.jar", "d.jar", "e.jar", "f.jar", "g.jar", "h.jar", "i.jar",
+            "j.jar");
+    ImmutableMap<String, String> directJarsToTargets = ImmutableMap.of();
+    ImmutableList<String> depsArtifacts = ImmutableList.of();
+    assertThat(Dependencies.reduceClasspath(classpath, directJarsToTargets, depsArtifacts))
+        .isEqualTo(classpath);
+  }
+
+  @Test
+  public void reducedClasspathTest() throws IOException {
+    Path cdeps = temporaryFolder.newFile("c.jdeps").toPath();
+    Path ddeps = temporaryFolder.newFile("d.jdeps").toPath();
+    Path gdeps = temporaryFolder.newFile("g.jdeps").toPath();
+    ImmutableList<String> classpath =
+        ImmutableList.of(
+            "a.jar", "b.jar", "c.jar", "d.jar", "e.jar", "f.jar", "g.jar", "h.jar", "i.jar",
+            "j.jar");
+    ImmutableMap<String, String> directJarsToTargets =
+        ImmutableMap.of(
+            "c.jar", "//a",
+            "d.jar", "//d",
+            "g.jar", "//e");
+    ImmutableList<String> depsArtifacts =
+        ImmutableList.of(cdeps.toString(), ddeps.toString(), gdeps.toString());
+    writeDeps(
+        cdeps,
+        ImmutableMap.of(
+            "b.jar", DepsProto.Dependency.Kind.EXPLICIT,
+            "e.jar", DepsProto.Dependency.Kind.EXPLICIT));
+    writeDeps(
+        ddeps,
+        ImmutableMap.of(
+            "f.jar", DepsProto.Dependency.Kind.UNUSED,
+            "j.jar", DepsProto.Dependency.Kind.UNUSED));
+    writeDeps(gdeps, ImmutableMap.of("i.jar", DepsProto.Dependency.Kind.IMPLICIT));
+    assertThat(Dependencies.reduceClasspath(classpath, directJarsToTargets, depsArtifacts))
+        .containsExactly("b.jar", "c.jar", "d.jar", "e.jar", "g.jar", "i.jar")
+        .inOrder();
+  }
+
+  void writeDeps(Path path, ImmutableMap<String, DepsProto.Dependency.Kind> deps)
+      throws IOException {
+    DepsProto.Dependencies.Builder builder =
+        DepsProto.Dependencies.newBuilder().setSuccess(true).setRuleLabel("//test");
+    for (Map.Entry<String, DepsProto.Dependency.Kind> e : deps.entrySet()) {
+      builder.addDependency(
+          DepsProto.Dependency.newBuilder().setPath(e.getKey()).setKind(e.getValue()));
+    }
+    try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(path))) {
+      builder.build().writeTo(os);
+    }
+  }
 }