Preserve assertion owners and receiver evaluation in assertInstanceOf migration - #1088
Draft
martinfrancois wants to merge 1 commit into
Draft
Conversation
…d call and dropped receiver qualifyWhenAssertInstanceOfDeclaredInClass pins that the migrated unqualified call must not bind to an assertInstanceOf declared in the class (JLS 6.5.7.1). noChangeWhenInstanceReceiverHasSideEffect pins that an instance receiver such as getAssertions() must not be dropped, since its side effects are lost. Both are marked known-failing with @disabled.
4 tasks
assertInstanceOf migration
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: 47 of 52 (Score: 1)
Review first: openrewrite/rewrite-static-analysis#994
What's changed?
Adds 2 known-failing tests to
AssertTrueInstanceofToAssertInstanceOfTestthat reproduce two defects inAssertTrueInstanceofToAssertInstanceOf:qualifyWhenAssertInstanceOfDeclaredInClass: the migrated unqualified call binds to a same-name method declared in the class instead of JUnit'sAssertions.assertInstanceOf.noChangeWhenInstanceReceiverHasSideEffect: migratinggetAssertions().assertTrue(...)deletes the receiver expression, so its side effects are lost.No recipe code changes. The tests are marked
@Disabledso the suite stays green; removing the annotation shows the failure. The class had 6 tests before and has 8 now; all 6 pre-existing tests still pass.What's your motivation?
Recipe: the JUnit
assertTrue(instanceof)toassertInstanceOfmigration.Case 1: keep the JUnit assertion owner
Before
This code calls JUnit's statically imported
assertTrue, and the class also declares its ownassertInstanceOf:Actual after the recipe
Using current main.
Expected after the recipe
Assertions.assertInstanceOf(String.class, obj);Per JLS 6.5.7.1, a method declared in the class shadows a single static import, so the new unqualified call binds to the local method. A compiled probe against junit-jupiter-api 5.13.3 confirmed it and printed "local method wins over static import". The output compiles, so the behavior change is silent. After the recipe runs, the code should call the qualified
Assertions.assertInstanceOf(String.class, obj), which is what the test pins.Case 2: preserve receiver evaluation
Before
The input contains
getAssertions().assertTrue(obj instanceof String);, wheregetAssertions()prints "side effect" and returns null.Actual after the recipe
Output from current main:
Expected after the recipe
(unchanged)The receiver expression is gone. Java evaluates the receiver of an instance call even when the resolved method is static (JLS 15.12.4.1), so the original code prints before asserting and the migrated code does not. A compiled probe confirmed both forms. The test pins no change as the safe behavior.
The cause is the recipe's JavaTemplate:
mi.getCoordinates().replace()with an unqualifiedassertInstanceOf(...)template plusmaybeAddImportreplaces the whole invocation, including any select.Found while preparing #1083, which fixes a related defect in this recipe (the qualified
Assertions.assertTrue(...)path) and lists these two remaining paths in its description.Anything in particular you'd like reviewers to focus on?
I think both are genuine bugs: the output compiles, but runtime behavior changes, either by calling a different method or by skipping the receiver's evaluation. If you agree this should change, I would gladly prepare the fix. If this behavior is intended, feel free to close this and I know it is settled.
Have you considered any alternatives or workarounds?
For the second defect, a receiver-preserving migration is possible, but it would keep the static-via-instance call style. Pinning no change looked safer; I am happy to implement either.
Any additional context
Pre-existing tests changed: None.
The tests fail the same way on current main (ded10c4) and on top of #1083. If #1083 merges first, the second test's wrong output becomes
org.junit.jupiter.api.Assertions.assertInstanceOf(...), with the receiver still dropped, so the reproduction stays valid. The merged #1044 and the issues #459 and #515 it addressed show that same-name shadowing is treated as a real bug class here. The tests use@Disabledwith the failure reason, because junit-pioneer's@ExpectedToFailis not on this repository's test classpath.This reproduction was prepared with AI assistance (Claude Code). I reviewed the tests and this description.
The added reproduction tests and the existing suite together cover changed and unchanged behavior. The known-failing tests remain disabled until implementation. The formatter run was calibrated per file; untouched lines were not reformatted.
Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv