aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar cnsun <cnsun@google.com>2018-02-06 10:57:49 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2018-02-06 10:59:37 -0800
commit33c419bb0952721f056f01a0374f9f2d238e98bd (patch)
tree2d85645b03699f85231cd28c8d9865058c9e3925 /src
parent11c59283ec83db0c62ad67e7f0ffbfb8a5183b06 (diff)
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
Diffstat (limited to 'src')
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/AbstractClassEntryState.java124
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/BUILD34
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassCache.java277
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ClassInfo.java94
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/DepsCheckerClassVisitor.java338
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ImportDepsChecker.java153
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/Main.java162
-rw-r--r--src/java_tools/import_deps_checker/java/com/google/devtools/build/importdeps/ResultCollector.java71
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/AbstractClassCacheTest.java96
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/BUILD160
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassCacheTest.java142
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ClassInfoTest.java82
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/DepsCheckerClassVisitorTest.java128
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/LazyClassEntryStateTest.java97
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/ResultCollectorTest.java81
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_bootclasspath_missing.txt87
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_complete_classpath.txt0
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_annotation_missing.txt12
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_exception_missing.txt5
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_interface_missing.txt9
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_members_missing.txt8
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_library_missing.txt18
-rwxr-xr-xsrc/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/golden_test.sh56
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/BUILD43
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Client.java67
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/Library.java60
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryAnnotations.java57
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryException.java17
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/LibraryInterface.java23
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/testdata/library_no_members/com/google/devtools/build/importdeps/testdata/Library.java51
-rw-r--r--src/java_tools/import_deps_checker/javatests/com/google/devtools/build/importdeps/tests.bzl64
31 files changed, 2616 insertions, 0 deletions
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 0000000000..209bf83fcb
--- /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 0000000000..a9d4cd32aa
--- /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 0000000000..c855afd6d3
--- /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 0000000000..d0914640ac
--- /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 0000000000..8c4b1cee69
--- /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 0000000000..e73a665be6
--- /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 0000000000..d549bc2e84
--- /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 0000000000..34253aa8ef
--- /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 0000000000..33460f6614
--- /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 0000000000..bbf7085f1a
--- /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 0000000000..241afd98c6
--- /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 0000000000..54d53f2f22
--- /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 0000000000..3d19b06638
--- /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 0000000000..b2bc7f2973
--- /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 0000000000..0d78504634
--- /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 0000000000..5a4e67edfe
--- /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 0000000000..e69de29bb2
--- /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 0000000000..60af5de912
--- /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 0000000000..767f4eb0b9
--- /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 0000000000..3c3568eb59
--- /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 0000000000..73831162b3
--- /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 0000000000..12447fc23a
--- /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 0000000000..d3f090c82c
--- /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 0000000000..ae40513a05
--- /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 0000000000..6b833c3b61
--- /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 0000000000..31049ba2d5
--- /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 0000000000..d0b883ae57
--- /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 0000000000..ff572a005a
--- /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 0000000000..649adc105a
--- /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 0000000000..991baa0cd7
--- /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 0000000000..3edf246089
--- /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,
+ )