Avoid existing methods when renaming near-miss Object methods - #974
Draft
martinfrancois wants to merge 3 commits into
Draft
Avoid existing methods when renaming near-miss Object methods#974martinfrancois wants to merge 3 commits into
Object methods#974martinfrancois wants to merge 3 commits into
Conversation
…ting method Java method names are case sensitive, so a type may legally declare both a near-miss method and the correctly named Object method, each with its own behavior. The recipe renamed the near-miss declaration regardless, leaving two declarations with the same erased signature in one type, which does not compile. Renaming onto an inherited final method has the same defect, because the result is an illegal override. Guard the hashCode(), equals(Object) and toString() branches independently: skip the rename when the enclosing type already declares the target signature, or when a supertype declares it final. Neither declaration is deleted or merged, since their bodies and callers can represent distinct behavior. Unambiguous near-miss declarations are still renamed together with their call sites. The guard reads sibling declarations from the enclosing type body in the LST rather than from JavaType.Class#getMethods(). For records, and for annotation-processed classes such as Lombok @DaTa, the type model also carries generated equals/hashCode/toString members that an explicit declaration replaces rather than collides with, so consulting it would make the recipe a silent no-op on those types. Two limits remain, both pre-existing on main and unchanged here: two near-miss variants of the same target in one type still collide with each other after renaming, and renaming a package-private near-miss to an Object method name can produce a weaker-access-privileges error.
…c or static near-miss methods
martinfrancois
marked this pull request as draft
August 16, 2026 01:10
Object methods
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: 36 of 52 (Score: 2.5)
Review first: #973
What's changed?
RenameMethodsNamedHashcodeEqualOrToStringrenames a near-miss method name such ashashcode,equalortostringinto the correctly spelledhashCode,equalsortoString. With this change it makes no change at all in two cases:final.Each of the visitor's three branches, one per target name, now also calls the new
canRenameTo(...), which permits the rename only when both of its checks allow it.The first check looks for the already declared method in the enclosing
J.Block, the body of the type as it was actually written, and deliberately not in the type model,JavaType.FullyQualified#getMethods(): for records, and for classes handled by an annotation processor such as Lombok's@Data, that model also lists members nobody wrote, for example a generatedhashCode(), and an explicit declaration replaces such a member rather than colliding with it.renameWhenRecordDoesNotExplicitlyDeclareTargetcovers that case: a record declaring onlyhashcode()is still renamed.The second check does use the type model, calling
TypeUtils.findDeclaredMethodwith the direct supertype of the declaring type, the target name and the parameter types of the misspelled method.What's your motivation?
Recipe:
org.openrewrite.staticanalysis.RenameMethodsNamedHashcodeEqualOrToString.Before
Actual after the recipe
Expected after the recipe
(unchanged)
Java method names are case sensitive, so one type may legally declare both
hashcode()andhashCode(), each with its own body and its own callers. On main the recipe checks the misspelled name, the parameters and the return type and then renames, without checking whether the type already declares the correctly spelled method, so the source it produces fails to compile. The recipe is listed incommon-static-analysis.yml, so anyone runningorg.openrewrite.staticanalysis.CommonStaticAnalysiscan get source that no longer builds.The rename is carried out by
ChangeMethodName, which rewrites call sites as well as the declaration. That is correct in general, but in this collision it spreads the damage: every call totostring()in the sources being rewritten becomes a call totoString(), the already existing method with a different body, and deleting the duplicate declaration by hand afterwards does not put those calls back. Reproduced on 2.39.0, 2.40.0 and 2.41.0-SNAPSHOT built from main.Affected code in real projects
jython/jythonPyArray.java:PyArrayimplements the Pythonarray.tostring()API in apublic String tostring()method and separately overridesObject.toString()at line 698 of the same class. The recipe from main renamestostring()and its five call sites totoString(), so the class declares two no-argtoString()methods and no longer compiles.checkstyle/checkstyleInputMagicNumberDefault3.java: this MagicNumber test input deliberately declares bothpublic int hashCode()andpublic int hashcode()in the same class. The recipe from main renameshashcode()tohashCode(), producing a second no-arghashCode()in the same class, which does not compile.Anything in particular you'd like reviewers to focus on?
No existing test expectation changed:
noncompliantMethodNamesandcompliantWhenHasMismatchingTypeInformationkeep their input and their expected output. The file is otherwise edited only to add"unused"to the class level@SuppressWarnings, for methods declared in the new test sources and never called from them, and to importorg.openrewrite.java.Assertions.versionfor the two record tests, which wrap their source inversion(..., 17)because records need Java 17.Some cases stay wrong. Main produces the same result as this branch for each, so this change neither introduces them nor makes them worse:
hashcode()andhashCODE(), both are renamed tohashCode()and the two renamed declarations then collide: the new check looks for a sibling already named exactlyhashCode, and at the moment either variant is examined the type declares no such method.tostring()on a type that declares notoString()of its own, is still renamed. The result overrides the publicObject.toString()with weaker access, which does not compile.staticmisspelled method is still renamed, and so is a method declared as an element of an annotation type. Neither result compiles: a static method carrying the signature of an inherited instance method is a case of hiding rather than overriding (JLS 8.4.8.2), which Java does not permit between a static and an instance method, and an@interfacemember may not carry the name of a method ofObject.Have you considered any alternatives or workarounds?
The inherited half of the check stops a rename only when the inherited method is
final. An inherited non-finalhashCode()remains a valid target and the rename goes ahead, because renaming onto it produces a normal override: legal Java and the transformation this recipe is designed to make. The cost is that calls made through the supertype then run the renamed body. Stopping whenever the supertype lookup finds the target method at all,finalor not, is the one line change of removing the.filter(m -> m.getFlags().contains(Flag.Final))call fromcanRenameTo, but it turns the recipe off:java.lang.ObjectdeclareshashCode(),equals(Object)andtoString(), so the lookup finds the target for every class and no rename is left. With that call removed, every test in this class that expects a rename fails, includingnoncompliantMethodNames.Any additional context
This change adds 8 tests to
RenameMethodsNamedHashcodeEqualOrToStringTest, taking it from 2 tests to 10. Without the code change in this pull request, these 5 tests fail:doNotRenameWhenEnumAlreadyDeclaresTargetdoNotRenameWhenInheritedTargetIsFinaldoNotRenameWhenInterfaceAlreadyDeclaresTargetdoNotRenameWhenRecordAlreadyDeclaresTargetdoNotRenameWhenTargetIsAlreadyDeclaredThese 3 tests show that correct renames still happen:
renameUpdatesCallSitesWhenThereIsNoCollisionrenameWhenExistingMethodIsAnOverloadWithDifferentParametersrenameWhenRecordDoesNotExplicitlyDeclareTargetThis change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and this description.
Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv