Repository navigation
Fix parentheses for moved nodes in format-preserving printer - #1181
Open
Enfiladeguide wants to merge 1 commit into
Open
Enfiladeguide wants to merge 1 commit into
Enfiladeguide wants to merge 1 commit into
Conversation
Since PHP-Parser 5, an operator prints its own parentheses, based on the precedence passed down by its parent. A node with preserved formatting reuses its original code instead, so this check never runs, and a node moved into a context that binds more tightly loses parentheses it needs: wrapping `a() && b()` in a new BooleanNot printed `!a() && b()`. Perform the same check for nodes with preserved formatting. A prefix operator at the end of the reused code (`$a === $b = $c`) extends as far to the right as possible, so pass the lhsPrecedence of the new context on to the trailing operand, unless it already has parentheses. Fixes nikic#1119. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Fixes #1119 (reproducer in #1120).
Since PHP-Parser 5, an operator decides about its own parentheses from the precedence its parent passes down (
pInfixOp(),pPrefixOp(),pPostfixOp()). A node with preserved formatting reuses its original code, so that decision never runs, and a node moved into a context that binds more tightly loses parentheses it needs:This PR performs the same check in
p()for nodes with preserved formatting:preservedOpRequiresParens()is the check ofpInfixOp()/pPostfixOp()(against$precedence) and ofpPrefixOp()(against$lhsPrecedence);$c === $d = $e,$f >> print $g) extends as far to the right as possible, so the trailing operand (getTrailingOperandName()) is printed with the$lhsPrecedenceof the new context, aspInfixOp()/pPrefixOp()do, unless the original code already has parentheses around it.As a result, a moved node gets the same parentheses as a newly created node in the same place, including the existing conservative approximation in
pFixup().New fixture
test/code/formatPreservation/fixupMovedNodes.test: the #1119 case,$a ?? ($b ? 'yes' : 'no'),$a - ($b - $c), a move into a preserved parent, trailing prefix operators, and cases that must stay without parentheses (left-associative minus, assignment on the RHS, a slot that already has parentheses). It fails on master and passes here. The full test suite, PHPStan and php-cs-fixer pass (PHP 8.5.4).To check the change beyond hand-picked cases, we ran a differential fuzz: random expressions are pretty-printed and parsed, one node is wrapped in a new operator, moved to another position, or replaces its parent, and the output of
printFormatPreserving()is parsed again and compared with the modified AST. Over 60,000 rewrites (3 seeds):832 outputs that were already correct on master change. In 829 of them the output is identical to printing a freshly constructed copy of the moved node in the same slot (the added parentheses come from the
lhsPrecedence == precedenceapproximation inpFixup()); in the other 3 the moved node keeps its own original inner parentheses. Concat (see the FIXME on its precedence) and Pipe were left out of the fuzz. Happy to contribute the fuzz script if it is useful.We ran into this through Rector's TernaryToNullCoalescingRector, which turned
$a !== null ? $a : ($b ? 'yes' : 'no')into$a ?? $b ? 'yes' : 'no'(rectorphp/rector#9912, worked around in the rule in rectorphp/rector-src#8674).Disclosure: the investigation and this change were prepared with automated tooling; the reproduction, tests and proposed change were checked by a person before posting.