Preserve observable array accesses and casts in identical branches - #995
Draft
martinfrancois wants to merge 1 commit into
Draft
Preserve observable array accesses and casts in identical branches#995martinfrancois wants to merge 1 commit into
martinfrancois wants to merge 1 commit into
Conversation
…cast doNotChangeWhenConditionContainsArrayAccess pins that collapsing branches must not delete an array access whose evaluation may throw ArrayIndexOutOfBoundsException. doNotChangeWhenConditionContainsCast pins the same for a cast that may throw ClassCastException. SideEffects.mayHaveSideEffects reports both as effect-free, so both are marked @ExpectedToFail until the shared helper covers them.
4 tasks
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: 39 of 52 (Score: 2)
Review first: openrewrite/rewrite#8467
What's changed?
Adds 2 known-failing tests to
AllBranchesIdenticalTestthat reproduce a case where the recipe deletes an expression that can throw at runtime: an array access in the condition, and a cast in the condition. No recipe code changes. The tests are marked@ExpectedToFailso the suite stays green; removing the mark shows the failure.What's your motivation?
Recipe: the identical-branch simplification in
SimplifyBooleanExpressionVisitor.The shared helper
SideEffects.mayHaveSideEffects(added in #959) only flags method invocations, assignments, assignment operations, increments and decrements, and new-class expressions.J.ArrayAccessandJ.TypeCastfall through as effect-free. So the purity guard inAllBranchesIdentical(and the same guard inRemoveDuplicateConditions,RemoveUnconditionalValueOverwrite, andSimplifyRedundantLogicalExpression) lets the recipe delete an evaluation that can throw.Before
Actual after the recipe
Using current main.
The same happens with
if ((Boolean) o)over anObject oparameter: the whole condition is deleted.Expected after the recipe
(unchanged)Both inputs MUST remain unchanged because removing either condition also removes an exception-producing evaluation. This output is wrong because it changes behavior. I compiled and ran both versions: with
new boolean[2], the original method throwsjava.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 2, and with aStringargument the cast version throwsjava.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Boolean. The cast version also unboxes, so anullargument would throw aNullPointerException. The collapsed methods throw nothing. After the recipe runs, the code should be unchanged, because deleting these evaluations removes exceptions the program relied on.Found while preparing #972 and #973, which fix related defects around this helper.
Anything in particular you'd like reviewers to focus on?
I think this is a genuine bug: the transformed code silently loses
ArrayIndexOutOfBoundsException,ClassCastException, and unboxingNullPointerExceptionbehavior. 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.Note that #973 has tests (
removeRedundantNullCheckWithArrayAccess,removeRedundantNullCheckWithCast) that intentionally keep array access and cast removal allowed in the null-check context. That is safe there, because one evaluation of the expression remains. This reproduction usesAllBranchesIdenticalon purpose, because it deletes the expression entirely.Any additional context
Pre-existing tests changed: None.
Related open PRs of mine touching the same helper: #972 guards
RemoveRedundantNullCheckBeforeInstanceofthroughSideEffects, and #973 adds a volatile field read check toSideEffects. Neither covers array access or casts, which is why the volatile side of this area is not reproduced here; it is resolved once those two merge.Test counts for the touched class: 12 tests on main, 14 with this change; the 2 new tests are skipped as expected failures; 0 new failures or errors are introduced.
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