Skip to content

Commit d379d75

Browse files
committed
Add ExplicitThis recipe to make 'this.' prefix explicit.
1 parent 65b6188 commit d379d75

File tree

2 files changed

+678
-0
lines changed

2 files changed

+678
-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.jspecify.annotations.Nullable;
21+
import org.openrewrite.*;
22+
import org.openrewrite.java.JavaVisitor;
23+
import org.openrewrite.java.tree.Expression;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.J.FieldAccess;
26+
import org.openrewrite.java.tree.J.Identifier;
27+
import org.openrewrite.java.tree.JLeftPadded;
28+
import org.openrewrite.java.tree.JavaType;
29+
import org.openrewrite.java.tree.JavaType.Method;
30+
import org.openrewrite.java.tree.Space;
31+
import org.openrewrite.marker.Markers;
32+
33+
import java.time.Duration;
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 ctx) {
78+
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
79+
this.isInsideFieldAccess = true;
80+
81+
J result = super.visitFieldAccess(fieldAccess, ctx);
82+
83+
this.isInsideFieldAccess = previousIsInsideFieldAccess;
84+
return result;
85+
}
86+
87+
@Override
88+
public J visitIdentifier(J.Identifier identifier, ExecutionContext ctx) {
89+
J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, ctx);
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 ctx) {
128+
if (!block.isStatic()) {
129+
return super.visitBlock(block, ctx);
130+
}
131+
132+
boolean previousStatic = this.isStatic;
133+
this.isStatic = true;
134+
135+
J.Block result = (J.Block) super.visitBlock(block, ctx);
136+
137+
this.isStatic = previousStatic;
138+
return result;
139+
}
140+
141+
@Override
142+
public J visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
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, ctx);
152+
153+
this.isStatic = previousStatic;
154+
155+
return result;
156+
}
157+
158+
@Override
159+
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
160+
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
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+
ClassContext currentContext = this.getCurrentClassContext();
181+
if (currentContext == null) {
182+
return m;
183+
}
184+
185+
JavaType.FullyQualified methodOwnerType = methodType.getDeclaringType();
186+
Expression thisExpression = this.createQualifiedThisExpression(currentContext, methodOwnerType);
187+
if (thisExpression == null) {
188+
return m;
189+
}
190+
191+
return m.withSelect(thisExpression);
192+
}
193+
194+
private boolean isPartOfDeclaration() {
195+
Cursor parent = this.getCursor().getParent();
196+
if (parent == null || !(parent.getValue() instanceof J.VariableDeclarations.NamedVariable)) {
197+
return false;
198+
}
199+
J.VariableDeclarations.NamedVariable namedVar = parent.getValue();
200+
return namedVar.getName() == this.getCursor().getValue();
201+
}
202+
203+
@Nullable
204+
private ClassContext getCurrentClassContext() {
205+
Cursor currentCursor = this.getCursor().dropParentUntil(p ->
206+
p instanceof J.ClassDeclaration ||
207+
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
208+
p == Cursor.ROOT_VALUE
209+
);
210+
211+
if (currentCursor.getValue() instanceof J.ClassDeclaration) {
212+
J.ClassDeclaration currentClass = currentCursor.getValue();
213+
JavaType.FullyQualified currentClassType = currentClass.getType();
214+
if (currentClassType == null) {
215+
return null;
216+
}
217+
String currentClassName = this.getSimpleClassName(currentClassType.getFullyQualifiedName());
218+
boolean currentIsAnonymous = this.isAnonymousClassName(currentClassName);
219+
return new ClassContext(currentClassType, currentIsAnonymous);
220+
} else if (currentCursor.getValue() instanceof J.NewClass) {
221+
J.NewClass newClass = currentCursor.getValue();
222+
JavaType type = newClass.getType();
223+
if (!(type instanceof JavaType.FullyQualified)) {
224+
return null;
225+
}
226+
return new ClassContext((JavaType.FullyQualified) type, true);
227+
}
228+
return null;
229+
}
230+
231+
@Nullable
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+
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+
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+
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.@Nullable 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.isEmpty()) {
334+
return false;
335+
}
336+
return Character.isDigit(simpleName.charAt(0));
337+
}
338+
}
339+
}

0 commit comments

Comments
 (0)