diff options
author | 2018-02-06 10:57:49 -0800 | |
---|---|---|
committer | 2018-02-06 10:59:37 -0800 | |
commit | 33c419bb0952721f056f01a0374f9f2d238e98bd (patch) | |
tree | 2d85645b03699f85231cd28c8d9865058c9e3925 /src/java_tools/import_deps_checker/java/com/google/devtools | |
parent | 11c59283ec83db0c62ad67e7f0ffbfb8a5183b06 (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')
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); + } +} |