Add a new tool to check the deps of aar_import. This is the first cl of a
series. The following CLs will integrate this into bazel.

RELNOTES:n/a.
PiperOrigin-RevId: 184706507
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/AbstractClassEntryState.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/AbstractClassEntryState.java
new file mode 100644
index 0000000..209bf83
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/AbstractClassEntryState.java
@@ -0,0 +1,124 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Optional;
+
+/**
+ * The state for a class entry used in {@link ClassCache}. A state can be
+ *
+ * <ul>
+ *   <li>EXISTING: this class exists.
+ *   <li>INCOMPLETE: this class exists, but at least one of its ancestor is missing.
+ *   <li>MISSING: this class is missing on the classpath.
+ * </ul>
+ */
+public abstract class AbstractClassEntryState {
+
+  public boolean isMissingState() {
+    return this instanceof MissingState;
+  }
+
+  public MissingState asMissingState() {
+    throw new IllegalStateException("Not a missing state " + this);
+  }
+
+  public boolean isExistingState() {
+    return this instanceof ExistingState;
+  }
+
+  public ExistingState asExistingState() {
+    throw new IllegalStateException("Not an existing state " + this);
+  }
+
+  public boolean isIncompleteState() {
+    return this instanceof IncompleteState;
+  }
+
+  public IncompleteState asIncompleteState() {
+    throw new IllegalStateException("Not an incomplete state " + this);
+  }
+
+  public abstract Optional<ClassInfo> classInfo();
+
+  /** A state to indicate that a class exists. */
+  @AutoValue
+  public abstract static class ExistingState extends AbstractClassEntryState {
+
+    public static ExistingState create(ClassInfo classInfo) {
+      return new AutoValue_AbstractClassEntryState_ExistingState(Optional.of(classInfo));
+    }
+
+    @Override
+    public ExistingState asExistingState() {
+      return this;
+    }
+  }
+
+  /** A state to indicate that a class is missing. */
+  public static final class MissingState extends AbstractClassEntryState {
+
+    private static final MissingState SINGLETON = new MissingState();
+
+    public static MissingState singleton() {
+      return SINGLETON;
+    }
+
+    private MissingState() {}
+
+    @Override
+    public MissingState asMissingState() {
+      return this;
+    }
+
+    @Override
+    public Optional<ClassInfo> classInfo() {
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * A state to indicate that a class is incomplete, that is, some ancesotor is missing on the
+   * classpath.
+   */
+  @AutoValue
+  public abstract static class IncompleteState extends AbstractClassEntryState {
+
+    public static IncompleteState create(
+        ClassInfo classInfo, ImmutableList<String> resolutionFailurePath) {
+      checkArgument(
+          !resolutionFailurePath.isEmpty(),
+          "The resolution path should contain at least one element, the missing ancestor. %s",
+          resolutionFailurePath);
+      return new AutoValue_AbstractClassEntryState_IncompleteState(
+          Optional.of(classInfo), resolutionFailurePath);
+    }
+
+    public abstract ImmutableList<String> getResolutionFailurePath();
+
+    public String getMissingAncestor() {
+      ImmutableList<String> path = getResolutionFailurePath();
+      return path.get(path.size() - 1);
+    }
+
+    @Override
+    public IncompleteState asIncompleteState() {
+      return this;
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/BUILD b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/BUILD
new file mode 100644
index 0000000..a9d4cd3
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/BUILD
@@ -0,0 +1,34 @@
+# Description:
+#   A checker to check the completeness of the deps of java_import or aar_import targets.
+
+package(
+    default_visibility = ["//src:__subpackages__"],
+)
+
+java_library(
+    name = "import_deps_checker",
+    srcs = glob(
+        ["*.java"],
+        exclude = ["Main.java"],
+    ),
+    deps = [
+        "//third_party:asm",
+        "//third_party:auto_value",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party/java/asm:asm-commons",
+        "//third_party/java/asm:asm-tree",
+    ],
+)
+
+java_binary(
+    name = "ImportDepsChecker",
+    srcs = ["Main.java"],
+    main_class = "com.google.devtools.build.importdeps.Main",
+    deps = [
+        ":import_deps_checker",
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:worker_protocol_java_proto",
+        "//third_party:guava",
+    ],
+)
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassCache.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassCache.java
new file mode 100644
index 0000000..c855afd
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassCache.java
@@ -0,0 +1,277 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Closer;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.ExistingState;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.MissingState;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/** A cache that stores all the accessible classes. */
+public final class ClassCache implements Closeable {
+
+  private final ImmutableMap<String, LazyClassEntry> classIndex;
+  /**
+   * If the cache is open, then the {@code closer} is nonnull. After the cache is closed, the {@code
+   * closer} is set to {@literal null}.
+   */
+  @Nullable private Closer closer;
+
+  public ClassCache(Path... jars) throws IOException {
+    this(ImmutableList.copyOf(jars));
+  }
+
+  public ClassCache(ImmutableList<Path> jars) throws IOException {
+    closer = Closer.create();
+    this.classIndex = buildClassIndex(jars, closer);
+  }
+
+  public AbstractClassEntryState getClassState(String internalName) {
+    ensureCacheIsOpen();
+    LazyClassEntry entry = classIndex.get(internalName);
+    if (entry == null) {
+      return MissingState.singleton();
+    }
+    return entry.getState(classIndex);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (closer == null) {
+      return;
+    }
+    closer.close();
+    closer = null;
+  }
+
+  private static ImmutableMap<String, LazyClassEntry> buildClassIndex(
+      ImmutableList<Path> jars, Closer closer) throws IOException {
+    HashMap<String, LazyClassEntry> result = new HashMap<>();
+    for (Path jarPath : jars) {
+      try {
+        ZipFile zipFile = closer.register(new ZipFile(jarPath.toFile()));
+        zipFile
+            .stream()
+            .forEach(
+                entry -> {
+                  String name = entry.getName();
+                  if (!name.endsWith(".class")) {
+                    return; // Not a class file.
+                  }
+                  String internalName = name.substring(0, name.lastIndexOf('.'));
+                  result.computeIfAbsent(internalName, key -> new LazyClassEntry(key, zipFile));
+                });
+      } catch (Throwable e) {
+        throw new RuntimeException("Error in reading zip file " + jarPath, e);
+      }
+    }
+    return ImmutableMap.copyOf(result);
+  }
+
+  private void ensureCacheIsOpen() {
+    checkState(closer != null, "The cache should be open!");
+  }
+
+  static class LazyClassEntry {
+    private final String internalName;
+    private final ZipFile zipFile;
+
+    /**
+     * The state of this class entry. If {@literal null}, then this class has not been resolved yet.
+     */
+    @Nullable private AbstractClassEntryState state = null;
+
+    private LazyClassEntry(String internalName, ZipFile zipFile) {
+      this.internalName = internalName;
+      this.zipFile = zipFile;
+    }
+
+    ZipFile getZipFile() {
+      return zipFile;
+    }
+
+    @Nullable
+    public AbstractClassEntryState getState(ImmutableMap<String, LazyClassEntry> classIndex) {
+      resolveIfNot(classIndex);
+      checkState(
+          state != null && !state.isMissingState(),
+          "The state cannot be null or MISSING. %s",
+          state);
+      return state;
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("internalName", internalName)
+          .add("state", state)
+          .toString();
+    }
+
+    private void resolveIfNot(ImmutableMap<String, LazyClassEntry> classIndex) {
+      if (state != null) {
+        return;
+      }
+      resolveClassEntry(this, classIndex);
+      checkNotNull(state, "After resolution, the state cannot be null");
+    }
+
+    private static void resolveClassEntry(
+        LazyClassEntry classEntry, ImmutableMap<String, LazyClassEntry> classIndex) {
+      if (classEntry.state != null) {
+        // Already resolved. See if it is the existing state.
+        return;
+      }
+
+      String entryName = classEntry.internalName + ".class";
+      ZipEntry zipEntry =
+          checkNotNull(
+              classEntry.zipFile.getEntry(entryName), "The zip entry %s is null.", entryName);
+      try (InputStream inputStream = classEntry.zipFile.getInputStream(zipEntry)) {
+        ClassReader classReader = new ClassReader(inputStream);
+        ImmutableList<String> resolutionFailurePath = null;
+        for (String superName :
+            combineWithoutNull(classReader.getSuperName(), classReader.getInterfaces())) {
+          LazyClassEntry superClassEntry = classIndex.get(superName);
+
+          if (superClassEntry == null) {
+            resolutionFailurePath = ImmutableList.of(superName);
+            break;
+          } else {
+            resolveClassEntry(superClassEntry, classIndex);
+            AbstractClassEntryState superState = superClassEntry.state;
+            if (superState instanceof ExistingState) {
+              // Do nothing. Good to proceed.
+              continue;
+            } else if (superState instanceof IncompleteState) {
+              resolutionFailurePath =
+                  ImmutableList.<String>builder()
+                      .add(superName)
+                      .addAll(((IncompleteState) superState).getResolutionFailurePath())
+                      .build();
+              break;
+            } else {
+              throw new RuntimeException("Cannot reach here. superState is " + superState);
+            }
+          }
+        }
+        ClassInfoBuilder classInfoBuilder = new ClassInfoBuilder();
+        classReader.accept(classInfoBuilder, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+        if (resolutionFailurePath == null) {
+          classEntry.state = ExistingState.create(classInfoBuilder.build(classIndex));
+        } else {
+          classEntry.state =
+              IncompleteState.create(classInfoBuilder.build(classIndex), resolutionFailurePath);
+        }
+      } catch (IOException e) {
+        throw new RuntimeException("Error when resolving class entry " + entryName);
+      } catch (RuntimeException e) {
+        System.err.println(
+            "A runtime exception occurred. The following is the content in the class index. "
+                + e.getMessage());
+        int counter = 0;
+        for (Map.Entry<String, LazyClassEntry> entry : classIndex.entrySet()) {
+          System.err.printf("%d  %s\n    %s\n\n", ++counter, entry.getKey(), entry.getValue());
+        }
+        throw e;
+      }
+    }
+  }
+
+  private static ImmutableList<String> combineWithoutNull(
+      @Nullable String first, @Nullable String[] others) {
+    ImmutableList.Builder<String> list = ImmutableList.builder();
+    if (first != null) {
+      list.add(first);
+    }
+    if (others != null) {
+      list.add(others);
+    }
+    return list.build();
+  }
+
+  /** Builder to build a ClassInfo object from the class file. */
+  private static class ClassInfoBuilder extends ClassVisitor {
+
+    private String internalName;
+    private final ImmutableSet.Builder<MemberInfo> members = ImmutableSet.builder();
+    private ImmutableList<String> superClasses;
+
+    public ClassInfoBuilder() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public void visit(
+        int version,
+        int access,
+        String name,
+        String signature,
+        String superName,
+        String[] interfaces) {
+      checkState(internalName == null && superClasses == null, "This visitor is already used.");
+      internalName = name;
+      superClasses = combineWithoutNull(superName, interfaces);
+    }
+
+    @Override
+    public FieldVisitor visitField(
+        int access, String name, String desc, String signature, Object value) {
+      members.add(MemberInfo.create(internalName, name, desc));
+      return null;
+    }
+
+    @Override
+    public MethodVisitor visitMethod(
+        int access, String name, String desc, String signature, String[] exceptions) {
+      members.add(MemberInfo.create(internalName, name, desc));
+      return null;
+    }
+
+    public ClassInfo build(ImmutableMap<String, LazyClassEntry> classIndex) {
+      return ClassInfo.create(
+          checkNotNull(internalName),
+          superClasses
+              .stream()
+              .map(classIndex::get)
+              .filter(Objects::nonNull)
+              .map(entry -> entry.state.classInfo().get())
+              .collect(ImmutableList.toImmutableList()),
+          checkNotNull(members).build());
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassInfo.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassInfo.java
new file mode 100644
index 0000000..d091464
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassInfo.java
@@ -0,0 +1,94 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.common.base.Strings;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Representation of a class. It maintains the internal name, declared members, as well as the super
+ * classes.
+ */
+@AutoValue
+public abstract class ClassInfo {
+
+  public static ClassInfo create(
+      String internalName,
+      ImmutableList<ClassInfo> superClasses,
+      ImmutableSet<MemberInfo> declaredMembers) {
+    return new AutoValue_ClassInfo(internalName, superClasses, declaredMembers);
+  }
+
+  public abstract String internalName();
+
+  /**
+   * Returns all the available super classes. There may be more super classes (super class or
+   * interfaces), but those do not exist on the classpath.
+   */
+  public abstract ImmutableList<ClassInfo> superClasses();
+
+  public abstract ImmutableSet<MemberInfo> declaredMembers();
+
+  public final boolean containsMember(MemberInfo memberInfo) {
+    if (declaredMembers().contains(memberInfo)) {
+      return true;
+    }
+    for (ClassInfo superClass : superClasses()) {
+      if (superClass.containsMember(memberInfo)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** A member is either a method or a field. */
+  @AutoValue
+  public abstract static class MemberInfo implements Comparable<MemberInfo> {
+
+    public static MemberInfo create(String owner, String memberName, String descriptor) {
+      checkArgument(!Strings.isNullOrEmpty(owner), "Empty owner name: %s", owner);
+      checkArgument(!Strings.isNullOrEmpty(memberName), "Empty method name: %s", memberName);
+      checkArgument(!Strings.isNullOrEmpty(descriptor), "Empty descriptor: %s", descriptor);
+      return new AutoValue_ClassInfo_MemberInfo(owner, memberName, descriptor);
+    }
+
+    /** The declaring class of this member. */
+    public abstract String owner();
+
+    /** The name of the member. */
+    public abstract String memberName();
+
+    /** The descriptor of the member. */
+    public abstract String descriptor();
+
+    @Memoized
+    @Override
+    public abstract int hashCode();
+
+    @Override
+    public int compareTo(MemberInfo other) {
+      return ComparisonChain.start()
+          .compare(this.owner(), other.owner())
+          .compare(this.memberName(), other.memberName())
+          .compare(this.descriptor(), other.descriptor())
+          .result();
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/DepsCheckerClassVisitor.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/DepsCheckerClassVisitor.java
new file mode 100644
index 0000000..8c4b1ce
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/DepsCheckerClassVisitor.java
@@ -0,0 +1,338 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/** Checker to check whether a class has missing dependencies on its classpath. */
+public class DepsCheckerClassVisitor extends ClassVisitor {
+
+  private String internalName;
+  private final ClassCache classCache;
+  private final ResultCollector resultCollector;
+
+  private final DepsCheckerAnnotationVisitor defaultAnnotationChecker =
+      new DepsCheckerAnnotationVisitor();
+  private final DepsCheckerFieldVisitor defaultFieldChecker = new DepsCheckerFieldVisitor();
+  private final DepsCheckerMethodVisitor defaultMethodChecker = new DepsCheckerMethodVisitor();
+
+  public DepsCheckerClassVisitor(ClassCache classCache, ResultCollector resultCollector) {
+    super(Opcodes.ASM6);
+    this.classCache = classCache;
+    this.resultCollector = resultCollector;
+  }
+
+  @Override
+  public void visit(
+      int version,
+      int access,
+      String name,
+      String signature,
+      String superName,
+      String[] interfaces) {
+    checkState(internalName == null, "Cannot reuse this class visitor %s", getClass());
+    this.internalName = name;
+    checkInternalName(superName);
+    checkInternalNameArray(interfaces);
+    super.visit(version, access, name, signature, superName, interfaces);
+  }
+
+  @Override
+  public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+    checkDescriptor(desc);
+    return defaultAnnotationChecker;
+  }
+
+  @Override
+  public FieldVisitor visitField(
+      int access, String name, String desc, String signature, Object value) {
+    checkDescriptor(desc);
+    return defaultFieldChecker;
+  }
+
+  @Override
+  public MethodVisitor visitMethod(
+      int access, String name, String desc, String signature, String[] exceptions) {
+    checkInternalNameArray(exceptions);
+    checkDescriptor(desc);
+    return defaultMethodChecker;
+  }
+
+  @Override
+  public AnnotationVisitor visitTypeAnnotation(
+      int typeRef, TypePath typePath, String desc, boolean visible) {
+    checkDescriptor(desc);
+    return defaultAnnotationChecker;
+  }
+
+  private void checkMember(String owner, String name, String desc) {
+    checkDescriptor(desc);
+    AbstractClassEntryState state = checkInternalName(owner);
+
+    Optional<ClassInfo> classInfo = state.classInfo();
+    if (!classInfo.isPresent()) {
+      checkState(state.isMissingState(), "The state should be MissingState. %s", state);
+      return; // The class is already missing.
+    }
+    MemberInfo member = MemberInfo.create(owner, name, desc);
+    if (!classInfo.get().containsMember(member)) {
+      resultCollector.addMissingMember(member);
+    }
+  }
+
+  private void checkDescriptor(String desc) {
+    checkType(Type.getType(desc));
+  }
+
+  private void checkType(Type type) {
+    switch (type.getSort()) {
+      case Type.BOOLEAN:
+      case Type.BYTE:
+      case Type.CHAR:
+      case Type.SHORT:
+      case Type.INT:
+      case Type.LONG:
+      case Type.FLOAT:
+      case Type.DOUBLE:
+      case Type.VOID:
+        return; // Ignore primitive types.
+      case Type.ARRAY:
+        checkType(type.getElementType());
+        return;
+      case Type.METHOD:
+        for (Type argumentType : type.getArgumentTypes()) {
+          checkType(argumentType);
+        }
+        checkType(type.getReturnType());
+        return;
+      case Type.OBJECT:
+        checkInternalName(type.getInternalName());
+        return;
+      default:
+        throw new UnsupportedOperationException("Unhandled type: " + type);
+    }
+  }
+
+  private AbstractClassEntryState checkInternalName(String internalName) {
+    AbstractClassEntryState state = classCache.getClassState(internalName);
+    if (state.isMissingState()) {
+      resultCollector.addMissingOrIncompleteClass(internalName, state);
+    } else if (state.isIncompleteState()) {
+      String missingAncestor = state.asIncompleteState().getMissingAncestor();
+      AbstractClassEntryState ancestorState = classCache.getClassState(missingAncestor);
+      checkState(
+          ancestorState.isMissingState(), "The ancestor should be missing. %s", ancestorState);
+      resultCollector.addMissingOrIncompleteClass(missingAncestor, ancestorState);
+      resultCollector.addMissingOrIncompleteClass(internalName, state);
+    }
+    return state;
+  }
+
+  private void checkInternalNameArray(@Nullable String[] internalNames) {
+    if (internalNames == null) {
+      return;
+    }
+    for (String internalName : internalNames) {
+      checkInternalName(internalName);
+    }
+  }
+
+  private static final ImmutableSet<Class<?>> PRIMITIVE_TYPES =
+      ImmutableSet.of(
+          Boolean.class,
+          Byte.class,
+          Short.class,
+          Character.class,
+          Integer.class,
+          Long.class,
+          Float.class,
+          Double.class,
+          String.class);
+
+  /** Annotation checker to check for missing classes in the annotation body. */
+  private class DepsCheckerAnnotationVisitor extends AnnotationVisitor {
+
+    DepsCheckerAnnotationVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String name, String desc) {
+      checkDescriptor(desc);
+      return this; // Recursively reuse this annotation visitor.
+    }
+
+    @Override
+    public void visit(String name, Object value) {
+      if (value instanceof Type) {
+        checkType(((Type) value)); // Class literals.
+        return;
+      }
+      if (PRIMITIVE_TYPES.contains(value.getClass())) {
+        checkType(Type.getType(value.getClass()));
+        return;
+      }
+      throw new UnsupportedOperationException("Unhandled value " + value);
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String name) {
+      return this; // Recursively reuse this annotation visitor.
+    }
+
+    @Override
+    public void visitEnum(String name, String desc, String value) {
+      checkMember(Type.getType(desc).getInternalName(), value, desc);
+    }
+  }
+
+  /** Field checker to check for missing classes in the field declaration. */
+  private class DepsCheckerFieldVisitor extends FieldVisitor {
+
+    DepsCheckerFieldVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+
+    @Override
+    public AnnotationVisitor visitTypeAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+  }
+
+  /** Method visitor to check whether there are missing classes in the method body. */
+  private class DepsCheckerMethodVisitor extends MethodVisitor {
+
+    DepsCheckerMethodVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+
+    @Override
+    public AnnotationVisitor visitTypeAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+
+    @Override
+    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+
+    @Override
+    public void visitLocalVariable(
+        String name, String desc, String signature, Label start, Label end, int index) {
+      checkDescriptor(desc);
+      super.visitLocalVariable(name, desc, signature, start, end, index);
+    }
+
+    @Override
+    public void visitTypeInsn(int opcode, String type) {
+      checkInternalName(type);
+      super.visitTypeInsn(opcode, type);
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      checkMember(owner, name, desc);
+      super.visitFieldInsn(opcode, owner, name, desc);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      checkMember(owner, name, desc);
+      super.visitMethodInsn(opcode, owner, name, desc, itf);
+    }
+
+    @Override
+    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
+      checkDescriptor(desc);
+      checkHandle(bsm);
+      for (Object bsmArg : bsmArgs) {
+        if (bsmArg instanceof Type) {
+          checkType(((Type) bsmArg)); // Class literals.
+          continue;
+        }
+        if (PRIMITIVE_TYPES.contains(bsmArg.getClass())) {
+          checkType(Type.getType(bsmArg.getClass()));
+          continue;
+        }
+        if (bsmArg instanceof Handle) {
+          checkHandle((Handle) bsmArg);
+          continue;
+        }
+        throw new UnsupportedOperationException("Unsupported bsmarg type: " + bsmArg);
+      }
+      super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
+    }
+
+    private void checkHandle(Handle handle) {
+      checkMember(handle.getOwner(), handle.getName(), handle.getDesc());
+    }
+
+    @Override
+    public void visitMultiANewArrayInsn(String desc, int dims) {
+      checkDescriptor(desc);
+      super.visitMultiANewArrayInsn(desc, dims);
+    }
+
+    @Override
+    public AnnotationVisitor visitTryCatchAnnotation(
+        int typeRef, TypePath typePath, String desc, boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+
+    @Override
+    public AnnotationVisitor visitLocalVariableAnnotation(
+        int typeRef,
+        TypePath typePath,
+        Label[] start,
+        Label[] end,
+        int[] index,
+        String desc,
+        boolean visible) {
+      checkDescriptor(desc);
+      return defaultAnnotationChecker;
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ImportDepsChecker.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ImportDepsChecker.java
new file mode 100644
index 0000000..e73a665
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ImportDepsChecker.java
@@ -0,0 +1,153 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Collectors;
+import java.util.zip.ZipFile;
+import org.objectweb.asm.ClassReader;
+
+/**
+ * Checker that checks the classes in the input jars have complete dependencies. If not, output the
+ * missing dependencies to a file.
+ */
+public class ImportDepsChecker implements Closeable {
+
+  private final ClassCache classCache;
+  private final ResultCollector resultCollector;
+  private final ImmutableList<Path> inputJars;
+
+  public ImportDepsChecker(
+      ImmutableList<Path> bootclasspath,
+      ImmutableList<Path> classpath,
+      ImmutableList<Path> inputJars)
+      throws IOException {
+    this.classCache =
+        new ClassCache(
+            ImmutableList.<Path>builder()
+                .addAll(bootclasspath)
+                .addAll(classpath)
+                .addAll(inputJars)
+                .build());
+    this.resultCollector = new ResultCollector();
+    this.inputJars = inputJars;
+  }
+
+  public ImportDepsChecker check() throws IOException {
+    for (Path path : inputJars) {
+      try (ZipFile jarFile = new ZipFile(path.toFile())) {
+        jarFile
+            .stream()
+            .forEach(
+                entry -> {
+                  String name = entry.getName();
+                  if (!name.endsWith(".class")) {
+                    return;
+                  }
+                  try (InputStream inputStream = jarFile.getInputStream(entry)) {
+                    ClassReader reader = new ClassReader(inputStream);
+                    DepsCheckerClassVisitor checker =
+                        new DepsCheckerClassVisitor(classCache, resultCollector);
+                    reader.accept(checker, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+                  } catch (IOException e) {
+                    throw new IOError(e);
+                  }
+                });
+      }
+    }
+    return this;
+  }
+
+  private static final String INDENT = "    ";
+
+  public void saveResult(Path resultFile) throws IOException {
+    if (!Files.exists(resultFile)) {
+      Files.createFile(resultFile); // Make sure the file exists.
+    }
+    try (BufferedWriter writer = Files.newBufferedWriter(resultFile, StandardCharsets.UTF_8)) {
+      ImmutableList<String> missingClasses = resultCollector.getSortedMissingClassInternalNames();
+      for (String missing : missingClasses) {
+        writer.append("Missing ").append(missing.replace('/', '.')).append('\n');
+      }
+
+      ImmutableList<IncompleteState> incompleteClasses =
+          resultCollector.getSortedIncompleteClasses();
+      for (IncompleteState incomplete : incompleteClasses) {
+        writer
+            .append("Incomplete ancestor classpath for ")
+            .append(incomplete.classInfo().get().internalName().replace('/', '.'))
+            .append('\n');
+
+        ImmutableList<String> failurePath = incomplete.getResolutionFailurePath();
+        checkState(!failurePath.isEmpty(), "The resolution failure path is empty. %s", failurePath);
+        writer
+            .append(INDENT)
+            .append("missing ancestor: ")
+            .append(failurePath.get(failurePath.size() - 1).replace('/', '.'))
+            .append('\n');
+        writer
+            .append(INDENT)
+            .append("resolution failure path: ")
+            .append(
+                failurePath
+                    .stream()
+                    .map(internalName -> internalName.replace('/', '.'))
+                    .collect(Collectors.joining(" -> ")))
+            .append('\n');
+      }
+      ImmutableList<MemberInfo> missingMembers = resultCollector.getSortedMissingMembers();
+      for (MemberInfo missing : missingMembers) {
+        writer
+            .append("Missing member '")
+            .append(missing.memberName())
+            .append("' in class ")
+            .append(missing.owner().replace('/', '.'))
+            .append(" : name=")
+            .append(missing.memberName())
+            .append(", descriptor=")
+            .append(missing.descriptor())
+            .append('\n');
+      }
+      if (missingClasses.size() + incompleteClasses.size() + missingMembers.size() != 0) {
+        writer
+            .append("===Total===\n")
+            .append("missing=")
+            .append(String.valueOf(missingClasses.size()))
+            .append('\n')
+            .append("incomplete=")
+            .append(String.valueOf(incompleteClasses.size()))
+            .append('\n')
+            .append("missing_members=")
+            .append(String.valueOf(missingMembers.size()));
+      }
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    classCache.close();
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/Main.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/Main.java
new file mode 100644
index 0000000..d549bc2
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/Main.java
@@ -0,0 +1,162 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * A checker that checks the completeness of the dependencies of an import target (java_import or
+ * aar_import). If incomplete, it prints out the list of missing class names to the output file.
+ */
+public class Main {
+
+  /** Command line options. */
+  public static class Options extends OptionsBase {
+    @Option(
+      name = "input",
+      allowMultiple = true,
+      defaultValue = "",
+      category = "input",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = ExistingPathConverter.class,
+      abbrev = 'i',
+      help = "Input jars with classes to check the completeness of their dependencies."
+    )
+    public List<Path> inputJars;
+
+    @Option(
+      name = "classpath_entry",
+      allowMultiple = true,
+      defaultValue = "",
+      category = "input",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = ExistingPathConverter.class,
+      help =
+          "Ordered classpath (Jar) to resolve symbols in the --input jars, like javac's -cp flag."
+    )
+    public List<Path> classpath;
+
+    @Option(
+      name = "bootclasspath_entry",
+      allowMultiple = true,
+      defaultValue = "",
+      category = "input",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = ExistingPathConverter.class,
+      help =
+          "Bootclasspath that was used to compile the --input Jar with, like javac's "
+              + "-bootclasspath_entry flag (required)."
+    )
+    public List<Path> bootclasspath;
+
+    @Option(
+      name = "output",
+      defaultValue = "",
+      category = "output",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      converter = PathConverter.class,
+      help = "Output path to save the result."
+    )
+    public Path output;
+  }
+
+  public static void main(String[] args) throws IOException {
+    Options options = parseCommandLineOptions(args);
+    try (ImportDepsChecker checker =
+        new ImportDepsChecker(
+            ImmutableList.copyOf(options.bootclasspath),
+            ImmutableList.copyOf(options.classpath),
+            ImmutableList.copyOf(options.inputJars))) {
+      checker.check().saveResult(options.output);
+    }
+  }
+
+  private static Options parseCommandLineOptions(String[] args) throws IOException {
+    OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class);
+    optionsParser.setAllowResidue(false);
+    optionsParser.enableParamsFileSupport(
+        new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()));
+    optionsParser.parseAndExitUponError(args);
+    Options options = optionsParser.getOptions(Options.class);
+
+    checkArgument(!options.inputJars.isEmpty(), "--input is required");
+    checkArgument(
+        !options.classpath.isEmpty(), "--classpath_entry is required, at least the bootclasspath");
+    checkArgument(!options.bootclasspath.isEmpty(), "--bootclasspath_entry is required");
+    return options;
+  }
+
+  /** Validating converter for Paths. A Path is considered valid if it resolves to a file. */
+  public static class PathConverter implements Converter<Path> {
+
+    private final boolean mustExist;
+
+    public PathConverter() {
+      this.mustExist = false;
+    }
+
+    protected PathConverter(boolean mustExist) {
+      this.mustExist = mustExist;
+    }
+
+    @Override
+    public Path convert(String input) throws OptionsParsingException {
+      try {
+        Path path = FileSystems.getDefault().getPath(input);
+        if (mustExist && !Files.exists(path)) {
+          throw new OptionsParsingException(
+              String.format("%s is not a valid path: it does not exist.", input));
+        }
+        return path;
+      } catch (InvalidPathException e) {
+        throw new OptionsParsingException(
+            String.format("%s is not a valid path: %s.", input, e.getMessage()), e);
+      }
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "a valid filesystem path";
+    }
+  }
+
+  /**
+   * Validating converter for Paths. A Path is considered valid if it resolves to a file and exists.
+   */
+  public static class ExistingPathConverter extends PathConverter {
+    public ExistingPathConverter() {
+      super(true);
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ResultCollector.java b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ResultCollector.java
new file mode 100644
index 0000000..34253aa
--- /dev/null
+++ b/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ResultCollector.java
@@ -0,0 +1,71 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/** The collector that saves all the missing classes. */
+public class ResultCollector {
+
+  private final HashSet<String> missingClasss = new HashSet<>();
+  private final HashMap<String, IncompleteState> incompleteClasses = new HashMap<>();
+  private final HashSet<MemberInfo> missingMembers = new HashSet<>();
+
+  public ResultCollector() {}
+
+  public void addMissingOrIncompleteClass(String internalName, AbstractClassEntryState state) {
+    checkArgument(
+        internalName.length() > 0 && Character.isJavaIdentifierStart(internalName.charAt(0)),
+        "The internal name is invalid. %s",
+        internalName);
+    if (state.isIncompleteState()) {
+      IncompleteState oldValue = incompleteClasses.put(internalName, state.asIncompleteState());
+      checkState(
+          oldValue == null || oldValue == state,
+          "The old value and the new value are not the same object. old=%s, new=%s",
+          oldValue,
+          state);
+      missingClasss.add(state.asIncompleteState().getMissingAncestor()); // Add the real missing.
+    } else if (state.isMissingState()) {
+      missingClasss.add(internalName);
+    } else {
+      throw new UnsupportedOperationException("Unsupported state " + state);
+    }
+  }
+
+  public void addMissingMember(MemberInfo member) {
+    missingMembers.add(member);
+  }
+
+  public ImmutableList<String> getSortedMissingClassInternalNames() {
+    return ImmutableList.sortedCopyOf(missingClasss);
+  }
+
+  public ImmutableList<IncompleteState> getSortedIncompleteClasses() {
+    return ImmutableList.sortedCopyOf(
+        Comparator.comparing(a -> a.classInfo().get().internalName()), incompleteClasses.values());
+  }
+
+  public ImmutableList<MemberInfo> getSortedMissingMembers() {
+    return ImmutableList.sortedCopyOf(missingMembers);
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/AbstractClassCacheTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/AbstractClassCacheTest.java
new file mode 100644
index 0000000..33460f6
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/AbstractClassCacheTest.java
@@ -0,0 +1,96 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/** Base class for {@link ClassCacheTest} and {@link DepsCheckerClassVisitorTest}. */
+public abstract class AbstractClassCacheTest {
+
+  static final String PACKAGE_NAME = "com/google/devtools/build/importdeps/testdata/";
+
+  final Path bootclasspath = getPathFromSystemProperty("classcache.test.bootclasspath");
+
+  final Path clientJar = getPathFromSystemProperty("classcache.test.Client");
+  final ImmutableList<String> clientJarPositives =
+      ImmutableList.of("Client", "Client$NestedAnnotation")
+          .stream()
+          .map(s -> PACKAGE_NAME + s)
+          .collect(ImmutableList.toImmutableList());
+
+  final Path libraryJar = getPathFromSystemProperty("classcache.test.Library");
+  final ImmutableList<String> libraryJarPositives =
+      ImmutableList.<String>builder()
+          .add("Library")
+          .addAll(
+              IntStream.range(1, 11)
+                  .mapToObj(i -> "Library$Class" + i)
+                  .collect(ImmutableList.toImmutableList()))
+          .build()
+          .stream()
+          .map(s -> PACKAGE_NAME + s)
+          .collect(ImmutableList.toImmutableList());
+
+  final Path libraryWoMembersJar = getPathFromSystemProperty("classcache.test.Library_no_members");
+
+  final Path libraryAnnotationsJar =
+      getPathFromSystemProperty("classcache.test.LibraryAnnotations");
+  final ImmutableList<String> libraryAnnotationsJarPositives =
+      ImmutableList.<String>builder()
+          .add("LibraryAnnotations")
+          .addAll(
+              Stream.of(
+                      "ClassAnnotation",
+                      "MethodAnnotation",
+                      "FieldAnnotation",
+                      "ConstructorAnnotation",
+                      "ParameterAnnotation",
+                      "TypeAnnotation",
+                      "AnnotationAnnotation")
+                  .map(name -> "LibraryAnnotations$" + name)
+                  .collect(ImmutableList.toImmutableList()))
+          .build()
+          .stream()
+          .map(s -> PACKAGE_NAME + s)
+          .collect(ImmutableList.toImmutableList());
+
+  final Path libraryExceptionJar = getPathFromSystemProperty("classcache.test.LibraryException");
+  final ImmutableList<String> libraryExceptionJarPositives =
+      ImmutableList.of(PACKAGE_NAME + "LibraryException");
+
+  final Path libraryInterfaceJar = getPathFromSystemProperty("classcache.test.LibraryInterface");
+  final ImmutableList<String> libraryInterfacePositives =
+      ImmutableList.of(PACKAGE_NAME + "LibraryInterface", PACKAGE_NAME + "LibraryInterface$Func");
+
+  static Path getPathFromSystemProperty(String propertyName) {
+    String path =
+        checkNotNull(
+            System.getProperty(propertyName), "The system property %s is not set.", propertyName);
+    return Paths.get(path);
+  }
+
+  /** Flattern a list of lists. */
+  static <T> ImmutableList<T> combine(ImmutableList<T>... lists) {
+    return Arrays.stream(lists)
+        .flatMap(ImmutableList::stream)
+        .collect(ImmutableList.toImmutableList());
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/BUILD b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/BUILD
new file mode 100644
index 0000000..bbf7085
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/BUILD
@@ -0,0 +1,160 @@
+# Description:
+#   Tests for the checker to check the completeness of the deps of java_import or aar_import targets.
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+java_test(
+    name = "ClassInfoTest",
+    srcs = ["ClassInfoTest.java"],
+    deps = [
+        "//src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:import_deps_checker",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "ResultCollectorTest",
+    srcs = ["ResultCollectorTest.java"],
+    deps = [
+        "//src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:import_deps_checker",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "LazyClassEntryStateTest",
+    srcs = ["LazyClassEntryStateTest.java"],
+    deps = [
+        "//src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:import_deps_checker",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "ClassCacheTest",
+    srcs = [
+        "AbstractClassCacheTest.java",
+        "ClassCacheTest.java",
+    ],
+    data = [
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_client",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryAnnotations",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryException",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryInterface",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library_no_members",
+        "//third_party/java/jdk:jdk8_rt_jar",
+    ],
+    jvm_flags = [
+        "-Dclasscache.test.bootclasspath=$(location //third_party/java/jdk:jdk8_rt_jar)",
+        "-Dclasscache.test.Client=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_client)",
+        "-Dclasscache.test.Library=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library)",
+        "-Dclasscache.test.Library_no_members=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library_no_members)",
+        "-Dclasscache.test.LibraryAnnotations=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryAnnotations)",
+        "-Dclasscache.test.LibraryException=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryException)",
+        "-Dclasscache.test.LibraryInterface=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryInterface)",
+    ],
+    test_class = "com.google.devtools.build.importdeps.ClassCacheTest",
+    deps = [
+        "//src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:import_deps_checker",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
+    name = "DepsCheckerClassVisitorTest",
+    srcs = [
+        "AbstractClassCacheTest.java",
+        "DepsCheckerClassVisitorTest.java",
+    ],
+    data = [
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_client",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryAnnotations",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryException",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryInterface",
+        "//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library_no_members",
+        "//third_party/java/jdk:jdk8_rt_jar",
+    ],
+    jvm_flags = [
+        "-Dclasscache.test.bootclasspath=$(location //third_party/java/jdk:jdk8_rt_jar)",
+        "-Dclasscache.test.Client=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_client)",
+        "-Dclasscache.test.Library=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library)",
+        "-Dclasscache.test.Library_no_members=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_Library_no_members)",
+        "-Dclasscache.test.LibraryAnnotations=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryAnnotations)",
+        "-Dclasscache.test.LibraryException=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryException)",
+        "-Dclasscache.test.LibraryInterface=$(location //src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata:testdata_lib_LibraryInterface)",
+    ],
+    test_class = "com.google.devtools.build.importdeps.DepsCheckerClassVisitorTest",
+    deps = [
+        "//src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:import_deps_checker",
+        "//third_party:asm",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+        "//third_party/java/jdk:jdk8_rt_jar",
+    ],
+)
+
+load("//src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps:tests.bzl", "create_golden_test")
+
+create_golden_test(
+    name = "bootclasspath_missing_golden_test",
+    golden_file = "golden_bootclasspath_missing.txt",
+    has_bootclasspath = False,
+    missing_jar = None,
+)
+
+create_golden_test(
+    name = "library_exception_missing_golden_test",
+    golden_file = "golden_library_exception_missing.txt",
+    has_bootclasspath = True,
+    missing_jar = "testdata_lib_LibraryException",
+)
+
+create_golden_test(
+    name = "library_annotation_missing_golden_test",
+    golden_file = "golden_library_annotation_missing.txt",
+    has_bootclasspath = True,
+    missing_jar = "testdata_lib_LibraryAnnotations",
+)
+
+create_golden_test(
+    name = "library_missing_golden_test",
+    golden_file = "golden_library_missing.txt",
+    has_bootclasspath = True,
+    missing_jar = "testdata_lib_Library",
+)
+
+create_golden_test(
+    name = "library_interface_missing_golden_test",
+    golden_file = "golden_library_interface_missing.txt",
+    has_bootclasspath = True,
+    missing_jar = "testdata_lib_LibraryInterface",
+)
+
+create_golden_test(
+    name = "library_members_missing_golden_test",
+    golden_file = "golden_library_members_missing.txt",
+    has_bootclasspath = True,
+    missing_jar = "testdata_lib_Library",
+    replacing_jar = "testdata_lib_Library_no_members",
+)
+
+create_golden_test(
+    name = "complete_classpath_golden_test",
+    golden_file = "golden_complete_classpath.txt",
+    has_bootclasspath = True,
+    missing_jar = None,
+    replacing_jar = None,
+)
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassCacheTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassCacheTest.java
new file mode 100644
index 0000000..241afd9
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassCacheTest.java
@@ -0,0 +1,142 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.ExistingState;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link ClassCache}. */
+@RunWith(JUnit4.class)
+public class ClassCacheTest extends AbstractClassCacheTest {
+
+  @Test
+  public void testLibraryJar() throws Exception {
+    try (ClassCache cache = new ClassCache(bootclasspath, libraryJar)) {
+      assertCache(
+          cache,
+          libraryJarPositives,
+          combine(
+              libraryInterfacePositives,
+              libraryAnnotationsJarPositives,
+              libraryExceptionJarPositives));
+    }
+  }
+
+  @Test
+  public void testClientJarWithSuperClasses() throws IOException {
+    try (ClassCache cache =
+        new ClassCache(bootclasspath, clientJar, libraryJar, libraryInterfaceJar)) {
+      assertCache(
+          cache,
+          clientJarPositives,
+          combine(libraryExceptionJarPositives, libraryAnnotationsJarPositives));
+    }
+  }
+
+  @Test
+  public void testClientJarWithoutSuperClasses() throws IOException {
+    try (ClassCache cache = new ClassCache(bootclasspath, clientJar)) {
+      // Client should be incomplete, as its parent class and interfaces are not available on the
+      // classpath. The following is the resolution path.
+      {
+        AbstractClassEntryState state = cache.getClassState(PACKAGE_NAME + "Client");
+        assertThat(state.isIncompleteState()).isTrue();
+
+        ImmutableList<String> failureCause = state.asIncompleteState().getResolutionFailurePath();
+        assertThat(failureCause).containsExactly(PACKAGE_NAME + "Library").inOrder();
+      }
+      assertThat(cache.getClassState(PACKAGE_NAME + "Client").isIncompleteState()).isTrue();
+      assertThat(cache.getClassState(PACKAGE_NAME + "Client$NestedAnnotation"))
+          .isInstanceOf(ExistingState.class);
+      assertThat(cache.getClassState(PACKAGE_NAME + "Client$NestedAnnotation").isExistingState())
+          .isTrue();
+      assertThat(cache.getClassState("java/lang/Object").isExistingState()).isTrue();
+      assertThat(cache.getClassState("java/util/List").isExistingState()).isTrue();
+    }
+  }
+
+  @Test
+  public void testLibraryException() throws IOException {
+    try (ClassCache cache = new ClassCache(bootclasspath, libraryExceptionJar)) {
+      assertCache(
+          cache,
+          libraryExceptionJarPositives,
+          combine(libraryAnnotationsJarPositives, libraryInterfacePositives, libraryJarPositives));
+    }
+  }
+
+  @Test
+  public void testLibraryAnnotations() throws IOException {
+    try (ClassCache cache = new ClassCache(bootclasspath, libraryAnnotationsJar)) {
+      assertCache(
+          cache,
+          libraryAnnotationsJarPositives,
+          combine(libraryExceptionJarPositives, libraryInterfacePositives, libraryJarPositives));
+    }
+  }
+
+  @Test
+  public void testCannotAccessClosedCache() throws IOException {
+    ClassCache cache = new ClassCache(ImmutableList.of());
+    cache.close();
+    cache.close(); // Can close multiple times.
+    assertThrows(IllegalStateException.class, () -> cache.getClassState("empty"));
+  }
+
+  /**
+   * A regression test. First query the super class, which does not exist. Then query the subclass,
+   * which does not exist either.
+   */
+  @Test
+  public void testSuperNotExistThenSubclassNotExist() throws IOException {
+    try (ClassCache cache =
+        new ClassCache(
+            libraryJar, libraryJar, libraryAnnotationsJar, libraryInterfaceJar, clientJar)) {
+      assertThat(
+              cache
+                  .getClassState("com/google/devtools/build/importdeps/testdata/Library$Class9")
+                  .isIncompleteState())
+          .isTrue();
+      assertThat(
+              cache
+                  .getClassState("com/google/devtools/build/importdeps/testdata/Library$Class10")
+                  .isIncompleteState())
+          .isTrue();
+    }
+  }
+
+  private void assertCache(
+      ClassCache cache, ImmutableList<String> positives, ImmutableList<String> negatives) {
+    for (String positive : positives) {
+      AbstractClassEntryState state = cache.getClassState(positive);
+      assertWithMessage(positive).that(state.isExistingState()).isTrue();
+      assertWithMessage(positive).that(state.asExistingState()).isInstanceOf(ExistingState.class);
+      assertWithMessage(positive)
+          .that(state.asExistingState().classInfo().get().internalName())
+          .isEqualTo(positive);
+    }
+    for (String negative : negatives) {
+      AbstractClassEntryState state = cache.getClassState(negative);
+      assertWithMessage(negative).that(state.isExistingState()).isFalse();
+    }
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassInfoTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassInfoTest.java
new file mode 100644
index 0000000..54d53f2
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassInfoTest.java
@@ -0,0 +1,82 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link ClassInfo} */
+@RunWith(JUnit4.class)
+public class ClassInfoTest {
+
+  public static final String JAVA_LANG_OBJECT = "java/lang/Object";
+  private final MemberInfo hashCodeMethod = MemberInfo.create(JAVA_LANG_OBJECT, "hashCode", "()I");
+  private final MemberInfo sizeMethod = MemberInfo.create(JAVA_LANG_OBJECT, "clear", "()V");
+
+  private final ClassInfo objectClass =
+      ClassInfo.create(JAVA_LANG_OBJECT, ImmutableList.of(), ImmutableSet.of(hashCodeMethod));
+
+  private final ClassInfo listClass =
+      ClassInfo.create(
+          "java/util/List", ImmutableList.of(objectClass), ImmutableSet.of(sizeMethod));
+
+  @Test
+  public void testMemberInfo() {
+    MemberInfo memberInfo = MemberInfo.create(JAVA_LANG_OBJECT, "a", "I");
+    assertThat(memberInfo.memberName()).isEqualTo("a");
+    assertThat(memberInfo.descriptor()).isEqualTo("I");
+    assertThat(memberInfo).isEqualTo(MemberInfo.create("java/lang/Object", "a", "I"));
+
+    assertThat(hashCodeMethod).isEqualTo(MemberInfo.create("java/lang/Object", "hashCode", "()I"));
+    assertThat(sizeMethod).isEqualTo(MemberInfo.create("java/lang/Object", "clear", "()V"));
+  }
+
+  @Test
+  public void testClassInfoCorrectlySet() {
+    assertThat(objectClass.internalName()).isEqualTo("java/lang/Object");
+    assertThat(objectClass.declaredMembers())
+        .containsExactly(MemberInfo.create("java/lang/Object", "hashCode", "()I"))
+        .inOrder();
+    assertThat(objectClass.containsMember(MemberInfo.create("java/lang/Object", "hashCode", "()I")))
+        .isTrue();
+
+    assertThat(listClass.internalName()).isEqualTo("java/util/List");
+    assertThat(listClass.declaredMembers()).containsExactly(sizeMethod);
+    assertThat(listClass.containsMember(hashCodeMethod)).isTrue();
+  }
+
+  @Test
+  public void testContainsMember() {
+    ClassInfo parent = objectClass;
+    ClassInfo child = listClass;
+    assertThat(child.superClasses()).contains(parent);
+    assertThat(parent.containsMember(MemberInfo.create("java/lang/Object", "hashCode", "()I")))
+        .isTrue();
+    assertThat(parent.containsMember(MemberInfo.create("java/lang/Object", "size", "()I")))
+        .isFalse();
+    assertThat(parent.containsMember(MemberInfo.create("java/lang/Object", "clear", "()V")))
+        .isFalse();
+
+    assertThat(child.containsMember(MemberInfo.create("java/lang/Object", "hashCode", "()I")))
+        .isTrue();
+    assertThat(child.containsMember(MemberInfo.create("java/lang/Object", "clear", "()V")))
+        .isTrue();
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/DepsCheckerClassVisitorTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/DepsCheckerClassVisitorTest.java
new file mode 100644
index 0000000..3d19b06
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/DepsCheckerClassVisitorTest.java
@@ -0,0 +1,128 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+
+/** Test for {@link DepsCheckerClassVisitor}. */
+@RunWith(JUnit4.class)
+public class DepsCheckerClassVisitorTest extends AbstractClassCacheTest {
+
+  @Test
+  public void testMissingLibraryException() throws IOException {
+    assertThat(
+            getMissingClassesInClient(
+                bootclasspath, libraryJar, libraryInterfaceJar, libraryAnnotationsJar, clientJar))
+        .containsExactlyElementsIn(libraryExceptionJarPositives);
+  }
+
+  @Test
+  public void testMissingLibraryInterface() throws IOException {
+    assertThat(
+            getMissingClassesInClient(
+                bootclasspath, libraryJar, libraryAnnotationsJar, libraryExceptionJar, clientJar))
+        .containsExactlyElementsIn(libraryInterfacePositives);
+  }
+
+  @Test
+  public void testMissingLibraryAnnotations() throws IOException {
+    assertThat(
+            getMissingClassesInClient(
+                bootclasspath, libraryJar, libraryExceptionJar, libraryInterfaceJar, clientJar))
+        .containsExactlyElementsIn(libraryAnnotationsJarPositives);
+  }
+
+  @Test
+  public void testMissingLibraryInClient() throws IOException {
+    assertThat(
+            getMissingClassesInClient(
+                bootclasspath,
+                libraryExceptionJar,
+                libraryInterfaceJar,
+                libraryAnnotationsJar,
+                clientJar))
+        .containsExactlyElementsIn(libraryJarPositives);
+  }
+
+  @Test
+  public void testMissingMembersInClient() throws IOException {
+    ResultCollector collector =
+        getResultCollector(
+            bootclasspath,
+            libraryAnnotationsJar,
+            libraryInterfaceJar,
+            libraryWoMembersJar,
+            libraryExceptionJar,
+            clientJar);
+    assertThat(collector.getSortedMissingClassInternalNames()).isEmpty();
+    assertThat(collector.getSortedMissingMembers())
+        .containsExactly(
+            MemberInfo.create(
+                "com/google/devtools/build/importdeps/testdata/Library$Class1",
+                "I",
+                "Lcom/google/devtools/build/importdeps/testdata/Library$Class1;"),
+            MemberInfo.create(
+                "com/google/devtools/build/importdeps/testdata/Library$Class3",
+                "field",
+                "Lcom/google/devtools/build/importdeps/testdata/Library$Class4;"),
+            MemberInfo.create(
+                "com/google/devtools/build/importdeps/testdata/Library$Class4",
+                "createClass5",
+                "()Lcom/google/devtools/build/importdeps/testdata/Library$Class5;"),
+            MemberInfo.create(
+                "com/google/devtools/build/importdeps/testdata/Library$Class5",
+                "create",
+                "(Lcom/google/devtools/build/importdeps/testdata/Library$Class7;)"
+                    + "Lcom/google/devtools/build/importdeps/testdata/Library$Class6;"))
+        .inOrder();
+  }
+
+  private ImmutableList<String> getMissingClassesInClient(Path... classpath) throws IOException {
+    ResultCollector resultCollector = getResultCollector(classpath);
+    return resultCollector.getSortedMissingClassInternalNames();
+  }
+
+  private ResultCollector getResultCollector(Path... classpath) throws IOException {
+    ImmutableList<String> clientClasses =
+        ImmutableList.of(PACKAGE_NAME + "Client", PACKAGE_NAME + "Client$NestedAnnotation");
+    ResultCollector resultCollector = new ResultCollector();
+    try (ClassCache cache = new ClassCache(ImmutableList.copyOf(classpath));
+        ZipFile zipFile = new ZipFile(clientJar.toFile())) {
+
+      AbstractClassEntryState state = cache.getClassState("java/lang/invoke/LambdaMetafactory");
+      System.out.println(state);
+      for (String clientClass : clientClasses) {
+        ZipEntry entry = zipFile.getEntry(clientClass + ".class");
+        try (InputStream classStream = zipFile.getInputStream(entry)) {
+          ClassReader reader = new ClassReader(classStream);
+          DepsCheckerClassVisitor checker = new DepsCheckerClassVisitor(cache, resultCollector);
+          reader.accept(checker, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+        }
+      }
+    }
+    return resultCollector;
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/LazyClassEntryStateTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/LazyClassEntryStateTest.java
new file mode 100644
index 0000000..b2bc7f2
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/LazyClassEntryStateTest.java
@@ -0,0 +1,97 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.ExistingState;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.MissingState;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link AbstractClassEntryState} */
+@RunWith(JUnit4.class)
+public class LazyClassEntryStateTest {
+
+  public static final String LIST_CLASS_NAME = "java/util/List";
+  public static final ImmutableSet<MemberInfo> METHOD_LIST =
+      ImmutableSet.of(MemberInfo.create(LIST_CLASS_NAME, "hashCode", "()I"));
+  public static final ClassInfo LIST_CLASS_INFO =
+      ClassInfo.create(LIST_CLASS_NAME, ImmutableList.of(), METHOD_LIST);
+
+  @Test
+  public void testExistingState() {
+    ExistingState state = ExistingState.create(LIST_CLASS_INFO);
+
+    assertThat(state.isExistingState()).isTrue();
+    assertThat(state.isIncompleteState()).isFalse();
+    assertThat(state.isMissingState()).isFalse();
+
+    assertThat(state.asExistingState()).isSameAs(state);
+    assertThrows(IllegalStateException.class, () -> state.asIncompleteState());
+    assertThrows(IllegalStateException.class, () -> state.asMissingState());
+
+    ClassInfo classInfo = state.classInfo().get();
+    assertThat(classInfo.internalName()).isEqualTo("java/util/List");
+    assertThat(classInfo.declaredMembers()).hasSize(1);
+    assertThat(classInfo.declaredMembers())
+        .containsExactly(MemberInfo.create("java/util/List", "hashCode", "()I"));
+  }
+
+  @Test
+  public void testIncompleteState() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> IncompleteState.create(LIST_CLASS_INFO, ImmutableList.of()));
+    IncompleteState state =
+        IncompleteState.create(LIST_CLASS_INFO, ImmutableList.of("java/lang/Object"));
+
+    assertThat(state.isExistingState()).isFalse();
+    assertThat(state.isIncompleteState()).isTrue();
+    assertThat(state.isMissingState()).isFalse();
+
+    assertThat(state.asIncompleteState()).isSameAs(state);
+    assertThrows(IllegalStateException.class, () -> state.asExistingState());
+    assertThrows(IllegalStateException.class, () -> state.asMissingState());
+
+    ClassInfo classInfo = state.classInfo().get();
+    assertThat(classInfo.internalName()).isEqualTo("java/util/List");
+    assertThat(classInfo.declaredMembers()).hasSize(1);
+    assertThat(classInfo.declaredMembers())
+        .containsExactly(MemberInfo.create("java/util/List", "hashCode", "()I"));
+
+    ImmutableList<String> failurePath = state.getResolutionFailurePath();
+    assertThat(failurePath).hasSize(1);
+    assertThat(failurePath).containsExactly("java/lang/Object");
+  }
+
+  @Test
+  public void testMissingState() {
+    MissingState state = MissingState.singleton();
+
+    assertThat(state.isMissingState()).isTrue();
+    assertThat(state.isExistingState()).isFalse();
+    assertThat(state.isIncompleteState()).isFalse();
+
+    assertThat(state.asMissingState()).isSameAs(state);
+    assertThrows(IllegalStateException.class, () -> state.asExistingState());
+    assertThrows(IllegalStateException.class, () -> state.asIncompleteState());
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ResultCollectorTest.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ResultCollectorTest.java
new file mode 100644
index 0000000..0d78504
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ResultCollectorTest.java
@@ -0,0 +1,81 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState;
+import com.google.devtools.build.importdeps.AbstractClassEntryState.MissingState;
+import com.google.devtools.build.importdeps.ClassInfo.MemberInfo;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link ResultCollector}. */
+@RunWith(JUnit4.class)
+public class ResultCollectorTest {
+
+  private ResultCollector collector;
+
+  @Before
+  public void setup() throws IOException {
+    collector = new ResultCollector();
+  }
+
+  @Test
+  public void testEmptyCollector() throws IOException {
+    assertThat(collector.getSortedMissingClassInternalNames()).isEmpty();
+    assertThat(collector.getSortedMissingMembers()).isEmpty();
+    assertThat(collector.getSortedIncompleteClasses()).isEmpty();
+  }
+
+  @Test
+  public void testOneMissingClass() throws IOException {
+    collector.addMissingOrIncompleteClass("java.lang.String", MissingState.singleton());
+    assertThat(collector.getSortedMissingClassInternalNames()).containsExactly("java.lang.String");
+    assertThat(collector.getSortedMissingMembers()).isEmpty();
+
+    collector.addMissingMember(MemberInfo.create("java/lang/Object", "field", "I"));
+    assertThat(collector.getSortedMissingMembers())
+        .containsExactly(MemberInfo.create("java/lang/Object", "field", "I"));
+    assertThat(collector.getSortedMissingClassInternalNames()).containsExactly("java.lang.String");
+  }
+
+  @Test
+  public void testIncompleteClasses() throws IOException {
+    collector.addMissingOrIncompleteClass(
+        "java/lang/String",
+        IncompleteState.create(
+            ClassInfo.create("java/lang/String", ImmutableList.of(), ImmutableSet.of()),
+            ImmutableList.of("java/lang/Object")));
+    assertThat(collector.getSortedIncompleteClasses()).hasSize(1);
+    assertThat(collector.getSortedIncompleteClasses().get(0).classInfo().get())
+        .isEqualTo(ClassInfo.create("java/lang/String", ImmutableList.of(), ImmutableSet.of()));
+  }
+
+  @Test
+  public void testResultsAreSorted() throws IOException {
+    collector.addMissingOrIncompleteClass("java.lang.String", MissingState.singleton());
+    collector.addMissingOrIncompleteClass("java.lang.Integer", MissingState.singleton());
+    collector.addMissingOrIncompleteClass("java.lang.Object", MissingState.singleton());
+
+    assertThat(collector.getSortedMissingClassInternalNames())
+        .containsExactly("java.lang.String", "java.lang.Integer", "java.lang.Object");
+    assertThat(collector.getSortedMissingMembers()).isEmpty();
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_bootclasspath_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_bootclasspath_missing.txt
new file mode 100644
index 0000000..5a4e67e
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_bootclasspath_missing.txt
@@ -0,0 +1,87 @@
+Missing java.lang.Exception
+Missing java.lang.Object
+Missing java.lang.String
+Missing java.lang.annotation.Annotation
+Missing java.lang.annotation.ElementType
+Missing java.lang.annotation.Retention
+Missing java.lang.annotation.RetentionPolicy
+Missing java.lang.annotation.Target
+Missing java.lang.invoke.CallSite
+Missing java.lang.invoke.LambdaMetafactory
+Missing java.lang.invoke.MethodHandle
+Missing java.lang.invoke.MethodHandles$Lookup
+Missing java.lang.invoke.MethodType
+Missing java.util.Objects
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Client
+    missing ancestor: java.lang.Object
+    resolution failure path: com.google.devtools.build.importdeps.testdata.Library -> java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class1
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class10
+    missing ancestor: java.lang.Object
+    resolution failure path: com.google.devtools.build.importdeps.testdata.Library$Class9 -> java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class2
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class3
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class4
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class5
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class6
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class7
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class8
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Library$Class9
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$AnnotationAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ClassAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ConstructorAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$FieldAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$MethodAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ParameterAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryAnnotations$TypeAnnotation
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryException
+    missing ancestor: java.lang.Exception
+    resolution failure path: java.lang.Exception
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryInterface
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.LibraryInterface$Func
+    missing ancestor: java.lang.Object
+    resolution failure path: java.lang.Object
+===Total===
+missing=14
+incomplete=23
+missing_members=0
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_complete_classpath.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_complete_classpath.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_complete_classpath.txt
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_annotation_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_annotation_missing.txt
new file mode 100644
index 0000000..60af5de
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_annotation_missing.txt
@@ -0,0 +1,12 @@
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$AnnotationAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ClassAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ConstructorAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$FieldAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$MethodAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$ParameterAnnotation
+Missing com.google.devtools.build.importdeps.testdata.LibraryAnnotations$TypeAnnotation
+===Total===
+missing=8
+incomplete=0
+missing_members=0
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_exception_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_exception_missing.txt
new file mode 100644
index 0000000..767f4eb
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_exception_missing.txt
@@ -0,0 +1,5 @@
+Missing com.google.devtools.build.importdeps.testdata.LibraryException
+===Total===
+missing=1
+incomplete=0
+missing_members=0
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_interface_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_interface_missing.txt
new file mode 100644
index 0000000..3c3568e
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_interface_missing.txt
@@ -0,0 +1,9 @@
+Missing com.google.devtools.build.importdeps.testdata.LibraryInterface
+Missing com.google.devtools.build.importdeps.testdata.LibraryInterface$Func
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Client
+    missing ancestor: com.google.devtools.build.importdeps.testdata.LibraryInterface
+    resolution failure path: com.google.devtools.build.importdeps.testdata.LibraryInterface
+===Total===
+missing=2
+incomplete=1
+missing_members=0
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_members_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_members_missing.txt
new file mode 100644
index 0000000..7383116
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_members_missing.txt
@@ -0,0 +1,8 @@
+Missing member 'I' in class com.google.devtools.build.importdeps.testdata.Library$Class1 : name=I, descriptor=Lcom/google/devtools/build/importdeps/testdata/Library$Class1;
+Missing member 'field' in class com.google.devtools.build.importdeps.testdata.Library$Class3 : name=field, descriptor=Lcom/google/devtools/build/importdeps/testdata/Library$Class4;
+Missing member 'createClass5' in class com.google.devtools.build.importdeps.testdata.Library$Class4 : name=createClass5, descriptor=()Lcom/google/devtools/build/importdeps/testdata/Library$Class5;
+Missing member 'create' in class com.google.devtools.build.importdeps.testdata.Library$Class5 : name=create, descriptor=(Lcom/google/devtools/build/importdeps/testdata/Library$Class7;)Lcom/google/devtools/build/importdeps/testdata/Library$Class6;
+===Total===
+missing=0
+incomplete=0
+missing_members=4
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_missing.txt b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_missing.txt
new file mode 100644
index 0000000..12447fc
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_missing.txt
@@ -0,0 +1,18 @@
+Missing com.google.devtools.build.importdeps.testdata.Library
+Missing com.google.devtools.build.importdeps.testdata.Library$Class1
+Missing com.google.devtools.build.importdeps.testdata.Library$Class10
+Missing com.google.devtools.build.importdeps.testdata.Library$Class2
+Missing com.google.devtools.build.importdeps.testdata.Library$Class3
+Missing com.google.devtools.build.importdeps.testdata.Library$Class4
+Missing com.google.devtools.build.importdeps.testdata.Library$Class5
+Missing com.google.devtools.build.importdeps.testdata.Library$Class6
+Missing com.google.devtools.build.importdeps.testdata.Library$Class7
+Missing com.google.devtools.build.importdeps.testdata.Library$Class8
+Missing com.google.devtools.build.importdeps.testdata.Library$Class9
+Incomplete ancestor classpath for com.google.devtools.build.importdeps.testdata.Client
+    missing ancestor: com.google.devtools.build.importdeps.testdata.Library
+    resolution failure path: com.google.devtools.build.importdeps.testdata.Library
+===Total===
+missing=11
+incomplete=1
+missing_members=0
\ No newline at end of file
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_test.sh b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_test.sh
new file mode 100755
index 0000000..d3f090c
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_test.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+gold_file=$1
+shift
+
+if [ -d "$TEST_TMPDIR" ]; then
+  # Running as part of blaze test
+  tmpdir="$TEST_TMPDIR"
+else
+  # Manual run from command line
+  tmpdir="$(mktemp -d)"
+fi
+
+if [ -d "$TEST_UNDECLARED_OUTPUTS_DIR" ]; then
+  # Running as part of blaze test: capture test output
+  output="$TEST_UNDECLARED_OUTPUTS_DIR"
+else
+  # Manual run from command line: just write into temp dir
+  output="${tmpdir}"
+fi
+
+actual_file="${output}/actual_result.txt"
+# Run the checker command.
+$@ --output "${actual_file}" &> ${output}/checker_output.txt
+
+checker_ret=$?
+if [[ "${checker_ret}"  != 0 ]] ; then
+  echo "Checker error!!! ${checker_ret}"
+  cat ${output}/checker_output.txt
+  exit ${checker_ret}
+fi
+
+diff "${gold_file}" "${actual_file}"
+
+ret=$?
+if [[ "${ret}" != 0 ]] ; then
+  echo "============== Actual Output =============="
+  cat "${actual_file}"
+  echo "" # New line.
+  echo "==========================================="
+fi
+exit ${ret}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/BUILD b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/BUILD
new file mode 100644
index 0000000..ae40513
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/BUILD
@@ -0,0 +1,43 @@
+# Description:
+#   Test data for testing dependency checking.
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+java_library(
+    name = "testdata_client",
+    srcs = ["Client.java"],
+    deps = [":testdata_lib_neverlink"],
+)
+
+java_library(
+    name = "testdata_lib_neverlink",
+    srcs = glob(["Library*.java"]),
+    neverlink = 1,
+)
+
+java_library(
+    name = "testdata_lib_Library",
+    srcs = ["Library.java"],
+)
+
+java_library(
+    name = "testdata_lib_Library_no_members",
+    srcs = ["library_no_members/com/google/devtools/build/importdeps/testdata/Library.java"],
+)
+
+java_library(
+    name = "testdata_lib_LibraryAnnotations",
+    srcs = ["LibraryAnnotations.java"],
+)
+
+java_library(
+    name = "testdata_lib_LibraryException",
+    srcs = ["LibraryException.java"],
+)
+
+java_library(
+    name = "testdata_lib_LibraryInterface",
+    srcs = ["LibraryInterface.java"],
+)
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Client.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Client.java
new file mode 100644
index 0000000..6b833c3
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Client.java
@@ -0,0 +1,67 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.AnnotationAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.ClassAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.ConstructorAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.FieldAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.MethodAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.ParameterAnnotation;
+import com.google.devtools.build.importdeps.testdata.LibraryAnnotations.TypeAnnotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+
+/** Client class that uses several libraries. */
+@ClassAnnotation
+public class Client<@TypeAnnotation T> extends Library implements LibraryInterface {
+
+  @SuppressWarnings("unused")
+  @FieldAnnotation
+  private Library.Class1 field;
+
+  @SuppressWarnings("unused")
+  @FieldAnnotation
+  private LibraryAnnotations annotations;
+
+  public static final Class1 I = Class1.I;
+
+  @ConstructorAnnotation
+  public Client() {}
+
+  @MethodAnnotation
+  public void method(@ParameterAnnotation int p, Library.Class2 p2) throws LibraryException {
+    Objects.nonNull(p2); // javac9 silently uses Objects.
+    Class3 c3 = new Class3();
+    Class4 c4 = c3.field;
+    c3.field = c4;
+    Func<Class5> func = c4::createClass5;
+    Class5 class5 = func.get();
+    @SuppressWarnings("unused")
+    Class6 class6 = class5.create(new Class7());
+    @SuppressWarnings("unused")
+    Class8[][] array = new Class8[10][10];
+    Class9[] array2 = new Class9[10];
+    array2[0] = new Class10();
+  }
+
+  /** An inner annotation. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.TYPE)
+  @AnnotationAnnotation
+  public @interface NestedAnnotation {}
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Library.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Library.java
new file mode 100644
index 0000000..31049ba
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Library.java
@@ -0,0 +1,60 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+/** A library class for testing. */
+public class Library {
+
+  /** A library class for testing. */
+  public static class Class1 {
+    public static final Class1 I = new Class1();
+  }
+
+  /** A library class for testing. */
+  public static class Class2 {}
+
+  /** A library class for testing. */
+  public static class Class3 {
+    public Class4 field;
+  }
+
+  /** A library class for testing. */
+  public static class Class4 {
+    Class5 createClass5() {
+      return new Class5();
+    }
+  }
+
+  /** A library class for testing. */
+  public static class Class5 {
+    public Class6 create(Class7 class7) {
+      return new Class6();
+    }
+  }
+
+  /** A library class for testing. */
+  public static class Class6 {}
+
+  /** A library class for testing. */
+  public static class Class7 {}
+
+  /** A library class for testing. */
+  public static class Class8 {}
+
+  /** A library class for testing. */
+  public static class Class9 {}
+
+  /** A library class for testing. */
+  public static class Class10 extends Class9 {}
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryAnnotations.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryAnnotations.java
new file mode 100644
index 0000000..d0b883a
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryAnnotations.java
@@ -0,0 +1,57 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** A library annotation for testing. */
+public class LibraryAnnotations {
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.TYPE)
+  public @interface ClassAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  public @interface MethodAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.FIELD)
+  public @interface FieldAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.CONSTRUCTOR)
+  public @interface ConstructorAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.PARAMETER)
+  public @interface ParameterAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+  public @interface TypeAnnotation {}
+
+  /** A library annotation for testing. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.ANNOTATION_TYPE)
+  public @interface AnnotationAnnotation {}
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryException.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryException.java
new file mode 100644
index 0000000..ff572a0
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryException.java
@@ -0,0 +1,17 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+/** A library exception for testing. */
+public class LibraryException extends Exception {}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryInterface.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryInterface.java
new file mode 100644
index 0000000..649adc1
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryInterface.java
@@ -0,0 +1,23 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+/** A library interface for testing. */
+public interface LibraryInterface {
+
+  /** A nested interface for testing. */
+  interface Func<T> {
+    T get();
+  }
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/library_no_members/com/google/devtools/build/importdeps/testdata/Library.java b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/library_no_members/com/google/devtools/build/importdeps/testdata/Library.java
new file mode 100644
index 0000000..991baa0
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/library_no_members/com/google/devtools/build/importdeps/testdata/Library.java
@@ -0,0 +1,51 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.importdeps.testdata;
+
+/**
+ * A library class for testing. This library class is very similar to {@link Library}, but it does
+ * not have any members (fields or methods).
+ */
+public class Library {
+
+  /** A library class for testing. */
+  public static class Class1 {}
+
+  /** A library class for testing. */
+  public static class Class2 {}
+
+  /** A library class for testing. */
+  public static class Class3 {}
+
+  /** A library class for testing. */
+  public static class Class4 {}
+
+  /** A library class for testing. */
+  public static class Class5 {}
+
+  /** A library class for testing. */
+  public static class Class6 {}
+
+  /** A library class for testing. */
+  public static class Class7 {}
+
+  /** A library class for testing. */
+  public static class Class8 {}
+
+  /** A library class for testing. */
+  public static class Class9 {}
+
+  /** A library class for testing. */
+  public static class Class10 extends Class9 {}
+}
diff --git a/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/tests.bzl b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/tests.bzl
new file mode 100644
index 0000000..3edf246
--- /dev/null
+++ b/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/tests.bzl
@@ -0,0 +1,64 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+'''Helpers to create golden tests, to minimize code duplication.'''
+
+def create_golden_test(name, golden_file, has_bootclasspath, missing_jar = None, replacing_jar = None):
+  '''Create a golden test for the dependency checker.'''
+  all_dep_jars = [
+      "testdata_client",
+      "testdata_lib_Library",
+      "testdata_lib_LibraryAnnotations",
+      "testdata_lib_LibraryException",
+      "testdata_lib_LibraryInterface",
+      ]
+  testdata_pkg = "//third_party/bazel/src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata"
+  import_deps_checker = "//third_party/bazel/src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps:ImportDepsChecker"
+  client_jar = testdata_pkg + ":testdata_client"
+  data = [
+      golden_file,
+      import_deps_checker,
+      "//third_party/java/jdk:jdk8_rt_jar"
+      ] + [testdata_pkg + ":" + x for x in all_dep_jars]
+  if (replacing_jar):
+    data.append(testdata_pkg + ":" + replacing_jar)
+
+  args = [
+      "$(location %s)" % golden_file,
+      "$(location %s)" % import_deps_checker,
+      ]
+  args.append("--bootclasspath_entry")
+  if has_bootclasspath:
+    args.append("$(location //third_party/java/jdk:jdk8_rt_jar)")
+  else:
+    args.append("$(location %s)" % client_jar) # Fake bootclasspath.
+
+  for dep in all_dep_jars:
+    if dep == missing_jar:
+      if replacing_jar:
+        args.append("--classpath_entry")
+        args.append("$(location %s:%s)" % (testdata_pkg, replacing_jar))
+      continue
+    args.append("--classpath_entry")
+    args.append("$(location %s:%s)" % (testdata_pkg, dep))
+
+  args = args + [
+      "--input",
+      "$(location %s:testdata_client)" % testdata_pkg,
+      ]
+  native.sh_test(
+      name=name,
+      srcs = ["golden_test.sh"],
+      args = args,
+      data = data,
+      )