diff --git a/its/ruling/src/test/java/org/sonar/java/it/AutoScanTest.java b/its/ruling/src/test/java/org/sonar/java/it/AutoScanTest.java index 8d692ed46a5..d481e2f0232 100644 --- a/its/ruling/src/test/java/org/sonar/java/it/AutoScanTest.java +++ b/its/ruling/src/test/java/org/sonar/java/it/AutoScanTest.java @@ -180,7 +180,7 @@ public void javaCheckTestSources() throws Exception { softly.assertThat(newTotal).isEqualTo(knownTotal); softly.assertThat(rulesCausingFPs).hasSize(6); softly.assertThat(rulesNotReporting).hasSize(7); - softly.assertThat(rulesSilenced).hasSize(76); + softly.assertThat(rulesSilenced).hasSize(77); /** * 4. Check total number of differences (FPs + FNs) @@ -188,7 +188,7 @@ public void javaCheckTestSources() throws Exception { * No differences would mean that we find the same issues with and without the bytecode and libraries */ String differences = Files.readString(pathFor(TARGET_ACTUAL + PROJECT_KEY + "-no-binaries_differences")); - softly.assertThat(differences).isEqualTo("Issues differences: 3481"); + softly.assertThat(differences).isEqualTo("Issues differences: 3483"); softly.assertAll(); } diff --git a/its/ruling/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/ruling/src/test/resources/autoscan/autoscan-diff-by-rules.json index 9ff2b366610..18c7d593d0b 100644 --- a/its/ruling/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/ruling/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -2909,6 +2909,12 @@ "falseNegatives": 5, "falsePositives": 0 }, + { + "ruleKey": "S6817", + "hasTruePositives": false, + "falseNegatives": 2, + "falsePositives": 0 + }, { "ruleKey": "S6818", "hasTruePositives": false, diff --git a/java-checks-test-sources/src/main/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java b/java-checks-test-sources/src/main/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java new file mode 100644 index 00000000000..c1c9baaac25 --- /dev/null +++ b/java-checks-test-sources/src/main/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java @@ -0,0 +1,32 @@ +package checks.spring; + +import javax.annotation.Nullable; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Async; + +@Configuration +public class AsyncMethodsOnConfigurationClassCheck { + + @Async // Noncompliant [[sc=3;ec=9;quickfixes=qf1]] {{Remove this "@Async" annotation from this method.}} + // fix@qf1 {{Remove "@Async"}} + // edit@qf1 [[sc=3;ec=9]] {{}} + public void asyncMethod() { + } + + public void method() { // Compliant + } + + @Nullable + @Async // Noncompliant + public void someMethod() { + } + +} + +class NotAConfigurationClass { + + @Async // Compliant + public void asyncMethod() { + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/CheckList.java b/java-checks/src/main/java/org/sonar/java/checks/CheckList.java index 7f3f2dd14a7..7a1860e4b26 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CheckList.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CheckList.java @@ -136,12 +136,13 @@ import org.sonar.java.checks.serialization.SerializableObjectInSessionCheck; import org.sonar.java.checks.serialization.SerializableSuperConstructorCheck; import org.sonar.java.checks.spring.AsyncMethodsCalledViaThisCheck; +import org.sonar.java.checks.spring.AsyncMethodsOnConfigurationClassCheck; import org.sonar.java.checks.spring.AsyncMethodsReturnTypeCheck; -import org.sonar.java.checks.spring.AutowiredOnMultipleConstructorsCheck; import org.sonar.java.checks.spring.AutowiredOnConstructorWhenMultipleConstructorsCheck; +import org.sonar.java.checks.spring.AutowiredOnMultipleConstructorsCheck; import org.sonar.java.checks.spring.ControllerWithSessionAttributesCheck; -import org.sonar.java.checks.spring.ModelAttributeNamingConventionForSpELCheck; import org.sonar.java.checks.spring.FieldDependencyInjectionCheck; +import org.sonar.java.checks.spring.ModelAttributeNamingConventionForSpELCheck; import org.sonar.java.checks.spring.OptionalRestParametersShouldBeObjectsCheck; import org.sonar.java.checks.spring.PersistentEntityUsedAsRequestParameterCheck; import org.sonar.java.checks.spring.RequestMappingMethodPublicCheck; @@ -283,6 +284,7 @@ public final class CheckList { AssertionsInProductionCodeCheck.class, AssertsOnParametersOfPublicMethodCheck.class, AsyncMethodsCalledViaThisCheck.class, + AsyncMethodsOnConfigurationClassCheck.class, AsyncMethodsReturnTypeCheck.class, AtLeastOneConstructorCheck.class, AuthorizationsStrongDecisionsCheck.class, @@ -945,8 +947,7 @@ public final class CheckList { UnusedPrivateFieldCheck.class, VerifiedServerHostnamesCheck.class, VolatileNonPrimitiveFieldCheck.class, - WeakSSLContextCheck.class - ); + WeakSSLContextCheck.class); private CheckList() { } diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java new file mode 100644 index 00000000000..5a9ac30d7ee --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheck.java @@ -0,0 +1,64 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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 GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.java.checks.spring; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.QuickFixHelper; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.reporting.JavaTextEdit; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S6817") +public class AsyncMethodsOnConfigurationClassCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + boolean isConfiguration = classTree.modifiers().annotations().stream() + .anyMatch(annotation -> annotation.annotationType().symbolType().is("org.springframework.context.annotation.Configuration")); + + if (isConfiguration) { + classTree.members().stream() + .filter(member -> member.is(Tree.Kind.METHOD)) + .map(MethodTree.class::cast) + .forEach(member -> member.modifiers().annotations().stream() + .filter(annotation -> annotation.annotationType().symbolType().is("org.springframework.scheduling.annotation.Async")) + .findFirst() + .ifPresent(annotation -> QuickFixHelper.newIssue(context) + .forRule(this) + .onTree(annotation) + .withMessage("Remove this \"@Async\" annotation from this method.") + .withQuickFix(() -> JavaQuickFix.newQuickFix("Remove \"@Async\"") + .addTextEdit(JavaTextEdit.removeTree(annotation)) + .build()) + .report())); + } + } + +} diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.html b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.html new file mode 100644 index 00000000000..966769f0283 --- /dev/null +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.html @@ -0,0 +1,51 @@ +

Why is this an issue?

+

@Configuration is a class-level annotation indicating that an object is a source of bean definitions. @Configuration +classes declare beans through @Bean-annotated methods. Calls to @Bean methods on @Configuration classes can +also be used to define inter-bean dependencies. The @Bean annotation indicates that a method instantiates, configures, and initializes a +new object to be managed by the Spring IoC container.

+

Annotating a method of a bean with @Async will make it execute in a separate thread. In other words, the caller will not wait for the +completion of the called method.

+

The @Async annotation is not supported on methods declared within a @Configuration class. This is because +@Async methods are typically used for asynchronous processing, and they require certain infrastructure to be set up, which may not be +available or appropriate in a @Configuration class.

+

How to fix it

+

Don’t use @Async annotations on methods of @Configuration classes.

+

Code examples

+

Noncompliant code example

+
+@EnableAsync
+@Configuration
+public class MyConfiguration {
+
+  @Async // Noncompliant - This is not allowed
+  public void asyncMethod() {
+    // ...
+  }
+}
+
+

Compliant solution

+
+@EnableAsync
+@Configuration
+public class MyConfiguration {
+
+  public void method() {
+    // ...
+  }
+}
+
+

Resources

+

Documentation

+ +

Articles & blog posts

+ + diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.json b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.json new file mode 100644 index 00000000000..925de26d30f --- /dev/null +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/S6817.json @@ -0,0 +1,23 @@ +{ + "title": "Use of the \"@Async\" annotation on methods declared within a \"@Configuration\" class in Spring Boot", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "spring" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-6817", + "sqKey": "S6817", + "scope": "Main", + "quickfix": "covered", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "LOGICAL" + } +} diff --git a/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 36077d9b942..b9e5113aa1f 100644 --- a/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/java-checks/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -486,6 +486,7 @@ "S6810", "S6813", "S6814", + "S6817", "S6818", "S6829", "S6837" diff --git a/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheckTest.java new file mode 100644 index 00000000000..ce8a80255dd --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/spring/AsyncMethodsOnConfigurationClassCheckTest.java @@ -0,0 +1,38 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2023 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 GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.java.checks.spring; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.internal.InternalCheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class AsyncMethodsOnConfigurationClassCheckTest { + + @Test + void test() { + InternalCheckVerifier.newInstance() + .onFile(mainCodeSourcesPath("checks/spring/AsyncMethodsOnConfigurationClassCheck.java")) + .withCheck(new AsyncMethodsOnConfigurationClassCheck()) + .withQuickFixes() + .verifyIssues(); + } + +}