Reputation: 11812
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
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
Reputation: 640
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
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
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