Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 167 additions & 22 deletions src/main/java/org/checkstyle/autofix/recipe/FinalLocalVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import org.checkstyle.autofix.PositionHelper;
import org.checkstyle.autofix.parser.CheckstyleViolation;
Expand All @@ -30,6 +32,8 @@
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.marker.Marker;
import org.openrewrite.marker.Markers;

/**
Expand All @@ -56,17 +60,94 @@ public String getDescription() {

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new LocalVariableVisitor();
return new JavaIsoVisitor<>() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we create this anon impl JavaIsoVisitor, after that LocalVariableVisitor and MarkViolationVisitor

It looks like all of these objects have the same generic extension, and I am curious, instead of having three visitor objects, can we have only one?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it true, that you have to visit each variable declaration twice?

The first time to mark everything, the second time to add modifiers. If so, maybe we can have two sequential visitors

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we visit each variable twice.

Copy link
Collaborator Author

@Anmol202005 Anmol202005 Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the correct approach IMO.
kindly see: https://docs.openrewrite.org/authoring-recipes/multiple-visitors

@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu,
ExecutionContext executionContext) {
return new LocalVariableVisitor()
.visitCompilationUnit(new MarkViolationVisitor()
.visitCompilationUnit(cu, executionContext), executionContext);
}
};
}

private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {
/**
* Visitor that identifies and marks variable declarations at violation locations.
* This visitor traverses the AST and adds markers to variables that match
* the checkstyle violation locations, preparing them for the final modifier addition.
*/
private final class MarkViolationVisitor extends JavaIsoVisitor<ExecutionContext> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely need more documentation around these two visitors. The logic looks cumbersome, and I think we should provide some details on how it works

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.


private Path sourcePath;
private J.CompilationUnit currentCompilationUnit;

@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu,
ExecutionContext executionContext) {
this.sourcePath = cu.getSourcePath().toAbsolutePath();
this.currentCompilationUnit = cu;
return super.visitCompilationUnit(cu, executionContext);
}

@Override
public J.VariableDeclarations visitVariableDeclarations(
J.VariableDeclarations multiVariable, ExecutionContext executionContext) {

final J.VariableDeclarations variableDeclarations;

final J.VariableDeclarations declarations = super
.visitVariableDeclarations(multiVariable, executionContext);

if (!(getCursor().getParentTreeCursor().getValue() instanceof J.ClassDeclaration)
&& !declarations.hasModifier(J.Modifier.Type.Final)) {

final List<J.VariableDeclarations.NamedVariable> variables = declarations
.getVariables();
final List<J.VariableDeclarations.NamedVariable> marked = new ArrayList<>();
for (J.VariableDeclarations.NamedVariable variable : variables) {
if (isAtViolationLocation(variable)) {
marked.add(variable.withMarkers(
variable.getMarkers().add(
new FinalLocalVariableMarker(UUID.randomUUID()))));
}
else {
marked.add(variable);
}
}
variableDeclarations = declarations.withVariables(marked);
}
else {
variableDeclarations = declarations;
}
return variableDeclarations;
}

private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable variable) {

final int line = PositionHelper
.computeLinePosition(currentCompilationUnit, variable, getCursor());
final int column = PositionHelper
.computeColumnPosition(currentCompilationUnit, variable, getCursor());

return violations.removeIf(violation -> {
final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath();
return violation.getLine() == line
&& violation.getColumn() == column
&& absolutePath.endsWith(sourcePath)
&& violation.getMessage().contains(variable.getSimpleName());
});
}
}

/**
* Visitor that processes marked variable declarations and applies the final modifier.
* This visitor handles both single and multi-variable declarations.
*/
private final class LocalVariableVisitor extends JavaIsoVisitor<ExecutionContext> {

@Override
public J.CompilationUnit visitCompilationUnit(
J.CompilationUnit cu, ExecutionContext executionContext) {
this.sourcePath = cu.getSourcePath();
return super.visitCompilationUnit(cu, executionContext);
}

Expand All @@ -83,33 +164,97 @@ public J.VariableDeclarations visitVariableDeclarations(
&& !declarations.hasModifier(J.Modifier.Type.Final)) {
final J.VariableDeclarations.NamedVariable variable = declarations
.getVariables().get(0);
if (isAtViolationLocation(variable)) {
final List<J.Modifier> modifiers = new ArrayList<>();
if (variable.getMarkers().findFirst(FinalLocalVariableMarker.class).isPresent()) {
declarations = addFinalModifier(declarations);
}
}
return declarations;
}

@Override
public J.Block visitBlock(J.Block block, ExecutionContext executionContext) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why we should specifically handle visiting blocks. My assumption was that any variable declaration node would be visited in visitVariableDeclarations?

Could you please provide an example of a variable declaration that won't be handled by visitVariableDeclarations but will be handled by visitBlock

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kindly see: #80

example:

int a = 1, b = 2, c = 3; // 1 violation
...
a = 7;
b = 8;

multiple variable declaration can not be handled by visitVariableDeclarations.

final J.Block visited = super.visitBlock(block, executionContext);

final Space finalPrefix = declarations.getTypeExpression().getPrefix();
final List<Statement> newStatements = new ArrayList<>();

modifiers.add(new J.Modifier(Tree.randomId(), finalPrefix,
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));
modifiers.addAll(declarations.getModifiers());
declarations = declarations.withModifiers(modifiers)
.withTypeExpression(declarations.getTypeExpression()
.withPrefix(Space.SINGLE_SPACE));
for (Statement stmt : visited.getStatements()) {
if (isVariableDeclaration(stmt)) {
handleMultiVariableDeclaration((J.VariableDeclarations) stmt, newStatements);
}
else {
newStatements.add(stmt);
}
}
return declarations;

return visited.withStatements(newStatements);
}

