Skip to content

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

Open
romainbrenguier wants to merge 4 commits into
masterfrom
new-rule/SONARJAVA-6767-S9341
Open

SONARJAVA-6767 Implement new rule S9341: Redundant Spring annotations should be removed#5930
romainbrenguier wants to merge 4 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 👍 / 👎

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>
@gitar-bot

gitar-bot Bot commented Aug 18, 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 successfully implements the new rule S9341 to detect and report redundant Spring annotations along with corresponding unit tests and rule documentation.

✅ 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

@sonarqube-next

Copy link
Copy Markdown
Contributor

Quality Gate failed Quality Gate failed

Failed conditions
1 New issue
88.2% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE SonarQube for IDE

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