Skip to content

Commit 31a7a38

Browse files
SONARJAVA-4881 With Spring 6, @transactional and @async annotated methods don't need to be public (#5094)
Since Spring 6, @transactional annotated methods are allowed to be protected and package-private. This PR updates the rule S2230 to only apply to projects where Spring appears with a version less than 6. Co-authored-by: Sander Knauff <S.knauff@alfen.com>
1 parent 5b4f8f3 commit 31a7a38

6 files changed

Lines changed: 97 additions & 31 deletions

File tree

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"ruleKey": "S2230",
3-
"hasTruePositives": true,
4-
"falseNegatives": 15,
3+
"hasTruePositives": false,
4+
"falseNegatives": 22,
55
"falsePositives": 0
6-
}
6+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"ruleKey": "S6810",
33
"hasTruePositives": false,
4-
"falseNegatives": 6,
4+
"falseNegatives": 5,
55
"falsePositives": 0
6-
}
6+
}

java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodVisibilityCheckSample.java renamed to java-checks-test-sources/default/src/main/java/checks/spring/TransactionalMethodVisibilityCheckSample_Spring5.java

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,30 @@
44
import org.springframework.scheduling.annotation.Async;
55
import org.springframework.transaction.annotation.Transactional;
66

7-
abstract class TransactionalMethodVisibilityCheckSample {
7+
abstract class TransactionalMethodVisibilityCheckSample_Spring5 {
88

99
public interface C {
1010
@Transactional
1111
int bar(); // Compliant
1212
}
1313

14-
private interface B {
15-
@Transactional
16-
int bar(); // Compliant
17-
}
18-
19-
private interface A {
20-
@Async
21-
int bar(); // Compliant
22-
}
23-
2414
@Async
2515
protected abstract Future<String> aMethod(); // Noncompliant
2616

27-
2817
@Async
29-
public Future<String> asyncMethod(){ // compliant
18+
Future<String> defaultVisibilityAsyncMethod(){ // Noncompliant
3019
return null;
3120
}
3221

3322
@Async
34-
private Future<String> asyncMethodPrivate(){ // Noncompliant {{Make this method "public" or remove the "@Async" annotation.}}
23+
protected Future<String> protectedVisibilityAsyncMethod(){ // Noncompliant
3524
return null;
3625
}
3726

38-
@org.springframework.transaction.annotation.Transactional
39-
public void publicTransactionalMethod() {} // Compliant
40-
4127
@org.springframework.transaction.annotation.Transactional
4228
protected void protectedTransactionalMethod() {} // Noncompliant {{Make this method "public" or remove the "@Transactional" annotation.}}
4329
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44-
30+
4531
@org.springframework.transaction.annotation.Transactional
4632
void defaultVisibilityTransactionalMethod() {} // Noncompliant {{Make this method "public" or remove the "@Transactional" annotation.}}
4733
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package checks.spring;
2+
3+
import java.util.concurrent.Future;
4+
5+
abstract class TransactionalMethodVisibilityCheckSample_Spring6 {
6+
7+
public interface C {
8+
@org.springframework.transaction.annotation.Transactional
9+
int bar(); // Compliant
10+
}
11+
12+
@org.springframework.scheduling.annotation.Async
13+
protected Future<String> protectedAsyncMethod(){ // compliant
14+
return null;
15+
}
16+
17+
@org.springframework.scheduling.annotation.Async
18+
private Future<String> privateAsyncMethod(){ // Noncompliant {{Make this method non-"private" or remove the "@Async" annotation.}}
19+
return null;
20+
}
21+
22+
@org.springframework.transaction.annotation.Transactional
23+
protected void protectedTransactionalMethod() {} // Compliant
24+
25+
26+
@org.springframework.transaction.annotation.Transactional
27+
private Future<String> privateTransactionalMethod(){ // Noncompliant {{Make this method non-"private" or remove the "@Transactional" annotation.}}
28+
return null;
29+
}
30+
}

java-checks/src/main/java/org/sonar/java/checks/spring/TransactionalMethodVisibilityCheck.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,29 @@
1919
import java.util.Collections;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.Optional;
23+
import java.util.function.Function;
2224
import org.sonar.check.Rule;
25+
import org.sonar.plugins.java.api.DependencyVersionAware;
2326
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
27+
import org.sonar.plugins.java.api.Version;
2428
import org.sonar.plugins.java.api.tree.AnnotationTree;
2529
import org.sonar.plugins.java.api.tree.MethodTree;
2630
import org.sonar.plugins.java.api.tree.Tree;
2731

2832
@Rule(key = "S2230")
29-
public class TransactionalMethodVisibilityCheck extends IssuableSubscriptionVisitor {
33+
public class TransactionalMethodVisibilityCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware {
3034

31-
private static final List<String> proxyAnnotations = List.of(
35+
private static final List<String> PROXY_ANNOTATIONS = List.of(
3236
"org.springframework.transaction.annotation.Transactional",
3337
"org.springframework.scheduling.annotation.Async");
3438

35-
private static final Map<String, String> annShortName = Map.of(
39+
private static final Map<String, String> ANNOTATION_SHORT_NAMES = Map.of(
3640
"org.springframework.transaction.annotation.Transactional", "@Transactional",
3741
"org.springframework.scheduling.annotation.Async", "@Async");
3842

43+
private boolean isSpring6OrLater = false;
44+
3945
@Override
4046
public List<Tree.Kind> nodesToVisit() {
4147
return Collections.singletonList(Tree.Kind.METHOD);
@@ -44,15 +50,20 @@ public List<Tree.Kind> nodesToVisit() {
4450
@Override
4551
public void visitNode(Tree tree) {
4652
MethodTree method = (MethodTree) tree;
47-
if (!method.symbol().isPublic()) {
48-
proxyAnnotations.stream()
53+
boolean handledBySpring = isSpring6OrLater ? !method.symbol().isPrivate() : method.symbol().isPublic();
54+
if (!handledBySpring) {
55+
PROXY_ANNOTATIONS.stream()
4956
.filter(annSymbol -> hasAnnotation(method, annSymbol))
5057
.forEach(annSymbol -> reportIssue(
5158
method.simpleName(),
52-
"Make this method \"public\" or remove the \"" + annShortName.get(annSymbol) + "\" annotation."));
59+
"Make this method " + requiredVisibilityMessage() + " or remove the \"" + ANNOTATION_SHORT_NAMES.get(annSymbol) + "\" annotation."));
5360
}
5461
}
5562

63+
private String requiredVisibilityMessage() {
64+
return isSpring6OrLater ? "non-\"private\"" : "\"public\"";
65+
}
66+
5667
private static boolean hasAnnotation(MethodTree method, String annotationSymbol) {
5768
for (AnnotationTree annotation : method.modifiers().annotations()) {
5869
if (annotation.symbolType().is(annotationSymbol)) {
@@ -62,4 +73,18 @@ private static boolean hasAnnotation(MethodTree method, String annotationSymbol)
6273
return false;
6374
}
6475

76+
/** Check that Spring transaction artifact is present, and record whether its version is before or after 6.0. */
77+
@Override
78+
public boolean isCompatibleWithDependencies(Function<String, Optional<Version>> dependencyFinder) {
79+
Optional<Version> springContextVersion = dependencyFinder.apply("spring-context");
80+
Optional<Version> springTxVersion = dependencyFinder.apply("spring-tx");
81+
if (springTxVersion.isEmpty() && springContextVersion.isEmpty()) {
82+
return false;
83+
}
84+
isSpring6OrLater = springContextVersion
85+
.or(() -> springTxVersion)
86+
.map(v -> v.isGreaterThanOrEqualTo("6.0"))
87+
.orElse(false);
88+
return true;
89+
}
6590
}

java-checks/src/test/java/org/sonar/java/checks/spring/TransactionalMethodVisibilityCheckTest.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.sonar.java.checks.spring;
1818

19+
import java.io.File;
20+
import java.util.Collections;
21+
import java.util.List;
1922
import org.junit.jupiter.api.Test;
2023
import org.sonar.java.checks.verifier.CheckVerifier;
2124

@@ -25,13 +28,35 @@
2528
class TransactionalMethodVisibilityCheckTest {
2629

2730
@Test
28-
void test() {
31+
void test_Spring5() {
2932
CheckVerifier.newVerifier()
30-
.onFile(mainCodeSourcesPath("checks/spring/TransactionalMethodVisibilityCheckSample.java"))
33+
.onFile(mainCodeSourcesPath("checks/spring/TransactionalMethodVisibilityCheckSample_Spring5.java"))
3134
.withCheck(new TransactionalMethodVisibilityCheck())
3235
.verifyIssues();
3336
}
3437

38+
@Test
39+
void test_spring6() {
40+
CheckVerifier.newVerifier()
41+
.onFile(mainCodeSourcesPath("checks/spring/TransactionalMethodVisibilityCheckSample_Spring6.java"))
42+
.withCheck(new TransactionalMethodVisibilityCheck())
43+
.withClassPath(List.of(new File("spring-tx-6.0.1.jar"),
44+
new File("spring-context-6.0.1.jar")))
45+
.verifyIssues();
46+
}
47+
48+
/** Check that when dependencies are not resolved, we do not raise issues. */
49+
@Test
50+
void test_noDependencies() {
51+
CheckVerifier.newVerifier()
52+
.onFiles(
53+
mainCodeSourcesPath("checks/spring/TransactionalMethodVisibilityCheckSample_Spring5.java"),
54+
mainCodeSourcesPath("checks/spring/TransactionalMethodVisibilityCheckSample_Spring6.java"))
55+
.withCheck(new TransactionalMethodVisibilityCheck())
56+
.withClassPath(Collections.emptyList())
57+
.verifyNoIssues();
58+
}
59+
3560
@Test
3661
void test_non_compiling() {
3762
CheckVerifier.newVerifier()

0 commit comments

Comments
 (0)