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

org.sonar.java.checks.SpecializedFunctionalInterfacesCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube Java
 * Copyright (C) 2012-2025 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.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TypeTree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S4276")
public class SpecializedFunctionalInterfacesCheck extends IssuableSubscriptionVisitor {

  private static final String AND_THEN = "andThen";

  @Override
  public List nodesToVisit() {
    return Arrays.asList(Tree.Kind.CLASS, Tree.Kind.VARIABLE);
  }

  @Override
  public void visitNode(Tree tree) {
    if (tree.is(Tree.Kind.CLASS)) {
      checkClassInterfaces(((ClassTree) tree));
    } else {
      checkVariableTypeAndInitializer((VariableTree) tree);
    }
  }

  private void checkClassInterfaces(ClassTree tree) {
    List reportTreeAndStringInterfaces = tree.superInterfaces().stream()
      .map(typeTree -> matchFunctionalInterface(typeTree.symbolType(), Collections.emptyList())
      .map(rs -> new InterfaceTreeAndStringPairReport(rs, typeTree)).orElse(null))
      .filter(Objects::nonNull)
      .toList();
    if (reportTreeAndStringInterfaces.isEmpty()) {
      return;
    }
    List secondaryLocations = reportTreeAndStringInterfaces.stream()
      .map(interf -> new JavaFileScannerContext.Location("Replace this interface.", interf.classInterface))
      .toList();
    reportIssue(tree.simpleName(), reportMessage(reportTreeAndStringInterfaces), secondaryLocations, null);
  }

  private void checkVariableTypeAndInitializer(VariableTree variableTree) {
    ExpressionTree initializer = variableTree.initializer();
    if ((variableTree.symbol().owner().isMethodSymbol() && !variableTree.parent().is(Tree.Kind.LAMBDA_EXPRESSION))
      || (initializer != null && (initializer.is(Tree.Kind.LAMBDA_EXPRESSION) || isAnonymousClass(initializer)))) {
      matchFunctionalInterface(variableTree.symbol().type(), variableTree.symbol().usages()).ifPresent(reportString -> {
        TypeTree variableType = variableTree.type();
        reportIssue(variableType, reportMessage(new InterfaceTreeAndStringPairReport(reportString, variableType)));
      });
    }
  }

  private static String reportMessage(InterfaceTreeAndStringPairReport onlyOneInterface) {
    return reportMessage(Collections.singletonList(onlyOneInterface));
  }

  private static String reportMessage(List interfacesToBeReported) {
    String functionalInterfaces = interfacesToBeReported.stream().map(x -> x.reportString)
      .collect(Collectors.joining("', '", (interfacesToBeReported.size() > 1 ? "s '" : " '"), "'"));
    return String.format("Refactor this code to use the more specialised Functional Interface%s", functionalInterfaces);
  }

  private static boolean isAnonymousClass(ExpressionTree initializeTree) {
    return initializeTree.is(Tree.Kind.NEW_CLASS) && ((NewClassTree) initializeTree).classBody() != null;
  }

  private static Optional matchFunctionalInterface(Type type, List usages) {
    if (type.isUnknown() || !type.isParameterized()) {
      return Optional.empty();
    }
    switch (type.fullyQualifiedName()) {
      case "java.util.function.Function":
        return handleFunctionInterface(type, usages);
      case "java.util.function.BiFunction":
        return handleBiFunctionInterface(type, usages);
      case "java.util.function.BiConsumer":
        return handleBiConsumerInterface(type, usages);
      case "java.util.function.Supplier":
        return handleSupplier(type, usages);
      case "java.util.function.Consumer",
        "java.util.function.Predicate",
        "java.util.function.UnaryOperator",
        "java.util.function.BinaryOperator":
        return handleSingleParameterFunctions(type, usages);
      default:
        return Optional.empty();
    }
  }

  private static Optional handleSingleParameterFunctions(Type parametrizedType, List usages) {
    if (isReferenced(usages)) {
      return Optional.empty();
    }
    return Optional.ofNullable(new ParameterTypeNameAndTreeType(parametrizedType, 0).paramTypeName)
      .map(s -> s + parametrizedType.name());
  }

  private static Optional handleFunctionInterface(Type parametrizedType,
                                                          List usages) {
    ParameterTypeNameAndTreeType firstArgument = new ParameterTypeNameAndTreeType(parametrizedType, 0);
    ParameterTypeNameAndTreeType secondArgument = new ParameterTypeNameAndTreeType(parametrizedType, 1);
    boolean usedAsMethodReference = isReferenced(usages);
    if (typeEquals(firstArgument.paramType, secondArgument.paramType)) {
      if (firstArgument.paramTypeName != null && !usedAsMethodReference) {
        return functionalInterfaceName("%sUnaryOperator", firstArgument.paramTypeName);
      }
      return functionalInterfaceName("UnaryOperator<%s>", firstArgument.paramType);
    }
    if (usesMethods(usages, Arrays.asList("compose", AND_THEN))) {
      return Optional.empty();
    }
    if (isBoolean(secondArgument) && !usedAsMethodReference) {
      return functionalInterfaceName("Predicate<%s>", firstArgument.paramType);
    }
    if (isBoolean(firstArgument)) {
      return Optional.empty();
    }
    if (firstArgument.paramTypeName != null && secondArgument.paramTypeName != null && !usedAsMethodReference) {
      return functionalInterfaceName("%sTo%sFunction", firstArgument.paramTypeName, secondArgument.paramTypeName);
    }
    if (secondArgument.paramTypeName != null && !usedAsMethodReference) {
      return functionalInterfaceName("To%sFunction<%s>", secondArgument.paramTypeName, firstArgument.paramType);
    }
    if (firstArgument.paramTypeName != null && !usedAsMethodReference) {
      return functionalInterfaceName("%sFunction<%s>", firstArgument.paramTypeName, secondArgument.paramType);
    }
    return Optional.empty();
  }

