Skip to content

SONARJAVA-6767 Implement new rule S9341: Redundant Spring annotations should be removed - #5930

Merged
romainbrenguier merged 7 commits into
masterfrom
new-rule/SONARJAVA-6767-S9341
Aug 19, 2026
Merged

SONARJAVA-6767 Implement new rule S9341: Redundant Spring annotations should be removed#5930
romainbrenguier merged 7 commits into
masterfrom
new-rule/SONARJAVA-6767-S9341

Conversation

@romainbrenguier

Copy link
Copy Markdown
Contributor

Detect redundant Spring annotations where a more specific composed annotation already implies the parent. Covers stereotype annotations (@component with @Service/@Repository/@Controller/@configuration), @RestController composition, @SpringBootApplication composition, and Spring test annotation redundancies.

Part of

Detect redundant Spring annotations where a more specific composed
annotation already implies the parent. Covers stereotype annotations
(@component with @Service/@Repository/@Controller/@configuration),
@RestController composition, @SpringBootApplication composition,
and Spring test annotation redundancies.
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6767

…nentscan with filters

- Fix @transactional + @DataJpaTest: only flag as redundant when no attributes are set,
  since custom attributes like readOnly or propagation change runtime behavior
- Fix @componentscan + @SpringBootApplication: reject any attribute (not just value/basePackages/basePackageClasses),
  since attributes like excludeFilters, lazyInit, useDefaultFilters are not exposed by @SpringBootApplication
- Add compliant test cases for both fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@romainbrenguier romainbrenguier changed the title SONARJAVA-6767 Implement new rule S934: Redundant Spring annotations should be removed SONARJAVA-6767 Implement new rule S9341: Redundant Spring annotations should be removed Aug 18, 2026
@romainbrenguier
romainbrenguier marked this pull request as ready for review August 18, 2026 10:01

@nathsou nathsou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the implementation. I found four issues that need addressing before this can be merged.

private static final List<RedundancyRule> REDUNDANCY_RULES = List.of(
new RedundancyRule(SpringUtils.COMPONENT_ANNOTATION,
List.of(SpringUtils.SERVICE_ANNOTATION, SpringUtils.REPOSITORY_ANNOTATION, SpringUtils.CONTROLLER_ANNOTATION, SpringUtils.CONFIGURATION_ANNOTATION), null),
new RedundancyRule(SpringUtils.CONTROLLER_ANNOTATION,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] The removal is unsafe when the parent annotation has explicit attributes. For example, @Component("orders") next to @Service supplies the bean name; removing it changes that name. Similarly, @Configuration(proxyBeanMethods = false) alongside @SpringBootApplication changes configuration semantics, and @EnableAutoConfiguration(exclude = Foo.class) loses the exclusion. Only @ComponentScan and @Transactional are guarded today. Report these pairs only when the parent annotation has no explicit attributes, and add compliant regression cases.

continue;
}
for (String impliedByFqn : rule.impliedByFqns) {
AnnotationTree impliedByAnnotation = annotationsByFqn.get(impliedByFqn);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Collapsing annotations by FQN breaks repeatable annotations. With @ExtendWith(SpringExtension.class), then @ExtendWith(MockitoExtension.class), then @SpringBootTest, this map retains Mockito while valuesForAnnotation examines the first matching semantic annotation. The check can therefore report Mockito as redundant. @ComponentScan has the same risk. Preserve/evaluate each annotation instance rather than one annotation per FQN, and cover repeated-annotation cases.

List.of(DATA_JPA_TEST), RedundantSpringAnnotationCheck::isTransactionalWithoutCustomAttributes)
);

@Override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Spring stereotype annotations can target records, but the visitor subscribes only to CLASS; @Component @Service record Foo() {} is ignored. Subscribe to Tree.Kind.RECORD as well and add a record test case.

List.of(DATA_JPA_TEST), RedundantSpringAnnotationCheck::isTransactionalWithoutCustomAttributes)
);

@Override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] The linked RSPEC says method-level @ResponseBody in a @RestController should be reported, but this check visits only classes. Either implement that behavior (while resolving the overlap with S6837) or update RSPEC to avoid promising it.

