Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.google.errorprone.bugpatterns.UnusedMethod Maven / Gradle / Ivy
/*
* Copyright 2018 The Error Prone Authors.
*
* 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.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Multimaps.asMap;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.fixes.SuggestedFix.emptyFix;
import static com.google.errorprone.fixes.SuggestedFixes.replaceIncludingComments;
import static com.google.errorprone.matchers.Matchers.SERIALIZATION_METHODS;
import static com.google.errorprone.suppliers.Suppliers.typeFromString;
import static com.google.errorprone.util.ASTHelpers.canBeRemoved;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor;
import static com.google.errorprone.util.ASTHelpers.isSubtype;
import static com.google.errorprone.util.ASTHelpers.scope;
import static com.google.errorprone.util.ASTHelpers.shouldKeep;
import static com.google.errorprone.util.MoreAnnotations.asStrings;
import static com.google.errorprone.util.MoreAnnotations.getAnnotationValue;
import static java.lang.String.format;
import static javax.lang.model.element.ElementKind.CONSTRUCTOR;
import static javax.lang.model.element.ElementKind.FIELD;
import static javax.lang.model.element.Modifier.FINAL;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.suppliers.Supplier;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
/** Bugpattern to detect unused declarations. */
@BugPattern(
altNames = {"Unused", "unused", "UnusedParameters"},
summary = "Unused.",
severity = WARNING,
documentSuppression = false)
public final class UnusedMethod extends BugChecker implements CompilationUnitTreeMatcher {
private static final String GWT_JAVASCRIPT_OBJECT = "com.google.gwt.core.client.JavaScriptObject";
private static final String EXEMPT_PREFIX = "unused";
private static final String JUNIT_PARAMS_VALUE = "value";
private static final String JUNIT_PARAMS_ANNOTATION_TYPE = "junitparams.Parameters";
private static final ImmutableSet EXEMPTING_METHOD_ANNOTATIONS =
ImmutableSet.of(
"com.fasterxml.jackson.annotation.JsonCreator",
"com.fasterxml.jackson.annotation.JsonValue",
"com.google.acai.AfterTest",
"com.google.acai.BeforeSuite",
"com.google.acai.BeforeTest",
"com.google.caliper.Benchmark",
"com.google.common.eventbus.Subscribe",
"com.google.inject.Provides",
"com.google.inject.Inject",
"com.google.inject.multibindings.ProvidesIntoMap",
"com.google.inject.multibindings.ProvidesIntoSet",
"com.google.inject.throwingproviders.CheckedProvides",
"com.tngtech.java.junit.dataprovider.DataProvider",
"jakarta.annotation.PreDestroy",
"jakarta.annotation.PostConstruct",
"jakarta.inject.Inject",
"jakarta.persistence.PostLoad",
"jakarta.persistence.PostPersist",
"jakarta.persistence.PostRemove",
"jakarta.persistence.PostUpdate",
"jakarta.persistence.PrePersist",
"jakarta.persistence.PreRemove",
"jakarta.persistence.PreUpdate",
"javax.annotation.PreDestroy",
"javax.annotation.PostConstruct",
"javax.inject.Inject",
"javax.persistence.PostLoad",
"javax.persistence.PostPersist",
"javax.persistence.PostRemove",
"javax.persistence.PostUpdate",
"javax.persistence.PrePersist",
"javax.persistence.PreRemove",
"javax.persistence.PreUpdate",
"org.apache.beam.sdk.transforms.DoFn.ProcessElement",
"org.aspectj.lang.annotation.Pointcut",
"org.aspectj.lang.annotation.After",
"org.aspectj.lang.annotation.Before",
"org.springframework.context.annotation.Bean",
"org.testng.annotations.AfterClass",
"org.testng.annotations.AfterMethod",
"org.testng.annotations.BeforeClass",
"org.testng.annotations.BeforeMethod",
"org.testng.annotations.DataProvider",
"org.junit.jupiter.api.BeforeAll",
"org.junit.jupiter.api.AfterAll",
"org.junit.jupiter.api.AfterEach",
"org.junit.jupiter.api.BeforeEach",
"org.junit.jupiter.api.RepeatedTest",
"org.junit.jupiter.api.Test",
"org.junit.jupiter.params.ParameterizedTest");
/** The set of types exempting a type that is extending or implementing them. */
private static final ImmutableSet EXEMPTING_SUPER_TYPES = ImmutableSet.of();
private final ImmutableSet additionalExemptingMethodAnnotations;
public UnusedMethod(ErrorProneFlags errorProneFlags) {
this.additionalExemptingMethodAnnotations =
errorProneFlags
.getList("UnusedMethod:ExemptingMethodAnnotations")
.map(ImmutableSet::copyOf)
.orElseGet(ImmutableSet::of);
}
@Override
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
// Map of symbols to method declarations. Initially this is a map of all of the methods. As we
// go we remove those variables which are used.
Map unusedMethods = new HashMap<>();
// We will skip reporting on the whole compilation if there are any native methods found.
// Use a TreeScanner to find all local variables and fields.
if (hasNativeMethods(tree)) {
return Description.NO_MATCH;
}
AtomicBoolean ignoreUnusedMethods = new AtomicBoolean(false);
ImmutableSet classesMadeVisible = getVisibleClasses(tree);
class MethodFinder extends SuppressibleTreePathScanner {
MethodFinder(VisitorState state) {
super(state);
}
@Override
public Void visitClass(ClassTree tree, Void unused) {
if (exemptedBySuperType(getType(tree), state)) {
return null;
}
return super.visitClass(tree, null);
}
private boolean exemptedBySuperType(Type type, VisitorState state) {
return EXEMPTING_SUPER_TYPES.stream()
.anyMatch(t -> isSubtype(type, typeFromString(t).get(state), state));
}
@Override
public Void visitMethod(MethodTree tree, Void unused) {
if (hasJUnitParamsParametersForMethodAnnotation(tree.getModifiers().getAnnotations())) {
// Since this method uses @Parameters, there will be another method that appears to
// be unused. Don't warn about unusedMethods at all in this case.
ignoreUnusedMethods.set(true);
}
if (isMethodSymbolEligibleForChecking(tree, classesMadeVisible)) {
unusedMethods.put(getSymbol(tree), getCurrentPath());
}
return super.visitMethod(tree, unused);
}
private boolean hasJUnitParamsParametersForMethodAnnotation(
Collection annotations) {
for (AnnotationTree tree : annotations) {
JCAnnotation annotation = (JCAnnotation) tree;
if (annotation.getAnnotationType().type != null
&& annotation
.getAnnotationType()
.type
.toString()
.equals(JUNIT_PARAMS_ANNOTATION_TYPE)) {
if (annotation.getArguments().isEmpty()) {
// @Parameters, which uses implicit provider methods
return true;
}
for (JCExpression arg : annotation.getArguments()) {
if (arg.getKind() != Kind.ASSIGNMENT) {
// Implicit value annotation, e.g. @Parameters({"1"}); no exemption required.
return false;
}
JCExpression var = ((JCAssign) arg).getVariable();
if (var.getKind() == Kind.IDENTIFIER) {
// Anything that is not @Parameters(value = ...), e.g.
// @Parameters(source = ...) or @Parameters(method = ...)
if (!((IdentifierTree) var).getName().contentEquals(JUNIT_PARAMS_VALUE)) {
return true;
}
}
}
}
}
return false;
}
private boolean isMethodSymbolEligibleForChecking(
MethodTree tree, Set classesMadeVisible) {
if (exemptedByName(tree.getName())) {
return false;
}
// Assume the method is called if annotated with a called-reflectively annotation.
if (exemptedByAnnotation(tree.getModifiers().getAnnotations())) {
return false;
}
if (shouldKeep(tree)) {
return false;
}
MethodSymbol methodSymbol = getSymbol(tree);
if (!canBeRemoved(methodSymbol, state)) {
return false;
}
if (isExemptedConstructor(methodSymbol, state)
|| isGeneratedConstructor(tree)
|| SERIALIZATION_METHODS.matches(tree, state)) {
return false;
}
// Ignore this method if the last parameter is a GWT JavaScriptObject.
if (!tree.getParameters().isEmpty()) {
Type lastParamType = getType(getLast(tree.getParameters()));
if (lastParamType != null && lastParamType.toString().equals(GWT_JAVASCRIPT_OBJECT)) {
return false;
}
}
if (!methodSymbol.isPrivate()
&& classesMadeVisible.stream()
.anyMatch(t -> isSubtype(t.type, methodSymbol.owner.type, state))) {
return false;
}
return true;
}
private boolean isExemptedConstructor(MethodSymbol methodSymbol, VisitorState state) {
if (!methodSymbol.getKind().equals(CONSTRUCTOR)) {
return false;
}
// Don't delete unused zero-arg constructors, given those are often there to limit
// instantiating the class at all (e.g. in utility classes).
if (methodSymbol.params().isEmpty()) {
return true;
}
return false;
}
}
new MethodFinder(state).scan(state.getPath(), null);
class FilterUsedMethods extends TreePathScanner {
@Override
public Void visitMemberSelect(MemberSelectTree memberSelectTree, Void unused) {
Symbol symbol = getSymbol(memberSelectTree);
unusedMethods.remove(symbol);
return super.visitMemberSelect(memberSelectTree, null);
}
@Override
public Void visitMemberReference(MemberReferenceTree tree, Void unused) {
super.visitMemberReference(tree, null);
MethodSymbol symbol = getSymbol(tree);
unusedMethods.remove(symbol);
symbol.getParameters().forEach(unusedMethods::remove);
return null;
}
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
handle(getSymbol(tree));
return super.visitMethodInvocation(tree, null);
}
@Override
public Void visitNewClass(NewClassTree tree, Void unused) {
handle(getSymbol(tree));
return super.visitNewClass(tree, null);
}
@Override
public Void visitAssignment(AssignmentTree tree, Void unused) {
handle(getSymbol(tree.getVariable()));
return super.visitAssignment(tree, unused);
}
private void handle(Symbol symbol) {
if (symbol instanceof MethodSymbol) {
unusedMethods.remove(symbol);
}
}
@Override
public Void visitMethod(MethodTree tree, Void unused) {
handleMethodSource(tree);
return super.visitMethod(tree, null);
}
/**
* If a method is annotated with @MethodSource, the annotation value refers to another method
* that is used reflectively to supply test parameters, so that method should not be
* considered unused.
*/
private void handleMethodSource(MethodTree tree) {
MethodSymbol sym = getSymbol(tree);
Name name = ORG_JUNIT_JUPITER_PARAMS_PROVIDER_METHODSOURCE.get(state);
sym.getRawAttributes().stream()
.filter(a -> a.type.tsym.getQualifiedName().equals(name))
.findAny()
// get the annotation value array as a set of Names
.flatMap(a -> getAnnotationValue(a, "value"))
.map(
y -> asStrings(y).map(state::getName).map(Name::toString).collect(toImmutableSet()))
// remove all potentially unused methods referenced by the @MethodSource
.ifPresent(
referencedNames ->
unusedMethods
.entrySet()
.removeIf(
e -> {
Symbol unusedSym = e.getKey();
String simpleName = unusedSym.getSimpleName().toString();
return referencedNames.contains(simpleName)
|| referencedNames.contains(
unusedSym.owner.getQualifiedName() + "#" + simpleName);
}));
}
}
new FilterUsedMethods().scan(state.getPath(), null);
if (ignoreUnusedMethods.get()) {
return Description.NO_MATCH;
}
fixNonConstructors(
unusedMethods.values().stream()
.filter(t -> !getSymbol(t.getLeaf()).isConstructor())
.collect(toImmutableList()),
state);
// Group unused constructors by the owning class to generate fixes, so that if we remove the
// last constructor, we add a private one.
ImmutableListMultimap unusedConstructors =
unusedMethods.values().stream()
.filter(t -> getSymbol(t.getLeaf()).isConstructor())
.collect(toImmutableListMultimap(t -> getSymbol(t.getLeaf()).owner, t -> t));
fixConstructors(unusedConstructors, state);
return Description.NO_MATCH;
}
private ImmutableSet getVisibleClasses(CompilationUnitTree tree) {
ImmutableSet.Builder classesMadeVisible = ImmutableSet.builder();
new TreePathScanner() {
@Override
public Void visitClass(ClassTree tree, Void unused) {
var symbol = getSymbol(tree);
if (!canBeRemoved(symbol)) {
classesMadeVisible.add(symbol);
}
return super.visitClass(tree, null);
}
}.scan(tree, null);
return classesMadeVisible.build();
}
private void fixNonConstructors(Iterable unusedPaths, VisitorState state) {
for (TreePath unusedPath : unusedPaths) {
Tree unusedTree = unusedPath.getLeaf();
MethodSymbol symbol = getSymbol((MethodTree) unusedTree);
String message = String.format("Method '%s' is never used.", symbol.getSimpleName());
state.reportMatch(
buildDescription(unusedTree)
.addFix(replaceIncludingComments(unusedPath, "", state))
.setMessage(message)
.build());
}
}
private void fixConstructors(
ImmutableListMultimap unusedConstructors, VisitorState state) {
for (Map.Entry> entry : asMap(unusedConstructors).entrySet()) {
Symbol symbol = entry.getKey();
List trees = entry.getValue();
SuggestedFix.Builder fix = SuggestedFix.builder();
int constructorCount = size(scope(symbol.members()).getSymbols(Symbol::isConstructor));
int finalFields =
size(
scope(symbol.members())
.getSymbols(s -> s.getKind().equals(FIELD) && s.getModifiers().contains(FINAL)));
boolean fixable;
if (constructorCount == trees.size()) {
fix.postfixWith(
getLast(trees).getLeaf(), format("private %s() {}", symbol.getSimpleName()));
fixable = finalFields == 0;
} else {
fixable = true;
}
String message = String.format("Constructor '%s' is never used.", symbol.getSimpleName());
trees.forEach(t -> fix.merge(replaceIncludingComments(t, "", state)));
state.reportMatch(
buildDescription(trees.get(0).getLeaf())
.addFix(fixable ? fix.build() : emptyFix())
.setMessage(message)
.build());
}
}
private static boolean hasNativeMethods(CompilationUnitTree tree) {
AtomicBoolean hasAnyNativeMethods = new AtomicBoolean(false);
new TreeScanner() {
@Override
public Void visitMethod(MethodTree tree, Void unused) {
if (tree.getModifiers().getFlags().contains(Modifier.NATIVE)) {
hasAnyNativeMethods.set(true);
}
return null;
}
}.scan(tree, null);
return hasAnyNativeMethods.get();
}
/**
* Looks at the list of {@code annotations} and see if there is any annotation which exists {@code
* exemptingAnnotations}.
*/
private boolean exemptedByAnnotation(List annotations) {
for (AnnotationTree annotation : annotations) {
Type annotationType = getType(annotation);
if (annotationType == null) {
continue;
}
TypeSymbol tsym = annotationType.tsym;
String annotationName = tsym.getQualifiedName().toString();
if (EXEMPTING_METHOD_ANNOTATIONS.contains(annotationName)
|| additionalExemptingMethodAnnotations.contains(annotationName)) {
return true;
}
}
return false;
}
private static boolean exemptedByName(Name name) {
return Ascii.toLowerCase(name.toString()).startsWith(EXEMPT_PREFIX);
}
private static final Supplier
ORG_JUNIT_JUPITER_PARAMS_PROVIDER_METHODSOURCE =
VisitorState.memoize(
state -> state.getName("org.junit.jupiter.params.provider.MethodSource"));
}