Skip to content

Commit

Permalink
Add support for Optional Chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
ljqx authored and ariya committed Apr 28, 2021
1 parent 70c0159 commit 0911ad8
Show file tree
Hide file tree
Showing 139 changed files with 2,268 additions and 114 deletions.
1 change: 1 addition & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Messages = {
InvalidLHSInForLoop: 'Invalid left-hand side in for-loop',
InvalidModuleSpecifier: 'Unexpected token',
InvalidRegExp: 'Invalid regular expression',
InvalidTaggedTemplateOnOptionalChain: 'Invalid tagged template on optional chain',
InvalidUnicodeEscapeSequence: 'Invalid Unicode escape sequence',
LetInLexicalBinding: 'let is disallowed as a lexically bound name',
MissingFromClause: 'Unexpected token',
Expand Down
24 changes: 20 additions & 4 deletions src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ export type ArrayExpressionElement = Expression | SpreadElement | null;
export type ArrayPatternElement = AssignmentPattern | BindingIdentifier | BindingPattern | RestElement | null;
export type BindingPattern = ArrayPattern | ObjectPattern;
export type BindingIdentifier = Identifier;
export type ChainElement = CallExpression | ComputedMemberExpression | StaticMemberExpression;
export type Declaration = AsyncFunctionDeclaration | ClassDeclaration | ExportDeclaration | FunctionDeclaration | ImportDeclaration | VariableDeclaration;
export type ExportableDefaultDeclaration = BindingIdentifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
export type ExportableNamedDeclaration = AsyncFunctionDeclaration | ClassDeclaration | FunctionDeclaration | VariableDeclaration;
export type ExportDeclaration = ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration;
export type Expression = ArrayExpression | ArrowFunctionExpression | AssignmentExpression | AsyncArrowFunctionExpression | AsyncFunctionExpression |
AwaitExpression | BinaryExpression | CallExpression | ClassExpression | ComputedMemberExpression |
AwaitExpression | BinaryExpression | CallExpression | ChainExpression | ClassExpression | ComputedMemberExpression |
ConditionalExpression | Identifier | FunctionExpression | Literal | NewExpression | ObjectExpression |
RegexLiteral | SequenceExpression | StaticMemberExpression | TaggedTemplateExpression |
ThisExpression | UnaryExpression | UpdateExpression | YieldExpression;
Expand Down Expand Up @@ -189,10 +190,12 @@ export class CallExpression {
readonly type: string;
readonly callee: Expression | Import;
readonly arguments: ArgumentListElement[];
constructor(callee: Expression | Import, args: ArgumentListElement[]) {
readonly optional: boolean;
constructor(callee: Expression | Import, args: ArgumentListElement[], optional: boolean) {
this.type = Syntax.CallExpression;
this.callee = callee;
this.arguments = args;
this.optional = optional;
}
}

Expand All @@ -207,6 +210,15 @@ export class CatchClause {
}
}

export class ChainExpression {
readonly type: string;
readonly expression: ChainElement;
constructor(expression: ChainElement) {
this.type = Syntax.ChainExpression;
this.expression = expression;
}
}

export class ClassBody {
readonly type: string;
readonly body: Property[];
Expand Down Expand Up @@ -247,11 +259,13 @@ export class ComputedMemberExpression {
readonly computed: boolean;
readonly object: Expression;
readonly property: Expression;
constructor(object: Expression, property: Expression) {
readonly optional: boolean;
constructor(object: Expression, property: Expression, optional: boolean) {
this.type = Syntax.MemberExpression;
this.computed = true;
this.object = object;
this.property = property;
this.optional = optional;
}
}

Expand Down Expand Up @@ -690,11 +704,13 @@ export class StaticMemberExpression {
readonly computed: boolean;
readonly object: Expression;
readonly property: Expression;
constructor(object: Expression, property: Expression) {
readonly optional: boolean;
constructor(object: Expression, property: Expression, optional: boolean) {
this.type = Syntax.MemberExpression;
this.computed = false;
this.object = object;
this.property = property;
this.optional = optional;
}
}

Expand Down
79 changes: 60 additions & 19 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,23 +1304,24 @@ export class Parser {
expr = this.inheritCoverGrammar(this.matchKeyword('new') ? this.parseNewExpression : this.parsePrimaryExpression);
}

let hasOptional = false;
while (true) {
if (this.match('.')) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = true;
this.expect('.');
const property = this.parseIdentifierName();
expr = this.finalize(this.startNode(startToken), new Node.StaticMemberExpression(expr, property));
let optional = false;
if (this.match('?.')) {
optional = true;
hasOptional = true;
this.expect('?.');
}

} else if (this.match('(')) {
if (this.match('(')) {
const asyncArrow = maybeAsync && (startToken.lineNumber === this.lookahead.lineNumber);
this.context.isBindingElement = false;
this.context.isAssignmentTarget = false;
const args = asyncArrow ? this.parseAsyncArguments() : this.parseArguments();
if (expr.type === Syntax.Import && args.length !== 1) {
this.tolerateError(Messages.BadImportCallArity);
}
expr = this.finalize(this.startNode(startToken), new Node.CallExpression(expr, args));
expr = this.finalize(this.startNode(startToken), new Node.CallExpression(expr, args, optional));
if (asyncArrow && this.match('=>')) {
for (let i = 0; i < args.length; ++i) {
this.reinterpretExpressionAsPattern(args[i]);
Expand All @@ -1333,21 +1334,41 @@ export class Parser {
}
} else if (this.match('[')) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = true;
this.context.isAssignmentTarget = !optional;
this.expect('[');
const property = this.isolateCoverGrammar(this.parseExpression);
this.expect(']');
expr = this.finalize(this.startNode(startToken), new Node.ComputedMemberExpression(expr, property));
expr = this.finalize(this.startNode(startToken), new Node.ComputedMemberExpression(expr, property, optional));

} else if (this.lookahead.type === Token.Template && this.lookahead.head) {
// Optional template literal is not included in the spec.
// https://github.com/tc39/proposal-optional-chaining/issues/54
if (optional) {
this.throwUnexpectedToken(this.lookahead);
}
if (hasOptional) {
this.throwError(Messages.InvalidTaggedTemplateOnOptionalChain);
}
const quasi = this.parseTemplateLiteral({ isTagged: true });
expr = this.finalize(this.startNode(startToken), new Node.TaggedTemplateExpression(expr, quasi));

} else if (this.match('.') || optional) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = !optional;
if (!optional) {
this.expect('.');
}
const property = this.parseIdentifierName();
expr = this.finalize(this.startNode(startToken), new Node.StaticMemberExpression(expr, property, optional));

} else {
break;
}
}
this.context.allowIn = previousAllowIn;
if (hasOptional) {
return new Node.ChainExpression(expr);
}

return expr;
}
Expand All @@ -1370,30 +1391,50 @@ export class Parser {
let expr = (this.matchKeyword('super') && this.context.inFunctionBody) ? this.parseSuper() :
this.inheritCoverGrammar(this.matchKeyword('new') ? this.parseNewExpression : this.parsePrimaryExpression);

let hasOptional = false;
while (true) {
let optional = false;
if (this.match('?.')) {
optional = true;
hasOptional = true;
this.expect('?.');
}
if (this.match('[')) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = true;
this.context.isAssignmentTarget = !optional;
this.expect('[');
const property = this.isolateCoverGrammar(this.parseExpression);
this.expect(']');
expr = this.finalize(node, new Node.ComputedMemberExpression(expr, property));

} else if (this.match('.')) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = true;
this.expect('.');
const property = this.parseIdentifierName();
expr = this.finalize(node, new Node.StaticMemberExpression(expr, property));
expr = this.finalize(node, new Node.ComputedMemberExpression(expr, property, optional));

} else if (this.lookahead.type === Token.Template && this.lookahead.head) {
// Optional template literal is not included in the spec.
// https://github.com/tc39/proposal-optional-chaining/issues/54
if (optional) {
this.throwUnexpectedToken(this.lookahead);
}
if (hasOptional) {
this.throwError(Messages.InvalidTaggedTemplateOnOptionalChain);
}
const quasi = this.parseTemplateLiteral({ isTagged: true });
expr = this.finalize(node, new Node.TaggedTemplateExpression(expr, quasi));

} else if (this.match('.') || optional) {
this.context.isBindingElement = false;
this.context.isAssignmentTarget = !optional;
if (!optional) {
this.expect('.');
}
const property = this.parseIdentifierName();
expr = this.finalize(node, new Node.StaticMemberExpression(expr, property, optional));

} else {
break;
}
}
if (hasOptional) {
return new Node.ChainExpression(expr);
}

return expr;
}
Expand Down
16 changes: 15 additions & 1 deletion src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,20 @@ export class Scanner {
++this.index;
this.curlyStack.pop();
break;

case '?':
++this.index;
if (this.source[this.index] === '?') {
++this.index;
str = '??';
} if (this.source[this.index] === '.' && !/^\d$/.test(this.source[this.index + 1])) {
// "?." in "foo?.3:0" should not be treated as optional chaining.
// See https://github.com/tc39/proposal-optional-chaining#notes
++this.index;
str = '?.';
}
break;

case ')':
case ';':
case ',':
Expand Down Expand Up @@ -647,7 +661,7 @@ export class Scanner {

// 1-character punctuators.
str = this.source[this.index];
if ('<>=!+-*%&|?^/'.indexOf(str) >= 0) {
if ('<>=!+-*%&|^/'.indexOf(str) >= 0) {
++this.index;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const Syntax = {
BreakStatement: 'BreakStatement',
CallExpression: 'CallExpression',
CatchClause: 'CatchClause',
ChainExpression: 'ChainExpression',
ClassBody: 'ClassBody',
ClassDeclaration: 'ClassDeclaration',
ClassExpression: 'ClassExpression',
Expand Down
2 changes: 1 addition & 1 deletion test/3rdparty/syntax/angular-1.2.5.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/backbone-1.1.0.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/jquery-1.9.1.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/jquery.mobile-1.4.2.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/mootools-1.4.5.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/underscore-1.5.2.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/3rdparty/syntax/yui-3.12.0.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions test/api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('esprima.Syntax', function () {
BreakStatement: 'BreakStatement',
CallExpression: 'CallExpression',
CatchClause: 'CatchClause',
ChainExpression: 'ChainExpression',
ClassBody: 'ClassBody',
ClassDeclaration: 'ClassDeclaration',
ClassExpression: 'ClassExpression',
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/ES6/arrow-function/migrated_0018.tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
}
}
],
"optional": false,
"range": [
0,
13
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/ES6/arrow-function/migrated_0019.tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
}
}
],
"optional": false,
"range": [
0,
17
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@
"type": "Literal",
"value": 0,
"raw": "0"
}
},
"optional": false
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@
"type": "Literal",
"value": 0,
"raw": "0"
}
},
"optional": false
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@
"type": "Literal",
"value": 0,
"raw": "0"
}
},
"optional": false
}
},
"kind": "init",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@
"type": "Identifier",
"name": "some_call"
},
"arguments": []
"arguments": [],
"optional": false
},
"property": {
"range": [
Expand All @@ -477,7 +478,8 @@
},
"type": "Identifier",
"name": "a"
}
},
"optional": false
},
"kind": "init",
"method": false,
Expand Down Expand Up @@ -569,7 +571,8 @@
},
"type": "Identifier",
"name": "a"
}
},
"optional": false
},
"kind": "init",
"method": false,
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/ES6/lexical-declaration/let_member.tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@
},
"type": "Identifier",
"name": "let"
}
},
"optional": false
},
"right": {
"range": [
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/ES6/meta-property/new-target-invoke.tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@
"name": "target"
}
},
"arguments": []
"arguments": [],
"optional": false
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@
},
"arguments": []
},
"arguments": []
"arguments": [],
"optional": false
}
}
]
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/ES6/spread-element/call-multi-spread.tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@
"name": "z"
}
}
]
],
"optional": false
}
}
],
Expand Down
Loading

0 comments on commit 0911ad8

Please sign in to comment.