  private static boolean usesMethods(List usages, List methods) {
    return usages.stream()
      .map(IdentifierTree::parent)
      .filter(MemberSelectExpressionTree.class::isInstance)
      .map(MemberSelectExpressionTree.class::cast)
      .map(MemberSelectExpressionTree::identifier)
      .map(IdentifierTree::name)
      .anyMatch(methods::contains);
  }

  private static Optional handleBiFunctionInterface(Type parametrizedType, List usages) {
    ParameterTypeNameAndTreeType firstArgument = new ParameterTypeNameAndTreeType(parametrizedType, 0);
    ParameterTypeNameAndTreeType secondArgument = new ParameterTypeNameAndTreeType(parametrizedType, 1);
    ParameterTypeNameAndTreeType thirdArgument = new ParameterTypeNameAndTreeType(parametrizedType, 2);
    if (typeEquals(firstArgument.paramType, secondArgument.paramType) && typeEquals(firstArgument.paramType, thirdArgument.paramType)) {
      return functionalInterfaceName("BinaryOperator<%s>", firstArgument.paramType);
    }
    if (usesMethods(usages, Collections.singletonList(AND_THEN))) {
      return Optional.empty();
    }
    if (isBoolean(thirdArgument)) {
      return functionalInterfaceName("BiPredicate<%s, %s>", firstArgument.paramType, secondArgument.paramType);
    }
    return Optional.empty();
  }

  private static Optional functionalInterfaceName(String pattern, Object... args) {
    return Optional.of(String.format(pattern, args));
  }

  private static Optional handleBiConsumerInterface(Type parametrizedType, List usages) {
    if (isReferenced(usages) || usesMethods(usages, Collections.singletonList(AND_THEN))) {
      return Optional.empty();
    }

    ParameterTypeNameAndTreeType firstArgument = new ParameterTypeNameAndTreeType(parametrizedType, 0);
    ParameterTypeNameAndTreeType secondArgument = new ParameterTypeNameAndTreeType(parametrizedType, 1);
    if (secondArgument.paramTypeName != null && !firstArgument.paramType.isPrimitiveWrapper()) {
      return Optional.of(String.format("Obj%sConsumer<%s>", secondArgument.paramTypeName, firstArgument.paramType));
    }
    return Optional.empty();
  }

  private static Optional handleSupplier(Type parametrizedType, List usages) {
    if (isReferenced(usages)) {
      return Optional.empty();
    }
    ParameterTypeNameAndTreeType supplierParamType = new ParameterTypeNameAndTreeType(parametrizedType, 0);
    if (isBoolean(supplierParamType)) {
      return Optional.of("BooleanSupplier");
    }
    return Optional.ofNullable(supplierParamType.paramTypeName).map(s -> s + "Supplier");
  }

  private static class InterfaceTreeAndStringPairReport {
    final String reportString;
    final TypeTree classInterface;

    InterfaceTreeAndStringPairReport(String report, TypeTree interf) {
      reportString = report;
      classInterface = interf;
    }
  }

  private static boolean isBoolean(ParameterTypeNameAndTreeType type) {
    return type.paramType.is("java.lang.Boolean");
  }

  private static class ParameterTypeNameAndTreeType {

    final Type paramType;

    @Nullable
    final String paramTypeName;

    ParameterTypeNameAndTreeType(Type parametrizedType, int typeArgumentIndex) {
      paramType = parametrizedType.typeArguments().get(typeArgumentIndex);
      paramTypeName = returnStringFromJavaObject(paramType);
    }

    @CheckForNull
    private static String returnStringFromJavaObject(Type argType) {
      if (argType.is("java.lang.Integer")) {
        return "Int";
      }
      if (argType.is("java.lang.Double") || argType.is("java.lang.Long")) {
        return argType.name();
      }
      return null;
    }
  }

  private static boolean isReferenced(List usages) {
    return usages.stream()
      .map(Tree::parent)
      .anyMatch(parent -> parent.is(Tree.Kind.ARGUMENTS, Tree.Kind.ASSIGNMENT, Tree.Kind.VARIABLE));
  }

  private static boolean typeEquals(Type type1, Type type2) {
    return !type1.name().startsWith("?") && type1.equals(type2);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy