aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/import_deps_checker/java/com/google/devtools
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/java_tools/import_deps_checker/java/com/google/devtools
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/java_tools/import_deps_checker/java/com/google/devtools')
-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
8 files changed, 1253 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);
+ }
+}