SpyOnInterface.java
package de.schegge.errorprone;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.AbstractMockChecker.TypeExtractor;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Type;
import javax.lang.model.element.Name;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.*;
import static com.google.errorprone.matchers.Matchers.allOf;
import static java.util.stream.Collectors.toList;
@AutoService(BugChecker.class)
@BugPattern(name = "SpyOnInterface", summary = "A @Spy on an interface is only a @Mock", severity = SeverityLevel.WARNING)
public class SpyOnInterface extends BugChecker implements VariableTreeMatcher {
private static final TypeExtractor<VariableTree> SPIED_VAR = fieldAnnotatedWithOneOf(Stream.of("org.mockito.Spy"));
private static final String LINK_URL = "https://schegge.de/2021/08/mockito---der-spion-der-mich-hasste/";
public static TypeExtractor<VariableTree> fieldAnnotatedWithOneOf(Stream<String> annotationClasses) {
return extractType(allOf(isField(), anyOf(annotationClasses.map(Matchers::hasAnnotation).collect(toList()))));
}
public static <T extends Tree> TypeExtractor<T> extractType(Matcher<T> m) {
return (tree, state) -> m.matches(tree, state) ? Optional.ofNullable(ASTHelpers.getType(tree)) : Optional.empty();
}
@Override
public final Description matchVariable(VariableTree tree, VisitorState state) {
return SPIED_VAR.extract(tree, state).map(type -> checkMockedType(type, tree, state)).orElse(NO_MATCH);
}
private Description checkMockedType(Type mockedClass, VariableTree tree, VisitorState state) {
if (!mockedClass.isInterface() || tree.getInitializer() != null) {
return NO_MATCH;
}
Description.Builder description = buildDescription(tree).setLinkUrl(LINK_URL);
String modifiersWithAnnotations = Objects.requireNonNullElse(state.getSourceForNode(tree.getModifiers()), "");
String modifiersWithoutSpy = modifiersWithAnnotations.replace("@org.mockito.Spy", "").replace("@Spy", "");
String type = state.getSourceForNode(tree.getType());
Name variable = tree.getName();
description.addFix(SuggestedFix.replace(tree, String.format("@Mock %s %s %s;", modifiersWithoutSpy, type, variable)));
return description.build();
}
}