Skip to content

Commit bf67b3d

Browse files
committed
Issue #33: FinalLocalVariable recipe created
1 parent 4bf149c commit bf67b3d

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
///////////////////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite.
3+
// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
///////////////////////////////////////////////////////////////////////////////////////////////
17+
18+
package org.checkstyle.autofix.recipe;
19+
20+
import java.nio.file.Path;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.concurrent.CancellationException;
24+
import java.util.function.Function;
25+
26+
import org.checkstyle.autofix.parser.CheckstyleViolation;
27+
import org.openrewrite.Cursor;
28+
import org.openrewrite.ExecutionContext;
29+
import org.openrewrite.PrintOutputCapture;
30+
import org.openrewrite.Recipe;
31+
import org.openrewrite.Tree;
32+
import org.openrewrite.TreeVisitor;
33+
import org.openrewrite.internal.RecipeRunException;
34+
import org.openrewrite.java.JavaIsoVisitor;
35+
import org.openrewrite.java.tree.J;
36+
import org.openrewrite.java.tree.Space;
37+
import org.openrewrite.marker.Markers;
38+
39+
/**
40+
* Fixes Checkstyle FinalLocalVariable violations by adding 'final' modifier to local variables
41+
* that are never reassigned.
42+
*/
43+
public class FinalLocalVariable extends Recipe {
44+
45+
private final List<CheckstyleViolation> violations;
46+
47+
public FinalLocalVariable(List<CheckstyleViolation> violations) {
48+
this.violations = violations;
49+
}
50+
51+
@Override
52+
public String getDisplayName() {
53+
return "FinalLocalVariable recipe";
54+
}
55+
56+
@Override
57+
public String getDescription() {
58+
return "Adds 'final' modifier to local variables that never have their values changed.";
59+
}
60+
61+
@Override
62+
public TreeVisitor<?, ExecutionContext> getVisitor() {
63+
return new LocalVariableVisitor();
64+
}
65+
66+
private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {
67+
68+
private Path sourcePath;
69+
70+
@Override
71+
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
72+
this.sourcePath = cu.getSourcePath();
73+
return super.visitCompilationUnit(cu, ctx);
74+
}
75+
76+
@Override
77+
public J.VariableDeclarations visitVariableDeclarations(
78+
J.VariableDeclarations multiVariable, ExecutionContext ctx) {
79+
J.VariableDeclarations declarations = super.visitVariableDeclarations(multiVariable,
80+
ctx);
81+
82+
if (!(getCursor().getParentTreeCursor().getValue() instanceof J.ClassDeclaration)
83+
&& declarations.getVariables().size() == 1) {
84+
final J.VariableDeclarations.NamedVariable variable = declarations
85+
.getVariables().get(0);
86+
if (isAtViolationLocation(variable)) {
87+
final List<J.Modifier> modifiers = new ArrayList<>();
88+
modifiers.add(new J.Modifier(Tree.randomId(), Space.EMPTY,
89+
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
90+
modifiers.addAll(Space.formatFirstPrefix(declarations.getModifiers(),
91+
Space.SINGLE_SPACE));
92+
declarations = declarations.withModifiers(modifiers)
93+
.withTypeExpression(declarations.getTypeExpression()
94+
.withPrefix(Space.SINGLE_SPACE));
95+
}
96+
}
97+
return declarations;
98+
}
99+
100+
private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable literal) {
101+
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
102+
103+
final int line = computeLinePosition(cursor, literal, getCursor());
104+
final int column = computeColumnPosition(cursor, literal, getCursor());
105+
106+
return violations.stream().anyMatch(violation -> {
107+
return violation.getLine() == line
108+
&& violation.getColumn() == column
109+
&& Path.of(violation.getFileName()).equals(sourcePath);
110+
});
111+
}
112+
113+
private int computePosition(
114+
J tree,
115+
J targetElement,
116+
Cursor cursor,
117+
Function<String, Integer> positionCalculator
118+
) {
119+
final TreeVisitor<?, PrintOutputCapture<TreeVisitor<?, ?>>> printer =
120+
tree.printer(cursor);
121+
122+
final PrintOutputCapture<TreeVisitor<?, ?>> capture =
123+
new PrintOutputCapture<>(printer) {
124+
@Override
125+
public PrintOutputCapture<TreeVisitor<?, ?>> append(String text) {
126+
if (targetElement.isScope(getContext().getCursor().getValue())) {
127+
super.append(targetElement.getPrefix().getWhitespace());
128+
throw new CancellationException();
129+
}
130+
return super.append(text);
131+
}
132+
};
133+
134+
final int result;
135+
try {
136+
printer.visit(tree, capture, cursor.getParentOrThrow());
137+
throw new IllegalStateException("Target element: " + targetElement
138+
+ ", not found in the syntax tree.");
139+
}
140+
catch (CancellationException exception) {
141+
result = positionCalculator.apply(capture.getOut());
142+
}
143+
catch (RecipeRunException exception) {
144+
if (exception.getCause() instanceof CancellationException) {
145+
result = positionCalculator.apply(capture.getOut());
146+
}
147+
else {
148+
throw exception;
149+
}
150+
}
151+
return result;
152+
}
153+
154+
private int computeLinePosition(J tree, J targetElement, Cursor cursor) {
155+
return computePosition(tree, targetElement, cursor,
156+
out -> 1 + Math.toIntExact(out.chars().filter(chr -> chr == '\n').count()));
157+
}
158+
159+
private int computeColumnPosition(J tree, J targetElement, Cursor cursor) {
160+
return computePosition(tree, targetElement, cursor, this::calculateColumnOffset);
161+
}
162+
163+
private int calculateColumnOffset(String out) {
164+
final int lineBreakIndex = out.lastIndexOf('\n');
165+
final int result;
166+
if (lineBreakIndex == -1) {
167+
result = out.length();
168+
}
169+
else {
170+
result = out.length() - lineBreakIndex - 1;
171+
}
172+
return result;
173+
}
174+
}
175+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
///////////////////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite.
3+
// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
///////////////////////////////////////////////////////////////////////////////////////////////
17+
18+
package org.checkstyle.autofix.recipe;
19+
20+
import java.io.IOException;
21+
import java.nio.file.Path;
22+
import java.util.List;
23+
24+
import org.checkstyle.autofix.parser.CheckstyleReportParser;
25+
import org.checkstyle.autofix.parser.CheckstyleViolation;
26+
import org.junit.jupiter.api.Test;
27+
import org.openrewrite.Recipe;
28+
29+
public class FinalLocalVariableTest extends AbstractRecipeTest {
30+
@Override
31+
protected Recipe getRecipe() {
32+
final String reportPath = "src/test/resources/org/checkstyle/autofix/recipe"
33+
+ "/finallocalvariable/report.xml";
34+
35+
final List<CheckstyleViolation> violations =
36+
CheckstyleReportParser.parse(Path.of(reportPath));
37+
return new FinalLocalVariable(violations);
38+
}
39+
40+
@Test
41+
void hexOctalLiteralTest() throws IOException {
42+
testRecipe("finallocalvariable", "MissingFinal");
43+
}
44+
45+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.checkstyle.autofix.recipe.finallocalvariable.missingfinal;
2+
3+
public class InputMissingFinal {
4+
public void computeSum() {
5+
int a = 5;
6+
int b = 10;
7+
int sum = a + b;
8+
9+
System.out.println("Sum: " + sum);
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.checkstyle.autofix.recipe.finallocalvariable.missingfinal;
2+
3+
public class OutputMissingFinal {
4+
public void computeSum() {
5+
final int a = 5;
6+
final int b = 10;
7+
final int sum = a + b;
8+
9+
System.out.println("Sum: " + sum);
10+
}
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<checkstyle version="10.12.3">
3+
<file name="org/checkstyle/autofix/recipe/finallocalvariable/missingfinal/InputMissingFinal.java">
4+
<error line="5" column="12" severity="error"
5+
message="Variable should be declared final."
6+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
7+
<error line="6" column="12" severity="error"
8+
message="Variable should be declared final."
9+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
10+
<error line="7" column="12" severity="error"
11+
message="Variable should be declared final."
12+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
13+
</file>
14+
</checkstyle>

0 commit comments

Comments
 (0)