Skip to content

Commit 0911ad8

Browse files
ljqxariya
authored andcommitted
Add support for Optional Chaining
1 parent 70c0159 commit 0911ad8

File tree

139 files changed

+2268
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+2268
-114
lines changed

src/messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const Messages = {
2727
InvalidLHSInForLoop: 'Invalid left-hand side in for-loop',
2828
InvalidModuleSpecifier: 'Unexpected token',
2929
InvalidRegExp: 'Invalid regular expression',
30+
InvalidTaggedTemplateOnOptionalChain: 'Invalid tagged template on optional chain',
3031
InvalidUnicodeEscapeSequence: 'Invalid Unicode escape sequence',
3132
LetInLexicalBinding: 'let is disallowed as a lexically bound name',
3233
MissingFromClause: 'Unexpected token',

src/nodes.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ export type ArrayExpressionElement = Expression | SpreadElement | null;
55
export type ArrayPatternElement = AssignmentPattern | BindingIdentifier | BindingPattern | RestElement | null;
66
export type BindingPattern = ArrayPattern | ObjectPattern;
77
export type BindingIdentifier = Identifier;
8+
export type ChainElement = CallExpression | ComputedMemberExpression | StaticMemberExpression;
89
export type Declaration = AsyncFunctionDeclaration | ClassDeclaration | ExportDeclaration | FunctionDeclaration | ImportDeclaration | VariableDeclaration;
910
export type ExportableDefaultDeclaration = BindingIdentifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;
1011
export type ExportableNamedDeclaration = AsyncFunctionDeclaration | ClassDeclaration | FunctionDeclaration | VariableDeclaration;
1112
export type ExportDeclaration = ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration;
1213
export type Expression = ArrayExpression | ArrowFunctionExpression | AssignmentExpression | AsyncArrowFunctionExpression | AsyncFunctionExpression |
13-
AwaitExpression | BinaryExpression | CallExpression | ClassExpression | ComputedMemberExpression |
14+
AwaitExpression | BinaryExpression | CallExpression | ChainExpression | ClassExpression | ComputedMemberExpression |
1415
ConditionalExpression | Identifier | FunctionExpression | Literal | NewExpression | ObjectExpression |
1516
RegexLiteral | SequenceExpression | StaticMemberExpression | TaggedTemplateExpression |
1617
ThisExpression | UnaryExpression | UpdateExpression | YieldExpression;
@@ -189,10 +190,12 @@ export class CallExpression {
189190
readonly type: string;
190191
readonly callee: Expression | Import;
191192
readonly arguments: ArgumentListElement[];
192-
constructor(callee: Expression | Import, args: ArgumentListElement[]) {
193+
readonly optional: boolean;
194+
constructor(callee: Expression | Import, args: ArgumentListElement[], optional: boolean) {
193195
this.type = Syntax.CallExpression;
194196
this.callee = callee;
195197
this.arguments = args;
198+
this.optional = optional;
196199
}
197200
}
198201

@@ -207,6 +210,15 @@ export class CatchClause {
207210
}
208211
}
209212

213+
export class ChainExpression {
214+
readonly type: string;
215+
readonly expression: ChainElement;
216+
constructor(expression: ChainElement) {
217+
this.type = Syntax.ChainExpression;
218+
this.expression = expression;
219+
}
220+
}
221+
210222
export class ClassBody {
211223
readonly type: string;
212224
readonly body: Property[];
@@ -247,11 +259,13 @@ export class ComputedMemberExpression {
247259
readonly computed: boolean;
248260
readonly object: Expression;
249261
readonly property: Expression;
250-
constructor(object: Expression, property: Expression) {
262+
readonly optional: boolean;
263+
constructor(object: Expression, property: Expression, optional: boolean) {
251264
this.type = Syntax.MemberExpression;
252265
this.computed = true;
253266
this.object = object;
254267
this.property = property;
268+
this.optional = optional;
255269
}
256270
}
257271

@@ -690,11 +704,13 @@ export class StaticMemberExpression {
690704
readonly computed: boolean;
691705
readonly object: Expression;
692706
readonly property: Expression;
693-
constructor(object: Expression, property: Expression) {
707+
readonly optional: boolean;
708+
constructor(object: Expression, property: Expression, optional: boolean) {
694709
this.type = Syntax.MemberExpression;
695710
this.computed = false;
696711
this.object = object;
697712
this.property = property;
713+
this.optional = optional;
698714
}
699715
}
700716

src/parser.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,23 +1304,24 @@ export class Parser {
13041304
expr = this.inheritCoverGrammar(this.matchKeyword('new') ? this.parseNewExpression : this.parsePrimaryExpression);
13051305
}
13061306

1307+
let hasOptional = false;
13071308
while (true) {
1308-
if (this.match('.')) {
1309-
this.context.isBindingElement = false;
1310-
this.context.isAssignmentTarget = true;
1311-
this.expect('.');
1312-
const property = this.parseIdentifierName();
1313-
expr = this.finalize(this.startNode(startToken), new Node.StaticMemberExpression(expr, property));
1309+
let optional = false;
1310+
if (this.match('?.')) {
1311+
optional = true;
1312+
hasOptional = true;
1313+
this.expect('?.');
1314+
}
13141315

1315-
} else if (this.match('(')) {
1316+
if (this.match('(')) {
13161317
const asyncArrow = maybeAsync && (startToken.lineNumber === this.lookahead.lineNumber);
13171318
this.context.isBindingElement = false;
13181319
this.context.isAssignmentTarget = false;
13191320
const args = asyncArrow ? this.parseAsyncArguments() : this.parseArguments();
13201321
if (expr.type === Syntax.Import && args.length !== 1) {
13211322
this.tolerateError(Messages.BadImportCallArity);
13221323
}
1323-
expr = this.finalize(this.startNode(startToken), new Node.CallExpression(expr, args));
1324+
expr = this.finalize(this.startNode(startToken), new Node.CallExpression(expr, args, optional));
13241325
if (asyncArrow && this.match('=>')) {
13251326
for (let i = 0; i < args.length; ++i) {
13261327
this.reinterpretExpressionAsPattern(args[i]);
@@ -1333,21 +1334,41 @@ export class Parser {
13331334
}
13341335
} else if (this.match('[')) {
13351336
this.context.isBindingElement = false;
1336-
this.context.isAssignmentTarget = true;
1337+
this.context.isAssignmentTarget = !optional;
13371338
this.expect('[');
13381339
const property = this.isolateCoverGrammar(this.parseExpression);
13391340
this.expect(']');
1340-
expr = this.finalize(this.startNode(startToken), new Node.ComputedMemberExpression(expr, property));
1341+
expr = this.finalize(this.startNode(startToken), new Node.ComputedMemberExpression(expr, property, optional));
13411342

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

1355+
} else if (this.match('.') || optional) {
1356+
this.context.isBindingElement = false;
1357+
this.context.isAssignmentTarget = !optional;
1358+
if (!optional) {
1359+
this.expect('.');
1360+
}
1361+
const property = this.parseIdentifierName();
1362+
expr = this.finalize(this.startNode(startToken), new Node.StaticMemberExpression(expr, property, optional));
1363+
13461364
} else {
13471365
break;
13481366
}
13491367
}
13501368
this.context.allowIn = previousAllowIn;
1369+
if (hasOptional) {
1370+
return new Node.ChainExpression(expr);
1371+
}
13511372

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

1394+
let hasOptional = false;
13731395
while (true) {
1396+
let optional = false;
1397+
if (this.match('?.')) {
1398+
optional = true;
1399+
hasOptional = true;
1400+
this.expect('?.');
1401+
}
13741402
if (this.match('[')) {
13751403
this.context.isBindingElement = false;
1376-
this.context.isAssignmentTarget = true;
1404+
this.context.isAssignmentTarget = !optional;
13771405
this.expect('[');
13781406
const property = this.isolateCoverGrammar(this.parseExpression);
13791407
this.expect(']');
1380-
expr = this.finalize(node, new Node.ComputedMemberExpression(expr, property));
1381-
1382-
} else if (this.match('.')) {
1383-
this.context.isBindingElement = false;
1384-
this.context.isAssignmentTarget = true;
1385-
this.expect('.');
1386-
const property = this.parseIdentifierName();
1387-
expr = this.finalize(node, new Node.StaticMemberExpression(expr, property));
1408+
expr = this.finalize(node, new Node.ComputedMemberExpression(expr, property, optional));
13881409

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

1422+
} else if (this.match('.') || optional) {
1423+
this.context.isBindingElement = false;
1424+
this.context.isAssignmentTarget = !optional;
1425+
if (!optional) {
1426+
this.expect('.');
1427+
}
1428+
const property = this.parseIdentifierName();
1429+
expr = this.finalize(node, new Node.StaticMemberExpression(expr, property, optional));
1430+
13931431
} else {
13941432
break;
13951433
}
13961434
}
1435+
if (hasOptional) {
1436+
return new Node.ChainExpression(expr);
1437+
}
13971438

13981439
return expr;
13991440
}

src/scanner.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,20 @@ export class Scanner {
608608
++this.index;
609609
this.curlyStack.pop();
610610
break;
611+
612+
case '?':
613+
++this.index;
614+
if (this.source[this.index] === '?') {
615+
++this.index;
616+
str = '??';
617+
} if (this.source[this.index] === '.' && !/^\d$/.test(this.source[this.index + 1])) {
618+
// "?." in "foo?.3:0" should not be treated as optional chaining.
619+
// See https://github.com/tc39/proposal-optional-chaining#notes
620+
++this.index;
621+
str = '?.';
622+
}
623+
break;
624+
611625
case ')':
612626
case ';':
613627
case ',':
@@ -647,7 +661,7 @@ export class Scanner {
647661

648662
// 1-character punctuators.
649663
str = this.source[this.index];
650-
if ('<>=!+-*%&|?^/'.indexOf(str) >= 0) {
664+
if ('<>=!+-*%&|^/'.indexOf(str) >= 0) {
651665
++this.index;
652666
}
653667
}

src/syntax.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const Syntax = {
1010
BreakStatement: 'BreakStatement',
1111
CallExpression: 'CallExpression',
1212
CatchClause: 'CatchClause',
13+
ChainExpression: 'ChainExpression',
1314
ClassBody: 'ClassBody',
1415
ClassDeclaration: 'ClassDeclaration',
1516
ClassExpression: 'ClassExpression',

test/3rdparty/syntax/angular-1.2.5.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

test/3rdparty/syntax/backbone-1.1.0.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

test/3rdparty/syntax/jquery-1.9.1.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

test/3rdparty/syntax/jquery.mobile-1.4.2.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

test/3rdparty/syntax/mootools-1.4.5.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)