diff --git a/acorn-loose/src/expression.js b/acorn-loose/src/expression.js index be46375bf..ca77f315d 100644 --- a/acorn-loose/src/expression.js +++ b/acorn-loose/src/expression.js @@ -159,17 +159,23 @@ lp.parseExprSubscripts = function() { } lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { + const optionalSupported = this.options.ecmaVersion >= 11 + let optionalChained = false for (;;) { if (this.curLineStart !== line && this.curIndent <= startIndent && this.tokenStartsLine()) { if (this.tok.type === tt.dot && this.curIndent === startIndent) --startIndent else - return base + break } let maybeAsyncArrow = base.type === "Identifier" && base.name === "async" && !this.canInsertSemicolon() + let optional = optionalSupported && this.eat(tt.questionDot) + if (optional) { + optionalChained = true + } - 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 +183,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 +194,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 +207,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) @@ -202,9 +217,16 @@ lp.parseSubscripts = function(base, start, noCalls, startIndent, line) { node.quasi = this.parseTemplate() base = this.finishNode(node, "TaggedTemplateExpression") } else { - return base + break } } + + if (optionalChained) { + const chainNode = this.startNodeAt(start) + chainNode.expression = base + base = this.finishNode(chainNode, "ChainExpression") + } + return base } lp.parseExprAtom = function() { diff --git a/acorn-walk/src/index.js b/acorn-walk/src/index.js index 498b1fc48..2a044b7df 100644 --- a/acorn-walk/src/index.js +++ b/acorn-walk/src/index.js @@ -192,7 +192,7 @@ base.Program = base.BlockStatement = (node, st, c) => { } base.Statement = skipThrough base.EmptyStatement = ignore -base.ExpressionStatement = base.ParenthesizedExpression = +base.ExpressionStatement = base.ParenthesizedExpression = base.ChainExpression = (node, st, c) => c(node.expression, st, "Expression") base.IfStatement = (node, st, c) => { c(node.test, st, "Expression") diff --git a/acorn/src/expression.js b/acorn/src/expression.js index da94085f5..5b8c2093d 100644 --- a/acorn/src/expression.js +++ b/acorn/src/expression.js @@ -273,21 +273,40 @@ 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 + let optionalChained = false + while (true) { - let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow) - if (element === base || element.type === "ArrowFunctionExpression") return element + let element = this.parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained) + + if (element.optional) optionalChained = true + if (element === base || element.type === "ArrowFunctionExpression") { + if (optionalChained) { + const chainNode = this.startNodeAt(startPos, startLoc) + chainNode.expression = element + element = this.finishNode(chainNode, "ChainExpression") + } + return element + } + base = element } } -pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) { +pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained) { + let optionalSupported = this.options.ecmaVersion >= 11 + let optional = optionalSupported && this.eat(tt.questionDot) + 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 (optionalSupported) { + 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 +314,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 +331,14 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow) let node = this.startNodeAt(startPos, startLoc) node.callee = base node.arguments = exprList + if (optionalSupported) { + node.optional = optional + } base = this.finishNode(node, "CallExpression") } else if (this.type === tt.backQuote) { + if (optional || optionalChained) { + 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}) diff --git a/acorn/src/lval.js b/acorn/src/lval.js index 8ad4b2639..03b3ce8c6 100644 --- a/acorn/src/lval.js +++ b/acorn/src/lval.js @@ -73,6 +73,10 @@ pp.toAssignable = function(node, isBinding, refDestructuringErrors) { this.toAssignable(node.expression, isBinding, refDestructuringErrors) break + case "ChainExpression": + this.raiseRecoverable(node.start, "Optional chaining cannot appear in left-hand side") + break + case "MemberExpression": if (!isBinding) break @@ -201,6 +205,10 @@ pp.checkLVal = function(expr, bindingType = BIND_NONE, checkClashes) { if (bindingType !== BIND_NONE && bindingType !== BIND_OUTSIDE) this.declareName(expr.name, bindingType, expr.start) break + case "ChainExpression": + this.raiseRecoverable(expr.start, "Optional chaining cannot appear in left-hand side") + break + case "MemberExpression": if (bindingType) this.raiseRecoverable(expr.start, "Binding member expression") break diff --git a/acorn/src/tokenize.js b/acorn/src/tokenize.js index a71a09342..c5840431e 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.questionDot, 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..1e821ff40 100644 --- a/acorn/src/tokentype.js +++ b/acorn/src/tokentype.js @@ -70,6 +70,7 @@ export const types = { colon: new TokenType(":", beforeExpr), dot: new TokenType("."), question: new TokenType("?", beforeExpr), + questionDot: new TokenType("?."), arrow: new TokenType("=>", beforeExpr), template: new TokenType("template"), invalidTemplate: new TokenType("invalidTemplate"), 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..18cb1f204 --- /dev/null +++ b/test/tests-optional-chaining.js @@ -0,0 +1,1429 @@ + +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"); +} + +// Simple +test("obj?.foo", { + "type": "Program", + "start": 0, + "end": 8, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 8, + "expression": { + "type": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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 }) + +// Simple with spacing +test("obj ?. foo", { + "type": "Program", + "start": 0, + "end": 10, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 10, + "expression": { + "type": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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 }) + +// Existing valid syntax +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 }) + +// Chaining +test("obj?.aaa?.bbb", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "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": "ChainExpression", + "start": 7, + "end": 15, + "expression": { + "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": "ChainExpression", + "start": 5, + "end": 13, + "expression": { + "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": "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 + } + }, + "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 `?.` even if it's parenthesized. +test("(obj.foo.bar)?.buz", { + "type": "Program", + "start": 0, + "end": 18, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 18, + "expression": { + "type": "ChainExpression", + "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": "ChainExpression", + "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 })