org.sonar.java.checks.StandardFunctionalInterfaceCheck Maven / Gradle / Ivy
/*
* SonarQube Java
* Copyright (C) 2012-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
@Rule(key = "S1711")
public class StandardFunctionalInterfaceCheck extends IssuableSubscriptionVisitor {
private static final MethodMatchers OBJECT_METHODS = MethodMatchers.or(
MethodMatchers.create()
.ofAnyType()
.names("equals")
.addParametersMatcher("java.lang.Object")
.build(),
MethodMatchers.create()
.ofAnyType()
.names("getClass", "hashcode", "notify", "notifyAll", "toString")
.addWithoutParametersMatcher()
.build(),
MethodMatchers.create()
.ofAnyType()
.names("wait")
.addWithoutParametersMatcher()
.addParametersMatcher("long")
.addParametersMatcher("long", "int")
.build());
private static final Set STD_INTERFACE_NAMES = new HashSet<>();
private static final Map> STD_INTERFACE_BY_PARAMETER_COUNT = new HashMap<>();
static {
registerInterface("java.util.function.BiConsumer", "void", "T", "U");
registerInterface("java.util.function.BiFunction", "R", "T", "U");
registerInterface("java.util.function.BinaryOperator", "T", "T", "T");
registerInterface("java.util.function.BiPredicate", "boolean", "T", "U");
registerInterface("java.util.function.BooleanSupplier", "boolean");
registerInterface("java.util.function.Consumer", "void", "T");
registerInterface("java.util.function.DoubleBinaryOperator", "double", "double", "double");
registerInterface("java.util.function.DoubleConsumer", "void", "double");
registerInterface("java.util.function.DoubleFunction", "R", "double");
registerInterface("java.util.function.DoublePredicate", "boolean", "double");
registerInterface("java.util.function.DoubleSupplier", "double");
registerInterface("java.util.function.DoubleToIntFunction", "int", "double");
registerInterface("java.util.function.DoubleToLongFunction", "long", "double");
registerInterface("java.util.function.DoubleUnaryOperator", "double", "double");
registerInterface("java.util.function.Function", "R", "T");
registerInterface("java.util.function.IntBinaryOperator", "int", "int", "int");
registerInterface("java.util.function.IntConsumer", "void", "int");
registerInterface("java.util.function.IntFunction", "R", "int");
registerInterface("java.util.function.IntPredicate", "boolean", "int");
registerInterface("java.util.function.IntSupplier", "int");
registerInterface("java.util.function.IntToDoubleFunction", "double", "int");
registerInterface("java.util.function.IntToLongFunction", "long", "int");
registerInterface("java.util.function.IntUnaryOperator", "int", "int");
registerInterface("java.util.function.LongBinaryOperator", "long", "long", "long");
registerInterface("java.util.function.LongConsumer", "void", "long");
registerInterface("java.util.function.LongFunction", "R", "long");
registerInterface("java.util.function.LongPredicate", "boolean", "long");
registerInterface("java.util.function.LongSupplier", "long");
registerInterface("java.util.function.LongToDoubleFunction", "double", "long");
registerInterface("java.util.function.LongToIntFunction", "int", "long");
registerInterface("java.util.function.LongUnaryOperator", "long", "long");
registerInterface("java.util.function.ObjDoubleConsumer", "void", "T", "double");
registerInterface("java.util.function.ObjIntConsumer", "void", "T", "int");
registerInterface("java.util.function.ObjLongConsumer", "void", "T", "long");
registerInterface("java.util.function.Predicate", "boolean", "T");
registerInterface("java.util.function.Supplier", "T");
registerInterface("java.util.function.ToDoubleBiFunction", "double", "T", "U");
registerInterface("java.util.function.ToDoubleFunction", "double", "T");
registerInterface("java.util.function.ToIntBiFunction", "int", "T", "U");
registerInterface("java.util.function.ToIntFunction", "int", "T");
registerInterface("java.util.function.ToLongBiFunction", "long", "T", "U");
registerInterface("java.util.function.ToLongFunction", "long", "T");
registerInterface("java.util.function.UnaryOperator", "T", "T");
// Each list of FunctionalInterface has to be sorted ascending by number of parametrized types so that smallest number
// of parametrized types take precedence. For example UnaryOperator and Function are equivalent,
// but UnaryOperator is preferred.
STD_INTERFACE_BY_PARAMETER_COUNT.values().forEach(list -> list.sort((a, b) -> Integer.compare(a.getGenericTypeCount(), b.getGenericTypeCount())));
}
private static void registerInterface(String name, String returnType, String... parameters) {
FunctionalInterface functionalInterface = new FunctionalInterface(name, returnType, parameters);
STD_INTERFACE_NAMES.add(functionalInterface.getName());
STD_INTERFACE_BY_PARAMETER_COUNT.computeIfAbsent(functionalInterface.getParameterCount(), key -> new ArrayList<>()).add(functionalInterface);
}
@Override
public List nodesToVisit() {
return Collections.singletonList(Tree.Kind.INTERFACE);
}
@Override
public void visitNode(Tree tree) {
ClassTree classTree = (ClassTree) tree;
// classTree.simpleName() never null for Tree.Kind.INTERFACE
IdentifierTree issueLocation = classTree.simpleName();
// The question "Why we raise issue only for interface annotated with @FunctionalInterface?"
// is discussed in comments of https://jira.sonarsource.com/browse/SONARJAVA-504
Optional.of(classTree)
.filter(StandardFunctionalInterfaceCheck::isFunctionalInterface)
.filter(StandardFunctionalInterfaceCheck::isNonStandardFunctionalInterface)
.filter(StandardFunctionalInterfaceCheck::hasNoExtension)
.flatMap(StandardFunctionalInterfaceCheck::lookupFunctionalMethod)
.flatMap(StandardFunctionalInterfaceCheck::lookupMatchingStandardInterface)
.ifPresent(standardInterface -> reportIssue(issueLocation, buildIssueMessage(classTree, standardInterface.replace('$', '.'))));
}
private static boolean isFunctionalInterface(ClassTree tree) {
return tree.symbol().metadata().isAnnotatedWith("java.lang.FunctionalInterface");
}
private static boolean isNonStandardFunctionalInterface(ClassTree tree) {
return !STD_INTERFACE_NAMES.contains(tree.symbol().type().fullyQualifiedName());
}
private static boolean hasNoExtension(ClassTree tree) {
return tree.superInterfaces().isEmpty();
}
private static Optional lookupFunctionalMethod(ClassTree interfaceTree) {
return interfaceTree.symbol().memberSymbols().stream()
.filter(Symbol::isMethodSymbol)
.map(MethodSymbol.class::cast)
.filter(MethodSymbol::isAbstract)
.filter(StandardFunctionalInterfaceCheck::isNotObjectMethod)
.findFirst();
}
private static Optional lookupMatchingStandardInterface(MethodSymbol functionalMethod) {
MethodTree declaration = functionalMethod.declaration();
if (!functionalMethod.thrownTypes().isEmpty() || (declaration != null && !declaration.typeParameters().isEmpty())) {
return Optional.empty();
}
Type returnType = declaration != null ? declaration.returnType().symbolType() : functionalMethod.returnType().type();
return STD_INTERFACE_BY_PARAMETER_COUNT.getOrDefault(functionalMethod.parameterTypes().size(), Collections.emptyList()).stream()
.map(standardInterface -> standardInterface.matchingSpecialization(functionalMethod, returnType))
.filter(Objects::nonNull)
.findFirst();
}
private static String buildIssueMessage(ClassTree interfaceTree, String standardInterface) {
if (interfaceTree.members().size() <= 1) {
return "Drop this interface in favor of \"" + standardInterface + "\".";
}
return "Make this interface extend \"" + standardInterface + "\" and remove the functional method declaration.";
}
private static boolean isNotObjectMethod(MethodSymbol method) {
MethodTree declaration = method.declaration();
return declaration == null || !OBJECT_METHODS.matches(declaration);
}
private static class FunctionalInterface {
private final String name;
private final List genericTypes;
private final String returnType;
private final List parameters;
private FunctionalInterface(String name, String returnType, String... parameters) {
int genericStart = name.indexOf('<');
if (genericStart != -1) {
this.name = name.substring(0, genericStart);
this.genericTypes = Arrays.asList(name.substring(genericStart + 1, name.length() - 1).split(","));
} else {
this.name = name;
this.genericTypes = Collections.emptyList();
}
this.returnType = returnType;
this.parameters = Arrays.asList(parameters);
}
private String getName() {
return name;
}
private int getGenericTypeCount() {
return genericTypes.size();
}
private int getParameterCount() {
return parameters.size();
}
@CheckForNull
private String matchingSpecialization(MethodSymbol method, Type actualReturnType) {
Map genericTypeMapping = genericTypes.isEmpty() ? Collections.emptyMap() : new HashMap<>();
String expectedReturnType = convertGenericType(returnType, actualReturnType, genericTypeMapping);
if (!expectedReturnType.equals(actualReturnType.fullyQualifiedName())) {
return null;
}
List methodParameters = method.parameterTypes();
for (int i = 0; i < parameters.size(); i++) {
Type actualType = methodParameters.get(i);
String expectedType = convertGenericType(parameters.get(i), actualType, genericTypeMapping);
if (!expectedType.equals(actualType.fullyQualifiedName())) {
return null;
}
}
return buildSpecializationName(genericTypeMapping);
}
private String convertGenericType(String expectedType, Type actualType, Map genericTypeMapping) {
if (genericTypes.isEmpty() || !genericTypes.contains(expectedType)) {
return expectedType;
}
String convertedType = genericTypeMapping.get(expectedType);
if (convertedType == null) {
if (actualType.isPrimitive() || actualType.isVoid() || actualType.isArray() || actualType.isUnknown()) {
return "!unknown!";
}
convertedType = actualType.fullyQualifiedName();
genericTypeMapping.put(expectedType, convertedType);
}
return convertedType;
}
private String buildSpecializationName(Map genericTypeMapping) {
if (genericTypes.isEmpty()) {
return name;
}
StringBuilder genericName = new StringBuilder();
genericName.append(name);
genericName.append('<');
boolean addComma = false;
for (String genericType : genericTypes) {
if (addComma) {
genericName.append(',');
} else {
addComma = true;
}
String typeName = genericTypeMapping.getOrDefault(genericType, genericType);
int packageEnd = typeName.lastIndexOf('.');
if (packageEnd != -1) {
typeName = typeName.substring(packageEnd + 1);
}
genericName.append(typeName);
}
genericName.append('>');
return genericName.toString();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy