Skip to content

Commit c8ef9f9

Browse files
SONARJAVA-5485 S1481: Report on unnamed variables in try with resources (#5090)
Co-authored-by: erwan-serandour <erwan.serandour@sonarsource.com>
1 parent 002039f commit c8ef9f9

6 files changed

Lines changed: 99 additions & 16 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"ruleKey": "S1481",
33
"hasTruePositives": true,
4-
"falseNegatives": 7,
4+
"falseNegatives": 10,
55
"falsePositives": 0
6-
}
6+
}

java-checks-test-sources/default/src/main/java/checks/unused/UnusedLocalVariableCheck.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ public void f(int unusedParameter, Object o) {
4141
} catch (Exception e) {
4242
}
4343

44-
try (Stream foo = Stream.of()) { // Compliant
44+
try (Stream stream1 = Stream.of(); // Noncompliant {{Remove this unused "stream1" local variable.}} [[quickfixes=qf_tr1]]
45+
// ^^^^^^^
46+
// fix@qf_tr1 {{Replace unused local variable with _}}
47+
// edit@qf_tr1 [[sc=10;ec=24]] {{var _}}
48+
Stream _ = Stream.of();
49+
Stream stream3 = Stream.of()) { // Noncompliant {{Remove this unused "stream3" local variable.}} [[quickfixes=qf_tr2]]
50+
// ^^^^^^^
51+
// fix@qf_tr2 {{Replace unused local variable with _}}
52+
// edit@qf_tr2 [[sc=10;ec=24]] {{var _}}
4553
} catch (Exception _) {
4654
}
4755

java-checks-test-sources/default/src/main/java/checks/unused/UnusedLocalVariableCheck_java22.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package checks.unused;
22

3+
import java.util.stream.Stream;
4+
35
public class UnusedLocalVariableCheck_java22 {
46
private UnusedLocalVariableCheck_java22() {}
57

@@ -21,4 +23,18 @@ public static int count(int[] elements) {
2123

2224
return count;
2325
}
26+
27+
public static void tryWithResources() {
28+
try (Stream foo = Stream.of()) { // Noncompliant {{Remove this unused "foo" local variable.}} [[quickfixes=qf_tr1]]
29+
// ^^^
30+
// fix@qf_tr1 {{Replace unused local variable with _}}
31+
// edit@qf_tr1 [[sc=10;ec=20]] {{var _}}
32+
} catch (Exception e) {
33+
}
34+
35+
Stream bar = Stream.of();
36+
try(bar) {
37+
} catch (Exception e) {
38+
}
39+
}
2440
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package checks.unused;
2+
3+
4+
public class UnusedLocalVariableCheck_withLambda {
5+
6+
public void subjectTryWithResources() {
7+
try (org.assertj.core.api.AutoCloseableSoftAssertions softlyTry = new org.assertj.core.api.AutoCloseableSoftAssertions()) { // Noncompliant
8+
}
9+
}
10+
11+
public void subjectEnhancedFor() {
12+
for (org.assertj.core.api.AutoCloseableSoftAssertions softlyFor : new org.assertj.core.api.AutoCloseableSoftAssertions[]{}) { // Noncompliant
13+
}
14+
}
15+
16+
// Commenting out decoy methods (or renaming the arguments in lambdas)
17+
// changes the outcome of the tests without semantics.
18+
19+
public void decoyTryWithResources() {
20+
org.assertj.core.api.SoftAssertions.assertSoftly(softlyTry -> {
21+
softlyTry.assertThat(5).isLessThan(3);
22+
});
23+
}
24+
25+
public void decoyEnhancedFor() {
26+
org.assertj.core.api.SoftAssertions.assertSoftly(softlyFor -> {
27+
softlyFor.assertThat(5).isLessThan(3);
28+
});
29+
}
30+
}

java-checks/src/main/java/org/sonar/java/checks/unused/UnusedLocalVariableCheck.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.sonar.java.reporting.JavaQuickFix;
3030
import org.sonar.java.reporting.JavaTextEdit;
3131
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
32-
import org.sonar.plugins.java.api.JavaVersion;
3332
import org.sonar.plugins.java.api.semantic.Symbol;
3433
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
3534
import org.sonar.plugins.java.api.tree.CaseLabelTree;
@@ -76,7 +75,7 @@ public void leaveNode(Tree tree) {
7675
IdentifierTree simpleName = variable.simpleName();
7776
if (!simpleName.isUnnamedVariable()) {
7877
boolean unresolved = UNRESOLVED_IDENTIFIERS_AND_SWITCH_CASE_VISITOR.isUnresolved(simpleName.name());
79-
if (!unresolved && isProperLocalVariable(variable) && isUnused(variable.symbol()) && !isMandatoryForeachVariable(context.getJavaVersion(), variable)) {
78+
if (!unresolved && isProperLocalVariable(variable) && isUnused(variable.symbol()) && canBeReplaced(variable)) {
8079
QuickFixHelper.newIssue(context)
8180
.forRule(this)
8281
.onTree(simpleName)
@@ -90,11 +89,12 @@ public void leaveNode(Tree tree) {
9089
}
9190

9291
/**
93-
* Before Java 22 it was not possible to remove the variable in a foreach statement even it is unused.
92+
* Before Java 22 it was not possible to remove the variable in a foreach statement or try with resources even it is unused.
9493
* For instance in {@code for (String element : list) {}}, it is only since Java 22 that it can be rewritten {@code for (var _ : list) {}}.
9594
*/
96-
private static boolean isMandatoryForeachVariable(JavaVersion javaVersion, VariableTree variable) {
97-
return variable.parent() instanceof ForEachStatement && !javaVersion.isJava22Compatible();
95+
private boolean canBeReplaced(VariableTree variable) {
96+
return context.getJavaVersion().isJava22Compatible()
97+
|| (!isForeachVariable(variable) && !isTryResource(variable));
9898
}
9999

100100
private static boolean isUnused(Symbol symbol) {
@@ -119,7 +119,6 @@ private static boolean isProperLocalVariable(VariableTree variable) {
119119
return symbol.isLocalVariable()
120120
&& !symbol.isParameter()
121121
&& !isDefinedInCatchClause(variable)
122-
&& !isTryResource(variable)
123122
&& !UNRESOLVED_IDENTIFIERS_AND_SWITCH_CASE_VISITOR.isSwitchPatternVariable(variable);
124123
}
125124

@@ -131,9 +130,13 @@ private static boolean isTryResource(VariableTree variable) {
131130
return variable.parent().is(Tree.Kind.LIST) && variable.parent().parent().is(Tree.Kind.TRY_STATEMENT);
132131
}
133132

133+
private static boolean isForeachVariable(VariableTree variable) {
134+
return variable.parent() instanceof ForEachStatement;
135+
}
136+
134137
private static List<JavaQuickFix> computeQuickFix(VariableTree variable) {
135-
if (variable.parent() instanceof ForEachStatement) {
136-
return makeQuickFixReplacingWithUnnamedVariable(variable);
138+
if (isForeachVariable(variable) || isTryResource(variable)) {
139+
return List.of(makeQuickFixReplacingWithUnnamedVariable(variable));
137140
}
138141
return getQuickFixTextSpan(variable).map(textSpan -> Collections.singletonList(
139142
JavaQuickFix.newQuickFix("Remove unused local variable")
@@ -143,11 +146,14 @@ private static List<JavaQuickFix> computeQuickFix(VariableTree variable) {
143146
).orElseGet(Collections::emptyList);
144147
}
145148

146-
private static List<JavaQuickFix> makeQuickFixReplacingWithUnnamedVariable(VariableTree variable) {
147-
var textSpan = AnalyzerMessage.textSpanBetween(variable.firstToken(), variable.lastToken());
148-
return List.of(JavaQuickFix.newQuickFix("Replace unused local variable with _")
149-
.addTextEdit(JavaTextEdit.replaceTextSpan(textSpan, "var _"))
150-
.build());
149+
private static JavaQuickFix makeQuickFixReplacingWithUnnamedVariable(VariableTree variable) {
150+
return JavaQuickFix.newQuickFix("Replace unused local variable with _")
151+
// This works with both enhanced for loop and try-with-resources.
152+
// In the latter case we keep the initializer:
153+
// `for(int elem: elems)` turns into `for(var _: elems)`
154+
// `try(Resource res = initializer())` turns into `try(var _ = initializer ())`
155+
.addTextEdit(JavaTextEdit.replaceBetweenTree(variable.type(), true, variable.simpleName(), true, "var _"))
156+
.build();
151157
}
152158

153159
private static Optional<AnalyzerMessage.TextSpan> getQuickFixTextSpan(VariableTree variable) {

java-checks/src/test/java/org/sonar/java/checks/unused/UnusedLocalVariableCheckTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,27 @@ void test_non_compiling() {
5959
.verifyIssues();
6060
}
6161

62+
@Test
63+
void test_with_lambda() {
64+
CheckVerifier.newVerifier()
65+
.onFile(TestUtils.mainCodeSourcesPath("checks/unused/UnusedLocalVariableCheck_withLambda.java"))
66+
.withCheck(new UnusedLocalVariableCheck())
67+
.withJavaVersion(22)
68+
.verifyIssues();
69+
}
70+
71+
/**
72+
* Test for false negative when a name is used in a lambda expression.
73+
* See SONARJAVA-5504 for details.
74+
*/
75+
@Test
76+
void test_with_lambda_without_semantics() {
77+
CheckVerifier.newVerifier()
78+
.onFile(TestUtils.mainCodeSourcesPath("checks/unused/UnusedLocalVariableCheck_withLambda.java"))
79+
.withCheck(new UnusedLocalVariableCheck())
80+
.withJavaVersion(22)
81+
.withoutSemantic()
82+
// False negatives.
83+
.verifyNoIssues();
84+
}
6285
}

0 commit comments

Comments
 (0)