Skip to content

Add ExplicitThis recipe to make this. prefix explicit.#791

Merged
timtebeek merged 9 commits intoopenrewrite:mainfrom
motlin:explicit-this
Mar 21, 2026
Merged

Add ExplicitThis recipe to make this. prefix explicit.#791
timtebeek merged 9 commits intoopenrewrite:mainfrom
motlin:explicit-this

Conversation

@motlin
Copy link
Copy Markdown
Contributor

@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
Copy Markdown
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.

Test(String value) {
super(value);
// Shadowed parameter: looks like a bug but replacing would change semantics
value = value;
Copy link
Copy Markdown
Contributor 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?

@motlin motlin force-pushed the explicit-this branch 2 times, most recently from a556fe4 to d379d75 Compare November 26, 2025 14:09
@timtebeek timtebeek self-requested a review January 12, 2026 12:27
@timtebeek timtebeek moved this from In Progress to Ready to Review in OpenRewrite Jan 15, 2026
@motlin motlin force-pushed the explicit-this branch 3 times, most recently from 760a442 to 49ce8ae Compare January 30, 2026 22:24
@motlin motlin force-pushed the explicit-this branch 2 times, most recently from 3450bc5 to bf790bb Compare March 12, 2026 01:00
Copy link
Copy Markdown
Member

@timtebeek timtebeek left a comment

Choose a reason for hiding this comment

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

I ran this recipe at scale against the Spring + Netflix organizations (~3000+ Java files across 77 repos) and found two bugs, plus several code simplification opportunities. I've pushed fixes to a branch with the suggested changes.

Bug fixes

1. Inherited members incorrectly qualified as SuperClass.this.method()

When a field or method is declared in a superclass, the recipe was generating AbstractBinderTests.this.getBinder() instead of this.getBinder(). This affected 763 out of 3074 patches.

Root cause: createQualifiedThisExpression only checked exact type match (isOfType). When the current class inherits from the declaring class, the types don't match exactly, so it fell through to createOuterThisReference.

Fix: Added TypeUtils.isAssignableTo(targetType.getFullyQualifiedName(), currentContext.type) to check if the current class is a subtype of the declaring class. Uses the FQN-based overload to handle parameterized types correctly.

2. Recipe was modifying Kotlin files

54 Kotlin files were incorrectly modified. Added Preconditions.check(Preconditions.not(new KotlinFileChecker<>()), ...) to exclude Kotlin, consistent with other Java-specific recipes.

Code simplifications

  • Replace magic 0x0008L with hasFlags(Flag.Static) and method.hasModifier(J.Modifier.Type.Static) (3 occurrences)
  • Use JavaElementFactory.newThis() instead of manual J.Identifier construction (2 occurrences)
  • Use TypeUtils.isOfType() for type comparison instead of FQN string equality
  • Simplify createFieldAccess by accepting pre-validated owner type parameter, removing redundant null checks
  • Use instanceof JavaType.FullyQualified (broader) instead of instanceof JavaType.Class

Net result: -55 lines, +16 lines.

Validation results (after fixes)

  • 3020 Java patches, 0 Kotlin patches
  • 108 remaining OuterClass.this. patterns are all legitimate (inner classes, @Nested test classes, anonymous classes)

Branch with changes: explicit-this on upstream

@github-project-automation github-project-automation bot moved this from Ready to Review to In Progress in OpenRewrite Mar 14, 2026
@knutwannheden
Copy link
Copy Markdown
Contributor

I guess it gets even more interesting when the inner class also extends its outer class. Would be good to have a test for.

@motlin
Copy link
Copy Markdown
Contributor Author

motlin commented Mar 15, 2026

@timtebeek this is great, thank you. I realize I could have found the first bug myself by first deleting this everywhere in my codebases and then re-running the recipe. The second one would have been hard for me since I don't work on any Kotlin codebases. I've taken all three of our commits, rebased on upstream/main, and pushed to this PR. Is it ready to land? Thank you!

@knutwannheden
Copy link
Copy Markdown
Contributor

Added test cases I described earlier.

motlin and others added 2 commits March 17, 2026 10:47
- Replace magic number 0x0008L with hasFlags(Flag.Static) and hasModifier()
- Use JavaElementFactory.newThis() instead of manual identifier construction
- Use TypeUtils.isOfType() for type comparison instead of FQN string equality
- Simplify createFieldAccess by accepting pre-validated owner type parameter
- Use instanceof JavaType.FullyQualified for safer type checking
timtebeek and others added 2 commits March 17, 2026 10:47
- Use TypeUtils.isAssignableTo with FQN to handle inherited fields/methods
  from superclasses (including parameterized types) — use plain this. instead
  of incorrectly generating SuperClass.this.
- Add Preconditions.check to exclude Kotlin files via KotlinFileChecker
When a nested class C accesses a member declared in A through an
intermediate class B that extends A, the recipe now correctly produces
B.this.field instead of A.this.field. This is done by walking the
enclosing class chain to find the nearest class assignable to the
declaring type.

Also fixes JavaToCsharp compilation for updated rewrite-csharp API.
@motlin
Copy link
Copy Markdown
Contributor Author

motlin commented Mar 18, 2026

Anything left to do here?

@timtebeek timtebeek moved this from In Progress to Ready to Review in OpenRewrite Mar 21, 2026
@timtebeek timtebeek changed the title Add ExplicitThis recipe to make 'this.' prefix explicit. Add ExplicitThis recipe to make this. prefix explicit. Mar 21, 2026
Copy link
Copy Markdown
Member

@timtebeek timtebeek left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the help (and patience) here! Looks good after some light polish, and at scale validation. That latter part is what took a little time to get to, but good to have this hardened and added now.

I've not yet added this to any larger sets, as it's a bit of an opinionated change, and it might be good that you try this copy on your own projects again to ensure that it's making all the right changes.

@timtebeek timtebeek merged commit 5d7e7b1 into openrewrite:main Mar 21, 2026
1 check passed
@github-project-automation github-project-automation bot moved this from Ready to Review to Done in OpenRewrite Mar 21, 2026
@greg-at-moderne
Copy link
Copy Markdown
Contributor

I just want to confirm what Tim wrote. Thanks for the contribution! It wasn't an easy one.

@motlin motlin deleted the explicit-this branch March 22, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants