Skip to content

Commit

Permalink
fix(parser): implemented optional chaining
Browse files Browse the repository at this point in the history
This implementation should now match the official specs.

I also added a bunch of new tests.

@aladdin-add Could you follow up this with ESTree, so we can merge this PR soon as the specs are settled.
  • Loading branch information
KFlash committed Aug 2, 2019
1 parent c8532d9 commit cc334f3
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 48 deletions.
6 changes: 4 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export const enum Errors {
FinallyWithoutTry,
UnCorrespondingFragmentTag,
InvalidCoalescing,
InvalidChaining
InvalidChaining,
InvalidTaggedTemplateChain
}

/*@internal*/
Expand Down Expand Up @@ -354,7 +355,8 @@ export const errorMessages: {
[Errors.UnCorrespondingFragmentTag]: 'Expected corresponding closing tag for JSX fragment',
[Errors.InvalidCoalescing]:
'Coalescing and logical operators used together in the same expression must be disambiguated with parentheses',
[Errors.InvalidChaining]: 'Constructors in/after an Optional Chain are not allowed'
[Errors.InvalidChaining]: 'Constructors in/after an Optional Chain are not allowed',
[Errors.InvalidTaggedTemplateChain]: 'Tagged Template Literals are not allowed in optionalChain'
};

export class ParseError extends SyntaxError {
Expand Down
43 changes: 22 additions & 21 deletions src/lexer/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,27 +215,6 @@ export function scanSingleToken(parser: ParserState, context: Context, state: Le
advanceChar(parser);
return token;

case Token.QuestionMark: {
let ch = advanceChar(parser);
if ((context & Context.OptionsNext) < 1) return Token.QuestionMark;
if (ch === Chars.QuestionMark) {
advanceChar(parser);
return Token.Coalesce;
} else if (ch === Chars.Period) {
const index = parser.index + 1;
// Check that it's not followed by any numbers
if (index < parser.end) {
ch = parser.source.charCodeAt(index);
if ((CharTypes[ch] & CharFlags.Decimal) < 1) {
advanceChar(parser);
return Token.OptionalChaining;
}
}
}

return Token.QuestionMark;
}

// `<`, `<=`, `<<`, `<<=`, `</`, `<!--`
case Token.LessThan:
let ch = advanceChar(parser);
Expand Down Expand Up @@ -492,6 +471,28 @@ export function scanSingleToken(parser: ParserState, context: Context, state: Le
}
return Token.Period;

// `?`, `??`, `?.`
case Token.QuestionMark: {
let ch = advanceChar(parser);
if ((context & Context.OptionsNext) < 1) return Token.QuestionMark;
if (ch === Chars.QuestionMark) {
advanceChar(parser);
return Token.Coalesce;
} else if (ch === Chars.Period) {
const index = parser.index + 1;
// Check that it's not followed by any numbers
if (index < parser.end) {
ch = parser.source.charCodeAt(index);
if ((CharTypes[ch] & CharFlags.Decimal) < 1) {
advanceChar(parser);
return Token.QuestionMarkPeriod;
}
}
}

return Token.QuestionMark;
}

// Look for identifier or keyword
case Token.Keyword:
return scanIdentifier(parser, context, /* isValidAsKeyword */ 1);
Expand Down
48 changes: 24 additions & 24 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3699,28 +3699,6 @@ export function parseMemberOrUpdateExpression(
break;
}

case Token.OptionalChaining: {
nextToken(parser, context); // skips: '?.'

if ((parser.token & Token.IsMemberOrCallExpression) === Token.IsMemberOrCallExpression) {
expr = parseMemberOrUpdateExpression(parser, context, expr, 0, start, line, column, 1);
} else {
parser.assignable = AssignmentKind.Assignable;

const property = parsePropertyOrPrivatePropertyName(parser, context);

expr = finishNode(parser, context, start, line, column, {
type: 'OptionalMemberExpression',
object: expr,
computed: false,
optional: true,
property
} as any);
}

break;
}

/* Call */
case Token.LeftParen: {
const args = parseArguments(parser, context, inGroup);
Expand Down Expand Up @@ -3749,10 +3727,32 @@ export function parseMemberOrUpdateExpression(
break;
}

/* Optional chaining */
case Token.QuestionMarkPeriod: {
nextToken(parser, context); // skips: '?.'

if ((parser.token & Token.IsMemberOrCallExpression) === Token.IsMemberOrCallExpression) {
expr = parseMemberOrUpdateExpression(parser, context, expr, 0, start, line, column, 1);
} else {
const property = parseIdentifier(parser, context, 0);
if (parser.token === Token.TemplateSpan) report(parser, Errors.InvalidTaggedTemplateChain);
expr = finishNode(parser, context, start, line, column, {
type: 'OptionalMemberExpression',
object: expr,
computed: false,
optional: true,
property
} as any);
}

parser.assignable = AssignmentKind.CannotAssign;

break;
}

/* Template */
default: {
parser.assignable = AssignmentKind.CannotAssign;

expr = finishNode(parser, context, parser.tokenPos, parser.linePos, parser.colPos, {
type: 'TaggedTemplateExpression',
tag: expr,
Expand Down Expand Up @@ -7204,7 +7204,7 @@ export function parseNewExpression(
colPos
);

if (parser.token === Token.OptionalChaining) report(parser, Errors.InvalidChaining);
if (parser.token === Token.QuestionMarkPeriod) report(parser, Errors.InvalidChaining);

// NewExpression without arguments.
const callee = parseMembeExpressionNoCall(parser, context, expr, inGroup, tokenPos, linePos, colPos);
Expand Down
2 changes: 1 addition & 1 deletion src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export const enum Token {

// Stage #3 proposals
Coalesce = 123 | IsBinaryOp | IsCoalesc | 1 << PrecStart, // ??,
OptionalChaining = 125 | IsMemberOrCallExpression, // ?.,
QuestionMarkPeriod = 125 | IsMemberOrCallExpression, // ?.,

}

Expand Down
77 changes: 77 additions & 0 deletions test/parser/next/optional-chaining.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { parseSource } from '../../../src/parser';
describe('Next - Optional chaining', () => {
for (const arg of [
'func?.()',
'obj?.prop ',
'func?.(...args)',
'a?.[x]',
'a?.()',
'a?.[++x]',
'a?.b.c(++x).d',
'a?.b[3].c?.(x).d',
'(a?.b).c',
'delete a?.b',
'func?.(a, b)',
'a?.func?.()',
'a?.func?.(a, b)',
Expand All @@ -30,9 +39,60 @@ describe('Next - Optional chaining', () => {
} else if (a?.b.c?.d?.e.f) {
console.log(a?.b.c?.d?.e.f);
}`,

'true?.valueOf()',
'0?.valueOf()',
'({})?.constructor',
'[]?.length',
'undefined?.["valueOf"]()',
'1?.["valueOf"]()',
'() => 0?.()',
'() => 1?.()',
'() => "hi"?.()',
'() => ({})?.a["b"]',
'() => (() => {})?.()()',
'(() => {})?.()?.()',
'null?.()().a["b"]',
'delete undefined?.()',
'delete null?.()',
'undefined?.(...a)',
'null?.(1, ...a)',
'({}).a?.(...a)',
'({ a: null }).a?.(...a)',
'undefined?.(...a)?.(1, ...a)',
'() => 5?.(...[])',
'delete o1?.x',
'o2.x?.["y"];',
'a?.[foo(a)]',
'a ?? a.b ?? (a?.b).c();',
'a ?? foo.bar?.baz ?? a.c',
'a ?? aobj?.[expr]?.[other] ?? foo.bar?.baz',
'a?.b[3].c?.(x).d ?? aobj?.[expr]?.[other] ?? foo.bar?.baz',
'const x = a?.b.c',
'(null)?.b === null',
'let a = b?.c',
'a?.b',
'!a ? a : a.b',
'foo(null?.x)',
'let a = b?? "default";',
'let a = b.c ?? "default";',
'let xn = x?.normalize("NFC")',
'a?.b === undefined',
'null?.foo === null',
'a?.b?.c?.d === undefined',
'o3?.a === o4?.a === undefined',
'o3?.a?.b === o4?.a?.b === undefined',
'o3?.a?.b?.c === o4?.a?.b?.c === undefined',
'x in (o3?.a)',
'obj.func?.[arg].property;',
'obj.func?.[arg.property];',
'obj.func?.[arg];',
'a?.trim()?.indexOf("hello")',
'foo?.x?.y?.z?()=>{foo}:bar;',
`if (a?.b?.c === 'foobar') {}
if (a?.b()?.c) {}
if (a?.b?.()?.c) {}`,
'new new class {}().constructor();',
`a?.b(...args);`,
`a?.b(...args).c;`,
`a?.b(...args).c(...args);`
Expand All @@ -50,12 +110,29 @@ describe('Next - Optional chaining', () => {
}

fail('Expressions - Optional chaining (fail)', [
['foo?.x?.y?.z?()=>foo;', Context.OptionsNext],
['const a = { b(){ return super?.c; } }', Context.OptionsNext],
['class A{ b(){ return super?.b; } }', Context.OptionsWebCompat],
['new a?.();', Context.OptionsNext | Context.Module | Context.Strict],
['new C?.b.d()', Context.OptionsNext | Context.OptionsWebCompat],
['a.?b.?()', Context.OptionsNext | Context.OptionsWebCompat],
['a.?()', Context.OptionsNext | Context.OptionsWebCompat],
['a?.b = c', Context.OptionsNext | Context.OptionsWebCompat],
['a?.[b] = c', Context.OptionsNext | Context.OptionsWebCompat],
['a?.{a} = c', Context.OptionsNext | Context.OptionsWebCompat],
['a?.(a) = c', Context.OptionsNext | Context.OptionsWebCompat],
['o3?.a in ()', Context.OptionsNext | Context.OptionsWebCompat],
['class C {} class D extends C { foo() { return super?.bar; } }', Context.OptionsNext | Context.OptionsWebCompat],
['class C {} class D extends C { foo() { return super?.["bar"]; }', Context.OptionsNext | Context.OptionsWebCompat],
['class C {} class D extends C { constructor() { super?.(); } }', Context.OptionsNext | Context.OptionsWebCompat],
['const o = { C: class {} }; new o?.C();', Context.OptionsNext | Context.OptionsWebCompat],
['const o = { C: class {} }; new o?.["C"]();', Context.OptionsNext | Context.OptionsWebCompat],
['class C {} new C?.();', Context.OptionsNext | Context.OptionsWebCompat],
['function tag() {} tag?', Context.OptionsNext | Context.OptionsWebCompat],
['const o = { tag() {} }; o?.tag``', Context.OptionsNext | Context.OptionsWebCompat],
['import?.("foo")', Context.OptionsNext | Context.OptionsWebCompat],
['new new class {}()?.constructor?.();', Context.OptionsNext | Context.OptionsWebCompat],
['a?.{a} = c', Context.None],
['a.?()', Context.None]
]);

Expand Down

0 comments on commit cc334f3

Please sign in to comment.