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 @@ -2117,6 +2117,12 @@
"falseNegatives": 1,
"falsePositives": 0
},
{
"ruleKey": "S5673",
"hasTruePositives": false,
"falseNegatives": 17,
"falsePositives": 0
},
{
"ruleKey": "S5679",
"hasTruePositives": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S5673",
"hasTruePositives": false,
"falseNegatives": 20,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package checks.spring;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;

public class SpringComponentSpecializationCheckSample {

// Service patterns

@Component // Noncompliant {{Use @Service instead of @Component, or rename this type if the @Component annotation is intentional}}
public class CustomerServiceImpl {
}

@Component // Noncompliant {{Use @Service instead of @Component, or rename this type if the @Component annotation is intentional}}
public class OrderService {
}

@Component // Noncompliant {{Use @Service instead of @Component, or rename this type if the @Component annotation is intentional}}
public class PaymentServiceFacade {
}

// Repository patterns

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public class ProductRepository {
}

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public class UserRepositoryImpl {
}

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public class OrderDao {
}

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public class CustomerDao {
}

// RestController patterns

@Component // Noncompliant {{Use @RestController instead of @Component, or rename this type if the @Component annotation is intentional}}
public class FooBarRestController {
}

@Component // Noncompliant {{Use @RestController instead of @Component, or rename this type if the @Component annotation is intentional}}
public class ApiRestController {
}

@Component // Noncompliant {{Use @RestController instead of @Component, or rename this type if the @Component annotation is intentional}}
public class UserRestControllerImpl {
}

// Controller patterns

@Component // Noncompliant {{Use @Controller instead of @Component, or rename this type if the @Component annotation is intentional}}
public class HomeController {
}

@Component // Noncompliant {{Use @Controller instead of @Component, or rename this type if the @Component annotation is intentional}}
public class LoginControllerImpl {
}

// Compliant - Correct annotations used

@Service
public class CustomerServiceImplCorrect {
}

@Repository
public class ProductRepositoryCorrect {
}

@RestController
public class FooBarRestControllerCorrect {
}

@Controller
public class HomeControllerCorrect {
}

// Generic component names

@Component
public class SomeOtherComponent {
}

@Component
public class UtilityHelper {
}

@Component
public class CacheManager {
}

@Component
public class DataProcessor {
}

// No annotation

public class PlainClass {
}

// Case variations

@Component // Noncompliant {{Use @Service instead of @Component, or rename this type if the @Component annotation is intentional}}
public class userservice {
}

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public class USERREPOSITORY {
}

@Component // Noncompliant {{Use @Controller instead of @Component, or rename this type if the @Component annotation is intentional}}
public class maincontroller {
}

@Component // Noncompliant {{Use @RestController instead of @Component, or rename this type if the @Component annotation is intentional}}
public class apirestcontroller {
}

// Interface patterns

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public interface UserRepository {
}

@Component // Noncompliant {{Use @Service instead of @Component, or rename this type if the @Component annotation is intentional}}
public interface PaymentService {
}

@Component // Noncompliant {{Use @Repository instead of @Component, or rename this type if the @Component annotation is intentional}}
public interface ProductDao {
}

// Compliant interfaces - correct annotations used

@Repository
public interface CategoryRepository {
}

@Service
public interface NotificationService {
}

// Compliant interfaces - generic names

@Component
public interface EventListener {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.spring;

import java.util.List;
import java.util.Optional;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.SpringUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S5673")
public class SpringComponentSpecializationCheck extends IssuableSubscriptionVisitor {

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

@Override
public void visitNode(Tree tree) {
var classTree = (ClassTree) tree;

Optional<AnnotationTree> componentAnnotation = classTree.modifiers().annotations().stream()
.filter(a -> SpringUtils.COMPONENT_ANNOTATION.equals(a.annotationType().symbolType().fullyQualifiedName()))
.findFirst();

if (componentAnnotation.isEmpty()) {
return;
}

String className = classTree.simpleName().name();
String suggestedAnnotation = getSuggestedAnnotation(className);

if (suggestedAnnotation != null) {
reportIssue(componentAnnotation.get(), String.format("Use @%s instead of @Component, or rename this type if the @Component annotation is intentional", suggestedAnnotation));
}
}

@CheckForNull
private static String getSuggestedAnnotation(String className) {
// Check RestController first to avoid false matches with Controller
if (endsWithIgnoreCase(className, "RestController") || endsWithIgnoreCase(className, "RestControllerImpl")) {
return "RestController";
}

if (endsWithIgnoreCase(className, "Controller") || endsWithIgnoreCase(className, "ControllerImpl")) {
return "Controller";
}

if (endsWithIgnoreCase(className, "Service") ||
endsWithIgnoreCase(className, "ServiceImpl") ||
endsWithIgnoreCase(className, "ServiceFacade")) {
return "Service";
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.

if (endsWithIgnoreCase(className, "Repository") ||
endsWithIgnoreCase(className, "RepositoryImpl") ||
endsWithIgnoreCase(className, "Dao")) {
return "Repository";
}

return null;
}

private static boolean endsWithIgnoreCase(String str, String suffix) {
if (str.length() < suffix.length()) {
return false;
}
return str.substring(str.length() - suffix.length()).equalsIgnoreCase(suffix);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.spring;

import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;

import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;

class SpringComponentSpecializationCheckTest {

@Test
void test() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/spring/SpringComponentSpecializationCheckSample.java"))
.withCheck(new SpringComponentSpecializationCheck())
.verifyIssues();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<p>The Spring Framework provides several specializations of the generic <code>@Component</code> stereotype annotation which better express the programmer's intent. Using them should be preferred.</p>
<h2>Why is this an issue?</h2>
<p>Spring provides specialized stereotype annotations (<code>@Service</code>, <code>@Repository</code>, <code>@Controller</code>, and <code>@RestController</code>) that make code more expressive and provide additional semantics. When a class name suggests it should use one of these specialized annotations but uses <code>@Component</code> instead, it reduces code clarity.</p>
<h2>How to fix it</h2>
<p>Replace <code>@Component</code> with the appropriate specialized annotation based on the class's role in the application.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
@Component // Noncompliant
public class CustomerServiceImpl {
// ...
}

@Component // Noncompliant
public class ProductRepository {
// ...
}

@Component // Noncompliant
public class FooBarRestController {
// ...
}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
@Service // Compliant
public class CustomerServiceImpl {
// ...
}

@Repository // Compliant
public class ProductRepository {
// ...
}

@RestController // Compliant
public class FooBarRestController {
// ...
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li><a href="https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-stereotype-annotations">Spring documentation - @Component and Further Stereotype Annotations</a></li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"title": "Spring components should use specialized annotations",
"type": "CODE_SMELL",
"code": {
"impacts": {
"MAINTAINABILITY": "LOW"
},
"attribute": "CLEAR"
},
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "2min"
},
"tags": [
"spring"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-5673",
"sqKey": "S5673",
"scope": "All",
"quickfix": "unknown"
}
Empty file.
Loading