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, ExecutionContext> 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 ___;
+ }
+ """
+ )
+ );
+ }
+}