Skip to content

Commit 7fe001e

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

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
public class FinalLocalVariable extends Recipe {
40+
41+
private final List<CheckstyleViolation> violations;
42+
43+
public FinalLocalVariable(List<CheckstyleViolation> violations) {
44+
this.violations = violations;
45+
}
46+
47+
@Override
48+
public String getDisplayName() {
49+
return "FinalLocalVariable recipe";
50+
}
51+
52+
@Override
53+
public String getDescription() {
54+
return "Adds 'final' modifier to local variables that never have their values changed.";
55+
}
56+
57+
@Override
58+
public TreeVisitor<?, ExecutionContext> getVisitor() {
59+
return new LocalVariableVisitor();
60+
}
61+
62+
private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {
63+
64+
private Path sourcePath;
65+
66+
@Override
67+
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
68+
this.sourcePath = cu.getSourcePath();
69+
return super.visitCompilationUnit(cu, ctx);
70+
}
71+
72+
@Override
73+
public J.VariableDeclarations visitVariableDeclarations(
74+
J.VariableDeclarations multiVariable, ExecutionContext ctx) {
75+
J.VariableDeclarations declarations = super.visitVariableDeclarations(multiVariable,
76+
ctx);
77+
78+
if (!(getCursor().getParentTreeCursor().getValue() instanceof J.ClassDeclaration)
79+
&& declarations.getVariables().size() == 1) {
80+
final J.VariableDeclarations.NamedVariable variable = declarations
81+
.getVariables().get(0);
82+
if (isAtViolationLocation(variable)) {
83+
final List<J.Modifier> modifiers = new ArrayList<>();
84+
modifiers.add(new J.Modifier(Tree.randomId(), Space.EMPTY,
85+
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
86+
modifiers.addAll(Space.formatFirstPrefix(declarations.getModifiers(),
87+
Space.SINGLE_SPACE));
88+
declarations = declarations.withModifiers(modifiers)
89+
.withTypeExpression(declarations.getTypeExpression()
90+
.withPrefix(Space.SINGLE_SPACE));
91+
}
92+
}
93+
return declarations;
94+
}
95+
96+
private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable literal) {
97+
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
98+
99+
final int line = computeLinePosition(cursor, literal, getCursor());
100+
final int column = computeColumnPosition(cursor, literal, getCursor());
101+
102+
return violations.stream().anyMatch(violation -> {
103+
return violation.getLine() == line
104+
&& violation.getColumn() == column
105+
&& Path.of(violation.getFileName()).equals(sourcePath);
106+
});
107+
}
108+
109+
private int computePosition(
110+
J tree,
111+
J targetElement,
112+
Cursor cursor,
113+
Function<String, Integer> positionCalculator
114+
) {
115+
final TreeVisitor<?, PrintOutputCapture<TreeVisitor<?, ?>>> printer =
116+
tree.printer(cursor);
117+
118+
final PrintOutputCapture<TreeVisitor<?, ?>> capture =
119+
new PrintOutputCapture<>(printer) {
120+
@Override
121+
public PrintOutputCapture<TreeVisitor<?, ?>> append(String text) {
122+
if (targetElement.isScope(getContext().getCursor().getValue())) {
123+
super.append(targetElement.getPrefix().getWhitespace());
124+
throw new CancellationException();
125+
}
126+
return super.append(text);
127+
}
128+
};
129+
130+
final int result;
131+
try {
132+
printer.visit(tree, capture, cursor.getParentOrThrow());
133+
throw new IllegalStateException("Target element: " + targetElement
134+
+ ", not found in the syntax tree.");
135+
}
136+
catch (CancellationException exception) {
137+
result = positionCalculator.apply(capture.getOut());
138+
}
139+
catch (RecipeRunException exception) {
140+
if (exception.getCause() instanceof CancellationException) {
141+
result = positionCalculator.apply(capture.getOut());
142+
}
143+
else {
144+
throw exception;
145+
}
146+
}
147+
return result;
148+
}
149+
150+
private int computeLinePosition(J tree, J targetElement, Cursor cursor) {
151+
return computePosition(tree, targetElement, cursor,
152+
out -> 1 + Math.toIntExact(out.chars().filter(chr -> chr == '\n').count()));
153+
}
154+
155+
private int computeColumnPosition(J tree, J targetElement, Cursor cursor) {
156+
return computePosition(tree, targetElement, cursor, this::calculateColumnOffset);
157+
}
158+
159+
private int calculateColumnOffset(String out) {
160+
final int lineBreakIndex = out.lastIndexOf('\n');
161+
final int result;
162+
if (lineBreakIndex == -1) {
163+
result = out.length();
164+
}
165+
else {
166+
result = out.length() - lineBreakIndex - 1;
167+
}
168+
return result;
169+
}
170+
}
171+
}
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="Use uppercase 'L' for long literals."
6+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
7+
<error line="6" column="12" severity="error"
8+
message="Use uppercase 'L' for long literals."
9+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
10+
<error line="7" column="12" severity="error"
11+
message="Use uppercase 'L' for long literals."
12+
source="com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"/>
13+
</file>
14+
</checkstyle>

0 commit comments

Comments
 (0)