Qualify generated java.util.Arrays calls when the name is shadowed - #967
Draft
martinfrancois wants to merge 3 commits into
Draft
Conversation
RemoveHashCodeCallsFromArrayInstances and
RemoveToStringCallsFromArrayInstances both emitted a bare
Arrays.hashCode(..) or Arrays.toString(..) and relied on
maybeAddImport("java.util.Arrays"). Adding an import does not make a
simple name unambiguous: when the compilation unit already declares
or imports its own type named Arrays, the generated call resolves to
that type instead of the JDK one, so the recipe emits code that binds
to the wrong class or does not compile.
Both recipes now emit a fully qualified java.util.Arrays reference and
hand only the select they just generated to
ImportService.shortenFullyQualifiedTypeReferencesIn. The simple name
and its import come back wherever Arrays is free, and the qualified
form stays wherever it is not. Scoping the shortener to that select
leaves qualified names the caller wrote inside the array argument
untouched. RemoveToStringCallsFromArrayInstances routes its direct,
String.valueOf, Objects.toString, implicit-argument and concatenation
paths through one helper so every emitting site gets the same
treatment.
Worth weighing on review: the shortener's conflict set is the
compilation unit's own declarations and imports, not the full JLS
scope, so an Arrays type inherited from a supertype, or a field or
variable named Arrays, is still not detected and still receives the
simple name. Those shapes were already wrong before this change;
covering them needs scope-aware shortening in rewrite-java rather
than a change here.
4 tasks
martinfrancois
marked this pull request as draft
August 16, 2026 01:10
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: 33 of 52 (Score: 2.75)
Review first: openrewrite/rewrite-migrate-java#1198
What's changed?
RemoveHashCodeCallsFromArrayInstancesandRemoveToStringCallsFromArrayInstancesnow write a reference tojava.util.Arraysthat resolves correctly when the file already binds the simple nameArraysto something else.Both recipes now emit the call fully qualified and pass only the receiver of the generated call, the
java.util.Arrayspart, toImportService.shortenFullyQualifiedTypeReferencesIn. That service rewrites the receiver back to the simple name and adds the import, but only when the compilation unit neither declares nor imports another type calledArrays. Passing the receiver rather than the whole call keeps the service away from any qualified name the user wrote inside the argument.In a file where nothing else is called
Arraysthe output is unchanged from current main, except that the import now comes from the shortening service rather than from themaybeAddImport("java.util.Arrays")call this change deletes from both recipes.RemoveToStringCallsFromArrayInstancesbuilt the same template twice, inbuildReplacementand invisitExpression. Both now go through one private helper,arraysToString, so all five rewrite paths emit the same call:array.toString(),Objects.toString(array),String.valueOf(array), an array passed to a method such asprintorformat, and string concatenation.What's your motivation?
Recipe:
RemoveHashCodeCallsFromArrayInstancesandRemoveToStringCallsFromArrayInstances.Before
Actual after the recipe
Expected after the recipe
On current main both recipes call
maybeAddImport("java.util.Arrays")and then print the simple name.maybeAddImportdeclines only when an existing import already binds the simple nameArrays, and it never looks at the types the file itself declares. A type declaration shadows an import of the same simple name throughout its scope (JLS 6.4), so javac resolves theArrays.hashCode(values)above to the user's own nested type and rejects it withmethod hashCode in class Object cannot be applied to given types.The silent variant is worse: if that user type declares a static method matching the generated call, the output compiles and calls it instead of the JDK one. Both shapes reproduce on v2.40.0 and on main 5785534.
The one collision current main does handle is a conflicting import. In a file containing
import other.Arrays;,maybeAddImportrefuses to add a second import for that simple name and the generated reference is left fully qualified, so that shape already compiles.Anything in particular you'd like reviewers to focus on?
No existing test expectation changed; both test files are additions only.
Three limits:
Arraysinherited from a supertype, and a variable namedArrays, are still not detected, on current main and on this branch.Arraysas taken as soon as a type of that name is declared anywhere in the compilation unit, even one not in scope at the call site, for example astatic class Arraysnested inside a different top level class of the same file. Every call these recipes write in that file then stays fully qualified. Only that one name is affected, and the qualified call is correct, so the cost is verbosity, not behaviour.javain scope at the call site and its own type namedArrays. There,java.util.Arrays.hashCode(values)does not compile, because a variable is chosen over a package name of the same spelling (JLS 6.5.2). Current main writesArrays.hashCode(values)there, which binds to the file's own type, so it does not compile either. Only the way it fails changes.Have you considered any alternatives or workarounds?
One alternative is to detect the conflict inside each recipe instead of in the import service. That is more exact and would fix the first limit above, but each recipe would then reimplement Java's name resolution rules. Each recipe has one
doAfterVisitline, the one that passes the generatedjava.util.Arraysreceiver to the shortening service; switching means replacing that line with a walk over the enclosing scopes looking for a binding of the simple nameArrays, roughly ten lines per recipe. This branch does not contain that check. Say the word in review and I will write it.Any additional context
This change adds 5 tests to
RemoveHashCodeCallsFromArrayInstancesTestand 4 toRemoveToStringCallsFromArrayInstancesTest. Without the code change in this pull request, these 5 tests fail:doesNotShortenQualifiedNamesTheUserWroteInsideTheArgumentin both classesqualifyArraysWhenMemberTypeShadowsItin both classesqualifyArraysWhenSameFileTopLevelTypeShadowsItin thehashCodeclassThe other 4 pass either way. Two are
qualifyArraysWhenAnotherArraysTypeIsImported, covering the conflicting import above, which I am happy to drop. The other two,doNotChangeHashCodeOnNonArrayWhenArraysIsShadowedanddoNotChangeToStringOnNonArrayWhenArraysIsShadowed, declare a nestedstatic class Arrays, call on a plainObjectreceiver and assert that nothing is rewritten.The two classes hold 8 and 22 tests on this branch.
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