From 61ad097545da04beb01082debda36b6937cec4fd Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 6 Jun 2020 14:57:12 +0900 Subject: [PATCH] add optional chaining --- acorn-loose/src/expression.js | 30 +- acorn/src/expression.js | 69 +- acorn/src/lval.js | 9 +- acorn/src/tokenize.js | 4 + acorn/src/tokentype.js | 1 + bin/run_test262.js | 1 - test/run.js | 1 + test/tests-optional-chaining.js | 1325 +++++++++++++++++++++++++++++++ 8 files changed, 1435 insertions(+), 5 deletions(-) create mode 100644 test/tests-optional-chaining.js diff --git a/acorn-loose/src/expression.js b/acorn-loose/src/expression.js index be46375bf..4c78332f5 100644 --- a/acorn-loose/src/expression.js +++ b/acorn-loose/src/expression.js @@ -159,6 +159,7 @@ lp.parseExprSubscripts = function() { } lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { + const optionalSupported = this.options.ecmaVersion >= 11 for (;;) { if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) { if (this.tok.type === tt.dot && this.curIndent === startIndent) @@ -167,9 +168,27 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { return base } + // Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining + // if all of the following conditions are true: + // - The next node is a `(Call|Member)Expression` node as well (check the current token). + // - The `base` node is parenthesized. + // - The `base` node is a `(Call|Member)Expression` node. + // - The `base` node can be short-circuited by optional chaining. + if ( + optionalSupported && + (this.toks.type === tt.dot || this.toks.type === tt.optionalChaining || (!noCalls && this.toks.type === tt.parenL)) + ) { + if (base.end !== this.toks.lastTokEnd && this.toks.isOptionalChained(base)) { + base = this.toks.createChainExpressionNode(base) + } else if (base.type === "ParenthesizedExpression" && this.toks.isOptionalChained(base.expression)) { + base.expression = this.toks.createChainExpressionNode(base.expression) + } + } + let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon() + let optional = optionalSupported && this.eat(tt.optionalChaining) - if (this.eat(tt.dot)) { + if ((optional && this.tok.type !== tt.parenL && this.tok.type !== tt.bracketL && this.tok.type !== tt.backQuote) || this.eat(tt.dot)) { let node = this.startNodeAt(start) node.object = base if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) @@ -177,6 +196,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { else node.property = this.parsePropertyAccessor() || this.dummyIdent() node.computed = false + if (optionalSupported) { + node.optional = optional + } base = this.finishNode(node, "MemberExpression") } else if (this.tok.type === tt.bracketL) { this.pushCx() @@ -185,6 +207,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { node.object = base node.property = this.parseExpression() node.computed = true + if (optionalSupported) { + node.optional = optional + } this.popCx() this.expect(tt.bracketR) base = this.finishNode(node, "MemberExpression") @@ -195,6 +220,9 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { let node = this.startNodeAt(start) node.callee = base node.arguments = exprList + if (optionalSupported) { + node.optional = optional + } base = this.finishNode(node, "CallExpression") } else if (this.tok.type === tt.backQuote) { let node = this.startNodeAt(start) diff --git a/acorn/src/expression.js b/acorn/src/expression.js index da94085f5..9d8318407 100644 --- a/acorn/src/expression.js +++ b/acorn/src/expression.js @@ -273,6 +273,24 @@ pp.parseSubscripts = function(base, startPos, startLoc, noCalls) { let maybeAsyncArrow = this.options.ecmaVersion >= 8 && base.type === "Identifier" && base.name === "async" && this.lastTokEnd === base.end && !this.canInsertSemicolon() && base.end - base.start === 5 && this.potentialArrowAt === base.start + + // Wrap the `base` node by a `ChainExpression` node to disconnect the optional chaining + // if all of the following conditions are true: + // - The next node is a `(Call|Member)Expression` node as well (check the current token). + // - The `base` node is parenthesized. + // - The `base` node is a `(Call|Member)Expression` node. + // - The `base` node can be short-circuited by optional chaining. + if ( + this.options.ecmaVersion >= 11 && + (this.type === tt.dot || this.type === tt.optionalChaining || (!noCalls && this.type === tt.parenL)) + ) { + if (base.end !== this.lastTokEnd && this.isOptionalChained(base)) { + base = this.createChainExpressionNode(base) + } else if (base.type === "ParenthesizedExpression" && this.isOptionalChained(base.expression)) { + base.expression = this.createChainExpressionNode(base.expression) + } + } + while (true) { let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow) if (element === base || element.type === "ArrowFunctionExpression") return element @@ -280,14 +298,33 @@ pp.parseSubscripts = function(base, startPos, startLoc, noCalls) { } } +pp.createChainExpressionNode = function(expression) { + const {start, end, loc} = expression + let startLoc, endLoc + if (loc) { + startLoc = loc.start + endLoc = loc.end + } + + const node = this.startNodeAt(start, startLoc) + node.expression = expression + return this.finishNodeAt(node, "ChainExpression", end, endLoc) +} + pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) { + let optional = this.options.ecmaVersion >= 11 && this.eat(tt.optionalChaining) + if (noCalls && optional) this.raise(this.lastTokStart, "Optional chaining cannot appear in the callee of new expressions") + let computed = this.eat(tt.bracketL) - if (computed || this.eat(tt.dot)) { + if (computed || (optional && this.type !== tt.parenL && this.type !== tt.backQuote) || this.eat(tt.dot)) { let node = this.startNodeAt(startPos, startLoc) node.object = base node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never") node.computed = !!computed if (computed) this.expect(tt.bracketR) + if (this.options.ecmaVersion >= 11) { + node.optional = optional + } base = this.finishNode(node, "MemberExpression") } else if (!noCalls && this.eat(tt.parenL)) { let refDestructuringErrors = new DestructuringErrors, oldYieldPos = this.yieldPos, oldAwaitPos = this.awaitPos, oldAwaitIdentPos = this.awaitIdentPos @@ -295,7 +332,7 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) this.awaitPos = 0 this.awaitIdentPos = 0 let exprList = this.parseExprList(tt.parenR, this.options.ecmaVersion >= 8, false, refDestructuringErrors) - if (maybeAsyncArrow && !this.canInsertSemicolon() && this.eat(tt.arrow)) { + if (maybeAsyncArrow && !optional && !this.canInsertSemicolon() && this.eat(tt.arrow)) { this.checkPatternErrors(refDestructuringErrors, false) this.checkYieldAwaitInDefaultParams() if (this.awaitIdentPos > 0) @@ -312,8 +349,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) let node = this.startNodeAt(startPos, startLoc) node.callee = base node.arguments = exprList + if (this.options.ecmaVersion >= 11) { + node.optional = optional + } base = this.finishNode(node, "CallExpression") } else if (this.type === tt.backQuote) { + if (optional || (base.end === this.lastTokEnd && this.isOptionalChained(base))) { + this.raise(this.start, "Optional chaining cannot appear in the tag of tagged template expressions") + } let node = this.startNodeAt(startPos, startLoc) node.tag = base node.quasi = this.parseTemplate({isTagged: true}) @@ -322,6 +365,28 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) return base } +pp.isOptionalChained = function(node) { + if (this.options.ecmaVersion >= 11) { + while (true) { + switch (node.type) { + case "CallExpression": + if (node.optional) return true + node = node.callee + break + + case "MemberExpression": + if (node.optional) return true + node = node.object + break + + default: + return false + } + } + } + return false +} + // Parse an atomic expression — either a single token that is an // expression, an expression started by a keyword like `function` or // `new`, or an expression wrapped in punctuation like `()`, `[]`, diff --git a/acorn/src/lval.js b/acorn/src/lval.js index 8ad4b2639..06065bff2 100644 --- a/acorn/src/lval.js +++ b/acorn/src/lval.js @@ -74,7 +74,11 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) { break case "MemberExpression": - if (!isBinding) break + if (isBinding) this.raise(node.start, "Assigning to rvalue") + if (this.isOptionalChained(node)) { + this.raise(node.start, "Optional chaining cannot appear in left-hand side") + } + break default: this.raise(node.start, "Assigning to rvalue") @@ -203,6 +207,9 @@ pp.checkLVal = function(expr, bindingType = BIND_NONE, checkClashes) { case "MemberExpression": if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression") + if (this.isOptionalChained(expr)) { + this.raise(expr.start, "Optional chaining cannot appear in left-hand side") + } break case "ObjectPattern": diff --git a/acorn/src/tokenize.js b/acorn/src/tokenize.js index a71a09342..6f7e00ba5 100644 --- a/acorn/src/tokenize.js +++ b/acorn/src/tokenize.js @@ -292,6 +292,10 @@ pp.readToken_eq_excl = function(code) { // '=!' pp.readToken_question = function() { // '?' if (this.options.ecmaVersion >= 11) { let next = this.input.charCodeAt(this.pos + 1) + if (next === 46) { + let next2 = this.input.charCodeAt(this.pos + 2) + if (next2 < 48 || next2 > 57) return this.finishOp(tt.optionalChaining, 2) + } if (next === 63) return this.finishOp(tt.coalesce, 2) } return this.finishOp(tt.question, 1) diff --git a/acorn/src/tokentype.js b/acorn/src/tokentype.js index 77b6ccb2c..d6df99f73 100644 --- a/acorn/src/tokentype.js +++ b/acorn/src/tokentype.js @@ -76,6 +76,7 @@ export const types = { ellipsis: new TokenType("...", beforeExpr), backQuote: new TokenType("`", startsExpr), dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}), + optionalChaining: new TokenType("?."), // Operators. These carry several kinds of properties to help the // parser use them properly (the presence of these properties is diff --git a/bin/run_test262.js b/bin/run_test262.js index f2ab82b3a..6e20acfa0 100644 --- a/bin/run_test262.js +++ b/bin/run_test262.js @@ -11,7 +11,6 @@ const unsupportedFeatures = [ "class-static-fields-public", "class-static-methods-private", "numeric-separator-literal", - "optional-chaining", ]; run( diff --git a/test/run.js b/test/run.js index 147e27a51..5485e6916 100644 --- a/test/run.js +++ b/test/run.js @@ -20,6 +20,7 @@ require("./tests-export-all-as-ns-from-source.js"); require("./tests-import-meta.js"); require("./tests-nullish-coalescing.js"); + require("./tests-optional-chaining.js"); var acorn = require("../acorn") var acorn_loose = require("../acorn-loose") diff --git a/test/tests-optional-chaining.js b/test/tests-optional-chaining.js new file mode 100644 index 000000000..be907c633 --- /dev/null +++ b/test/tests-optional-chaining.js @@ -0,0 +1,1325 @@ + +if (typeof exports != "undefined") { + var driver = require("./driver.js"); + var test = driver.test, testFail = driver.testFail, testAssert = driver.testAssert, misMatch = driver.misMatch; + var acorn = require("../acorn"); +} + +test("obj?.foo", { + "type": "Program", + "start": 0, + "end": 8, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 8, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 8, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "foo" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj?.[foo]", { + "type": "Program", + "start": 0, + "end": 10, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 10, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 10, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "foo" + }, + "computed": true, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj?.()", { + "type": "Program", + "start": 0, + "end": 7, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 7, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 7, + "callee": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "arguments": [], + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("obj ?. foo", { + "type": "Program", + "start": 0, + "end": 10, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 10, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 10, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 7, + "end": 10, + "name": "foo" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj ?. [foo]", { + "type": "Program", + "start": 0, + "end": 12, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 12, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 12, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 8, + "end": 11, + "name": "foo" + }, + "computed": true, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj ?. ()", { + "type": "Program", + "start": 0, + "end": 9, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 9, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 9, + "callee": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "arguments": [], + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("obj?.0:.1", { + "type": "Program", + "start": 0, + "end": 9, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 9, + "expression": { + "type": "ConditionalExpression", + "start": 0, + "end": 9, + "test": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "consequent": { + "type": "Literal", + "start": 4, + "end": 6, + "value": 0, + "raw": ".0" + }, + "alternate": { + "type": "Literal", + "start": 7, + "end": 9, + "value": 0.1, + "raw": ".1" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("obj?.aaa?.bbb", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 13, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 8, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "aaa" + }, + "computed": false, + "optional": true + }, + "property": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "bbb" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj?.aaa.bbb", { + "type": "Program", + "start": 0, + "end": 12, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 12, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 12, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 8, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "aaa" + }, + "computed": false, + "optional": true + }, + "property": { + "type": "Identifier", + "start": 9, + "end": 12, + "name": "bbb" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.aaa)?.bbb", { + "type": "Program", + "start": 0, + "end": 15, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 15, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 15, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 12, + "end": 15, + "name": "bbb" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.aaa).bbb", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "bbb" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.aaa.bbb).ccc", { + "type": "Program", + "start": 0, + "end": 18, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 18, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 18, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 13, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 13, + "object": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + }, + "property": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "bbb" + }, + "computed": false, + "optional": false + } + }, + "property": { + "type": "Identifier", + "start": 15, + "end": 18, + "name": "ccc" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("func?.()?.bbb", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 13, + "object": { + "type": "CallExpression", + "start": 0, + "end": 8, + "callee": { + "type": "Identifier", + "start": 0, + "end": 4, + "name": "func" + }, + "arguments": [], + "optional": true + }, + "property": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "bbb" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("func?.().bbb", { + "type": "Program", + "start": 0, + "end": 12, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 12, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 12, + "object": { + "type": "CallExpression", + "start": 0, + "end": 8, + "callee": { + "type": "Identifier", + "start": 0, + "end": 4, + "name": "func" + }, + "arguments": [], + "optional": true + }, + "property": { + "type": "Identifier", + "start": 9, + "end": 12, + "name": "bbb" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(func?.())?.bbb", { + "type": "Program", + "start": 0, + "end": 15, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 15, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 15, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "CallExpression", + "start": 1, + "end": 9, + "callee": { + "type": "Identifier", + "start": 1, + "end": 5, + "name": "func" + }, + "arguments": [], + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 12, + "end": 15, + "name": "bbb" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(func?.()).bbb", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "CallExpression", + "start": 1, + "end": 9, + "callee": { + "type": "Identifier", + "start": 1, + "end": 5, + "name": "func" + }, + "arguments": [], + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "bbb" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("obj?.aaa?.()", { + "type": "Program", + "start": 0, + "end": 12, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 12, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 12, + "callee": { + "type": "MemberExpression", + "start": 0, + "end": 8, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "aaa" + }, + "computed": false, + "optional": true + }, + "arguments": [], + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("obj?.aaa()", { + "type": "Program", + "start": 0, + "end": 10, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 10, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 10, + "callee": { + "type": "MemberExpression", + "start": 0, + "end": 8, + "object": { + "type": "Identifier", + "start": 0, + "end": 3, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "aaa" + }, + "computed": false, + "optional": true + }, + "arguments": [], + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.aaa)?.()", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 14, + "callee": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + } + }, + "arguments": [], + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.aaa)()", { + "type": "Program", + "start": 0, + "end": 12, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 12, + "expression": { + "type": "CallExpression", + "start": 0, + "end": 12, + "callee": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + } + }, + "arguments": [], + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("delete obj?.foo", { + "type": "Program", + "start": 0, + "end": 15, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 15, + "expression": { + "type": "UnaryExpression", + "start": 0, + "end": 15, + "operator": "delete", + "prefix": true, + "argument": { + "type": "MemberExpression", + "start": 7, + "end": 15, + "object": { + "type": "Identifier", + "start": 7, + "end": 10, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 12, + "end": 15, + "name": "foo" + }, + "computed": false, + "optional": true + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +// OK if paranthesized. +test("new (obj?.foo)()", { + "type": "Program", + "start": 0, + "end": 16, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 16, + "expression": { + "type": "NewExpression", + "start": 0, + "end": 16, + "callee": { + "type": "MemberExpression", + "start": 5, + "end": 13, + "object": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "foo" + }, + "computed": false, + "optional": true + }, + "arguments": [] + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.foo)`template`", { + "type": "Program", + "start": 0, + "end": 20, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 20, + "expression": { + "type": "TaggedTemplateExpression", + "start": 0, + "end": 20, + "tag": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "foo" + }, + "computed": false, + "optional": true + }, + "quasi": { + "type": "TemplateLiteral", + "start": 10, + "end": 20, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 11, + "end": 19, + "value": { + "raw": "template", + "cooked": "template" + }, + "tail": true + } + ] + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.foo).bar = 0", { + "type": "Program", + "start": 0, + "end": 18, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 18, + "expression": { + "type": "AssignmentExpression", + "start": 0, + "end": 18, + "operator": "=", + "left": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "foo" + }, + "computed": false, + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "bar" + }, + "computed": false, + "optional": false + }, + "right": { + "type": "Literal", + "start": 17, + "end": 18, + "value": 0, + "raw": "0" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj?.foo).bar++", { + "type": "Program", + "start": 0, + "end": 16, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 16, + "expression": { + "type": "UpdateExpression", + "start": 0, + "end": 16, + "operator": "++", + "prefix": false, + "argument": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "object": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "foo" + }, + "computed": false, + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "bar" + }, + "computed": false, + "optional": false + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("for ((obj?.foo).bar of []);", { + "type": "Program", + "start": 0, + "end": 27, + "body": [ + { + "type": "ForOfStatement", + "start": 0, + "end": 27, + "await": false, + "left": { + "type": "MemberExpression", + "start": 5, + "end": 19, + "object": { + "type": "ChainExpression", + "start": 6, + "end": 14, + "expression": { + "type": "MemberExpression", + "start": 6, + "end": 14, + "object": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "foo" + }, + "computed": false, + "optional": true + } + }, + "property": { + "type": "Identifier", + "start": 16, + "end": 19, + "name": "bar" + }, + "computed": false, + "optional": false + }, + "right": { + "type": "ArrayExpression", + "start": 23, + "end": 25, + "elements": [] + }, + "body": { + "type": "EmptyStatement", + "start": 26, + "end": 27 + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +// With ParenthesizedExpression +test("(obj?.aaa).bbb", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "object": { + "type": "ParenthesizedExpression", + "start": 0, + "end": 10, + "expression": { + "type": "ChainExpression", + "start": 1, + "end": 9, + "expression": { + "type": "MemberExpression", + "start": 1, + "end": 9, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 9, + "name": "aaa" + }, + "computed": false, + "optional": true + } + } + }, + "property": { + "type": "Identifier", + "start": 11, + "end": 14, + "name": "bbb" + }, + "computed": false, + "optional": false + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11, preserveParens: true }) + +// No ChainExpression if `callee|object` doesn't contain `?.`. +test("(obj.foo.bar)?.buz", { + "type": "Program", + "start": 0, + "end": 18, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 18, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 18, + "object": { + "type": "MemberExpression", + "start": 1, + "end": 12, + "object": { + "type": "MemberExpression", + "start": 1, + "end": 8, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "foo" + }, + "computed": false, + "optional": false + }, + "property": { + "type": "Identifier", + "start": 9, + "end": 12, + "name": "bar" + }, + "computed": false, + "optional": false + }, + "property": { + "type": "Identifier", + "start": 15, + "end": 18, + "name": "buz" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("(obj.foo())?.buz", { + "type": "Program", + "start": 0, + "end": 16, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 16, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 16, + "object": { + "type": "CallExpression", + "start": 1, + "end": 10, + "callee": { + "type": "MemberExpression", + "start": 1, + "end": 8, + "object": { + "type": "Identifier", + "start": 1, + "end": 4, + "name": "obj" + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 8, + "name": "foo" + }, + "computed": false, + "optional": false + }, + "arguments": [], + "optional": false + }, + "property": { + "type": "Identifier", + "start": 13, + "end": 16, + "name": "buz" + }, + "computed": false, + "optional": true + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +// With an old ecmaVersion. +testFail("obj?.foo", "Unexpected token (1:4)", { ecmaVersion: 10 }) +testFail("obj?.[foo]", "Unexpected token (1:4)", { ecmaVersion: 10 }) +testFail("obj?.()", "Unexpected token (1:4)", { ecmaVersion: 10 }) + +// Syntax errors. +testFail("obj?.0", "Unexpected token (1:6)", { ecmaVersion: 11 }) +testFail("async?.() => {}", "Unexpected token (1:10)", { ecmaVersion: 11 }) +testFail("new obj?.()", "Optional chaining cannot appear in the callee of new expressions (1:7)", { ecmaVersion: 11 }) +testFail("new obj?.foo()", "Optional chaining cannot appear in the callee of new expressions (1:7)", { ecmaVersion: 11 }) +testFail("obj?.foo\n`template`", "Optional chaining cannot appear in the tag of tagged template expressions (2:0)", { ecmaVersion: 11 }) +testFail("obj?.foo = 0", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("obj?.foo.bar = 0", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("obj?.().foo = 0", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("obj?.foo++", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("obj?.foo--", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("++obj?.foo", "Optional chaining cannot appear in left-hand side (1:2)", { ecmaVersion: 11 }) +testFail("--obj?.foo", "Optional chaining cannot appear in left-hand side (1:2)", { ecmaVersion: 11 }) +testFail("obj?.foo.bar++", "Optional chaining cannot appear in left-hand side (1:0)", { ecmaVersion: 11 }) +testFail("for (obj?.foo in {});", "Optional chaining cannot appear in left-hand side (1:5)", { ecmaVersion: 11 }) +testFail("for (obj?.foo.bar in {});", "Optional chaining cannot appear in left-hand side (1:5)", { ecmaVersion: 11 }) +testFail("for (obj?.foo of []);", "Optional chaining cannot appear in left-hand side (1:5)", { ecmaVersion: 11 }) +testFail("for (obj?.foo.bar of []);", "Optional chaining cannot appear in left-hand side (1:5)", { ecmaVersion: 11 })