fix: keep implicit multiplication round-trippable after a symbol or accessor#3661
Open
sarathfrancis90 wants to merge 1 commit into
Open
Conversation
…ccessor
When stringifying an implicit multiplication, a ConstantNode that follows an
unparenthesized factor was wrapped in parentheses so that adjacent numbers such
as `4 4` (which is not valid input) would be serialized recoverably as `4 (4)`.
However, when the preceding factor ends with a SymbolNode or AccessorNode the
parenthesized ConstantNode reparses as a function call or index access rather
than an implicit multiplication. For example `parse('x 2').toString()` returned
`'x (2)'`, which parses back as the function call `x(2)` instead of `x * 2`, so
the node no longer round-tripped and evaluating the serialized form threw or
produced a different value. The same applied to `a b 2` (-> `a b (2)`, parsed as
`a * b(2)`) and `A[1] 2` (-> `A[1] (2)`, parsed as the index access `A[1](2)`).
Only force the parentheses when the preceding factor does not end with a symbol
or accessor, in which case the bare ConstantNode is unambiguous and recoverable.
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.
Summary
Node.toString()of an implicit multiplication can produce a string that no longer parses back to the same expression when the left-hand factor ends with a symbol or an accessor. The serialized output reparses as a function call or an index access instead of a multiplication, so the round-tripparse(str).toString()is broken and re-evaluating the serialized form throws or returns a different value.Root cause
In
src/expression/node/OperatorNode.js, when stringifying an implicit multiplication withimplicit: 'hide', aConstantNodethat follows an unparenthesized factor is deliberately wrapped in parentheses. This exists so that an expression such as4 4(two adjacent constants, which is not valid input) serializes recoverably as4 (4).The problem is that this rule fired regardless of what the preceding factor was. When the preceding factor ends with a
SymbolNodeor anAccessorNode, a directly-following parenthesis binds as invocation/indexing:x 2→x (2)→ parses as the callx(2)a b 2→a b (2)→ parses asa * b(2)A[1] 2→A[1] (2)→ parses as the index accessA[1](2)A parenthesized expression / function call already ends with
), and)(...)is treated by the parser as implicit multiplication, so those cases were and remain safe — only a trailing symbol or accessor is affected.Fix
Add a small helper
endsWithSymbolOrAccessor(expr, parenthesis)that determines whether the printed form of the preceding factor ends with a symbol or accessor (recursing into the last-printed operand of operator nodes, and correctly distinguishing prefix unary operators like-afrom postfix ones likea!). The forced-parentheses branch now skips wrapping theConstantNodein that case, leaving the bare constant, which is unambiguous and recoverable:x 2→x 2→ parses back tox * 2a b 2→a b 2→ parses back toa * b * 2A[1] 2→A[1] 2→ parses back toA[1] * 2The original recoverable cases are preserved (
(4)(4)→4 (4),sin(1)2→sin(1) (2)).Tests
Added a unit test in
test/unit-tests/expression/node/OperatorNode.test.jsthat, for bothparenthesis: 'auto'andparenthesis: 'keep', asserts the serialized form reparses to the identical grouping for a symbol factor, a nested symbol product, and an accessor factor, and spot-checks the exact strings (x 2,a b 2,A[1] 2). The test fails on the current code ('x (2)' !== 'x 2') and passes with the fix.Verified locally:
npm run lint— clean (--max-warnings 0)OperatorNodeunit suite — 53 passingOnly files under
src(plus the test and aHISTORY.mdentry) are changed; nodist/libbuilds are committed. PR is targeted atdevelopper the contributing guidelines.