In part 1 of this series we did a lot of talking about what Codemods are and why you should (or shouldn't) write them. This time we're going to get our hands dirty and write one.
There's quite a bit to get through in this part: we're going to examine an AST and construct a complete - albeit simple - codemod, little-by-little.
Our Codemod
To keep this particular article at a reasonable length,
we'll develop a very simple codemod. It'll simply rename
a method in all JavaScript classes it sees. Specifically, it'll
rename componentWillUpdate to UNSAFE_componentWillUpdate.
(This change forms part of a Codemod for React but you don't need to know
anything about React to follow this tutorial.)
Writing a Codemod
I've found this set of steps handy when developing codemods. If you don't do these things you can easily develop a codemod that changes the wrong code or changes the right code in the wrong way. The TL;DR is "think before you code, and have some tests".
- Figure out what we want to change
- Ensure you understand precisely what structures you want the codemod to change, and precisely what you want it to change those things into.
- Figure out what we don't want to change
- Do you always want to make the change, or only when you're extending a particular class?
- Do you always want to make the change, or only when there aren't any parameters?
- Find structures similar to what you think you want to target and decide if you want those to change too
- Construct a 'test bed'
- Develop one or more 'test' files containing examples of positive (things you do want to change) and negative (things you don't want to change) sample code
- Build a target profile
- Identify the AST structures you want to change and construct
- Fire tracer bullets
- Write codemod code that targets the structure you're hoping to reach, and makes a tiny change there. Don't attempt to make the full final change straight away. Focus initially on targeting.
- You may need to evolve your testbed as you do this
- Apply changes
- Now that you're targeting the correct nodes, write codemod code that makes the desired changes
- Use the codemod
- And be sure to check the output for unexpected changes!
- Evaluate
- Was it easier/quicker/better than making the changes by hand?
Let's work through those steps for our codemod:
That's easy. We want to find all class methods called componentWillUpdate and rename them
to UNSAFE_componentWillUpdate. We don't need to change structure; we're just changing the
name of an identifier.
Let's think about things similar to what we want to change, to identify related things we
don't want to change.
A convenient way to do this is to take the thing we want to target and create variants of
it, changing one thing at a time:
- Our start point is that we want to change methods called
componentWillUpdate.
- We don't want to change methods that aren't called
componentWillUpdate.
- We don't want to change all functions that are called
componentWillUpdate; only class methods.
- We don't want to change
static class functions that are called componentWillUpdate; only instance methods.
- We only want to change the name of the
componentWillUpdate method. None of its contents should change.
- Do we always want to change methods called
componentWillUpdate? If we were sharing
this codemod publicly and therefore wanted to be super-careful, we should probably only change that method
if it's on a class extending React's Component class, but let's just assume for the sake
of this tutorial that we don't need to worry about that because we happen to know there are no uses
of componentWillUpdate outside of our React components.
3. Construct a testbed
We now write some sample code to illustrate all the above positive and negative matches:
class Class1 {
componentWillUpdate() {
return false;
}
notComponentWillUpdate() {
}
}
function componentWillUpdate() {
}
class Class2 {
static componentWillUpdate() {
}
}
As we develop our codemod we'll run it against the above code to gain confidence that it's changing the correct pieces of code.
4. Build a target profile
We need to identify the AST pattern we want to target. A particularly easy
way to do this is to use the excellent AST Explorer
web site to explore the AST.
JSCodeShift uses the recast library, so make sure that's the selected option.
Here is a link to AST Explorer with
the above code already entered, and the
correct options selected: https://astexplorer.net/#/gist/4ba8eac986f73baf39c5c9683af03b93/10917053e0e1b9b41cc3dd9b806f0cec69d9ac00
Go take a look at that. I'll wait. Make sure you click around the sample
code (on the left) and examine the appropriate parts of the syntax tree (on the right). Look to see what sorts of AST nodes we're going to be interested in.
After a little clicking around we can see that we're interested in MethodDefinition nodes whose key is an Identifier with a name of componentWillUpdate.
Aside
One area of difficulty when working with these ASTs is that different parsers produce slightly different AST structures. The esprima parser generates MethodDefinition nodes whereas the babylon7 parser calls them ClassMethods.
For this reason, it's vital to ensure you're using the exact same parser configuration in AST Explorer that you'll be using when running your codemod.
Note in particular that recast is merely a layer on top of a real parser, so when we use recast in AST Explorer, it's important to choose the appropriate parser from the recast settings.
5. Fire tracer bullets
This is where things get interesting.
At this point, we're actually going to create and run a real codemod. The neat thing is that we can do it directly inside AST Explorer.
Click the 'Transform' button in AST Explorer and select 'JSCodeShift'. The two-window display will change into a four-window display. The two new panes at the bottom are the codemod source (bottom-left) and the resulting output (bottom-right).
AST Explorer gives us a sample codemod that reverses all of the identifiers in the source.
That sample is a little more complex than we need, so replace the codemod code with this simpler skeleton:
1. export default function transformer(file, api) {
2. const j = api.jscodeshift;
3.
4. return j(file.source)
5. .find(j.Identifier)
6. .forEach(path => path.node.name = 'Foo')
7. .toSource();
8. }
It's only 8 lines long but it is a complete codemod! Let's walk through it, line-by-line:
- 1: The codemod consists of a single exported function called with the current file and the JSCodeShift API.
- 2: We extract the
jscodeshift helper from the API and call it j as we will be using it a lot.
- 4: Here we begin a multi-line chained operation passing objects from one function to another: we start by parsing the current file. JSCodeShift will return a wrapper around the file that allows us to perform sophisticated search and replace operations.
- 5: We search for anything of type
Identifier. Note the uppercase I in j.Identifier: this references the type Identifier.
- 6: We tweak each (Identifier) node, changing its
name to Foo. We'll cover the path business in detail in the next article, so for now, just focus on the fact that we can write path.node to get the node in question.
- 7: We regenerate the output code from our modified tree by calling
.toSource().
If you look at the output pane you'll see that our code has been heavily transformed to:
class Foo {
Foo() {
return false;
}
Foo() {
}
}
function Foo() {
}
class Foo {
static Foo() {
}
}
Every Identifier - every mention of x, somefunc, Array and NotArray - has been changed into Foo, as we said it should in the code.
That's not (of course) what we want from our final codemod, but it serves to show how powerful codemods are, and it
provides a good basis for developing our codemod.
It'll require remarkably few changes to turn that into our final codemod!
Target the correct thing
Firstly, we don't want to target all Identifiers.
From our AST investigations earlier, we know we want to start by finding all the MethodDefinitions.
Our tracer bullet will simply be to set the name of the method to Foo. That's achieved by setting the .name property of
the method's .key property.
i.e.
1. export default function transformer(file, api) {
2. const j = api.jscodeshift;
3.
4. return j(file.source)
5. .find(j.MethodDefinition)
6. .forEach(path => path.node.key.name = 'Foo')
7. .toSource();
8. }
The output:
class Class1 {
Foo() {
return false;
}
Foo() {
}
}
function componentWillUpdate() {
}
class Class2 {
static Foo() {
}
}
We're now renaming all methods on classes, and we're not renaming the classes or functions.
But we're still hitting methods we don't want: we're renaming methods whose name is not componentWillUpdate and we're renaming static methods.
Happily, the .find() method takes a second parameter that supports property-based filtering.
We only want to target:
- methods named
componentWillUpdate. We're looking for methods whose key property is an object with a name property equal to componentWillUpdate.
- methods which are not
static. We're looking for methods whose static property is false.
We adjust our .find() call as follows:
1. export default function transformer(file, api) {
2. const j = api.jscodeshift;
3.
4. return j(file.source)
5. .find(j.MethodDefinition, {
6. key: { name: 'componentWillUpdate' },
7. static: false
8. })
9. .forEach(path => (path.node.key.name = "Foo"))
10. .toSource();
11. }
which produces the transformed output:
class Class1 {
Foo() {
return false;
}
notComponentWillUpdate() {
}
}
function componentWillUpdate() {
}
class Class2 {
static componentWillUpdate() {
}
}
That's perfect. We're now targeting exactly the one method we want
to, and leaving everything else alone.
6. Apply changes
For this codemod, this bit's easy. We don't need any information
from the existing node. We just want to set its name to UNSAFE_componentWillUpdate, which merely requires us to change
from setting Foo to UNSAFE_componentWillUpdate:
1. export default function transformer(file, api) {
2. const j = api.jscodeshift;
3.
4. return j(file.source)
5. .find(j.MethodDefinition, {
6. key: { name: 'componentWillUpdate' },
7. static: false
8. })
9. .forEach(path => (path.node.key.name = "UNSAFE_componentWillUpdate"))
10. .toSource();
11. }
So the output is now:
class Class1 {
UNSAFE_componentWillUpdate() {
return false;
}
notComponentWillUpdate() {
}
}
function componentWillUpdate() {
}
class Class2 {
static componentWillUpdate() {
}
}
That looks perfect!
7. Use the codemod
At this point, we'd run it across our codebase and carefully check
the output. We've done some due diligence so we're hoping it'll work
well, but there are often more edge cases that we've missed.
8. Evaluate
This isn't a fabulous codemod. It only replaces one method name,
and it doesn't rename existing references to that method.
And to be honest, with what it actually does, we could probably have
achieved the same thing with a regular-expression-based search
and replace.
If this was a codemod we'd be sharing with others, it would be
worth turning our sample code into a repeatable unit test, but we won't
cover that here.
Of course, our main objective here was to illustrate how to create
a simple codemod, and it hopefully served that purpose. Let me know?
Summary
We've covered how to use JSCodeShift to create a simple codemod
from scratch, but, as importantly, we've covered a
process for developing more reliable codemods.
Have fun developing some codemods, and let me know how you get on!
In part 3, we'll cover a more advanced codemod which involves moving
existing code around.