babel: [Bug]: traverse calculates scope incorrectly for var in function expression with same name as var

💻

  • Would you like to work on a fix?

How are you using Babel?

Programmatic API (babel.transform, babel.parse)

Input code

const {transformSync} = require('@babel/core');

function transformWithPlugin(code) {
  return transformSync(code, {
    plugins: [
      () => ({
        visitor: {
          Identifier(path) {
            const binding = path.scope.getBinding(path.node.name);
            path.node.leadingComments = [{type: 'CommentBlock', value: binding.path.type}];
          }
        }
      })
    ],
    retainLines: true
  }).code;
}

// Correct
console.log(transformWithPlugin('(function f() { let x = 1; return x; })'));
// (function /*FunctionExpression*/f() {let /*VariableDeclarator*/x = 1;return (/*VariableDeclarator*/x);});

// Wrong
console.log(transformWithPlugin('(function x() { let x = 1; return x; })'));
// (function /*FunctionExpression*/x() {let /*FunctionExpression*/x = 1;return (/*FunctionExpression*/x);});

Configuration file name

n/a

Configuration

n/a

Current and expected behavior

In the example above, the first transform is correct:

(function f() { let x = 1; return x; }) is transformed to: (function /*FunctionExpression*/f() {let /*VariableDeclarator*/x = 1;return (/*VariableDeclarator*/x);});

path.scope.getBinding('x').path points to the variable let x.

But if the function expression is named x, same as the var, it’s incorrect (2nd transform):

(function x() { let x = 1; return x; }) is transformed to: (function /*FunctionExpression*/x() {let /*FunctionExpression*/x = 1;return (/*FunctionExpression*/x);});

Here path.scope.getBinding('x').path erroneously points to function x.

Same problem occurs if let x is replaced with const x, var x, function x() {} or class x {}.

Environment

System:
  OS: macOS 10.15.7
Binaries:
  Node: 16.13.1 - ~/.nvm/versions/node/v16.13.1/bin/node
  npm: 8.1.2 - ~/.nvm/versions/node/v16.13.1/bin/npm
npmPackages:
  @babel/core: ^7.16.0 => 7.16.0
  @babel/generator: ^7.16.0 => 7.16.0
  @babel/helper-module-transforms: ^7.16.0 => 7.16.0
  @babel/helper-plugin-utils: ^7.14.5 => 7.14.5
  @babel/parser: ^7.16.4 => 7.16.4
  @babel/plugin-transform-arrow-functions: ^7.16.0 => 7.16.0
  @babel/plugin-transform-modules-commonjs: ^7.16.0 => 7.16.0
  @babel/plugin-transform-react-jsx: ^7.16.0 => 7.16.0
  @babel/plugin-transform-strict-mode: ^7.16.0 => 7.16.0
  @babel/register: ^7.16.0 => 7.16.0
  @babel/traverse: ^7.16.3 => 7.16.3
  @babel/types: ^7.16.0 => 7.16.0
  babel-jest: ^27.4.2 => 27.4.2
  babel-plugin-dynamic-import-node: ^2.3.3 => 2.3.3
  eslint: ^7.32.0 => 7.32.0
  jest: ^27.4.3 => 27.4.3

Possible solution

Not specifically. I assume cause is that the scopes tracker is seeing the binding to function expression before the binding to let x.

Additional context

I would be happy to work on a fix. Any pointers on where to look in the codebase would be appreciated.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 15 (14 by maintainers)

Most upvoted comments

@overlookmotel you’re right. I was misreading the function/class expression id part. Now it makes sense, renaming the id won’t touch params, nor local vars.

NB: that last example does not actually illustrate the issue, because function declaration binds x/someOtherName in outer scope. Function expression is different, there the name is bound inside:

var f = function x(g = x) {
  let x = 2;
  return x;
}