Skip to content

SONARJAVA-6960 Validate issue gaps against remediation metadata - #6131

Closed
nathsou wants to merge 2 commits into
masterfrom
nathan/validate-remediation-metadata
Closed

nathsou wants to merge 2 commits into
masterfrom
nathan/validate-remediation-metadata

Conversation

@nathsou

@nathsou nathsou commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Part of SONARJAVA-6960

Summary

Validate issues produced by both the modern and deprecated check verifiers against SonarJava's bundled remediation metadata.

  • Reject an explicit gap with Constant/Issue remediation (the S1191 failure mode rejected by SonarQube's DebtCalculator).
  • Reject an explicit gap when located metadata has no remediation, because that gap would be ignored.
  • Validate remediation function names, required cost properties, duration syntax/range, property types and incompatible properties.
  • Forward actual gaps to analyzer-commons so [[effortToFix=N]] expectations detect incorrect or missing reported values. The deprecated verifier now checks these expectations independently of metadata availability too.
  • Cache metadata lookups per rule key, including absent resources, and reuse one Gson instance. Remove the deprecated verifier's duplicate metadata parser.
  • Check that every annotated top-level check discovered in the built-in checks packages has metadata on each check module's test classpath. Missing resources remain supported for custom checks.
  • Support current <ruleKey>.json and legacy <ruleKey>_java.json resource names.

Linear and linear-with-offset remediation may omit the gap: DebtCalculator then uses a multiplier of one. The previous deprecated-verifier assertion requiring an explicit linear gap was incorrect and has been removed. SonarJava's nonpositive internal cost sentinels become null gaps and remain valid.

Coverage and limits

This validates emitted issues exercised by tests against the metadata bundled with the analyzer. It does not compare against the latest upstream RSPEC checkout or server-side remediation overrides, and it cannot exercise untested reporting branches. Metadata alone cannot establish whether a reported gap counts the correct things: rule fixtures should supply [[effortToFix=N]] when the exact value matters.

Verification

  • JDK 21 reactor run: all 262 testkit tests, all 13 AWS tests, and all 4 S1191/classpath tests pass.
  • The classpath guards check all discovered annotated top-level checks, not just S1191 and S6263.
  • The earlier S1191 mutation check (temporarily changing Linear to Constant/Issue) failed all three rule tests with gap 2.0, as intended; the metadata was restored.
  • A broader run exercised 2,331 java-checks tests with 71 failures in rule/semantic expectations in the partially prepared local fixture environment, and no remediation/gap-validation failures. This is not a claim that the complete rule suite is green; the final focused run above passed after preparing the fixtures.
  • git diff --check passes.

The stricter legacy gap comparison also exposed a fake check in the testkit that reported no gap despite its fixture expecting four; the fake check now reports the expected gap.

@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod Bot changed the title Validate issue gaps against remediation metadata SONARJAVA-6960 Validate issue gaps against remediation metadata Sep 14, 2026
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6960

@nathsou nathsou self-assigned this Sep 14, 2026
Comment thread java-checks/pom.xml
@sonarqube-next

Copy link
Copy Markdown
Contributor

Quality Gate failed Quality Gate failed

Failed conditions
1 New issue
80.0% 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

@datadog-sonarsource

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

Build | Test Analyze

View in Datadog · View in GitHub Actions

Failed to query server version: GET ***/api/server/version failed with HTTP 404 Not Found.

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: f370a8e | Docs | View more details | Give us feedback!

Comment on lines +30 to +42
@Test
void rule_metadata_is_available_on_test_classpath() throws IOException {
var rules = ClassPath.from(getClass().getClassLoader()).getTopLevelClassesRecursive("org.sonar.java.checks").stream()
.map(ClassPath.ClassInfo::load)
.filter(check -> check.isAnnotationPresent(Rule.class))
.toList();
assertThat(rules).isNotEmpty();
for (Class<?> rule : rules) {
String key = rule.getAnnotation(Rule.class).key();
assertThat(getClass().getResource("/org/sonar/l10n/java/rules/java/" + key + ".json"))
.as("Remediation metadata for %s (%s)", key, rule.getName()).isNotNull();
}
}

@gitar-bot gitar-bot Bot Sep 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: Repo-wide metadata guard duplicated inside two rule test classes

The new rule_metadata_is_available_on_test_classpath test is a module-wide invariant (it discovers every annotated top-level check under org.sonar.java.checks and asserts its <ruleKey>.json is on the test classpath), but it is copy-pasted verbatim into two unrelated single-rule test classes, so a future change to the invariant has to be made in two places and the guard is invisible to anyone looking for it. Since both modules already put ../sonar-java-plugin/src/main/resources on the test classpath (java-checks/pom.xml:104, java-checks-aws/pom.xml:90), the shared body belongs in one helper (e.g. in java-checks-testkit next to RuleMetadataValidator, or a dedicated RuleMetadataClasspathTest per module) that both modules call with their base package.

Extract the guard into a shared testkit helper and call it from one dedicated test class per check module.:

// java-checks-testkit: new public helper, e.g. org.sonar.java.checks.verifier.RuleMetadataAssert
public static void assertMetadataAvailable(ClassLoader loader, String basePackage) throws IOException {
  var rules = ClassPath.from(loader).getTopLevelClassesRecursive(basePackage).stream()
    .map(ClassPath.ClassInfo::load)
    .filter(check -> check.isAnnotationPresent(Rule.class))
    .toList();
  assertThat(rules).isNotEmpty();
  for (Class<?> rule : rules) {
    String key = rule.getAnnotation(Rule.class).key();
    assertThat(loader.getResource("org/sonar/l10n/java/rules/java/" + key + ".json"))
      .as("Remediation metadata for %s (%s)", key, rule.getName()).isNotNull();
  }
}
// then each module keeps a one-line dedicated test calling this helper

Was this helpful? React with 👍 / 👎

Comment on lines +34 to +43
@ParameterizedTest
@ValueSource(strings = {
"{\"func\":\"Constant/Issue\",\"constantCost\":\"5min\"}",
"{\"func\":\"Linear\",\"linearFactor\":\"2h\"}",
"{\"func\":\"Linear with offset\",\"linearFactor\":\"1min\",\"linearOffset\":\"0min\"}"
})
void valid_remediation(String remediation) {
assertThat(RuleMetadataValidator.readFunction(new StringReader("{\"remediation\":" + remediation + "}"), "Test"))
.isIn("Constant/Issue", "Linear", "Linear with offset");
}

@gitar-bot gitar-bot Bot Sep 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: valid_remediation test cannot detect a wrong function mapping

valid_remediation asserts only isIn("Constant/Issue", "Linear", "Linear with offset"), so it passes for any of the three inputs even if readFunction returned the wrong function name (e.g. returning "Linear" for the Constant/Issue input), which is exactly the mapping the validate gap rejection depends on. Pair each input with its expected return value so the mapping itself is covered.

Assert the exact function returned for each metadata input.:

@ParameterizedTest
@CsvSource(delimiter = '|', value = {
  "{"func":"Constant/Issue","constantCost":"5min"} | Constant/Issue",
  "{"func":"Linear","linearFactor":"2h"} | Linear",
  "{"func":"Linear with offset","linearFactor":"1min","linearOffset":"0min"} | Linear with offset"
})
void valid_remediation(String remediation, String expectedFunction) {
  assertThat(RuleMetadataValidator.readFunction(new StringReader("{"remediation":" + remediation + "}"), "Test"))
    .isEqualTo(expectedFunction);
}

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown
CI failed: The CI build failed due to a SonarQube server endpoint returning HTTP 404 during scanner bootstrapping.

Overview

One job failure was encountered where the Maven build failed during the execution of the sonar-maven-plugin due to a server connection issue (HTTP 404 Not Found on the server version query).

Failures

SonarQube Scanner Bootstrapping Failure (confidence: high)

  • Type: infrastructure
  • Affected jobs: 103938963282
  • Related to change: no
  • Root cause: The sonar-maven-plugin failed to bootstrap because the target server endpoint returned an HTTP 404 Not Found error when querying the server version.
  • Suggested fix: Verify the SonarQube server URL configuration, check that the target server is up and accessible, and ensure correct properties are set in the workflow/environment.

Summary

  • Change-related failures: 0
  • Infrastructure/flaky failures: 1 (SonarQube scanner connection/endpoint failure)
  • Recommended action: Check the SonarQube server status and configuration URLs in the CI environment.
Code Review 👍 Approved with suggestions 3 resolved / 5 findings

Adds comprehensive remediation metadata validation for issue gaps across both modern and deprecated check verifiers, with metadata caching and support for legacy resource naming. The implementation correctly handles linear gaps omitting cost properties and validates gaps through analyzer-commons expectations.

Consider extracting the duplicate rule_metadata_is_available_on_test_classpath test body from the two rule test classes into a shared helper so future changes to the invariant need only be made in one place. Additionally, pair each input in valid_remediation with its expected return value to verify that readFunction returns the correct function name for each remediation type, not just that the inputs are in the valid set.

💡 Quality: Repo-wide metadata guard duplicated inside two rule test classes

📄 java-checks/src/test/java/org/sonar/java/checks/SunPackagesUsedCheckTest.java:30-42 📄 java-checks-aws/src/test/java/org/sonar/java/checks/aws/AwsLongTermAccessKeysCheckTest.java:30-42

The new rule_metadata_is_available_on_test_classpath test is a module-wide invariant (it discovers every annotated top-level check under org.sonar.java.checks and asserts its <ruleKey>.json is on the test classpath), but it is copy-pasted verbatim into two unrelated single-rule test classes, so a future change to the invariant has to be made in two places and the guard is invisible to anyone looking for it. Since both modules already put ../sonar-java-plugin/src/main/resources on the test classpath (java-checks/pom.xml:104, java-checks-aws/pom.xml:90), the shared body belongs in one helper (e.g. in java-checks-testkit next to RuleMetadataValidator, or a dedicated RuleMetadataClasspathTest per module) that both modules call with their base package.

Extract the guard into a shared testkit helper and call it from one dedicated test class per check module.
// java-checks-testkit: new public helper, e.g. org.sonar.java.checks.verifier.RuleMetadataAssert
public static void assertMetadataAvailable(ClassLoader loader, String basePackage) throws IOException {
  var rules = ClassPath.from(loader).getTopLevelClassesRecursive(basePackage).stream()
    .map(ClassPath.ClassInfo::load)
    .filter(check -> check.isAnnotationPresent(Rule.class))
    .toList();
  assertThat(rules).isNotEmpty();
  for (Class<?> rule : rules) {
    String key = rule.getAnnotation(Rule.class).key();
    assertThat(loader.getResource("org/sonar/l10n/java/rules/java/" + key + ".json"))
      .as("Remediation metadata for %s (%s)", key, rule.getName()).isNotNull();
  }
}
// then each module keeps a one-line dedicated test calling this helper
💡 Quality: valid_remediation test cannot detect a wrong function mapping

📄 java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/RuleMetadataValidatorTest.java:34-43

valid_remediation asserts only isIn("Constant/Issue", "Linear", "Linear with offset"), so it passes for any of the three inputs even if readFunction returned the wrong function name (e.g. returning "Linear" for the Constant/Issue input), which is exactly the mapping the validate gap rejection depends on. Pair each input with its expected return value so the mapping itself is covered.

Assert the exact function returned for each metadata input.
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
  "{"func":"Constant/Issue","constantCost":"5min"} | Constant/Issue",
  "{"func":"Linear","linearFactor":"2h"} | Linear",
  "{"func":"Linear with offset","linearFactor":"1min","linearOffset":"0min"} | Linear with offset"
})
void valid_remediation(String remediation, String expectedFunction) {
  assertThat(RuleMetadataValidator.readFunction(new StringReader("{"remediation":" + remediation + "}"), "Test"))
    .isEqualTo(expectedFunction);
}
✅ 3 resolved
Performance: Rule metadata re-read and re-parsed for every reported issue

