Faster Refactoring with Comby
When was the last time you refactored code? Was it in the past week? The past day? Or even code you pushed five minutes ago? In this post I’ll show you how to perform large-scale refactors, regardless of the language, in just a few minutes.
A few weeks ago, I had to change literally hundreds of files due to a change in one of our shared libraries. I admit, it was tempting to only adjust the code I’m working on and leave everything else as it was. Then, I remembered a nifty tool I’d looked into a few months ago, and realised it was the perfect opportunity to try it out.
Code that rewrites your code
Did you know that you can write code that modifies your code? Tools like jscodeshift make refactoring with confidence possible. The way jscodeshift approaches this is by turning your code into an AST (abstract syntax tree). It also exposes a jQuery-like API to query and modify the AST, which gives you superpower when it comes to large-scale refactors.
Facebook uses codemods internally to upgrade their tens of thousands of React components automatically. In the past few years I wrote a couple of codemods, but let me tell you: getting your head around jscodeshift takes time and effort. This is where Comby comes in handy.
Comby — the better search and replace
Comby is a no brainer. It’s a very simple but very powerful parser with an elegant pattern matching API. You can think of it as a much better search and replace. It’s language agnostic, easy to learn, very quick to write and has saved me many hours already.
Let’s have a look at a really simple example, before jumping into my real-world problem. In the code below we want to flip the order of the params so we end up with
First, we have to define a match template. You can think of match templates as stencils where you define holes (e.g.
:[whatever]). These holes are then used to extract substrings from the code - in our stencil analogy you can imagine cutting out the contents of the holes and then moving them around as you see fit. Comby is language agnostic but understands the relation between delimiters, strings, and comments. Our match template consists of two holes
arg2, but you can name them however you want.
Now it’s time to define our rewrite template to make the flip of the params happen. In this case, it’s almost the same as our match template, but we swap the params order.
Running Comby will produce the following output:
Comby provides a CLI, but for the purpose of this blog post we focus on the core principles of Comby. I recommend to checkout out the online playground where you can create and test your rewrite patterns.
A real-world example
Let’s have a look at one of my recent refactoring tasks. The API in one of our shared libraries to perform a request changed from a single param string to an object. So we needed to go from this:
Let’s start with defining our match template again. As in the previous example, this match template consists of two holes:
arg1. The leading string
request. and the parentheses define our bounding boxes.
The rewrite template puts the holes into the new format, so we end up with this new function call:
This works, but there is a caveat: It doesn’t inspect the value of
:[method], which can also be
request.config. These methods shouldn’t be changed. Let’s improve our rewrite.
Comby allows you to define rules, to control which holes get modified, and which get skipped. A rule can be a simple string comparison (like below), or you can compare holes with other holes.
Running Comby again produces the desired output, where
request.config is preserved, but
request.post() are updated:
View this example in the online playground.
Now on to another recent challenge. This time I had to refactor HTML files. Buttons with a class of
primary and attribute of
disabled should get an additional CSS class of
shadow. A search and replace doesn’t work here, because the locations of the
class attribute and the
disabled attribute are not guaranteed.
In order to decouple the match template from a specific order whilst preserving the additional attributes and classes, I defined a few placeholder
holes. For simplicity I named them 1 to 5.
Our rewrite template is again almost identical to the match template.
That wasn’t too complicated right? Just imagine doing the same with a RegExp.
The attentive reader may have noticed that the order does still matter, as the
disabled attribute needs to be in front of the
class attribute. However, in my case, there were just a few places where this wasn’t the case so I fixed the order upfront by hand — keep it simple. I’m pretty sure there is a Comby way of doing this as well, but we don’t want to overcomplicate things here. Life is already too complicated enough these days.
We just scratched the surface of what we can do with Comby, but hopefully the concept we covered make sense. Check out the documentation to learn more about what Comby has to offer.
As developers, we do our best to produce high quality and robust code. Refactoring is a necessary and ongoing process to sustain a healthy code base. We’re hesitant to touch code outside of our comfort zone for various reasons. But this should not prevent us from doing so. Tools like Comby and jscodeshift can help you to make confident refactors.
Hopefully this post was helpful to make you refactor code with confidence. If this post helped you out in one of your projects, share your story with me — I’m @buckstefan on Twitter.