Skip to content

Commit d318c6c

Browse files
fix(parser): scope strict-mode block function declarations to their block (#22800)
* fix(parser): scope strict-mode block function declarations to their block Only sloppy-mode Annex B hoists a plain function declaration out of its block; strict, async and generator ones stay block-scoped. Fixes #22797 * fix(parser): keep block functions lexical in the full pre-walk too A tap on the deprecated preStatement hook walks nested blocks, where the typed hooks declared a strict block function in the enclosing scope. * fix(parser): bind in the block only the functions that do not hoist A function body is walked as its own block scope, so binding every function there hid the tags plugins put on scope-level ones, such as the nested __webpack_require__ rename. Also count an exported declaration as the scope's own.
1 parent bb28093 commit d318c6c

12 files changed

Lines changed: 253 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Scope strict-mode, async and generator block function declarations to their block.

‎lib/javascript/JavascriptParser.js‎

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ const EMPTY_MODULE_DECLARATIONS = [];
182182
// object pattern, so a tap on that type is not served from the list.
183183
const BLOCK_DECLARATION_TYPES = new Set([
184184
"VariableDeclaration",
185+
"FunctionDeclaration",
185186
"ClassDeclaration",
186187
"ExportNamedDeclaration",
187188
"ExportDefaultDeclaration"
@@ -1122,6 +1123,35 @@ const hoistedDeclarationsOf = (node, trusted) => {
11221123
return collectHoistedDeclarations(node.body, EMPTY_HOISTED_DECLARATIONS);
11231124
};
11241125

1126+
/**
1127+
* Whether a statement is one of the list's own, or the declaration of an
1128+
* `export` that is, rather than nested in one of them. It is found by its start
1129+
* offset since the list is in source order.
1130+
* @param {(Statement | ModuleDeclaration)[]} statements the list
1131+
* @param {Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration} statement the statement to find
1132+
* @returns {boolean} whether the list holds it directly
1133+
*/
1134+
const holdsStatement = (statements, statement) => {
1135+
const start = /** @type {Range} */ (statement.range)[0];
1136+
let low = 0;
1137+
let high = statements.length - 1;
1138+
while (low <= high) {
1139+
const middle = (low + high) >>> 1;
1140+
const candidate = statements[middle];
1141+
if (candidate === statement) return true;
1142+
if (/** @type {Range} */ (candidate.range)[0] < start) low = middle + 1;
1143+
else high = middle - 1;
1144+
}
1145+
// the last statement starting before it is the one that could hold it
1146+
const holder = high >= 0 ? statements[high] : undefined;
1147+
return (
1148+
holder !== undefined &&
1149+
(holder.type === "ExportNamedDeclaration" ||
1150+
holder.type === "ExportDefaultDeclaration") &&
1151+
holder.declaration === statement
1152+
);
1153+
};
1154+
11251155
/**
11261156
* Which statements of a list declare into its block scope, scanned for an AST
11271157
* some other parser built. The parser's own record is read instead when there
@@ -1484,6 +1514,9 @@ class JavascriptParser extends Parser {
14841514
this.prevStatement = undefined;
14851515
/** @type {boolean} */
14861516
this._hasParserRecord = false;
1517+
// the statements of the scope last hoisted into, which its block pass reads next
1518+
/** @type {(Statement | ModuleDeclaration)[] | undefined} */
1519+
this._scopeStatements = undefined;
14871520
// BACKWARD-COMPAT START
14881521

14891522
// WHY: the hoisting pass reaches declarations alone, so a tap that
@@ -2729,6 +2762,7 @@ class JavascriptParser extends Parser {
27292762
* @param {Program | BlockStatement} node the node owning the scope
27302763
*/
27312764
_preWalkScope(node) {
2765+
this._scopeStatements = node.body;
27322766
// BACKWARD-COMPAT START
27332767
if (this._needsFullPreWalk || this.hooks.preStatement.taps.length !== 0) {
27342768
if (node.type === "Program") this.preWalkStatements(node.body);
@@ -2756,6 +2790,25 @@ class JavascriptParser extends Parser {
27562790
}
27572791
}
27582792

2793+
/**
2794+
* Whether a function declaration stays in its block rather than hoisting to
2795+
* the scope last pre-walked, which only sloppy-mode Annex B lets it do.
2796+
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
2797+
* @returns {boolean} whether its block, not the scope, binds it
2798+
*/
2799+
_isBlockScopedFunction(statement) {
2800+
const statements = this._scopeStatements;
2801+
// WHY: strictness is read here, not in the parser: `javascript/auto`
2802+
// reads a script as a strict module first.
2803+
return (
2804+
statements !== undefined &&
2805+
(this.scope.isStrict ||
2806+
statement.generator === true ||
2807+
statement.async === true) &&
2808+
!holdsStatement(statements, statement)
2809+
);
2810+
}
2811+
27592812
// BACKWARD-COMPAT START
27602813
/**
27612814
* Walk every statement of a scope to declare what hoists to it. Only a tap
@@ -3041,8 +3094,18 @@ class JavascriptParser extends Parser {
30413094
let bail =
30423095
preStatement.taps.length !== 0 ? preStatement.call(statement) : undefined;
30433096
if (!bail) {
3044-
const typed = preStatementByType.get(statement.type);
3045-
if (typed !== undefined) bail = typed.call(statement);
3097+
// WHY: a block-scoped function is the block pass's; the full pre-walk
3098+
// still shows it to `preStatement`, but not to the typed hooks, which
3099+
// would declare it in the scope (`InnerGraphPlugin` tags it top-level).
3100+
if (
3101+
statement.type === "FunctionDeclaration" &&
3102+
this._isBlockScopedFunction(statement)
3103+
) {
3104+
bail = true;
3105+
} else {
3106+
const typed = preStatementByType.get(statement.type);
3107+
if (typed !== undefined) bail = typed.call(statement);
3108+
}
30463109
}
30473110
if (bail) {
30483111
this.prevStatement =
@@ -3132,6 +3195,9 @@ class JavascriptParser extends Parser {
31323195
case "ClassDeclaration":
31333196
this.blockPreWalkClassDeclaration(statement);
31343197
break;
3198+
case "FunctionDeclaration":
3199+
this.blockPreWalkFunctionDeclaration(statement);
3200+
break;
31353201
case "ExportNamedDeclaration":
31363202
this.blockPreWalkExportNamedDeclaration(statement);
31373203
break;
@@ -3578,6 +3644,17 @@ class JavascriptParser extends Parser {
35783644
}
35793645
}
35803646

3647+
/**
3648+
* Bind a function declaration in the block holding it, when it does not
3649+
* hoist to the enclosing scope.
3650+
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration
3651+
*/
3652+
blockPreWalkFunctionDeclaration(statement) {
3653+
if (statement.id && this._isBlockScopedFunction(statement)) {
3654+
this.defineVariable(statement.id.name);
3655+
}
3656+
}
3657+
35813658
/**
35823659
* Walk function declaration.
35833660
* @param {FunctionDeclaration | MaybeNamedFunctionDeclaration} statement function declaration

‎lib/javascript/syntax-parser.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7938,6 +7938,7 @@ const declaresIntoBlock = (statement) => {
79387938
const type = statement.type;
79397939
if (
79407940
type === "VariableDeclaration" ||
7941+
type === "FunctionDeclaration" ||
79417942
type === "ClassDeclaration" ||
79427943
type === "ExportNamedDeclaration" ||
79437944
type === "ExportDefaultDeclaration"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function strictOuter() {
2+
"use strict";
3+
{
4+
function require() {}
5+
}
6+
return require("./lib");
7+
}
8+
9+
function strictInner() {
10+
"use strict";
11+
{
12+
function require(request) {
13+
return "local " + request;
14+
}
15+
return require("./lib");
16+
}
17+
}
18+
19+
function sloppyAsync() {
20+
{
21+
async function require() {}
22+
}
23+
return require("./lib");
24+
}
25+
26+
function sloppyGenerator() {
27+
{
28+
function* require() {}
29+
}
30+
return require("./lib");
31+
}
32+
33+
function sloppyAnnexB() {
34+
{
35+
function require() {
36+
return "annex b";
37+
}
38+
}
39+
return require("./lib");
40+
}
41+
42+
module.exports = {
43+
strictOuter: strictOuter(),
44+
strictInner: strictInner(),
45+
sloppyAsync: sloppyAsync(),
46+
sloppyGenerator: sloppyGenerator(),
47+
sloppyAnnexB: sloppyAnnexB()
48+
};

‎test/configCases/parsing/custom-parser-hoisting/index.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { alias, reexported } from "./barrel";
22

33
const hoisting = require("./hoisting");
44
const blockDeclarations = require("./block-declarations");
5+
const blockFunctions = require("./block-functions");
56

67
it("should hoist from every position with a custom parser", () => {
78
expect(hoisting).toEqual({
@@ -47,6 +48,16 @@ it("should bind every block position with a custom parser", () => {
4748
});
4849
});
4950

51+
it("should scope a strict or async block function to its block with a custom parser", () => {
52+
expect(blockFunctions).toEqual({
53+
strictOuter: "lib",
54+
strictInner: "local ./lib",
55+
sloppyAsync: "lib",
56+
sloppyGenerator: "lib",
57+
sloppyAnnexB: "annex b"
58+
});
59+
});
60+
5061
it("should read imports and re-exports with a custom parser", () => {
5162
expect(alias).toBe("named");
5263
expect(reexported).toBe("reexported");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "lib";
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function strictOuter() {
2+
"use strict";
3+
{
4+
function require() {}
5+
}
6+
return require("./lib");
7+
}
8+
9+
function strictInner() {
10+
"use strict";
11+
{
12+
function require(request) {
13+
return "local " + request;
14+
}
15+
return require("./lib");
16+
}
17+
}
18+
19+
function sloppyAsync() {
20+
{
21+
async function require() {}
22+
}
23+
return require("./lib");
24+
}
25+
26+
function sloppyGenerator() {
27+
{
28+
function* require() {}
29+
}
30+
return require("./lib");
31+
}
32+
33+
function sloppyAnnexB() {
34+
{
35+
function require() {
36+
return "annex b";
37+
}
38+
}
39+
return require("./lib");
40+
}
41+
42+
module.exports = {
43+
strictOuter: strictOuter(),
44+
strictInner: strictInner(),
45+
sloppyAsync: sloppyAsync(),
46+
sloppyGenerator: sloppyGenerator(),
47+
sloppyAnnexB: sloppyAnnexB()
48+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { viaImport, viaRequire } from "./module";
2+
3+
const blockFunctions = require("./block-functions");
4+
5+
it("should scope a strict or async block function to its block", () => {
6+
expect(blockFunctions).toEqual({
7+
strictOuter: "lib",
8+
strictInner: "local ./lib",
9+
sloppyAsync: "lib",
10+
sloppyGenerator: "lib",
11+
sloppyAnnexB: "annex b"
12+
});
13+
});
14+
15+
it("should scope a block function to its block in a module", () => {
16+
expect(viaImport).toBe("lib");
17+
expect(viaRequire).toBe("lib");
18+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "lib";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import lib from "./lib";
2+
3+
{
4+
function require() {}
5+
}
6+
7+
export const viaImport = lib;
8+
export const viaRequire = require("./lib");

0 commit comments

Comments
 (0)