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 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.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
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.Map;
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 SymbolReferenceMaps classToSymbols;
  private final ClassReferenceGraph classReferenceGraph;
  private final ExcludedErrors excludedErrors;

  @VisibleForTesting
  SymbolReferenceMaps getClassToSymbols() {
    return classToSymbols;
  }

  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);
    SymbolReferenceMaps 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);
    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(SymbolReferenceMaps newSymbolMaps) {
    return new LinkageChecker(
        classDumper, classPath, newSymbolMaps, classReferenceGraph, excludedErrors);
  }

  private LinkageChecker(
      ClassDumper classDumper,
      List classPath,
      SymbolReferenceMaps symbolReferenceMaps,
      ClassReferenceGraph classReferenceGraph,
      ExcludedErrors excludedErrors) {
    this.classDumper = Preconditions.checkNotNull(classDumper);
    this.classPath = ImmutableList.copyOf(classPath);
    this.classReferenceGraph = Preconditions.checkNotNull(classReferenceGraph);
    this.classToSymbols = Preconditions.checkNotNull(symbolReferenceMaps);
    this.excludedErrors = Preconditions.checkNotNull(excludedErrors);
  }

  /**
   * Searches the classpath for linkage errors.
   * 
   * @return {@link SymbolProblem}s found in the class path and referencing classes
   * @throws IOException I/O error reading files in the classpath
   */
  public ImmutableSetMultimap findSymbolProblems() throws IOException {
    // Having Problem in key will dedup SymbolProblems
    ImmutableSetMultimap.Builder problemToClass =
        ImmutableSetMultimap.builder();

    ImmutableSetMultimap classToClassSymbols =
        classToSymbols.getClassToClassSymbols();
    
    for (ClassFile classFile : classToClassSymbols.keySet()) {
      ImmutableSet classSymbols = classToClassSymbols.get(classFile);
      for (ClassSymbol classSymbol : classSymbols) {
        if (classSymbol instanceof SuperClassSymbol) {
          ImmutableList problems =
              findAbstractParentProblems(classFile, (SuperClassSymbol) classSymbol);
          if (!problems.isEmpty()) {
            String superClassName = classSymbol.getClassBinaryName();
            ClassPathEntry superClassLocation = classDumper.findClassLocation(superClassName);
            ClassFile superClassFile = new ClassFile(superClassLocation, superClassName);
            for (SymbolProblem problem : problems) {
              problemToClass.put(problem, superClassFile);
            }
          }
        }
        if (!classFile.getClassPathEntry().getClassNames()
            .contains(classSymbol.getClassBinaryName())) {

          if (classSymbol instanceof InterfaceSymbol) {
            ImmutableList problems =
                findInterfaceProblems(classFile, (InterfaceSymbol) classSymbol);
            if (!problems.isEmpty()) {
              String interfaceName = classSymbol.getClassBinaryName();
              ClassPathEntry interfaceLocation = classDumper.findClassLocation(interfaceName);
              ClassFile interfaceClassFile = new ClassFile(interfaceLocation, interfaceName);
              for (SymbolProblem problem : problems) {
                problemToClass.put(problem, interfaceClassFile);
              }
            }
          } else {
            findSymbolProblem(classFile, classSymbol)
                .ifPresent(problem -> problemToClass.put(problem, classFile.topLevelClassFile()));
          }
        }
      }    
    }
    
    ImmutableSetMultimap classToMethodSymbols =
        classToSymbols.getClassToMethodSymbols();
    for (ClassFile classFile : classToMethodSymbols.keySet()) {
      ImmutableSet methodSymbols = classToMethodSymbols.get(classFile);
      for (MethodSymbol methodSymbol : methodSymbols) {
        if (!classFile.getClassPathEntry().getClassNames()
            .contains(methodSymbol.getClassBinaryName())) {
          findSymbolProblem(classFile, methodSymbol)
              .ifPresent(problem -> problemToClass.put(problem, classFile.topLevelClassFile()));
        }
      }
    }

    ImmutableSetMultimap classToFieldSymbols =
        classToSymbols.getClassToFieldSymbols();
    for (ClassFile classFile : classToFieldSymbols.keySet()) {
      ImmutableSet fieldSymbols = classToFieldSymbols.get(classFile);
      for (FieldSymbol fieldSymbol : fieldSymbols) {
        if (!classFile.getClassPathEntry().getClassNames()
            .contains(fieldSymbol.getClassBinaryName())) {
          findSymbolProblem(classFile, fieldSymbol)
              .ifPresent(problem -> problemToClass.put(problem, classFile.topLevelClassFile()));
        }
      }
    }

    // Filter classes in whitelist
    SetMultimap filteredMap =
        Multimaps.filterEntries(problemToClass.build(), this::problemFilter);
    return ImmutableSetMultimap.copyOf(filteredMap);
  }

  /**
   * Returns true if the linkage error {@code entry} should be reported. False if it should be
   * suppressed.
   */
  private boolean problemFilter(Map.Entry entry) {
    SymbolProblem symbolProblem = entry.getKey();
    ClassFile sourceClass = entry.getValue();
    return !excludedErrors.contains(symbolProblem, sourceClass);
  }

  /**
   * 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 findSymbolProblem(ClassFile classFile, MethodSymbol symbol) {
    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 classFileLocation = classDumper.findClassLocation(targetClassName);
      ClassFile containingClassFile =
          classFileLocation == null ? null : new ClassFile(classFileLocation, targetClassName);

      if (!isClassAccessibleFrom(targetJavaClass, sourceClassName)) {
        return Optional.of(
            new SymbolProblem(symbol, ErrorType.INACCESSIBLE_CLASS, containingClassFile));
      }

      if (targetJavaClass.isInterface() != symbol.isInterfaceMethod()) {
        return Optional.of(
            new SymbolProblem(symbol, ErrorType.INCOMPATIBLE_CLASS_CHANGE, containingClassFile));
      }

      // Check the existence of the parent class or interface for the class
      Optional parentSymbolProblem = findParentSymbolProblem(targetClassName);
      if (parentSymbolProblem.isPresent()) {
        return parentSymbolProblem;
      }

      // 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 SymbolProblem(symbol, ErrorType.INACCESSIBLE_MEMBER, containingClassFile));
            }
            // The method is found and accessible. Returning no error.
            return Optional.empty();
          }
        }
      }

      // Slf4J catches LinkageError to check the existence of other classes
      if (classDumper.catchesLinkageError(sourceClassName)) {
        return Optional.empty();
      }

      // The class is in class path but the symbol is not found
      return Optional.of(
          new SymbolProblem(symbol, ErrorType.SYMBOL_NOT_FOUND, containingClassFile));
    } catch (ClassNotFoundException ex) {
      if (classDumper.catchesLinkageError(sourceClassName)) {
        return Optional.empty();
      }
      ClassSymbol classSymbol = new ClassSymbol(symbol.getClassBinaryName());
      return Optional.of(new SymbolProblem(classSymbol, ErrorType.CLASS_NOT_FOUND, null));
    }
  }

  /**
   * Returns the linkage errors for unimplemented methods in {@code classFile}. Such unimplemented
   * methods manifest as {@link AbstractMethodError} in runtime.
   */
  private ImmutableList findInterfaceProblems(
      ClassFile classFile, InterfaceSymbol interfaceSymbol) {
    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 SymbolProblem(missingMethodOnClass, ErrorType.ABSTRACT_METHOD, classFile));
        }
      }
    } catch (ClassNotFoundException ex) {
      // Missing classes are reported by findSymbolProblem 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 findSymbolProblem(ClassFile classFile, FieldSymbol symbol) {
    String sourceClassName = classFile.getBinaryName();
    String targetClassName = symbol.getClassBinaryName();

    String fieldName = symbol.getName();
    try {
      JavaClass targetJavaClass = classDumper.loadJavaClass(targetClassName);
      ClassPathEntry classFileLocation = classDumper.findClassLocation(targetClassName);
      ClassFile containingClassFile =
          classFileLocation == null ? null : new ClassFile(classFileLocation, targetClassName);

      if (!isClassAccessibleFrom(targetJavaClass, sourceClassName)) {
        return Optional.of(
            new SymbolProblem(symbol, ErrorType.INACCESSIBLE_CLASS, containingClassFile));
      }

      for (JavaClass javaClass : getClassHierarchy(targetJavaClass)) {
        for (Field field : javaClass.getFields()) {
          if (field.getName().equals(fieldName)) {
            if (!isMemberAccessibleFrom(javaClass, field, sourceClassName)) {
              return Optional.of(
                  new SymbolProblem(symbol, ErrorType.INACCESSIBLE_MEMBER, containingClassFile));
            }
            // 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 SymbolProblem(symbol, ErrorType.SYMBOL_NOT_FOUND, containingClassFile));
    } catch (ClassNotFoundException ex) {
      if (classDumper.catchesLinkageError(sourceClassName)) {
        return Optional.empty();
      }
      ClassSymbol classSymbol = new ClassSymbol(symbol.getClassBinaryName());
      return Optional.of(new SymbolProblem(classSymbol, ErrorType.CLASS_NOT_FOUND, null));
    }
  }

  /**
   * 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 findSymbolProblem(ClassFile classFile, ClassSymbol symbol) {
    String sourceClassName = classFile.getBinaryName();
    String targetClassName = symbol.getClassBinaryName();

    try {
      JavaClass targetClass = classDumper.loadJavaClass(targetClassName);
      ClassPathEntry classFileLocation = classDumper.findClassLocation(targetClassName);
      ClassFile containingClassFile =
          classFileLocation == null ? null : new ClassFile(classFileLocation, targetClassName);

      boolean isSubclassReference = symbol instanceof SuperClassSymbol;
      if (isSubclassReference
          && !ClassDumper.hasValidSuperclass(
              classDumper.loadJavaClass(sourceClassName), targetClass)) {
        return Optional.of(
            new SymbolProblem(symbol, ErrorType.INCOMPATIBLE_CLASS_CHANGE, containingClassFile));
      }

      if (!isClassAccessibleFrom(targetClass, sourceClassName)) {
        return Optional.of(
            new SymbolProblem(symbol, ErrorType.INACCESSIBLE_CLASS, containingClassFile));
      }
      return Optional.empty();
    } catch (ClassNotFoundException ex) {
      if (classDumper.isUnusedClassSymbolReference(sourceClassName, symbol)
          || classDumper.catchesLinkageError(sourceClassName)) {
        // The class reference is unused in the source
        return Optional.empty();
      }
      return Optional.of(new SymbolProblem(symbol, ErrorType.CLASS_NOT_FOUND, null));
    }
  }

  /**
   * 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 findParentSymbolProblem(String baseClassName) {
    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
        SymbolProblem problem =
            new SymbolProblem(
                new ClassSymbol(potentiallyMissingClassName), ErrorType.SYMBOL_NOT_FOUND, null);
        return Optional.of(problem);
      }
    }
    return Optional.empty();
  }

  private ImmutableList findAbstractParentProblems(
      ClassFile classFile, SuperClassSymbol superClassSymbol) {
    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);
            continue;
          }
          if (implementedMethods.contains(abstractMethod)) {
            continue;
          }
          String unimplementedMethodName = abstractMethod.getName();
          String unimplementedMethodDescriptor = abstractMethod.getSignature();

          MethodSymbol missingMethodOnClass =
              new MethodSymbol(
                  className, unimplementedMethodName, unimplementedMethodDescriptor, false);
          builder.add(
              new SymbolProblem(missingMethodOnClass, ErrorType.ABSTRACT_METHOD, classFile));
        }
        abstractClass = abstractClass.getSuperClass();
      }
    } catch (ClassNotFoundException ex) {
      // Missing classes are reported by findSymbolProblem method.
    }
    return builder.build();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy