Skip to main content

Writing Codemods Like a Pro

Prerequisites

  • Understanding ASTs — This article requires a basic understanding of how to inspect, traverse, and manipulate Abstract Syntax Trees. Codemod Studio has a built-in AST tree explorer that's great for visualizing node structures interactively.
  • Codemod CLI installed — You'll need the Codemod CLI available via npx codemod. See the CLI docs if you haven't set it up yet.

Overview

Throughout this guide, we'll break down the thought process behind writing a real-world codemod—from identifying patterns and planning edge cases to implementing and testing the transform.

By the end, you will learn:

  • How to write a codemod that solves a real-world problem using JSSG, Codemod's transformation engine.
  • How to use ast-grep pattern matching and AST manipulation techniques.
  • How to test and publish your codemod using the Codemod CLI.

Let's learn by example!

Problem

Before ES6, JavaScript codebases relied heavily on var for variable declarations. Due to var's scoping issues, let and const were introduced—but many codebases still haven't migrated.

Refactoring manually is tedious and error-prone. Simple find-and-replace won't work either, because there are edge cases where blindly swapping var for const would break your code.

This is a perfect job for a codemod. We'll build a no-vars transform that automatically converts var declarations to const or let wherever it's safe to do so.

Before:

After:

Planning Our Codemod

If you're new to codemod development, you might think this is as simple as: find all var declarations and replace them with const. But that would break your code in most cases.

Let's consider this snippet that covers several real-world patterns:

This covers the following cases:

  • var is declared and never mutated
  • var is declared and mutated
  • var is declared as a loop index
  • var is declared inside a loop

Now, take a moment—which of these would break if we just replaced var with const?

Here's a summary of the patterns and their safe transforms:

#1 var is a loop index declaration

The index is mutated (i++), so it must become let.

#2 var is a mutated variable

Reassigned variables must become let.

#3 var is in a loop and mutated

#4 Global or local non-mutated var

Safe to become const x = "foo".

#5 var is declared twice

Keep as `var`. Converting to let or const would cause a SyntaxError for duplicate declarations in the same scope.

#6 var is hoisted

Keep as `var`. The variable is used before its declaration, relying on var's hoisting behavior.

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

Keep as `var`. Converting to let/const would change the closure behavior since let creates a new binding per iteration.

Test Cases

Now that we have a concrete list of patterns, let's prepare test fixtures. We'll use the Codemod CLI's built-in testing framework.

Test input (tests/no-vars/input.js):

Expected output (tests/no-vars/expected.js):

With this plan in mind, let's build the codemod.

Developing the Codemod

Scaffolding the Project

Start by scaffolding a new JSSG codemod package:

Pick JavaScript ast-grep (JSSG) codemod when prompted. This gives you:

Now open scripts/codemod.ts—this is where we'll write our transform.

Understanding the Transform Signature

Every JSSG codemod exports a default transform function:

The function receives a parsed AST (root) and returns either a string (modified source code) or null (no changes).

Step 1: Detect Code Patterns

Our detection strategy:

  1. Find all var declarations broadly
  2. Filter out declarations that must stay as var (hoisted, duplicated, closure-referenced)
  3. Classify the remaining ones as let or const

1.1 Finding All var Declarations

We use ast-grep's pattern syntax to match all var declarations structurally. The pattern var $DECL matches any variable_declaration node whose first token is the var keyword — no text filtering needed:

Tip

Use Codemod Studio to explore AST node kinds for your target patterns. Paste sample code and use the built-in AST tree explorer to see the exact tree structure.

1.2 Filtering Out Non-Transformable Cases

Now we need to identify var declarations that must stay as var. We'll write helper functions for each unsafe case.

Check if a variable is declared twice in the same scope:

Check if a variable is used before its declaration (hoisting):

Check if a variable is inside a loop that contains closures:

When a loop contains closures (nested functions), converting var to let/const changes semantics because let creates a new binding per iteration. To be safe, we keep all var declarations as-is when the enclosing loop contains any closures.

Helper to find the enclosing scope:

Step 2: Classify and Transform

Now we classify the remaining var declarations as either let or const:

  • If the variable is mutated (reassigned or updated), use let
  • If the variable is a for-loop initializer (e.g., for (var i = ...)), use let
  • Otherwise, use const

Check if a variable is mutated:

Check if it's a for-loop initializer:

Putting It All Together

Here's the complete transform:

AST Quirk

In tree-sitter's JavaScript grammar, for (var x of items) does not wrap var x in a variable_declaration node like a normal var statement would. Instead, var and the identifier are bare children of the for_in_statement. We handle this by matching structurally with for (var $X of $Y) $BODY and replacing only the keyword token at child(2) — no regex, no full-node text replacement. This is the kind of detail you'll discover when exploring AST structures in Codemod Studio—always verify your assumptions against the actual tree!

Note: When writing codemods, always consider having fallbacks for undesirable cases. Here we chose let as a fallback when const is not applicable, rather than keeping var, since let is arguably the better default.

Testing the Codemod

Set up your test fixtures under the tests/ directory:

Then run the tests:

Use the --verbose flag for detailed output when debugging:

And if you've intentionally changed behavior, update the snapshots:

Publishing to the Registry

Once your tests pass, you can publish your codemod to the Codemod Registry so others can use it:

Anyone can then run your codemod with:

See the publishing guide for setting up CI/CD with trusted publishers.

Wrapping Up

After applying this transform, we successfully convert var declarations to const or let wherever it's safe—handling edge cases like hoisting, duplicate declarations, and closure references in loops.

Before:

After:

Takeaways

  • Identify patterns methodically. Do a thorough code search and capture as many possible patterns as you can before writing a single line of transform code.
  • Test before you transform. Create test fixtures using the captured patterns—include both cases that should and should not be transformed.
  • Use JSSG and ast-grep patterns. JSSG's pattern matching makes it easy to express complex structural queries without manually walking the AST.
  • Publish and share. Once your codemod is tested, publish it to the Codemod Registry so your team (or the community) can run it with a single command.

Next Steps

  • JSSG Quickstart — Build your first JSSG codemod in minutes.
  • JSSG API Reference — Full reference for node navigation, editing, and pattern matching.
  • Testing Guide — Learn about snapshot testing, strictness levels, and CI integration.
  • Codemod Studio — Generate and test codemods visually with AI assistance.

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.

Writing Codemods Like a Pro | Codemod Blog