See section /src/manipulating-ast-with-js/README.md#scope describing my experiences reproducing Tan Li Hau lessons in youtube video "Manipulating AST with JavaScript". Tan starts to talk about scope at 38:40
FunctionDeclaration(path) {
if (path.scope.hasBinding("n")) {
// ...
}
}
This will walk up the scope tree and check for that particular binding.
You can also check if a scope has its own binding:
FunctionDeclaration(path) {
if (path.scope.hasOwnBinding("n")) {
// ...
}
}
This will generate an identifier that doesn't collide with any locally defined variables.
FunctionDeclaration(path) {
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid" }
path.scope.generateUidIdentifier("uid");
// Node { type: "Identifier", name: "_uid2" }
}
The path.scope.push
method has the following signature:
scope.push({
id: t.identifier("myVar"),
init: t.numericLiteral(42),
kind: "const"
});
The method takes an object with the following properties:
id
: The identifier node representing the variable name. This is typically created usingt.identifier(name)
.init
: (Optional) The initial value of the variable. This should be an AST node representing the value, such ast.numericLiteral(42)
for the number42
.kind
: (Optional) The kind of variable declaration. This can be"var"
,"let"
, or"const"
. If omitted, it defaults to"var"
.
Here is an example of how you might use path.scope.push
within a Babel plugin to add a new constant variable to the current scope:
module.exports = function(babel) {
const { types: t } = babel;
return {
name: "add-variable-plugin",
visitor: {
FunctionDeclaration(path) {
path.scope.push({
id: t.identifier("newVar"),
init: t.numericLiteral(42),
kind: "const"
});
}
}
};
};
In this example:
- The plugin defines a visitor for
FunctionDeclaration
nodes. - When a function declaration is encountered, the plugin adds a new constant variable
newVar
with an initial value of42
to the scope of that function.
Given the input:
➜ babel-learning git:(main) ✗ cat src/scope/scopepush/input.js
function tutu(x) {
return newVar;
}
When we run Babel using this plugin we get:
➜ babel-learning git:(main) npx babel src/scope/scopepush/input.js --plugins=./src/scope/scopepush/scopepush.cjs
"use strict";
function tutu(x) {
const newVar = 42;
return newVar;
}
This example shows how to
- The
path.scope.generateUidIdentifierBasedOnNode(path.node.id)
generates a unique identifier based on the node id - The function declaration is converted to an expression
t.toExpression(path.node)
and - remove a function declaration from its current scope
path.remove()
- push it to the parent scope.
- the expression is pushed to the parent scope.
➜ src git:(main) cat scope/scopepush/scopeparentpush.cjs
module.exports = function(babel) {
const { types: t } = babel;
return {
name: "pushing-to-parent-plugin",
visitor: {
FunctionDeclaration(path) {
const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
let node = t.toExpression(path.node);
path.remove();
path.scope.parent.push({ id, init: node });
}
}
};
};
When we run Babel using this plugin and pipe the output to diff -y
we get:
➜ scopepush git:(main) ✗ npx babel square.js --plugins=./scopeparentpush.cjs | diff -y - square.js
var _square = function square(n) { | function square(n) {
return n * n; return n * n;
}; | }
\ No newline at end of file
Notice that since kind
was not specified in path.scope.parent.push({ id, init: node });
the variable is declared as var
.
FunctionDeclaration(path) {
path.scope.rename("n", "x");
}
- function square(n) {
- return n * n;
+ function square(x) {
+ return x * x;
}
Alternatively, you can rename a binding to a generated unique identifier:
FunctionDeclaration(path) {
path.scope.rename("n");
}
- function square(n) {
- return n * n;
+ function square(_n) {
+ return _n * _n;
}
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.
This can be confirmed by the code in example src/scope/referencepaths.mjs.
import { parse } from "@babel/parser";
import _traverse from "@babel/traverse";
const traverse = _traverse.default || _traverse;
/* Return the path of the first Identifier node in the AST of the code */
function getIdentifierPath(code) {
const ast = parse(code);
let nodePath;
traverse(ast, {
Identifier: function (path) {
nodePath = path;
path.stop(); // Stop traversing
},
});
return nodePath;
}
function testReferencePaths() { //0123456789012345678901234567890123456
const path = getIdentifierPath("function square(n) { return n * n}");
console.log(path.node.loc.start); // { line: 1, column: 9, index: 9 }
const referencePaths = path.context.scope.bindings.n.referencePaths;
console.log(referencePaths.length); // 2
console.log(referencePaths[0].node.loc.start) /* { line: 1, column: 28, index: 28, } */
console.log(referencePaths[1].node.loc.start) /* { line: 1, column: 32, index: 32, } */
}
testReferencePaths();
Notice that the array referencePaths
does not contain the declaration as a parameter
at column 26 of the binding n
.
See the question at Stack StackOverflow How do I traverse the scope of a Path in a babel plugin
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 decided to slightly modify the 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 = 2;
return f;
}
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.
In the following code, we simple traverse the usages of the binding localIdentifier
replacing the references to the parent node (the CallExpression
) 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'
) {
const localIdentifier = path.parent.id.name; // f
scope.bindings[localIdentifier].referencePaths.forEach(p => {
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-input.js --plugins=./example-scope-plugin.js
const f = require('foo-bar');
const result = 99 * 2;
let a = 99;
function h() {
let f = 2;
return f;
}
See examples in folder /src/scope/generator/-transform/.
We want to write a transformation so that a generator declaration function* xxx(...) { ...}
is hoisted to a constant declaration with the same name of the generator function. The constant must be initialized to a call to the function with name buildGenerator
with argument the bare function. const xxx = buildGenerator(function(...) { ... })
.
The transformed declaration must be hoisted at the top of the scope where the generator is.
For instance, given this input:
➜ src git:(main) ✗ cat scope/generator-transform/input-generator-declaration-local.js
function chuchu() {
function* add(a,b,c) { return a+b+c; }
add(2,3,4)
}
It has to be transformed to:
➜ generator-transform git:(main) ✗ npx babel input-generator-declaration-local.js --plugins=./generator-transform-plugin.cjs
function chuchu() {
const add = buildGenerator(function (a, b, c) {
return a + b + c;
});
add(2, 3, 4);
}
chuchu();
If the generator is in the global scope, it has to work also:
➜ generator-transform git:(main) ✗ cat input-generator-declaration-global.js
function* add(a, b, c) { return a + b + c; }
add(2, 3, 4)
chuchu();
Has to be transformed into:
➜ generator-transform git:(main) ✗ npx babel input-generator-declaration-global.js --plugins=./generator-transform-plugin.cjs
const add = buildGenerator(function (a, b, c) {
return a + b + c;
});
add(2, 3, 4);
chuchu();
Here is the plugin generator-transform-plugin.cjs:
/src/scope/generator-transform/generator-transform-plugin.cjs
module.exports = function (babel) {
const { types: t } = babel;
return {
name: "generator-transform",
visitor: {
FunctionDeclaration(path) {
if (path.get("generator").node) {
const functionName = path.get("id.name").node;
path.node.id = undefined;
path.node.generator = false; // avoid infinite loop
path.replaceWith(
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier(functionName),
t.callExpression(t.identifier("buildGenerator"), [
t.toExpression(path.node),
]),
),
]),
);
// hoist it
const node = path.node;
const currentScope = path.scope.path.node;
path.remove();
if (currentScope.body.body) {
currentScope.body.body.unshift(node);
} else {
currentScope.body.unshift(node);
}
}
},
},
}
}