Skip to content

Commit 9374d70

Browse files
committed
Issue #80: Resolve MultiVariableDeclaration Bug
1 parent 85c5edb commit 9374d70

File tree

8 files changed

+263
-30
lines changed

8 files changed

+263
-30
lines changed

src/main/java/org/checkstyle/autofix/recipe/FinalLocalVariable.java

Lines changed: 160 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,22 @@
1919

2020
import java.nio.file.Path;
2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.List;
24+
import java.util.UUID;
2325

2426
import org.checkstyle.autofix.PositionHelper;
2527
import org.checkstyle.autofix.parser.CheckstyleViolation;
28+
import org.jspecify.annotations.Nullable;
2629
import org.openrewrite.ExecutionContext;
2730
import org.openrewrite.Recipe;
2831
import org.openrewrite.Tree;
2932
import org.openrewrite.TreeVisitor;
3033
import org.openrewrite.java.JavaIsoVisitor;
3134
import org.openrewrite.java.tree.J;
3235
import org.openrewrite.java.tree.Space;
36+
import org.openrewrite.java.tree.Statement;
37+
import org.openrewrite.marker.Marker;
3338
import org.openrewrite.marker.Markers;
3439

3540
/**
@@ -56,17 +61,92 @@ public String getDescription() {
5661

5762
@Override
5863
public TreeVisitor<?, ExecutionContext> getVisitor() {
59-
return new LocalVariableVisitor();
64+
return new JavaIsoVisitor<>() {
65+
@Override
66+
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu,
67+
ExecutionContext ctx) {
68+
return new LocalVariableVisitor()
69+
.visitCompilationUnit(new MarkViolationVisitor()
70+
.visitCompilationUnit(cu, ctx), ctx);
71+
}
72+
};
6073
}
6174

62-
private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {
75+
/**
76+
* Visitor that identifies and marks variable declarations at violation locations.
77+
* This visitor traverses the AST and adds markers to variables that match
78+
* the checkstyle violation locations, preparing them for the final modifier addition.
79+
*/
80+
private final class MarkViolationVisitor extends JavaIsoVisitor<ExecutionContext> {
6381

6482
private Path sourcePath;
83+
private J.CompilationUnit currentCompilationUnit;
84+
85+
@Override
86+
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
87+
this.sourcePath = cu.getSourcePath().toAbsolutePath();
88+
this.currentCompilationUnit = cu;
89+
return super.visitCompilationUnit(cu, ctx);
90+
}
91+
92+
@Override
93+
public J.VariableDeclarations visitVariableDeclarations(
94+
J.VariableDeclarations multiVariable, ExecutionContext executionContext) {
95+
96+
final J.VariableDeclarations variableDeclarations;
97+
98+
final J.VariableDeclarations declarations = super
99+
.visitVariableDeclarations(multiVariable, executionContext);
100+
101+
if (!(getCursor().getParentTreeCursor().getValue() instanceof J.ClassDeclaration)
102+
&& !declarations.hasModifier(J.Modifier.Type.Final)) {
103+
104+
final List<J.VariableDeclarations.NamedVariable> variables = declarations
105+
.getVariables();
106+
final List<J.VariableDeclarations.NamedVariable> marked = new ArrayList<>();
107+
for (J.VariableDeclarations.NamedVariable variable : variables) {
108+
if (isAtViolationLocation(variable)) {
109+
marked.add(variable.withMarkers(
110+
variable.getMarkers().add(new FinalLocalVariableMarker())));
111+
}
112+
else {
113+
marked.add(variable);
114+
}
115+
}
116+
variableDeclarations = declarations.withVariables(marked);
117+
}
118+
else {
119+
variableDeclarations = declarations;
120+
}
121+
return variableDeclarations;
122+
}
123+
124+
private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable variable) {
125+
126+
final int line = PositionHelper
127+
.computeLinePosition(currentCompilationUnit, variable, getCursor());
128+
final int column = PositionHelper
129+
.computeColumnPosition(currentCompilationUnit, variable, getCursor());
130+
131+
return violations.removeIf(violation -> {
132+
final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath();
133+
return violation.getLine() == line
134+
&& violation.getColumn() == column
135+
&& absolutePath.endsWith(sourcePath)
136+
&& violation.getMessage().contains(variable.getSimpleName());
137+
});
138+
}
139+
}
140+
141+
/**
142+
* Visitor that processes marked variable declarations and applies the final modifier.
143+
* This visitor handles both single and multi-variable declarations.
144+
*/
145+
private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {
65146

66147
@Override
67148
public J.CompilationUnit visitCompilationUnit(
68149
J.CompilationUnit cu, ExecutionContext executionContext) {
69-
this.sourcePath = cu.getSourcePath();
70150
return super.visitCompilationUnit(cu, executionContext);
71151
}
72152

@@ -83,33 +163,91 @@ public J.VariableDeclarations visitVariableDeclarations(
83163
&& !declarations.hasModifier(J.Modifier.Type.Final)) {
84164
final J.VariableDeclarations.NamedVariable variable = declarations
85165
.getVariables().get(0);
86-
if (isAtViolationLocation(variable)) {
87-
final List<J.Modifier> modifiers = new ArrayList<>();
166+
if (variable.getMarkers().findFirst(FinalLocalVariableMarker.class).isPresent()) {
167+
declarations = addFinalModifier(declarations);
168+
}
169+
}
170+
return declarations;
171+
}
172+
173+
@Override
174+
public J.Block visitBlock(J.Block block, ExecutionContext executionContext) {
175+
final J.Block visited = super.visitBlock(block, executionContext);
88176

89-
final Space finalPrefix = declarations.getTypeExpression().getPrefix();
177+
final List<Statement> newStatements = new ArrayList<>();
90178

91-
modifiers.add(new J.Modifier(Tree.randomId(), finalPrefix,
92-
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
93-
modifiers.addAll(declarations.getModifiers());
94-
declarations = declarations.withModifiers(modifiers)
95-
.withTypeExpression(declarations.getTypeExpression()
96-
.withPrefix(Space.SINGLE_SPACE));
179+
for (Statement stmt : visited.getStatements()) {
180+
if (isVariableDeclaration(stmt)) {
181+
handleMultiVariableDeclaration((J.VariableDeclarations) stmt, newStatements);
182+
}
183+
else {
184+
newStatements.add(stmt);
185+
}
186+
}
187+
188+
return visited.withStatements(newStatements);
189+
}
190+
191+
private void handleMultiVariableDeclaration(J.VariableDeclarations varDecl,
192+
List<Statement> newStatements) {
193+
final List<J.VariableDeclarations.NamedVariable> violationsList = new ArrayList<>();
194+
final List<J.VariableDeclarations.NamedVariable> nonViolations = new ArrayList<>();
195+
196+
for (J.VariableDeclarations.NamedVariable variable : varDecl.getVariables()) {
197+
if (variable.getMarkers().findFirst(FinalLocalVariableMarker.class).isPresent()) {
198+
violationsList.add(variable.withPrefix(Space.SINGLE_SPACE));
199+
}
200+
else {
201+
nonViolations.add(variable.withPrefix(Space.SINGLE_SPACE));
202+
}
203+
}
204+
if (violationsList.isEmpty()) {
205+
newStatements.add(varDecl);
206+
}
207+
else if (nonViolations.isEmpty()) {
208+
newStatements.add(addFinalModifier(varDecl));
209+
}
210+
else {
211+
newStatements.add(varDecl.withVariables(nonViolations));
212+
for (J.VariableDeclarations.NamedVariable variable : violationsList) {
213+
newStatements.add(addFinalModifier(varDecl
214+
.withVariables(Collections.singletonList(variable))));
97215
}
98216
}
99-
return declarations;
100217
}
101218

102-
private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable literal) {
103-
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
219+
private J.VariableDeclarations addFinalModifier(J.VariableDeclarations varDecl) {
220+
final List<J.Modifier> modifiers = new ArrayList<>(varDecl.getModifiers());
221+
final Space finalPrefix = varDecl.getTypeExpression().getPrefix();
222+
modifiers.add(new J.Modifier(Tree.randomId(), finalPrefix,
223+
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
104224

105-
final int line = PositionHelper.computeLinePosition(cursor, literal, getCursor());
106-
final int column = PositionHelper.computeColumnPosition(cursor, literal, getCursor());
225+
modifiers.addAll(varDecl.getModifiers());
107226

108-
return violations.stream().anyMatch(violation -> {
109-
return violation.getLine() == line
110-
&& violation.getColumn() == column
111-
&& Path.of(violation.getFileName()).endsWith(sourcePath);
112-
});
227+
return varDecl.withModifiers(modifiers)
228+
.withTypeExpression(varDecl.getTypeExpression().withPrefix(Space.SINGLE_SPACE));
229+
}
230+
231+
private boolean isVariableDeclaration(Statement stmt) {
232+
return stmt instanceof J.VariableDeclarations varDecl
233+
&& varDecl.getVariables().size() > 1
234+
&& !varDecl.hasModifier(J.Modifier.Type.Final)
235+
&& varDecl.getTypeExpression() != null
236+
&& !(getCursor().getParentTreeCursor()
237+
.getValue() instanceof J.ClassDeclaration);
238+
}
239+
}
240+
241+
public static class FinalLocalVariableMarker implements Marker {
242+
243+
@Override
244+
public @Nullable UUID getId() {
245+
return null;
246+
}
247+
248+
@Override
249+
public <M extends Marker> M withId(UUID id) {
250+
return null;
113251
}
114252
}
115253
}

src/test/java/org/checkstyle/autofix/recipe/FinalLocalVariableTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,9 @@ void annotationDeclaration() throws Exception {
5151
verify("AnnotationDeclaration");
5252
}
5353

54+
@Test
55+
void multiple() throws Exception {
56+
verify("MultipleVariable");
57+
}
58+
5459
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--- src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/multiplevariable/InputMultipleVariable.java
2+
+++ src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/multiplevariable/OutputMultipleVariable.java
3+
@@ -8,15 +8,17 @@
4+
5+
package org.checkstyle.autofix.recipe.finallocalvariable.multiplevariable;
6+
7+
-public class InputMultipleVariable {
8+
+public class OutputMultipleVariable {
9+
10+
public void multipleDeclarations() {
11+
12+
- int x = 10, y = 20, z = 30; // 3 violations
13+
+ final int x = 10, y = 20, z = 30;
14+
15+
- String name = "John", city = "NYC"; // 2 violations
16+
+ final String name = "John", city = "NYC";
17+
18+
- double price = 19.99, tax = 0.08; // violation, "should be declared final"
19+
+ double tax = 0.08;
20+
+
21+
+ final double price = 19.99;
22+
23+
tax = 50;
24+
System.out.println("Sum: " + (x + y + z));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*xml
2+
<module name="Checker">
3+
<module name="TreeWalker">
4+
<module name="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
5+
</module>
6+
</module>
7+
*/
8+
9+
package org.checkstyle.autofix.recipe.finallocalvariable.multiplevariable;
10+
11+
public class InputMultipleVariable {
12+
13+
public void multipleDeclarations() {
14+
15+
int x = 10, y = 20, z = 30; // 3 violations
16+
17+
String name = "John", city = "NYC"; // 2 violations
18+
19+
double price = 19.99, tax = 0.08; // violation, "should be declared final"
20+
21+
tax = 50;
22+
System.out.println("Sum: " + (x + y + z));
23+
System.out.println(name + " lives in " + city);
24+
System.out.println("Total: " + (price + (price * tax)));
25+
}
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*xml
2+
<module name="Checker">
3+
<module name="TreeWalker">
4+
<module name="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
5+
</module>
6+
</module>
7+
*/
8+
9+
package org.checkstyle.autofix.recipe.finallocalvariable.multiplevariable;
10+
11+
public class OutputMultipleVariable {
12+
13+
public void multipleDeclarations() {
14+
15+
final int x = 10, y = 20, z = 30;
16+
17+
final String name = "John", city = "NYC";
18+
19+
double tax = 0.08;
20+
21+
final double price = 19.99;
22+
23+
tax = 50;
24+
System.out.println("Sum: " + (x + y + z));
25+
System.out.println(name + " lives in " + city);
26+
System.out.println("Total: " + (price + (price * tax)));
27+
}
28+
}

src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/singlelocaltest/DiffSingleLocalTest.diff

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
--- src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/singlelocaltest/InputSingleLocalTest.java
22
+++ src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/singlelocaltest/OutputSingleLocalTest.java
3-
@@ -15,36 +15,35 @@
3+
@@ -15,37 +15,37 @@
44
import java.util.HashMap;
55
import java.util.List;
66

@@ -17,7 +17,7 @@
1717
- StringBuilder builder = new StringBuilder(); // violation, "should be declared final"
1818
- Date currentDate = new Date(); // violation, "should be declared final"
1919
- File // prefix comment preceding target element
20-
- tempFile = new File("temp.txt"); // violation, "should be declared final"
20+
- tempFile = new File("temp.txt"); // violation, "should be declared final"
2121
+ final String name = "John Doe";
2222
+ final int age = 25;
2323
+ final double salary = 50000.0;
@@ -28,7 +28,7 @@
2828
+ final StringBuilder builder = new StringBuilder();
2929
+ final Date currentDate = new Date();
3030
+ final File // prefix comment preceding target element
31-
+ tempFile = new File("temp.txt");
31+
+ tempFile = new File("temp.txt");
3232
}
3333

3434
public void methodWithCalculations() {
@@ -53,9 +53,12 @@
5353
- String trimmed = input.trim(); // violation, "should be declared final"
5454
- String upperCase = trimmed.toUpperCase(); // violation, "should be declared final"
5555
- String formatted = "[" + upperCase + "]"; // violation, "should be declared final"
56+
- int a,b; // violation, "should be declared final"
5657
+ final String trimmed = input.trim();
5758
+ final String upperCase = trimmed.toUpperCase();
5859
+ final String formatted = "[" + upperCase + "]";
59-
return formatted;
60-
}
61-
60+
+ int a;
61+
+ final int b;
62+
a = upperCase.indexOf(formatted);
63+
b = upperCase.indexOf(formatted);
64+
a = a + formatted.length();

src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/singlelocaltest/InputSingleLocalTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void basicLocalVariables() {
2727
StringBuilder builder = new StringBuilder(); // violation, "should be declared final"
2828
Date currentDate = new Date(); // violation, "should be declared final"
2929
File // prefix comment preceding target element
30-
tempFile = new File("temp.txt"); // violation, "should be declared final"
30+
tempFile = new File("temp.txt"); // violation, "should be declared final"
3131
}
3232

3333
public void methodWithCalculations() {
@@ -45,6 +45,10 @@ public String processResult(String input) {
4545
String trimmed = input.trim(); // violation, "should be declared final"
4646
String upperCase = trimmed.toUpperCase(); // violation, "should be declared final"
4747
String formatted = "[" + upperCase + "]"; // violation, "should be declared final"
48+
int a,b; // violation, "should be declared final"
49+
a = upperCase.indexOf(formatted);
50+
b = upperCase.indexOf(formatted);
51+
a = a + formatted.length();
4852
return formatted;
4953
}
5054

0 commit comments

Comments
 (0)