📄 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/RuleMetadataValidator.java:37-51 📄 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java:158 📄 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/JavaCheckVerifier.java:402
RuleMetadataValidator.validate is invoked once per AnalyzerMessage (JavaCheckVerifier.java:158 and :402), and load performs a classloader resource lookup, opens a stream and builds a fresh Gson instance on every call. A single check test file that raises hundreds of issues (e.g. the large S1192/S1481 test sources in java-checks) therefore re-reads and re-parses the same <ruleKey>.json hundreds of times, and the java-checks suite runs thousands of such verifications. Cache the parsed result per rule key (e.g. a static Map<String, Optional<RuleMetadata>> and a single shared Gson) so each metadata file is read at most once per JVM.

Quality: Validation silently no-ops if metadata copy stops resolving

📄 java-checks-testkit/src/main/java/org/sonar/java/checks/verifier/internal/RuleMetadataValidator.java:55-63 📄 java-checks/pom.xml:104-114 📄 java-checks-aws/pom.xml:91-101
The guard only fires when load finds <ruleKey>.json on the classpath, and the only thing putting SonarJava's metadata there is the new <testResource> pointing at the sibling module source dir ../sonar-java-plugin/src/main/resources (java-checks/pom.xml:108-113, java-checks-aws/pom.xml:95-100). If those metadata files are ever moved or renamed within sonar-java-plugin, every lookup returns null, the validator becomes a no-op for all built-in rules, and no test fails — the S1191-style regression this PR targets would silently become undetectable again. Add a test in java-checks (or java-checks-aws) asserting that metadata for a known rule key is resolvable from the test classpath, so a broken resource wiring fails loudly.

Quality: Test resources reach across modules into a sibling source tree

📄 java-checks/pom.xml:104-114 📄 java-checks-aws/pom.xml:91-101
Both check modules now declare <directory>../sonar-java-plugin/src/main/resources</directory> as a test resource, i.e. java-checks/java-checks-aws consume the source tree of a module that depends on them (sonar-java-plugin depends on java-checks, not the reverse). Maven silently skips a missing resource directory, so any build that does not have the full multi-module checkout laid out with that exact relative path (single-module builds from a partial source tarball, or a future module relocation) produces empty metadata and silently disables the new validation instead of failing. Prefer packaging the metadata into an artifact the check modules can depend on (e.g. a test-jar of the module owning the JSON files) so the dependency is resolved by Maven rather than by a relative path.

🤖 Prompt for agents
Code Review: Adds comprehensive remediation metadata validation for issue gaps across both modern and deprecated check verifiers, with metadata caching and support for legacy resource naming. The implementation correctly handles linear gaps omitting cost properties and validates gaps through analyzer-commons expectations.
  
  Consider extracting the duplicate `rule_metadata_is_available_on_test_classpath` test body from the two rule test classes into a shared helper so future changes to the invariant need only be made in one place. Additionally, pair each input in `valid_remediation` with its expected return value to verify that `readFunction` returns the correct function name for each remediation type, not just that the inputs are in the valid set.

1. 💡 Quality: Repo-wide metadata guard duplicated inside two rule test classes
   Files: java-checks/src/test/java/org/sonar/java/checks/SunPackagesUsedCheckTest.java:30-42, java-checks-aws/src/test/java/org/sonar/java/checks/aws/AwsLongTermAccessKeysCheckTest.java:30-42

   The new `rule_metadata_is_available_on_test_classpath` test is a module-wide invariant (it discovers every annotated top-level check under `org.sonar.java.checks` and asserts its `<ruleKey>.json` is on the test classpath), but it is copy-pasted verbatim into two unrelated single-rule test classes, so a future change to the invariant has to be made in two places and the guard is invisible to anyone looking for it. Since both modules already put `../sonar-java-plugin/src/main/resources` on the test classpath (java-checks/pom.xml:104, java-checks-aws/pom.xml:90), the shared body belongs in one helper (e.g. in java-checks-testkit next to `RuleMetadataValidator`, or a dedicated `RuleMetadataClasspathTest` per module) that both modules call with their base package.

   Fix (Extract the guard into a shared testkit helper and call it from one dedicated test class per check module.):
   // java-checks-testkit: new public helper, e.g. org.sonar.java.checks.verifier.RuleMetadataAssert
   public static void assertMetadataAvailable(ClassLoader loader, String basePackage) throws IOException {
     var rules = ClassPath.from(loader).getTopLevelClassesRecursive(basePackage).stream()
       .map(ClassPath.ClassInfo::load)
       .filter(check -> check.isAnnotationPresent(Rule.class))
       .toList();
     assertThat(rules).isNotEmpty();
     for (Class<?> rule : rules) {
       String key = rule.getAnnotation(Rule.class).key();
       assertThat(loader.getResource("org/sonar/l10n/java/rules/java/" + key + ".json"))
         .as("Remediation metadata for %s (%s)", key, rule.getName()).isNotNull();
     }
   }
   // then each module keeps a one-line dedicated test calling this helper

2. 💡 Quality: valid_remediation test cannot detect a wrong function mapping
   Files: java-checks-testkit/src/test/java/org/sonar/java/checks/verifier/internal/RuleMetadataValidatorTest.java:34-43

   `valid_remediation` asserts only `isIn("Constant/Issue", "Linear", "Linear with offset")`, so it passes for any of the three inputs even if `readFunction` returned the wrong function name (e.g. returning "Linear" for the `Constant/Issue` input), which is exactly the mapping the `validate` gap rejection depends on. Pair each input with its expected return value so the mapping itself is covered.

   Fix (Assert the exact function returned for each metadata input.):
   @ParameterizedTest
   @CsvSource(delimiter = '|', value = {
     "{"func":"Constant/Issue","constantCost":"5min"} | Constant/Issue",
     "{"func":"Linear","linearFactor":"2h"} | Linear",
     "{"func":"Linear with offset","linearFactor":"1min","linearOffset":"0min"} | Linear with offset"
   })
   void valid_remediation(String remediation, String expectedFunction) {
     assertThat(RuleMetadataValidator.readFunction(new StringReader("{"remediation":" + remediation + "}"), "Test"))
       .isEqualTo(expectedFunction);
   }

Review coverage

Functional validation No results

Rules No rules evaluated

Auto-approval Not enabled · Set up

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

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

@nathsou

nathsou commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of https://github.com/SonarSource/rspec/pull/8170

@nathsou nathsou closed this Sep 15, 2026
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.

1 participant