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

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

There is a newer version: 8.6.0.37351
Show newest version
/*
 * 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