m90
m90

Reputation: 11812

How do I traverse the scope of a Path in a babel plugin

I'm trying to write a simple babel plugin, but I am having a hard time traversing a matched node with a nested visitor. I would like to find all require calls in a module that require a certain module and then apply some transformation in the same scope.

To illustrate this with a contrived example I'd like to transform source code like:

const f = require('foo-bar');
const result = f() * 2;

into something like:

const result = 99 * 2; // as i "know" that calling f will always return 99

I was trying to do the following:

module.exports = ({ types: t }) => ({
    visitor: {
        CallExpression(path) {
            if (path.node.callee.name === 'require'
                && path.node.arguments.length === 1
                && t.isStringLiteral(p.node.arguments[0])
                && path.node.arguments[0].value === 'foo-bar'
            ) {
                const localIdentifier = path.parent.id.name;
                // if i print here it will show me that it successfully
                // found all require calls
                p.scope.traverse({
                    Identifier(subp) {
                        // this will never run at all
                        if (subp.name === localIdentifier) {
                            console.log('MATCH!');
                        }
                    }
                });
            }
        }
    }
});

Is my approach flawed or is there something I need to do differently from a code perspective?

Upvotes: 6

Views: 3879

Answers (4)

Casiano
Casiano

Reputation: 491

I decided to slightly modify your input example to have at least two scopes:

➜  manipulating-ast-with-js git:(main) cat example-scope-input.js 
const f = require('foo-bar');
const result = f() * 2;
let a = f();
function h() {
  let f = x => x;
  return f(3);
}

The key point is that during a traversing, the path.scope.bindings object contains all the bindings in the current scope. The bindings are stored in an object where the keys are the names of the bindings and the values are objects with information about the binding. The referencePaths property of the binding object is an array of paths that reference the usages of the binding (See the explanation at "referencePaths of a binding"). In the following code, we simple traverse the usages of the binding localIdentifier replacing the references to the parent node (the CallExpression node) with a NumericLiteral(99):

➜  manipulating-ast-with-js git:(main) ✗ cat example-scope-plugin.js 
module.exports = ({ types: t }) => {
  return {
    visitor: {
      CallExpression(path) {
        const { scope, node } = path;
        if (!(node.callee.name === 'require'       && 
              node.arguments.length === 1          && 
              t.isStringLiteral(node.arguments[0]) && 
              node.arguments[0].value === 'foo-bar')) return; 
          const localIdentifier = path.parent.id.name; // f
          for (const p of scope.bindings[localIdentifier].referencePaths) { /* Same as scope.getBinding(localIdentifier) */
            if (!(p.parentPath.isCallExpression()  && 
                  p.parent.callee === p.node)) continue; 
            p.parentPath.replaceWith(t.NumericLiteral(99));
          };   
      }
    }
  }
};

When we run Babel using this plugin we get:

➜  manipulating-ast-with-js git:(main) ✗ npx babel example-scope-const f = require('foo-bar');
const result = 99 * 2;
let a = 99;
function h() {
  let f = x => x;
  return f(3);
}

Upvotes: 0

Junior Tour
Junior Tour

Reputation: 640

Traverse the parent scope, search for node:

function inScope(scope, nodeName) {
  if (!scope || !scope.bindings) return false

  let ret = false
  let cur = scope
  while (cur) {
    ret = cur.bindings[nodeName]
    if (cur === scope.parent || ret) {
      break
    }
    cur = scope.parent
  }

  return ret
}


// your visitor.js
  return {
    visitor: {
      CallExpression(path) {
        inScope(path.scope, path.node.name)
      }
  }

Upvotes: 0

macabeus
macabeus

Reputation: 4562

I know that this question is very old, but this answer could be useful for someone that arrived here by Google. You could use a traverse inside of another traverse using node.scope.traverse, for example, if you want to change each CallExpression only if inside at the body of try:

module.exports = ({ types: t }) => ({
  visitor: {
    TryStatement(path) {
      const { scope, node } = path

      const traversalHandler = {
        CallExpression(path) {
          path.replaceWith(t.Identifier('foo'))
        }
      }

      scope.traverse(node, traversalHandler, this)
    }
  }
})

Upvotes: 9

Souradeep Nanda
Souradeep Nanda

Reputation: 3278

I cant seem to find much documentation on path.scope.traverse.

It has been almost 2 years, but I hope this solves your problem.

module.exports = ({ types: t }) => ({
    visitor: {
        CallExpression(path) {
            if (path.node.callee.name === 'require'
                && path.node.arguments.length === 1
                && t.isStringLiteral(path.node.arguments[0])
                && path.node.arguments[0].value === 'foo-bar'
            ) {
                this.localIdentifier = path.parent.id.name;
            }
            if(path.node.callee.name === this.localIdentifier){
                path.replaceWith(t.NumericLiteral(99))
            }
        }
    }
});

Upvotes: 0

Related Questions