SONARJAVA-6833: S2699 - Fix false positives for disabled tests, Java assert, and AssertJ methods - #6096
SONARJAVA-6833: S2699 - Fix false positives for disabled tests, Java assert, and AssertJ methods#6096asya-vorobeva wants to merge 2 commits into
Conversation
asya-vorobeva
commented
Sep 8, 2026
- Add missing AssertJ assertion method prefixes (accepts, matches, startsWith) to ASSERTJ_ASSERTION_METHODS_PREDICATE so methods like accepts(), startsWith(), and matches() are recognized when called on AssertJ assertion types
- Skip assertion checking for test methods annotated with @disabled (JUnit 5) or @ignore (JUnit 4) since those tests are never executed
- Recognize Java built-in assert statement as a valid assertion in AbstractAssertionVisitor, fixing FPs for both S2699 and S6103
…assert, and AssertJ methods - Add missing AssertJ assertion method prefixes (accepts, matches, startsWith) to ASSERTJ_ASSERTION_METHODS_PREDICATE so methods like accepts(), startsWith(), and matches() are recognized when called on AssertJ assertion types - Skip assertion checking for test methods annotated with @disabled (JUnit 5) or @ignore (JUnit 4) since those tests are never executed - Recognize Java built-in assert statement as a valid assertion in AbstractAssertionVisitor, fixing FPs for both S2699 and S6103 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| @Test | ||
| public void assertj_predicate_accepts() { | ||
| Assertions.assertThat(longPredicateMethod()).accepts(1L, 2L); | ||
| } | ||
|
|
||
| @Test | ||
| public void assertj_string_starts_with() { | ||
| Assertions.assertThat("hello world").startsWith("hello"); | ||
| } | ||
|
|
||
| @Test | ||
| public void assertj_string_matches() { | ||
| Assertions.assertThat("hello").matches("[a-z]+"); | ||
| } | ||
|
|
There was a problem hiding this comment.
⚠️ Quality: New AssertJ samples don't exercise the predicate change
All five new sample tests start their chain with Assertions.assertThat(...), whose name already matches ASSERTION_METHODS_PATTERN ((assert|verify|fail|...).*) in methodNameMatchesAssertionMethodPattern, so AbstractAssertionVisitor sets hasAssertion = true on the assertThat invocation itself and never needs to match the terminal accepts/startsWith/matches call. These samples therefore pass identically with or without the ASSERTJ_ASSERTION_METHODS_PREDICATE change, leaving the PR's main code change with zero coverage (the existing bdd_assertions_* samples in the same file use BDDAssertions.then(...) precisely because then is not name-matched). Use the BDD entry point (or a bare AbstractAssert receiver) so the terminal method is what decides, and/or extend UnitTestUtilsTest.testAssertJAssertionMethodPattern with the three new alternatives.
Drive the samples through BDDAssertions.then(...) so the new terminal-method names are what makes them compliant:
@Test
public void assertj_predicate_accepts() { // Compliant
BDDAssertions.then(longPredicateMethod()).accepts(1L, 2L);
}
@Test
public void assertj_string_starts_with() { // Compliant
BDDAssertions.then("hello world").startsWith("hello");
}
@Test
public void assertj_string_matches() { // Compliant
BDDAssertions.then("hello").matches("[a-z]+");
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| private static boolean isDisabledTest(MethodTree methodTree) { | ||
| SymbolMetadata metadata = methodTree.symbol().metadata(); | ||
| return metadata.isAnnotatedWith(JUNIT5_DISABLED_ANNOTATION) || metadata.isAnnotatedWith(JUNIT4_IGNORE_ANNOTATION); | ||
| } |
There was a problem hiding this comment.
💡 Edge Case: @Disabled/@ignore skipped only at method level, not class level
isDisabledTest only inspects the method symbol's metadata, so a test class annotated with @Disabled (JUnit 5) or @Ignore (JUnit 4) — whose methods are equally never executed, which is the PR's stated rationale — still raises S2699 on every assertion-less method. The same gap applies to a @Disabled enclosing @Nested class. Extend the check to the enclosing class(es).
Also consider the enclosing class annotations:
private static boolean isDisabledTest(MethodTree methodTree) {
Symbol.TypeSymbol enclosingClass = methodTree.symbol().enclosingClass();
return isDisabled(methodTree.symbol().metadata())
|| (enclosingClass != null && isDisabled(enclosingClass.metadata()));
}
private static boolean isDisabled(SymbolMetadata metadata) {
return metadata.isAnnotatedWith(JUNIT5_DISABLED_ANNOTATION) || metadata.isAnnotatedWith(JUNIT4_IGNORE_ANNOTATION);
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| private static final String JUNIT5_DISABLED_ANNOTATION = "org.junit.jupiter.api.Disabled"; | ||
| private static final String JUNIT4_IGNORE_ANNOTATION = "org.junit.Ignore"; |
There was a problem hiding this comment.
💡 Quality: Disabled-test annotation names duplicated from IgnoredTestsCheck
IgnoredTestsCheck (S1607) already encodes the same pair of fully qualified names (org.junit.Ignore, org.junit.jupiter.api.Disabled), and JUnit4AnnotationsCheck maps between them; the new private constants and isDisabledTest re-implement that knowledge in a third place, so a future addition (e.g. TestNG's org.testng.annotations.Ignore) has to be applied several times. Move the annotation list and the predicate into UnitTestUtils, next to the other test-annotation sets, and reuse it from both checks.
Add a shared helper in UnitTestUtils and call it from AssertionsInTestsCheck:
// UnitTestUtils.java
public static final List<String> SKIPPED_TEST_ANNOTATIONS = List.of("org.junit.Ignore", "org.junit.jupiter.api.Disabled");
public static boolean isSkippedTest(MethodTree methodTree) {
SymbolMetadata metadata = methodTree.symbol().metadata();
return SKIPPED_TEST_ANNOTATIONS.stream().anyMatch(metadata::isAnnotatedWith);
}
// AssertionsInTestsCheck.java, replacing the two constants and isDisabledTest
if (isSkippedTest(methodTree) || isSpringBootAssertableContext(methodTree)) {
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| @Override | ||
| public void visitAssertStatement(AssertStatementTree tree) { | ||
| hasAssertion = true; | ||
| } |
There was a problem hiding this comment.
💡 Quality: Rule descriptions not updated for assert/@disabled exemptions
S2699's description enumerates exactly which assertion mechanisms are recognised ("assertions from any of the following known frameworks") and says nothing about Java's assert statement or about skipped tests being exempt, so after this PR the documented behaviour no longer matches the implementation for both S2699 and S6103 (the latter now accepts a bare assert inside an AssertJ consumer). Since these HTML files are generated from RSPEC, the corresponding RSPEC entries need updating along with the code change.
Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar
This comment has been minimized.
This comment has been minimized.
|
❌ Ruling needs updating. A fix PR has been created: #6097 Please review and merge it into your branch. |
Ruling Diff SummaryDetected changes in 2 rule files: 2 issues removed, 0 issues added. S2699 (
|
|



