From 2b762d2bf27e96d85b43a0a82e1e35f00d3c787a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Mon, 2 Mar 2026 11:58:53 +0100 Subject: [PATCH 1/2] Fix FP on S6810 when semantic information is missing --- .../AsyncMethodsReturnTypeCheckSample.java | 34 +++++++++++++++++++ .../spring/AsyncMethodsReturnTypeCheck.java | 4 ++- .../AsyncMethodsReturnTypeCheckTest.java | 9 +++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java new file mode 100644 index 00000000000..a4e5dd8f1f8 --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java @@ -0,0 +1,34 @@ +package checks.spring; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import org.springframework.scheduling.annotation.Async; + +public class AsyncMethodsReturnTypeCheckSample { + + @Async + public CompletableFuture unknownTypeArg() { // Compliant + return CompletableFuture.completedFuture(new Data()); + } + + @Async + public void voidType() { // Compliant + return; + } + + @Async + public CompletableFuture futureSubtype() { // Compliant + return CompletableFuture.completedFuture(42); + } + + @Async + public Data unknownType() { // Compliant, no issue is raised when the return type is unknown (it might be a subtype of Future) + return new Data(); + } + + @Async + public Integer builtinType() { // Noncompliant {{Async methods should return 'void' or a 'Future' type.}} + return 42; + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java index 1dc23272e1c..6f44d9fcdf1 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java @@ -20,6 +20,7 @@ import org.sonar.check.Rule; import org.sonar.java.checks.helpers.SpringUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; @@ -36,9 +37,10 @@ public void visitNode(Tree tree) { var mt = (MethodTree) tree; if (mt.symbol().metadata().isAnnotatedWith(SpringUtils.ASYNC_ANNOTATION)) { var returnType = mt.returnType(); + Type symbolType = returnType.symbolType(); // returnType can only be null if the method is a constructor. Since the @Async annotation is not allowed on constructors, and since // we hence only visit methods, not constructors, we assume that returnType is not null. - if (!returnType.symbolType().isVoid() && !returnType.symbolType().isSubtypeOf("java.util.concurrent.Future")) { + if (!symbolType.isUnknown() && !symbolType.isVoid() && !symbolType.isSubtypeOf("java.util.concurrent.Future")) { reportIssue(returnType, "Async methods should return 'void' or a 'Future' type."); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheckTest.java index 4c412ddcdf4..e3390a735c1 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheckTest.java @@ -20,6 +20,7 @@ import org.sonar.java.checks.verifier.CheckVerifier; import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; class AsyncMethodsReturnTypeCheckTest { @Test @@ -29,4 +30,12 @@ void test() { .withCheck(new AsyncMethodsReturnTypeCheck()) .verifyIssues(); } + + @Test + void test_non_compiling() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/spring/AsyncMethodsReturnTypeCheckSample.java")) + .withCheck(new AsyncMethodsReturnTypeCheck()) + .verifyIssues(); + } } From 32d8e11e64b310ca837c9b0c61f6160662841fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 5 Mar 2026 11:07:20 +0100 Subject: [PATCH 2/2] Apply fixes following review --- .../spring/AsyncMethodsReturnTypeCheckSample.java | 1 + .../checks/spring/AsyncMethodsReturnTypeCheck.java | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java index a4e5dd8f1f8..76e5cef64e8 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/spring/AsyncMethodsReturnTypeCheckSample.java @@ -28,6 +28,7 @@ public Data unknownType() { // Compliant, no issue is raised when the return typ @Async public Integer builtinType() { // Noncompliant {{Async methods should return 'void' or a 'Future' type.}} +// ^^^^^^^ return 42; } diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java index 6f44d9fcdf1..0fca7cca747 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsReturnTypeCheck.java @@ -23,6 +23,7 @@ import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeTree; @Rule(key = "S6810") public class AsyncMethodsReturnTypeCheck extends IssuableSubscriptionVisitor { @@ -36,13 +37,14 @@ public List nodesToVisit() { public void visitNode(Tree tree) { var mt = (MethodTree) tree; if (mt.symbol().metadata().isAnnotatedWith(SpringUtils.ASYNC_ANNOTATION)) { - var returnType = mt.returnType(); - Type symbolType = returnType.symbolType(); + TypeTree returnType = mt.returnType(); // returnType can only be null if the method is a constructor. Since the @Async annotation is not allowed on constructors, and since // we hence only visit methods, not constructors, we assume that returnType is not null. - if (!symbolType.isUnknown() && !symbolType.isVoid() && !symbolType.isSubtypeOf("java.util.concurrent.Future")) { - reportIssue(returnType, "Async methods should return 'void' or a 'Future' type."); + Type symbolType = returnType.symbolType(); + if (symbolType.isUnknown() || symbolType.isVoid() || symbolType.isSubtypeOf("java.util.concurrent.Future")) { + return; } + reportIssue(returnType, "Async methods should return 'void' or a 'Future' type."); } } }