All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.tools.opensource.classpath.LinkageChecker Maven / Gradle / Ivy

/*
 * 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.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(
                      classFile, (InterfaceSymbol) classSymbol, interfaceClassFile);
              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 whitelist
    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}.
   *
   * @see Java
   *     Virtual Machine Specification: 5.4.3.3. Method Resolution
   * @see Java
   *     Virtual Machine Specification: 5.4.3.4. Interface Method Resolution
   */
  @VisibleForTesting
  Optional findLinkageProblem(
      ClassFile classFile, MethodSymbol symbol, ClassFile sourceClassFile) {
    String sourceClassName = classFile.getBinaryName();
    String targetClassName = symbol.getClassBinaryName();
    String methodName = symbol.getName();

    // Skip references to Java runtime class. For example, java.lang.String.
    if (classDumper.isSystemClass(targetClassName)) {
      return Optional.empty();
    }

    try {
      JavaClass targetJavaClass = classDumper.loadJavaClass(targetClassName);
      ClassPathEntry classPathEntry = classDumper.findClassLocation(targetClassName);
      ClassFile targetClassFile =
          classPathEntry == null ? null : new ClassFile(classPathEntry, targetClassName);

      if (!isClassAccessibleFrom(targetJavaClass, sourceClassName)) {
        return Optional.of(new InaccessibleClassProblem(sourceClassFile, targetClassFile, symbol));
      }

      if (targetJavaClass.isInterface() != symbol.isInterfaceMethod()) {
        return Optional.of(
            new IncompatibleClassChangeProblem(sourceClassFile, targetClassFile, symbol));
      }

      // Check the existence of the parent class or interface for the class
      Optional 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()));
      for (JavaClass javaClass : typesToCheck) {
        for (Method method : javaClass.getMethods()) {
          if (method.getName().equals(methodName)
              && method.getSignature().equals(symbol.getDescriptor())) {
            if (!isMemberAccessibleFrom(javaClass, method, sourceClassName)) {
              return Optional.of(
                  new InaccessibleMemberProblem(sourceClassFile, targetClassFile, symbol));
            }
            // The method is found and accessible. Returning no error.
            return Optional.empty();
          }
        }
      }

      // 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 linkage errors for unimplemented methods in {@code classFile}. Such unimplemented
   * methods manifest as {@link AbstractMethodError}s at runtime.
   */
  private ImmutableList findInterfaceProblems(
      ClassFile classFile, InterfaceSymbol interfaceSymbol, ClassFile sourceClassFile) {
    String interfaceName = interfaceSymbol.getClassBinaryName();
    if (classDumper.isSystemClass(interfaceName)) {
      return ImmutableList.of();
    }

    ImmutableList.Builder builder = ImmutableList.builder();
    try {
      JavaClass implementingClass = classDumper.loadJavaClass(classFile.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(
                  classFile.getBinaryName(), interfaceMethodName, interfaceMethodDescriptor, false);
          builder.add(new AbstractMethodProblem(sourceClassFile, classFile, missingMethodOnClass));
        }
      }
    } 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)) {
        return Optional.of(new InaccessibleClassProblem(sourceClassFile, targetClassFile, symbol));
      }

      for (JavaClass javaClass : getClassHierarchy(targetJavaClass)) {
        for (Field field : javaClass.getFields()) {
          if (field.getName().equals(fieldName)) {
            if (!isMemberAccessibleFrom(javaClass, field, sourceClassName)) {
              return Optional.of(
                  new InaccessibleMemberProblem(sourceClassFile, targetClassFile, symbol));
            }
            // 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)) {
        return Optional.of(new InaccessibleClassProblem(sourceClassFile, targetClassFile, symbol));
      }
      return Optional.empty();
    } catch (ClassNotFoundException ex) {
      if (classDumper.isUnusedClassSymbolReference(sourceClassName, symbol)
          || classDumper.catchesLinkageErrorOnClass(sourceClassName)) {
        // The class reference is unused in the source
        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 sourceClassFile) {
    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();
  
            MethodSymbol missingMethodOnClass =
                new MethodSymbol(
                    className, unimplementedMethodName, unimplementedMethodDescriptor, false);
            builder.add(
                new AbstractMethodProblem(sourceClassFile, classFile, missingMethodOnClass));
          }
        }
        abstractClass = abstractClass.getSuperClass();
      }
    } catch (ClassNotFoundException ex) {
      // Missing classes are reported by findLinkageProblem method.
    }
    return builder.build();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy