Skip to content

Object-rest transform changes for-of var bindings into block-scoped bindings #12153

Description

@ko-success

Describe the bug

When lowering object rest for an ES2015 target, SWC transforms a var destructuring declaration in a for-of head into declarations using let inside the loop body.
The bound names of:

for (var { a, ...b } of iterable) {}

belong to the surrounding variable environment and must remain accessible after the loop.
However, moving them into let declarations inside the loop body changes their scope and lifetime.

Input code

for (var { a, ...b } of [{ x: 1, y: 2 }]) {}
console.log(a, b);

Config

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript"
    },
    "minify": {
      "compress": false,
      "mangle": false
    },
    "target": "es2015"
  },
  "minify": false
}

Link to the code that reproduces this issue

https://play.swc.rs/?version=1.16.1&code=H4sIAAAAAAAAA0vLL1LQKEssUqhWSNRR0NPTS1KoVchPU4iuVqiwUjDUUai0UjBSqI3VVKiu5UrOzyvOz0nVy8lP1wCqTtK0BgCoRhSNPwAAAA%3D%3D&config=H4sIAAAAAAAAA1WNvQqAMBCDd5%2BidHZQwcW3OcopFVvLXQdL8d09K%2F5tyReS5EopPbPRg8oixQQgRnq8EE4%2BwiZEo3HAhmyIuoR7fXWc9XZM345ZXSBkFjbCwljfgQM%2FLXjj30oEmjCWH%2B6atj8%2FSvbuX6X9AERNOxe2AAAA

SWC Info output

No response

Expected behavior

Both a and b are declared using var, so they should remain accessible after the loop.
Expected output:
undefined {x:1,y:2}

Using var declaration inside the loop body would also preserve the required variable scope:

...
for (var _ref of [
    {
        x: 1,
        y: 2
    }
]){
    var { a } = _ref, b = _object_without_properties(_ref, [
        "a"
    ]);
}
console.log(a, b);

Actual behavior

function _object_without_properties(source, excluded) {
    if (source == null) return {};
    var target = {}, sourceKeys, key, i;
    if (typeof Reflect !== "undefined" && Reflect.ownKeys) {
        sourceKeys = Reflect.ownKeys(Object(source));
        for(i = 0; i < sourceKeys.length; i++){
            key = sourceKeys[i];
            if (excluded.indexOf(key) >= 0) continue;
            if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
            target[key] = source[key];
        }
        return target;
    }
    target = _object_without_properties_loose(source, excluded);
    if (Object.getOwnPropertySymbols) {
        sourceKeys = Object.getOwnPropertySymbols(source);
        for(i = 0; i < sourceKeys.length; i++){
            key = sourceKeys[i];
            if (excluded.indexOf(key) >= 0) continue;
            if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
            target[key] = source[key];
        }
    }
    return target;
}
function _object_without_properties_loose(source, excluded) {
    if (source == null) return {};
    var target = {}, sourceKeys = Object.getOwnPropertyNames(source), key, i;
    for(i = 0; i < sourceKeys.length; i++){
        key = sourceKeys[i];
        if (excluded.indexOf(key) >= 0) continue;
        if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
        target[key] = source[key];
    }
    return target;
}
for (var _ref of [
    {
        x: 1,
        y: 2
    }
]){
    let { a } = _ref, b = _object_without_properties(_ref, [
        "a"
    ]);
}
console.log(a, b); // reference error

Version

1.16.1

Additional context

The closest previous fix appears to be #7437. That change used let for declarations generated by the object-rest transform in order to preserve per-iteration lexical bindings. However, the transform currently appears to use VarDeclKind::Let unconditionally.
The transform likely needs to distinguish the original declaration kind:

  • An original var declaration must preserve function/global variable scope.
  • Original let and const declarations must preserve per-iteration lexical bindings.

Simply changing all generated declarations back to var would likely regress #6988, so the transformation should branch based on the declaration kind or hoist var bindings and emit assignments inside each iteration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions