What
docs/plans/issue-2088.md's findTopLevelFunctionNodeByName (condition 4's identifier-resolution chain, WU-2) counts every top-level declaration of name reachable from the module root and fails safe (null) once more than one exists (round 14, #2633). Round 15 widens what this count sees: var is function-scoped, not block-scoped, so a var name (or, in sloppy-mode/CommonJS code, an Annex-B block-level function name(){}) declared anywhere inside a nested if/for/try/switch/bare-block body at module level hoists to the SAME module-scope binding a direct top-level declaration would — invisible to round 14's own count, which only walked root's direct children. Round 15 closes that soundness gap by additionally walking each non-declaration top-level statement's own subtree (skipping nested function bodies, exactly as the existing functionScopeDeclaresVar already does for the analogous #2257 shadow check) for a var declarator or a block-level function_declaration matching name.
This fix is unconditionally conservative in two ways neither this round nor round 14 attempts to resolve further:
- It never resolves to a declaration that exists ONLY inside a nested block. If
name has zero direct-child declarations and exactly one hoisted (nested) one, the total count is exactly 1 — unambiguous — but result (the node the caller's arrow/this-body check inspects) is only ever assigned from a DIRECT-CHILD match, so the function still returns null regardless. This is no worse than round 14's own pre-existing behavior for this shape (which also returned null, for the simpler reason that it found no direct-child match at all), so it is not a regression — but a fuller fix could resolve result to that sole nested declaration and let T1 fire.
- It does not gate the Annex-B
function_declaration count on the file's own strict/sloppy/module parse goal. Annex B's block-level function hoisting (ECMA-262 Annex B.3.3) applies only in sloppy (non-strict, non-module) script code; in an ES module or a "use strict" file, a block-level function name(){} is purely block-scoped — a distinct binding, not a redeclaration, and already correctly handled by the shadow axis instead (findResolvingScopeNode). This function has no way to know the file's own parse goal, so it counts a block-level function_declaration as a possible duplicate regardless of mode — costing recall (an unnecessary fail-safe) in a strict-mode/ESM file that shadows a module-level name with a block-scoped function of the same name, a shape rounds 10-12's own shadow-axis fixtures ((v)-(z), (aa), (ad)) already exercise correctly via the shadow mechanism and would, under this round's over-broad count, additionally trip the duplicate-declaration fail-safe too — landing on the same (safe) outcome, but by the wrong mechanism for the wrong reason.
Why this wasn't fixed inline
Both costs are the same "detect an ambiguity, then fail safe outright, rather than resolve one layer deeper" trade-off this resolution chain has already accepted repeatedly (round 10's shadow fail-safe, #2625; round 13's reassignment fail-safe, #2631; round 14's duplicate-declaration fail-safe, #2633). Extending the fix to also (1) attempt resolution when the hoisted count is exactly 1, or (2) thread the file's own parse-goal (strict/sloppy/module) through this call chain to gate the Annex-B branch, would each add scope to a resolution chain already found and fixed across fifteen rounds, for a coverage gap with no observed instance in this codebase today rather than an unavoidable safety boundary.
Suggested fix shape (not binding — decide at execute/fix time)
For (1): track the sole hoisted declaration's own node (not just a count) when the direct-children loop finds none, and let the caller check IT for this-freedom precisely as it already does for a direct-child match, only falling back to fail-safe once a second declaration (direct or hoisted) appears. For (2): determine the file's parse goal once (ES module: file extension/import/export presence; "use strict": a leading string-literal expression statement) and skip the Annex-B function_declaration branch of the count entirely when neither applies. Add WU-10 escape-fallback/correlation cases proving each, once implemented, without weakening round 15's own fail-safe default for the genuinely ambiguous case.
Where
What
docs/plans/issue-2088.md'sfindTopLevelFunctionNodeByName(condition 4's identifier-resolution chain, WU-2) counts every top-level declaration ofnamereachable from the module root and fails safe (null) once more than one exists (round 14, #2633). Round 15 widens what this count sees:varis function-scoped, not block-scoped, so avar name(or, in sloppy-mode/CommonJS code, an Annex-B block-levelfunction name(){}) declared anywhere inside a nestedif/for/try/switch/bare-block body at module level hoists to the SAME module-scope binding a direct top-level declaration would — invisible to round 14's own count, which only walkedroot's direct children. Round 15 closes that soundness gap by additionally walking each non-declaration top-level statement's own subtree (skipping nested function bodies, exactly as the existingfunctionScopeDeclaresVaralready does for the analogous #2257 shadow check) for avardeclarator or a block-levelfunction_declarationmatchingname.This fix is unconditionally conservative in two ways neither this round nor round 14 attempts to resolve further:
namehas zero direct-child declarations and exactly one hoisted (nested) one, the total count is exactly 1 — unambiguous — butresult(the node the caller's arrow/this-body check inspects) is only ever assigned from a DIRECT-CHILD match, so the function still returnsnullregardless. This is no worse than round 14's own pre-existing behavior for this shape (which also returnednull, for the simpler reason that it found no direct-child match at all), so it is not a regression — but a fuller fix could resolveresultto that sole nested declaration and let T1 fire.function_declarationcount on the file's own strict/sloppy/module parse goal. Annex B's block-level function hoisting (ECMA-262 Annex B.3.3) applies only in sloppy (non-strict, non-module) script code; in an ES module or a"use strict"file, a block-levelfunction name(){}is purely block-scoped — a distinct binding, not a redeclaration, and already correctly handled by the shadow axis instead (findResolvingScopeNode). This function has no way to know the file's own parse goal, so it counts a block-levelfunction_declarationas a possible duplicate regardless of mode — costing recall (an unnecessary fail-safe) in a strict-mode/ESM file that shadows a module-level name with a block-scoped function of the same name, a shape rounds 10-12's own shadow-axis fixtures ((v)-(z), (aa), (ad)) already exercise correctly via the shadow mechanism and would, under this round's over-broad count, additionally trip the duplicate-declaration fail-safe too — landing on the same (safe) outcome, but by the wrong mechanism for the wrong reason.Why this wasn't fixed inline
Both costs are the same "detect an ambiguity, then fail safe outright, rather than resolve one layer deeper" trade-off this resolution chain has already accepted repeatedly (round 10's shadow fail-safe, #2625; round 13's reassignment fail-safe, #2631; round 14's duplicate-declaration fail-safe, #2633). Extending the fix to also (1) attempt resolution when the hoisted count is exactly 1, or (2) thread the file's own parse-goal (strict/sloppy/module) through this call chain to gate the Annex-B branch, would each add scope to a resolution chain already found and fixed across fifteen rounds, for a coverage gap with no observed instance in this codebase today rather than an unavoidable safety boundary.
Suggested fix shape (not binding — decide at execute/fix time)
For (1): track the sole hoisted declaration's own node (not just a count) when the direct-children loop finds none, and let the caller check IT for
this-freedom precisely as it already does for a direct-child match, only falling back to fail-safe once a second declaration (direct or hoisted) appears. For (2): determine the file's parse goal once (ES module: file extension/import/exportpresence;"use strict": a leading string-literal expression statement) and skip the Annex-Bfunction_declarationbranch of the count entirely when neither applies. Add WU-10 escape-fallback/correlation cases proving each, once implemented, without weakening round 15's own fail-safe default for the genuinely ambiguous case.Where
docs/plans/issue-2088.md— WU-2'sfindTopLevelFunctionNodeByName/ newcountHoistedVarScopeDeclarations(condition 4's identifier-resolution chain) and its doc comment; WU-7's Rust mirror.