com.google.cloud.tools.opensource.classpath.LinkageChecker Maven / Gradle / Ivy
Show all versions of dependencies Show documentation
/*
* Copyright 2018 Google LLC.
*
* 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.cloud.tools.opensource.classpath;
import static com.google.cloud.tools.opensource.classpath.ClassDumper.getClassHierarchy;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.cloud.tools.opensource.dependencies.Bom;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Utility;
import org.eclipse.aether.artifact.Artifact;
/** A tool to find linkage errors in a class path. */
public class LinkageChecker {
private static final Logger logger = Logger.getLogger(LinkageChecker.class.getName());
private final ClassDumper classDumper;
private final ImmutableList classPath;
private final SymbolReferences symbolReferences;
private final ClassReferenceGraph classReferenceGraph;
private final ExcludedErrors excludedErrors;
@VisibleForTesting
SymbolReferences getSymbolReferences() {
return symbolReferences;
}
public ClassReferenceGraph getClassReferenceGraph() {
return classReferenceGraph;
}
public static LinkageChecker create(List classPath) throws IOException {
return create(classPath, ImmutableSet.copyOf(classPath), null);
}
/**
* Returns Linkage Checker for {@code classPath}.
*
* @param classPath JAR files to find linkage errors in
* @param entryPoints JAR files to specify entry point classes in reachability
* @param exclusionFile exclusion file to suppress linkage errors
*/
public static LinkageChecker create(
List classPath,
Iterable entryPoints,
@Nullable Path exclusionFile)
throws IOException {
Preconditions.checkArgument(!classPath.isEmpty(), "The linkage classpath is empty.");
ClassDumper dumper = ClassDumper.create(classPath);
SymbolReferences symbolReferenceMaps = dumper.findSymbolReferences();
ClassReferenceGraph classReferenceGraph =
ClassReferenceGraph.create(symbolReferenceMaps, ImmutableSet.copyOf(entryPoints));
return new LinkageChecker(
dumper,
classPath,
symbolReferenceMaps,
classReferenceGraph,
ExcludedErrors.create(exclusionFile));
}
public static LinkageChecker create(Bom bom) throws IOException {
return create(bom, null);
}
public static LinkageChecker create(Bom bom, Path exclusionFile) throws IOException {
// duplicate code from DashboardMain follows. We need to refactor to extract this.
ImmutableList managedDependencies = bom.getManagedDependencies();
ClassPathBuilder classPathBuilder = new ClassPathBuilder();
ClassPathResult classPathResult = classPathBuilder.resolve(managedDependencies, true);
ImmutableList classpath = classPathResult.getClassPath();
// When checking a BOM, entry point classes are the ones in the artifacts listed in the BOM
List artifactsInBom = classpath.subList(0, managedDependencies.size());
ImmutableSet entryPoints = ImmutableSet.copyOf(artifactsInBom);
return LinkageChecker.create(classpath, entryPoints, exclusionFile);
}
@VisibleForTesting
LinkageChecker cloneWith(SymbolReferences newSymbolMaps) {
return new LinkageChecker(
classDumper, classPath, newSymbolMaps, classReferenceGraph, excludedErrors);
}
private LinkageChecker(
ClassDumper classDumper,
List classPath,
SymbolReferences symbolReferenceMaps,
ClassReferenceGraph classReferenceGraph,
ExcludedErrors excludedErrors) {
this.classDumper = Preconditions.checkNotNull(classDumper);
this.classPath = ImmutableList.copyOf(classPath);
this.classReferenceGraph = Preconditions.checkNotNull(classReferenceGraph);
this.symbolReferences = Preconditions.checkNotNull(symbolReferenceMaps);
this.excludedErrors = Preconditions.checkNotNull(excludedErrors);
}
/**
* Searches the classpath for linkage errors.
*
* @return {@link LinkageProblem}s found in the class path and referencing classes
* @throws IOException I/O error reading files in the classpath
*/
public ImmutableSet findLinkageProblems() throws IOException {
ImmutableSet.Builder problemToClass = ImmutableSet.builder();
// This sourceClassFile is a source of references to other symbols.
for (ClassFile classFile : symbolReferences.getClassFiles()) {
ImmutableSet classSymbols = symbolReferences.getClassSymbols(classFile);
for (ClassSymbol classSymbol : classSymbols) {
if (classSymbol instanceof SuperClassSymbol) {
String superClassName = classSymbol.getClassBinaryName();
ClassPathEntry superClassLocation = classDumper.findClassLocation(superClassName);
if (superClassLocation != null) {
ClassFile superClassFile = new ClassFile(superClassLocation, superClassName);
ImmutableList problems =
findAbstractParentProblems(
classFile, (SuperClassSymbol) classSymbol, superClassFile);
for (LinkageProblem problem : problems) {
problemToClass.add(problem);
}
}
}
ImmutableSet classFileNames = classFile.getClassPathEntry().getFileNames();
String classBinaryName = classSymbol.getClassBinaryName();
String classFileName = classDumper.getFileName(classBinaryName);
if (!classFileNames.contains(classFileName)) {
if (classSymbol instanceof InterfaceSymbol) {
String interfaceName = classSymbol.getClassBinaryName();
ClassPathEntry interfaceLocation = classDumper.findClassLocation(interfaceName);
if (interfaceLocation != null) {
ClassFile interfaceClassFile = new ClassFile(interfaceLocation, interfaceName);
ImmutableList problems =
findInterfaceProblems(
interfaceClassFile, (InterfaceSymbol) classSymbol, classFile);
for (LinkageProblem problem : problems) {
problemToClass.add(problem);
}
}
} else {
findLinkageProblem(classFile, classSymbol, classFile.topLevelClassFile())
.ifPresent(problemToClass::add);
}
}
}
}
for (ClassFile classFile : symbolReferences.getClassFiles()) {
ImmutableSet methodSymbols = symbolReferences.getMethodSymbols(classFile);
ImmutableSet classFileNames = classFile.getClassPathEntry().getFileNames();
for (MethodSymbol methodSymbol : methodSymbols) {
String classBinaryName = methodSymbol.getClassBinaryName();
String classFileName = classDumper.getFileName(classBinaryName);
if (!classFileNames.contains(classFileName)) {
findLinkageProblem(classFile, methodSymbol, classFile.topLevelClassFile())
.ifPresent(problemToClass::add);
}
}
}
for (ClassFile classFile : symbolReferences.getClassFiles()) {
ImmutableSet fieldSymbols = symbolReferences.getFieldSymbols(classFile);
ImmutableSet classFileNames = classFile.getClassPathEntry().getFileNames();
for (FieldSymbol fieldSymbol : fieldSymbols) {
String classBinaryName = fieldSymbol.getClassBinaryName();
String classFileName = classDumper.getFileName(classBinaryName);
if (!classFileNames.contains(classFileName)) {
findLinkageProblem(classFile, fieldSymbol, classFile.topLevelClassFile())
.ifPresent(problemToClass::add);
}
}
}
// Filter classes in exclusion file
ImmutableSet filteredMap =
problemToClass.build().stream().filter(this::problemFilter).collect(toImmutableSet());
return filteredMap;
}
/**
* Returns true if the linkage error {@code entry} should be reported. False if it should be
* suppressed.
*/
private boolean problemFilter(LinkageProblem linkageProblem) {
return !excludedErrors.contains(linkageProblem);
}
/**
* Returns an {@code Optional} describing the linkage error for the method reference if the
* reference does not have a valid referent in the input class path; otherwise an empty {@code
* Optional}.
*
* Because the Java Virtual Machine has special handling for {@link
* java.lang.invoke.MethodHandle#invoke(Object...)} and {@link
* java.lang.invoke.MethodHandle#invokeExact(Object...)}, this method does not report the
* references to them as linkage errors.
*
* @see Java
* Virtual Machine Specification: 5.4.3.3. Method Resolution
* @see Java
* Virtual Machine Specification: 5.4.3.4. Interface Method Resolution
* @see parentLinkageProblem =
findParentClassLinkageProblem(targetClassName, sourceClassFile);
if (parentLinkageProblem.isPresent()) {
return parentLinkageProblem;
}
// Checks the target class, its parent classes, and its interfaces.
// Interface check is needed to avoid false positive for a method reference to an abstract
// class that implements an interface. For example, Guava's ImmutableList is an abstract class
// that implements the List interface, but the class does not have a get() method. A method
// reference to ImmutableList.get() should not be reported as a linkage error.
Iterable typesToCheck =
Iterables.concat(
getClassHierarchy(targetJavaClass),
Arrays.asList(targetJavaClass.getAllInterfaces()));
String changedReturnType = null;
for (JavaClass javaClass : typesToCheck) {
for (Method method : javaClass.getMethods()) {
if (method.getName().equals(methodName)) {
String expectedMethodDescriptor = symbol.getDescriptor();
String actualMethodDescriptor = method.getSignature();
if (actualMethodDescriptor.equals(expectedMethodDescriptor)) {
if (!isMemberAccessibleFrom(javaClass, method, sourceClassName)) {
AccessModifier modifier = AccessModifier.fromFlag(method.getModifiers());
return Optional.of(
new InaccessibleMemberProblem(
sourceClassFile, targetClassFile, symbol, modifier));
}
// The method is found and accessible. Returning no error.
return Optional.empty();
} else {
String expectedParameterDescriptors =
parseParameterDescriptors(expectedMethodDescriptor);
String actualParameterDescriptors = parseParameterDescriptors(actualMethodDescriptor);
if (actualParameterDescriptors.equals(expectedParameterDescriptors)) {
// Not returning result yet, because there can be another supertype that has the
// exact method that matches the name, argument types, and return type.
changedReturnType = Utility.methodSignatureReturnType(actualMethodDescriptor);
}
}
}
}
}
if (changedReturnType != null) {
// When only the return types are different, we can report this specific problem
// rather than more generic SymbolNotFoundProblem.
return Optional.of(
new ReturnTypeChangedProblem(
sourceClassFile, targetClassFile, symbol, changedReturnType));
}
// Slf4J catches LinkageError to check the existence of other classes
if (classDumper.catchesLinkageErrorOnMethod(sourceClassName)) {
return Optional.empty();
}
// The class is in class path but the symbol is not found
return Optional.of(new SymbolNotFoundProblem(sourceClassFile, targetClassFile, symbol));
} catch (ClassNotFoundException ex) {
if (classDumper.catchesLinkageErrorOnClass(sourceClassName)) {
return Optional.empty();
}
ClassSymbol classSymbol = new ClassSymbol(symbol.getClassBinaryName());
return Optional.of(new ClassNotFoundProblem(sourceClassFile, classSymbol));
}
}
/**
* Returns the parameter descriptors from {@code methodDescriptor}.
*
* @see Java
* Virtual Machine Specification: 4.3.3. Method Descriptors
*/
private static String parseParameterDescriptors(String methodDescriptor) {
// E.g., '(Ljava/lang/String;)Ljava/lang/Integer;' => '(Ljava/lang/String;)'
return methodDescriptor.substring(0, methodDescriptor.indexOf(')') + 1);
}
/**
* Returns the linkage errors for unimplemented methods in {@code classFile}. Such unimplemented
* methods manifest as {@link AbstractMethodError}s at runtime.
*/
private ImmutableList findInterfaceProblems(
ClassFile interfaceClassFile,
InterfaceSymbol interfaceSymbol,
ClassFile implementationClassFile) {
String interfaceName = interfaceSymbol.getClassBinaryName();
if (classDumper.isSystemClass(interfaceName)) {
return ImmutableList.of();
}
ImmutableList.Builder builder = ImmutableList.builder();
try {
JavaClass implementingClass =
classDumper.loadJavaClass(implementationClassFile.getBinaryName());
if (implementingClass.isAbstract()) {
// Abstract class does not need to implement methods in an interface.
return ImmutableList.of();
}
JavaClass interfaceDefinition = classDumper.loadJavaClass(interfaceName);
for (Method interfaceMethod : interfaceDefinition.getMethods()) {
if (interfaceMethod.getCode() != null) {
// This interface method has default implementation. Subclass does not have to implement
// it.
continue;
}
String interfaceMethodName = interfaceMethod.getName();
String interfaceMethodDescriptor = interfaceMethod.getSignature();
boolean methodFound = false;
Iterable typesToCheck = Iterables.concat(getClassHierarchy(implementingClass));
for (JavaClass javaClass : typesToCheck) {
for (Method method : javaClass.getMethods()) {
if (method.getName().equals(interfaceMethodName)
&& method.getSignature().equals(interfaceMethodDescriptor)) {
methodFound = true;
break;
}
}
}
if (!methodFound) {
MethodSymbol missingMethodOnClass =
new MethodSymbol(
interfaceClassFile.getBinaryName(),
interfaceMethodName,
interfaceMethodDescriptor,
false);
builder.add(
new AbstractMethodProblem(
implementationClassFile, missingMethodOnClass, interfaceClassFile));
}
}
} catch (ClassNotFoundException ex) {
// Missing classes are reported by findLinkageProblem method.
}
return builder.build();
}
/**
* Returns an {@code Optional} describing the linkage error for the field reference if the
* reference does not have a valid referent in the input class path; otherwise an empty {@code
* Optional}.
*/
@VisibleForTesting
Optional findLinkageProblem(
ClassFile classFile, FieldSymbol symbol, ClassFile sourceClassFile) {
String sourceClassName = classFile.getBinaryName();
String targetClassName = symbol.getClassBinaryName();
String fieldName = symbol.getName();
try {
JavaClass targetJavaClass = classDumper.loadJavaClass(targetClassName);
ClassPathEntry classFileLocation = classDumper.findClassLocation(targetClassName);
ClassFile targetClassFile =
classFileLocation == null ? null : new ClassFile(classFileLocation, targetClassName);
if (!isClassAccessibleFrom(targetJavaClass, sourceClassName)) {
AccessModifier modifier = AccessModifier.fromFlag(targetJavaClass.getModifiers());
return Optional.of(
new InaccessibleClassProblem(
sourceClassFile,
targetClassFile,
new ClassSymbol(symbol.getClassBinaryName()),
modifier));
}
for (JavaClass javaClass : getClassHierarchy(targetJavaClass)) {
for (Field field : javaClass.getFields()) {
if (field.getName().equals(fieldName)) {
if (!isMemberAccessibleFrom(javaClass, field, sourceClassName)) {
AccessModifier modifier = AccessModifier.fromFlag(field.getModifiers());
return Optional.of(
new InaccessibleMemberProblem(
sourceClassFile, targetClassFile, symbol, modifier));
}
// The field is found and accessible. Returning no error.
return Optional.empty();
}
}
}
// The field was not found in the class from the classpath
return Optional.of(new SymbolNotFoundProblem(sourceClassFile, targetClassFile, symbol));
} catch (ClassNotFoundException ex) {
if (classDumper.catchesLinkageErrorOnClass(sourceClassName)) {
return Optional.empty();
}
ClassSymbol classSymbol = new ClassSymbol(symbol.getClassBinaryName());
return Optional.of(new ClassNotFoundProblem(sourceClassFile, classSymbol));
}
}
/**
* Returns true if the field or method of a class is accessible from {@code sourceClassName}.
*
* @see JLS 6.6.1
* Determining Accessibility
*/
private boolean isMemberAccessibleFrom(
JavaClass targetClass, FieldOrMethod member, String sourceClassName) {
// The order of these if statements for public, protected, and private are in the same order
// they
// appear in JLS 6.6.1
if (member.isPublic()) {
return true;
}
if (member.isProtected()) {
if (ClassDumper.classesInSamePackage(targetClass.getClassName(), sourceClassName)) {
return true;
}
try {
JavaClass sourceClass = classDumper.loadJavaClass(sourceClassName);
if (ClassDumper.isClassSubClassOf(sourceClass, targetClass)) {
return true;
}
} catch (ClassNotFoundException ex) {
logger.warning(
"The source class "
+ sourceClassName
+ " of a reference was not found in the class path when checking accessibility");
return false;
}
}
if (member.isPrivate()) {
// Access from within same top-level class is allowed to read private class. However, such
// cases are already filtered at errorsFromSymbolReferences.
return false;
}
// Default: package private
if (ClassDumper.classesInSamePackage(targetClass.getClassName(), sourceClassName)) {
return true;
}
return false;
}
/**
* Returns an {@code Optional} describing the linkage error for the class reference if the
* reference does not have a valid referent in the input class path; otherwise an empty {@code
* Optional}.
*/
@VisibleForTesting
Optional findLinkageProblem(
ClassFile classFile, ClassSymbol symbol, ClassFile sourceClassFile) {
String sourceClassName = classFile.getBinaryName();
String targetClassName = symbol.getClassBinaryName();
try {
JavaClass targetClass = classDumper.loadJavaClass(targetClassName);
ClassPathEntry classFileLocation = classDumper.findClassLocation(targetClassName);
ClassFile targetClassFile =
classFileLocation == null ? null : new ClassFile(classFileLocation, targetClassName);
boolean isSubclassReference = symbol instanceof SuperClassSymbol;
if (isSubclassReference
&& !ClassDumper.hasValidSuperclass(
classDumper.loadJavaClass(sourceClassName), targetClass)) {
return Optional.of(
new IncompatibleClassChangeProblem(sourceClassFile, targetClassFile, symbol));
}
if (!isClassAccessibleFrom(targetClass, sourceClassName)
&& classDumper.isClassSymbolReferenceUsed(sourceClassName, symbol)) {
AccessModifier modifier = AccessModifier.fromFlag(targetClass.getModifiers());
return Optional.of(
new InaccessibleClassProblem(sourceClassFile, targetClassFile, symbol, modifier));
}
return Optional.empty();
} catch (ClassNotFoundException ex) {
if (!classDumper.isClassSymbolReferenceUsed(sourceClassName, symbol)
|| classDumper.catchesLinkageErrorOnClass(sourceClassName)) {
// The class reference is unused in the source, or catches NoClassDefFoundError
return Optional.empty();
}
return Optional.of(new ClassNotFoundProblem(sourceClassFile, symbol));
}
}
/**
* Returns true if the {@code javaClass} is accessible {@code from sourceClassName} in terms of
* the access modifiers in the {@code javaClass}.
*
* @see
* JLS 8.1.1. Class Modifiers
*/
private boolean isClassAccessibleFrom(JavaClass javaClass, String sourceClassName)
throws ClassNotFoundException {
if (javaClass.isPrivate()) {
// Nested class can be declared as private. Class reference within same file is allowed to
// access private class. However, such cases are already filtered at
// errorsFromSymbolReferences.
return false;
}
String targetClassName = javaClass.getClassName();
if (javaClass.isPublic()
|| ClassDumper.classesInSamePackage(targetClassName, sourceClassName)) {
String enclosingClassName = ClassDumper.enclosingClassName(targetClassName);
if (enclosingClassName != null) {
// Nested class can be declared as private or protected, in addition to
// public and package private. Protected is treated same as package private.
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-ClassModifier
JavaClass enclosingJavaClass = classDumper.loadJavaClass(enclosingClassName);
return isClassAccessibleFrom(enclosingJavaClass, sourceClassName);
} else {
// Top-level class can be declared as public or package private.
return true;
}
} else {
// The class is not public and not in the same package as the source class.
return false;
}
}
/**
* Returns an {@code Optional} describing the symbol problem in the parent classes or interfaces
* of {@code baseClassName}, if any of them are missing; otherwise an empty {@code Optional}.
*/
private Optional findParentClassLinkageProblem(
String baseClassName, ClassFile sourceClassFile) {
Queue queue = new ArrayDeque<>();
queue.add(baseClassName);
while (!queue.isEmpty()) {
String className = queue.remove();
if (Object.class.getName().equals(className)) {
continue; // java.lang.Object is the root of the inheritance tree
}
String potentiallyMissingClassName = className;
try {
JavaClass baseClass = classDumper.loadJavaClass(className);
queue.add(baseClass.getSuperclassName());
for (String interfaceName : baseClass.getInterfaceNames()) {
potentiallyMissingClassName = interfaceName;
JavaClass interfaceClass = classDumper.loadJavaClass(interfaceName);
// An interface may implement other interfaces
queue.addAll(Arrays.asList(interfaceClass.getInterfaceNames()));
}
} catch (ClassNotFoundException ex) {
// potentiallyMissingClassName (either className or interfaceName) is missing
LinkageProblem problem =
new ClassNotFoundProblem(sourceClassFile, new ClassSymbol(potentiallyMissingClassName));
return Optional.of(problem);
}
}
return Optional.empty();
}
private ImmutableList findAbstractParentProblems(
ClassFile classFile, SuperClassSymbol superClassSymbol, ClassFile superClassFile) {
ImmutableList.Builder builder = ImmutableList.builder();
String superClassName = superClassSymbol.getClassBinaryName();
if (classDumper.isSystemClass(superClassName)) {
return ImmutableList.of();
}
try {
String className = classFile.getBinaryName();
JavaClass implementingClass = classDumper.loadJavaClass(className);
if (implementingClass.isAbstract()) {
return ImmutableList.of();
}
JavaClass superClass = classDumper.loadJavaClass(superClassName);
if (!superClass.isAbstract()) {
return ImmutableList.of();
}
JavaClass abstractClass = superClass;
// Equality of BCEL's Method class is on its name and descriptor field
Set implementedMethods = new HashSet<>();
implementedMethods.addAll(ImmutableList.copyOf(implementingClass.getMethods()));
while (abstractClass.isAbstract()) {
for (Method abstractMethod : abstractClass.getMethods()) {
if (!abstractMethod.isAbstract()) {
// This abstract method has implementation. Subclass does not have to implement it.
implementedMethods.add(abstractMethod);
} else if (!implementedMethods.contains(abstractMethod)) {
String unimplementedMethodName = abstractMethod.getName();
String unimplementedMethodDescriptor = abstractMethod.getSignature();
String abstractClassName = abstractClass.getClassName();
MethodSymbol missingMethodOnClass =
new MethodSymbol(
abstractClassName,
unimplementedMethodName,
unimplementedMethodDescriptor,
false);
builder.add(new AbstractMethodProblem(classFile, missingMethodOnClass, superClassFile));
}
}
abstractClass = abstractClass.getSuperClass();
}
} catch (ClassNotFoundException ex) {
// Missing classes are reported by findLinkageProblem method.
}
return builder.build();
}
}