com.google.errorprone.bugpatterns.LambdaFunctionalInterface Maven / Gradle / Ivy
/*
* Copyright 2017 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.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Streams;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.Optional;
import javax.lang.model.element.Modifier;
/** @author [email protected] (Ali Mesbah) */
@BugPattern(
name = "LambdaFunctionalInterface",
summary =
"Use Java's utility functional interfaces instead of Function for primitive types.",
severity = SUGGESTION)
public class LambdaFunctionalInterface extends BugChecker implements MethodTreeMatcher {
private static final String JAVA_UTIL_FUNCTION_FUNCTION = "java.util.function.Function";
private static final String JAVA_LANG_NUMBER = "java.lang.Number";
private static final ImmutableMap methodMappings =
ImmutableMap.builder()
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.DoubleFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.DoubleToIntFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.DoubleToLongFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.DoubleFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.IntFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.IntToDoubleFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.IntToLongFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.IntFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.LongFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.LongToIntFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.LongToDoubleFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.LongFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.ToLongFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.ToIntFunction")
.put(
JAVA_UTIL_FUNCTION_FUNCTION + "",
"java.util.function.ToDoubleFunction")
.build();
private static final ImmutableMap applyMappings =
ImmutableMap.builder()
.put("java.util.function.DoubleToIntFunction", "applyAsInt")
.put("java.util.function.DoubleToLongFunction", "applyAsLong")
.put("java.util.function.IntToDoubleFunction", "applyAsDouble")
.put("java.util.function.IntToLongFunction", "applyAsLong")
.put("java.util.function.LongToIntFunction", "applyAsInt")
.put("java.util.function.LongToDoubleFunction", "applyAsDouble")
.put("java.util.function.ToIntFunction", "applyAsInt")
.put("java.util.function.ToDoubleFunction", "applyAsDouble")
.put("java.util.function.ToLongFunction", "applyAsLong")
.build();
/**
* Identifies methods with parameters that have a generic argument with Int, Long, or Double. If
* pre-conditions are met, it refactors them to the primitive specializations.
*
* PreConditions:
* (1): The method declaration has to be private (to do a safe refactoring)
* (2): Its parameters have to meet the following conditions:
* 2.1 Contain type java.util.function.Function
* 2.2 At least one argument type of the Function must be subtype of Number
* (3): All its invocations in the top-level enclosing class have to meet the following
* conditions as well:
* 3.1: lambda argument of Kind.LAMBDA_EXPRESSION
* 3.2: same as 2.1
* 3.3: same as 2.2
*
*
*
* Refactoring Changes for matched methods:
* (1) Add the imports
* (2) Change the method signature to use utility function instead of Function
* (3) Find and change the 'apply' calls to the corresponding applyAsT
*
*/
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
MethodSymbol methodSym = ASTHelpers.getSymbol(tree);
// precondition (1)
if (!methodSym.getModifiers().contains(Modifier.PRIVATE)) {
return Description.NO_MATCH;
}
ImmutableList params =
tree.getParameters().stream()
.filter(param -> hasFunctionAsArg(param, state))
.filter(
param ->
isFunctionArgSubtypeOf(
param, 0, state.getTypeFromString(JAVA_LANG_NUMBER), state)
|| isFunctionArgSubtypeOf(
param, 1, state.getTypeFromString(JAVA_LANG_NUMBER), state))
.collect(toImmutableList());
// preconditions (2) and (3)
if (params.isEmpty() || !methodCallsMeetConditions(methodSym, state)) {
return Description.NO_MATCH;
}
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
for (Tree param : params) {
getMappingForFunctionFromTree(param)
.ifPresent(
mappedFunction -> {
fixBuilder.addImport(getImportName(mappedFunction));
fixBuilder.replace(
param,
getFunctionName(mappedFunction) + " " + ASTHelpers.getSymbol(param).name);
refactorInternalApplyMethods(tree, fixBuilder, param, mappedFunction);
});
}
return describeMatch(tree, fixBuilder.build());
}
private void refactorInternalApplyMethods(
MethodTree tree, SuggestedFix.Builder fixBuilder, Tree param, String mappedFunction) {
getMappingForApply(mappedFunction)
.ifPresent(
apply -> {
tree.accept(
new TreeScanner() {
@Override
public Void visitMethodInvocation(MethodInvocationTree callTree, Void unused) {
if (getSymbol(callTree).name.contentEquals("apply")) {
Symbol receiverSym = getSymbol(getReceiver(callTree));
if (receiverSym != null
&& receiverSym.equals(ASTHelpers.getSymbol(param))) {
fixBuilder.replace(
callTree.getMethodSelect(), receiverSym.name + "." + apply);
}
}
return super.visitMethodInvocation(callTree, unused);
}
},
null);
});
}
private boolean methodCallsMeetConditions(Symbol sym, VisitorState state) {
ImmutableMultimap methodCallMap =
methodCallsForSymbol(sym, getTopLevelClassTree(state));
if (methodCallMap.isEmpty()) {
// no method invocations for this method, safe to refactor
return true;
}
for (MethodInvocationTree methodInvocationTree : methodCallMap.values()) {
if (methodInvocationTree.getArguments().stream()
.filter(a -> Kind.LAMBDA_EXPRESSION.equals(a.getKind()))
.filter(a -> hasFunctionAsArg(a, state))
.noneMatch(
a ->
isFunctionArgSubtypeOf(a, 0, state.getTypeFromString(JAVA_LANG_NUMBER), state)
|| isFunctionArgSubtypeOf(
a, 1, state.getTypeFromString(JAVA_LANG_NUMBER), state))) {
return false;
}
}
return true;
}
private static ClassTree getTopLevelClassTree(VisitorState state) {
return (ClassTree)
Streams.findLast(
Streams.stream(state.getPath().iterator())
.filter((Tree t) -> t.getKind() == Kind.CLASS))
.orElseThrow(() -> new IllegalArgumentException("No enclosing class found"));
}
private ImmutableMultimap methodCallsForSymbol(
Symbol sym, ClassTree classTree) {
ImmutableMultimap.Builder methodMap = ImmutableMultimap.builder();
classTree.accept(
new TreeScanner() {
@Override
public Void visitMethodInvocation(MethodInvocationTree callTree, Void unused) {
final MethodSymbol methodSymbol = getSymbol(callTree);
if (methodSymbol != null && sym.equals(methodSymbol)) {
methodMap.put(methodSymbol.toString(), callTree);
}
return super.visitMethodInvocation(callTree, unused);
}
},
null);
return methodMap.build();
}
private static boolean hasFunctionAsArg(Tree param, VisitorState state) {
return ASTHelpers.isSameType(
ASTHelpers.getType(param), state.getTypeFromString(JAVA_UTIL_FUNCTION_FUNCTION), state);
}
private static boolean isFunctionArgSubtypeOf(
Tree param, int argIndex, Type type, VisitorState state) {
return ASTHelpers.isSubtype(
ASTHelpers.getType(param).getTypeArguments().get(argIndex), type, state);
}
private static Optional getMappingForFunctionFromTree(Tree param) {
Optional type = ofNullable(ASTHelpers.getType(param));
return (type == null) ? empty() : getMappingForFunction(type.get().toString());
}
private static Optional getMappingForFunction(String function) {
return ofNullable(methodMappings.get(function));
}
private static Optional getMappingForApply(String apply) {
return ofNullable(applyMappings.get(apply));
}
private static String getFunctionName(String fullyQualifiedName) {
return fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.') + 1);
}
private static String getImportName(String fullyQualifiedName) {
int cutPosition = fullyQualifiedName.indexOf('<');
return (cutPosition < 0) ? fullyQualifiedName : fullyQualifiedName.substring(0, cutPosition);
}
}