Skip to content

Commit c485d95

Browse files
committed
Add ExplicitThis recipe to make 'this.' prefix explicit.
1 parent 6eab4c9 commit c485d95

File tree

2 files changed

+662
-0
lines changed

2 files changed

+662
-0
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.staticanalysis;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.*;
21+
import org.openrewrite.java.JavaVisitor;
22+
import org.openrewrite.java.tree.Expression;
23+
import org.openrewrite.java.tree.J;
24+
import org.openrewrite.java.tree.J.FieldAccess;
25+
import org.openrewrite.java.tree.J.Identifier;
26+
import org.openrewrite.java.tree.JLeftPadded;
27+
import org.openrewrite.java.tree.JavaType;
28+
import org.openrewrite.java.tree.JavaType.Method;
29+
import org.openrewrite.java.tree.Space;
30+
import org.openrewrite.marker.Markers;
31+
32+
import java.time.Duration;
33+
import java.util.Collections;
34+
35+
@Value
36+
@EqualsAndHashCode(callSuper = false)
37+
public class ExplicitThis extends Recipe {
38+
39+
@Override
40+
public String getDisplayName() {
41+
return "`field` → `this.field`";
42+
}
43+
44+
@Override
45+
public String getDescription() {
46+
return "Add explicit 'this.' prefix to field and method access.";
47+
}
48+
49+
@Override
50+
public Duration getEstimatedEffortPerOccurrence() {
51+
return Duration.ofSeconds(5);
52+
}
53+
54+
@Override
55+
public TreeVisitor<?, ExecutionContext> getVisitor() {
56+
return new ExplicitThisVisitor();
57+
}
58+
59+
private static final class ExplicitThisVisitor extends JavaVisitor<ExecutionContext> {
60+
61+
private boolean isStatic;
62+
private boolean isInsideFieldAccess;
63+
64+
private static class ClassContext {
65+
final JavaType.FullyQualified type;
66+
final boolean isAnonymous;
67+
68+
ClassContext(JavaType.FullyQualified type, boolean isAnonymous) {
69+
this.type = type;
70+
this.isAnonymous = isAnonymous;
71+
}
72+
}
73+
74+
@Override
75+
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext executionContext) {
76+
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
77+
this.isInsideFieldAccess = true;
78+
79+
J result = super.visitFieldAccess(fieldAccess, executionContext);
80+
81+
this.isInsideFieldAccess = previousIsInsideFieldAccess;
82+
return result;
83+
}
84+
85+
@Override
86+
public J visitIdentifier(J.Identifier identifier, ExecutionContext executionContext) {
87+
J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, executionContext);
88+
89+
if (this.isStatic) {
90+
return id;
91+
}
92+
93+
if (this.isInsideFieldAccess) {
94+
return id;
95+
}
96+
97+
JavaType.Variable fieldType = id.getFieldType();
98+
if (fieldType == null) {
99+
return id;
100+
}
101+
102+
if (fieldType.getOwner() == null || !(fieldType.getOwner() instanceof JavaType.Class)) {
103+
return id;
104+
}
105+
106+
// Skip static fields - check the Modifier.STATIC flag (0x0008)
107+
if ((fieldType.getFlagsBitMap() & 0x0008L) != 0) {
108+
return id;
109+
}
110+
111+
String name = id.getSimpleName();
112+
if ("this".equals(name) || "super".equals(name)) {
113+
return id;
114+
}
115+
116+
if (this.isPartOfDeclaration()) {
117+
return id;
118+
}
119+
120+
J.FieldAccess fieldAccess = this.createFieldAccess(id);
121+
return fieldAccess != null ? fieldAccess : id;
122+
}
123+
124+
@Override
125+
public J visitBlock(J.Block block, ExecutionContext executionContext) {
126+
if (!block.isStatic()) {
127+
return super.visitBlock(block, executionContext);
128+
}
129+
130+
boolean previousStatic = this.isStatic;
131+
this.isStatic = true;
132+
133+
J.Block result = (J.Block) super.visitBlock(block, executionContext);
134+
135+
this.isStatic = previousStatic;
136+
return result;
137+
}
138+
139+
@Override
140+
public J visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
141+
boolean previousStatic = this.isStatic;
142+
143+
JavaType.Method methodType = method.getMethodType();
144+
if (methodType != null) {
145+
// Check if the method is static - set isStatic flag using Modifier.STATIC (0x0008)
146+
this.isStatic = (methodType.getFlagsBitMap() & 0x0008L) != 0;
147+
}
148+
149+
J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, executionContext);
150+
151+
this.isStatic = previousStatic;
152+
153+
return result;
154+
}
155+
156+
@Override
157+
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
158+
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, executionContext);
159+
160+
if (this.isStatic) {
161+
return m;
162+
}
163+
164+
if (m.getName().getSimpleName().equals("super") || m.getName().getSimpleName().equals("this")) {
165+
return m;
166+
}
167+
168+
Method methodType = m.getMethodType();
169+
// Skip if already qualified, type info is missing, or the method is static (Modifier.STATIC = 0x0008)
170+
if (
171+
m.getSelect() != null ||
172+
methodType == null ||
173+
(methodType.getFlagsBitMap() & 0x0008L) != 0
174+
) {
175+
return m;
176+
}
177+
178+
JavaType.FullyQualified methodOwnerType = methodType.getDeclaringType();
179+
if (methodOwnerType == null) {
180+
return m;
181+
}
182+
183+
ClassContext currentContext = this.getCurrentClassContext();
184+
if (currentContext == null) {
185+
return m;
186+
}
187+
188+
Expression thisExpression = this.createQualifiedThisExpression(currentContext, methodOwnerType);
189+
if (thisExpression == null) {
190+
return m;
191+
}
192+
193+
return m.withSelect(thisExpression);
194+
}
195+
196+
private boolean isPartOfDeclaration() {
197+
Cursor parent = this.getCursor().getParent();
198+
if (parent == null || !(parent.getValue() instanceof J.VariableDeclarations.NamedVariable)) {
199+
return false;
200+
}
201+
J.VariableDeclarations.NamedVariable namedVar = (J.VariableDeclarations.NamedVariable) parent.getValue();
202+
return namedVar.getName() == this.getCursor().getValue();
203+
}
204+
205+
private ClassContext getCurrentClassContext() {
206+
Cursor currentCursor = this.getCursor().dropParentUntil(p ->
207+
p instanceof J.ClassDeclaration ||
208+
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
209+
p == Cursor.ROOT_VALUE
210+
);
211+
212+
if (currentCursor.getValue() instanceof J.ClassDeclaration) {
213+
J.ClassDeclaration currentClass = currentCursor.getValue();
214+
JavaType.FullyQualified currentClassType = currentClass.getType();
215+
if (currentClassType == null) {
216+
return null;
217+
}
218+
String currentClassName = this.getSimpleClassName(currentClassType.getFullyQualifiedName());
219+
boolean currentIsAnonymous = this.isAnonymousClassName(currentClassName);
220+
return new ClassContext(currentClassType, currentIsAnonymous);
221+
} else if (currentCursor.getValue() instanceof J.NewClass) {
222+
J.NewClass newClass = currentCursor.getValue();
223+
JavaType type = newClass.getType();
224+
if (!(type instanceof JavaType.FullyQualified)) {
225+
return null;
226+
}
227+
return new ClassContext((JavaType.FullyQualified) type, true);
228+
}
229+
return null;
230+
}
231+
232+
private Expression createQualifiedThisExpression(ClassContext currentContext, JavaType.FullyQualified targetType) {
233+
if (currentContext.type.getFullyQualifiedName().equals(targetType.getFullyQualifiedName())) {
234+
return new Identifier(
235+
Tree.randomId(),
236+
Space.EMPTY,
237+
Markers.EMPTY,
238+
Collections.emptyList(),
239+
"this",
240+
currentContext.type,
241+
null
242+
);
243+
}
244+
245+
if (currentContext.isAnonymous) {
246+
String ownerClassName = this.getSimpleClassName(targetType.getFullyQualifiedName());
247+
if (this.isAnonymousClassName(ownerClassName)) {
248+
return null;
249+
}
250+
return this.createOuterThisReference(targetType, ownerClassName);
251+
}
252+
253+
String simpleClassName = this.getSimpleClassName(targetType.getFullyQualifiedName());
254+
return this.createOuterThisReference(targetType, simpleClassName);
255+
}
256+
257+
private J.FieldAccess createOuterThisReference(JavaType.FullyQualified ownerType, String simpleClassName) {
258+
J.Identifier outerClassIdentifier = new J.Identifier(
259+
Tree.randomId(),
260+
Space.EMPTY,
261+
Markers.EMPTY,
262+
Collections.emptyList(),
263+
simpleClassName,
264+
ownerType,
265+
null
266+
);
267+
268+
J.Identifier thisIdentifier = new J.Identifier(
269+
Tree.randomId(),
270+
Space.EMPTY,
271+
Markers.EMPTY,
272+
Collections.emptyList(),
273+
"this",
274+
ownerType,
275+
null
276+
);
277+
278+
return new J.FieldAccess(
279+
Tree.randomId(),
280+
Space.EMPTY,
281+
Markers.EMPTY,
282+
outerClassIdentifier,
283+
JLeftPadded.build(thisIdentifier),
284+
null
285+
);
286+
}
287+
288+
private J.FieldAccess createFieldAccess(J.Identifier identifier) {
289+
JavaType.Variable fieldType = identifier.getFieldType();
290+
if (fieldType == null || fieldType.getOwner() == null) {
291+
return null;
292+
}
293+
294+
JavaType.FullyQualified fieldOwnerType = (JavaType.FullyQualified) fieldType.getOwner();
295+
296+
ClassContext currentContext = this.getCurrentClassContext();
297+
if (currentContext == null) {
298+
return null;
299+
}
300+
301+
Expression thisExpression = this.createQualifiedThisExpression(currentContext, fieldOwnerType);
302+
if (thisExpression == null) {
303+
return null;
304+
}
305+
306+
return new J.FieldAccess(
307+
Tree.randomId(),
308+
identifier.getPrefix(),
309+
Markers.EMPTY,
310+
thisExpression,
311+
JLeftPadded.build(identifier.withPrefix(Space.EMPTY)),
312+
identifier.getFieldType()
313+
);
314+
}
315+
316+
/**
317+
* Extracts the simple class name from a fully qualified class name.
318+
* Handles both package-separated names (dots) and inner class separators (dollar signs).
319+
* Examples: "com.example.Outer$Inner" -> "Inner", "com.example.Outer" -> "Outer"
320+
*/
321+
private String getSimpleClassName(String fullyQualifiedName) {
322+
int lastDot = fullyQualifiedName.lastIndexOf('.');
323+
int lastDollar = fullyQualifiedName.lastIndexOf('$');
324+
int lastSeparator = Math.max(lastDot, lastDollar);
325+
return lastSeparator >= 0 ? fullyQualifiedName.substring(lastSeparator + 1) : fullyQualifiedName;
326+
}
327+
328+
/**
329+
* Detects if a class name represents an anonymous class.
330+
* Anonymous classes are identified by numeric names (generated by the compiler as 1, 2, 3, etc.).
331+
*/
332+
private boolean isAnonymousClassName(String simpleName) {
333+
if (simpleName == null || simpleName.isEmpty()) {
334+
return false;
335+
}
336+
return Character.isDigit(simpleName.charAt(0));
337+
}
338+
}
339+
}

0 commit comments

Comments
 (0)