@@ -182,6 +182,7 @@ const EMPTY_MODULE_DECLARATIONS = [];
182182// object pattern, so a tap on that type is not served from the list.
183183const 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
0 commit comments