transform-block-scoping: closure created in a for-loop initializer captures the wrong binding
- Would you like to work on a fix?
How are you using Babel?
babel-loader (webpack)
Input code
for (let i = 0, f = () => i; i < 1;) {
i = 42;
console.log(f());
}Configuration file name
No response
Configuration
{
"plugins": ["@babel/plugin-transform-block-scoping"]
}Current and expected behavior
Babel changes the observable runtime behavior of this program.
The original JavaScript prints 0.
However, @babel/plugin-transform-block-scoping transforms it into:
for (var i = 0, f = () => i; i < 1;) {
i = 42;
console.log(f());
}The transformed program prints 42.
The closure f is created while evaluating the lexical for initializer and captures the initial i binding.
Before executing the first iteration, the ECMAScript for semantics create a new per-iteration binding for i. Therefore, the assignment i = 42 in the loop body updates the per-iteration binding, while f must continue to observe the initial binding whose value is 0.
Lowering both bindings to the same var i loses this distinction. As a result, the transform silently changes valid JavaScript semantics.
Environment
- Babel version(s): 8.0.4; also reproduces with 7.29.1
- Plugin: @babel/plugin-transform-block-scoping
- Node: 24.14.0
- npm: 11.11.0
- OS: macOS
- Monorepo: no
Possible solution
The loop-binding capture analysis should also account for closures created in a ForStatement initializer.
Currently, references outside the loop body appear to be discarded even when they occur inside a closure. Consequently, the reference to i in f = () => i is not treated as requiring preservation of the lexical environment.
The transform needs to preserve the initializer environment separately from the per-iteration environment when lowering the lexical declaration to var.
Additional context
No response
Source: babel/babel