How to write custom ESLint rules

Frontend developer working on the platform at Mews

Hello there! My name is Daria, and today we’re going to look into how to write custom ESLint rules. I’ll walk you through writing a rule step by step so that by the end of the guide you’ll be confident enough to create your own rules.

For writing custom ESLint rules, you will need to:

  • Set up ESLint configuration if you don’t have it yet (not covered here, but you can check out official docs for how to start with ESLint)
  • Add a plugin for custom rules
  • Write a custom rule
  • Add a test for the rule
  • Enable the rule in your ESLint configuration

Before writing any custom rules I’d recommend going through ESLint core rules along with plugins created by the community. Chances are that your issue isn’t unique and was already solved by someone else.

But sometimes you want to enforce a particular coding style that is relevant only for your project. You may want to support migrating from a library or to provide a strict guideline that everyone has to follow.

If that’s your case, then let’s get to creating our own rule. First, we should add a plugin.

Adding a plugin

To add a plugin you need to create an npm module with a name in either of these formats:

  • eslint-plugin-'plugin-name' e.g. eslint-plugin-mews
  • @'scope'/eslint-plugin-'plugin-name' e.g. @mews/eslint-plugin-mews
  • @'scope'/eslint-plugin e.g. @mews/eslint-plugin

I chose the third variant code>@mews/eslint-plugin. Now, you either create the module manually or use a generator. For the simplest plugin you only need four files: package.json, index.js where you export the rules you’ve created, 'rule-file', and 'test-file'. The last two files we’ll create together in this guide.

// package.json
{
    "name": "@mews/eslint-plugin",
    "version": "1.0.0",
    "private": true,
    "main": "index.js",
    "peerDependencies": {
        "eslint": "8.14.0"
    }
}
// index.js
module.exports = {
    rules: {}
};

Now that we have a framework set up, let’s jump into creating a rule.

Coming up with the rule

We’re going to write a rule to forbid empty catch blocks. Why? Because empty catch blocks silently swallow an error, and such errors are very hard to trace.

For example, this would be considered valid code for our rule:

try { foo() } catch (e) { bar() }

But this example would trigger an error:

try { foo() } catch (e) {}

To detect such examples in the code we need to take a look at AST or Abstract Syntax Tree. AST is a tree representation of the source code that tells us about the code structure. Tools like ESLint create AST for a given piece of code and execute rules on it. To figure out specific instructions for our custom rule, we need to inspect AST manually.

Fortunately, there’s a free tool for that, it’s called the AST explorer.

The AST explorer

Let’s go through our example here. Click on the empty brackets {} in the invalid example on the left side, and you should see a portion of the AST highlighted on the right:

AST excerpt 1

Now we need to find the node to start at. We could start from the top level Program node and navigate downwards but that’s too complex. It’s better to choose something closer to our target node, e.g. CatchClause that represents, you guessed it, a catch clause 🙂

Tip: If you ever get lost in the AST, you can hover onto different node types on the right. The tool will then highlight relevant parts of the code on the left side.

Writing the rule

We have our starting node, great! Let’s start writing our rule. Based on ESLint conventions, we should create a separate file and call it e.g. no-empty-catch.js. The rule file needs to export an object with meta and create properties. meta is an object with the metadata of the rule and create is a method that takes in the context object and returns an object with methods to traverse AST. The methods’ names map to node types in AST. Let’s go ahead and add a message describing the problem to the meta object:

// no-empty-catch.js
module.exports = {
    meta: {
        messages: {
            emptyCatch: 'Empty catch block is not allowed.',
        },
    },
    create(context) {
        return {}
    }
};
Now since we’ve decided to start at the CatchClause node, let’s add the eponymous method to our rule. All methods take node argument, which allows us to use this node’s properties while traversing down the tree:
// no-empty-catch.js
module.exports = {
    meta: {
        messages: {
            emptyCatch: 'Empty catch block is not allowed.',
        },
    },
    create(context) {
        return {
            CatchClause(node) {
            }
        }
    }
};
Let’s take a look at the AST explorer again. Click on the bar() part in the valid example, and you should see the following highlighted:
AST excerpt 2
Now if we compare ASTs for invalid and valid examples, we notice that the only difference is that valid example has a few more nodes inside the BlockStatement, while invalid example has BlockStatement with an empty body. That means our rule will be simple, we can just check for the empty body and detect invalid code!
// no-empty-catch.js
module.exports = {
    meta: {
        messages: {
            emptyCatch: 'Empty catch block is not allowed.',
        },
    },
    create(context) {
        return {
            CatchClause(node) {
                // start at CatchClause and go down to BlockStatement's body
                if (node.body.body.length === 0) {
                }
            }
        }
    }
};
We’ve detected invalid code, the only thing left is to actually publish an error to the code. To do that we will call context.report(), it accepts an object with node and messageId properties. We will pass node.body as a node, because it refers to BlockStatement or empty brackets {} in our example:
// no-empty-catch.js
module.exports = {
    meta: {
        messages: {
            emptyCatch: 'Empty catch block is not allowed.',
        },
    },
    create(context) {
        return {
            CatchClause(node) {
                if (node.body.body.length === 0) {
                    context.report({ node: node.body, messageId: 'emptyCatch' });
                }
            }
        }
    }
};
When you have doubts regarding rule instructions you can always check AST node types specification for the parser that you’re using, e.g. the ESTree Spec. That said, we’ve finished writing our rule, hooray! Let’s test it.

Testing the rule

ESLint provides a utility RuleTester and this is how it’s used:
// no-empty-catch.spec.js
const { RuleTester } = require('eslint');
const noEmptyCatchRule = require('../no-empty-catch.js');
const ruleTester = new RuleTester();
ruleTester.run(...);
ruleTester.run() method runs the tests and needs three arguments: the name of the rule, the rule object itself, and an object with valid and invalid code examples. Let’s pass these arguments:
// no-empty-catch.spec.js
const { RuleTester } = require('eslint');
const noEmptyCatchRule = require('../no-empty-catch.js');
const ruleTester = new RuleTester();
ruleTester.run('no-empty-catch', noEmptyCatchRule, {
    valid: [{
        code: 'try { foo() } catch (e) { bar() }',
    }],
    invalid: [{
        code: 'try { foo() } catch (e) {}',
        // we can use messageId from the rule object
        errors: [{ messageId: 'emptyCatch' }],
    }]
});
Run the test file using your favorite test framework.
Test results
We’ve confirmed that our rule works, let’s export it from our plugin:
// index.js
module.exports = {
    rules: {
        'no-empty-catch': require('./no-empty-catch'),
    },
};

Enabling the rule

As a final step we need to include our new plugin along with the rule in our ESLint config. Don’t forget to include the plugin as a peer dependency in your package.
// eslint config file
module.exports = {
    plugins: ['@mews'],
    extends: [...],
    rules: {
        ...
        '@mews/no-empty-catch': 'error',
    }
}
That’s it!

Is this something you’d like to work with?

Or you’d like to work with outstanding engineers like Daria? 👩‍💻

TL;DR

Creating custom ESLint rules isn’t hard once you have the framework in place. Paste your valid and invalid code examples into an AST explorer and play with them. Write a test for each rule. If you get stuck, check out the ESLint docs or the source code for their core rules. See, that wasn’t too hard, was it? Now you can create your very own rules for your very own codebase. For more information, check out the official guide on working with ESLint rules.
Frontend developer working on the platform at Mews
Share:

More About