diff --git a/src/parser/expression.js b/src/parser/expression.js index ab94736f2a..2132e71a98 100644 --- a/src/parser/expression.js +++ b/src/parser/expression.js @@ -348,6 +348,23 @@ pp.parseSubscripts = function (base, startPos, startLoc, noCalls) { node.property = this.parseIdentifier(true); node.computed = false; base = this.finishNode(node, "MemberExpression"); + } else if (this.hasPlugin("lightscript") && this.match(tt.elvis)) { + // `x?.y` + const node = this.startNodeAt(startPos, startLoc); + const op = this.state.value; + this.next(); + node.object = base; + if (op === "?.") { + node.property = this.parseIdentifier(true); + node.computed = false; + } else if (op === "?[") { + node.property = this.parseExpression(); + node.computed = true; + this.expect(tt.bracketR); + } else { + this.unexpected(); + } + base = this.finishNode(node, "SafeMemberExpression"); } else if (this.hasPlugin("lightscript") && !noCalls && this.eat(tt.tilde)) { const node = this.startNodeAt(startPos, startLoc); node.left = base; diff --git a/src/plugins/lightscript.js b/src/plugins/lightscript.js index 5a108bbefa..091fe953b6 100644 --- a/src/plugins/lightscript.js +++ b/src/plugins/lightscript.js @@ -450,7 +450,8 @@ pp.parseAwaitArrow = function (left) { }; pp.tryParseNamedArrowDeclaration = function () { - let node = this.startNode(), call = this.startNode(); + let node = this.startNode(); + const call = this.startNode(); if (!this.match(tt.name)) return null; if (this.state.value === "type") return null; diff --git a/src/tokenizer/index.js b/src/tokenizer/index.js index a196b07ea0..186934fa6e 100644 --- a/src/tokenizer/index.js +++ b/src/tokenizer/index.js @@ -523,7 +523,16 @@ export default class Tokenizer { return this.finishToken(tt.colon); } - case 63: ++this.state.pos; return this.finishToken(tt.question); + case 63: + if (this.hasPlugin("lightscript")) { + const next = this.input.charCodeAt(this.state.pos + 1); + // `?.` or `?[` + if (next === 46 || next === 91) { + return this.finishOp(tt.elvis, 2); + } + } + ++this.state.pos; + return this.finishToken(tt.question); case 64: ++this.state.pos; return this.finishToken(tt.at); case 96: // '`' diff --git a/src/tokenizer/types.js b/src/tokenizer/types.js index ac5d522443..2857e9475f 100644 --- a/src/tokenizer/types.js +++ b/src/tokenizer/types.js @@ -58,6 +58,7 @@ export const types = { colonEq: new TokenType(":=", { beforeExpr, isAssign }), tilde: new TokenType("~"), awaitArrow: new TokenType("<-", { beforeExpr, isAssign }), + elvis: new TokenType("?."), num: new TokenType("num", { startsExpr }), regexp: new TokenType("regexp", { startsExpr }), diff --git a/test/fixtures/lightscript/safe-member-expression/basic/actual.js b/test/fixtures/lightscript/safe-member-expression/basic/actual.js new file mode 100644 index 0000000000..f45302af16 --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/basic/actual.js @@ -0,0 +1 @@ +x?.y diff --git a/test/fixtures/lightscript/safe-member-expression/basic/expected.json b/test/fixtures/lightscript/safe-member-expression/basic/expected.json new file mode 100644 index 0000000000..062c5f5ff1 --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/basic/expected.json @@ -0,0 +1,99 @@ +{ + "type": "File", + "start": 0, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 4 + } + }, + "expression": { + "type": "SafeMemberExpression", + "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 + }, + "identifierName": "x" + }, + "name": "x" + }, + "property": { + "type": "Identifier", + "start": 3, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 3 + }, + "end": { + "line": 1, + "column": 4 + }, + "identifierName": "y" + }, + "name": "y" + }, + "computed": false + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/lightscript/safe-member-expression/chained/actual.js b/test/fixtures/lightscript/safe-member-expression/chained/actual.js new file mode 100644 index 0000000000..4249e41c2a --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/chained/actual.js @@ -0,0 +1 @@ +x.y?.a?.b.c[d](e, f)[g]?.h diff --git a/test/fixtures/lightscript/safe-member-expression/chained/expected.json b/test/fixtures/lightscript/safe-member-expression/chained/expected.json new file mode 100644 index 0000000000..e6f6c2c71f --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/chained/expected.json @@ -0,0 +1,348 @@ +{ + "type": "File", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "expression": { + "type": "SafeMemberExpression", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "object": { + "type": "CallExpression", + "start": 0, + "end": 20, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 20 + } + }, + "callee": { + "type": "MemberExpression", + "start": 0, + "end": 14, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 14 + } + }, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 11 + } + }, + "object": { + "type": "SafeMemberExpression", + "start": 0, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "object": { + "type": "SafeMemberExpression", + "start": 0, + "end": 6, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 6 + } + }, + "object": { + "type": "MemberExpression", + "start": 0, + "end": 3, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 3 + } + }, + "object": { + "type": "Identifier", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + }, + "identifierName": "x" + }, + "name": "x" + }, + "property": { + "type": "Identifier", + "start": 2, + "end": 3, + "loc": { + "start": { + "line": 1, + "column": 2 + }, + "end": { + "line": 1, + "column": 3 + }, + "identifierName": "y" + }, + "name": "y" + }, + "computed": false + }, + "property": { + "type": "Identifier", + "start": 5, + "end": 6, + "loc": { + "start": { + "line": 1, + "column": 5 + }, + "end": { + "line": 1, + "column": 6 + }, + "identifierName": "a" + }, + "name": "a" + }, + "computed": false + }, + "property": { + "type": "Identifier", + "start": 8, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 8 + }, + "end": { + "line": 1, + "column": 9 + }, + "identifierName": "b" + }, + "name": "b" + }, + "computed": false + }, + "property": { + "type": "Identifier", + "start": 10, + "end": 11, + "loc": { + "start": { + "line": 1, + "column": 10 + }, + "end": { + "line": 1, + "column": 11 + }, + "identifierName": "c" + }, + "name": "c" + }, + "computed": false + }, + "property": { + "type": "Identifier", + "start": 12, + "end": 13, + "loc": { + "start": { + "line": 1, + "column": 12 + }, + "end": { + "line": 1, + "column": 13 + }, + "identifierName": "d" + }, + "name": "d" + }, + "computed": true + }, + "arguments": [ + { + "type": "Identifier", + "start": 15, + "end": 16, + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 1, + "column": 16 + }, + "identifierName": "e" + }, + "name": "e" + }, + { + "type": "Identifier", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 18 + }, + "end": { + "line": 1, + "column": 19 + }, + "identifierName": "f" + }, + "name": "f" + } + ] + }, + "property": { + "type": "Identifier", + "start": 21, + "end": 22, + "loc": { + "start": { + "line": 1, + "column": 21 + }, + "end": { + "line": 1, + "column": 22 + }, + "identifierName": "g" + }, + "name": "g" + }, + "computed": true + }, + "property": { + "type": "Identifier", + "start": 25, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 26 + }, + "identifierName": "h" + }, + "name": "h" + }, + "computed": false + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/lightscript/safe-member-expression/computed/actual.js b/test/fixtures/lightscript/safe-member-expression/computed/actual.js new file mode 100644 index 0000000000..ffe21b97a0 --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/computed/actual.js @@ -0,0 +1 @@ +x?[y + y] diff --git a/test/fixtures/lightscript/safe-member-expression/computed/expected.json b/test/fixtures/lightscript/safe-member-expression/computed/expected.json new file mode 100644 index 0000000000..c3870b58b9 --- /dev/null +++ b/test/fixtures/lightscript/safe-member-expression/computed/expected.json @@ -0,0 +1,132 @@ +{ + "type": "File", + "start": 0, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "expression": { + "type": "SafeMemberExpression", + "start": 0, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 9 + } + }, + "object": { + "type": "Identifier", + "start": 0, + "end": 1, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 1 + }, + "identifierName": "x" + }, + "name": "x" + }, + "property": { + "type": "BinaryExpression", + "start": 3, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 3 + }, + "end": { + "line": 1, + "column": 8 + } + }, + "left": { + "type": "Identifier", + "start": 3, + "end": 4, + "loc": { + "start": { + "line": 1, + "column": 3 + }, + "end": { + "line": 1, + "column": 4 + }, + "identifierName": "y" + }, + "name": "y" + }, + "operator": "+", + "right": { + "type": "Identifier", + "start": 7, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 8 + }, + "identifierName": "y" + }, + "name": "y" + } + }, + "computed": true + } + } + ], + "directives": [] + } +} \ No newline at end of file