Introduce scope tracking in the parser (#9493)

* Introduce scope tracking

* Fix tests

* Add new tests

* Remove constructor-super check from transform as it is now in parser

* Correctly handle class properties and class scope

* Fix duplicate name check

* Convert scope identifier storage to array

* Enter a new scope in typescript module blocks

* Add test for duplicate declaration

* Rename error for duplicate exports

* Treat class declarations as lexical declaration

* Update whitelist

* Add tests

* Fix scope tracking for function declarations

* Migrate try-catch duplicate error

* Fix test

* More tests

* One more test

* Make scope a separate class and fix review comments

* Do not allow new.target in top scope arrow function

* Correctly enter new scope for declare module and treat type aliases as lexical declarations

* Tests for typescript scope tracking to not mark type aliases as duplicate

* Fix flow scope tracking

* Remove ident from test names as redundant

* Add test case for var and function

* Improve error messages

* Improve literal regex
This commit is contained in:
Daniel Tschinder
2019-02-25 11:04:52 -08:00
committed by GitHub
parent 918f149a63
commit a7391144b3
284 changed files with 5904 additions and 1842 deletions

View File

@@ -29,10 +29,23 @@ import {
} from "../util/identifier";
import type { Pos, Position } from "../util/location";
import * as charCodes from "charcodes";
import {
BIND_OUTSIDE,
BIND_VAR,
functionFlags,
SCOPE_ARROW,
SCOPE_CLASS,
SCOPE_DIRECT_SUPER,
SCOPE_SUPER,
SCOPE_PROGRAM,
} from "../util/scopeflags";
export default class ExpressionParser extends LValParser {
// Forward-declaration: defined in statement.js
+parseBlock: (allowDirectives?: boolean) => N.BlockStatement;
+parseBlock: (
allowDirectives?: boolean,
createNewLexicalScope?: boolean,
) => N.BlockStatement;
+parseClass: (
node: N.Class,
isStatement: boolean,
@@ -41,10 +54,9 @@ export default class ExpressionParser extends LValParser {
+parseDecorators: (allowExport?: boolean) => void;
+parseFunction: <T: N.NormalFunction>(
node: T,
isStatement: boolean,
statement?: number,
allowExpressionBody?: boolean,
isAsync?: boolean,
optionalId?: boolean,
) => T;
+parseFunctionParams: (node: N.Function, allowModifiers?: boolean) => void;
+takeDecorators: (node: N.HasDecorators) => void;
@@ -74,6 +86,7 @@ export default class ExpressionParser extends LValParser {
// Convenience method to parse an Expression only
getExpression(): N.Expression {
this.scope.enter(SCOPE_PROGRAM);
this.nextToken();
const expr = this.parseExpression();
if (!this.match(tt.eof)) {
@@ -128,7 +141,7 @@ export default class ExpressionParser extends LValParser {
const startPos = this.state.start;
const startLoc = this.state.startLoc;
if (this.isContextual("yield")) {
if (this.state.inGenerator) {
if (this.scope.inGenerator) {
let left = this.parseYield(noIn);
if (afterLeftParse) {
left = afterLeftParse.call(this, left, startPos, startLoc);
@@ -336,7 +349,7 @@ export default class ExpressionParser extends LValParser {
if (
this.match(tt.name) &&
this.state.value === "await" &&
this.state.inAsync
this.scope.inAsync
) {
throw this.raise(
this.state.start,
@@ -421,8 +434,8 @@ export default class ExpressionParser extends LValParser {
parseMaybeUnary(refShorthandDefaultPos: ?Pos): N.Expression {
if (
this.isContextual("await") &&
(this.state.inAsync ||
(!this.state.inFunction && this.options.allowAwaitOutsideFunction))
(this.scope.inAsync ||
(!this.scope.inFunction && this.options.allowAwaitOutsideFunction))
) {
return this.parseAwait();
} else if (this.state.type.prefix) {
@@ -806,11 +819,7 @@ export default class ExpressionParser extends LValParser {
switch (this.state.type) {
case tt._super:
if (
!this.state.inMethod &&
!this.state.inClassProperty &&
!this.options.allowSuperOutsideMethod
) {
if (!this.scope.allowSuper && !this.options.allowSuperOutsideMethod) {
this.raise(
this.state.start,
"super is only allowed in object methods and classes",
@@ -819,6 +828,18 @@ export default class ExpressionParser extends LValParser {
node = this.startNode();
this.next();
if (
this.match(tt.parenL) &&
!this.scope.allowDirectSuper &&
!this.options.allowSuperOutsideMethod
) {
this.raise(
node.start,
"super() is only valid inside a class constructor of a subclass. " +
"Maybe a typo in the method name ('constructor') or not extending another class?",
);
}
if (
!this.match(tt.parenL) &&
!this.match(tt.bracketL) &&
@@ -826,17 +847,7 @@ export default class ExpressionParser extends LValParser {
) {
this.unexpected();
}
if (
this.match(tt.parenL) &&
this.state.inMethod !== "constructor" &&
!this.options.allowSuperOutsideMethod
) {
this.raise(
node.start,
"super() is only valid inside a class constructor. " +
"Make sure the method name is spelled exactly as 'constructor'.",
);
}
return this.finishNode(node, "Super");
case tt._import:
@@ -870,20 +881,17 @@ export default class ExpressionParser extends LValParser {
!this.canInsertSemicolon()
) {
this.next();
return this.parseFunction(node, false, false, true);
return this.parseFunction(node, undefined, false, true);
} else if (
canBeArrow &&
id.name === "async" &&
this.match(tt.name) &&
!this.canInsertSemicolon()
) {
const oldInAsync = this.state.inAsync;
this.state.inAsync = true;
const params = [this.parseIdentifier()];
this.expect(tt.arrow);
// let foo = async bar => {};
this.parseArrowExpression(node, params, true);
this.state.inAsync = oldInAsync;
return node;
}
@@ -900,12 +908,9 @@ export default class ExpressionParser extends LValParser {
this.expectPlugin("doExpressions");
const node = this.startNode();
this.next();
const oldInFunction = this.state.inFunction;
const oldLabels = this.state.labels;
this.state.labels = [];
this.state.inFunction = false;
node.body = this.parseBlock(false);
this.state.inFunction = oldInFunction;
node.body = this.parseBlock();
this.state.labels = oldLabels;
return this.finishNode(node, "DoExpression");
}
@@ -1068,10 +1073,10 @@ export default class ExpressionParser extends LValParser {
this.next();
meta = this.createIdentifier(meta, "function");
if (this.state.inGenerator && this.eat(tt.dot)) {
if (this.scope.inGenerator && this.eat(tt.dot)) {
return this.parseMetaProperty(node, meta, "sent");
}
return this.parseFunction(node, false);
return this.parseFunction(node);
}
parseMetaProperty(
@@ -1309,7 +1314,7 @@ export default class ExpressionParser extends LValParser {
if (this.eat(tt.dot)) {
const metaProp = this.parseMetaProperty(node, meta, "target");
if (!this.state.inFunction && !this.state.inClassProperty) {
if (!this.scope.inNonArrowFunction && !this.state.inClassProperty) {
let error = "new.target can only be used in functions";
if (this.hasPlugin("classProperties")) {
@@ -1575,6 +1580,7 @@ export default class ExpressionParser extends LValParser {
isGenerator,
isAsync,
/* isConstructor */ false,
false,
"ObjectMethod",
);
}
@@ -1588,6 +1594,7 @@ export default class ExpressionParser extends LValParser {
/* isGenerator */ false,
/* isAsync */ false,
/* isConstructor */ false,
false,
"ObjectMethod",
);
this.checkGetterSetterParams(prop);
@@ -1713,32 +1720,28 @@ export default class ExpressionParser extends LValParser {
isGenerator: boolean,
isAsync: boolean,
isConstructor: boolean,
allowDirectSuper: boolean,
type: string,
inClassScope: boolean = false,
): T {
const oldInFunc = this.state.inFunction;
const oldInMethod = this.state.inMethod;
const oldInAsync = this.state.inAsync;
const oldInGenerator = this.state.inGenerator;
const oldYieldPos = this.state.yieldPos;
const oldAwaitPos = this.state.awaitPos;
this.state.inFunction = true;
this.state.inMethod = node.kind || true;
this.state.inAsync = isAsync;
this.state.inGenerator = isGenerator;
this.state.yieldPos = 0;
this.state.awaitPos = 0;
this.initFunction(node, isAsync);
node.generator = !!isGenerator;
const allowModifiers = isConstructor; // For TypeScript parameter properties
this.scope.enter(
functionFlags(isAsync, node.generator) |
SCOPE_SUPER |
(inClassScope ? SCOPE_CLASS : 0) |
(allowDirectSuper ? SCOPE_DIRECT_SUPER : 0),
);
this.parseFunctionParams((node: any), allowModifiers);
this.checkYieldAwaitInDefaultParams();
this.parseFunctionBodyAndFinish(node, type);
this.state.inFunction = oldInFunc;
this.state.inMethod = oldInMethod;
this.state.inAsync = oldInAsync;
this.state.inGenerator = oldInGenerator;
this.state.yieldPos = oldYieldPos;
this.state.awaitPos = oldAwaitPos;
@@ -1753,17 +1756,12 @@ export default class ExpressionParser extends LValParser {
params: ?(N.Expression[]),
isAsync: boolean,
): N.ArrowFunctionExpression {
this.scope.enter(functionFlags(isAsync, false) | SCOPE_ARROW);
this.initFunction(node, isAsync);
const oldInFunc = this.state.inFunction;
const oldInAsync = this.state.inAsync;
const oldInGenerator = this.state.inGenerator;
const oldMaybeInArrowParameters = this.state.maybeInArrowParameters;
const oldYieldPos = this.state.yieldPos;
const oldAwaitPos = this.state.awaitPos;
this.state.inFunction = true;
this.state.inAsync = isAsync;
this.state.inGenerator = false;
this.state.maybeInArrowParameters = false;
this.state.yieldPos = 0;
this.state.awaitPos = 0;
@@ -1771,9 +1769,6 @@ export default class ExpressionParser extends LValParser {
if (params) this.setArrowFunctionParameters(node, params);
this.parseFunctionBody(node, true);
this.state.inAsync = oldInAsync;
this.state.inGenerator = oldInGenerator;
this.state.inFunction = oldInFunc;
this.state.maybeInArrowParameters = oldMaybeInArrowParameters;
this.state.yieldPos = oldYieldPos;
this.state.awaitPos = oldAwaitPos;
@@ -1819,57 +1814,89 @@ export default class ExpressionParser extends LValParser {
// Parse function body and check parameters.
parseFunctionBody(node: N.Function, allowExpression: ?boolean): void {
const isExpression = allowExpression && !this.match(tt.braceL);
const oldStrict = this.state.strict;
let useStrict = false;
const oldInParameters = this.state.inParameters;
this.state.inParameters = false;
if (isExpression) {
node.body = this.parseMaybeAssign();
this.checkParams(node, false, allowExpression);
} else {
const nonSimple = !this.isSimpleParamList(node.params);
if (!oldStrict || nonSimple) {
useStrict = this.strictDirective(this.state.end);
// If this is a strict mode function, verify that argument names
// are not repeated, and it does not try to bind the words `eval`
// or `arguments`.
if (useStrict && nonSimple) {
// This logic is here to align the error location with the estree plugin
const errorPos =
// $FlowIgnore
(node.kind === "method" || node.kind === "constructor") &&
// $FlowIgnore
!!node.key
? node.key.end
: node.start;
this.raise(
errorPos,
"Illegal 'use strict' directive in function with non-simple parameter list",
);
}
}
// Start a new scope with regard to labels
// flag (restore them to their old value afterwards).
const oldInFunc = this.state.inFunction;
const oldLabels = this.state.labels;
this.state.inFunction = true;
this.state.labels = [];
node.body = this.parseBlock(true);
this.state.inFunction = oldInFunc;
if (useStrict) this.state.strict = true;
// Add the params to varDeclaredNames to ensure that an error is thrown
// if a let/const declaration in the function clashes with one of the params.
this.checkParams(
node,
!oldStrict && !useStrict && !allowExpression && !nonSimple,
allowExpression,
);
node.body = this.parseBlock(true, false);
this.state.labels = oldLabels;
}
this.scope.exit();
this.checkFunctionNameAndParams(node, allowExpression);
this.state.inParameters = oldInParameters;
}
checkFunctionNameAndParams(
node: N.Function,
isArrowFunction: ?boolean,
): void {
// If this is a strict mode function, verify that argument names
// are not repeated, and it does not try to bind the words `eval`
// or `arguments`.
const isStrict = this.isStrictBody(node);
// Also check for arrow functions
const checkLVal = this.state.strict || isStrict || isArrowFunction;
const oldStrict = this.state.strict;
if (isStrict) this.state.strict = isStrict;
if (checkLVal) {
const nameHash: any = Object.create(null);
if (node.id) {
this.checkLVal(node.id, true, undefined, "function name");
}
for (const param of node.params) {
if (isStrict && param.type !== "Identifier") {
this.raise(param.start, "Non-simple parameter in strict mode");
}
this.checkLVal(param, true, nameHash, "function parameter list");
}
// Ensure the function name isn't a forbidden identifier in strict mode, e.g. 'eval'
if (this.state.strict && node.id) {
this.checkLVal(node.id, BIND_OUTSIDE, undefined, "function name");
}
this.state.strict = oldStrict;
}
isSimpleParamList(
params: $ReadOnlyArray<N.Pattern | N.TSParameterProperty>,
): boolean {
for (let i = 0, len = params.length; i < len; i++) {
if (params[i].type !== "Identifier") return false;
}
return true;
}
checkParams(
node: N.Function,
allowDuplicates: boolean,
// eslint-disable-next-line no-unused-vars
isArrowFunction: ?boolean,
): void {
// $FlowIssue
const nameHash: {} = Object.create(null);
for (let i = 0; i < node.params.length; i++) {
this.checkLVal(
node.params[i],
BIND_VAR,
allowDuplicates ? null : nameHash,
"function paramter list",
);
}
}
// Parses a comma-separated list of expressions, and returns them as
// an array. `close` is the token type that ends the list, and
// `allowEmpty` can be turned on to allow subsequent commas with
@@ -1987,22 +2014,21 @@ export default class ExpressionParser extends LValParser {
checkKeywords: boolean,
isBinding: boolean,
): void {
const state = this.state;
if (state.inGenerator && word === "yield") {
if (this.scope.inGenerator && word === "yield") {
this.raise(
startLoc,
"Can not use 'yield' as identifier inside a generator",
);
}
if (state.inAsync && word === "await") {
if (this.scope.inAsync && word === "await") {
this.raise(
startLoc,
"Can not use 'await' as identifier inside an async function",
);
}
if (state.inClassProperty && word === "arguments") {
if (this.state.inClassProperty && word === "arguments") {
this.raise(
startLoc,
"'arguments' is not allowed in class field initializer",
@@ -2012,14 +2038,14 @@ export default class ExpressionParser extends LValParser {
this.raise(startLoc, `Unexpected keyword '${word}'`);
}
const reservedTest = !state.strict
const reservedTest = !this.state.strict
? isReservedWord
: isBinding
? isStrictBindReservedWord
: isStrictReservedWord;
if (reservedTest(word, this.inModule)) {
if (!state.inAsync && word === "await") {
if (!this.scope.inAsync && word === "await") {
this.raise(
startLoc,
"Can not use keyword 'await' outside an async function",