Search code examples
javascriptbabeljsabstract-syntax-treecode-transformation

Renaming Function Parameters with Babel Without Changing Other Identical Identifiers


I'm working on a JavaScript project where I need to be able to programmatically rename function parameters. The specific challenge I'm facing is differentiating between function parameters and other variables with the same name within the function body. My goal is to rename only the parameters and not affect any other identifiers that might have the same name within the scope of the function.

Here's a snippet of what I tried:

JSFiddle: https://jsfiddle.net/jfbhzLrp/

window.onload = function() {
  const code = `function process(x, y) { return { z: x * y, x: 4 }; }`;

  const result = Babel.transform(code, {
    plugins: [{
      visitor: {
        Identifier(path) {
          if (path.node.name === 'x' && path.scope.getBinding(path.node.name).kind === 'param') {
            path.node.name = 'a';
          }
        },
        FunctionDeclaration(path) {
          path.node.params.forEach(param => {
            if (param.name === 'x') {
              param.name = 'a';
            }
          });
        },
      }
    }]
  });

  console.log(result.code);
};

In the above script, I want to ensure that only the parameter x in the function declaration is and it's usages in the function body is renamed to a, but any variables/object keys named x inside the function body remains unchanged.

This is what I expect:

function process(a, y) {
  return {
    z: a * y,
    x: 4
  };
}

But this is what I get:

function process(a, y) {
  return {
    z: a * y,
    a: 4
  };
}

As you can see the x key in the returned object has transformed to a too.
This is first time I use Babel and I thought condition path.scope.getBinding(path.node.name).kind === 'param' would work but it didn't.

I appreciate any advice or examples on how to achieve this using Babel. Thanks!

JSFiddle: https://jsfiddle.net/jfbhzLrp/


Solution

  • After a lot of trial and error I was able to make this function. Hope it helps someone else.

    function onready() {
    
    function renameFunctionParam(func, paramName, newName) {
      if (typeof func !== 'string') {
        console.error('\'func\' not a string', func, paramName, newName);
        throw new Error('\'func\' not a string');
      }
      let topLevelPath;
      const result = Babel.transform(func, {
        plugins: [{
          visitor: {
            Identifier(path) {
              const isFunctionParam = path.scope.getBinding(paramName)?.kind === 'param';
              const isObjectProperty = path.parent.type === 'ObjectProperty' && path.parent.key === path.node;
              const isNestedFunction = path.scope.parent?.block?.type === 'FunctionExpression' ||
                path.scope.parent?.block?.type === 'ArrowFunctionExpression';
              const isSameScope = path.scope.block === topLevelPath.scope.block;
              if (path.node.name === paramName && isFunctionParam && !isObjectProperty && !isNestedFunction && isSameScope) {
                path.node.name = newName;
              }
            },
            FunctionDeclaration(path) {
              if (topLevelPath)
                return;
              topLevelPath = path;
              path.node.params.forEach(param => {
                if (param.name === paramName) {
                  param.name = newName;
                }
              });
            },
            FunctionExpression(path) {
              if (topLevelPath)
                return;
              topLevelPath = path;
              path.node.params.forEach(param => {
                if (param.name === paramName) {
                  param.name = newName;
                }
              });
            },
            ArrowFunctionExpression(path) {
              if (topLevelPath)
                return;
              topLevelPath = path;
              path.node.params.forEach(param => {
                if (param.name === paramName) {
                  param.name = newName;
                }
              });
            },
          },
        }],
      });
    
      return result.code;
    }
    const func = `function process(x, y) {
     const localVariable = x - 2;
     const nestedFunc = (x) => x * 2;
     return { z: x * y, x: 4 }; 
    }`;
    const modified = renameFunctionParam(func , 'x', 'a');
    console.log("Before:\n"+func);
    console.log("After:\n"+modified);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
    <script>
    let Babel;
    require(['https://unpkg.com/@babel/[email protected]/babel.min.js'], _Babel => {
      _Babel.transform(`() => {}`, {
        plugins: [
          function(_Babel) {
            return {
              visitor: {
                ArrowFunctionExpression(path) {
                  Babel = _Babel; // it contains the 'types' you may need them..
                },
              },
            };
          }],
      });
      onready();
    });
    </script>