This article serves as a simple introductory guide to ESLint plugins, how they work, how to set up, write and use one.
What are ESLint Plugins
It's an extension for ESLint that will enforce rules, that are not implemented into the ESLint core. For example, one of the most popular plugins, eslint-plugin-babel
, supports experimental features and linter that are not in the ESLint core.
Plugins usually stored as separate NPM modules, which exports rule
object, where key
is a name, and value
is another object with the methods that enforce the rule:
module.exports = {
rules: {
"rule-name": {
create: function (context) {
// rule implementation ...
}
}
}
};
Why should you care writing your own?
There are a lot of plugins released, and chances for finding one you need are high. But there might be cases when you need to have rule very specific to your codebase.
Recently our development team decided to enforce the role for async function naming. Meaning, If a function returns a promise or declared as an async
, it must have a suffix to its name: function someFunctionAsync() {}.
Let's take this example for the tutorial and write a plugin that will warn us about wrong function naming.
Creating a plugin
For this tutorial, we will create a local plugin package and use it in the simple node package. The project structure will look like:
plugin-tutorial
│
└───my-eslint-rules
│ │ package.json
│ │ index.js
│
└───node-app
│ package.json
│ index.js
Start by create the main folder mkdir plugin-tutorial && cd plugin-tutorial
Setup plugin package
Each valid plugin should meet the following criteria:
- Be separate NPM package
- Follow name format of
eslint-plugin-<plugin-name>
- Export
rule
object
- Create plugin package:
mkdir my-eslint-rules && cd my-eslint-rules && npm init --yes
- Name the package in
package.json
:"name": "eslint-plugin-my-eslint-rules"
- Create
index.js
that exports therules
object with the custom rule, let's sayasync-func-name
: ```javascript
module.exports = {
rules: {
"async-func-name": {
create: function (context) {
return { /* ...rule methods */ }
}
}
}
};
### Writing the rule
For constructing and testing the rule, we will use a tool [AST explorer](https://astexplorer.net). The AST stands for an abstract syntax tree or just syntax tree, and it is a representation of the source code in a programming language.
Setup AST Explorer by selecting parser to `eslint-babel` and transformer to `ESLint v4`.
By now you should see four windows in the explorer:
* top left window will be used to write a source code
* top right window is the explorer of the source code. When you hover on expressions, you should see the highlighted parts in your code
* bottom left is the rule
* bottom right is the output after the rule, that runs against the code
![](https://i.imgur.com/Vc1M86u.png)
The rule is a function that takes a `context` object, that has additional functionality and information that is relevant to the context of the rule.
The main method is `context.report()` which publishes warning or error. It takes one argument, object, and can have the following properties: message, message, loc, data, fix. We will use a simple example of the report object:
```javascript
context.report({
node: node,
message: "Async function name must end in 'Async'"
});
The rule must return an object with methods that ESLint calls to “visit” nodes while traversing the syntax tree of source code. In our example, we have one method, FunctionDeclaration
, which takes one argument node
object. The object holds function information, such as type, name, body, locations of each value.
To check if function name has an Async
suffix we need access the name, which is in the id
object of FunctionDelacarion
node: node.id.name
.
So the main logic of the rule should be to check if the function has async
property and if the name does not include Async
suffix, call context.report().
After applying the rule, the output in the AST explorer should warn you with the message:
After composing the rule in the explorer and making sure it captures rule conditions, move the logic back to the index.js
of the plugin package, and that completes plugin:
module.exports = {
rules: {
"async-func-name": {
create: function (context) {
return {
FunctionDeclaration(node) {
if (node.async && !/Async$/.test(node.id.name)) {
context.report({
node,
message: "Async function name must end in 'Async'"
});
}
}
}
}
}
}
};
Applying plugin to node project
First let's setup the node project:
- from
plugin-tutorial
runmkdir node-app && cd node-app && npm init --yes && touch index.js
and add the same function used in the AST explorer to theindex.js
: ```javascript
async function myFunction() {
return "";
}
* install ESLint `npm i eslint --save-dev`
* install the plugin you created: `npm i ../my-eslint-rules --save-dev`
* tell app to use ESLint and plugin by creating configuration file `.eslintrc`:
```json
{
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"my-eslint-rules/async-func-name": "warn"
},
"plugins": ["my-eslint-rules"]
}
- run ESLint command from the terminal and node app folder with command:
./node_modules/.bin/eslint index.js
That's pretty much it. You should see an ESLint warning after running it in the terminal if you set the wrong name for an async function.
The important part of using plugins in the project is the .eslintrc
configuration file. The rules from plugin should follow naming "<plugin-name>/<rule-name>": [warn/error]
. And the plugin name should be added to the plugins
field in an array.
Different method for writing plugins
The more effortless way write plugins is by using Yeoman generator. You need to install yo
and generator-eslint
packages and run yo
command from the terminal. It will generate the plugin template with a more structured project, includes test setup.
Where you might struggle
The plugin example in this tutorial was done using common js, meaning there was no import modules or new js syntax.
To use es6 language features, such as imports, in custom plugins you will need to install additional@babel-core, @babel/node, @babel/preset-env, babel-eslint, babel-register
and add the following configuration to the:
-
.eslintrc
```json
{
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"modules": true
}
}
}
* .babelrc
{
"presets": ["@babel/preset-env"]
}
</code></pre></div><h2>
<a name="summary" href="#summary">
</a>
Summary
</h2>
<p>We are using ESLint daily, but most do not know how it works in the background. Hope that now you have a better understanding of how ESLint works, how to write your custom plugins and use abstract syntax tree.</p>
Top comments (2)
thank you so much.
tysm