diff --git a/lib/Parser.js b/lib/Parser.js index 554fa667aa9..cc53cce5794 100644 --- a/lib/Parser.js +++ b/lib/Parser.js @@ -13,7 +13,6 @@ const util = require("util"); const vm = require("vm"); const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); const StackedSetMap = require("./util/StackedSetMap"); -const TrackingSet = require("./util/TrackingSet"); const acornParser = acorn.Parser.extend(acornDynamicImport); @@ -34,8 +33,6 @@ const defaultParserOptions = { // regexp to match at lease one "magic comment" const webpackCommentRegExp = new RegExp(/(^|\W)webpack[A-Z]{1,}[A-Za-z]{1,}:/); -const EMPTY_ARRAY = []; - const EMPTY_COMMENT_OPTIONS = { options: null, errors: null @@ -857,6 +854,14 @@ class Parser extends Tapable { } } + // Block-Prewalking iterates the scope for block variable declarations + blockPrewalkStatements(statements) { + for (let index = 0, len = statements.length; index < len; index++) { + const statement = statements[index]; + this.blockPrewalkStatement(statement); + } + } + // Walking iterates the statements and expressions and processes them walkStatements(statements) { for (let index = 0, len = statements.length; index < len; index++) { @@ -870,9 +875,6 @@ class Parser extends Tapable { case "BlockStatement": this.prewalkBlockStatement(statement); break; - case "ClassDeclaration": - this.prewalkClassDeclaration(statement); - break; case "DoWhileStatement": this.prewalkDoWhileStatement(statement); break; @@ -924,6 +926,23 @@ class Parser extends Tapable { } } + blockPrewalkStatement(statement) { + switch (statement.type) { + case "VariableDeclaration": + this.blockPrewalkVariableDeclaration(statement); + break; + case "ExportDefaultDeclaration": + this.blockPrewalkExportDefaultDeclaration(statement); + break; + case "ExportNamedDeclaration": + this.blockPrewalkExportNamedDeclaration(statement); + break; + case "ClassDeclaration": + this.blockPrewalkClassDeclaration(statement); + break; + } + } + walkStatement(statement) { if (this.hooks.statement.call(statement) !== undefined) return; switch (statement.type) { @@ -993,7 +1012,11 @@ class Parser extends Tapable { } walkBlockStatement(statement) { - this.walkStatements(statement.body); + this.inBlockScope(() => { + const body = statement.body; + this.blockPrewalkStatements(body); + this.walkStatements(body); + }); } walkExpressionStatement(statement) { @@ -1111,20 +1134,30 @@ class Parser extends Tapable { } walkForStatement(statement) { - if (statement.init) { - if (statement.init.type === "VariableDeclaration") { - this.walkStatement(statement.init); + this.inBlockScope(() => { + if (statement.init) { + if (statement.init.type === "VariableDeclaration") { + this.blockPrewalkVariableDeclaration(statement.init); + this.walkStatement(statement.init); + } else { + this.walkExpression(statement.init); + } + } + if (statement.test) { + this.walkExpression(statement.test); + } + if (statement.update) { + this.walkExpression(statement.update); + } + const body = statement.body; + if (body.type === "BlockStatement") { + // no need to add additional scope + this.blockPrewalkStatements(body.body); + this.walkStatements(body.body); } else { - this.walkExpression(statement.init); + this.walkStatement(body); } - } - if (statement.test) { - this.walkExpression(statement.test); - } - if (statement.update) { - this.walkExpression(statement.update); - } - this.walkStatement(statement.body); + }); } prewalkForInStatement(statement) { @@ -1135,13 +1168,23 @@ class Parser extends Tapable { } walkForInStatement(statement) { - if (statement.left.type === "VariableDeclaration") { - this.walkVariableDeclaration(statement.left); - } else { - this.walkPattern(statement.left); - } - this.walkExpression(statement.right); - this.walkStatement(statement.body); + this.inBlockScope(() => { + if (statement.left.type === "VariableDeclaration") { + this.blockPrewalkVariableDeclaration(statement.left); + this.walkVariableDeclaration(statement.left); + } else { + this.walkPattern(statement.left); + } + this.walkExpression(statement.right); + const body = statement.body; + if (body.type === "BlockStatement") { + // no need to add additional scope + this.blockPrewalkStatements(body.body); + this.walkStatements(body.body); + } else { + this.walkStatement(body); + } + }); } prewalkForOfStatement(statement) { @@ -1152,13 +1195,23 @@ class Parser extends Tapable { } walkForOfStatement(statement) { - if (statement.left.type === "VariableDeclaration") { - this.walkVariableDeclaration(statement.left); - } else { - this.walkPattern(statement.left); - } - this.walkExpression(statement.right); - this.walkStatement(statement.body); + this.inBlockScope(() => { + if (statement.left.type === "VariableDeclaration") { + this.blockPrewalkVariableDeclaration(statement.left); + this.walkVariableDeclaration(statement.left); + } else { + this.walkPattern(statement.left); + } + this.walkExpression(statement.right); + const body = statement.body; + if (body.type === "BlockStatement") { + // no need to add additional scope + this.blockPrewalkStatements(body.body); + this.walkStatements(body.body); + } else { + this.walkStatement(body); + } + }); } // Declarations @@ -1172,7 +1225,7 @@ class Parser extends Tapable { walkFunctionDeclaration(statement) { const wasTopLevel = this.scope.topLevelScope; this.scope.topLevelScope = false; - this.inScope(statement.params, () => { + this.inFunctionScope(true, statement.params, () => { for (const param of statement.params) { this.walkPattern(param); } @@ -1213,6 +1266,33 @@ class Parser extends Tapable { } } + enterDeclaration(declaration, onIdent) { + switch (declaration.type) { + case "VariableDeclaration": + for (const declarator of declaration.declarations) { + switch (declarator.type) { + case "VariableDeclarator": { + this.enterPattern(declarator.id, onIdent); + break; + } + } + } + break; + case "FunctionDeclaration": + this.enterPattern(declaration.id, onIdent); + break; + case "ClassDeclaration": + this.enterPattern(declaration.id, onIdent); + break; + } + } + + blockPrewalkExportNamedDeclaration(statement) { + if (statement.declaration) { + this.blockPrewalkStatement(statement.declaration); + } + } + prewalkExportNamedDeclaration(statement) { let source; if (statement.source) { @@ -1225,16 +1305,11 @@ class Parser extends Tapable { if ( !this.hooks.exportDeclaration.call(statement, statement.declaration) ) { - const originalDefinitions = this.scope.definitions; - const tracker = new TrackingSet(this.scope.definitions); - this.scope.definitions = tracker; this.prewalkStatement(statement.declaration); - const newDefs = Array.from(tracker.getAddedItems()); - this.scope.definitions = originalDefinitions; - for (let index = newDefs.length - 1; index >= 0; index--) { - const def = newDefs[index]; - this.hooks.exportSpecifier.call(statement, def, def, index); - } + let index = 0; + this.enterDeclaration(statement.declaration, def => { + this.hooks.exportSpecifier.call(statement, def, def, index++); + }); } } if (statement.specifiers) { @@ -1276,18 +1351,24 @@ class Parser extends Tapable { } } + blockPrewalkExportDefaultDeclaration(statement) { + if (statement.declaration.type === "ClassDeclaration") { + this.blockPrewalkClassDeclaration(statement.declaration); + } + } + prewalkExportDefaultDeclaration(statement) { - if (statement.declaration.id) { - const originalDefinitions = this.scope.definitions; - const tracker = new TrackingSet(this.scope.definitions); - this.scope.definitions = tracker; - this.prewalkStatement(statement.declaration); - const newDefs = Array.from(tracker.getAddedItems()); - this.scope.definitions = originalDefinitions; - for (let index = 0, len = newDefs.length; index < len; index++) { - const def = newDefs[index]; - this.hooks.exportSpecifier.call(statement, def, "default"); - } + this.prewalkStatement(statement.declaration); + if ( + statement.declaration.id && + statement.declaration.type !== "FunctionExpression" && + statement.declaration.type !== "ClassExpression" + ) { + this.hooks.exportSpecifier.call( + statement, + statement.declaration.id.name, + "default" + ); } } @@ -1331,12 +1412,20 @@ class Parser extends Tapable { } prewalkVariableDeclaration(statement) { + if (statement.kind !== "var") return; + this._prewalkVariableDeclaration(statement, this.hooks.varDeclarationVar); + } + + blockPrewalkVariableDeclaration(statement) { + if (statement.kind === "var") return; const hookMap = statement.kind === "const" ? this.hooks.varDeclarationConst - : statement.kind === "let" - ? this.hooks.varDeclarationLet - : this.hooks.varDeclarationVar; + : this.hooks.varDeclarationLet; + this._prewalkVariableDeclaration(statement, hookMap); + } + + _prewalkVariableDeclaration(statement, hookMap) { for (const declarator of statement.declarations) { switch (declarator.type) { case "VariableDeclarator": { @@ -1385,7 +1474,7 @@ class Parser extends Tapable { } } - prewalkClassDeclaration(statement) { + blockPrewalkClassDeclaration(statement) { if (statement.id) { this.scope.renames.set(statement.id.name, null); this.scope.definitions.add(statement.id.name); @@ -1415,11 +1504,15 @@ class Parser extends Tapable { } walkCatchClause(catchClause) { - // Error binding is optional in catch clause since ECMAScript 2019 - const errorBinding = - catchClause.param === null ? EMPTY_ARRAY : [catchClause.param]; - - this.inScope(errorBinding, () => { + this.inBlockScope(() => { + // Error binding is optional in catch clause since ECMAScript 2019 + if (catchClause.param !== null) { + this.enterPattern(catchClause.param, ident => { + this.scope.renames.set(ident, null); + this.scope.definitions.add(ident); + }); + this.walkPattern(catchClause.param); + } this.prewalkStatement(catchClause.body); this.walkStatement(catchClause.body); }); @@ -1600,7 +1693,7 @@ class Parser extends Tapable { scopeParams.push(expression.id.name); } - this.inScope(scopeParams, () => { + this.inFunctionScope(true, scopeParams, () => { for (const param of expression.params) { this.walkPattern(param); } @@ -1616,7 +1709,7 @@ class Parser extends Tapable { } walkArrowFunctionExpression(expression) { - this.inScope(expression.params, () => { + this.inFunctionScope(false, expression.params, () => { for (const param of expression.params) { this.walkPattern(param); } @@ -1791,7 +1884,7 @@ class Parser extends Tapable { scopeParams.push(functionExpression.id.name); } - this.inScope(scopeParams, () => { + this.inFunctionScope(true, scopeParams, () => { if (renameThis) { this.scope.renames.set("this", renameThis); } @@ -1896,6 +1989,12 @@ class Parser extends Tapable { } } + /** + * @deprecated + * @param {any} params scope params + * @param {function(): void} fn inner function + * @returns {void} + */ inScope(params, fn) { const oldScope = this.scope; this.scope = { @@ -1909,19 +2008,54 @@ class Parser extends Tapable { this.scope.renames.set("this", null); - for (const param of params) { - if (typeof param !== "string") { - this.enterPattern(param, param => { - this.scope.renames.set(param, null); - this.scope.definitions.add(param); - }); - } else if (param) { - this.scope.renames.set(param, null); - this.scope.definitions.add(param); - } + this.enterPatterns(params, ident => { + this.scope.renames.set(ident, null); + this.scope.definitions.add(ident); + }); + + fn(); + + this.scope = oldScope; + } + + inFunctionScope(hasThis, params, fn) { + const oldScope = this.scope; + this.scope = { + topLevelScope: oldScope.topLevelScope, + inTry: false, + inShorthand: false, + isStrict: oldScope.isStrict, + definitions: oldScope.definitions.createChild(), + renames: oldScope.renames.createChild() + }; + + if (hasThis) { + this.scope.renames.set("this", null); } + this.enterPatterns(params, ident => { + this.scope.renames.set(ident, null); + this.scope.definitions.add(ident); + }); + fn(); + + this.scope = oldScope; + } + + inBlockScope(fn) { + const oldScope = this.scope; + this.scope = { + topLevelScope: oldScope.topLevelScope, + inTry: oldScope.inTry, + inShorthand: false, + isStrict: oldScope.isStrict, + definitions: oldScope.definitions.createChild(), + renames: oldScope.renames.createChild() + }; + + fn(); + this.scope = oldScope; } @@ -1936,6 +2070,16 @@ class Parser extends Tapable { } } + enterPatterns(patterns, onIdent) { + for (const pattern of patterns) { + if (typeof pattern !== "string") { + this.enterPattern(pattern, onIdent); + } else if (pattern) { + onIdent(pattern); + } + } + } + enterPattern(pattern, onIdent) { if (!pattern) return; switch (pattern.type) { @@ -2137,6 +2281,7 @@ class Parser extends Tapable { if (this.hooks.program.call(ast, comments) === undefined) { this.detectStrictMode(ast.body); this.prewalkStatements(ast.body); + this.blockPrewalkStatements(ast.body); this.walkStatements(ast.body); } this.scope = oldScope; diff --git a/test/cases/parsing/block-scopes/index.js b/test/cases/parsing/block-scopes/index.js new file mode 100644 index 00000000000..2019fb4cc87 --- /dev/null +++ b/test/cases/parsing/block-scopes/index.js @@ -0,0 +1,76 @@ +import ok from "./module"; + +// This should not leak an "ok" declaration into this scope +export default (function ok() {}); + +it("should allow block scopes", () => { + expect(ok).toBe("ok"); + if (true) { + const ok = "no"; + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); + { + let ok = "no"; + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); + { + class ok {} + expect(new ok()).toBeInstanceOf(ok); + } + expect(ok).toBe("ok"); + for (let ok = "no", once = true; once; once = !once) { + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); + for (const ok of ["no"]) { + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); + for (const ok in { no: 1 }) { + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); + try { + throw "no"; + } catch (ok) { + expect(ok).toBe("no"); + } + expect(ok).toBe("ok"); +}); + +it("should allow function scopes in block scopes", () => { + let f; + { + f = () => { + expect(ok).toBe("no"); + }; + const ok = "no"; + } + f(); +}); + +it("should not block scope vars (for)", () => { + expect(ok).toBe(undefined); + for (var ok = "no", once = true; once; once = !once) { + expect(ok).toBe("no"); + } + expect(ok).toBe("no"); +}); + +it("should not block scope vars (for-of)", () => { + expect(ok).toBe(undefined); + for (var ok of ["no"]) { + expect(ok).toBe("no"); + } + expect(ok).toBe("no"); +}); + +it("should not block scope vars (for-in)", () => { + expect(ok).toBe(undefined); + for (var ok in { no: 1 }) { + expect(ok).toBe("no"); + } + expect(ok).toBe("no"); +}); diff --git a/test/cases/parsing/block-scopes/module.js b/test/cases/parsing/block-scopes/module.js new file mode 100644 index 00000000000..5c6b89abfc8 --- /dev/null +++ b/test/cases/parsing/block-scopes/module.js @@ -0,0 +1 @@ +export default "ok";