Skip to content

Commit 1e30b0d

Browse files
committed
Add ExplicitThis recipe to make 'this.' prefix explicit.
1 parent df434ea commit 1e30b0d

File tree

2 files changed

+680
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)