… annotations, and records

- Use multimap for annotation collection to properly handle repeatable annotations
  like multiple @ExtendWith or @componentscan instances
- Add attribute guards to prevent unsafe removal of annotations with explicit
  attributes (@component with bean name, @configuration with proxyBeanMethods,
  @EnableAutoConfiguration with exclude, @SpringBootConfiguration with attributes)
- Add Tree.Kind.RECORD to visited nodes so records are also checked
- Evaluate @ExtendWith per annotation instance using AST arguments instead of
  merged metadata to avoid false positives on non-Spring extensions
- Document that method-level @responsebody is handled by S6837

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +136 to +150
private static boolean isExtendWithSpringExtensionOnly(AnnotationTree annotation) {
var arguments = annotation.arguments();
if (arguments.size() != 1) {
return false;
}
ExpressionTree arg = arguments.get(0);
if (arg.is(Tree.Kind.MEMBER_SELECT)) {
return isSpringExtensionClassRef((MemberSelectExpressionTree) arg);
}
if (arg.is(Tree.Kind.NEW_ARRAY)) {
var initializers = ((NewArrayTree) arg).initializers();
return initializers.size() == 1
&& initializers.get(0).is(Tree.Kind.MEMBER_SELECT)
&& isSpringExtensionClassRef((MemberSelectExpressionTree) initializers.get(0));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: @ExtendWith(value = SpringExtension.class) not detected

isExtendWithSpringExtensionOnly only handles a single argument that is a MEMBER_SELECT (SpringExtension.class) or a NEW_ARRAY. When the argument is written in the explicit named form @ExtendWith(value = SpringExtension.class), the argument tree is an ASSIGNMENT, so the method returns false and the redundant annotation is not reported (false negative). Consider unwrapping an ASSIGNMENT whose name is value to its expression before checking, so the named form is treated the same as the shorthand.

Was this helpful? React with 👍 / 👎

romainbrenguier and others added 3 commits August 18, 2026 16:30
The default module uses Spring Boot 2.0.2 which does not have the
proxyBeanMethods attribute on @SpringBootConfiguration (added in 2.2).
This caused a compilation failure in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Predicate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…S9341

Cover additional code paths in isExtendWithSpringExtensionOnly():
- Single-element array syntax: @ExtendWith({SpringExtension.class})
- Named parameter: @ExtendWith(value = SpringExtension.class)
- Single-element array with non-SpringExtension class

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@nathsou nathsou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one issue that should be addressed before merging.

List.of(SpringUtils.SERVICE_ANNOTATION, SpringUtils.REPOSITORY_ANNOTATION, SpringUtils.CONTROLLER_ANNOTATION, SpringUtils.CONFIGURATION_ANNOTATION),
RedundantSpringAnnotationCheck::hasNoExplicitAttributes),
new RedundancyRule(SpringUtils.CONTROLLER_ANNOTATION,
List.of(SpringUtils.REST_CONTROLLER_ANNOTATION), null),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @Controller@RestController redundancy rule has no hasNoExplicitAttributes guard, unlike the sibling @Component/@Configuration rules, so it fires even when @Controller carries a custom bean name.

For example:

@Controller("myCustomBeanName")
@RestController
class MyController {}

This reports "Remove this @controller annotation, already implied by @RestController", even though @RestController is meta-annotated with a plain, argument-less @Controller. Removing the annotated one would silently drop the custom bean name and fall back to Spring's default auto-generated name — a behavior-changing false positive that the analogous COMPONENT_ANNOTATION/CONFIGURATION_ANNOTATION rules explicitly avoid via RedundantSpringAnnotationCheck::hasNoExplicitAttributes.

…estController

Add hasNoExplicitAttributes guard to the Controller→RestController
redundancy rule, consistent with the sibling Component and Configuration
rules. Without the guard, @controller("myCustomBeanName") @RestController
was reported as redundant, even though removing it would silently drop the
custom bean name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqube-next

Copy link
Copy Markdown
Contributor

@romainbrenguier
romainbrenguier merged commit 2a90277 into master Aug 19, 2026
16 checks passed
@romainbrenguier
romainbrenguier deleted the new-rule/SONARJAVA-6767-S9341 branch August 19, 2026 13:34
@gitar-bot

gitar-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 3 findings

Implements rule S9341 to detect redundant Spring annotations while properly handling custom attributes and record types. Consider updating isExtendWithSpringExtensionOnly to correctly detect @ExtendWith without member selection wrappers.

💡 Edge Case: @ExtendWith(value = SpringExtension.class) not detected

📄 java-checks/src/main/java/org/sonar/java/checks/spring/RedundantSpringAnnotationCheck.java:136-150

isExtendWithSpringExtensionOnly only handles a single argument that is a MEMBER_SELECT (SpringExtension.class) or a NEW_ARRAY. When the argument is written in the explicit named form @ExtendWith(value = SpringExtension.class), the argument tree is an ASSIGNMENT, so the method returns false and the redundant annotation is not reported (false negative). Consider unwrapping an ASSIGNMENT whose name is value to its expression before checking, so the named form is treated the same as the shorthand.

✅ 2 resolved
Edge Case: @transactional flagged redundant even when it carries custom attributes

📄 java-checks/src/main/java/org/sonar/java/checks/spring/RedundantSpringAnnotationCheck.java:65-66
The rule for @transactional implied by @DataJpaTest (RedundantSpringAnnotationCheck.java:65-66) has no special condition, so ANY @transactional is reported as removable when @DataJpaTest is present. But @DataJpaTest only supplies a default @transactional; a user commonly overrides it, e.g. @transactional(propagation = Propagation.NOT_SUPPORTED), readOnly = true, a custom isolation/timeout, or a specific transactionManager. Removing such an annotation silently changes runtime behavior, so this is a false positive. Add a special condition (like isComponentScanWithoutCustomAttributes) that only flags @transactional when it has no explicitly-set attributes, and add test cases covering the attribute-carrying variant.

Bug: ComponentScan redundancy check misses filter/other custom attributes

📄 java-checks/src/main/java/org/sonar/java/checks/spring/RedundantSpringAnnotationCheck.java:126-139 📄 java-checks/src/main/java/org/sonar/java/checks/spring/RedundantSpringAnnotationCheck.java:58-59
isComponentScanWithoutCustomAttributes (RedundantSpringAnnotationCheck.java:126-139) only treats @componentscan as non-redundant when value/basePackages/basePackageClasses are set. Any other explicit attribute — excludeFilters, includeFilters, nameGenerator, lazyInit, useDefaultFilters, etc. — causes the method to return true and the annotation to be reported as removable. @SpringBootApplication does not expose includeFilters/excludeFilters, so removing a @componentscan(excludeFilters = ...) drops scanning configuration and changes behavior — a false positive. Broaden the condition to return false whenever ANY attribute is explicitly set (i.e. treat a non-empty attribute list as custom), and add corresponding test cases.

🤖 Prompt for agents
Code Review: Implements rule S9341 to detect redundant Spring annotations while properly handling custom attributes and record types. Consider updating isExtendWithSpringExtensionOnly to correctly detect @ExtendWith without member selection wrappers.

1. 💡 Edge Case: @ExtendWith(value = SpringExtension.class) not detected
   Files: java-checks/src/main/java/org/sonar/java/checks/spring/RedundantSpringAnnotationCheck.java:136-150

   isExtendWithSpringExtensionOnly only handles a single argument that is a MEMBER_SELECT (SpringExtension.class) or a NEW_ARRAY. When the argument is written in the explicit named form `@ExtendWith(value = SpringExtension.class)`, the argument tree is an ASSIGNMENT, so the method returns false and the redundant annotation is not reported (false negative). Consider unwrapping an ASSIGNMENT whose name is `value` to its expression before checking, so the named form is treated the same as the shorthand.

Implementation Status ✅ 1 / 1 issues implemented
SONARJAVA-6767 — 1 / 1 objectives

The PR implements rule S9341 for removing redundant Spring annotations.

✅ 1 complete
  • ✅ Implement new rule S9341: Redundant Spring annotations should be removed
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants