Andrew Vassilyev
Posted on May 9, 2024
ESLint is a potent tool, but you cannot create a good plugin because the documentation doesn't explain how it works. For example, you want to do something with non-closured variables inside a function. It is not a trivial task. The first idea that came to my head was to find similar solutions in open source. The most popular is the react-hooks/exhaustive-deps
rule. But if you try to see the sources you meet a total horror. Let's try to understand how to avoid this complexity inside ESLint plugins.
Prelude
Let's open the official tutorial on how to create a plugin. It looks nice, and I suppose the authors of the React plugin saw this tutorial. Why am I thinking like that? Because they use a single attach to an ESLint traverser(LOL. line 1358)
It is a bad idea because the plugin tries to traverse the AST tree by itself. Let's try to use the ESLint traverse instead of creating our own.
This move allows you to simplify our code significantly. Why can we do what?
How does ESLint work?
The central meaning of the article is ESLint use DFS to traverse the AST tree. Why is it important?
Let's see a plugin structure:
create(context) {
return {'Selector': callback}
}
No. It is a bad view of the plugin structure. Let's add the meaningful markup to the code.
create(context) {
// Memory
return {'CSS-like selector': callback}
}
Let's remember this and look up to another place.
Babel and async code
Do you know how Babel transforms your async code for Internet Explorer? Link to babel playgroud
The source code was simple
async function foo() {
await 1;
}
Yes, Babel transforms our async code to the state-machine. And... structure is similar to our plugin structure. We have cases(CSS-Selectors) and a memory part.
Pseudo-async version of plugins
In a better world, the plugin would look like this:
async create(context) {
// do smth
await waitForSelector('Selector');
// do smth
await waitForSelector('Selector 2');
}
And we can transform this code to babel-like output:
create(context) {
let flag1 = false;
return {
'Selector': () => {flag1 = true},
'Selector:exit': () => {flag1 = false},
'Selector2': () => {if (!flag1) {return} /* do logic */}
}
}
You can use memory to save and toggle flags inside state-machine logic. Do you remember about BFS? Yes. We can use selectors to set am I inside block
flags to determine whether we should execute logic.
And I use it inside my plugin:
Memory
Traverse
How to use this info?
Let's try to implement the logic from react-hooks/exhaustive-deps
: we need to run the analysis inside the defined before functions. For instance, inside useCallback
.
You can write like the React team:
create(context) {
return {
`CallExpression`: traverseAndAnalyse,
};
}
But you can remember about ESLint traverse logic and memory place:
const functionNameFromConfig = 'useCallback';
create(context) {
let insideF = false;
function enterF() {insideF = true;}
function exitF() {insideF = false;}
function runAnalysis() {
if (!isnideF) {
return;
}
// analyse
}
return {
`CallExpression[callee.name="${functionNameFromConfig}"]`: enterF,
`CallExpression[callee.name="${functionNameFromConfig}"]:exit`: exitF,
`AnyExpression inside function`: runAnalysis,
};
}
Think async, but write code like Babel. Do not traverse AST by yourself. It is a significant simplification of the code.
Useful links
https://astexplorer.net - a helpful tool to see the AST version of your code
ESLint Selectors - the list of selectors
typescript parser playground. astexplorer analogue for typescript. It has an essential feature: ESQuery filter. It allows you to debug your selectors in real-time.
Posted on May 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.