15
Creating a simple ESLint plugin to glance at AST
In JavaScript world, ESLint is the most popular linter.
In this article, I’ll explain how ESLint analyzes our code by creating an ESLint plugin.
You might already know how it works.
But first, let me show you how ESLint works.
Let’s say we have a project like this. And ESLint has been installed via npm.
├──.eslintrc.json // ESLint configuration
├── node_modules
├── package-lock.json
├── package.json
└── src
└── index.js
You can use ESLint to src/index.js
like this
npx eslint src
As the result, an error was shown.
/Users/xxx/foo/src/index.js
1:7 error 'foo' is assigned a value but never used no-unused-vars
This is the code inside the src/index.js
.
const foo = () => "baz"
foo
is defined but it’s not called from anywhere. That’s why the error happened.
This is the basic usage of ESLint.
From the next section, i'll create a plugin to see how ESLint detects such errors.
The purpose of using plugins is to add additional rules.
ESLint has lots of rules. no-unused-vars
that we saw above is one of the rules.
But if you want to apply an additional rule that is not available in the official rule, you can add it by creating a plugin. (There are other several ways)
But how ESLint analyze source code to check if the source code aligns with rules or not?
ESLint uses AST (Abstract Syntax Tree) to analyze JavaScript code.What’s AST?
AST represents source code structure.For example, AST of const hoge = 0;
is like this. (You can see AST easily with AST Explorer)
{
"type": "File",
"start": 0,
"end": 16,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 2,
"column": 0
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 16,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 2,
"column": 0
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 15,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 15
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 14
}
},
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"loc": {
"start": {
"line": 1,
"column": 6
},
"end": {
"line": 1,
"column": 10
},
"identifierName": "hoge"
},
"name": "hoge"
},
"init": {
"type": "NumericLiteral",
"start": 13,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 14
}
},
"extra": {
"rawValue": 0,
"raw": "0"
},
"value": 0
}
}
],
"kind": "const"
}
],
"directives": []
},
"comments": []
}
By using AST, we can get detailed information about the source code.It’s convenient to analyze source code rather than analyze plain text.
I created a plugin.
https://github.com/osuke/eslint-plugin-later-becomes-never
And the structure is simple like this.
├── lib
│ └── index.js
├── package.json
This plugin finds the string TODO in comment blocks and complains about it.
Let’s say you have the below code in your project
// Todo: will change variable name later
let hoge = 0;
ESLint shows an error like this
/Users/xxx/eslint-test/src/index.js
1:1 error Later becomes never later-becomes-never/no-todo
You can see the rule no-todo
which comes from later-becomes-never
plugin.
To define rules for the plugin, using AST is necessary.
The definition of the rule is in lib/index.js
.
module.exports = {
rules: {
"no-todo": {
create: function (context) {
return {
Program: (node) => {
node.comments.forEach((comment) => {
if (comment.value.toLowerCase().indexOf("todo") !== -1) {
context.report({
loc: comment.loc,
message: "Later becomes never",
});
}
});
},
};
},
},
},
};
In the file, a rule named “no-todo“ is defined.We can manipulate AST by using node.Comments are extracted from the source code by using AST. (Line7)
After that, I checked if each comment has a string todo or not. (Line8)
If it has, an error message is shown (Line9 - 12)
Let’s use ESLint with the plugin to the below JavaScript.Line 1 is supposed to have an error. Line 3 is not.
// Todo: will change variable name later
let hoge = 0;
// doit
This is the result. It works :)
/Users/xxx/eslint-test/index.js
1:1 error Later becomes never later-becomes-never/no-todo
✖ 1 problem (1 error, 0 warnings)