fix: optional chaining short-circuits only one link, and pollutes indexed objects - #3688
Open
Jaybhade wants to merge 1 commit into
Open
fix: optional chaining short-circuits only one link, and pollutes indexed objects#3688Jaybhade wants to merge 1 commit into
Jaybhade wants to merge 1 commit into
Conversation
…exed objects An optional chain kept its short-circuit state in the evaluation context and only honoured it in the accessor directly following the optional one, so: - `obj?.foo.bar.baz` threw "Cannot access property" instead of returning undefined, since only `bar` looked at the state that `?.` had set; - the state was written onto the context of an index expression, which is the object being indexed, so `A[first(nothing?.foo)]` left an `optionalShortCircuit` property on `A` and any later chain indexing `A` read that stale flag and short-circuited when it should not have. The state now travels along the chain itself, and every link of the chain honours it, including calls (`obj?.foo.fn()`) and accesses of a call result (`obj?.fn().foo`). Arguments of a call are evaluated outside the chain, so a short-circuit in an argument does not short-circuit the chain around the call.
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.
Optional chaining keeps its short-circuit state in the evaluation context, and only the accessor directly after the optional one looks at it. Two things follow from that.
1. The chain stops short-circuiting after one link
baris the only link that consults the state?.set (prevOptionalChainingwasisAccessorNode(this.object) && this.object.optionalChaining, so it looked exactly one accessor back), sobazis read off theundefinedthatbarreturned. The same applies to a call at the end of a chain (obj?.foo.fn()threw "No access to method") and to a property of a call result (obj?.fn().foo).2. The state is written onto the object of an index expression
The context of an index expression is the object being indexed, so the flag ends up on the user's own array or matrix, and is still there for the next evaluation:
That last expression evaluates to
10on its own; it only fails because it read the stale flag left onA. Amath.matrixis affected the same way.The fix
The short-circuit state is now an
OptionalChainobject that travels along the chain instead of living in the evaluation context, so it can never be confused with — or written onto — the object of an index expression. Every link of a chain honours it, not just the first one after?., which also covers calls in a chain and accesses of a call result. Arguments of a call are evaluated outside the chain (x?.f(y?.z).gkeeps working whenyis nullish), matching how a nested chain behaves in JavaScript.Behaviour that was already correct is unchanged: a chain still throws when nothing short-circuited (
obj?.foo.barwithobj.foo === undefinedthrows, as its existing test requires), and(obj?.foo).barstill ends the chain at the parentheses.Tests
Six cases added to
test/unit-tests/expression/parse.test.js, next to the existing optional chaining tests: chains longer than two links in dot and bracket notation, calls in a chain, accesses of a call result, an argument that short-circuits inside a chain, and the index-expression case above (asserting the value and thatAis untouched). Five of them fail ondevelop; the sixth guards against short-circuiting a chain that should not short-circuit.npm run test:all(6658 unit + 36 generated + 282 node + types) andnpm run lintpass.