blob: a632d2a1f75e02081d02f4fb1f062d345aae8ce8 [file] [log] [blame]
// 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.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
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.io.PrintStream;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
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 in a set of JARs. */
public final class ClassCache implements Closeable {
private final LazyClasspath lazyClasspath;
private boolean isClosed;
public ClassCache(
ImmutableSet<Path> bootclasspath,
ImmutableSet<Path> directClasspath,
ImmutableSet<Path> regularClasspath,
ImmutableSet<Path> inputJars,
boolean populateMembers)
throws IOException {
lazyClasspath =
new LazyClasspath(
bootclasspath, directClasspath, regularClasspath, inputJars, populateMembers);
}
public AbstractClassEntryState getClassState(String internalName) {
checkState(!isClosed, "The cache has been closed.");
LazyClassEntry entry = lazyClasspath.getLazyEntry(internalName);
if (entry == null) {
return MissingState.singleton();
}
return entry.getState(lazyClasspath);
}
public ImmutableMap<Path, Boolean> collectUsedJarsInRegularClasspath() {
return lazyClasspath.collectUsedJarsInRegularClasspath();
}
@Override
public void close() throws IOException {
lazyClasspath.close();
isClosed = true;
}
static class LazyClassEntry {
private final String internalName;
private final ZipFile zipFile;
private final Path jarPath;
private final boolean isDirectDep;
/**
* 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, Path jarPath, boolean isDirectDep) {
this.internalName = internalName;
this.zipFile = zipFile;
this.jarPath = jarPath;
this.isDirectDep = isDirectDep;
}
ZipFile getZipFile() {
return zipFile;
}
@Nullable
public AbstractClassEntryState getState(LazyClasspath classpath) {
resolveIfNot(classpath);
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();
}
boolean isResolved() {
return state != null;
}
private void resolveIfNot(LazyClasspath lazyClasspath) {
if (isResolved()) {
return;
}
resolveClassEntry(this, lazyClasspath, /* explicitUse= */ true);
checkNotNull(state, "After resolution, the state cannot be null");
}
private static void resolveClassEntry(
LazyClassEntry classEntry, LazyClasspath lazyClasspath, boolean explicitUse) {
if (classEntry.state != null) {
// Already resolved. See if it is the existing state.
if (classEntry.state instanceof ExistingState) {
ExistingState state = (ExistingState) classEntry.state;
if (!state.direct() && explicitUse) {
// If the state was previously indirect, update now for direct dep
classEntry.state = ExistingState.create(state.classInfo().get(), explicitUse);
}
}
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.Builder<ResolutionFailureChain> resolutionFailureChainsBuilder =
ImmutableList.builder();
for (String superName :
combineWithoutNull(classReader.getSuperName(), classReader.getInterfaces())) {
Optional<ResolutionFailureChain> failurePath =
resolveSuperClassEntry(superName, lazyClasspath);
failurePath.map(resolutionFailureChainsBuilder::add);
}
ClassInfoBuilder classInfoBuilder =
new ClassInfoBuilder().setJarPath(classEntry.jarPath).setDirect(classEntry.isDirectDep);
// Only visit the class if we need to extract its list of members. If we do visit, skip
// code and debug attributes since we just care about finding declarations here.
if (lazyClasspath.populateMembers) {
classReader.accept(
classInfoBuilder,
ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
} else {
classInfoBuilder.setNames(
classReader.getClassName(), classReader.getSuperName(), classReader.getInterfaces());
}
ImmutableList<ResolutionFailureChain> resolutionFailureChains =
resolutionFailureChainsBuilder.build();
if (resolutionFailureChains.isEmpty()) {
classEntry.state =
ExistingState.create(
classInfoBuilder.build(lazyClasspath, /*incomplete=*/ false), explicitUse);
} else {
ClassInfo classInfo = classInfoBuilder.build(lazyClasspath, /*incomplete=*/ true);
classEntry.state =
IncompleteState.create(
classInfo,
ResolutionFailureChain.createWithParent(classInfo, resolutionFailureChains));
}
} catch (IOException e) {
throw new RuntimeException("Error when resolving class entry " + entryName, e);
} catch (RuntimeException e) {
System.err.println(
"A runtime exception occurred. The following is the content in the class index. "
+ e.getMessage());
lazyClasspath.printClasspath(System.err);
throw e;
}
}
private static Optional<ResolutionFailureChain> resolveSuperClassEntry(
String superName, LazyClasspath lazyClasspath) {
LazyClassEntry superClassEntry = lazyClasspath.getLazyEntry(superName);
if (superClassEntry == null) {
return Optional.of(ResolutionFailureChain.createMissingClass(superName));
} else {
resolveClassEntry(superClassEntry, lazyClasspath, /* explicitUse= */ false);
AbstractClassEntryState superState = superClassEntry.state;
if (superState instanceof ExistingState) {
// Do nothing. Good to proceed.
return Optional.empty();
} else if (superState instanceof IncompleteState) {
return Optional.of(superState.asIncompleteState().resolutionFailureChain());
} else {
throw new RuntimeException("Cannot reach here. superState is " + superState);
}
}
}
}
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();
}
/** The classpath, emulating the behavior of the real classpath. */
@VisibleForTesting
static final class LazyClasspath implements Closeable {
private final ClassIndex bootclasspath;
private final ClassIndex regularClasspath;
private final ClassIndex inputJars;
private final ImmutableList<ClassIndex> orderedClasspath;
final boolean populateMembers; // accessed from other inner classes
private final Closer closer = Closer.create();
public LazyClasspath(
ImmutableSet<Path> bootclasspath,
ImmutableSet<Path> directClasspath,
ImmutableSet<Path> regularClasspath,
ImmutableSet<Path> inputJars,
boolean populateMembers)
throws IOException {
this.populateMembers = populateMembers;
this.bootclasspath = new ClassIndex("boot classpath", bootclasspath, Predicates.alwaysTrue());
this.inputJars = new ClassIndex("input jars", inputJars, Predicates.alwaysTrue());
this.regularClasspath =
new ClassIndex(
"regular classpath",
regularClasspath,
jar ->
bootclasspath.contains(jar)
|| inputJars.contains(jar)
|| directClasspath.contains(jar));
// Reflect runtime resolution order, with input before classpath similar to javac
this.orderedClasspath =
ImmutableList.of(this.bootclasspath, this.inputJars, this.regularClasspath);
this.orderedClasspath.forEach(closer::register);
}
public LazyClassEntry getLazyEntry(String internalName) {
return orderedClasspath
.stream()
.map(classIndex -> classIndex.getClassEntry(internalName))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
public ImmutableMap<Path, Boolean> collectUsedJarsInRegularClasspath() {
return regularClasspath.collectUsedJarFiles();
}
public void printClasspath(PrintStream stream) {
orderedClasspath.forEach(c -> c.printClasspath(stream));
}
@Override
public void close() throws IOException {
closer.close();
}
}
/**
* Representation of a class path, composed of a list of JARs. It indexes all the class files with
* the class names.
*/
private static final class ClassIndex implements Closeable {
private final String name;
private final ImmutableMap<String, LazyClassEntry> classIndex;
private final Closer closer;
public ClassIndex(String name, ImmutableSet<Path> jarFiles, Predicate<Path> isDirect)
throws IOException {
this.name = name;
this.closer = Closer.create();
classIndex = buildClassIndex(jarFiles, closer, isDirect);
}
@Override
public void close() throws IOException {
closer.close();
}
public LazyClassEntry getClassEntry(String internalName) {
return classIndex.get(internalName);
}
/** Second argument in the Map is if the jar is used directly (at least once). */
public ImmutableMap<Path, Boolean> collectUsedJarFiles() {
Map<Path, Boolean> usedJars = new HashMap<>();
for (Map.Entry<String, LazyClassEntry> entry : classIndex.entrySet()) {
LazyClassEntry clazz = entry.getValue();
if (clazz.isResolved()) {
if (!usedJars.containsKey(clazz.jarPath) || clazz.state.direct()) {
usedJars.put(clazz.jarPath, clazz.state.direct());
}
}
}
return ImmutableSortedMap.copyOf(usedJars);
}
private void printClasspath(PrintStream stream) {
stream.println("Classpath: " + name);
int counter = 0;
for (Map.Entry<String, LazyClassEntry> entry : classIndex.entrySet()) {
stream.printf("%d %s\n %s\n\n", ++counter, entry.getKey(), entry.getValue());
}
}
private static ImmutableMap<String, LazyClassEntry> buildClassIndex(
ImmutableSet<Path> jars, Closer closer, Predicate<Path> isDirect) throws IOException {
HashMap<String, LazyClassEntry> result = new HashMap<>();
for (Path jarPath : jars) {
boolean jarIsDirect = isDirect.test(jarPath);
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, jarPath, jarIsDirect));
});
} catch (Throwable e) {
throw new RuntimeException("Error in reading zip file " + jarPath, e);
}
}
return ImmutableMap.copyOf(result);
}
}
/** 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;
private Path jarPath;
private boolean directDep;
public ClassInfoBuilder() {
super(Opcodes.ASM7);
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
setNames(name, superName, interfaces);
}
@Override
public FieldVisitor visitField(
int access, String name, String desc, String signature, Object value) {
members.add(MemberInfo.create(name, desc));
return null;
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
members.add(MemberInfo.create(name, desc));
return null;
}
void setNames(String name, String superName, String[] interfaces) {
checkState(internalName == null && superClasses == null, "This visitor is already used.");
internalName = name;
superClasses = combineWithoutNull(superName, interfaces);
}
public ClassInfoBuilder setJarPath(Path jarPath) {
this.jarPath = jarPath;
return this;
}
public ClassInfoBuilder setDirect(boolean direct) {
this.directDep = direct;
return this;
}
public ClassInfo build(LazyClasspath lazyClasspath, boolean incomplete) {
ImmutableList<ClassInfo> superClassInfos =
superClasses
.stream()
.map(lazyClasspath::getLazyEntry)
// nulls possible when building ClassInfo for an "incomplete" class
.filter(entry -> entry != null && entry.state != null)
.map(entry -> entry.state.classInfo().get())
.collect(ImmutableList.toImmutableList());
checkState(
incomplete || superClassInfos.size() == superClasses.size(),
"Missing class info for some of %s's super types %s: %s",
internalName,
superClasses,
superClassInfos);
return ClassInfo.create(
checkNotNull(internalName),
checkNotNull(jarPath),
directDep,
superClassInfos,
members.build());
}
}
}