SimplifyBooleanExpressionVisitor: correctly negate a nested ternary - #8456
Merged
timtebeek merged 3 commits intoAug 11, 2026
Conversation
maybeNegate negated a J.Ternary by flipping its condition and swapping its branches. Doing both preserves the ternary's value instead of negating it, so `!(a ? b ? x : y : z)` lost the negation of the nested branch: cycle one produced `a ? !b ? y : x : !z`, and the next cycle normalized that back to `a ? b ? x : y : !z`, which also broke single-cycle convergence. Negate the ternary's branches instead, the same way unpackExpression already negates the branches of a directly negated ternary.
Replacing `!(...)` with a ternary drops the parentheses, so `!(a ? b : c) && d` printed as `a ? !b : !c && d`, which regroups as `a ? !b : (!c && d)`. Wrap the ternary again when the parent binds tighter than a ternary does. Also keep the prefix outside the parentheses that `not()` inserts, so it emits `!(o instanceof String)` rather than `!( o instanceof String)`.
Contributor
|
@timtebeek can we leverage |
Replaces the local parent-precedence check, as suggested in review. ParenthesizeVisitor.visitTernary was missing two positions where Java requires a ternary to be parenthesized: as the condition of another ternary, since `a ? b : c ? d : e` groups as `a ? b : (c ? d : e)`, and as the operand of a cast, since `(Object) a ? b : c` casts the condition.
timtebeek
approved these changes
Aug 11, 2026
timtebeek
left a comment
Member
There was a problem hiding this comment.
Thanks a lot! Approved from my end; but since I've added changes as well @knutwannheden might want to approve too.
knutwannheden
approved these changes
Aug 11, 2026
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.
What's changed?
maybeNegateinSimplifyBooleanExpressionVisitornegated a ternary by inverting its condition andswapping its two branches. Those two edits cancel each other out:
c ? t : fand!c ? f : tarethe same value, not opposites. So where a real negation was needed, the visitor produced an
expression equal to the one it was asked to negate, and the negation disappeared from the output.
The method now negates the two branches instead and leaves the condition alone: the negation of
c ? t : fisc ? !t : !f. That is the same ruleunpackExpressionin this class already applieswhen a ternary is negated directly, as in the existing
ternayNegationtest.What's your motivation?
The broken path is reached when a ternary is nested inside another negated ternary. Input:
Output from current main, first cycle:
The nested part
!b ? y : xequalsb ? x : y, so whenais true this returnsxwhere theinput returns
!x. The compiled result is the opposite of the input. The second cycle then rewrites!b ? y : xback tob ? x : y, which both shows the two are the same expression and makes therecipe fail the single-cycle convergence check in
RewriteTest.A ternary nested in the false position loses its negation the same way:
!(a ? x : b ? y : z)becomes
a ? !x : b ? y : zafter both cycles, so theb ? y : zpart is returned unnegated.The visitor is shared:
SimplifyBooleanExpression,InvertConditionandRemoveObjectsIsNullinherit this behavior wherever they negate an expression with a nested ternary in it.
Anything in particular you'd like reviewers to focus on?
No existing test expectation changed.
SimplifyBooleanExpressionVisitorTesthas 75 tests on main;this change adds 1,
nestedTernaryNegation, with four methods: a nested ternary in the trueposition, one in the false position, one whose nested condition is itself negated
(
!(a ? !b ? x : y : z)becomesa ? b ? !y : !x : !z, in a single cycle), and an unchangedcontrol without a negation. Without the code change, that test fails; the other 75 pass before and
after. I also ran the three consumers of the visitor that have their own tests:
rewrite-groovy'sSimplifyBooleanExpressionVisitorTest(61 tests),InvertConditionTest(1) andRemoveObjectsIsNullTest(7), all green with this change.Two limits, both present on main today and out of scope here:
!(a ? (b ? x : y) : z), reaches the correctoutput but needs two cycles, because
maybeNegatehas no parentheses case and first wraps theinner ternary in a
!(...). Main does the same.<to>=, which is not equivalent when an operand isNaN. Thatmapping is existing code and main already applies it to a comparison in an unnested ternary; this
change lets it reach comparisons one nesting level deeper.
Have you considered any alternatives or workarounds?
Keeping the condition inversion and negating the branches as well would also be correct, but it
edits three nodes where one rule suffices, and it diverges from how
unpackExpressionnegates adirectly negated ternary. Reusing the same rule keeps the two paths consistent.
Any additional context
Our open PR #8447 touches the same file for an unrelated concern (operands with side effects).
The two changes are independent and edit different methods, but whichever merges second may need a
trivial rebase of the shared test file.
This change was prepared with AI assistance (Claude Code). I reviewed the code, the test and this
description.
Checklist
I ran the formatter calibrated to each file's existing style. It also wanted to re-indent lines this
change does not touch, so I left those alone and kept the diff limited to this change.