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

com.spotify.missinglink.ConflictChecker Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015 Spotify AB
 *
 * 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.spotify.missinglink;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

import com.spotify.missinglink.Conflict.ConflictCategory;
import com.spotify.missinglink.datamodel.AccessedField;
import com.spotify.missinglink.datamodel.Artifact;
import com.spotify.missinglink.datamodel.ArtifactName;
import com.spotify.missinglink.datamodel.CalledMethod;
import com.spotify.missinglink.datamodel.ClassTypeDescriptor;
import com.spotify.missinglink.datamodel.DeclaredClass;
import com.spotify.missinglink.datamodel.DeclaredField;
import com.spotify.missinglink.datamodel.DeclaredFieldBuilder;
import com.spotify.missinglink.datamodel.DeclaredMethod;
import com.spotify.missinglink.datamodel.Dependency;
import com.spotify.missinglink.datamodel.FieldDependencyBuilder;
import com.spotify.missinglink.datamodel.MethodDependencyBuilder;
import com.spotify.missinglink.datamodel.MethodDescriptor;
import com.spotify.missinglink.datamodel.TypeDescriptor;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import static java.util.stream.Collectors.toList;

/**
 * Inputs:
 * 

* 1) The full set of artifact dependencies (D). This can be extracted from the dependency graph. * The exact structure of the graph is not interesting, only knowing which dependencies are a part * of it. Note that this means that the same library can appear multiple times (with different * versions) * 2) The classpath of artifacts that is actually used (C). Ordering is important here. If a class * appears * more than once, the first occurrence will be used. *

* Assumptions: *

* 1) Each artifact could be compiled successfully using their own dependencies. * 2) Each artifact was compiled against the same JDK, or at the very least only using parts of the * JDK * that didn't change compatibility. This is not a fully safe assumption, but to catch the * kind of problems that could occur due to this would need a more broad analysis. *

* Strategy: * 1) Identify which classes are a part of D but does not exist in C. This is the missing set (M) * 2) Identify which classes are a part of D but are replaced in D (or is not the first occurrence) * This is the suspicious set (S) * 3) Walk through the class hierarchy graph. * If something depends on something in M, also add that class to M. * If something depends on something in S, also add that class to S * 4) Walk through the method call graph, starting from the main entry point (the primary artifact) * Whenever a method call is reached, check if the class and method exists. * If it doesn't exist, also look in parent classes * (implementations could exist both in superclasses and interfaces). *

* Note that we only need to try to verify the method if it's made to a class that is in M or S. * If it is in M: fail. * If it is in S: check it. *

* If we don't have access to one of the parents, we could simply assume that the call is safe * This would however lead to all methods being marked as safe, since everything ultimately * inherits from Object (or some other jdk class). *

* The alternative is to mark such calls as failures, which may lead to false positives. * This might be ok for the MVP. *

* So we need to have the JDK classes (or some other provided dependencies) as input * in order to lookup inheritance. *

*

*

* For now, this is not really in place - we simply just look at all the things in the classpath */ public class ConflictChecker { public static final ArtifactName UNKNOWN_ARTIFACT_NAME = new ArtifactName(""); /** * @param projectArtifact the main artifact of the project we're verifying * (this is considered the entry point for reachability) * @param artifactsToCheck all artifacts that are on the runtime classpath * @param allArtifacts all artifacts, including implicit artifacts (runtime provided * artifacts) * @param omitted list of artifacts If it is null, all classes are considered suspect, * not classes from classes in an omitted artifact * @return a list of conflicts */ public ImmutableList check(Artifact projectArtifact, List artifactsToCheck, List allArtifacts, List omitted) { final CheckerStateBuilder stateBuilder = new CheckerStateBuilder(); if (omitted != null) { stateBuilder.potentialConflictClasses(Sets.newHashSet()); } createCanonicalClassMapping(stateBuilder, allArtifacts, omitted); CheckerState state = stateBuilder.build(); // brute-force reachability analysis Set reachableClasses = reachableFrom(projectArtifact.classes().values(), state.knownClasses()); final ImmutableList.Builder builder = ImmutableList.builder(); // Then go through everything in the classpath to make sure all the method calls / field references // are satisfied. for (Artifact artifact : artifactsToCheck) { for (DeclaredClass clazz : artifact.classes().values()) { if (!reachableClasses.contains(clazz.className())) { continue; } for (DeclaredMethod method : clazz.methods().values()) { checkForBrokenMethodCalls(state, artifact, clazz, method, builder); checkForBrokenFieldAccess(state, artifact, clazz, method, builder); } } } return builder.build(); } /** * Create a canonical mapping of which classes are kept. First come first serve in the classpath * * @param stateBuilder conflict checker state we're populating * @param allArtifacts maven artifacts to populate checker state with */ private void createCanonicalClassMapping(CheckerStateBuilder stateBuilder, List allArtifacts, List omitted) { for (Artifact artifact : allArtifacts) { for (DeclaredClass clazz : artifact.classes().values()) { if (stateBuilder.knownClasses().putIfAbsent(clazz.className(), clazz) != null) { stateBuilder.potentialConflictClasses().map(p -> p.add(clazz.className())); } else { stateBuilder.putSourceMapping(clazz.className(), artifact.name()); } } } if (omitted != null) { for (Artifact artifact : omitted) { for (ClassTypeDescriptor classTypeDescriptor : artifact.classes().keySet()) { stateBuilder.potentialConflictClasses().get().add(classTypeDescriptor); stateBuilder.sourceMappings().putIfAbsent(classTypeDescriptor, artifact.name()); } } } } private void checkForBrokenMethodCalls(CheckerState state, Artifact artifact, DeclaredClass clazz, DeclaredMethod method, ImmutableList.Builder builder) { for (CalledMethod calledMethod : method.methodCalls()) { final TypeDescriptor owningClass = calledMethod.owner(); final DeclaredClass calledClass = state.knownClasses().get(owningClass); if (!state.potentialConflictClasses().isPresent() || state.potentialConflictClasses().get() .contains(owningClass)) { if (calledClass == null) { builder.add(conflict(ConflictCategory.CLASS_NOT_FOUND, "Class not found: " + owningClass, dependency(clazz, method, calledMethod), artifact.name(), state.sourceMappings().get(owningClass) )); } else if (missingMethod(calledMethod.descriptor(), calledClass, state.knownClasses())) { builder.add(conflict(ConflictCategory.METHOD_SIGNATURE_NOT_FOUND, "Method not found: " + calledMethod.pretty(), dependency(clazz, method, calledMethod), artifact.name(), state.sourceMappings().get(owningClass) )); } else { // Everything is ok! } } } } private void checkForBrokenFieldAccess(CheckerState state, Artifact artifact, DeclaredClass clazz, DeclaredMethod method, ImmutableList.Builder builder) { for (AccessedField field : method.fieldAccesses()) { final TypeDescriptor owningClass = field.owner(); final DeclaredClass calledClass = state.knownClasses().get(owningClass); if (!state.potentialConflictClasses().isPresent() || state.potentialConflictClasses().get() .contains(owningClass)) { DeclaredField declaredField = new DeclaredFieldBuilder() .descriptor(field.descriptor()) .name(field.name()) .build(); if (calledClass == null) { builder.add(conflict(ConflictCategory.CLASS_NOT_FOUND, "Class not found: " + owningClass, dependency(clazz, method, field), artifact.name(), state.sourceMappings().get(owningClass) )); } else if (missingField(declaredField, calledClass, state.knownClasses())) { builder.add(conflict(ConflictCategory.FIELD_NOT_FOUND, "Field not found: " + field.name(), dependency(clazz, method, field), artifact.name(), state.sourceMappings().get(owningClass) )); } else { // Everything is ok! } } } } public static ImmutableSet reachableFrom( ImmutableCollection values, Map knownClasses) { Queue toCheck = new LinkedList<>(values); Set reachable = Sets.newHashSet(); while (!toCheck.isEmpty()) { DeclaredClass current = toCheck.remove(); if (!reachable.add(current.className())) { continue; } toCheck.addAll(current.parents().stream() .map(knownClasses::get) .filter(declaredClass -> declaredClass != null) .collect(toList())); toCheck.addAll(current.methods().values() .stream() .flatMap(declaredMethod -> declaredMethod.methodCalls().stream()) .map(CalledMethod::owner) .filter(typeDescriptor -> !reachable.contains(typeDescriptor)) .map(knownClasses::get) .filter(declaredClass -> declaredClass != null) .collect(toList())); toCheck.addAll(current.methods().values() .stream() .flatMap(declaredMethod -> declaredMethod.fieldAccesses().stream()) .map(AccessedField::owner) .filter(typeDescriptor -> !reachable.contains(typeDescriptor)) .map(knownClasses::get) .filter(declaredClass -> declaredClass != null) .collect(toList())); } return ImmutableSet.copyOf(reachable); } private Conflict conflict(ConflictCategory category, String reason, Dependency dependency, ArtifactName usedBy, ArtifactName existsIn) { if (existsIn == null) { existsIn = UNKNOWN_ARTIFACT_NAME; } return new ConflictBuilder() .category(category) .dependency(dependency) .reason(reason) .usedBy(usedBy) .existsIn(existsIn) .build(); } private Dependency dependency(DeclaredClass clazz, DeclaredMethod method, CalledMethod calledMethod) { return new MethodDependencyBuilder() .fromClass(clazz.className()) .fromMethod(method.descriptor()) .fromLineNumber(calledMethod.lineNumber()) .targetMethod(calledMethod.descriptor()) .targetClass(calledMethod.owner()) .build(); } private Dependency dependency(DeclaredClass clazz, DeclaredMethod method, AccessedField field) { return new FieldDependencyBuilder() .fromClass(clazz.className()) .fromMethod(method.descriptor()) .fromLineNumber(field.lineNumber()) .targetClass(field.owner()) .fieldType(field.descriptor()) .fieldName(field.name()) .build(); } private boolean missingMethod(MethodDescriptor descriptor, DeclaredClass calledClass, Map classMap) { final DeclaredMethod method = calledClass.methods().get(descriptor); if (method != null) { // TODO: also validate return type return false; } // Might be defined in a super class for (ClassTypeDescriptor parentClass : calledClass.parents()) { final DeclaredClass declaredClass = classMap.get(parentClass); // TODO 6/2/15 mbrown -- treat properly, by flagging as a different type of Conflict // note that declaredClass can only be null not on the first call to this // method but on the recursive invocations if (declaredClass == null) { System.out.printf("Warning: Cannot find parent %s of class %s\n", parentClass, calledClass.className()); } else if (!missingMethod(descriptor, declaredClass, classMap)) { return false; } } return true; } private boolean missingField(DeclaredField field, DeclaredClass calledClass, Map classMap) { if (calledClass.fields().contains(field)) { // TODO: also validate return type return false; } // Might be defined in a super class for (ClassTypeDescriptor parentClass : calledClass.parents()) { final DeclaredClass declaredClass = classMap.get(parentClass); // TODO 6/2/15 mbrown -- treat properly, by flagging as a different type of Conflict if (declaredClass == null) { System.out.printf("Warning: Cannot find parent %s of class %s\n", parentClass, calledClass.className()); } else if (!missingField(field, declaredClass, classMap)) { return false; } } return true; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy