8 min read

Prerequisites

  • Understanding ASTs - This article requires a basic understanding of how to inspect, traverse, and manipulate ASTs.
  • Basic Understanding of Codemods - This article requires a basic understanding of writing simple codemods. If you're unfamiliar with writing codemods, check out our tutorial on writing your first codemod.

Overview

Throughout this document, we will break down some of the thought process a codemod guru, like Christoph Nakazawa, uses to make useful codemods.

By the end of this tutorial, you will learn:

  • How to write a codemod that solves a real-world problem.
  • Usage of more advanced AST manipulation techniques.
  • New methods and tools that make your codemod development more efficient.

Let's learn by example together!

Problem

Before the emergence of ES6, JS codebases heavily relied on var for variable declarations.

Due to the issues with var's scope issues, let and const declarations were introduced to put an end to the shortcomings of var.

However, even after the introduction of let and const, there are still a lot of codebases that haven't been migrated to use the new declaration types. Refactoring those codebases can be a tedious process.

To add, resorting to search-and-replace methods aren't applicable in this scenario, as there are edge cases (which we discuss below) that aren't possible to cover with mere find-and-replace operations.

In this example, we will take a look at the codemod no-vars.

The no-vars codemod has been developed to automatically refactor codebases to use const and let declarations instead of var wherever possible.

Before change:

1var exampleVariable = "hello world";

After change:

1const exampleVariable = "hello world";

Planning Our Codemod

If you are new to codemod development, you might get tricked into thinking that developing a transformation for this scenario is simpler than it is.

One might think that it is safe to simply:

  • Find all variable declarations.
  • Filter by variable declarations where kind is var.
  • Replace found declarations with const.

However, this would probably lead to breaking your code in most cases.

Let’s consider this sample code snippet which covers some of the possible cases of how a var might be declared.

1var notMutatedVar = "definitely not mutated";
2var mutatedVar = "yep, i'm mutated";
3
4for (var i = 0; i < 5; i++) {
5 mutatedVar = "foo";
6 var anotherInsideLoopVar = "should i be changed?";
7}
8
9for (var x of text) {
10 text += x + " ";
11}

As we can see, this code covers the following cases:

  • var is declared and not mutated
  • var is declared and mutated
  • var is declared and initialized as a loop index
  • var is declared inside a loop

Such cases can also be referred to as patterns.

Now, take a moment to try to find out which cases would break the code if we were to transform var into const.

Let’s see if you could point them all out. Here’s a brief summary of different occurring patterns and the corresponding safe transform we can apply for each one:

  1. var is a loop index declaration
  2. var is a mutated variable
  3. var is in a loop and mutated
  4. Global or local non-mutated var
  5. var is declared twice
  6. var is hoisted
  7. var is declared in a loop and referenced inside a closure

#1 var is a loop index declaration

Before change:

1for (var i = 0; i < 5; i++)

After change:

1for (let i = 0; i < 5; i++)

#2 var is a mutated variable

Before change:

1var x = 1;
2x=1;

After change:

1let x = 1;
2x = 2;

#3 var is in a loop and mutated

Before change:

1for (var i = 0; i < 5; i++) {
2 var x = "foo";
3 x = “bar”;
4}

After change:

1for (let i = 0; i < 5; i++) {
2 let x = "foo";
3 x = “bar”;
4}

#4 Global or local non-mutated var

Stays as:

1var x = “foo”;

#5 var is declared twice

Stays as:

1var x;
2var x;

#6 var is hoisted

Stays as:

1x = 5;
2var x;

#7 var is declared in a loop and referenced inside a closure

Stays as:

1for (var i = 0; i<5; i++){
2 var a = "hello";
3 function myFunction() {
4 a = "world";
5 return a;
6 }
7}

Now that we have a concrete list of possible patterns and their corresponding suitable actions, let's prepare a test case to validate if the codemod we write successfully satisfies our plan.

Before State

1var notMutatedVar = "definitely not mutated";
2var mutatedVar = "yep, i'm mutated";
3
4for (var i = 0; i < 5; i++) {
5 mutatedVar = "foo";
6 var anotherInsideLoopVar = "should i be changed?";
7}
8
9for (var x of text) {
10 text += x + " ";
11}

After State

1const notMutatedVar = "definitely not mutated";
2let mutatedVar = "yep, i'm mutated";
3
4for (let i = 0; i < 5; i++) {
5 mutatedVar = "foo";
6 const anotherInsideLoopVar = "should i be changed?";
7}
8
9for (const x of text) {
10 text += x + " ";
11}

We can verify if our codemod is correct if it can transform the previous “Before” state of the code to the “After” state illustrated above.

With this plan in mind, let's take a look at a step-by-step process of how the codemod pro Christoph Nakazawa puts it into action in his no-vars transform.

Developing the Codemod

Now that we’ve done the prep work in the previous section, we can confidently start writing our codemod.

Our workflow for writing the codemod will be as follows:

  1. Detect code patterns
  2. Transform patterns

To get started with writing our codemod, let's start by opening up ASTExplorer.

To follow along, set your transform setting to jscodeshift. Your parser setting should then automatically change to recast.

Now that your environment is set up, let's start following our workflow.

#1 Detect Code Patterns

To detect our target code patterns we will:

  1. Find all nodes that conform to a broad rule which encompasses all code patterns. This includes both, patterns that should and should not be detected.
  2. Then, we extract (filter) the nodes we want to modify.

1.1 Finding Nodes

In our case, we can start first by finding all variable declarations. To do this, we can insert a simple var declaration snippet into ASTExplorer, and use the tree explorer to find the structure name for variable declarations.

Now we know that we can find all variable declarations by finding j.VariableDeclaration instances as shown below.

1const updatedAnything = root.find(j.VariableDeclaration)

TipIt’s good practice to leverage tools like AST Explorer and TS AST Viewer within your workflow to efficiently analyze ASTs of your identified code patterns.
1.2 Extract The Nodes To Be Modified

Now that we’ve captured all variable declarations, we can now start filtering them based on the patterns we’ve identified while planning our codemod.

In the previous step, we targeted all variable declarations, which include var, let, and const declarations. So, let’s first start filtering for var declarations only.

We do this by using JSCodeshift’s filter method as shown below:

1const updatedAnything = root.find(j.VariableDeclaration).filter(
2 dec => dec.value.kind === 'var' // getting all var declarations
3 )

Now that we’re targeting only var declarations, let’s rule out all var declarations that we cannot transform into let or const.

To do so, we will call a second filter which calls the custom helper function isTruelyVar. This filter checks for every var if it conforms to any of such cases:

  • var is declared in a loop and referenced inside a closure
  • var is declared twice
  • var is hoisted

If any of those cases occur, we refrain from transforming the var declaration at all. Rather, we fall back to a var declaration.

1.filter(declaration => {
2 return declaration.value.declarations.every(declarator => {
3 // checking if the var is inside a closure
4 // or declared twice or is a function declaration that might be hoisted
5 return !isTruelyVar(declaration, declarator);
6 });

After ruling out all non-transformable var occurrences, we can now apply the final filter to determine whether the remaining var occurrences will be transformed into let or const.

To do so, for each var inside a loop, we check if:

  • The var is declared as the iterable object of a For...of/in loop
  • If a variable is mutated inside the loop.
1.forEach(declaration => {
2 // True if parent path is either a For...of or in loop
3 const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
4 if (
5 declaration.value.declarations.some(declarator => {
6 // If declarator is not initialized and parent loop is initialized
7 // or
8 // If var is mutated
9 return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
10 })
11 )

This filter allows us to pinpoint 2 possible cases:

  1. The variable is mutated
  2. The variable is not initialized and the parent loop is a For...of/in loop

#2 Transforming the Nodes

With the 2 possible cases that we’ve identified, now we can determine whether we will transform the var into either a let or const declaration

In the case of the occurrence of either case (1) or (2), we resort to replacing var with let.

Otherwise, we can safely replace var with const.

1.forEach(declaration => {
2 // True if parent path is either a For...of or in loop
3const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
4if (
5 declaration.value.declarations.some(declarator => {
6 // If declarator is not initialized and parent loop is initialized
7 // or
8 // If var is mutated
9return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
10 })
11 ) {
12 // In either one of the previous cases, we fall back to using let instead of const
13 declaration.value.kind = 'let';
14 }else {
15 // Else, var is safe to be converted to const
16 declaration.value.kind = 'const';
17 }
18 }).size() !== 0;
19return updatedAnything ? root.toSource() :null; //replacing the source AST with the manipulated AST after applying our transforms

NoteNote here that while writing codemods, we should always consider having fallbacks for undesirable cases. The codemod developer here chose let as a fallback when const is not applicable, rather than keeping the declaration as var, as the use of let is arguably better.

Finally ending up with the following transform:

1const updatedAnything = root.find(j.VariableDeclaration).filter(
2 dec => dec.value.kind === 'var'
3 ).filter(declaration => {
4 return declaration.value.declarations.every(declarator => {
5 return !isTruelyVar(declaration, declarator);
6 });
7 }).forEach(declaration => {
8 const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
9 if (
10 declaration.value.declarations.some(declarator => {
11 return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
12 })
13 ) {
14 declaration.value.kind = 'let';
15 } else {
16 declaration.value.kind = 'const';
17 }
18 }).size() !== 0;
19 return updatedAnything ? root.toSource() : null;

Wrapping Up

After applying this transform, we successfully get our desired code output.

1const notMutatedVar = "definitely not mutated";
2let mutatedVar = "yep, i'm mutated";
3
4for (let i = 0; i < 5; i++) {
5 mutatedVar = "foo";
6 const anotherInsideLoopVar = "should i be changed?";
7}
8
9for (const x of text) {
10 text += x + " ";
11}

Takeaways

  • Do a code search and methodically find and capture as many possible code patterns as possible.
  • Create a test file using the captured code patterns. Use the code patterns as a reference for writing your test cases, which include both, patterns that should and should not be detected.
  • Write and test your codemods with the test file you created before.

Start migrating in seconds

Save days of manual work by running automation recipes to automate framework upgrades, right from your CLI, IDE or web app.

Get started