diff --git a/src/main/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscores.java b/src/main/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscores.java new file mode 100644 index 0000000000..81bb9bd44c --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscores.java @@ -0,0 +1,153 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import org.openrewrite.*; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.RenameVariable; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JLeftPadded; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.marker.Markers; + +import java.util.Iterator; + +import static java.util.Collections.emptyList; + +public class RemovePrivateFieldUnderscores extends Recipe { + + @Override + public String getDisplayName() { + return "Remove underscores from private class field names"; + } + + @Override + public String getDescription() { + return "Removes prefix or suffix underscores from private class field names, adding `this.` only where necessary to resolve ambiguity."; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaVisitor() { + @Override + public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations mv = (J.VariableDeclarations) super.visitVariableDeclarations(multiVariable, ctx); + if (!mv.hasModifier(J.Modifier.Type.Private)) { + return mv; + } + + Cursor parent = getCursor().getParentTreeCursor(); + if (!(parent.getValue() instanceof J.Block && parent.getParentTreeCursor().getValue() instanceof J.ClassDeclaration)) { + return mv; + } + + for (J.VariableDeclarations.NamedVariable variable : mv.getVariables()) { + String oldName = variable.getSimpleName(); + if (oldName.startsWith("_") || oldName.endsWith("_")) { + String newName = oldName.startsWith("_") ? oldName.substring(1) : oldName.substring(0, oldName.length() - 1); + if (newName.startsWith("_") || newName.endsWith("_") || newName.isEmpty()) { + continue; + } + + doAfterVisit(new QualifyAmbiguousFieldAccess(variable, newName)); + doAfterVisit(new RenameVariable(variable, newName)); + } + } + return mv; + } + }; + } + + private static class QualifyAmbiguousFieldAccess extends JavaVisitor { + private final JavaType.Variable fieldType; + private final String newFieldName; + + public QualifyAmbiguousFieldAccess(J.VariableDeclarations.NamedVariable field, String newFieldName) { + this.fieldType = field.getVariableType(); + this.newFieldName = newFieldName; + } + + @Override + public J visitIdentifier(J.Identifier identifier, ExecutionContext ctx) { + J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, ctx); + if (id.getFieldType() != null && id.getFieldType().equals(this.fieldType)) { + if (getCursor().getParentTreeCursor().getValue() instanceof J.VariableDeclarations.NamedVariable) { + return id; + } + + if (getCursor().getParentTreeCursor().getValue() instanceof J.FieldAccess) { + return id; + } + + if (!isAmbiguous()) { + return id; + } + + return new J.FieldAccess( + Tree.randomId(), + identifier.getPrefix(), + Markers.EMPTY, + new J.Identifier( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + emptyList(), + "this", + identifier.getType(), + null + ), + JLeftPadded.build(identifier.withPrefix(Space.EMPTY).withSimpleName(id.getSimpleName())), + identifier.getType() + ); + } + return id; + } + + private boolean isAmbiguous() { + for (Iterator it = getCursor().getPath(); it.hasNext(); ) { + Object scope = it.next(); + + if (scope instanceof J.MethodDeclaration) { + for (Statement param : ((J.MethodDeclaration) scope).getParameters()) { + if (param instanceof J.VariableDeclarations) { + J.VariableDeclarations.NamedVariable namedVar = ((J.VariableDeclarations) param).getVariables().get(0); + if (namedVar.getSimpleName().equals(newFieldName)) { + return true; + } + } + } + } + else if (scope instanceof J.Block) { + for (Statement statement : ((J.Block) scope).getStatements()) { + if (statement.getId().equals(getCursor().getValue())) { + break; + } + if (statement instanceof J.VariableDeclarations) { + for (J.VariableDeclarations.NamedVariable namedVar : ((J.VariableDeclarations) statement).getVariables()) { + if (namedVar.getSimpleName().equals(newFieldName)) { + return true; + } + } + } + } + } + } + return false; + } + } +} diff --git a/src/test/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscoresTest.java b/src/test/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscoresTest.java new file mode 100644 index 0000000000..8c6cfa8252 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/RemovePrivateFieldUnderscoresTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class RemovePrivateFieldUnderscoresTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new RemovePrivateFieldUnderscores()); + } + + @DocumentExample + @Test + void removesPrefixUnderscore() { + rewriteRun( + //language=java + java( + """ + public class ParseLocation { + private String _ruleName; + + public String getRuleName() { + return _ruleName; + } + + public void setRuleName(String ruleName) { + _ruleName = ruleName; + } + } + """, + """ + public class ParseLocation { + private String ruleName; + + public String getRuleName() { + return ruleName; + } + + public void setRuleName(String ruleName) { + this.ruleName = ruleName; + } + } + """ + ) + ); + } + + @Test + void removesSuffixUnderscore() { + rewriteRun( + //language=java + java( + """ + public class ParseLocation { + private String ruleName_; + + public String getRuleName() { + return ruleName_; + } + + public void setRuleName(String ruleName) { + ruleName_ = ruleName; + } + } + """, + """ + public class ParseLocation { + private String ruleName; + + public String getRuleName() { + return ruleName; + } + + public void setRuleName(String ruleName) { + this.ruleName = ruleName; + } + } + """ + ) + ); + } + + @Test + void doesNotAddThisWhenNoAmbiguity() { + rewriteRun( + //language=java + java( + """ + public class Calculator { + private int _operand1; + private int operand2_; + + public Calculator(int a, int b) { + _operand1 = a; + operand2_ = b; + } + + public int sum() { + return _operand1 + operand2_; + } + } + """, + """ + public class Calculator { + private int operand1; + private int operand2; + + public Calculator(int a, int b) { + operand1 = a; + operand2 = b; + } + + public int sum() { + return operand1 + operand2; + } + } + """ + ) + ); + } + + @Test + void addsThisWhenFieldIsShadowedByLocalVariable() { + rewriteRun( + //language=java + java( + """ + public class Calculator { + private int _operand1; + private int operand2_; + + public Calculator(int a, int b) { + int operand1 = 10; + // ... do something else with operand1 ... + _operand1 = a; + operand2_ = b; + } + + public int sum() { + int operand2 = 10; + // ... do something else with operand2 ... + return _operand1 + operand2_; + } + } + """, + """ + public class Calculator { + private int operand1; + private int operand2; + + public Calculator(int a, int b) { + int operand1 = 10; + // ... do something else with operand1 ... + this.operand1 = a; + operand2 = b; + } + + public int sum() { + int operand2 = 10; + // ... do something else with operand2 ... + return operand1 + this.operand2; + } + } + """ + ) + ); + } + + @Test + void handlesFieldsAlreadyQualifiedWithThis() { + rewriteRun( + //language=java + java( + """ + public class ParseLocation { + private String _ruleName; + + public String getRuleName() { + return this._ruleName; + } + + public void setRuleName(String ruleName) { + this._ruleName = ruleName; + } + } + """, + """ + public class ParseLocation { + private String ruleName; + + public String getRuleName() { + return this.ruleName; + } + + public void setRuleName(String ruleName) { + this.ruleName = ruleName; + } + } + """ + ) + ); + } + + @Test + void doesNotChangeNonPrivateFields() { + rewriteRun( + //language=java + java( + """ + public class MyClass { + public String _publicField; + protected String _protectedField; + String packagePrivateField_; + } + """ + ) + ); + } + + @Test + void doesNotChangeLocalVariables() { + rewriteRun( + //language=java + java( + """ + public class MyClass { + public String myMethod(String str) { + String _str = bang(str); + String str_ = "str_"; + return _str; + } + + private String bang(String s) { + return s; + } + } + """ + ) + ); + } + + @Test + void doesNotChangeReservedKeyword() { + rewriteRun( + //language=java + java( + """ + public class MyClass { + private String class_; + private String _class; + } + """ + ) + ); + } + + @Test + void doesNotRenameToUnnamedVariable() { + rewriteRun( + //language=java + java( + """ + class MyClass { + private String __; + private int ___; + } + """ + ) + ); + } +}