Skip to content

Commit 3da3b1e

Browse files
committed
Issue #33: FinalLocalVariable recipe created
1 parent d247386 commit 3da3b1e

File tree

12 files changed

+731
-90
lines changed

12 files changed

+731
-90
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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;
19+
20+
import java.util.concurrent.CancellationException;
21+
import java.util.function.Function;
22+
23+
import org.openrewrite.Cursor;
24+
import org.openrewrite.PrintOutputCapture;
25+
import org.openrewrite.TreeVisitor;
26+
import org.openrewrite.internal.RecipeRunException;
27+
import org.openrewrite.java.tree.J;
28+
29+
public final class PositionHelper {
30+
31+
private PositionHelper() {
32+
// Utility class
33+
}
34+
35+
public static int computeLinePosition(J tree, J targetElement, Cursor cursor) {
36+
return computePosition(tree, targetElement, cursor,
37+
out -> 1 + Math.toIntExact(out.chars().filter(chr -> chr == '\n').count()));
38+
}
39+
40+
public static int computeColumnPosition(J tree, J targetElement, Cursor cursor) {
41+
return computePosition(tree, targetElement, cursor, out -> {
42+
int column = calculateColumnOffset(out);
43+
if (targetElement instanceof J.Literal literal
44+
&& literal.getValue() instanceof Number
45+
&& literal.getValueSource() != null
46+
&& literal.getValueSource().matches("^[+-].*")) {
47+
column++;
48+
}
49+
return column;
50+
});
51+
}
52+
53+
private static int computePosition(
54+
J tree,
55+
J targetElement,
56+
Cursor cursor,
57+
Function<String, Integer> positionCalculator
58+
) {
59+
final TreeVisitor<?, PrintOutputCapture<TreeVisitor<?, ?>>> printer =
60+
tree.printer(cursor);
61+
62+
final PrintOutputCapture<TreeVisitor<?, ?>> capture =
63+
new PrintOutputCapture<>(printer) {
64+
@Override
65+
public PrintOutputCapture<TreeVisitor<?, ?>> append(String text) {
66+
if (targetElement.isScope(getContext().getCursor().getValue())) {
67+
super.append(targetElement.getPrefix().getWhitespace());
68+
throw new CancellationException();
69+
}
70+
return super.append(text);
71+
}
72+
};
73+
74+
final int result;
75+
try {
76+
printer.visit(tree, capture, cursor.getParentOrThrow());
77+
throw new IllegalStateException("Target element: " + targetElement
78+
+ ", not found in the syntax tree.");
79+
}
80+
catch (CancellationException exception) {
81+
result = positionCalculator.apply(capture.getOut());
82+
}
83+
catch (RecipeRunException exception) {
84+
if (exception.getCause() instanceof CancellationException) {
85+
result = positionCalculator.apply(capture.getOut());
86+
}
87+
else {
88+
throw exception;
89+
}
90+
}
91+
return result;
92+
}
93+
94+
private static int calculateColumnOffset(String out) {
95+
final int lineBreakIndex = out.lastIndexOf('\n');
96+
final int result;
97+
if (lineBreakIndex == -1) {
98+
result = out.length();
99+
}
100+
else {
101+
result = out.length() - lineBreakIndex;
102+
}
103+
return result;
104+
}
105+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
24+
import org.checkstyle.autofix.PositionHelper;
25+
import org.checkstyle.autofix.parser.CheckstyleViolation;
26+
import org.openrewrite.ExecutionContext;
27+
import org.openrewrite.Recipe;
28+
import org.openrewrite.Tree;
29+
import org.openrewrite.TreeVisitor;
30+
import org.openrewrite.java.JavaIsoVisitor;
31+
import org.openrewrite.java.tree.J;
32+
import org.openrewrite.java.tree.Space;
33+
import org.openrewrite.marker.Markers;
34+
35+
/**
36+
* Fixes Checkstyle FinalLocalVariable violations by adding 'final' modifier to local variables
37+
* that are never reassigned.
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+
76+
J.VariableDeclarations declarations = super.visitVariableDeclarations(multiVariable,
77+
ctx);
78+
79+
if (!(getCursor().getParentTreeCursor().getValue() instanceof J.ClassDeclaration)
80+
&& declarations.getVariables().size() == 1
81+
&& declarations.getTypeExpression() != null
82+
&& !declarations.hasModifier(J.Modifier.Type.Final)) {
83+
final J.VariableDeclarations.NamedVariable variable = declarations
84+
.getVariables().get(0);
85+
if (isAtViolationLocation(variable)) {
86+
final List<J.Modifier> modifiers = new ArrayList<>();
87+
modifiers.add(new J.Modifier(Tree.randomId(), Space.EMPTY,
88+
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
89+
modifiers.addAll(declarations.getModifiers());
90+
declarations = declarations.withModifiers(modifiers)
91+
.withTypeExpression(declarations.getTypeExpression()
92+
.withPrefix(Space.SINGLE_SPACE));
93+
}
94+
}
95+
return declarations;
96+
}
97+
98+
private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable literal) {
99+
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
100+
101+
final int line = PositionHelper.computeLinePosition(cursor, literal, getCursor());
102+
final int column = PositionHelper.computeColumnPosition(cursor, literal, getCursor());
103+
104+
return violations.stream().anyMatch(violation -> {
105+
return violation.getLine() == line
106+
&& violation.getColumn() == column
107+
&& Path.of(violation.getFileName()).equals(sourcePath);
108+
});
109+
}
110+
}
111+
}

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

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,12 @@
1919

2020
import java.nio.file.Path;
2121
import java.util.List;
22-
import java.util.concurrent.CancellationException;
23-
import java.util.function.Function;
2422

23+
import org.checkstyle.autofix.PositionHelper;
2524
import org.checkstyle.autofix.parser.CheckstyleViolation;
26-
import org.openrewrite.Cursor;
2725
import org.openrewrite.ExecutionContext;
28-
import org.openrewrite.PrintOutputCapture;
2926
import org.openrewrite.Recipe;
3027
import org.openrewrite.TreeVisitor;
31-
import org.openrewrite.internal.RecipeRunException;
3228
import org.openrewrite.java.JavaIsoVisitor;
3329
import org.openrewrite.java.tree.J;
3430
import org.openrewrite.java.tree.JavaType;
@@ -93,8 +89,8 @@ && isAtViolationLocation(result)) {
9389
private boolean isAtViolationLocation(J.Literal literal) {
9490
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
9591

96-
final int line = computeLinePosition(cursor, literal, getCursor());
97-
final int column = computeColumnPosition(cursor, literal, getCursor());
92+
final int line = PositionHelper.computeLinePosition(cursor, literal, getCursor());
93+
final int column = PositionHelper.computeColumnPosition(cursor, literal, getCursor());
9894

9995
return violations.stream().anyMatch(violation -> {
10096
final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath();
@@ -103,88 +99,5 @@ private boolean isAtViolationLocation(J.Literal literal) {
10399
&& absolutePath.equals(sourcePath);
104100
});
105101
}
106-
107-
/**
108-
* Computes the position of a target element within a syntax tree using position calculator.
109-
* This method traverses the given syntax tree and captures the printed output until the
110-
* target element is encountered. When the target is found, a CancellationException
111-
* is thrown to interrupt traversal, and the captured output is passed to the provided
112-
* positionCalculator to compute the position.
113-
*
114-
* @param tree the root of the syntax tree to traverse
115-
* @param targetElement the element whose position is to be computed
116-
* @param cursor the current cursor in the tree traversal
117-
* @param positionCalculator a function to compute the position from the printed output
118-
* @return the computed position of the target element
119-
* @throws IllegalStateException if the target element is not found in the tree
120-
* @throws RecipeRunException if an error occurs during traversal
121-
*/
122-
private int computePosition(
123-
J tree,
124-
J targetElement,
125-
Cursor cursor,
126-
Function<String, Integer> positionCalculator
127-
) {
128-
final TreeVisitor<?, PrintOutputCapture<TreeVisitor<?, ?>>> printer =
129-
tree.printer(cursor);
130-
131-
final PrintOutputCapture<TreeVisitor<?, ?>> capture =
132-
new PrintOutputCapture<>(printer) {
133-
@Override
134-
public PrintOutputCapture<TreeVisitor<?, ?>> append(String text) {
135-
if (targetElement.isScope(getContext().getCursor().getValue())) {
136-
super.append(targetElement.getPrefix().getWhitespace());
137-
throw new CancellationException();
138-
}
139-
return super.append(text);
140-
}
141-
};
142-
143-
final int result;
144-
try {
145-
printer.visit(tree, capture, cursor.getParentOrThrow());
146-
throw new IllegalStateException("Target element: " + targetElement
147-
+ ", not found in the syntax tree.");
148-
}
149-
catch (CancellationException exception) {
150-
result = positionCalculator.apply(capture.getOut());
151-
}
152-
catch (RecipeRunException exception) {
153-
if (exception.getCause() instanceof CancellationException) {
154-
result = positionCalculator.apply(capture.getOut());
155-
}
156-
else {
157-
throw exception;
158-
}
159-
}
160-
return result;
161-
}
162-
163-
private int computeLinePosition(J tree, J targetElement, Cursor cursor) {
164-
return computePosition(tree, targetElement, cursor,
165-
out -> 1 + Math.toIntExact(out.chars().filter(chr -> chr == '\n').count()));
166-
}
167-
168-
private int computeColumnPosition(J tree, J targetElement, Cursor cursor) {
169-
return computePosition(tree, targetElement, cursor, out -> {
170-
int column = calculateColumnOffset(out);
171-
if (((J.Literal) targetElement).getValueSource().matches("^[+-].*")) {
172-
column++;
173-
}
174-
return column;
175-
});
176-
}
177-
178-
private int calculateColumnOffset(String out) {
179-
final int lineBreakIndex = out.lastIndexOf('\n');
180-
final int result;
181-
if (lineBreakIndex == -1) {
182-
result = out.length();
183-
}
184-
else {
185-
result = out.length() - lineBreakIndex;
186-
}
187-
return result;
188-
}
189102
}
190103
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.util.List;
21+
22+
import org.checkstyle.autofix.parser.CheckConfiguration;
23+
import org.checkstyle.autofix.parser.CheckstyleViolation;
24+
import org.junit.jupiter.api.Test;
25+
import org.openrewrite.Recipe;
26+
27+
public class FinalLocalVariableTest extends AbstractRecipeTestSupport {
28+
29+
@Override
30+
protected String getSubpackage() {
31+
return "finallocalvariable";
32+
}
33+
34+
@Override
35+
protected Recipe createRecipe(List<CheckstyleViolation> violations, CheckConfiguration config) {
36+
37+
return new FinalLocalVariable(violations);
38+
}
39+
40+
@Test
41+
void singleLocalTest() throws Exception {
42+
verify("SingleLocalTest");
43+
}
44+
45+
@Test
46+
void classFieldTest() throws Exception {
47+
verify("ClassFieldTest");
48+
}
49+
50+
@Test
51+
void edgeCaseTest() throws Exception {
52+
verify("EdgeCaseTest");
53+
}
54+
55+
@Test
56+
void enhancedForLoop() throws Exception {
57+
verify("EnhancedForLoop");
58+
}
59+
60+
}

0 commit comments

Comments
 (0)