Choose an unused name and rename references for underscore identifiers - #1194
Draft
martinfrancois wants to merge 4 commits into
Draft
Conversation
The recipe hard coded `__` as the replacement and never checked whether that name was already declared, so a parameter, local variable, field, method or nested class sitting next to an existing `__` was collapsed onto one spelling and the output no longer compiled. The class path renamed only the class declaration, leaving the field, return, parameter and `new` expression types spelled `_`, which Java 9 and later reject, and it renamed the explicit constructor through the method path, which overwrote the `<constructor>` method type name. The replacement now grows by another underscore until the name is free: a variable or class rename skips every name used in its source file, and a method rename skips the method names of the type declaring the root of the override chain, so overriding declarations and callers in other compilation units settle on the same name. A renamed class now also renames the identifiers bound to it and its explicit constructors, whose method type keeps its `<constructor>` identity. Because a class rename can also rename `_.java`, the recipe becomes a ScanningRecipe that collects every source path and skips candidates whose `.java` file is already occupied next to it, so the rename never overwrites a sibling source file. Two limits a reviewer should weigh: type references are updated only inside the compilation unit that declares the type, and a collision with a declaration the recipe cannot see, such as one in a subclass in another file, remains possible. Both are stated in the recipe description, and recipes.csv is updated to match.
4 tasks
4 tasks
martinfrancois
marked this pull request as draft
August 16, 2026 01:10
martinfrancois
force-pushed
the
fix/rename-underscore-identifier-avoid-collisions
branch
from
August 16, 2026 20:20
d01d231 to
8d8cf64
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suggested review order: 43 of 52 (Score: 1.5)
Review first: openrewrite/rewrite-static-analysis#991
What's changed?
RenameUnderscoreIdentifiernow renames_to a name that is not already in use, instead of always renaming it to__. When it renames a class, it now also renames the references to that class, not only its declaration.The recipe picks the first name in the sequence
__,___,____and so on that is free. For a variable or a class, a name is free when no identifier anywhere in the source file being visited uses it, and no field or method declared on or inherited by a class that the same file declares uses it. Avoiding inherited names matters because such a rename would not collide visibly: JLS 6.4 says a declaration of a field, a method or a type namednshadows every other declaration ofnin scope where it occurs, so the simple names in that class would silently rebind.For a method the source file is not consulted. The name is derived from the root of the override chain, reached through
TypeUtils.findOverriddenMethod, and from the method names of that root's declaring type, its supertypes and its interfaces. Field names are not consulted, since a field and a method may share a name in Java. Every source file that declares, overrides or calls the method reaches the same root and reads the same hierarchy, so they all compute the same name without seeing each other.Renaming a top level class also renames its source file, so the recipe is now a
ScanningRecipe<Set<Path>>that collects the source path of every file in the run. A candidate name is skipped when a source file already exists at<candidate>.javabeside the file being renamed, so the renamed compilation unit is never written over an existing source file.A method declaration, a method invocation, a method reference or a class declaration with no type attribution is now left unchanged, whereas main renamed it: the type is what the new code needs, both to compute a name that other source files agree on and to find the references to a renamed class. That deliberately reduces what the recipe does, since
_then survives into the output and the Java 9 keyword error with it, while main renamed the declaration to__with no collision check of any kind. Variable renames do not use type attribution and are unaffected.What's your motivation?
Recipe:
org.openrewrite.java.migrate.lang.RenameUnderscoreIdentifier.Before
Actual after the recipe
Expected after the recipe
The output on main does not compile, in two different ways. A rename that collides with an existing declaration gives a duplicate declaration error,
variable __ is already defined in method sumfor the example above. A class declaration that main renames while leaving the references to it spelled_givesas of release 9, '_' is a keywordat each of those references. In both cases the recipe reports the file as changed and raises no error, so a build sees a run that succeeded and source that does not compile.RenameUnderscoreIdentifierruns insideJava8toJava11, which bothUpgradeToJava17andUpgradeToJava21include, so any of those three upgrades can produce it. Reproduced on 3.41.0 and on current main.Affected code in real projects
yinwang0/rubysonar_.java: a Java 7 static analyzer whose utility class is literally named_and calls itself with qualified references such as_.die(..)at lines 99, 167, 217 and 530; the recipe from main renames the declaration toclass __while leaving those references spelled_, so each of them hits the release-9 keyword error and the file no longer compiles.Anything in particular you'd like reviewers to focus on?
No existing test changed its expectation, and 905 of the 1137 added lines are the test file. One added and one deleted line are the regenerated
recipes.csvrow; the rest is the implementation.Three limits:
__declared in a file whose own name is not__.java, for example a second, package private top level class in another file of the same package, is invisible to the check.___whenever another declaration anywhere in the source file already uses__, even when__is free in the enclosing scope.The accumulator is a flat
Set<Path>rather than a map keyed byJavaProject, so a multi-project run can hold source paths belonging to another project. Such a path can only make the recipe skip a candidate name and move on to the next one; it can never make it choose a name that collides.Have you considered any alternatives or workarounds?
When the file name that a class rename would need is already occupied by another source file, this pull request skips that candidate name and moves on to the next free one. The alternative is to leave that class unrenamed, which keeps the
_keyword error in the output. The choice lives in one place,availableName(JavaType.FullyQualified), so switching to the other behaviour is a few lines there.Any additional context
Pre-existing tests changed: None.
This change adds 24 tests to
RenameUnderscoreIdentifierTest. Without the code change in this pull request, 18 of them fail. They cover five shapes:parameterCollision;inheritedMemberNameIsNotShadowed;nestedTypeRenamedInEveryReferencePosition;callersInAnotherSourceFileFollowTheDeclaration;classFileRenameDoesNotOverwriteACollidingClassFile.The full list is the test file in this change. All 18 are new tests; the 11 that already existed keep their expectations. The other 6 new tests pass either way and pin down behaviour this change keeps.
This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and this description.
I ran the formatter with the repository's
.editorconfig. It also wanted to re-indent lines that this change does not touch, so I left those alone and kept the diff limited to this change.Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv