From b125dbea75c237cfd4553305f14d9d30fdd3a22c Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Sat, 13 Feb 2021 16:26:40 -0500 Subject: [PATCH] [[FEAT]] Add support for optional chaining --- src/jshint.js | 35 +++++++++++++- src/lex.js | 10 +++- src/options.js | 4 +- tests/test262/expectations.txt | 46 ++---------------- tests/unit/options.js | 24 ++++++++++ tests/unit/parser.js | 85 ++++++++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 48 deletions(-) diff --git a/src/jshint.js b/src/jshint.js index 07e0671f88..ccaf3d5171 100644 --- a/src/jshint.js +++ b/src/jshint.js @@ -2753,7 +2753,9 @@ var JSHINT = (function() { } } } else { - if (c.id !== "." && c.id !== "[" && c.id !== "(") { + if (c.id === "?." && !c.paren) { + error("E024", c, "?."); + } else if (c.id !== "." && c.id !== "[" && c.id !== "(") { /* istanbul ignore next */ warning("W056", state.tokens.curr); } @@ -3054,6 +3056,31 @@ var JSHINT = (function() { return that; }, 160, true); + infix("?.", function(context, left, that) { + if (!state.inES11()) { + warning("W119", state.tokens.curr, "Optional chaining", "11"); + } + + + if (checkPunctuator(state.tokens.next, "[")) { + that.left = left; + advance(); + that.right = state.tokens.curr.led(context, left); + } else if (checkPunctuator(state.tokens.next, "(")) { + that.left = left; + advance(); + that.right = state.tokens.curr.led(context, left); + } else { + state.syntax["."].led.call(that, context, left); + } + + if (state.tokens.next.type === "(template)") { + error("E024", state.tokens.next, "`"); + } + + return that; + }, 160, true); + infix("(", function(context, left, that) { if (state.option.immed && left && !left.immed && left.id === "function") { warning("W062"); @@ -3274,7 +3301,11 @@ var JSHINT = (function() { (ret.type === "(number)" && checkPunctuator(pn, ".") && /^\d+$/.test(ret.value)) || // Used to wrap object destructuring assignment - (opening.beginsStmt && ret.id === "=" && ret.left.id === "{"); + (opening.beginsStmt && ret.id === "=" && ret.left.id === "{") || + // Used to allow optional chaining with other language features which + // are otherwise restricted. + (ret.id === "?." && + (preceeding.id === "new" || state.tokens.next.type === "(template)")); } } diff --git a/src/lex.js b/src/lex.js index 4748833fd6..f6858f69b8 100644 --- a/src/lex.js +++ b/src/lex.js @@ -287,15 +287,23 @@ Lexer.prototype = { // Peek more characters ch2 = this.peek(1); + ch3 = this.peek(2); if (ch1 === "?") { + // Optional chaining + if (ch2 === "." && !reg.decimalDigit.test(ch3)) { + return { + type: Token.Punctuator, + value: "?." + }; + } + return { type: Token.Punctuator, value: ch2 === "?" ? "??" : "?" }; } - ch3 = this.peek(2); ch4 = this.peek(3); // 4-character punctuator: >>>= diff --git a/src/options.js b/src/options.js index 394a8c8187..b3ab40a6f9 100644 --- a/src/options.js +++ b/src/options.js @@ -1052,8 +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'", `import.meta` and the nullish - * coalescing operator. + * additions: "export * as ns from 'module'", `import.meta`, the nullish + * coalescing operator, and optional chaining. */ esversion: 5 }; diff --git a/tests/test262/expectations.txt b/tests/test262/expectations.txt index 6b4a25f303..72fb298852 100644 --- a/tests/test262/expectations.txt +++ b/tests/test262/expectations.txt @@ -9136,48 +9136,6 @@ test/language/expressions/object/ident-name-prop-name-literal-default-escaped-ex test/language/expressions/object/ident-name-prop-name-literal-default-escaped-ext.js(strict mode) test/language/expressions/object/ident-name-prop-name-literal-extends-escaped-ext.js(default) test/language/expressions/object/ident-name-prop-name-literal-extends-escaped-ext.js(strict mode) -test/language/expressions/optional-chaining/call-expression.js(default) -test/language/expressions/optional-chaining/call-expression.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-do.js(default) -test/language/expressions/optional-chaining/iteration-statement-do.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-for-await-of.js(default) -test/language/expressions/optional-chaining/iteration-statement-for-await-of.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-for-in.js(default) -test/language/expressions/optional-chaining/iteration-statement-for-in.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-for-of-type-error.js(default) -test/language/expressions/optional-chaining/iteration-statement-for-of-type-error.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-for.js(default) -test/language/expressions/optional-chaining/iteration-statement-for.js(strict mode) -test/language/expressions/optional-chaining/iteration-statement-while.js(default) -test/language/expressions/optional-chaining/iteration-statement-while.js(strict mode) -test/language/expressions/optional-chaining/member-expression-async-identifier.js(default) -test/language/expressions/optional-chaining/member-expression-async-identifier.js(strict mode) -test/language/expressions/optional-chaining/member-expression-async-literal.js(default) -test/language/expressions/optional-chaining/member-expression-async-literal.js(strict mode) -test/language/expressions/optional-chaining/member-expression-async-this.js(default) -test/language/expressions/optional-chaining/member-expression-async-this.js(strict mode) -test/language/expressions/optional-chaining/member-expression.js(default) -test/language/expressions/optional-chaining/member-expression.js(strict mode) -test/language/expressions/optional-chaining/new-target-optional-call.js(default) -test/language/expressions/optional-chaining/new-target-optional-call.js(strict mode) -test/language/expressions/optional-chaining/optional-call-preserves-this.js(default) -test/language/expressions/optional-chaining/optional-call-preserves-this.js(strict mode) -test/language/expressions/optional-chaining/optional-chain-async-optional-chain-square-brackets.js(default) -test/language/expressions/optional-chaining/optional-chain-async-optional-chain-square-brackets.js(strict mode) -test/language/expressions/optional-chaining/optional-chain-async-square-brackets.js(default) -test/language/expressions/optional-chaining/optional-chain-async-square-brackets.js(strict mode) -test/language/expressions/optional-chaining/optional-chain-expression-optional-expression.js(default) -test/language/expressions/optional-chaining/optional-chain-expression-optional-expression.js(strict mode) -test/language/expressions/optional-chaining/optional-chain.js(default) -test/language/expressions/optional-chaining/optional-chain.js(strict mode) -test/language/expressions/optional-chaining/optional-expression.js(default) -test/language/expressions/optional-chaining/optional-expression.js(strict mode) -test/language/expressions/optional-chaining/runtime-semantics-evaluation.js(default) -test/language/expressions/optional-chaining/runtime-semantics-evaluation.js(strict mode) -test/language/expressions/optional-chaining/short-circuiting.js(default) -test/language/expressions/optional-chaining/short-circuiting.js(strict mode) -test/language/expressions/optional-chaining/super-property-optional-call.js(default) -test/language/expressions/optional-chaining/super-property-optional-call.js(strict mode) test/language/module-code/top-level-await/await-awaits-thenable-not-callable.js(default) test/language/module-code/top-level-await/await-awaits-thenable-not-callable.js(strict mode) test/language/module-code/top-level-await/await-awaits-thenables-that-throw.js(default) @@ -10324,4 +10282,6 @@ test/language/statements/for-of/dstr/obj-prop-elem-target-obj-literal-prop-ref.j test/language/expressions/import.meta/syntax/escape-sequence-import.js(default) test/language/expressions/import.meta/syntax/escape-sequence-import.js(strict mode) test/language/expressions/import.meta/syntax/escape-sequence-meta.js(default) -test/language/expressions/import.meta/syntax/escape-sequence-meta.js(strict mode) \ No newline at end of file +test/language/expressions/import.meta/syntax/escape-sequence-meta.js(strict mode) +test/language/expressions/optional-chaining/call-expression-super-no-base.js(default) +test/language/expressions/optional-chaining/call-expression-super-no-base.js(strict mode) \ No newline at end of file diff --git a/tests/unit/options.js b/tests/unit/options.js index e18f8d4127..ca0d54324f 100644 --- a/tests/unit/options.js +++ b/tests/unit/options.js @@ -3708,6 +3708,30 @@ singleGroups.nullishCoalescing = function (test) { test.done(); }; +singleGroups.optionalChaining = function (test) { + var code = [ + "new ({}?.constructor)();", + "({}?.toString)``;", + // Invalid forms: + "([])?.x;", + "([]?.x).x;", + "([]?.x)?.x;" + ]; + + TestRun(test) + .addError(1, 21, "Bad constructor.") + .addError(2, 15, "Expected an assignment or function call and instead saw an expression.") + .addError(3, 1, "Unnecessary grouping operator.") + .addError(3, 7, "Expected an assignment or function call and instead saw an expression.") + .addError(4, 1, "Unnecessary grouping operator.") + .addError(4, 9, "Expected an assignment or function call and instead saw an expression.") + .addError(5, 1, "Unnecessary grouping operator.") + .addError(5, 10, "Expected an assignment or function call and instead saw an expression.") + .test(code, { singleGroups: 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 b0f1af4e97..70bab6999a 100644 --- a/tests/unit/parser.js +++ b/tests/unit/parser.js @@ -10411,3 +10411,88 @@ exports.nullishCoalescing.negative = function(test) { test.done(); }; + +exports.optionalChaining = function (test) { + TestRun(test, "prior language editions") + .addError(1, 5, "'Optional chaining' is only available in ES11 (use 'esversion: 11').") + .addError(1, 7, "Expected an assignment or function call and instead saw an expression.") + .test( + "true?.x;", + { esversion: 10 } + ); + + TestRun(test, "literal property name") + .addError(1, 7, "Expected an assignment or function call and instead saw an expression.") + .addError(2, 5, "Expected an assignment or function call and instead saw an expression.") + .addError(3, 7, "Expected an assignment or function call and instead saw an expression.") + .test([ + "true?.x;", + "[]?.x;", + "({}?.x);" + ], { esversion: 11 } + ); + + TestRun(test, "literal property name restriction") + .addError(1, 40, "Expected an assignment or function call and instead saw an expression.") + .addError(1, 46, "Strict violation.") + .test( + "(function() { 'use strict'; arguments?.callee; })();", + { esversion: 11 } + ); + + TestRun(test, "dynamic property name") + .addError(1, 14, "Expected an assignment or function call and instead saw an expression.") + .addError(2, 11, "Expected an assignment or function call and instead saw an expression.") + .addError(2, 7, "['x'] is better written in dot notation.") + .test([ + "true?.[void 0];", + "true?.['x'];" + ], { esversion: 11 } + ); + + TestRun(test, "arguments") + .addError(1, 10, "Expected an assignment or function call and instead saw an expression.") + .addError(2, 14, "Expected an assignment or function call and instead saw an expression.") + .addError(3, 20, "Expected an assignment or function call and instead saw an expression.") + .addError(4, 15, "Expected an assignment or function call and instead saw an expression.") + .test([ + "true.x?.();", + "true.x?.(true);", + "true.x?.(true, true);", + "true.x?.(...[]);" + ], { esversion: 11 } + ); + + TestRun(test, "new") + .addError(1, 7, "Unexpected '?.'.") + .test( + "new {}?.constructor();", + { esversion: 11 } + ); + + TestRun(test, "template invocation - literal property name") + .addError(1, 15, "Expected an assignment or function call and instead saw an expression.") + .addError(1, 15, "Unexpected '`'.") + .test( + "true?.toString``;", + { esversion: 11 } + ); + + TestRun(test, "template invocation - dynamic property name") + .addError(1, 15, "Expected an assignment or function call and instead saw an expression.") + .addError(1, 15, "Unexpected '`'.") + .test( + "true?.[void 0]``;", + { esversion: 11 } + ); + + TestRun(test, "ternary") + .addError(1, 8, "A leading decimal point can be confused with a dot: '.1'.") + .addError(1, 11, "Expected an assignment or function call and instead saw an expression.") + .test( + "true?.1 : null;", + { esversion: 11 } + ); + + test.done(); +};