The Great Blog

How To Write Your First ESLint Plugin

July 14, 2019

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
  1. Create plugin package: mkdir my-eslint-rules && cd my-eslint-rules && npm init --yes
  2. Name the package in package.json: "name": "eslint-plugin-my-eslint-rules"
  3. Create index.js that exports the rules object with the custom rule, let’s say async-func-name:
module.exports = {
  rules: {
    "async-func-name": {
      create: function(context) {
        return {
          /* ...rule methods */
        }
      },
    },
  },
}

Writing a rule

For constructing and testing the rule, we will use a tool AST explorer. 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

A 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:

context.report({
    node: node,
    message: "Async function name must end in 'Async'"
});

A 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 run mkdir node-app && cd node-app && npm init --yes && touch index.js and add the same function used in the AST explorer to the index.js:
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:
{
  "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
{
  "parser": "babel-eslint",
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "modules": true
    }
  }
}
  • .babelrc
{
  "presets": ["@babel/preset-env"]
}

Summary

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.


Linas Spukas

Hi there! My name is Linas Spukas, I am a full stack web developer and this is my blog. About stuff and things... in development. Enjoy.