
org.sonar.java.checks.spring.SpringIncompatibleTransactionalCheck 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.spring;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.semantic.SymbolMetadata.AnnotationValue;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ClassTree;
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.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
@Rule(key = "S2229")
public class SpringIncompatibleTransactionalCheck extends IssuableSubscriptionVisitor {
private static final String SPRING_TRANSACTIONAL_ANNOTATION = "org.springframework.transaction.annotation.Transactional";
private static final String JAVAX_TRANSACTIONAL_ANNOTATION = "javax.transaction.Transactional";
private static final String MANDATORY = "MANDATORY";
private static final String NESTED = "NESTED";
private static final String NEVER = "NEVER";
private static final String NOT_SUPPORTED = "NOT_SUPPORTED";
private static final String REQUIRED = "REQUIRED";
private static final String REQUIRES_NEW = "REQUIRES_NEW";
private static final String SUPPORTS = "SUPPORTS";
// Made name to represent no annotation
private static final String NOT_TRANSACTIONAL = "SONAR_NOT_TRANSACTIONAL";
private static final Map> INCOMPATIBLE_PROPAGATION_MAP = buildIncompatiblePropagationMap();
private static Map> buildIncompatiblePropagationMap() {
Map> map = new HashMap<>();
map.put(NOT_TRANSACTIONAL, new HashSet<>(Arrays.asList(MANDATORY, NESTED, REQUIRED, REQUIRES_NEW)));
map.put(MANDATORY, new HashSet<>(Arrays.asList(NESTED, NEVER, NOT_SUPPORTED, REQUIRES_NEW)));
map.put(NESTED, new HashSet<>(Arrays.asList(NESTED, NEVER, NOT_SUPPORTED, REQUIRES_NEW)));
map.put(NEVER, new HashSet<>(Arrays.asList(MANDATORY, NESTED, REQUIRED, REQUIRES_NEW)));
map.put(NOT_SUPPORTED, new HashSet<>(Arrays.asList(MANDATORY, NESTED, REQUIRED, REQUIRES_NEW)));
map.put(REQUIRED, new HashSet<>(Arrays.asList(NESTED, NEVER, NOT_SUPPORTED, REQUIRES_NEW)));
map.put(REQUIRES_NEW, new HashSet<>(Arrays.asList(NESTED, NEVER, NOT_SUPPORTED, REQUIRES_NEW)));
map.put(SUPPORTS, new HashSet<>(Arrays.asList(MANDATORY, NESTED, NEVER, NOT_SUPPORTED, REQUIRED, REQUIRES_NEW)));
return map;
}
@Override
public List nodesToVisit() {
return Collections.singletonList(Tree.Kind.CLASS);
}
@Override
public void visitNode(Tree tree) {
ClassTree classTree = (ClassTree) tree;
Map methodsPropagationMap = collectMethodsPropagation(classTree);
if (hasSameValues(methodsPropagationMap.values())) {
return;
}
methodsPropagationMap
.forEach((symbol, propagation) -> checkMethodInvocations((MethodTree) symbol.declaration(), propagation, methodsPropagationMap));
}
private void checkMethodInvocations(MethodTree method, @Nullable String callerPropagation, Map methodsPropagationMap) {
BlockTree methodBody = method.block();
if (methodBody == null) {
return;
}
methodBody.accept(new BaseTreeVisitor() {
@Override
public void visitMethodInvocation(MethodInvocationTree methodInvocation) {
super.visitMethodInvocation(methodInvocation);
Symbol calleeMethodSymbol = methodInvocation.methodSymbol();
if (calleeMethodSymbol.isUnknown()) {
return;
}
if (methodsPropagationMap.containsKey(calleeMethodSymbol) && methodInvocationOnThisInstance(methodInvocation)) {
String calleePropagation = methodsPropagationMap.get(calleeMethodSymbol);
checkIncompatiblePropagation(methodInvocation, callerPropagation, calleeMethodSymbol, calleePropagation);
}
}
});
}
private static boolean methodInvocationOnThisInstance(MethodInvocationTree methodInvocation) {
if (methodInvocation.methodSymbol().isStatic()) {
return false;
}
ExpressionTree expression = methodInvocation.methodSelect();
if (expression.is(Tree.Kind.MEMBER_SELECT)) {
return ExpressionUtils.isThis(((MemberSelectExpressionTree) expression).expression());
}
return expression.is(Tree.Kind.IDENTIFIER);
}
private void checkIncompatiblePropagation(MethodInvocationTree methodInvocation, @Nullable String callerPropagation, Symbol calleeMethodSymbol, String calleePropagation) {
Set incompatiblePropagation = INCOMPATIBLE_PROPAGATION_MAP.getOrDefault(callerPropagation, Collections.emptySet());
if (incompatiblePropagation.contains(calleePropagation)) {
String message = "\"" + calleeMethodSymbol.name() + "'s\" @Transactional requirement is incompatible with the one for this method.";
List secondaryLocations = Collections.singletonList(
new JavaFileScannerContext.Location("Incompatible method definition.", ((MethodTree) calleeMethodSymbol.declaration()).simpleName()));
reportIssue(ExpressionUtils.methodName(methodInvocation), message, secondaryLocations, null);
}
}
private static Map collectMethodsPropagation(ClassTree classTree) {
Map methodPropagationMap = new HashMap<>();
// When the propagation of the class itself is unknown (incomplete semantic), we do nothing to avoid FP.
getPropagationIfKnown(classTree.symbol(), NOT_TRANSACTIONAL).ifPresent(classPropagation -> {
for (Tree member : classTree.members()) {
if (member.is(Tree.Kind.METHOD)) {
MethodTree method = (MethodTree) member;
if (method.symbol().isPublic()) {
getPropagationIfKnown(method.symbol(), classPropagation).ifPresent(propagation ->
methodPropagationMap.put(method.symbol(), propagation)
);
}
}
}
});
return methodPropagationMap;
}
private static boolean hasSameValues(Collection methodsPropagationList) {
return methodsPropagationList.stream().distinct().count() <= 1;
}
/**
* Returns Optional.Empty if the Propagation can not be reliably known: if something has unknown type in the process.
*/
private static Optional getPropagationIfKnown(Symbol symbol, String inheritedPropagation) {
String defaultValue = NOT_TRANSACTIONAL.equals(inheritedPropagation) ? REQUIRED : inheritedPropagation;
Optional propagation = Optional.of(inheritedPropagation);
for (SymbolMetadata.AnnotationInstance annotationInstance : symbol.metadata().annotations()) {
Symbol annotationSymbol = annotationInstance.symbol();
Type annotationType = annotationSymbol.type();
if (annotationSymbol.isUnknown()) {
return Optional.empty();
} else if (annotationType.is(SPRING_TRANSACTIONAL_ANNOTATION)) {
propagation = getAnnotationAttributeAsString(annotationInstance.values(), "propagation", defaultValue);
} else if (annotationType.is(JAVAX_TRANSACTIONAL_ANNOTATION)) {
propagation = getAnnotationAttributeAsString(annotationInstance.values(), "value", defaultValue);
}
}
return propagation;
}
private static Optional getAnnotationAttributeAsString(List values, String attributeName, String defaultValue) {
for (AnnotationValue annotationValue : values) {
if (attributeName.equals(annotationValue.name())) {
Object value = annotationValue.value();
if (value instanceof Symbol.VariableSymbol variableSymbol) {
// expected values are constant from a Enum, translated into variable symbol
return Optional.of(variableSymbol.name());
} else {
return Optional.empty();
}
}
}
return Optional.of(defaultValue);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy