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:

+ +

@Singleton beans are simpler but more limited:

+ +

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:

+ +

If you choose to use @Singleton, document why you need it and confirm that:

+ +

Exceptions

+

This rule may be safely ignored when:

+ +

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:

+ +

It might be intentional if:

+ +

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:

+ +

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:

+ +

Reduced flexibility

+

Choosing @Singleton limits your future options:

+ +

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 }