Skip to content

Commit

Permalink
[[FEAT]] Add support for optional chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
jugglinmike authored and rwaldron committed Feb 15, 2021
1 parent 1aeeaac commit 59028b0
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 48 deletions.
35 changes: 33 additions & 2 deletions src/jshint.js
Expand Up @@ -2746,7 +2746,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);
}
Expand Down Expand Up @@ -3047,6 +3049,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");
Expand Down Expand Up @@ -3264,7 +3291,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)"));
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/lex.js
Expand Up @@ -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: >>>=
Expand Down
4 changes: 2 additions & 2 deletions src/options.js
Expand Up @@ -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
};
Expand Down
46 changes: 3 additions & 43 deletions tests/test262/expectations.txt
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
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)
24 changes: 24 additions & 0 deletions tests/unit/options.js
Expand Up @@ -3695,6 +3695,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];",
Expand Down
85 changes: 85 additions & 0 deletions tests/unit/parser.js
Expand Up @@ -10398,3 +10398,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();
};

0 comments on commit 59028b0

Please sign in to comment.