While developing Lebab, major part of the work is detecting patterns in source code.
After having detected the part of Abstract Syntax Tree (AST) that interests us,
it’s usually fairly simple to apply a transformation on it.
Say, we’d like to transform CommonJS require() calls to ES6 import syntax.
Given the following code:
To detect this require() pattern, we used to write code like this:
That’s straightforward to write, but damn hard to read.
Even while looking at the AST above, it’s hard to tell whether it’s matching it correctly.
Pattern matching to the rescue
What if we could just use the AST that Esprima produces as a pattern to match against?
Then it would all come down to a fairly simple pattern-matching operation.
It shouldn’t be hard to write a function that does a deep comparison of two objects.
Actually we don’t even have to write one by ourselves.
The popular Lodash library provides a function _.matches that does exactly this.
We can give it our AST pattern and it creates a function that matches against it:
With all the conditional logic gone, it’s now straightforward to understand what this function matches.
Pattern matching extended
However _.matches() is too limited for our needs.
We’d also like to check that require() takes just one string argument,
and that’s not achievable by plain pattern matching.
What if we could write custom assertions inside our AST pattern:
Then we could extend the pattern matching with conditional logic where needed.
Turns out, that’s really easy to achieve as Lodash provides _.matches() extension mechanism.
We can use _.isMatchWith() to add custom comparison logic.
Here’s our custom matches() function:
Not only can we now write simple assertions like the one above,
but we can also compose it with further call to matches():
Depending your inclination, you might consider the above code really neat or really horrible.
But we can alternatively extract it to several functions:
Extracting data
With isRequire() implemented, we can easily detect the pattern and return new AST for ES6 import declaration:
But wait…
These node.declarations[0].init.arguments[0] expressions are just like the ones we tried to eliminate.
It’s not quite as bad as the initial matcher function,
but we’re partly repeating the AST that we already specified in the matcher function.
What if we could simply label the things we need to extract within our matcher function:
and instead of returning true, our matcher would return us an object with extracted values:
Turns out, it’s also fairly easy to implement.
Our extract() will simply produce a function that creates and object with a single match:
Within matches() we then combine all these singular matches into a single object:
Extracting extended
The nice thing about this implementation is that extract() can be composed
with further conditional logic, even with additional matches():
The above pattern is common enough to improve our extract() function
with an optional second parameter to specify the nested pattern:
The implementation of this is somewhat similar to matches().
This time we’ll need to combine extracted fields from nested matches:
In the end the full implementation of our import transform reads like so:
Happy days.
With this knowledge under your belt,
go implement your favorite ES5-to-ES6 transform and send pull request to Lebab.