Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transform-regenerator incorrectly hoists loop variable declaration #12806

Closed
1 task done
djsweet opened this issue Feb 15, 2021 · 5 comments
Closed
1 task done

transform-regenerator incorrectly hoists loop variable declaration #12806

djsweet opened this issue Feb 15, 2021 · 5 comments
Labels
i: bug outdated A closed issue/PR that is archived due to age. Recommended to make a new issue

Comments

@djsweet
Copy link

djsweet commented Feb 15, 2021

Bug Report

  • I would like to work on a fix!

Current behavior

Variable declaration scope is not respected directly by @babel/transform-regenerator. In this example, capture is supposed to be scoped to the for ... of loop, but instead is hoisted outside of it. This breaks the expectation of the arrow function passed to setTimeout that capture won't change: because it is scoped outside of the for ... of loop, it does change on every iteration, meaning that the callback is always given capture = 4.

I get this output from npx babel --plugins=@babel/transform-regenerator:

function test() {                                           
  var offset, capture;                                      
  return regeneratorRuntime.async(function test$(_context) {
    while (1) switch (_context.prev = _context.next) {      
      case 0:                                               
        for (offset of [1, 2, 3, 4]) {                      
          capture = offset;
          setTimeout(() => {                                
            console.log(capture);                           
          }, 0);                                            
        }                                                   
                                                            
      case 1:                                               
      case "end":                                           
        return _context.stop();                             
    }                                                       
  }, null, null, null, Promise);                            
}                                                           
                                                            

And the resulting console output is

4
4
4
4

Input Code

async function test() {               
  for (const offset of [1, 2, 3, 4]) {
    const capture = offset;           
    setTimeout(() => {                
      console.log(capture);           
    }, 0);                            
  }                                   
}                                     

Expected behavior

Correct code is generated by @babel/transform-regenerator if preceded by @babel/transform-block-scoping, by running npx babel --plugins=@babel/transform-block-scoping,@babel/transform-regenerator.

function test() {
  var _loop, offset;

  return regeneratorRuntime.async(function test$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _loop = function (offset) {
            var capture = offset;
            setTimeout(() => {
              console.log(capture);
            }, 0);
          };

          for (offset of [1, 2, 3, 4]) {
            _loop(offset);
          }

        case 2:
        case "end":
          return _context.stop();
      }
    }
  }, null, null, null, Promise);
}

When executed, capture is always the value of offset at the triggering iteration, so the resulting console output is

1
2
3
4

Babel Configuration (babel.config.js, .babelrc, package.json#babel, cli command, .eslintrc)

npx babel --plugins=@babel/transform-regenerator # Buggy
npx babel --plugins=@babel/transform-block-scoping,@babel/transform-regenerator # Correct

Environment

  System:
    OS: Linux 5.4 Ubuntu 20.04 LTS (Focal Fossa)
  Binaries:
    Node: 10.21.0 - ~/.nvm/versions/node/v10.21.0/bin/node
    npm: 6.14.4 - ~/.nvm/versions/node/v10.21.0/bin/npm
  npmPackages:
    @babel/cli: 7.12.16 => 7.12.16
    @babel/core: 7.12.16 => 7.12.16
    @babel/plugin-transform-block-scoping: 7.12.13 => 7.12.13
    @babel/plugin-transform-regenerator: 7.12.13 => 7.12.13

Possible Solution

If this is intentional behavior, packages/babel-compat-data/data/plugins.json should be updated such that transform-block-scoping is always triggered for browser versions which also trigger transform-regenerator, e.g.

"transform-block-scoping": {
    "chrome": "50",
    "opera": "37",
    "edge": "14",
    "firefox": "53",
    "safari": "11",
    "node": "6",
    "ios": "11",
    "samsung": "5",
    "electron": "1.1"
},

as particular filters in browserslist can result in this plugin configuration.

Additional context
I discovered the issue with the following configuration in my browserslist when using @babel/preset-env:

[
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
]

Removing not ie <= 11 corrected the issue, as did adding not chrome <= 49. It seems that transform-regenerator has been depending on a preceding transform-block-scoping, which is always the case when supporting IE, but is not the case otherwise. Setting not chrome <= 49 results in preset-env skipping over transform-regenerator entirely, avoiding the bug.

@babel-bot
Copy link
Collaborator

Hey @djsweet! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack community that typically always has someone willing to help. You can sign-up here for an invite."

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 16, 2021

This is quite hard to fix in transform-regenerator.

Would you like to open a PR adding generators to

, so that (as a workaround) we always compile let/const when compiling generators?

At some point we'll need a proper fix, but we can at least "hide" the bug for now.

@nicolo-ribaudo
Copy link
Member

Fixing this will also fix #12784

@odysseus654
Copy link

I'm uncertain of this but would this resolve an issue I've been finding in my code, where "const" variable declarations outside of a for loop inside a generator function have their value unexpectedly changed? Prettifying the code shows the variables have been reused by local variables inside the for loop, I'm currently working around this by inserting fake references to the variables late in the function to keep them "alive".

IlyaSemenov added a commit to IlyaSemenov/babel that referenced this issue May 2, 2023
IlyaSemenov added a commit to IlyaSemenov/babel that referenced this issue May 3, 2023
nicolo-ribaudo added a commit that referenced this issue May 8, 2023
* fix: enable transform-block-scoping with generators feature (#12806)

* Update Babel 8 fixtures

* Fix test difference in Babel 7 and 8

---------

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
@liuxingbaoyu
Copy link
Member

It has been fixed.
repl

@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 8, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
i: bug outdated A closed issue/PR that is archived due to age. Recommended to make a new issue
Projects
None yet
Development

No branches or pull requests

5 participants