JavaScript AST Manipulation for Fun and Profit

For many JavaScript developers, the code they write is the final product. But what if you could write code that understands and even rewrites other code? This isn't science fiction; it's the power of Abstract Syntax Trees (ASTs). By the end of this article, you'll understand what ASTs are, how to manipulate them, and how to build your own codemods to automate refactoring, and boost your productivity.

What is an Abstract Syntax Tree (AST)?

At its core, an AST is a tree representation of your code. Think of it like a diagram of a sentence, but for code. Each part of your code, from variables and functions to expressions and statements, is represented as a node in the tree. This tree structure provides a powerful way to analyze and manipulate code programmatically.

Let's take a simple example:

const sum = 1 + 2;

This line of code can be represented as the following AST:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "sum"
          },
          "init": {
            "type": "BinaryExpression",
            "left": {
              "type": "NumericLiteral",
              "value": 1
            },
            "operator": "+",
            "right": {
              "type": "NumericLiteral",
              "value": 2
            }
          }
        }
      ],
      "kind": "const"
    }
  ]
}

As you can see, the AST provides a detailed, structured representation of the code. This structure is what allows us to traverse and manipulate the code with precision.

To explore ASTs further, I highly recommend using AST Explorer. It's an online tool that lets you write code and see its corresponding AST in real-time. It's an invaluable resource for learning and experimenting with ASTs.

Prerequisites and Environment Setup

Before we dive into the world of AST manipulation, you'll need a few things:

  • Node.js and npm/yarn: If you don't have them installed, you can download them from the official Node.js website.
  • A code editor: I recommend using Visual Studio Code, but any code editor will do.

Once you have the prerequisites, create a new directory for our project and initialize a new Node.js project:

mkdir js-ast-manipulation
cd js-ast-manipulation
npm init -y

Next, we'll need to install the necessary Babel packages. Babel is a popular JavaScript compiler that provides a suite of tools for working with ASTs.

npm install --save-dev @babel/core @babel/parser @babel/traverse @babel/generator

Here's a breakdown of what each package does:

  • @babel/core: The main Babel package.
  • @babel/parser: Parses JavaScript code and generates an AST.
  • @babel/traverse: Provides a simple and powerful way to traverse and manipulate the AST.
  • @babel/generator: Generates JavaScript code from an AST.

Parsing: From Code to AST

The first step in any AST manipulation task is to parse the code and generate an AST. The @babel/parser package makes this easy.

Let's create a file named parser.js and add the following code:

const parser = require("@babel/parser");

const code = "const sum = 1 + 2;";

const ast = parser.parse(code);

console.log(JSON.stringify(ast, null, 2));

Now, run the script from your terminal:

node parser.js

You should see the same AST that we saw earlier. The @babel/parser package has taken our string of JavaScript code and transformed it into a detailed, structured tree that we can now traverse and manipulate.

Traversal: The Heart of AST Manipulation

Now that we have an AST, we can start to traverse it. Traversal is the process of visiting each node in the tree. This is where the @babel/traverse package comes in.

@babel/traverse uses the Visitor pattern to traverse the AST. The Visitor pattern is a design pattern that allows you to add new behavior to an existing object structure without modifying the structure itself. In our case, it allows us to define a set of "visitor" functions that will be called for each node in the AST.

Let's create a file named traverse.js and add the following code:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const code = "const sum = 1 + 2;";

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    console.log(`Entering: ${path.node.type}`);
  },
  exit(path) {
    console.log(`Exiting: ${path.node.type}`);
  }
});

When you run this script, you'll see a list of all the nodes in the AST, in the order that they are visited. The enter function is called when the traversal enters a node, and the exit function is called when it exits a node.

The path object that is passed to our visitor functions is a wrapper around each node that provides a wealth of information about the node, including its parent, its siblings, and its scope.

Manipulation: Modifying the AST

Now for the exciting part: manipulating the AST. With @babel/traverse, we can not only visit each node in the tree, but we can also modify them.

Let's say we want to rename the sum variable to total. We can do this by modifying the name property of the Identifier node.

Create a file named manipulate.js and add the following code:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;

const code = "const sum = 1 + 2;";

const ast = parser.parse(code);

traverse(ast, {
  Identifier(path) {
    if (path.node.name === "sum") {
      path.node.name = "total";
    }
  }
});

const output = generate(ast, code);
console.log(output.code);

When you run this script, you'll see the following output:

const total = 1 + 2;

We've successfully renamed the sum variable to total!

Generation: From AST back to Code

The final step in our AST manipulation journey is to generate human-readable code from our modified AST. The @babel/generator package makes this a breeze.

As you saw in the previous example, the generate function takes our modified AST and returns an object with a code property that contains the generated code.

The generate function also takes an optional options object that allows you to customize the output. For example, you can use the retainLines option to preserve the original line numbers, or the compact option to generate minified code.

Building a Practical Codemod: A Step-by-Step Guide

Now that we've covered the basics of AST manipulation, let's build a practical codemod that automatically replaces all instances of var with let or const.

1. Set up the project

Create a new file named codemod.js and add the following code:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;

const code = `
  var a = 1;
  var b = 2;
  b = 3;
`;

const ast = parser.parse(code);

// ...

2. Traverse the AST and replace var with let or const

Next, we'll traverse the AST and replace all VariableDeclaration nodes that have a kind of var with let or const. We'll use the scope property of the path object to determine if a variable is ever reassigned. If it is, we'll use let. Otherwise, we'll use const.

traverse(ast, {
  VariableDeclaration(path) {
    if (path.node.kind === "var") {
      const isReassigned = path.node.declarations.some(declaration => {
        const binding = path.scope.getBinding(declaration.id.name);
        return binding && binding.constantViolations.length > 0;
      });

      path.node.kind = isReassigned ? "let" : "const";
    }
  }
});

3. Generate the transformed code

Finally, we'll generate the transformed code from our modified AST:

const output = generate(ast, code);
console.log(output.code);

When you run this script, you'll see the following output:

const a = 1;
let b = 2;
b = 3;

Our codemod has successfully replaced var with let and const!

Conclusion

In this article, we've only scratched the surface of what's possible with AST manipulation. From automating complex refactoring to creating your own custom language features, the possibilities are endless.

You now have the knowledge and tools to start your own AST manipulation journey. I encourage you to experiment with the concepts we've covered today and to explore the wealth of resources available online.

Further Learning

Author

Efe Omoregie

Efe Omoregie

Software engineer with a passion for computer science, programming and cloud computing