Fail spec parsing lambdas on parameter missing a =

Fail speculative parsing of arrow function expressions whenever it has a
parameter with an initialiser that is missing '='. Ordinarily this is
allowed for better error recovery in the language service, but for
speculative parsing, the errors can compound. When the initialiser is an
error, and when the '=>' is missing (which is also allowed), what is
putatively an arrow function may actually be something else.

For example, `(a / 8) + function ()
{ }` is currently parsed as if someone had intended to write

`(a = /8)+function()/) => { }` but they forgot the `=` of the
initialiser, the `=>` of the lambda, forgot to close the regular
expression, and mistakenly inserted a newline right after the regular
expression.
This commit is contained in:
Nathan Shively-Sanders
2017-09-12 14:01:05 -07:00
parent 403f585622
commit 4c4316da72
7 changed files with 84 additions and 20 deletions

View File

@@ -2240,7 +2240,7 @@ namespace ts {
isStartOfType(/*inStartOfParameter*/ true);
}
function parseParameter(): ParameterDeclaration {
function parseParameter(requireEqualsToken?: boolean): ParameterDeclaration {
const node = <ParameterDeclaration>createNode(SyntaxKind.Parameter);
if (token() === SyntaxKind.ThisKeyword) {
node.name = createIdentifier(/*isIdentifier*/ true);
@@ -2269,19 +2269,11 @@ namespace ts {
node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken);
node.type = parseParameterType();
node.initializer = parseBindingElementInitializer(/*inParameter*/ true);
node.initializer = parseInitializer(/*inParameter*/ true, requireEqualsToken);
return addJSDocComment(finishNode(node));
}
function parseBindingElementInitializer(inParameter: boolean) {
return inParameter ? parseParameterInitializer() : parseNonParameterInitializer();
}
function parseParameterInitializer() {
return parseInitializer(/*inParameter*/ true);
}
function fillSignature(
returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken,
flags: SignatureFlags,
@@ -2334,7 +2326,8 @@ namespace ts {
setYieldContext(!!(flags & SignatureFlags.Yield));
setAwaitContext(!!(flags & SignatureFlags.Await));
const result = parseDelimitedList(ParsingContext.Parameters, flags & SignatureFlags.JSDoc ? parseJSDocParameter : parseParameter);
const result = parseDelimitedList(ParsingContext.Parameters,
flags & SignatureFlags.JSDoc ? parseJSDocParameter : () => parseParameter(!!(flags & SignatureFlags.RequireCompleteParameterList)));
setYieldContext(savedYieldContext);
setAwaitContext(savedAwaitContext);
@@ -3017,7 +3010,7 @@ namespace ts {
return expr;
}
function parseInitializer(inParameter: boolean): Expression {
function parseInitializer(inParameter: boolean, requireEqualsToken?: boolean): Expression {
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
@@ -3032,11 +3025,18 @@ namespace ts {
// do not try to parse initializer
return undefined;
}
if (inParameter && requireEqualsToken) {
// this occurs with speculative parsing of lambdas, so try to consume the initializer,
// but signal that the parameter was missing the equals sign so it can abort if it wants
parseAssignmentExpressionOrHigher();
const result = createNode(SyntaxKind.Identifier, scanner.getStartPos()) as Identifier;
result.escapedText = "= not found" as __String;
return result;
}
}
// Initializer[In, Yield] :
// = AssignmentExpression[?In, ?Yield]
parseExpected(SyntaxKind.EqualsToken);
return parseAssignmentExpressionOrHigher();
}
@@ -3351,8 +3351,7 @@ namespace ts {
function tryParseAsyncSimpleArrowFunctionExpression(): ArrowFunction | undefined {
// We do a check here so that we won't be doing unnecessarily call to "lookAhead"
if (token() === SyntaxKind.AsyncKeyword) {
const isUnParenthesizedAsyncArrowFunction = lookAhead(isUnParenthesizedAsyncArrowFunctionWorker);
if (isUnParenthesizedAsyncArrowFunction === Tristate.True) {
if (lookAhead(isUnParenthesizedAsyncArrowFunctionWorker) === Tristate.True) {
const asyncModifier = parseModifiersForArrowFunction();
const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0);
return parseSimpleArrowFunctionExpression(<Identifier>expr, asyncModifier);
@@ -3386,7 +3385,6 @@ namespace ts {
const node = <ArrowFunction>createNode(SyntaxKind.ArrowFunction);
node.modifiers = parseModifiersForArrowFunction();
const isAsync = hasModifier(node, ModifierFlags.Async) ? SignatureFlags.Await : SignatureFlags.None;
// Arrow functions are never generators.
//
// If we're speculatively parsing a signature for a parenthesized arrow function, then
@@ -3409,7 +3407,8 @@ 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) {
if (!allowAmbiguity && ((token() !== SyntaxKind.EqualsGreaterThanToken && token() !== SyntaxKind.OpenBraceToken) ||
find(node.parameters, p => p.initializer && ts.isIdentifier(p.initializer) && p.initializer.escapedText === "= not found"))) {
// Returning undefined here will cause our caller to rewind to where we started from.
return undefined;
}
@@ -5158,7 +5157,7 @@ namespace ts {
const node = <BindingElement>createNode(SyntaxKind.BindingElement);
node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken);
node.name = parseIdentifierOrPattern();
node.initializer = parseBindingElementInitializer(/*inParameter*/ false);
node.initializer = parseInitializer(/*inParameter*/ false);
return finishNode(node);
}
@@ -5175,7 +5174,7 @@ namespace ts {
node.propertyName = propertyName;
node.name = parseIdentifierOrPattern();
}
node.initializer = parseBindingElementInitializer(/*inParameter*/ false);
node.initializer = parseInitializer(/*inParameter*/ false);
return finishNode(node);
}
@@ -5214,7 +5213,7 @@ namespace ts {
node.name = parseIdentifierOrPattern();
node.type = parseTypeAnnotation();
if (!isInOrOfKeyword(token())) {
node.initializer = parseInitializer(/*inParameter*/ false);
node.initializer = parseNonParameterInitializer();
}
return finishNode(node);
}

View File

@@ -0,0 +1,15 @@
=== tests/cases/conformance/parser/ecmascript5/ArrowFunctionExpressions/parserArrowFunctionExpression5.ts ===
function foo(q: string, b: number) {
>foo : Symbol(foo, Decl(parserArrowFunctionExpression5.ts, 0, 0))
>q : Symbol(q, Decl(parserArrowFunctionExpression5.ts, 0, 13))
>b : Symbol(b, Decl(parserArrowFunctionExpression5.ts, 0, 23))
return true ? (q ? true : false) : (b = q.length, function() { });
>q : Symbol(q, Decl(parserArrowFunctionExpression5.ts, 0, 13))
>b : Symbol(b, Decl(parserArrowFunctionExpression5.ts, 0, 23))
>q.length : Symbol(String.length, Decl(lib.d.ts, --, --))
>q : Symbol(q, Decl(parserArrowFunctionExpression5.ts, 0, 13))
>length : Symbol(String.length, Decl(lib.d.ts, --, --))
};

View File

@@ -0,0 +1,25 @@
=== tests/cases/conformance/parser/ecmascript5/ArrowFunctionExpressions/parserArrowFunctionExpression5.ts ===
function foo(q: string, b: number) {
>foo : (q: string, b: number) => boolean | (() => void)
>q : string
>b : number
return true ? (q ? true : false) : (b = q.length, function() { });
>true ? (q ? true : false) : (b = q.length, function() { }) : boolean | (() => void)
>true : true
>(q ? true : false) : boolean
>q ? true : false : boolean
>q : string
>true : true
>false : false
>(b = q.length, function() { }) : () => void
>b = q.length, function() { } : () => void
>b = q.length : number
>b : number
>q.length : number
>q : string
>length : number
>function() { } : () => void
};

View File

@@ -0,0 +1,12 @@
tests/cases/conformance/parser/ecmascript5/RegularExpressions/parserRegularExpressionDivideAmbiguity7.ts(1,2): error TS2304: Cannot find name 'a'.
tests/cases/conformance/parser/ecmascript5/RegularExpressions/parserRegularExpressionDivideAmbiguity7.ts(2,3): error TS1005: ';' expected.
==== tests/cases/conformance/parser/ecmascript5/RegularExpressions/parserRegularExpressionDivideAmbiguity7.ts (2 errors) ====
(a/8
~
!!! error TS2304: Cannot find name 'a'.
){}
~
!!! error TS1005: ';' expected.

View File

@@ -0,0 +1,8 @@
//// [parserRegularExpressionDivideAmbiguity7.ts]
(a/8
){}
//// [parserRegularExpressionDivideAmbiguity7.js]
(a / 8);
{ }

View File

@@ -0,0 +1,3 @@
function foo(q: string, b: number) {
return true ? (q ? true : false) : (b = q.length, function() { });
};