Skip to content

Conversation

@motlin
Copy link

@motlin motlin commented Nov 25, 2025

What's changed?

Added a new ExplicitThis recipe that automatically adds the explicit this. prefix to instance field accesses and method invocations.

What's your motivation?

This recipe improves code clarity by making it immediately obvious when a field or method belongs to the current instance versus being a local variable, parameter, or static member.
The explicit this. prefix follows a common coding style preference in Java codebases.

Anything in particular you'd like reviewers to focus on?

I wound up writing quite a bit of code to determine whether we're looking at a non-static field that's not already prefixed with this. Is there a better way?

Should I register the recipe in any suite? Should I add the recipe to common-static-analysis.yml (commented out initially)?

Anyone you would like to review specifically?

Have you considered any alternatives or workarounds?

An alternative would be to make this configurable (e.g., only apply to fields, or only to methods), but starting with a simple "always add this." approach keeps the recipe straightforward.

Any additional context

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

@greg-at-moderne
Copy link
Contributor

Can you add a test with a typical setter implementation?
This is probably the most common example where of variable name shadowing. One being a field and another being a local.

import org.openrewrite.marker.Markers;

import java.time.Duration;
import java.util.Collections;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.util.Collections;
import static java.util.Collections.emptyList;

Comment on lines 75 to 80
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext executionContext) {
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
this.isInsideFieldAccess = true;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext executionContext) {
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
this.isInsideFieldAccess = true;
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext ctx) {
J result = super.visitFieldAccess(fieldAccess, ctx);
public J visitIdentifier(J.Identifier identifier, ExecutionContext ctx) {
J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, ctx);

Comment on lines 125 to 130
public J visitBlock(J.Block block, ExecutionContext executionContext) {
if (!block.isStatic()) {
return super.visitBlock(block, executionContext);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitBlock(J.Block block, ExecutionContext executionContext) {
if (!block.isStatic()) {
return super.visitBlock(block, executionContext);
}
public J visitBlock(J.Block block, ExecutionContext ctx) {
return super.visitBlock(block, ctx);
J.Block result = (J.Block) super.visitBlock(block, ctx);
public J visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {

this.isStatic = (methodType.getFlagsBitMap() & 0x0008L) != 0;
}

J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, executionContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, executionContext);
J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, ctx);

Comment on lines 157 to 161
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, executionContext);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, executionContext);
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
if ("super".equals(m.getName().getSimpleName()) || "this".equals(m.getName().getSimpleName())) {

String currentClassName = this.getSimpleClassName(currentClassType.getFullyQualifiedName());
boolean currentIsAnonymous = this.isAnonymousClassName(currentClassName);
return new ClassContext(currentClassType, currentIsAnonymous);
} else if (currentCursor.getValue() instanceof J.NewClass) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (currentCursor.getValue() instanceof J.NewClass) {
}
if (currentCursor.getValue() instanceof J.NewClass) {

spec.recipe(new ExplicitThis());
}

@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Test
@Test

Test(String value) {
super(value);
// Shadowed parameter: looks like a bug but replacing would change semantics
value = value;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test with a typical setter implementation? This is probably the most common example where of variable name shadowing. One being a field and another being a local.

@greg-at-moderne I added tests for value = value in this constructor, and field = field in a setter. While these are "obvious" bugs - setting a parameter to itself - it was a deliberate choice not to replace these and the test shows they do not get replaced. The replacement would change semantics. It's not a bad idea to find and fix these but my preference is to separate recipes that may change semantics from recipes that perform pure refactorings and don't need much scrutiny. What do you think?

Comment on lines 32 to 33
import java.time.Duration;
import java.util.Collections;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.time.Duration;
import java.util.Collections;
import java.time.Duration;

Comment on lines 77 to 80
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext executionContext) {
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
this.isInsideFieldAccess = true;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext executionContext) {
boolean previousIsInsideFieldAccess = this.isInsideFieldAccess;
this.isInsideFieldAccess = true;
public J visitFieldAccess(FieldAccess fieldAccess, ExecutionContext ctx) {
J result = super.visitFieldAccess(fieldAccess, ctx);
public J visitIdentifier(J.Identifier identifier, ExecutionContext ctx) {
J.Identifier id = (J.Identifier) super.visitIdentifier(identifier, ctx);

Comment on lines 127 to 130
public J visitBlock(J.Block block, ExecutionContext executionContext) {
if (!block.isStatic()) {
return super.visitBlock(block, executionContext);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitBlock(J.Block block, ExecutionContext executionContext) {
if (!block.isStatic()) {
return super.visitBlock(block, executionContext);
}
public J visitBlock(J.Block block, ExecutionContext ctx) {
return super.visitBlock(block, ctx);
J.Block result = (J.Block) super.visitBlock(block, ctx);
public J visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {

this.isStatic = (methodType.getFlagsBitMap() & 0x0008L) != 0;
}

J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, executionContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, executionContext);
J.MethodDeclaration result = (J.MethodDeclaration) super.visitMethodDeclaration(method, ctx);

Comment on lines 159 to 161
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, executionContext);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, executionContext);
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
if ("super".equals(m.getName().getSimpleName()) || "this".equals(m.getName().getSimpleName())) {

Comment on lines +209 to +208
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
p == Cursor.ROOT_VALUE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
p == Cursor.ROOT_VALUE
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
p == Cursor.ROOT_VALUE

String currentClassName = this.getSimpleClassName(currentClassType.getFullyQualifiedName());
boolean currentIsAnonymous = this.isAnonymousClassName(currentClassName);
return new ClassContext(currentClassType, currentIsAnonymous);
} else if (currentCursor.getValue() instanceof J.NewClass) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (currentCursor.getValue() instanceof J.NewClass) {
}
if (currentCursor.getValue() instanceof J.NewClass) {

spec.recipe(new ExplicitThis());
}

@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Test
@Test

return m;
}

if (m.getName().getSimpleName().equals("super") || m.getName().getSimpleName().equals("this")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (m.getName().getSimpleName().equals("super") || m.getName().getSimpleName().equals("this")) {
if ("super".equals(m.getName().getSimpleName()) || "this".equals(m.getName().getSimpleName())) {

Comment on lines +203 to +207
@Nullable
private ClassContext getCurrentClassContext() {
Cursor currentCursor = this.getCursor().dropParentUntil(p ->
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private ClassContext getCurrentClassContext() {
Cursor currentCursor = this.getCursor().dropParentUntil(p ->
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
private @Nullable ClassContext getCurrentClassContext() {
p instanceof J.ClassDeclaration ||
(p instanceof J.NewClass && ((J.NewClass) p).getBody() != null) ||
p == Cursor.ROOT_VALUE

String currentClassName = this.getSimpleClassName(currentClassType.getFullyQualifiedName());
boolean currentIsAnonymous = this.isAnonymousClassName(currentClassName);
return new ClassContext(currentClassType, currentIsAnonymous);
} else if (currentCursor.getValue() instanceof J.NewClass) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (currentCursor.getValue() instanceof J.NewClass) {
}
if (currentCursor.getValue() instanceof J.NewClass) {

Comment on lines +231 to +232
@Nullable
private Expression createQualifiedThisExpression(ClassContext currentContext, JavaType.FullyQualified targetType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private Expression createQualifiedThisExpression(ClassContext currentContext, JavaType.FullyQualified targetType) {
private @Nullable Expression createQualifiedThisExpression(ClassContext currentContext, JavaType.FullyQualified targetType) {

spec.recipe(new ExplicitThis());
}

@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Test
@Test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants