Break out of speculative parsing on bad parameter initializer (#19158)

* Break out of speculative parsing on bad parameter initializer

* Remove uses of 'finally'

* give up -> stop

* Do without exceptions

* Remove `resetAfterSpeculation`

* Use Fail and FailList objects

* Remove `inSpeculation` parameter to parseDelimitedList

* Don't use `createNodeArray`, it's not always in scope

* Move Fail and FailList inside initializeState

* More code review

* More code review
This commit is contained in:
Andy 2017-11-13 09:18:36 -08:00 committed by GitHub
parent 120fb6fc95
commit e7df83263d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 176 additions and 50 deletions

View file

@ -531,6 +531,18 @@ namespace ts {
let TokenConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node;
let IdentifierConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node;
let SourceFileConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node;
interface Fail extends Node { kind: SyntaxKind.Unknown; }
interface FailList extends NodeArray<Node> { pos: -1; }
let Fail: Fail;
let FailList: FailList;
function isFail(x: Node | undefined): x is Fail {
Debug.assert(Fail !== undefined);
return x === Fail;
}
function isFailList(x: NodeArray<Node> | undefined): x is FailList {
Debug.assert(Fail !== undefined);
return x === FailList;
}
// tslint:enable variable-name
let sourceFile: SourceFile;
@ -681,6 +693,9 @@ namespace ts {
IdentifierConstructor = objectAllocator.getIdentifierConstructor();
SourceFileConstructor = objectAllocator.getSourceFileConstructor();
Fail = createNode(SyntaxKind.Unknown) as Fail;
FailList = createNodeArray([], -1) as FailList;
sourceText = _sourceText;
syntaxCursor = _syntaxCursor;
@ -736,7 +751,7 @@ namespace ts {
processReferenceComments(sourceFile);
sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
Debug.assert(token() === SyntaxKind.EndOfFileToken);
Debug.assertEqual(token(), SyntaxKind.EndOfFileToken);
sourceFile.endOfFileToken = addJSDocComment(parseTokenNode() as EndOfFileToken);
setExternalModuleIndicator(sourceFile);
@ -1003,7 +1018,7 @@ namespace ts {
return currentToken = scanner.scanJsxAttributeValue();
}
function speculationHelper<T>(callback: () => T, isLookAhead: boolean): T {
function speculationHelper<T>(callback: () => T, isLookAhead: boolean): T | undefined {
// Keep track of the state we'll need to rollback to if lookahead fails (or if the
// caller asked us to always reset our state).
const saveToken = currentToken;
@ -1015,6 +1030,7 @@ namespace ts {
// descent nature of our parser. However, we still store this here just so we can
// assert that invariant holds.
const saveContextFlags = contextFlags;
const saveParsingContext = parsingContext;
// If we're only looking ahead, then tell the scanner to only lookahead as well.
// Otherwise, if we're actually speculatively parsing, then tell the scanner to do the
@ -1023,7 +1039,8 @@ namespace ts {
? scanner.lookAhead(callback)
: scanner.tryScan(callback);
Debug.assert(saveContextFlags === contextFlags);
Debug.assertEqual(saveContextFlags, contextFlags);
Debug.assertEqual(saveParsingContext, parsingContext);
// If our callback returned something 'falsy' or we're just looking ahead,
// then unconditionally restore us to where we were.
@ -1577,7 +1594,7 @@ namespace ts {
return createNodeArray(list, listPos);
}
function parseListElement<T extends Node>(parsingContext: ParsingContext, parseElement: () => T): T {
function parseListElement<T extends Node | Fail>(parsingContext: ParsingContext, parseElement: () => T): T {
const node = currentNode(parsingContext);
if (node) {
return <T>consumeNode(node);
@ -1901,17 +1918,24 @@ namespace ts {
}
// Parses a comma-delimited list of elements
function parseDelimitedList<T extends Node>(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): NodeArray<T> {
function parseDelimitedList<T extends Node>(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): NodeArray<T>;
function parseDelimitedList<T extends Node>(kind: ParsingContext, parseElement: () => T | Fail, considerSemicolonAsDelimiter?: boolean): NodeArray<T> | FailList;
function parseDelimitedList<T extends Node>(kind: ParsingContext, parseElement: () => T | Fail, considerSemicolonAsDelimiter?: boolean): NodeArray<T> | FailList {
const saveParsingContext = parsingContext;
parsingContext |= 1 << kind;
const list = [];
const list: T[] = [];
const listPos = getNodePos();
let commaStart = -1; // Meaning the previous token was not a comma
while (true) {
if (isListElement(kind, /*inErrorRecovery*/ false)) {
const startPos = scanner.getStartPos();
list.push(parseListElement(kind, parseElement));
const elem = parseListElement(kind, parseElement);
if (isFail(elem)) {
parsingContext = saveParsingContext;
return FailList;
}
list.push(elem);
commaStart = scanner.getTokenPos();
if (parseOptional(SyntaxKind.CommaToken)) {
@ -2271,7 +2295,13 @@ namespace ts {
isStartOfType(/*inStartOfParameter*/ true);
}
function parseParameter(requireEqualsToken?: boolean): ParameterDeclaration {
function tryParseParameter(): ParameterDeclaration | Fail {
return parseParameterWorker(/*inSpeculation*/ true);
}
function parseParameter(): ParameterDeclaration {
return parseParameterWorker(/*inSpeculation*/ false) as ParameterDeclaration;
}
function parseParameterWorker(inSpeculation: boolean): ParameterDeclaration | Fail {
const node = <ParameterDeclaration>createNode(SyntaxKind.Parameter);
if (token() === SyntaxKind.ThisKeyword) {
node.name = createIdentifier(/*isIdentifier*/ true);
@ -2285,7 +2315,11 @@ namespace ts {
// FormalParameter [Yield,Await]:
// BindingElement[?Yield,?Await]
node.name = parseIdentifierOrPattern();
const name = parseIdentifierOrPattern(inSpeculation);
if (isFail(name)) {
return Fail;
}
node.name = name;
if (getFullWidth(node.name) === 0 && !hasModifiers(node) && isModifierKind(token())) {
// in cases like
// 'use strict'
@ -2300,20 +2334,27 @@ namespace ts {
node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken);
node.type = parseParameterType();
node.initializer = parseInitializer(/*inParameter*/ true, requireEqualsToken);
const initializer = parseInitializer(/*inParameter*/ true, inSpeculation);
if (isFail(initializer)) {
return Fail;
}
node.initializer = initializer;
return addJSDocComment(finishNode(node));
}
function fillSignature(
returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken,
flags: SignatureFlags,
signature: SignatureDeclaration): void {
/** @return 'true' on success. */
function fillSignature(returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, flags: SignatureFlags, signature: SignatureDeclaration, inSpeculation?: boolean): boolean {
if (!(flags & SignatureFlags.JSDoc)) {
signature.typeParameters = parseTypeParameters();
}
signature.parameters = parseParameterList(flags);
const parameters = parseParameterList(flags, inSpeculation);
if (isFailList(parameters)) {
return false;
}
signature.parameters = parameters;
signature.type = parseReturnType(returnToken, !!(flags & SignatureFlags.Type));
return true;
}
function parseReturnType(returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, isType: boolean): TypeNode | undefined {
@ -2336,7 +2377,7 @@ namespace ts {
return false;
}
function parseParameterList(flags: SignatureFlags) {
function parseParameterList(flags: SignatureFlags, inSpeculation: boolean): NodeArray<ParameterDeclaration> | FailList {
// FormalParameters [Yield,Await]: (modified)
// [empty]
// FormalParameterList[?Yield,Await]
@ -2357,9 +2398,9 @@ namespace ts {
setYieldContext(!!(flags & SignatureFlags.Yield));
setAwaitContext(!!(flags & SignatureFlags.Await));
const result = parseDelimitedList(ParsingContext.Parameters,
flags & SignatureFlags.JSDoc ? parseJSDocParameter : () => parseParameter(!!(flags & SignatureFlags.RequireCompleteParameterList)));
const result = parseDelimitedList<ParameterDeclaration>(
ParsingContext.Parameters,
flags & SignatureFlags.JSDoc ? parseJSDocParameter : inSpeculation ? tryParseParameter : parseParameter);
setYieldContext(savedYieldContext);
setAwaitContext(savedAwaitContext);
@ -3032,14 +3073,16 @@ namespace ts {
while ((operatorToken = parseOptionalToken(SyntaxKind.CommaToken))) {
expr = makeBinaryExpression(expr, operatorToken, parseAssignmentExpressionOrHigher());
}
if (saveDecoratorContext) {
setDecoratorContext(/*val*/ true);
}
return expr;
}
function parseInitializer(inParameter: boolean, requireEqualsToken?: boolean): Expression {
function parseInitializer(inParameter: boolean): Expression | undefined;
function parseInitializer(inParameter: boolean, inSpeculation?: boolean): Expression | Fail | undefined;
function parseInitializer(inParameter: boolean, inSpeculation?: boolean): Expression | Fail | undefined {
if (token() !== SyntaxKind.EqualsToken) {
// It's not uncommon during typing for the user to miss writing the '=' token. Check if
// there is no newline after the last token and if we're on an expression. If so, parse
@ -3054,12 +3097,8 @@ namespace ts {
// do not try to parse initializer
return undefined;
}
if (inParameter && requireEqualsToken) {
// = is required when speculatively parsing arrow function parameters,
// so return a fake initializer as a signal that the equals token was missing
const result = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics._0_expected, "=") as Identifier;
result.escapedText = "= not found" as __String;
return result;
if (inSpeculation) {
return Fail;
}
}
@ -3225,7 +3264,7 @@ namespace ts {
// it out, but don't allow any ambiguity, and return 'undefined' if this could be an
// expression instead.
const arrowFunction = triState === Tristate.True
? parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ true)
? parseParenthesizedArrowFunctionExpressionHead(/*inSpeculation*/ false)
: tryParse(parsePossibleParenthesizedArrowFunctionExpressionHead);
if (!arrowFunction) {
@ -3373,7 +3412,7 @@ namespace ts {
}
function parsePossibleParenthesizedArrowFunctionExpressionHead(): ArrowFunction {
return parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ false);
return parseParenthesizedArrowFunctionExpressionHead(/*inSpeculation*/ true);
}
function tryParseAsyncSimpleArrowFunctionExpression(): ArrowFunction | undefined {
@ -3409,7 +3448,7 @@ namespace ts {
return Tristate.False;
}
function parseParenthesizedArrowFunctionExpressionHead(allowAmbiguity: boolean): ArrowFunction {
function parseParenthesizedArrowFunctionExpressionHead(inSpeculation: boolean): ArrowFunction | undefined {
const node = <ArrowFunction>createNode(SyntaxKind.ArrowFunction);
node.modifiers = parseModifiersForArrowFunction();
const isAsync = hasModifier(node, ModifierFlags.Async) ? SignatureFlags.Await : SignatureFlags.None;
@ -3420,7 +3459,10 @@ namespace ts {
// a => (b => c)
// And think that "(b =>" was actually a parenthesized arrow function with a missing
// close paren.
fillSignature(SyntaxKind.ColonToken, isAsync | (allowAmbiguity ? SignatureFlags.None : SignatureFlags.RequireCompleteParameterList), node);
if (!fillSignature(SyntaxKind.ColonToken, isAsync | (inSpeculation ? SignatureFlags.RequireCompleteParameterList : SignatureFlags.None), node, inSpeculation)) {
return undefined;
}
// If we couldn't get parameters, we definitely could not parse out an arrow function.
if (!node.parameters) {
@ -3435,8 +3477,7 @@ namespace ts {
// - "a ? (b): c" will have "(b):" parsed as a signature with a return type annotation.
//
// So we need just a bit of lookahead to ensure that it can only be a signature.
if (!allowAmbiguity && ((token() !== SyntaxKind.EqualsGreaterThanToken && token() !== SyntaxKind.OpenBraceToken) ||
find(node.parameters, p => p.initializer && ts.isIdentifier(p.initializer) && p.initializer.escapedText === "= not found"))) {
if (inSpeculation && token() !== SyntaxKind.EqualsGreaterThanToken && token() !== SyntaxKind.OpenBraceToken) {
// Returning undefined here will cause our caller to rewind to where we started from.
return undefined;
}
@ -4574,7 +4615,6 @@ namespace ts {
if (saveDecoratorContext) {
setDecoratorContext(/*val*/ false);
}
const node = <FunctionExpression>createNode(SyntaxKind.FunctionExpression);
node.modifiers = parseModifiers();
parseExpected(SyntaxKind.FunctionKeyword);
@ -4590,7 +4630,6 @@ namespace ts {
fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node);
node.body = parseFunctionBlock(isGenerator | isAsync);
if (saveDecoratorContext) {
setDecoratorContext(/*val*/ true);
}
@ -4653,7 +4692,6 @@ namespace ts {
}
const block = parseBlock(!!(flags & SignatureFlags.IgnoreMissingOpenBrace), diagnosticMessage);
if (saveDecoratorContext) {
setDecoratorContext(/*val*/ true);
}
@ -5227,18 +5265,38 @@ namespace ts {
// DECLARATIONS
function tryParseArrayBindingElement(): ArrayBindingElement | Fail {
return parseArrayBindingElementWorker(/*inSpeculation*/ true);
}
function parseArrayBindingElement(): ArrayBindingElement {
return parseArrayBindingElementWorker(/*inSpeculation*/ false) as ArrayBindingElement;
}
function parseArrayBindingElementWorker(inSpeculation: boolean): ArrayBindingElement | Fail {
if (token() === SyntaxKind.CommaToken) {
return <OmittedExpression>createNode(SyntaxKind.OmittedExpression);
}
const node = <BindingElement>createNode(SyntaxKind.BindingElement);
node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken);
node.name = parseIdentifierOrPattern();
node.initializer = parseInitializer(/*inParameter*/ false);
const name = parseIdentifierOrPattern(inSpeculation);
if (isFail(name)) {
return Fail;
}
node.name = name;
const init = parseInitializer(/*inParameter*/ false, inSpeculation);
if (isFail(init)) {
return Fail;
}
node.initializer = init;
return finishNode(node);
}
function tryParseObjectBindingElement(): BindingElement | Fail {
return parseObjectBindingElementWorker(/*inSpeculation*/ true);
}
function parseObjectBindingElement(): BindingElement {
return parseObjectBindingElementWorker(/*inSpeculation*/ false) as BindingElement;
}
function parseObjectBindingElementWorker(inSpeculation: boolean): BindingElement | Fail {
const node = <BindingElement>createNode(SyntaxKind.BindingElement);
node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken);
const tokenIsIdentifier = isIdentifier();
@ -5249,24 +5307,46 @@ namespace ts {
else {
parseExpected(SyntaxKind.ColonToken);
node.propertyName = propertyName;
node.name = parseIdentifierOrPattern();
const name = parseIdentifierOrPattern(inSpeculation);
if (isFail(name)) {
return Fail;
}
node.name = name;
}
node.initializer = parseInitializer(/*inParameter*/ false);
const init = parseInitializer(/*inParameter*/ false, inSpeculation);
if (isFail(init)) {
return Fail;
}
node.initializer = init;
return finishNode(node);
}
function parseObjectBindingPattern(): ObjectBindingPattern {
function parseObjectBindingPattern(inSpeculation: boolean): ObjectBindingPattern | Fail {
const node = <ObjectBindingPattern>createNode(SyntaxKind.ObjectBindingPattern);
parseExpected(SyntaxKind.OpenBraceToken);
node.elements = parseDelimitedList(ParsingContext.ObjectBindingElements, parseObjectBindingElement);
const elements = parseDelimitedList<BindingElement>(
ParsingContext.ObjectBindingElements,
inSpeculation ? tryParseObjectBindingElement : parseObjectBindingElement,
/*considerSemicolonAsDelimiter*/ undefined);
if (isFailList(elements)) {
return Fail;
}
node.elements = elements;
parseExpected(SyntaxKind.CloseBraceToken);
return finishNode(node);
}
function parseArrayBindingPattern(): ArrayBindingPattern {
function parseArrayBindingPattern(inSpeculation: boolean): ArrayBindingPattern | Fail {
const node = <ArrayBindingPattern>createNode(SyntaxKind.ArrayBindingPattern);
parseExpected(SyntaxKind.OpenBracketToken);
node.elements = parseDelimitedList(ParsingContext.ArrayBindingElements, parseArrayBindingElement);
const elements = parseDelimitedList<BindingElement | OmittedExpression>(
ParsingContext.ArrayBindingElements,
inSpeculation ? tryParseArrayBindingElement : parseArrayBindingElement,
/*considerSemicolonAsDelimiter*/ undefined);
if (isFailList(elements)) {
return Fail;
}
node.elements = elements;
parseExpected(SyntaxKind.CloseBracketToken);
return finishNode(node);
}
@ -5275,12 +5355,14 @@ namespace ts {
return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.OpenBracketToken || isIdentifier();
}
function parseIdentifierOrPattern(): Identifier | BindingPattern {
function parseIdentifierOrPattern(): Identifier | BindingPattern;
function parseIdentifierOrPattern(inSpeculation: boolean): Identifier | BindingPattern | Fail;
function parseIdentifierOrPattern(inSpeculation?: boolean): Identifier | BindingPattern | Fail {
if (token() === SyntaxKind.OpenBracketToken) {
return parseArrayBindingPattern();
return parseArrayBindingPattern(inSpeculation);
}
if (token() === SyntaxKind.OpenBraceToken) {
return parseObjectBindingPattern();
return parseObjectBindingPattern(inSpeculation);
}
return parseIdentifier();
}
@ -5328,9 +5410,7 @@ namespace ts {
else {
const savedDisallowIn = inDisallowInContext();
setDisallowInContext(inForStatementInitializer);
node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations, parseVariableDeclaration);
setDisallowInContext(savedDisallowIn);
}
@ -5428,7 +5508,7 @@ namespace ts {
}
}
function parseNonParameterInitializer() {
function parseNonParameterInitializer(): Expression | undefined {
return parseInitializer(/*inParameter*/ false);
}

View file

@ -0,0 +1,16 @@
//// [parserArrowFunctionExpression7.ts]
({
async m() {
for (;;) {
}
}
});
//// [parserArrowFunctionExpression7.js]
({
async m() {
for (;;) {
}
}
});

View file

@ -0,0 +1,10 @@
=== tests/cases/conformance/parser/ecmascript5/ArrowFunctionExpressions/parserArrowFunctionExpression7.ts ===
({
async m() {
>m : Symbol(m, Decl(parserArrowFunctionExpression7.ts, 0, 2))
for (;;) {
}
}
});

View file

@ -0,0 +1,13 @@
=== tests/cases/conformance/parser/ecmascript5/ArrowFunctionExpressions/parserArrowFunctionExpression7.ts ===
({
>({ async m() { for (;;) { } }}) : { m(): Promise<void>; }
>{ async m() { for (;;) { } }} : { m(): Promise<void>; }
async m() {
>m : () => Promise<void>
for (;;) {
}
}
});

View file

@ -0,0 +1,7 @@
// @target: esnext
({
async m() {
for (;;) {
}
}
});