Skip to content

SONARJAVA-7290 False negative java:S1854: a dead store inside a lambda body is not reported - #6462

Open
david-schroeter wants to merge 7 commits into
masterfrom
ds/SONARJAVA-7290
Open

david-schroeter wants to merge 7 commits into
masterfrom
ds/SONARJAVA-7290

Conversation

@david-schroeter

@david-schroeter david-schroeter commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary by Gitar

  • Lambda support:
    • Extended DeadStoreCheck to analyze lambda expression bodies, not only methods
    • Treated values discarded by void expression lambdas like expression statements via isExpressionValueDiscarded
  • Assignment handling:
    • Reported unused prefix and compound assignments in discarded lambda positions
    • Removed the expression-statement restriction so unused assignments in arguments, conditions, and returns are reported
  • Tests:
    • Added lambda dead store regression samples to DeadStoreCheckSample.java
    • Added non-compiling lambda cases for unresolved identifiers
  • Try handling:
    • Stopped TryVisitor from descending into lambda bodies so nested try-finally blocks don't exclude the enclosing scope
  • Constructor support:
    • Added CONSTRUCTOR to visited nodes so dead stores in constructors are analyzed

This will update automatically on new commits.

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

SONARJAVA-7290

@david-schroeter

Copy link
Copy Markdown
Contributor Author

/pvf

@hashicorp-vault-sonar-prod

Copy link
Copy Markdown
Contributor

📊 Performance Validation complete
🆕 104 new · 🗑️ 0 lost issues
View dashboard

Projects with unexpected PVF results:

  • crate

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

I would remove the exception when the return value of the assignation is used, but the assignation itself is not. The rspec doesn't mention any such exception

Comment on lines 179 to 182
private static boolean isVoidLambda(Tree tree) {
// A void expression lambda discards its body's value without an enclosing expression statement.
return tree instanceof LambdaExpressionTree lambda && lambda.symbol().returnType().type().isVoid();
}

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.

i struggle to understand why only void lambdas are considered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

isExpressionValueDiscarded checks whether the expression’s result is discarded.
For a standalone statement such as ++x;, the parent is an EXPRESSION_STATEMENT, so the result is discarded.
In an expression-bodied lambda such as IntConsumer consumer = x -> ++x, the parent of ++x is the lambda itself. There is no expression-statement wrapper, so we inspect the lambda’s return type:

  • For a void lambda, the result is discarded.
  • For a value-returning lambda, such as IntUnaryOperator operator = x -> ++x, the result is returned and therefore used.
    That is why this helper considers only void lambdas. Reporting still depends on the other checks, including whether the updated local variable is read afterward.

@david-schroeter

Copy link
Copy Markdown
Contributor Author

/pvf

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@david-schroeter ❌ Your /pvf command was not dispatched. See the failed run.

No candidate-version artifact for aaa7daf; wait for Build to finish

@datadog-sonarsource

This comment has been minimized.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

⚖️ Ruling update ready for review: #6466

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

⚖️ Ruling update ready for review: #6466

@sonarqube-next

sonarqube-next Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

⚖️ Ruling update ready for review: #6466

@david-schroeter

Copy link
Copy Markdown
Contributor Author

@rombirli

After testing chained assignments, I broadened the reporting to make ordinary assignments, compound assignments, and prefix updates consistent: we now report when the updated local variable is never subsequently read, even if the expression’s result is consumed.

Preserving existing behavior means continuing to report:

int foo(int x) {
  return x = x + 2;
}

The same unused-store reasoning applies to:

int foo(int x) {
  return x += 2;
}

Both expressions return a useful result, but storing that result in x is unnecessary. Both can become return x + 2;.

Exempting assignments whose results are consumed would also remove existing findings such as:

void example(BufferedReader reader) throws IOException {
  String line;
  if ((line = reader.readLine()) != null) {
    System.out.println("Line available");
  }
}

The comparison uses the assignment expression’s result, but the store to line is unnecessary: the condition could directly compare reader.readLine() with null.

This extension therefore seems aligned with S1854’s description of unused assignments, although it deliberately adds findings beyond the lambda fix.

These equivalent forms still remain compliant because they explicitly read the updated variable:

x += 2;
return x;
x = x + 2;
return x;

Suggesting return x + 2; for those cases could be a separate simplification concern.

Does this consistency improvement make sense within this PR, or would you prefer preserving the existing reporting policy and handling the extension separately?

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@gitar-bot

gitar-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 closed / 2 findings

🟡 Medium risk · Extending S1854 dead-store analysis to lambdas, constructors, and discarded assignments could produce new false-positive findings in analyzed Java code.

Extends S1854 dead store detection to lambda bodies and adds CONSTRUCTOR visiting so dead stores in constructors are analyzed. Plain assignments used as expressions are no longer reported in methods, and the missing constructor test coverage finding is addressed by a reproducer example.

✅ 2 closed
✅ Bug: Plain assignments used as expressions are no longer reported in methods

📄 java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java:159-163 📄 java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java:79-84 📄 java-checks-test-sources/default/src/main/java/checks/DeadStoreCheckSample.java:114-117
Before this commit, a plain ASSIGNMENT was reported whenever its target was dead afterwards (element.is(Tree.Kind.ASSIGNMENT) || ...). Now it has to pass isExpressionValueDiscarded, so it is only reported when its parent is an expression statement or a void lambda. That change applies to every method, not only to lambdas. Code like String line; if ((line = r.readLine()) != null) { log(); } or a = b = 0; use(a);, where line/b is never read again, was flagged before and is now a new false negative. The store to the variable is still useless even though the expression's value is used, because it could be rewritten as if (r.readLine() != null) or a = 0. The new sample int y = x = x + 1; return y;, marked as compliant, is such a dead store to x (it is equivalent to int y = x + 1). The PR is meant to fix a lambda false negative, but this commit removes true positives for existing method code, no test covers that, and the ruling expectations were not updated. Restore the unconditional check for plain ASSIGNMENT and limit the new parent check to compound assignments, as before.

✅ Quality: New CONSTRUCTOR visiting has no test coverage

📄 java-checks/src/main/java/org/sonar/java/checks/DeadStoreCheck.java:57
This commit adds Tree.Kind.CONSTRUCTOR to nodesToVisit(). On master only METHOD was visited, so this widens the rule to all constructor bodies and will raise new issues on existing code. None of the sample files (DeadStoreCheckSample.java default and non-compiling, DeadStoreCheckBrokenSemantic.java) contain a constructor, so DeadStoreCheckTest never exercises this path. A regression that skipped constructors again, or that mis-handled them (for example field-vs-local handling or this(...)/super(...) calls), would go unnoticed. Add a constructor with a // Noncompliant dead store and a compliant case to the sample.

Verification Rules 1/1 met Repos 4 inspected · PRs 1 inspected

Functional validation No results

Rules 1 passed 2 not applicable

✅ Repository license compliance

Broader impact Repos 4 inspected PRs 1 inspected
Cross-repo 4 repositories inspected 0 open impacts

SonarSource/sonarqube Inspected
SonarSource/sonarlint-vscode Inspected
SonarSource/sonarlint-intellij Inspected
SonarSource/sonar-java-symbolic-execution Inspected

Cross-PR 1 PR inspected 0 open impacts

PR 6449: SONARJAVA-7283 Decouple the frontend AST from Eclipse JDT core Inspected

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

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

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