diff --git a/acorn-loose/src/expression.js b/acorn-loose/src/expression.js index d2507f435..be46375bf 100644 --- a/acorn-loose/src/expression.js +++ b/acorn-loose/src/expression.js @@ -102,7 +102,7 @@ lp.parseExprOp = function(left, start, minPrec, noIn, indent, line) { let rightStart = this.storeCurrentPos() node.right = this.parseExprOp(this.parseMaybeUnary(false), rightStart, prec, noIn, indent, line) } - this.finishNode(node, /&&|\|\|/.test(node.operator) ? "LogicalExpression" : "BinaryExpression") + this.finishNode(node, /&&|\|\||\?\?/.test(node.operator) ? "LogicalExpression" : "BinaryExpression") return this.parseExprOp(node, start, minPrec, noIn, indent, line) } } diff --git a/acorn/src/expression.js b/acorn/src/expression.js index 006aa3bed..ed7a1f895 100644 --- a/acorn/src/expression.js +++ b/acorn/src/expression.js @@ -186,11 +186,20 @@ pp.parseExprOp = function(left, leftStartPos, leftStartLoc, minPrec, noIn) { if (prec != null && (!noIn || this.type !== tt._in)) { if (prec > minPrec) { let logical = this.type === tt.logicalOR || this.type === tt.logicalAND + let coalesce = this.type === tt.coalesce + if (coalesce) { + // Handle the precedence of `tt.coalesce` as equal to the range of logical expressions. + // In other words, `node.right` shouldn't contain logical expressions in order to check the mixed error. + prec = tt.logicalAND.binop + } let op = this.value this.next() let startPos = this.start, startLoc = this.startLoc let right = this.parseExprOp(this.parseMaybeUnary(null, false), startPos, startLoc, prec, noIn) - let node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical) + let node = this.buildBinary(leftStartPos, leftStartLoc, left, right, op, logical || coalesce) + if ((logical && this.type === tt.coalesce) || (coalesce && (this.type === tt.logicalOR || this.type === tt.logicalAND))) { + this.raiseRecoverable(this.start, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + } return this.parseExprOp(node, leftStartPos, leftStartLoc, minPrec, noIn) } } diff --git a/acorn/src/tokenize.js b/acorn/src/tokenize.js index 835fdcd96..a71a09342 100644 --- a/acorn/src/tokenize.js +++ b/acorn/src/tokenize.js @@ -289,6 +289,14 @@ pp.readToken_eq_excl = function(code) { // '=!' return this.finishOp(code === 61 ? tt.eq : tt.prefix, 1) } +pp.readToken_question = function() { // '?' + if (this.options.ecmaVersion >= 11) { + let next = this.input.charCodeAt(this.pos + 1) + if (next === 63) return this.finishOp(tt.coalesce, 2) + } + return this.finishOp(tt.question, 1) +} + pp.getTokenFromCode = function(code) { switch (code) { // The interpretation of a dot depends on whether it is followed @@ -306,7 +314,6 @@ pp.getTokenFromCode = function(code) { case 123: ++this.pos; return this.finishToken(tt.braceL) case 125: ++this.pos; return this.finishToken(tt.braceR) case 58: ++this.pos; return this.finishToken(tt.colon) - case 63: ++this.pos; return this.finishToken(tt.question) case 96: // '`' if (this.options.ecmaVersion < 6) break @@ -356,6 +363,9 @@ pp.getTokenFromCode = function(code) { case 61: case 33: // '=!' return this.readToken_eq_excl(code) + case 63: // '?' + return this.readToken_question() + case 126: // '~' return this.finishOp(tt.prefix, 1) } diff --git a/acorn/src/tokentype.js b/acorn/src/tokentype.js index 7a8c48a6d..77b6ccb2c 100644 --- a/acorn/src/tokentype.js +++ b/acorn/src/tokentype.js @@ -108,6 +108,7 @@ export const types = { star: binop("*", 10), slash: binop("/", 10), starstar: new TokenType("**", {beforeExpr: true}), + coalesce: binop("??", 1), // Keyword token types. _break: kw("break"), diff --git a/bin/run_test262.js b/bin/run_test262.js index 1521bbe15..0c5a788eb 100644 --- a/bin/run_test262.js +++ b/bin/run_test262.js @@ -10,7 +10,6 @@ const unsupportedFeatures = [ "class-static-fields-private", "class-static-fields-public", "class-static-methods-private", - "coalesce-expression", "numeric-separator-literal", "optional-chaining", "top-level-await" diff --git a/test/run.js b/test/run.js index f0e89ac97..147e27a51 100644 --- a/test/run.js +++ b/test/run.js @@ -19,6 +19,7 @@ require("./tests-dynamic-import.js"); require("./tests-export-all-as-ns-from-source.js"); require("./tests-import-meta.js"); + require("./tests-nullish-coalescing.js"); var acorn = require("../acorn") var acorn_loose = require("../acorn-loose") diff --git a/test/tests-nullish-coalescing.js b/test/tests-nullish-coalescing.js new file mode 100644 index 000000000..6cd14a5d9 --- /dev/null +++ b/test/tests-nullish-coalescing.js @@ -0,0 +1,546 @@ + +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("a ?? b", { + "type": "Program", + "start": 0, + "end": 6, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 6, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 6, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 5, + "end": 6, + "name": "b" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a ?? b ?? c", { + "type": "Program", + "start": 0, + "end": 11, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 11, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 11, + "left": { + "type": "LogicalExpression", + "start": 0, + "end": 6, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 5, + "end": 6, + "name": "b" + } + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 10, + "end": 11, + "name": "c" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("a | b ?? c | d", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 14, + "left": { + "type": "BinaryExpression", + "start": 0, + "end": 5, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "|", + "right": { + "type": "Identifier", + "start": 4, + "end": 5, + "name": "b" + } + }, + "operator": "??", + "right": { + "type": "BinaryExpression", + "start": 9, + "end": 14, + "left": { + "type": "Identifier", + "start": 9, + "end": 10, + "name": "c" + }, + "operator": "|", + "right": { + "type": "Identifier", + "start": 13, + "end": 14, + "name": "d" + } + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a ?? b ? c : d", { + "type": "Program", + "start": 0, + "end": 14, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 14, + "expression": { + "type": "ConditionalExpression", + "start": 0, + "end": 14, + "test": { + "type": "LogicalExpression", + "start": 0, + "end": 6, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 5, + "end": 6, + "name": "b" + } + }, + "consequent": { + "type": "Identifier", + "start": 9, + "end": 10, + "name": "c" + }, + "alternate": { + "type": "Identifier", + "start": 13, + "end": 14, + "name": "d" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("(a || b) ?? c", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "LogicalExpression", + "start": 1, + "end": 7, + "left": { + "type": "Identifier", + "start": 1, + "end": 2, + "name": "a" + }, + "operator": "||", + "right": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + } + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 12, + "end": 13, + "name": "c" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a || (b ?? c)", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "||", + "right": { + "type": "LogicalExpression", + "start": 6, + "end": 12, + "left": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 11, + "end": 12, + "name": "c" + } + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("(a && b) ?? c", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "LogicalExpression", + "start": 1, + "end": 7, + "left": { + "type": "Identifier", + "start": 1, + "end": 2, + "name": "a" + }, + "operator": "&&", + "right": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + } + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 12, + "end": 13, + "name": "c" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a && (b ?? c)", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "&&", + "right": { + "type": "LogicalExpression", + "start": 6, + "end": 12, + "left": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 11, + "end": 12, + "name": "c" + } + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("(a ?? b) || c", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "LogicalExpression", + "start": 1, + "end": 7, + "left": { + "type": "Identifier", + "start": 1, + "end": 2, + "name": "a" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + } + }, + "operator": "||", + "right": { + "type": "Identifier", + "start": 12, + "end": 13, + "name": "c" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a ?? (b || c)", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "??", + "right": { + "type": "LogicalExpression", + "start": 6, + "end": 12, + "left": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + }, + "operator": "||", + "right": { + "type": "Identifier", + "start": 11, + "end": 12, + "name": "c" + } + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +test("(a ?? b) && c", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "LogicalExpression", + "start": 1, + "end": 7, + "left": { + "type": "Identifier", + "start": 1, + "end": 2, + "name": "a" + }, + "operator": "??", + "right": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + } + }, + "operator": "&&", + "right": { + "type": "Identifier", + "start": 12, + "end": 13, + "name": "c" + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) +test("a ?? (b && c)", { + "type": "Program", + "start": 0, + "end": 13, + "body": [ + { + "type": "ExpressionStatement", + "start": 0, + "end": 13, + "expression": { + "type": "LogicalExpression", + "start": 0, + "end": 13, + "left": { + "type": "Identifier", + "start": 0, + "end": 1, + "name": "a" + }, + "operator": "??", + "right": { + "type": "LogicalExpression", + "start": 6, + "end": 12, + "left": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "b" + }, + "operator": "&&", + "right": { + "type": "Identifier", + "start": 11, + "end": 12, + "name": "c" + } + } + } + } + ], + "sourceType": "script" +}, { ecmaVersion: 11 }) + +testFail("a ?? b", "Unexpected token (1:3)", { ecmaVersion: 10 }) +testFail("?? b", "Unexpected token (1:0)", { ecmaVersion: 11 }) +testFail("a ??", "Unexpected token (1:4)", { ecmaVersion: 11 }) +testFail("a || b ?? c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:7)", { ecmaVersion: 11 }) +testFail("a && b ?? c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:7)", { ecmaVersion: 11 }) +testFail("a ?? b || c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:7)", { ecmaVersion: 11 }) +testFail("a ?? b && c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:7)", { ecmaVersion: 11 }) + +testFail("a+1 || b+1 ?? c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:11)", { ecmaVersion: 11 }) +testFail("a+1 && b+1 ?? c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:11)", { ecmaVersion: 11 }) +testFail("a+1 ?? b+1 || c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:11)", { ecmaVersion: 11 }) +testFail("a+1 ?? b+1 && c", "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses (1:11)", { ecmaVersion: 11 })