Mutating an AST
Transforming code in place by mutating an AST
The script from the previous module read files from the codebase, traversed the source code within those files to find the relevant code (nodes in an AST), and logged that data. With the data, we were able to define and build a component. Now that we have the component, it's time to go through and replace all the
button elements with our new
There are a few ways this could be approached:
Manually: go through all the files, look for
buttonelements, and replace them with the
Buttoncomponent. This is good for complex changes, but only works well in smaller codebases.
Find/Replace: use a tool that can find then replace pieces of code based on text or a regular expression. This is reasonable for simple cases, and works well in large codebases.
ASTs: use AST-based tooling. This is ideal for larger codebases and can also handle complex transformations.
All approaches are valid. The scope and complexity of the change can help determine which is the most efficient.
The button transformation needed here is complex. For example, the
className string needs to be broken up into multiple props, but those props are only required if they're not the same as the default. Since the sample codebase only contains a handful of buttons, it would be realistic to manually replace them all. However, we're going to use AST-based tooling to understand how it can be applied to transform code.
Again, let's start by creating a new sibling directory to the Flash app, so initialize a new
package.json, and add the necessary dependencies.
Some of these packages were used in the previous module (
typescript). The new babel packages (
@babel/generator) will be covered shortly. For more details on the type packages, see the sidenote on typings at the end.
Creating a transform script#
Let's continue by creating a new
transform.ts file that is mostly a copy of the previous
audit.ts script, but with all the logging removed. This logging was only necessary for performing the initial audit.
Running this script won't do or log anything, but it retains the same logic for finding the
button elements. Currently, the node's name (
button, but now we need to change it to our
Button component. The first step is to reassign the name.
After updating the node's name, the tree can be turned back into source code using the
generate method. This accepts an AST as the input, and will return a string of code that is equivalent to the AST. Finally, the script will overwrite the existing file with the new code string.
Running this should now make a transformation in the Flash app codebase.
Look at a file that contained a
button element that also had the
button class name. For example, the login component (
src/components/LogIn/LogIn.js). The opening element should now be an uppercase
You've created your first codemod! 🎉
The script converted the code into an AST, changed the AST, and then converted the mutated AST back into code.
However, there are still a number of issues:
Only the opening element was renamed so the closing element is still lowercase
The props are no longer correct since
classNameis no longer an option, and many have defaults that aren't required. Recall from implementing the
Buttoncomponent that using the
blockprops instead was done for ease of maintenance and styling consistency.
Buttoncomponent isn't imported.
The formatting changed in some of the files.
Let's tackle each of these problems individually. The first three are requirements. The last is optional since the code is still correct, but formatting is lost.
Renaming closing elements#
Before updating the script, take a moment to paste the "After" code from above into AST Explorer and explore the tree. Can you see what the problem is?
JSXOpeningElement node is referenced by the
openingElement property of a
JSXElement also has a
closingElement property that references a
The problem here is only the
name was changed, and not the
name. The script needs to be updated to also rename the closing element. However, the script is currently only traversing
JSXOpeningElement nodes. For the code audit, all the data that we cared about was on this node.
It is possible to reference parent nodes from the path object. Rather, instead of traversing up and down the tree, it's usually easiest to target the highest node in the tree, while also trying to be as specific (low) as possible in the tree. A more technical definition would be to target the closest shared ancestor of the two nodes.
For example, this script needs to modify both the opening and closing elements. They are both properties on the
JSXElement node. In other words, they are both child nodes of the
In this case, the
JSXElement node is the closest shared ancestor and the parent of both nodes we need to transform. This is not always the case. The closest shared ancestor could be anywhere higher in the tree and often is not the parent node. For example, the closed shared ancestor node could be the grandparent of some nodes and the parent of others.
This is a general guideline that is usually a good starting point. The node to target depends on the exact case. For complex transformations, it may even require targeting multiple nodes.
Now the script can be updated to target the
JSXElement node, and update both the opening and closing element names.