Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S9068",
"hasTruePositives": false,
"falseNegatives": 3,
"falsePositives": 0
}
1 change: 1 addition & 0 deletions java-checks-test-sources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<module>java-17</module>
<module>spring-3.2</module>
<module>spring-web-4.0</module>
<module>quarkus-arc-3.15</module>

<!-- should be the last module to be able to test classpath files "target/test-classpath.txt" of the previous modules -->
<module>test-classpath-reader</module>
Expand Down
114 changes: 114 additions & 0 deletions java-checks-test-sources/quarkus-arc-3.15/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.sonarsource.java</groupId>
<artifactId>java-checks-test-sources</artifactId>
<version>8.37.0-SNAPSHOT</version>
</parent>

<artifactId>quarkus-arc-3.15</artifactId>

<name>SonarQube Java :: Checks Test Sources :: Quarkus ArC 3.15</name>

<properties>
<sonar.skip>true</sonar.skip>
<forbiddenapis.skip>true</forbiddenapis.skip>
<skipTests>true</skipTests>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<version>3.15.1</version>
<scope>provided</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>analyze-tests</id>
<properties>
<sonar.skip>false</sonar.skip>
</properties>
</profile>
</profiles>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.simplify4u.plugins</groupId>
<artifactId>sign-maven-plugin</artifactId>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<executions>
<execution>
<id>default-install</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<licenseSets>
<licenseSet>
<excludes>
<exclude>src/main/java/**</exclude>
<exclude>src/test/java/**</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package checks.quarkus;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Singleton;

@Singleton // Noncompliant [[sc=1;ec=11]]
class NoncompliantService {
public String foo() {
return "foo";
}
}

class NoncompliantProducerClass {
@Singleton // Noncompliant
//^^^^^^^^^^
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";
}
}

@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();
}
}

@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() {
return "hello";
}
}

@Dependent
class SingletonCheckCompliantDependent {
}

class CompliantNoScope {
public String hello() {
return "hello";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<File> getTestJars(List<String> jars) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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 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.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")
public class SingletonInsteadOfApplicationScopedCheck extends IssuableSubscriptionVisitor implements DependencyVersionAware {

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 boolean isCompatibleWithDependencies(Function<String, Optional<Version>> dependencyFinder) {
return dependencyFinder.apply("quarkus-arc").isPresent();
}

@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.CLASS, Tree.Kind.RECORD, Tree.Kind.METHOD);
}

@Override
public void visitNode(Tree tree) {
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, modifiers, declarationToken))
.forEach(annotation -> reportIssue(annotation, MESSAGE));
}

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<SyntaxToken> 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<SyntaxTrivia> trivias) {
return trivias.stream().anyMatch(SingletonInsteadOfApplicationScopedCheck::isSingletonComment);
}

private static boolean isSingletonComment(SyntaxTrivia trivia) {
return trivia.comment().toLowerCase(Locale.ROOT).contains("singleton");
}
}
Loading
Loading