-
Notifications
You must be signed in to change notification settings - Fork 5
Issue #80: Resolve MultiVariableDeclaration Bug #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
||
| /** | ||
|
|
@@ -56,17 +60,94 @@ public String getDescription() { | |
|
|
||
| @Override | ||
| public TreeVisitor<?, ExecutionContext> getVisitor() { | ||
| return new LocalVariableVisitor(); | ||
| return new JavaIsoVisitor<>() { | ||
| @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> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
|
|
||
|
|
@@ -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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Could you please provide an example of a variable declaration that won't be handled by
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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()); | ||
rdiachenko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need id? couldn't find usages of this field
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Marker interface contains abstract method like 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 |
|---|---|---|
| @@ -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; | ||
|
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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