
org.sonar.java.checks.CollectionInappropriateCallsCheck 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.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.model.Symbols;
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.Type;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.Tree.Kind;
import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;
@Rule(key = "S2175")
public class CollectionInappropriateCallsCheck extends IssuableSubscriptionVisitor {
private static final String JAVA_UTIL_COLLECTION = "java.util.Collection";
private static final List TYPE_CHECKERS = new TypeCheckerListBuilder()
.on(JAVA_UTIL_COLLECTION)
.method("remove").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.method("removeAll").argument(1).outOf(1).shouldMatchCollectionOfParametrizedType(1).add()
.method("contains").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.on("java.util.List")
.method("indexOf").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.method("lastIndexOf").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.on("java.util.Map")
.method("containsKey").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.method("containsValue").argument(1).outOf(1).shouldMatchParametrizedType(2).add()
.method("get").argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.method("getOrDefault").argument(1).outOf(2).shouldMatchParametrizedType(1).add()
.method("remove")
.argument(1).outOf(1).shouldMatchParametrizedType(1).add()
.argument(1).outOf(2).shouldMatchParametrizedType(1).add()
.argument(2).outOf(2).shouldMatchParametrizedType(2).add()
.build();
@Override
public List nodesToVisit() {
return Collections.singletonList(Kind.METHOD_INVOCATION);
}
@Override
public void visitNode(Tree tree) {
MethodInvocationTree mit = (MethodInvocationTree) tree;
TYPE_CHECKERS.stream()
.filter(typeChecker -> typeChecker.methodMatcher.matches(mit))
.forEach(typeChecker -> checkMethodInvocation(mit, typeChecker));
}
private void checkMethodInvocation(MethodInvocationTree tree, TypeChecker typeChecker) {
ExpressionTree argument = tree.arguments().get(typeChecker.argumentIndex);
Type argumentTypeToCheck = argument.symbolType();
if (typeChecker.argumentIsACollection) {
argumentTypeToCheck = getTypeArgumentAt(findSuperTypeMatching(argumentTypeToCheck, JAVA_UTIL_COLLECTION), 0);
}
if (argumentTypeToCheck.isUnknown()) {
// could happen with type inference.
return;
}
Type actualMethodType = getMethodOwnerType(tree);
Type checkedMethodType = findSuperTypeMatching(actualMethodType, typeChecker.methodOwnerType);
Type parameterType = getTypeArgumentAt(checkedMethodType, typeChecker.parametrizedTypeIndex);
boolean isCallToParametrizedOrUnknownMethod = isCallToParametrizedOrUnknownMethod(argument);
if (!isCallToParametrizedOrUnknownMethod && tree.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
isCallToParametrizedOrUnknownMethod = isCallToParametrizedOrUnknownMethod(((MemberSelectExpressionTree) tree.methodSelect()).expression());
}
if (!checkedMethodType.isUnknown()
&& !parameterType.isUnknown()
&& !isCallToParametrizedOrUnknownMethod
&& !isArgumentCompatible(argumentTypeToCheck, parameterType)) {
reportIssue(ExpressionUtils.methodName(tree), message(actualMethodType, checkedMethodType, parameterType, argumentTypeToCheck));
}
}
private static String message(Type actualMethodType, Type checkedMethodType, Type parameterType, Type argumentType) {
String actualType = typeNameWithParameters(actualMethodType);
boolean actualTypeHasTheParameterType = actualMethodType.typeArguments().stream().anyMatch(typeArg -> typeArg.equals(parameterType));
boolean checkedTypeHasSeveralParameters = checkedMethodType.typeArguments().size() > 1;
String typeDescription = checkedTypeHasSeveralParameters ? (" in a \"" + parameterType + "\" type") : "";
if (actualTypeHasTheParameterType) {
return MessageFormat.format("A \"{0}\" cannot contain a \"{1}\"{2}.", actualType, argumentType.name(), typeDescription);
}
String checkedType = typeNameWithParameters(checkedMethodType);
return MessageFormat.format("\"{0}\" is a \"{1}\" which cannot contain a \"{2}\"{3}.",
actualType, checkedType, argumentType.name(), typeDescription);
}
private static String typeNameWithParameters(Type type) {
if (type.isParameterized()) {
return type.name() + type.typeArguments().stream()
.map(Type::name)
.collect(Collectors.joining(", ", "<", ">"));
}
return type.name();
}
private static boolean isCallToParametrizedOrUnknownMethod(ExpressionTree expressionTree) {
if (expressionTree.is(Tree.Kind.METHOD_INVOCATION)) {
Symbol.MethodSymbol symbol = ((MethodInvocationTree) expressionTree).methodSymbol();
return symbol.isUnknown() || symbol.isParametrizedMethod();
}
return false;
}
private static Type getMethodOwnerType(MethodInvocationTree mit) {
if (mit.methodSelect().is(Kind.MEMBER_SELECT)) {
return ((MemberSelectExpressionTree) mit.methodSelect()).expression().symbolType();
}
return mit.methodSymbol().owner().type();
}
private static Type getTypeArgumentAt(Type type, int index) {
if (type.isParameterized()) {
List parameters = type.typeArguments();
if (index < parameters.size()) {
return parameters.get(index);
}
}
return Symbols.unknownType;
}
private static Type findSuperTypeMatching(Type type, String genericTypeName) {
if (type.is(genericTypeName)) {
return type;
}
return type.symbol().superTypes()
.stream()
.filter(superType -> superType.is(genericTypeName))
.findFirst()
.orElse(Symbols.unknownType);
}
private static boolean isArgumentCompatible(Type argumentType, Type collectionParameterType) {
return isSubtypeOf(argumentType, collectionParameterType)
|| isSubtypeOf(collectionParameterType, argumentType)
|| autoboxing(argumentType, collectionParameterType);
}
private static boolean isSubtypeOf(Type type, Type superType) {
return type.isSubtypeOf(superType.erasure());
}
private static boolean autoboxing(Type argumentType, Type collectionParameterType) {
return argumentType.isPrimitive()
&& isSubtypeOf(argumentType.primitiveWrapperType(), collectionParameterType);
}
private static class TypeChecker {
private final String methodOwnerType;
private final MethodMatchers methodMatcher;
private final int argumentIndex;
private boolean argumentIsACollection;
private final int parametrizedTypeIndex;
private TypeChecker(String methodOwnerType, MethodMatchers methodMatcher, int argumentIndex, boolean argumentIsACollection, int parametrizedTypeIndex) {
this.methodOwnerType = methodOwnerType;
this.methodMatcher = methodMatcher;
this.argumentIndex = argumentIndex;
this.argumentIsACollection = argumentIsACollection;
this.parametrizedTypeIndex = parametrizedTypeIndex;
}
}
private static class TypeCheckerListBuilder {
private final List typeCheckers = new ArrayList<>();
private String methodOwnerType;
private String methodName;
private int argumentPosition;
private boolean argumentIsACollection;
private int argumentCount;
private int parametrizedTypePosition;
private TypeCheckerListBuilder on(String methodOwnerType) {
this.methodOwnerType = methodOwnerType;
return this;
}
private TypeCheckerListBuilder method(String methodName) {
this.methodName = methodName;
return this;
}
private TypeCheckerListBuilder argument(int argumentPosition) {
this.argumentPosition = argumentPosition;
return this;
}
private TypeCheckerListBuilder outOf(int argumentCount) {
this.argumentCount = argumentCount;
return this;
}
private TypeCheckerListBuilder shouldMatchParametrizedType(int parametrizedTypePosition) {
this.parametrizedTypePosition = parametrizedTypePosition;
this.argumentIsACollection = false;
return this;
}
private TypeCheckerListBuilder shouldMatchCollectionOfParametrizedType(int parametrizedTypePosition) {
this.parametrizedTypePosition = parametrizedTypePosition;
this.argumentIsACollection = true;
return this;
}
private TypeCheckerListBuilder add() {
int argumentIndex = argumentPosition - 1;
int parametrizedTypeIndex = parametrizedTypePosition - 1;
List methodMatcherParameters = new ArrayList<>();
for (int i = 0; i < argumentCount; i++) {
String parameterType = ANY;
if (i == argumentIndex) {
parameterType = argumentIsACollection ? JAVA_UTIL_COLLECTION : "java.lang.Object";
}
methodMatcherParameters.add(parameterType);
}
MethodMatchers methodMatcher = MethodMatchers.create()
.ofSubTypes(methodOwnerType)
.names(methodName)
.addParametersMatcher(methodMatcherParameters.toArray(new String[0]))
.build();
typeCheckers.add(new TypeChecker(methodOwnerType, methodMatcher, argumentIndex, argumentIsACollection, parametrizedTypeIndex));
return this;
}
private List build() {
return typeCheckers;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy