Skip to content

SONARJAVA-7227 Preserve casts documenting potential precision loss in S1905 - #6391

Open
nathsou wants to merge 1 commit into
masterfrom
nathan/s1905-precision-casts
Open

nathsou wants to merge 1 commit into
masterfrom
nathan/s1905-precision-casts

Conversation

@nathsou

@nathsou nathsou commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

S1905 reports the explicit casts recommended by S9395, so following one rule's remediation produces an issue from the other. Preserve casts from int to float, long to float, and long to double: these document potentially lossy widening conversions even when the compiler considers the casts redundant.

The exception uses the operand and cast types independently of whether S9395 is enabled. Regression samples cover initializers, assignments, compound assignments, method and constructor arguments, returns, and parentheses, while retaining reports for identity and lossless widening casts.

Community report: https://community.sonarsource.com/t/java-s9395-clashes-with-java-s1905/189375
RSPEC: https://github.com/SonarSource/rspec/pull/8386

Validation: the new samples produce 21 unwanted findings before the fix. After the fix, all 5 focused tests pass, including the existing runs without semantic analysis:

mvn -B -pl java-checks -am test -Dtest=RedundantTypeCastCheckTest,S9395CheckTest -Dsurefire.failIfNoSpecifiedTests=false

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

SONARJAVA-7227

@nathsou
nathsou requested a review from rombirli October 22, 2026 14:26
@nathsou nathsou self-assigned this Oct 22, 2026
@sonarqube-next

Copy link
Copy Markdown
Contributor

@datadog-sonarsource

datadog-sonarsource Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Pipelines

❌ Errors

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

🚦 4 Pipeline jobs failed

Build | Ruling QA (warp-custom-ubuntu-24-04, only-sonarqube-project, LATEST_RELEASE) — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Build | Ruling QA (warp-custom-ubuntu-24-04, without-sonarqube-project, LATEST_RELEASE) — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

Build | Ruling QA (warp-custom-windows-2022-m, only-sonarqube-project, LATEST_RELEASE) — 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

View all 4 failed jobs.

Useful? React with 👍 / 👎

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

@github-actions

Copy link
Copy Markdown
Contributor

⚖️ Ruling update ready for review: #6392

@github-actions

Copy link
Copy Markdown
Contributor

Ruling Diff Summary

Detected changes in 2 rule files: 4 issues removed, 0 issues added.

S1905 (java) on guava - 3 issues removed, 0 issues added

Removed src/com/google/common/cache/LongAdder.java (line 186)

       181 |     /**
       182 |      * Returns the {@link #sum} as a {@code float}
       183 |      * after a widening primitive conversion.
       184 |      */
       185 |     public float floatValue() {
>>>    186 |         return (float)sum();
       187 |     }
       188 | 
       189 |     /**
       190 |      * Returns the {@link #sum} as a {@code double} after a widening
       191 |      * primitive conversion.

Removed src/com/google/common/cache/LongAdder.java (line 194)

       189 |     /**
       190 |      * Returns the {@link #sum} as a {@code double} after a widening
       191 |      * primitive conversion.
       192 |      */
       193 |     public double doubleValue() {
>>>    194 |         return (double)sum();
       195 |     }
       196 | 
       197 |     private void writeObject(ObjectOutputStream s) throws IOException {
       198 |         s.defaultWriteObject();
       199 |         s.writeLong(sum());

Removed src/com/google/common/collect/Maps.java (line 200)

       195 |     }
       196 |     if (expectedSize < Ints.MAX_POWER_OF_TWO) {
       197 |       // This is the calculation used in JDK8 to resize when a putAll
       198 |       // happens; it seems to be the most conservative calculation we
       199 |       // can make.  0.75 is the default load factor.
>>>    200 |       return (int) ((float) expectedSize / 0.75F + 1.0F);
       201 |     }
       202 |     return Integer.MAX_VALUE; // any large value
       203 |   }
       204 | 
       205 |   /**
S1905 (java) on sonarqube-6.5 - 1 issues removed, 0 issues added

Removed src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java (line 122)

(source file not found at this revision: src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java)

@gitar-bot

gitar-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
CI failed: Ruling QA jobs report S1905 ruling diffs versus expected baselines, likely caused by this PR's preserved precision-loss casts; the stacked PR #6392 updates the expected results, but the logs shown do not include the actual mismatch entries.

Overview

Four unique log templates were analyzed (4 logs total) for commit bb825b8. All visible failure signals come from Ruling QA jobs that generate a diff between expected and actual S1905 ruling results. The excerpts are truncated and do not include the comparison result itself, so the mismatch is inferred from job context and the stacked PR rather than confirmed by an explicit error line.

Failures

S1905 ruling results differ from expected baseline (confidence: medium)

  • Type: test
  • Affected jobs: 119882647742 (Ruling QA, warp-custom-ubuntu-24-04, without-sonarqube-project, LATEST_RELEASE); 119882647879 (Ruling QA, warp-custom-windows-2022-l, without-sonarqube-project); plus one additional Ruling QA log (warp-custom-ubuntu-24-04, only-sonarqube-project)
  • Related to change: yes
  • Root cause: This PR changes S1905 to preserve casts documenting potentially lossy widening conversions, which can change ruling output. The expected S1905 files (its/ruling/src/test/resources/expected/guava/java-S1905.json and its/ruling/src/test/resources/expected/sonarqube-6.5/java-S1905.json) are not updated in this PR; the stacked PR Update ruling results for PR #6391 #6392 updates them. The logs show the diff report being generated and uploaded (diff_warp-custom-ubuntu-24-04_without-sonarqube-project, diff_warp-custom-ubuntu-24-04_only-sonarqube-project), but the diff contents and the failing assertion are not visible in the excerpt.
  • Suggested fix: Confirm the S1905 diff contents in the uploaded diff artifacts. Update the expected S1905 ruling results as done in PR Update ruling results for PR #6391 #6392 (or include those updates in this PR), then rerun Ruling QA to verify expected and actual match.

Summary

  • Change-related failures: 1 failure group (S1905 ruling mismatch across Ruling QA jobs), attributed to this PR's behavior change with medium confidence.
  • Infrastructure/flaky failures: None identified in the provided findings.
  • Recommended action: Inspect the uploaded S1905 diff artifacts to confirm the exact mismatched entries, then update the expected ruling files (stacked PR Update ruling results for PR #6391 #6392 covers this) and rerun Ruling QA.
Code Review 👍 Approved with suggestions 0 closed / 1 findings

🟡 Medium risk · Preserving these casts may leave unnecessary conversions unreported by S1905, though the exception is limited to three widening pairs.

Preserves explicit casts from int to float and long to float/double in S1905 so they no longer clash with S9395's remediation. One open suggestion: the exemption also hides redundant casts of exactly representable constants (such as float f = (float) 1;), which S9395 never flags, so the same safe-constant exclusion should be applied here with a matching Noncompliant sample.

💡 Edge Case: Exemption also hides redundant casts of exactly representable constants

📄 java-checks/src/main/java/org/sonar/java/checks/RedundantTypeCastCheck.java:98-104

isPotentiallyLossyWideningConversion only looks at the operand and target types. That means any int→float or long→float/double cast is now exempt, including casts of compile-time constants whose values are exactly representable, such as float f = (float) 0; or double d = (double) 1L;. S9395 never asks for those casts: S9395Check.isSafeConstant (S9395Check.java:156-171) skips integer constants with |v| ≤ 2^24 for float and long constants with |v| ≤ 2^53 for double. So these casts are still truly redundant, and S1905 stops reporting them without any rule conflict to justify it. The conversion can't lose precision there, so the cast documents nothing. To fix this, apply the same safe-constant exclusion here: keep reporting when ExpressionUtils.resolveAsConstant(tree.expression()) gives a value inside the exact range. The simplest way is to share S9395's isLossyWideningConversion/isSafeConstant logic. Also add a Noncompliant sample like float f = (float) 1;.

Skip the exemption for constants that convert exactly, matching S9395
private static boolean isPotentiallyLossyWideningConversion(TypeCastTree tree) {
  Type sourceType = tree.expression().symbolType();
  Type targetType = tree.type().symbolType();
  boolean lossyTypes = (sourceType.isPrimitive(Type.Primitives.INT) && targetType.isPrimitive(Type.Primitives.FLOAT))
    || (sourceType.isPrimitive(Type.Primitives.LONG)
      && (targetType.isPrimitive(Type.Primitives.FLOAT) || targetType.isPrimitive(Type.Primitives.DOUBLE)));
  if (!lossyTypes) {
    return false;
  }
  Object constant = ExpressionUtils.resolveAsConstant(ExpressionUtils.skipParentheses(tree.expression()));
  if (constant instanceof Number n) {
    long abs = Math.abs(n.longValue());
    return targetType.isPrimitive(Type.Primitives.FLOAT) ? abs > (1L << 24) : abs > (1L << 53);
  }
  return true;
}
🤖 Prompt for agents
Code Review: Preserves explicit casts from `int` to `float` and `long` to `float`/`double` in S1905 so they no longer clash with S9395's remediation. One open suggestion: the exemption also hides redundant casts of exactly representable constants (such as `float f = (float) 1;`), which S9395 never flags, so the same safe-constant exclusion should be applied here with a matching Noncompliant sample.

1. 💡 Edge Case: Exemption also hides redundant casts of exactly representable constants
   Files: java-checks/src/main/java/org/sonar/java/checks/RedundantTypeCastCheck.java:98-104

   `isPotentiallyLossyWideningConversion` only looks at the operand and target types. That means any `int`→`float` or `long`→`float`/`double` cast is now exempt, including casts of compile-time constants whose values are exactly representable, such as `float f = (float) 0;` or `double d = (double) 1L;`. S9395 never asks for those casts: `S9395Check.isSafeConstant` (S9395Check.java:156-171) skips integer constants with |v| ≤ 2^24 for `float` and long constants with |v| ≤ 2^53 for `double`. So these casts are still truly redundant, and S1905 stops reporting them without any rule conflict to justify it. The conversion can't lose precision there, so the cast documents nothing. To fix this, apply the same safe-constant exclusion here: keep reporting when `ExpressionUtils.resolveAsConstant(tree.expression())` gives a value inside the exact range. The simplest way is to share S9395's `isLossyWideningConversion`/`isSafeConstant` logic. Also add a Noncompliant sample like `float f = (float) 1;`.

   Fix (Skip the exemption for constants that convert exactly, matching S9395):
   private static boolean isPotentiallyLossyWideningConversion(TypeCastTree tree) {
     Type sourceType = tree.expression().symbolType();
     Type targetType = tree.type().symbolType();
     boolean lossyTypes = (sourceType.isPrimitive(Type.Primitives.INT) && targetType.isPrimitive(Type.Primitives.FLOAT))
       || (sourceType.isPrimitive(Type.Primitives.LONG)
         && (targetType.isPrimitive(Type.Primitives.FLOAT) || targetType.isPrimitive(Type.Primitives.DOUBLE)));
     if (!lossyTypes) {
       return false;
     }
     Object constant = ExpressionUtils.resolveAsConstant(ExpressionUtils.skipParentheses(tree.expression()));
     if (constant instanceof Number n) {
       long abs = Math.abs(n.longValue());
       return targetType.isPrimitive(Type.Primitives.FLOAT) ? abs > (1L << 24) : abs > (1L << 53);
     }
     return true;
   }

Verification Rules 1/2 met Repos 5 inspected
Rules 1 failed 1 passed 1 not applicable

❌ Repository license compliance - LICENSE.txt contains SSALv1 text, but the central exception for sonar-java requires LGPL-3.0; the file contains no LGPL text and no local override exists. This predates the PR, and the PR does not change licensing.
✅ Internal artifact registries

Functional validation No results

Broader impact Repos 5 inspected
Cross-repo 5 repositories inspected 0 open impacts

SonarSource/sonarlint-intellij Inspected
SonarSource/sonarlint-language-server Inspected
SonarSource/sonarlint-vscode Inspected
SonarSource/sonar-java-symbolic-execution Inspected
SonarSource/sonar-lits Inspected

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

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 → Showing compact verification results.

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

Comment on lines +98 to +104
private static boolean isPotentiallyLossyWideningConversion(TypeCastTree tree) {
Type sourceType = tree.expression().symbolType();
Type targetType = tree.type().symbolType();
return (sourceType.isPrimitive(Type.Primitives.INT) && targetType.isPrimitive(Type.Primitives.FLOAT))
|| (sourceType.isPrimitive(Type.Primitives.LONG)
&& (targetType.isPrimitive(Type.Primitives.FLOAT) || targetType.isPrimitive(Type.Primitives.DOUBLE)));
}

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: Exemption also hides redundant casts of exactly representable constants

isPotentiallyLossyWideningConversion only looks at the operand and target types. That means any int→float or long→float/double cast is now exempt, including casts of compile-time constants whose values are exactly representable, such as float f = (float) 0; or double d = (double) 1L;. S9395 never asks for those casts: S9395Check.isSafeConstant (S9395Check.java:156-171) skips integer constants with |v| ≤ 2^24 for float and long constants with |v| ≤ 2^53 for double. So these casts are still truly redundant, and S1905 stops reporting them without any rule conflict to justify it. The conversion can't lose precision there, so the cast documents nothing. To fix this, apply the same safe-constant exclusion here: keep reporting when ExpressionUtils.resolveAsConstant(tree.expression()) gives a value inside the exact range. The simplest way is to share S9395's isLossyWideningConversion/isSafeConstant logic. Also add a Noncompliant sample like float f = (float) 1;.

Skip the exemption for constants that convert exactly, matching S9395:

private static boolean isPotentiallyLossyWideningConversion(TypeCastTree tree) {
  Type sourceType = tree.expression().symbolType();
  Type targetType = tree.type().symbolType();
  boolean lossyTypes = (sourceType.isPrimitive(Type.Primitives.INT) && targetType.isPrimitive(Type.Primitives.FLOAT))
    || (sourceType.isPrimitive(Type.Primitives.LONG)
      && (targetType.isPrimitive(Type.Primitives.FLOAT) || targetType.isPrimitive(Type.Primitives.DOUBLE)));
  if (!lossyTypes) {
    return false;
  }
  Object constant = ExpressionUtils.resolveAsConstant(ExpressionUtils.skipParentheses(tree.expression()));
  if (constant instanceof Number n) {
    long abs = Math.abs(n.longValue());
    return targetType.isPrimitive(Type.Primitives.FLOAT) ? abs > (1L << 24) : abs > (1L << 53);
  }
  return true;
}
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

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.

this looks correct to me

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I checked RedundantTypeCastCheck.java:98-104 against S9395Check.isSafeConstant (lines 156-172), and the finding still holds.

isPotentiallyLossyWideningConversion looks only at the source and target types. A cast like (float) 0 or (double) 1L is therefore exempted from S1905, even though S9395 would not ask for it: isSafeConstant treats those constants as exactly representable (|v| ≤ 2^24 for float, ≤ 2^53 for double). So S1905 stops reporting casts that really are redundant and S9395 doesn't require.

If you want to keep this as is, the trade-off is losing those true positives. If you'd rather fix it, I can apply the same constant check here and add a float f = (float) 1; // Noncompliant sample. Reply "fix this" and I'll make the change.

@rombirli rombirli 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.

Please fix ruling.
Also gitar comment seems correct to me, see S9335, these max values are taken into account

@rombirli

Copy link
Copy Markdown
Contributor

Also gitar comment seems correct to me, see S9335, these max values are taken into account

BTW i would find it better if some parts of S9335 were refactored into helpers to avoid code duplication

This branch has not been deployed

No deployments
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