From d4fe94700f15faad049ca6d922e920d4407cbc46 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Sun, 15 Nov 2020 16:58:52 -0500 Subject: [PATCH] [[FEAT]] Implement support for nullish coalescing --- src/jshint.js | 23 +++++++++++ src/lex.js | 9 ++++- src/options.js | 3 +- tests/test262/expectations.txt | 40 ------------------- tests/unit/options.js | 26 ++++++++++++ tests/unit/parser.js | 73 ++++++++++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 42 deletions(-) diff --git a/src/jshint.js b/src/jshint.js index ed987099ac..c8d4ecd51e 100644 --- a/src/jshint.js +++ b/src/jshint.js @@ -2435,6 +2435,26 @@ var JSHINT = (function() { return that; }, andPrecedence); + infix("??", function(context, left, that) { + if (!left.paren && (left.id === "||" || left.id === "&&")) { + error("E024", that, "??"); + } + + if (!state.inES11()) { + warning("W119", that, "nullish coalescing", "11"); + } + + increaseComplexityCount(); + that.left = left; + var right = that.right = expression(context, 39); + + if (!right.paren && (right.id === "||" || right.id === "&&")) { + error("E024", that.right, that.right.id); + } + + return that; + }, 39); + // The Exponentiation operator, introduced in ECMAScript 2016 // // ExponentiationExpression[Yield] : @@ -3236,6 +3256,9 @@ var JSHINT = (function() { // Used to cover a unary expression as the left-hand side of the // exponentiation operator (beginsUnaryExpression(ret) && state.tokens.next.id === "**") || + // Used to cover a logical operator as the right-hand side of the + // nullish coalescing operator + (preceeding.id === "??" && (ret.id === "&&" || ret.id === "||")) || // Used to delineate an integer number literal from a dereferencing // punctuator (otherwise interpreted as a decimal point) (ret.type === "(number)" && diff --git a/src/lex.js b/src/lex.js index e21d479167..4748833fd6 100644 --- a/src/lex.js +++ b/src/lex.js @@ -249,7 +249,6 @@ Lexer.prototype = { case "]": case ":": case "~": - case "?": return { type: Token.Punctuator, value: ch1 @@ -288,6 +287,14 @@ Lexer.prototype = { // Peek more characters ch2 = this.peek(1); + + if (ch1 === "?") { + return { + type: Token.Punctuator, + value: ch2 === "?" ? "??" : "?" + }; + } + ch3 = this.peek(2); ch4 = this.peek(3); diff --git a/src/options.js b/src/options.js index 3fae239adf..98d46bd545 100644 --- a/src/options.js +++ b/src/options.js @@ -1052,7 +1052,8 @@ exports.val = { * 10](https://www.ecma-international.org/ecma-262/10.0/index.html). * Notable additions: optional catch bindings. * - `11` - To enable language features introduced by ECMAScript 11. Notable - * additions: "export * as ns from 'module'". + * additions: "export * as ns from 'module'" and the nullish coalescing + * operator. */ esversion: 5 }; diff --git a/tests/test262/expectations.txt b/tests/test262/expectations.txt index 66b7cb2f8e..d117ae4394 100644 --- a/tests/test262/expectations.txt +++ b/tests/test262/expectations.txt @@ -9060,46 +9060,6 @@ test/language/expressions/class/private-static-method-brand-check-multiple-evalu test/language/expressions/class/private-static-method-brand-check-multiple-evaluations-of-class-factory.js(strict mode) test/language/expressions/class/private-static-setter-multiple-evaluations-of-class-factory.js(default) test/language/expressions/class/private-static-setter-multiple-evaluations-of-class-factory.js(strict mode) -test/language/expressions/coalesce/abrupt-is-a-short-circuit.js(default) -test/language/expressions/coalesce/abrupt-is-a-short-circuit.js(strict mode) -test/language/expressions/coalesce/chainable-if-parenthesis-covered-logical-and.js(default) -test/language/expressions/coalesce/chainable-if-parenthesis-covered-logical-and.js(strict mode) -test/language/expressions/coalesce/chainable-if-parenthesis-covered-logical-or.js(default) -test/language/expressions/coalesce/chainable-if-parenthesis-covered-logical-or.js(strict mode) -test/language/expressions/coalesce/chainable-with-bitwise-and.js(default) -test/language/expressions/coalesce/chainable-with-bitwise-and.js(strict mode) -test/language/expressions/coalesce/chainable-with-bitwise-or.js(default) -test/language/expressions/coalesce/chainable-with-bitwise-or.js(strict mode) -test/language/expressions/coalesce/chainable-with-bitwise-xor.js(default) -test/language/expressions/coalesce/chainable-with-bitwise-xor.js(strict mode) -test/language/expressions/coalesce/chainable.js(default) -test/language/expressions/coalesce/chainable.js(strict mode) -test/language/expressions/coalesce/follows-null.js(default) -test/language/expressions/coalesce/follows-null.js(strict mode) -test/language/expressions/coalesce/follows-undefined.js(default) -test/language/expressions/coalesce/follows-undefined.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-0.js(default) -test/language/expressions/coalesce/short-circuit-number-0.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-42.js(default) -test/language/expressions/coalesce/short-circuit-number-42.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-empty-string.js(default) -test/language/expressions/coalesce/short-circuit-number-empty-string.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-false.js(default) -test/language/expressions/coalesce/short-circuit-number-false.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-object.js(default) -test/language/expressions/coalesce/short-circuit-number-object.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-string.js(default) -test/language/expressions/coalesce/short-circuit-number-string.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-symbol.js(default) -test/language/expressions/coalesce/short-circuit-number-symbol.js(strict mode) -test/language/expressions/coalesce/short-circuit-number-true.js(default) -test/language/expressions/coalesce/short-circuit-number-true.js(strict mode) -test/language/expressions/coalesce/short-circuit-prevents-evaluation.js(default) -test/language/expressions/coalesce/short-circuit-prevents-evaluation.js(strict mode) -test/language/expressions/coalesce/tco-pos-null.js(strict mode) -test/language/expressions/coalesce/tco-pos-undefined.js(strict mode) -test/language/expressions/conditional/coalesce-expr-ternary.js(default) -test/language/expressions/conditional/coalesce-expr-ternary.js(strict mode) test/language/expressions/logical-assignment/lgcl-and-assignment-operator-bigint.js(default) test/language/expressions/logical-assignment/lgcl-and-assignment-operator-bigint.js(strict mode) test/language/expressions/logical-assignment/lgcl-and-assignment-operator-lhs-before-rhs.js(default) diff --git a/tests/unit/options.js b/tests/unit/options.js index b2fc831b1e..b130069ac7 100644 --- a/tests/unit/options.js +++ b/tests/unit/options.js @@ -2735,6 +2735,12 @@ exports.maxcomplexity = function (test) { TestRun(test) .test(src, { es3: true }); + TestRun(test, "nullish coalescing operator") + .addError(1, 11, "This function's cyclomatic complexity is too high. (2)") + .test([ + "function f() { 0 ?? 0; }" + ], { esversion: 11, expr: true, maxcomplexity: 1 }); + test.done(); }; @@ -3669,6 +3675,26 @@ singleGroups.destructuringAssign = function (test) { test.done(); }; +singleGroups.nullishCoalescing = function (test) { + TestRun(test) + .addError(1, 1, "Unnecessary grouping operator.") + .addError(2, 6, "Unnecessary grouping operator.") + .test([ + "(0) ?? 0;", + "0 ?? (0);" + ], { singleGroups: true, expr: true, esversion: 11 }); + + TestRun(test) + .test([ + "0 ?? (0 || 0);", + "(0 ?? 0) || 0;", + "0 ?? (0 && 0);", + "(0 ?? 0) && 0;" + ], { singleGroups: true, expr: true, esversion: 11 }); + + test.done(); +}; + exports.elision = function (test) { var code = [ "var a = [1,,2];", diff --git a/tests/unit/parser.js b/tests/unit/parser.js index ea3b1352a0..00e05cbbbc 100644 --- a/tests/unit/parser.js +++ b/tests/unit/parser.js @@ -10267,3 +10267,76 @@ exports.parensAfterDeclaration = function (test) { test.done(); }; + +exports.nullishCoalescing = {}; + +exports.nullishCoalescing.positive = function(test) { + TestRun(test, "requires esversion: 11") + .addError(1, 3, "'nullish coalescing' is only available in ES11 (use 'esversion: 11').") + .test([ + "0 ?? 0;" + ], { esversion: 10, expr: true }); + + TestRun(test, "does not stand alone") + .addError(1, 6, "Expected an assignment or function call and instead saw an expression.") + .test([ + "0 ?? 0;" + ], { esversion: 11 }); + + TestRun(test, "precedence with bitwise OR") + .test([ + "0 | 0 ?? 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "precedence with conditional expression") + .test([ + "0 ?? 0 ? 0 ?? 0 : 0 ?? 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "precedence with expression") + .test([ + "0 ?? 0, 0 ?? 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "covered") + .test([ + "0 || (0 ?? 0);", + "(0 || 0) ?? 0;", + "(0 ?? 0) || 0;", + "0 ?? (0 || 0);", + "0 && (0 ?? 0);", + "(0 && 0) ?? 0;", + "(0 ?? 0) && 0;", + "0 ?? (0 && 0);" + ], { esversion: 11, expr: true }); + + test.done(); +}; + +exports.nullishCoalescing.negative = function(test) { + TestRun(test, "precedence with logical OR") + .addError(1, 8, "Unexpected '??'.") + .test([ + "0 || 0 ?? 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "precedence with logical OR") + .addError(1, 8, "Unexpected '||'.") + .test([ + "0 ?? 0 || 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "precedence with logical AND") + .addError(1, 8, "Unexpected '??'.") + .test([ + "0 && 0 ?? 0;" + ], { esversion: 11, expr: true }); + + TestRun(test, "precedence with logical AND") + .addError(1, 8, "Unexpected '&&'.") + .test([ + "0 ?? 0 && 0;" + ], { esversion: 11, expr: true }); + + test.done(); +};