diff --git a/README.md b/README.md index 1401fc62c6..35e554e8db 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ require("babylon").parse("code", { | `optionalCatchBinding` ([proposal](https://github.com/babel/proposals/issues/7)) | `try {throw 0;} catch{do();}` | | `throwExpressions` ([proposal](https://github.com/babel/proposals/issues/23)) | `() => throw new Error("")` | | `pipelineOperator` ([proposal](https://github.com/babel/proposals/issues/29)) | `a \|> b` | +| `nullishCoalescingOperator` ([proposal](https://github.com/babel/proposals/issues/14)) | `a ?? b` | ### FAQ diff --git a/ast/spec.md b/ast/spec.md index 00703e5c69..4a13d6d697 100644 --- a/ast/spec.md +++ b/ast/spec.md @@ -791,6 +791,7 @@ enum BinaryOperator { | "|" | "^" | "&" | "in" | "instanceof" | "|>" + | "??" } ``` diff --git a/src/parser/expression.js b/src/parser/expression.js index 1e1e36cfda..b44122514c 100644 --- a/src/parser/expression.js +++ b/src/parser/expression.js @@ -297,6 +297,10 @@ export default class ExpressionParser extends LValParser { this.state.potentialArrowAt = startPos; } + if (node.operator === "??") { + this.expectPlugin("nullishCoalescingOperator"); + } + node.right = this.parseExprOp( this.parseMaybeUnary(), startPos, diff --git a/src/tokenizer/index.js b/src/tokenizer/index.js index 281ec74587..fa484ed017 100644 --- a/src/tokenizer/index.js +++ b/src/tokenizer/index.js @@ -581,7 +581,10 @@ export default class Tokenizer extends LocationParser { // '?' const next = this.input.charCodeAt(this.state.pos + 1); const next2 = this.input.charCodeAt(this.state.pos + 2); - if (next === 46 && !(next2 >= 48 && next2 <= 57)) { + if (next === 63) { + // '??' + this.finishOp(tt.nullishCoalescing, 2); + } else if (next === 46 && !(next2 >= 48 && next2 <= 57)) { // '.' not followed by a number this.state.pos += 2; this.finishToken(tt.questionDot); diff --git a/src/tokenizer/types.js b/src/tokenizer/types.js index ee8747ba9d..fb39a693a5 100644 --- a/src/tokenizer/types.js +++ b/src/tokenizer/types.js @@ -132,6 +132,7 @@ export const types: { [name: string]: TokenType } = { bang: new TokenType("!", { beforeExpr, prefix, startsExpr }), tilde: new TokenType("~", { beforeExpr, prefix, startsExpr }), pipeline: new BinopTokenType("|>", 0), + nullishCoalescing: new BinopTokenType("??", 1), logicalOR: new BinopTokenType("||", 1), logicalAND: new BinopTokenType("&&", 2), bitwiseOR: new BinopTokenType("|", 3), diff --git a/test/fixtures/experimental/nullish-coalescing-operator/expression/actual.js b/test/fixtures/experimental/nullish-coalescing-operator/expression/actual.js new file mode 100644 index 0000000000..ea99cb192c --- /dev/null +++ b/test/fixtures/experimental/nullish-coalescing-operator/expression/actual.js @@ -0,0 +1 @@ +foo ?? 1; diff --git a/test/fixtures/experimental/nullish-coalescing-operator/expression/expected.json b/test/fixtures/experimental/nullish-coalescing-operator/expression/expected.json new file mode 100644 index 0000000000..9e85e6f302 --- /dev/null +++ b/test/fixtures/experimental/nullish-coalescing-operator/expression/expected.json @@ -0,0 +1,102 @@ +{ + "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": "BinaryExpression", + "start": 0, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 8 + } + }, + "left": { + "type": "Identifier", + "start": 0, + "end": 3, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 3 + }, + "identifierName": "foo" + }, + "name": "foo" + }, + "operator": "??", + "right": { + "type": "NumericLiteral", + "start": 7, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 8 + } + }, + "extra": { + "rawValue": 1, + "raw": "1" + }, + "value": 1 + } + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/experimental/nullish-coalescing-operator/expression/options.json b/test/fixtures/experimental/nullish-coalescing-operator/expression/options.json new file mode 100644 index 0000000000..1bb4736991 --- /dev/null +++ b/test/fixtures/experimental/nullish-coalescing-operator/expression/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["nullishCoalescingOperator"] +} diff --git a/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/actual.js b/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/actual.js new file mode 100644 index 0000000000..ea99cb192c --- /dev/null +++ b/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/actual.js @@ -0,0 +1 @@ +foo ?? 1; diff --git a/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/options.json b/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/options.json new file mode 100644 index 0000000000..a7057a230a --- /dev/null +++ b/test/fixtures/experimental/nullish-coalescing-operator/no-plugin-error/options.json @@ -0,0 +1,3 @@ +{ + "throws": "This experimental syntax requires enabling the parser plugin: 'nullishCoalescingOperator' (1:7)" +}