diff --git a/ast/spec.md b/ast/spec.md index 0ff1c87c51..72562cb48c 100644 --- a/ast/spec.md +++ b/ast/spec.md @@ -845,10 +845,11 @@ interface MemberExpression <: Expression, Pattern { object: Expression | Super; property: Expression; computed: boolean; + nullPropagation: boolean | null; } ``` -A member expression. If `computed` is `true`, the node corresponds to a computed (`a[b]`) member expression and `property` is an `Expression`. If `computed` is `false`, the node corresponds to a static (`a.b`) member expression and `property` is an `Identifier`. +A member expression. If `computed` is `true`, the node corresponds to a computed (`a[b]`) member expression and `property` is an `Expression`. If `computed` is `false`, the node corresponds to a static (`a.b`) member expression and `property` is an `Identifier`. The `nullPropagation` flags indecates that the member expression can be called even if the object is null or undefined. If this is the object value (null/undefined) should be returned. ### BindExpression diff --git a/src/parser/expression.js b/src/parser/expression.js index 5e5184a875..e8532ae1a9 100644 --- a/src/parser/expression.js +++ b/src/parser/expression.js @@ -284,14 +284,23 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { node.object = base; node.callee = this.parseNoCallExpr(); return this.parseSubscripts(this.finishNode(node, "BindExpression"), startPos, startLoc, noCalls); - } else if (this.eat(tt.question)) { - const node = this.startNodeAt(startPos, startLoc); - node.object = base; - node.optional = true; - this.next(); - node.property = this.parseIdentifier(true); - node.computed = false; - base = this.finishNode(node, "MemberExpression"); + } else if (this.eat(tt.questionDot)) { + base.optional = true; + + if (this.eat(tt.bracketL)) { + const node = this.startNodeAt(startPos, startLoc); + node.object = base; + node.property = this.parseExpression(); + node.computed = true; + this.expect(tt.bracketR); + base = this.finishNode(node, "MemberExpression"); + } else { + const node = this.startNodeAt(startPos, startLoc); + node.object = base; + node.property = this.parseIdentifier(true); + node.computed = false; + base = this.finishNode(node, "MemberExpression"); + } } else if (this.eat(tt.dot)) { const node = this.startNodeAt(startPos, startLoc); node.object = base; @@ -303,6 +312,7 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { node.object = base; node.property = this.parseExpression(); node.computed = true; + delete node.nullPropagation; this.expect(tt.bracketR); base = this.finishNode(node, "MemberExpression"); } else if (!noCalls && this.match(tt.parenL)) { diff --git a/src/tokenizer/index.js b/src/tokenizer/index.js index 076b636381..e03609f6fa 100644 --- a/src/tokenizer/index.js +++ b/src/tokenizer/index.js @@ -391,6 +391,18 @@ export default class Tokenizer { return this.finishOp(code === 61 ? tt.eq : tt.prefix, 1); } + readToken_question() { // '?' + const next = this.input.charCodeAt(this.state.pos + 1); + if (next === 46) { // 46 = question '.' + this.state.pos += 2; + return this.finishToken(tt.questionDot); + } + else { + ++this.state.pos; + return this.finishToken(tt.question); + } + } + getTokenFromCode(code) { switch (code) { // The interpretation of a dot depends on whether it is followed @@ -425,7 +437,7 @@ export default class Tokenizer { return this.finishToken(tt.colon); } - case 63: ++this.state.pos; return this.finishToken(tt.question); + case 63: return this.readToken_question(); case 64: ++this.state.pos; return this.finishToken(tt.at); case 96: // '`' @@ -844,7 +856,7 @@ export default class Tokenizer { const type = this.state.type; let update; - if (type.keyword && prevType === tt.dot) { + if (type.keyword && (prevType === tt.dot || prevType === tt.questionDot)) { this.state.exprAllowed = false; } else if (update = type.updateContext) { update.call(this, prevType); diff --git a/src/tokenizer/types.js b/src/tokenizer/types.js index 1d78aabb40..3afbf09a1f 100644 --- a/src/tokenizer/types.js +++ b/src/tokenizer/types.js @@ -75,6 +75,7 @@ export const types = { doubleColon: new TokenType("::", { beforeExpr }), dot: new TokenType("."), question: new TokenType("?", { beforeExpr }), + questionDot: new TokenType("?."), arrow: new TokenType("=>", { beforeExpr }), template: new TokenType("template"), ellipsis: new TokenType("...", { beforeExpr }), diff --git a/test/fixtures/experimental/uncategorised/63/actual.js b/test/fixtures/experimental/uncategorised/63/actual.js new file mode 100644 index 0000000000..eeb713af30 --- /dev/null +++ b/test/fixtures/experimental/uncategorised/63/actual.js @@ -0,0 +1,5 @@ +o?.x?.y + +o?.x ? o.x.z?.w : o.y?.z?.w + +o?.[0]?.["1"]?.x \ No newline at end of file diff --git a/test/fixtures/experimental/uncategorised/63/expected.json b/test/fixtures/experimental/uncategorised/63/expected.json new file mode 100644 index 0000000000..7f36efbc14 --- /dev/null +++ b/test/fixtures/experimental/uncategorised/63/expected.json @@ -0,0 +1,566 @@ +{ + "type": "File", + "start": 0, + "end": 54, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 54, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "expression": { + "type": "MemberExpression", + "start": 0, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "object": { + "type": "Identifier", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + } + }, + "name": "o", + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 3, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 3 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "name": "x" + }, + "computed": false, + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 6, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 7 + } + }, + "name": "y" + }, + "computed": false + } + }, + { + "type": "ExpressionStatement", + "start": 9, + "end": 36, + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "expression": { + "type": "ConditionalExpression", + "start": 9, + "end": 36, + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "test": { + "type": "MemberExpression", + "start": 9, + "end": 13, + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 4 + } + }, + "object": { + "type": "Identifier", + "start": 9, + "end": 10, + "loc": { + "start": { + "line": 3, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "name": "o", + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 12, + "end": 13, + "loc": { + "start": { + "line": 3, + "column": 3 + }, + "end": { + "line": 3, + "column": 4 + } + }, + "name": "x" + }, + "computed": false + }, + "consequent": { + "type": "MemberExpression", + "start": 16, + "end": 24, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 15 + } + }, + "object": { + "type": "MemberExpression", + "start": 16, + "end": 21, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 12 + } + }, + "object": { + "type": "MemberExpression", + "start": 16, + "end": 19, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 10 + } + }, + "object": { + "type": "Identifier", + "start": 16, + "end": 17, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 8 + } + }, + "name": "o" + }, + "property": { + "type": "Identifier", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 3, + "column": 10 + } + }, + "name": "x" + }, + "computed": false + }, + "property": { + "type": "Identifier", + "start": 20, + "end": 21, + "loc": { + "start": { + "line": 3, + "column": 11 + }, + "end": { + "line": 3, + "column": 12 + } + }, + "name": "z" + }, + "computed": false, + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 23, + "end": 24, + "loc": { + "start": { + "line": 3, + "column": 14 + }, + "end": { + "line": 3, + "column": 15 + } + }, + "name": "w" + }, + "computed": false + }, + "alternate": { + "type": "MemberExpression", + "start": 27, + "end": 36, + "loc": { + "start": { + "line": 3, + "column": 18 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "object": { + "type": "MemberExpression", + "start": 27, + "end": 33, + "loc": { + "start": { + "line": 3, + "column": 18 + }, + "end": { + "line": 3, + "column": 24 + } + }, + "object": { + "type": "MemberExpression", + "start": 27, + "end": 30, + "loc": { + "start": { + "line": 3, + "column": 18 + }, + "end": { + "line": 3, + "column": 21 + } + }, + "object": { + "type": "Identifier", + "start": 27, + "end": 28, + "loc": { + "start": { + "line": 3, + "column": 18 + }, + "end": { + "line": 3, + "column": 19 + } + }, + "name": "o" + }, + "property": { + "type": "Identifier", + "start": 29, + "end": 30, + "loc": { + "start": { + "line": 3, + "column": 20 + }, + "end": { + "line": 3, + "column": 21 + } + }, + "name": "y" + }, + "computed": false, + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 32, + "end": 33, + "loc": { + "start": { + "line": 3, + "column": 23 + }, + "end": { + "line": 3, + "column": 24 + } + }, + "name": "z" + }, + "computed": false, + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 35, + "end": 36, + "loc": { + "start": { + "line": 3, + "column": 26 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "name": "w" + }, + "computed": false + } + } + }, + { + "type": "ExpressionStatement", + "start": 38, + "end": 54, + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "expression": { + "type": "MemberExpression", + "start": 38, + "end": 54, + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "object": { + "type": "MemberExpression", + "start": 38, + "end": 51, + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 13 + } + }, + "object": { + "type": "MemberExpression", + "start": 38, + "end": 44, + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 6 + } + }, + "object": { + "type": "Identifier", + "start": 38, + "end": 39, + "loc": { + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 1 + } + }, + "name": "o", + "nullPropagation": true + }, + "property": { + "type": "NumericLiteral", + "start": 42, + "end": 43, + "loc": { + "start": { + "line": 5, + "column": 4 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "value": 0 + }, + "computed": true, + "nullPropagation": true + }, + "property": { + "type": "StringLiteral", + "start": 47, + "end": 50, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "value": "1" + }, + "computed": true, + "nullPropagation": true + }, + "property": { + "type": "Identifier", + "start": 53, + "end": 54, + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "name": "x" + }, + "computed": false + } + } + ] + } +} \ No newline at end of file