private boolean isAtViolationLocation(J.VariableDeclarations.NamedVariable literal) {
final J.CompilationUnit cursor = getCursor().firstEnclosing(J.CompilationUnit.class);
private void handleMultiVariableDeclaration(J.VariableDeclarations varDecl,
List<Statement> newStatements) {
final List<J.VariableDeclarations.NamedVariable> violationsList = new ArrayList<>();
final List<J.VariableDeclarations.NamedVariable> nonViolations = new ArrayList<>();

final int line = PositionHelper.computeLinePosition(cursor, literal, getCursor());
final int column = PositionHelper.computeColumnPosition(cursor, literal, getCursor());
for (J.VariableDeclarations.NamedVariable variable : varDecl.getVariables()) {
if (variable.getMarkers().findFirst(FinalLocalVariableMarker.class).isPresent()) {
violationsList.add(variable.withPrefix(Space.SINGLE_SPACE));
}
else {
nonViolations.add(variable.withPrefix(Space.SINGLE_SPACE));
}
}
if (violationsList.isEmpty()) {
newStatements.add(varDecl);
}
else if (nonViolations.isEmpty()) {
newStatements.add(addFinalModifier(varDecl));
}
else {
newStatements.add(varDecl.withVariables(nonViolations));
for (J.VariableDeclarations.NamedVariable variable : violationsList) {
newStatements.add(addFinalModifier(varDecl
.withVariables(Collections.singletonList(variable))));
}
}
}

return violations.stream().anyMatch(violation -> {
return violation.getLine() == line
&& violation.getColumn() == column
&& Path.of(violation.getFileName()).endsWith(sourcePath);
});
private J.VariableDeclarations addFinalModifier(J.VariableDeclarations varDecl) {
final List<J.Modifier> modifiers = new ArrayList<>();
final Space finalPrefix = varDecl.getTypeExpression().getPrefix();
modifiers.add(new J.Modifier(Tree.randomId(), finalPrefix,
Markers.EMPTY, null, J.Modifier.Type.Final, new ArrayList<>()));

modifiers.addAll(varDecl.getModifiers());

return varDecl.withModifiers(modifiers)
.withTypeExpression(varDecl.getTypeExpression().withPrefix(Space.SINGLE_SPACE));
}

private boolean isVariableDeclaration(Statement stmt) {
return stmt instanceof J.VariableDeclarations varDecl
&& varDecl.getVariables().size() > 1
&& !varDecl.hasModifier(J.Modifier.Type.Final)
&& varDecl.getTypeExpression() != null
&& !(getCursor().getParentTreeCursor()
.getValue() instanceof J.ClassDeclaration);
}
}

private static final class FinalLocalVariableMarker implements Marker {

private final UUID id;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need id? couldn't find usages of this field

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marker interface contains abstract method like getId() which we need to implement.
Since Id is not used i have updated the marker class as :

public static class FinalLocalVariableMarker implements Marker {

        public FinalLocalVariableMarker() {}

        @Override
        public UUID getId() {
            return null;
        }

        @Override
        public <M extends Marker> M withId(UUID id) {
            return null;
        }
    }


private FinalLocalVariableMarker(UUID uuid) {
this.id = uuid;
}

@Override
public UUID getId() {
return id;
}

@Override
public <M extends Marker> M withId(UUID uuid) {
return (M) new FinalLocalVariableMarker(uuid);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,69 @@ void annotationDeclaration() throws Exception {
verify("AnnotationDeclaration");
}

@Test
void multiple() throws Exception {
verify("MultipleVariable");
}

@Test
void localVariable() throws Exception {
verify("LocalVariableOne");
}

@Test
void localVariableTwo() throws Exception {
verify("LocalVariableTwo");
}

@Test
void localVariableThree() throws Exception {
verify("LocalVariableThree");
}

@Test
void localVariableFour() throws Exception {
verify("LocalVariableFour");
}

@Test
void localVariableFive() throws Exception {
verify("LocalVariableFive");
}

@Test
void localVariableCheckRecord() throws Exception {
verify("LocalVariableCheckRecord");
}

@Test
void finalLocalVariable2One() throws Exception {
verify("FinalLocalVariable2One");
}

@Test
void finalLocalForLoop() throws Exception {
verify("VariableEnhancedForLoopVariable");
}

@Test
void finalLocalForLoop2() throws Exception {
verify("VariableEnhancedForLoopVariable2");
}

@Test
void localVariableAssignedMultipleTimes() throws Exception {
verify("LocalVariableAssignedMultipleTimes");
}

@Test
void localVariableCheckSwitchExpressions() throws Exception {
verify("LocalVariableCheckSwitchExpressions");
}

@Test
void localVariableCheckSwitchAssignment() throws Exception {
verify("LocalVariableCheckSwitchAssignment");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--- src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/finallocalvariable2one/InputFinalLocalVariable2One.java
+++ src/test/resources/org/checkstyle/autofix/recipe/finallocalvariable/finallocalvariable2one/OutputFinalLocalVariable2One.java
@@ -10,7 +10,7 @@
*/
package org.checkstyle.autofix.recipe.finallocalvariable.finallocalvariable2one;

-public class InputFinalLocalVariable2One {
+public class OutputFinalLocalVariable2One {
private int m_ClassVariable = 0;
//static block
static
@@ -49,9 +49,7 @@
}
};
}
-
- // violation below "Variable 'aArg' should be declared final"
- public void method(int aArg, final int aFinal, int aArg2)
+ public void method(final int aArg, final int aFinal, int aArg2)
{
int z = 0;

Loading
Loading