From 9124f75fdeebda8f96307c4e5f8930df905713eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Mon, 20 Jul 2026 14:17:38 +0200
Subject: [PATCH 01/12] generate rule metadata
---
.../org/sonar/l10n/java/rules/java/S9068.html | 217 ++++++++++++++++++
.../org/sonar/l10n/java/rules/java/S9068.json | 25 ++
.../main/resources/profiles/Sonar_way/S9068 | 0
3 files changed, 242 insertions(+)
create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json
create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9068
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
new file mode 100644
index 00000000000..2ec6c31a198
--- /dev/null
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
@@ -0,0 +1,217 @@
+This rule raises an issue when a CDI bean uses the @Singleton annotation from jakarta.inject instead of
+@ApplicationScoped in a Quarkus application.
+Why is this an issue?
+In Quarkus applications, both @Singleton (from Jakarta Dependency Injection) and @ApplicationScoped (from Jakarta
+Contexts and Dependency Injection) create single-instance beans. However, they behave differently in important ways.
+@ApplicationScoped beans are full CDI beans with these capabilities:
+
+ - Support for CDI interceptors (like
@Transactional, @CacheResult, custom interceptors)
+ - Support for CDI decorators
+ - Can be mocked in tests using QuarkusMock
+ - Participate in CDI events and observers
+ - Support lazy initialization via client proxies
+
+@Singleton beans are simpler but more limited:
+
+ - Created eagerly at startup
+ - Do not support interception - methods annotated with interceptor annotations will be ignored
+ - Cannot be mocked using QuarkusMock in tests
+ - Direct references without proxy support
+
+For most application-level services, @ApplicationScoped is the better choice because it provides the full CDI feature set that Quarkus
+applications typically need.
+When @Singleton might be acceptable
+There are specific scenarios where @Singleton may be intentionally chosen:
+
+ - Low-level infrastructure beans: Beans that provide basic utilities without needing interceptors, decorators, or mocking (e.g.,
+ configuration holders, simple factories)
+ - Performance-critical paths: Beans where the proxy overhead of
@ApplicationScoped is measured and found to be
+ significant (rare, and should be documented)
+ - Third-party integration: When integrating libraries that specifically expect non-proxied instances
+ - Startup beans: Beans that must be instantiated eagerly at startup and have no CDI feature requirements
+
+If you choose to use @Singleton, document why you need it and confirm that:
+
+ - The bean does not and will not use CDI interceptors
+ - The bean does not need to be mocked in tests, or you use alternative mocking strategies
+ - You understand the limitations and accept them for this specific case
+
+Exceptions
+This rule may be safely ignored when:
+
+ - The bean is a low-level infrastructure component with no interceptor requirements
+ - The bean’s design explicitly prohibits the use of CDI proxies (e.g., final classes used intentionally)
+ - You have documented performance reasons for avoiding the proxy overhead
+
+However, these cases are rare. Most application services should use @ApplicationScoped.
+Distinguishing false positives from intentional usage
+To determine if a flagged @Singleton is a false positive or intentional:
+It’s likely a real issue if:
+
+ - The bean uses any CDI interceptor annotations (
@Transactional, @CacheResult, @Retry, etc.)
+ - The bean needs to be mocked in tests
+ - The bean is an application-level service (controllers, services, repositories)
+ - There’s no documented reason for using
@Singleton
+
+It might be intentional if:
+
+ - The bean is clearly infrastructure-level (e.g., named
ConfigurationHolder, SimpleFactory)
+ - There’s a comment explaining why
@Singleton is used
+ - The bean is extremely simple with no methods that would benefit from interception
+ - The bean is part of a documented design pattern that requires non-proxied instances
+
+When in doubt, prefer @ApplicationScoped unless you have a specific, documented reason to use @Singleton.
+What is the potential impact?
+Using @Singleton instead of @ApplicationScoped can lead to several issues:
+Silent interceptor failures
+Interceptors like @Transactional, @CacheResult, or custom interceptors will be silently ignored on
+@Singleton beans. This can cause:
+
+ - Database transactions not being created, leading to data inconsistency
+ - Cache annotations being ignored, resulting in performance degradation
+ - Security checks being bypassed if implemented via interceptors
+
+These failures are particularly dangerous because the code compiles and runs without errors - the interceptors simply don’t work.
+Testing difficulties
+@Singleton beans cannot be mocked using QuarkusMock in tests. This means:
+
+ - Integration tests cannot isolate the bean for testing
+ - External dependencies (databases, APIs) cannot be easily stubbed
+ - Tests may become slower and more fragile
+ - Test coverage may be reduced
+
+Reduced flexibility
+Choosing @Singleton limits your future options:
+
+ - Adding interceptors later requires changing the scope annotation
+ - Adopting CDI decorators becomes impossible
+ - Lazy initialization via proxies is unavailable
+
+How to fix it in Quarkus
+Replace the @Singleton annotation with @ApplicationScoped. Update the import statement to use
+jakarta.enterprise.context.ApplicationScoped instead of jakarta.inject.Singleton. This is the recommended approach for most
+application beans.
+Code examples
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant
+public class UserService {
+ public String getUser(Long id) {
+ // implementation
+ return "user";
+ }
+}
+
+Compliant solution
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class UserService {
+ public String getUser(Long id) {
+ // implementation
+ return "user";
+ }
+}
+
+When using interceptors like @Transactional, @ApplicationScoped is required for the interceptor to function.
+@Singleton beans are not intercepted, so the transaction will not work.
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+import jakarta.transaction.Transactional;
+
+@Singleton // Noncompliant
+public class OrderService {
+ @Transactional
+ public void createOrder(Order order) {
+ // This transaction will NOT work as expected
+ // because @Singleton doesn't support interception
+ }
+}
+
+Compliant solution
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.transaction.Transactional;
+
+@ApplicationScoped
+public class OrderService {
+ @Transactional
+ public void createOrder(Order order) {
+ // Transaction works correctly
+ }
+}
+
+In test scenarios, @ApplicationScoped beans can be mocked using QuarkusMock, while @Singleton beans cannot. This makes
+testing easier and more reliable.
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant
+public class EmailService {
+ public void sendEmail(String to, String subject) {
+ // Actual email sending logic
+ // Cannot be easily mocked in tests
+ }
+}
+
+Compliant solution
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class EmailService {
+ public void sendEmail(String to, String subject) {
+ // Actual email sending logic
+ // Can be mocked in tests with QuarkusMock
+ }
+}
+
+// In test:
+// QuarkusMock.installMockForType(mockEmailService, EmailService.class);
+
+If you have a documented, valid reason to use @Singleton (such as a low-level infrastructure bean with no interceptor needs), you can
+keep it but should add a comment explaining why. This is an alternative to changing the annotation, though it’s only appropriate in specific
+cases.
+Noncompliant code example
+
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant
+public class ApplicationConfig {
+ private final String version = "1.0.0";
+
+ public String getVersion() {
+ return version;
+ }
+}
+
+Compliant solution
+
+import jakarta.inject.Singleton;
+
+// Using @Singleton intentionally: this is a simple configuration holder
+// with no interceptor requirements and no need for testing isolation.
+// It's instantiated eagerly at startup to validate configuration.
+@Singleton
+public class ApplicationConfig {
+ private final String version = "1.0.0";
+
+ public String getVersion() {
+ return version;
+ }
+}
+
+Resources
+Documentation
+
+
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json
new file mode 100644
index 00000000000..5e9dfbe8d80
--- /dev/null
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.json
@@ -0,0 +1,25 @@
+{
+ "title": "\"@ApplicationScoped\" should be preferred over \"@Singleton\" in Quarkus applications",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5 min"
+ },
+ "tags": [
+ "jee",
+ "injection",
+ "testing"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-9068",
+ "sqKey": "S9068",
+ "scope": "All",
+ "quickfix": "unknown",
+ "code": {
+ "impacts": {
+ "MAINTAINABILITY": "MEDIUM"
+ },
+ "attribute": "MODULAR"
+ }
+}
diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9068 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9068
new file mode 100644
index 00000000000..e69de29bb2d
From ad4a2f3d71b46d32fd70b14e404f706bba0cfd60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Mon, 20 Jul 2026 14:56:39 +0200
Subject: [PATCH 02/12] implement check
---
...InsteadOfApplicationScopedCheckSample.java | 66 +++++++++++++++++++
...gletonInsteadOfApplicationScopedCheck.java | 55 ++++++++++++++++
...onInsteadOfApplicationScopedCheckTest.java | 42 ++++++++++++
3 files changed, 163 insertions(+)
create mode 100644 java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
create mode 100644 java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
create mode 100644 java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
new file mode 100644
index 00000000000..933052ddd68
--- /dev/null
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -0,0 +1,66 @@
+package checks.quarkus;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Singleton;
+
+@Singleton // Noncompliant {{Replace "@Singleton" by "@ApplicationScoped" or add a comment indicating why "@Singleton" is necessary.}} [[sc=1;ec=11]]
+class NoncompliantService {
+ public String foo() {
+ return "foo";
+ }
+}
+
+class NoncompliantProducerClass {
+ @Singleton // Noncompliant {{Replace "@Singleton" by "@ApplicationScoped" or add a comment indicating why "@Singleton" is necessary.}}
+//^^^^^^^^^^
+ public NoncompliantService produceService() {
+ return new NoncompliantService();
+ }
+}
+
+// Using @Singleton intentionally: low-level config holder, singleton scope required for eager init.
+@Singleton
+class CompliantWithComment {
+ private static final String VERSION = "1.0";
+
+ public String getVersion() {
+ return VERSION;
+ }
+}
+
+class CompliantProducerWithComment {
+ // Singleton scope required: external library expects a non-proxied instance.
+ @Singleton
+ public CompliantWithComment produceConfig() {
+ return new CompliantWithComment();
+ }
+}
+
+/**
+ * Using @Singleton intentionally: this is a low-level infrastructure bean with no interceptor requirements.
+ * Singleton scope required to avoid proxy overhead measured on this critical path.
+ */
+@Singleton
+class CompliantWithJavadocComment {
+ public String getConfig() {
+ return "config";
+ }
+}
+
+@ApplicationScoped
+class CompliantApplicationScoped {
+ public String hello() {
+ return "hello";
+ }
+}
+
+@Dependent
+class CompliantDependent {
+}
+
+class CompliantNoScope {
+ public String hello() {
+ return "hello";
+ }
+}
diff --git a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
new file mode 100644
index 00000000000..2c0c3faf09b
--- /dev/null
+++ b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
@@ -0,0 +1,55 @@
+/*
+ * SonarQube Java
+ * Copyright (C) SonarSource Sàrl
+ * mailto:info AT sonarsource DOT com
+ *
+ * You can redistribute and/or modify this program under the terms of
+ * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
+ *
+ * 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.quarkus;
+
+import java.util.List;
+import org.sonar.check.Rule;
+import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
+import org.sonar.plugins.java.api.tree.AnnotationTree;
+import org.sonar.plugins.java.api.tree.ClassTree;
+import org.sonar.plugins.java.api.tree.MethodTree;
+import org.sonar.plugins.java.api.tree.ModifiersTree;
+import org.sonar.plugins.java.api.tree.Tree;
+
+@Rule(key = "S9068")
+public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscriptionVisitor {
+
+ private static final String JAKARTA_SINGLETON = "jakarta.inject.Singleton";
+ private static final String MESSAGE = "Replace \"@Singleton\" by \"@ApplicationScoped\" or add a comment indicating why \"@Singleton\" is necessary.";
+
+ @Override
+ public List nodesToVisit() {
+ return List.of(Tree.Kind.CLASS, Tree.Kind.METHOD);
+ }
+
+ @Override
+ public void visitNode(Tree tree) {
+ ModifiersTree modifiers = tree instanceof ClassTree classTree
+ ? classTree.modifiers()
+ : ((MethodTree) tree).modifiers();
+
+ modifiers.annotations().stream()
+ .filter(annotation -> annotation.annotationType().symbolType().is(JAKARTA_SINGLETON))
+ .filter(annotation -> !hasJustifyingComment(annotation))
+ .forEach(annotation -> reportIssue(annotation, MESSAGE));
+ }
+
+ private static boolean hasJustifyingComment(AnnotationTree annotation) {
+ return annotation.firstToken().trivias().stream()
+ .anyMatch(trivia -> trivia.comment().toLowerCase(java.util.Locale.ROOT).contains("singleton"));
+ }
+}
diff --git a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
new file mode 100644
index 00000000000..1d2d2ddcd56
--- /dev/null
+++ b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube Java
+ * Copyright (C) SonarSource Sàrl
+ * mailto:info AT sonarsource DOT com
+ *
+ * You can redistribute and/or modify this program under the terms of
+ * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
+ *
+ * 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.quarkus;
+
+import org.junit.jupiter.api.Test;
+import org.sonar.java.checks.verifier.CheckVerifier;
+
+import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
+
+class SingletonInsteadOfApplicationScopedCheckTest {
+
+ @Test
+ void test() {
+ CheckVerifier.newVerifier()
+ .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java"))
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .verifyIssues();
+ }
+
+ @Test
+ void testWithoutSemantic() {
+ CheckVerifier.newVerifier()
+ .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java"))
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withoutSemantic()
+ .verifyNoIssues();
+ }
+}
From 0441475fe6d8753b344d81fdb67f829ca456a8f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Mon, 20 Jul 2026 15:12:30 +0200
Subject: [PATCH 03/12] fix duplicate class names
---
.../SingletonInsteadOfApplicationScopedCheckSample.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
index 933052ddd68..6276de29c73 100644
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -49,14 +49,14 @@ public String getConfig() {
}
@ApplicationScoped
-class CompliantApplicationScoped {
+class SingletonCheckCompliantApplicationScoped {
public String hello() {
return "hello";
}
}
@Dependent
-class CompliantDependent {
+class SingletonCheckCompliantDependent {
}
class CompliantNoScope {
From 91e48f6836ce16188656f781522ed66189adc82e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Mon, 20 Jul 2026 16:09:01 +0200
Subject: [PATCH 04/12] also flag comments on the same line/following line
---
...InsteadOfApplicationScopedCheckSample.java | 34 +++++++++++-
...gletonInsteadOfApplicationScopedCheck.java | 52 ++++++++++++++++---
2 files changed, 77 insertions(+), 9 deletions(-)
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
index 6276de29c73..b240d41ece4 100644
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -4,7 +4,7 @@
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Singleton;
-@Singleton // Noncompliant {{Replace "@Singleton" by "@ApplicationScoped" or add a comment indicating why "@Singleton" is necessary.}} [[sc=1;ec=11]]
+@Singleton // Noncompliant [[sc=1;ec=11]]
class NoncompliantService {
public String foo() {
return "foo";
@@ -12,7 +12,7 @@ public String foo() {
}
class NoncompliantProducerClass {
- @Singleton // Noncompliant {{Replace "@Singleton" by "@ApplicationScoped" or add a comment indicating why "@Singleton" is necessary.}}
+ @Singleton // Noncompliant
//^^^^^^^^^^
public NoncompliantService produceService() {
return new NoncompliantService();
@@ -48,6 +48,36 @@ public String getConfig() {
}
}
+@Singleton // singleton scope required: this bean is a lightweight config holder with no interception needs
+class CompliantWithInlineComment {
+ public String getConfig() {
+ return "config";
+ }
+}
+
+class CompliantProducerWithInlineComment {
+ @Singleton // singleton scope required: non-proxied instance expected by external library
+ public CompliantWithInlineComment produceConfig() {
+ return new CompliantWithInlineComment();
+ }
+}
+
+@Singleton
+// singleton scope required: lightweight config holder with no interception needs
+class CompliantWithFollowingLineComment {
+ public String getConfig() {
+ return "config";
+ }
+}
+
+class CompliantProducerWithFollowingLineComment {
+ @Singleton
+ // singleton scope required: non-proxied instance expected by external library
+ public CompliantWithFollowingLineComment produceConfig() {
+ return new CompliantWithFollowingLineComment();
+ }
+}
+
@ApplicationScoped
class SingletonCheckCompliantApplicationScoped {
public String hello() {
diff --git a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
index 2c0c3faf09b..aa7ee3191ca 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
@@ -17,12 +17,17 @@
package org.sonar.java.checks.quarkus;
import java.util.List;
+import java.util.Locale;
+import java.util.stream.Stream;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.MethodTree;
+import org.sonar.plugins.java.api.tree.ModifierKeywordTree;
import org.sonar.plugins.java.api.tree.ModifiersTree;
+import org.sonar.plugins.java.api.tree.SyntaxToken;
+import org.sonar.plugins.java.api.tree.SyntaxTrivia;
import org.sonar.plugins.java.api.tree.Tree;
@Rule(key = "S9068")
@@ -38,18 +43,51 @@ public List nodesToVisit() {
@Override
public void visitNode(Tree tree) {
- ModifiersTree modifiers = tree instanceof ClassTree classTree
- ? classTree.modifiers()
- : ((MethodTree) tree).modifiers();
+ ModifiersTree modifiers;
+ SyntaxToken declarationToken;
+ if (tree instanceof ClassTree classTree) {
+ modifiers = classTree.modifiers();
+ declarationToken = classTree.declarationKeyword();
+ } else {
+ MethodTree methodTree = (MethodTree) tree;
+ modifiers = methodTree.modifiers();
+ declarationToken = methodTree.returnType().firstToken();
+ }
modifiers.annotations().stream()
.filter(annotation -> annotation.annotationType().symbolType().is(JAKARTA_SINGLETON))
- .filter(annotation -> !hasJustifyingComment(annotation))
+ .filter(annotation -> !hasJustifyingComment(annotation, modifiers, declarationToken))
.forEach(annotation -> reportIssue(annotation, MESSAGE));
}
- private static boolean hasJustifyingComment(AnnotationTree annotation) {
- return annotation.firstToken().trivias().stream()
- .anyMatch(trivia -> trivia.comment().toLowerCase(java.util.Locale.ROOT).contains("singleton"));
+ private static boolean hasJustifyingComment(AnnotationTree annotation, ModifiersTree modifiers, SyntaxToken declarationToken) {
+ // Comment appearing before the annotation (trivia on its @ token)
+ if (containsSingletonComment(annotation.firstToken().trivias())) {
+ return true;
+ }
+ // Comment after the annotation (inline or on the following line): it attaches as trivia to the
+ // next token in the stream, which may be another annotation's @, a modifier keyword, or the
+ // declaration keyword / return type.
+ Stream candidateTokens = Stream.concat(
+ Stream.concat(
+ modifiers.annotations().stream()
+ .map(Tree::firstToken)
+ .filter(t -> !t.equals(annotation.firstToken())),
+ modifiers.modifiers().stream()
+ .map(ModifierKeywordTree::keyword)
+ ),
+ Stream.of(declarationToken)
+ );
+ return candidateTokens
+ .flatMap(token -> token.trivias().stream())
+ .anyMatch(SingletonInsteadOfApplicationScopedCheck::isSingletonComment);
+ }
+
+ private static boolean containsSingletonComment(List trivias) {
+ return trivias.stream().anyMatch(SingletonInsteadOfApplicationScopedCheck::isSingletonComment);
+ }
+
+ private static boolean isSingletonComment(SyntaxTrivia trivia) {
+ return trivia.comment().toLowerCase(Locale.ROOT).contains("singleton");
}
}
From 9e6825382d73bc9376d1b8b75136297d86dca888 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Mon, 20 Jul 2026 16:10:28 +0200
Subject: [PATCH 05/12] fix autoscan test
---
.../src/test/resources/autoscan/diffs/diff_S9068.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
new file mode 100644
index 00000000000..975f9bc5bef
--- /dev/null
+++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
@@ -0,0 +1,6 @@
+{
+ "ruleKey": "S9068",
+ "hasTruePositives": false,
+ "falseNegatives": 4,
+ "falsePositives": 0
+}
From a393ce21671b120e7fd658bda880bc577ac8d1cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Tue, 21 Jul 2026 09:15:28 +0200
Subject: [PATCH 06/12] add record to the nodes to visit
---
.../SingletonInsteadOfApplicationScopedCheckSample.java | 7 +++++++
.../quarkus/SingletonInsteadOfApplicationScopedCheck.java | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
index b240d41ece4..5c5869a642e 100644
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -78,6 +78,13 @@ public CompliantWithFollowingLineComment produceConfig() {
}
}
+@Singleton // Noncompliant [[sc=1;ec=11]]
+record NoncompliantRecord(String value) {}
+
+// Singleton scope required: external lib needs a non-proxied instance.
+@Singleton
+record CompliantRecord(String value) {}
+
@ApplicationScoped
class SingletonCheckCompliantApplicationScoped {
public String hello() {
diff --git a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
index aa7ee3191ca..fb9bc0cc4c7 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
@@ -38,7 +38,7 @@ public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscripti
@Override
public List nodesToVisit() {
- return List.of(Tree.Kind.CLASS, Tree.Kind.METHOD);
+ return List.of(Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
}
@Override
From 63f3de7e8f3a0e56f5fb3e04ab663b40c468233a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Tue, 21 Jul 2026 09:56:24 +0200
Subject: [PATCH 07/12] add heuristic to raise only on files with quarkus
imports
---
...InsteadOfApplicationScopedCheckSample.java | 1 +
...pplicationScopedCheckSampleNonQuarkus.java | 8 +++++++
...gletonInsteadOfApplicationScopedCheck.java | 24 ++++++++++++++++++-
...onInsteadOfApplicationScopedCheckTest.java | 8 +++++++
4 files changed, 40 insertions(+), 1 deletion(-)
create mode 100644 java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
index 5c5869a642e..de976e55a97 100644
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -1,5 +1,6 @@
package checks.quarkus;
+import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Singleton;
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
new file mode 100644
index 00000000000..5c74d974cc8
--- /dev/null
+++ b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
@@ -0,0 +1,8 @@
+package checks.quarkus;
+
+import jakarta.inject.Singleton;
+
+// No io.quarkus.* import: rule should not trigger
+@Singleton
+class NonQuarkusSingleton {
+}
diff --git a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
index fb9bc0cc4c7..d62d9a5f693 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
@@ -23,26 +23,48 @@
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.ClassTree;
+import org.sonar.plugins.java.api.tree.CompilationUnitTree;
+import org.sonar.plugins.java.api.tree.ExpressionTree;
+import org.sonar.plugins.java.api.tree.ImportTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.ModifierKeywordTree;
import org.sonar.plugins.java.api.tree.ModifiersTree;
import org.sonar.plugins.java.api.tree.SyntaxToken;
import org.sonar.plugins.java.api.tree.SyntaxTrivia;
import org.sonar.plugins.java.api.tree.Tree;
+import org.sonar.java.checks.helpers.ExpressionsHelper;
@Rule(key = "S9068")
public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscriptionVisitor {
private static final String JAKARTA_SINGLETON = "jakarta.inject.Singleton";
+ private static final String QUARKUS_PREFIX = "io.quarkus";
private static final String MESSAGE = "Replace \"@Singleton\" by \"@ApplicationScoped\" or add a comment indicating why \"@Singleton\" is necessary.";
+ private boolean analyzingQuarkusFile = false;
+
+ @Override
+ public void leaveFile(org.sonar.plugins.java.api.JavaFileScannerContext context) {
+ analyzingQuarkusFile = false;
+ }
+
@Override
public List nodesToVisit() {
- return List.of(Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
+ return List.of(Tree.Kind.COMPILATION_UNIT, Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
}
@Override
public void visitNode(Tree tree) {
+ if (tree instanceof CompilationUnitTree compilationUnit) {
+ analyzingQuarkusFile = compilationUnit.imports().stream()
+ .filter(ImportTree.class::isInstance)
+ .map(ImportTree.class::cast)
+ .anyMatch(i -> ExpressionsHelper.concatenate((ExpressionTree) i.qualifiedIdentifier()).startsWith(QUARKUS_PREFIX));
+ return;
+ }
+ if (!analyzingQuarkusFile) {
+ return;
+ }
ModifiersTree modifiers;
SyntaxToken declarationToken;
if (tree instanceof ClassTree classTree) {
diff --git a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
index 1d2d2ddcd56..4051fe7b0c1 100644
--- a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
+++ b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
@@ -39,4 +39,12 @@ void testWithoutSemantic() {
.withoutSemantic()
.verifyNoIssues();
}
+
+ @Test
+ void testNonQuarkusFile() {
+ CheckVerifier.newVerifier()
+ .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java"))
+ .withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .verifyNoIssues();
+ }
}
From accfbf229cea7f707edc473989305a1c67585920 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Tue, 21 Jul 2026 11:26:59 +0200
Subject: [PATCH 08/12] check the classpath to determine if the analyzed
project is using Quarkus
---
java-checks-test-sources/pom.xml | 1 +
.../quarkus-arc-3.15/pom.xml | 114 ++++++++++++++++++
...InsteadOfApplicationScopedCheckSample.java | 1 -
.../test/classpath/TestClasspathUtils.java | 1 +
...gletonInsteadOfApplicationScopedCheck.java | 35 ++----
...onInsteadOfApplicationScopedCheckTest.java | 15 ++-
6 files changed, 136 insertions(+), 31 deletions(-)
create mode 100644 java-checks-test-sources/quarkus-arc-3.15/pom.xml
rename java-checks-test-sources/{default => quarkus-arc-3.15}/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java (98%)
diff --git a/java-checks-test-sources/pom.xml b/java-checks-test-sources/pom.xml
index 1489eb9debe..226f86cf559 100644
--- a/java-checks-test-sources/pom.xml
+++ b/java-checks-test-sources/pom.xml
@@ -22,6 +22,7 @@
java-17
spring-3.2
spring-web-4.0
+ quarkus-arc-3.15
test-classpath-reader
diff --git a/java-checks-test-sources/quarkus-arc-3.15/pom.xml b/java-checks-test-sources/quarkus-arc-3.15/pom.xml
new file mode 100644
index 00000000000..1fd497fddbe
--- /dev/null
+++ b/java-checks-test-sources/quarkus-arc-3.15/pom.xml
@@ -0,0 +1,114 @@
+
+
+ 4.0.0
+
+
+ org.sonarsource.java
+ java-checks-test-sources
+ 8.37.0-SNAPSHOT
+
+
+ quarkus-arc-3.15
+
+ SonarQube Java :: Checks Test Sources :: Quarkus ArC 3.15
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+ io.quarkus
+ quarkus-arc
+ 3.15.1
+ provided
+
+
+
+
+
+ analyze-tests
+
+ false
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+
+
+
+ org.simplify4u.plugins
+ sign-maven-plugin
+
+
+ sign-artifacts
+ none
+
+
+
+
+ maven-jar-plugin
+
+
+ default-jar
+ none
+
+
+
+
+ maven-source-plugin
+
+
+ attach-sources
+ none
+
+
+
+
+ maven-javadoc-plugin
+
+
+ attach-javadocs
+ none
+
+
+
+
+ maven-install-plugin
+
+
+ default-install
+ none
+
+
+
+
+ com.mycila
+ license-maven-plugin
+
+
+
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+
+
+
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java b/java-checks-test-sources/quarkus-arc-3.15/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
similarity index 98%
rename from java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
rename to java-checks-test-sources/quarkus-arc-3.15/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
index de976e55a97..5c5869a642e 100644
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
+++ b/java-checks-test-sources/quarkus-arc-3.15/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java
@@ -1,6 +1,5 @@
package checks.quarkus;
-import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Singleton;
diff --git a/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/classpath/TestClasspathUtils.java b/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/classpath/TestClasspathUtils.java
index b07799d8816..00899a8228a 100644
--- a/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/classpath/TestClasspathUtils.java
+++ b/java-checks-test-sources/test-classpath-reader/src/main/java/org/sonar/java/test/classpath/TestClasspathUtils.java
@@ -48,6 +48,7 @@ public final class TestClasspathUtils {
public static final Module JAVA_17_MODULE = new Module("java-checks-test-sources/java-17");
public static final Module SPRING_32_MODULE = new Module("java-checks-test-sources/spring-3.2");
public static final Module SPRING_WEB_40_MODULE = new Module("java-checks-test-sources/spring-web-4.0");
+ public static final Module QUARKUS_ARC_315_MODULE = new Module("java-checks-test-sources/quarkus-arc-3.15");
private static final Path testJarsPath = Path.of("java-checks-test-sources/target/test-jars");
public static List getTestJars(List jars) {
diff --git a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
index d62d9a5f693..a0acf218be5 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheck.java
@@ -18,53 +18,40 @@
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Function;
import java.util.stream.Stream;
import org.sonar.check.Rule;
+import org.sonar.plugins.java.api.DependencyVersionAware;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
+import org.sonar.plugins.java.api.Version;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.ClassTree;
-import org.sonar.plugins.java.api.tree.CompilationUnitTree;
-import org.sonar.plugins.java.api.tree.ExpressionTree;
-import org.sonar.plugins.java.api.tree.ImportTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.ModifierKeywordTree;
import org.sonar.plugins.java.api.tree.ModifiersTree;
import org.sonar.plugins.java.api.tree.SyntaxToken;
import org.sonar.plugins.java.api.tree.SyntaxTrivia;
import org.sonar.plugins.java.api.tree.Tree;
-import org.sonar.java.checks.helpers.ExpressionsHelper;
@Rule(key = "S9068")
-public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscriptionVisitor {
+public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware {
private static final String JAKARTA_SINGLETON = "jakarta.inject.Singleton";
- private static final String QUARKUS_PREFIX = "io.quarkus";
private static final String MESSAGE = "Replace \"@Singleton\" by \"@ApplicationScoped\" or add a comment indicating why \"@Singleton\" is necessary.";
- private boolean analyzingQuarkusFile = false;
-
@Override
- public void leaveFile(org.sonar.plugins.java.api.JavaFileScannerContext context) {
- analyzingQuarkusFile = false;
+ public boolean isCompatibleWithDependencies(Function> dependencyFinder) {
+ return dependencyFinder.apply("quarkus-arc").isPresent();
}
@Override
public List nodesToVisit() {
- return List.of(Tree.Kind.COMPILATION_UNIT, Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
+ return List.of(Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
}
@Override
public void visitNode(Tree tree) {
- if (tree instanceof CompilationUnitTree compilationUnit) {
- analyzingQuarkusFile = compilationUnit.imports().stream()
- .filter(ImportTree.class::isInstance)
- .map(ImportTree.class::cast)
- .anyMatch(i -> ExpressionsHelper.concatenate((ExpressionTree) i.qualifiedIdentifier()).startsWith(QUARKUS_PREFIX));
- return;
- }
- if (!analyzingQuarkusFile) {
- return;
- }
ModifiersTree modifiers;
SyntaxToken declarationToken;
if (tree instanceof ClassTree classTree) {
@@ -96,10 +83,8 @@ private static boolean hasJustifyingComment(AnnotationTree annotation, Modifiers
.map(Tree::firstToken)
.filter(t -> !t.equals(annotation.firstToken())),
modifiers.modifiers().stream()
- .map(ModifierKeywordTree::keyword)
- ),
- Stream.of(declarationToken)
- );
+ .map(ModifierKeywordTree::keyword)),
+ Stream.of(declarationToken));
return candidateTokens
.flatMap(token -> token.trivias().stream())
.anyMatch(SingletonInsteadOfApplicationScopedCheck::isSingletonComment);
diff --git a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
index 4051fe7b0c1..4bdf743b52e 100644
--- a/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
+++ b/java-checks/src/test/java/org/sonar/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckTest.java
@@ -19,32 +19,37 @@
import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;
-import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
+import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPathInModule;
+import static org.sonar.java.test.classpath.TestClasspathUtils.QUARKUS_ARC_315_MODULE;
class SingletonInsteadOfApplicationScopedCheckTest {
+ private static final String SAMPLE = mainCodeSourcesPathInModule(QUARKUS_ARC_315_MODULE, "checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java");
+
@Test
void test() {
CheckVerifier.newVerifier()
- .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java"))
+ .onFile(SAMPLE)
.withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withClassPath(QUARKUS_ARC_315_MODULE.getClassPath())
.verifyIssues();
}
@Test
void testWithoutSemantic() {
CheckVerifier.newVerifier()
- .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSample.java"))
+ .onFile(SAMPLE)
.withCheck(new SingletonInsteadOfApplicationScopedCheck())
.withoutSemantic()
.verifyNoIssues();
}
@Test
- void testNonQuarkusFile() {
+ void testWithoutQuarkusOnClasspath() {
CheckVerifier.newVerifier()
- .onFile(mainCodeSourcesPath("checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java"))
+ .onFile(SAMPLE)
.withCheck(new SingletonInsteadOfApplicationScopedCheck())
+ .withClassPath(java.util.List.of())
.verifyNoIssues();
}
}
From 79b4271bc81677c910f50a3856ad1250511814a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Tue, 21 Jul 2026 15:20:42 +0200
Subject: [PATCH 09/12] update rule metadata
---
.../org/sonar/l10n/java/rules/java/S9068.html | 127 ++----------------
1 file changed, 12 insertions(+), 115 deletions(-)
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
index 2ec6c31a198..b3ca8a4ebcc 100644
--- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9068.html
@@ -5,73 +5,33 @@ Why is this an issue?
Contexts and Dependency Injection) create single-instance beans. However, they behave differently in important ways.
@ApplicationScoped beans are full CDI beans with these capabilities:
- - Support for CDI interceptors (like
@Transactional, @CacheResult, custom interceptors)
- - Support for CDI decorators
- - Can be mocked in tests using QuarkusMock
- - Participate in CDI events and observers
- Support lazy initialization via client proxies
+ - Can be mocked in tests using QuarkusMock
+ - Can be destroyed and recreated at runtime thanks to proxy-based method delegation
@Singleton beans are simpler but more limited:
- - Created eagerly at startup
- - Do not support interception - methods annotated with interceptor annotations will be ignored
+ - Created eagerly when the bean is injected
- Cannot be mocked using QuarkusMock in tests
- - Direct references without proxy support
For most application-level services, @ApplicationScoped is the better choice because it provides the full CDI feature set that Quarkus
applications typically need.
When @Singleton might be acceptable
-There are specific scenarios where @Singleton may be intentionally chosen:
+@Singleton might have a slightly better performance than @ApplicationScoped because of the absence of client proxy. Thus,
+there are specific scenarios where @Singleton may be intentionally chosen:
- - Low-level infrastructure beans: Beans that provide basic utilities without needing interceptors, decorators, or mocking (e.g.,
- configuration holders, simple factories)
+ - Low-level infrastructure beans: Beans that provide basic utilities and don’t require lazy initialization
- Performance-critical paths: Beans where the proxy overhead of
@ApplicationScoped is measured and found to be
significant (rare, and should be documented)
- Third-party integration: When integrating libraries that specifically expect non-proxied instances
- - Startup beans: Beans that must be instantiated eagerly at startup and have no CDI feature requirements
If you choose to use @Singleton, document why you need it and confirm that:
- - The bean does not and will not use CDI interceptors
- The bean does not need to be mocked in tests, or you use alternative mocking strategies
- You understand the limitations and accept them for this specific case
-Exceptions
-This rule may be safely ignored when:
-
- - The bean is a low-level infrastructure component with no interceptor requirements
- - The bean’s design explicitly prohibits the use of CDI proxies (e.g., final classes used intentionally)
- - You have documented performance reasons for avoiding the proxy overhead
-
-However, these cases are rare. Most application services should use @ApplicationScoped.
-Distinguishing false positives from intentional usage
-To determine if a flagged @Singleton is a false positive or intentional:
-It’s likely a real issue if:
-
- - The bean uses any CDI interceptor annotations (
@Transactional, @CacheResult, @Retry, etc.)
- - The bean needs to be mocked in tests
- - The bean is an application-level service (controllers, services, repositories)
- - There’s no documented reason for using
@Singleton
-
-It might be intentional if:
-
- - The bean is clearly infrastructure-level (e.g., named
ConfigurationHolder, SimpleFactory)
- - There’s a comment explaining why
@Singleton is used
- - The bean is extremely simple with no methods that would benefit from interception
- - The bean is part of a documented design pattern that requires non-proxied instances
-
-When in doubt, prefer @ApplicationScoped unless you have a specific, documented reason to use @Singleton.
What is the potential impact?
Using @Singleton instead of @ApplicationScoped can lead to several issues:
-Silent interceptor failures
-Interceptors like @Transactional, @CacheResult, or custom interceptors will be silently ignored on
-@Singleton beans. This can cause:
-
- - Database transactions not being created, leading to data inconsistency
- - Cache annotations being ignored, resulting in performance degradation
- - Security checks being bypassed if implemented via interceptors
-
-These failures are particularly dangerous because the code compiles and runs without errors - the interceptors simply don’t work.
Testing difficulties
@Singleton beans cannot be mocked using QuarkusMock in tests. This means:
@@ -83,14 +43,11 @@ Testing difficulties
Reduced flexibility
Choosing @Singleton limits your future options:
- - Adding interceptors later requires changing the scope annotation
- - Adopting CDI decorators becomes impossible
- Lazy initialization via proxies is unavailable
How to fix it in Quarkus
Replace the @Singleton annotation with @ApplicationScoped. Update the import statement to use
-jakarta.enterprise.context.ApplicationScoped instead of jakarta.inject.Singleton. This is the recommended approach for most
-application beans.
+jakarta.enterprise.context.ApplicationScoped instead of jakarta.inject.Singleton.
Code examples
Noncompliant code example
@@ -116,67 +73,8 @@ Compliant solution
}
}
-When using interceptors like @Transactional, @ApplicationScoped is required for the interceptor to function.
-@Singleton beans are not intercepted, so the transaction will not work.
-Noncompliant code example
-
-import jakarta.inject.Singleton;
-import jakarta.transaction.Transactional;
-
-@Singleton // Noncompliant
-public class OrderService {
- @Transactional
- public void createOrder(Order order) {
- // This transaction will NOT work as expected
- // because @Singleton doesn't support interception
- }
-}
-
-Compliant solution
-
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.transaction.Transactional;
-
-@ApplicationScoped
-public class OrderService {
- @Transactional
- public void createOrder(Order order) {
- // Transaction works correctly
- }
-}
-
-In test scenarios, @ApplicationScoped beans can be mocked using QuarkusMock, while @Singleton beans cannot. This makes
-testing easier and more reliable.
-Noncompliant code example
-
-import jakarta.inject.Singleton;
-
-@Singleton // Noncompliant
-public class EmailService {
- public void sendEmail(String to, String subject) {
- // Actual email sending logic
- // Cannot be easily mocked in tests
- }
-}
-
-Compliant solution
-
-import jakarta.enterprise.context.ApplicationScoped;
-
-@ApplicationScoped
-public class EmailService {
- public void sendEmail(String to, String subject) {
- // Actual email sending logic
- // Can be mocked in tests with QuarkusMock
- }
-}
-
-// In test:
-// QuarkusMock.installMockForType(mockEmailService, EmailService.class);
-
-If you have a documented, valid reason to use @Singleton (such as a low-level infrastructure bean with no interceptor needs), you can
-keep it but should add a comment explaining why. This is an alternative to changing the annotation, though it’s only appropriate in specific
-cases.
+If you have a documented, valid reason to use @Singleton, you can keep it but should add a comment explaining why. This is an
+alternative to changing the annotation, though it’s only appropriate in specific cases.
Noncompliant code example
import jakarta.inject.Singleton;
@@ -209,9 +107,8 @@ Compliant solution
Resources
Documentation
From cf0774279337e6b25640b8ee41b33388efa12f59 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Tue, 21 Jul 2026 15:21:04 +0200
Subject: [PATCH 10/12] remove unused sample
---
...onInsteadOfApplicationScopedCheckSampleNonQuarkus.java | 8 --------
1 file changed, 8 deletions(-)
delete mode 100644 java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
diff --git a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java b/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
deleted file mode 100644
index 5c74d974cc8..00000000000
--- a/java-checks-test-sources/default/src/main/java/checks/quarkus/SingletonInsteadOfApplicationScopedCheckSampleNonQuarkus.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package checks.quarkus;
-
-import jakarta.inject.Singleton;
-
-// No io.quarkus.* import: rule should not trigger
-@Singleton
-class NonQuarkusSingleton {
-}
From 498a7d561da8391efeda3481de61a1e2c613152d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Thu, 23 Jul 2026 09:12:43 +0200
Subject: [PATCH 11/12] add quarkus module to autoscan
---
its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java
index bfae131a57a..282efbcc754 100644
--- a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java
+++ b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java
@@ -123,7 +123,7 @@ public void javaCheckTestSources() throws Exception {
.setProjectName(PROJECT_NAME)
.setProjectVersion("0.1.0-SNAPSHOT")
.setSourceEncoding("UTF-8")
- .setSourceDirs("aws/src/main/java/,default/src/main/java/,java-17/src/main/java/,spring-3.2/src/main/java/,spring-web-4.0/src/main/java/")
+ .setSourceDirs("aws/src/main/java/,default/src/main/java/,java-17/src/main/java/,quarkus-arc-3.15/src/main/java/,spring-3.2/src/main/java/,spring-web-4.0/src/main/java/")
.setTestDirs("default/src/test/java/,java-17/src/test/java/,test-classpath-reader/src/test/java")
.setProperty("sonar.java.source", "26")
// common properties
From 2e953c0bc30617d8604ef8df303b49468022207b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?=
Date: Thu, 23 Jul 2026 10:26:06 +0200
Subject: [PATCH 12/12] update autoscan diff
---
its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
index 975f9bc5bef..7e42fdc3ebe 100644
--- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
+++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9068.json
@@ -1,6 +1,6 @@
{
"ruleKey": "S9068",
"hasTruePositives": false,
- "falseNegatives": 4,
+ "falseNegatives": 3,
"falsePositives": 0
}