Writing custom EsLint rules

In statically compiled languages, we usually lean on the compiler to catch out common errors (or plain stupidities). In dynamic languages we don’t have this luxury. While you could argue over whether this is a good or a bad thing, it’s certainly true that a good static analysis tool can help you quite a bit in detecting mistakes. For Javascript, a few tools are available: there’s the good old JsLint, which is very strict, JsHint, created because not all of us are Douglas Crockford and then there’s EsLint. In this post I’ll show you how to create custom EsLint rules.

EsLint is quite a nice alternative for JsHint and is very flexible. While JsHint certainly has its benefits and comes out of the box with a lot of configurable options, EsLint allows you to configure your own custom rules.

These custom EsLint rules can be added to their library and released to the community as optional extra checks, they can be company specific to enforce a certain coding style or they can be project specific.

In this post I want to talk about creating project specific custom EsLint rules. It’s easily transferrable to a more common use plugin if you want to (by publishing to NPM). I found creating a project specific plugin has a few more hurdles, so I’ll explain that.

Analyzing code

Asbtract Syntax Trees

Before we start writing custom EsLint rules, I first want to show how code is analyzed and how we can plug in to that. In order to analyze code we must first build an abstract syntax tree. In this case we want to build an ES 6 abstract syntax tree (AST). An AST is essentially a data structure which describes the code. The next example shows some sample code and the corresponding syntax tree:

var a = 1 + 1;
custom EsLint rules - Abstract Syntax Tree

The above visualization can also be presented as a pure data structure, here in the form of JSON:

{
    type: "VariableDeclaration",
    declarations: [{
        type: "VariableDeclarator",
     id: {
         type: "Identifier",
            name: "a"
        },
        init: {
            type: "BinaryExpression",
            left: {
                type: "Literal",
                value: 1,
                raw: "1"
            },
            operator: "+",
            right: {
                type: "Literal",
                value: 1,
                raw: "1"
            }
        }
    }],
    kind: "var"
}

When we have this syntax tree, you could walk the structure and then write something like this for each node:

if(node.type === "VariableDeclarator" && node.id.name.length < 2){
    console.log("Variable names should be more than 1 character");
}

Writing custom EsLint rules

Since all of this AST-generation and node-walking is not specific to any rule, it can be externalized, and that’s exactly what EsLint gives us. EsLint builds the syntax tree and walks all the nodes for us. We can then define interception points for the nodes we want to intercept. Apart from that, EsLint also gives us the infrastructure to report on problems that are found. Here’s the above example rewritten as an EsLint rule:

module.exports.rules = {
    "var-length": context => ({
        VariableDeclarator: (node) => {
            if(node.id.name.length < 2){
                context.report(node, 'Variable names should be longer than 1 character');
            }
        }
    })
};

This can then be plugged in to EsLint and it will report the errors for any Javascript code you throw at it.

EsLint plugins

In order to write a custom EsLint rule, you need to create an EsLint plugin. An EsLint plugin must follow a set of conventions before it can be loaded by EsLint:

  • It must be a node package (distributed through NPM, although there’s a way around it, read on …)
  • Its name must start with eslint-plugin

The documentation mentions a way to write custom rules in a local directory and running them through a command-line option. This still works, but is deprecated and will soon break in newer versions of EsLint. It’s recommended to go the plugin-route as described in this post.

Creating the plugin

With the above requirements, we can go two routes:

The generator sets you up with a nice folder structure, including tests, a proper description and some documentation. However, if you just want to write some quick rules, I find it easier to just create a folder and the structure myself. Essentially, you need two files:

  • package.json (remember, it has to be an NPM package)
  • index.js, where your rules will live

Here’s a basic version of the package.json:

{
  "name": "eslint-plugin-my-eslist-plugin", // remember, the name has to start with eslint-plugin
  "version": "0.0.1",
  "main": "index.js",
  "devDependencies": {
    "eslint": "~2.6.0"
  },
  "engines": {
    "node": ">=0.10.0"
  }
}

And this is what index.js looks like, with our custom EsLint rule:

module.exports.rules = {
    "var-length": context => ({
        VariableDeclarator: (node) => {
            if(node.id.name.length < 2){
                context.report(node, 'Variable names should be longer than 1 character');
            }
        }
        // , more interception points (see https://github.com/estree/estree)
    })
    // more rules
};

Installing the plugin

As I mentioned before, if you want to share your plugin, you can distribute it via NPM. This doesn’t always make sense though as you might have project specific rules. In those cases, you can just create the folder with your plugin locally and commit it to your code repository. For it to work, you still need to install it as a node package though. You can do that with the following NPM command:

npm install -S ./my-eslint-plugin

This will install the package from the local folder my-eslint-plugin. That way, you can keep the rules locally to your project and still use them while running EsLint.

Configuring the plugin

For EsLint to recognize and use the plugin we have to notify it through the configuration. We need to do two things:

  • Tell it to use the plugin
  • Switch on the rules

To tell it to use plugin, we can add a plugins node into the configuration, specifying the name of the plugin (without the “eslint-plugin”-prefix):

"plugins": [
    "my-eslint-plugin"
]

Next we need to define the rules:

"rules": {
    "my-eslint-plugin/var-length": "warn"
}

With the plugin installed, you can now run EsLint and it will report on one letter variable names.

Example

While this is all nice, the above rule is probably not very useful, since there’s already a built-in rule for that (http://eslint.org/docs/rules/id-length).

As for general styling rules, EsLint has probably most of them already covered, and the ones it hasn’t are probably quite obscure. Custom EsLint rules come in handy on a project-level basis.

As an example, I’m currently working on an Angular 1 project. The intention is to port this over to a different framework soon. Because of that, we want to make sure we’re as independent of Angular as possible. There are certain things we can do just as easily in plain JS instead of using angular’s utility methods. For others, we can use different libraries that we can port over as well when we port the application.

Now, we don’t want to go off and change all these occurrences at once, because that would be a lot of upfront work. Ideally, we want the following:

  • Get notified when there’s a call to an angular-method which could be done easily in plain JS in the module we’re working on
  • Get notified on the CI-server (with a warning) if an angular-method is used
  • Once we get rid of the warnings for that angular-method, fail the build on the CI-server if that call is detected again

So, as an example, here are a few rules we defined in our project:

module.exports.rules = {
    "no-angular-copy": context => ({
        MemberExpression: function(node) {
            if (node.object.name === "angular" && node.property.name === "copy") {
                context.report(node, "Don't use angular.copy, use cloneDeep from lodash instead.");
            }
        }
    }),
    "no-angular-isDefined": context => ({
        MemberExpression: function(node) {
            if (node.object.name === "angular") {
                if(node.property.name === "isDefined") {
                    context.report(node, "Don't use angular.isDefined. Use vanilla JS.");
                } else if (node.property.name === "isUndefined") {
                    context.report(node, "Don't use angular.isUndefined. Use vanilla JS");
                }
            }
        }
    })
};

We then enabled the rules with a warning in our configuration. As soon as we notice no more warnings for one of these rules, we will switch them to errors. The CI-build is configured to fail when EsLint finds an error. On top of that we have the EsLint plugin for VsCode, which looks like this in the editor:

Custom EsLint rules - VS Code EsLint

This combination ensures that we clean up angular calls while we continue development and that no new calls will be introduced.

Sidenote: the rules here are not foolproof since someone could assign angular to a temp variable and then call the methods on the temp variable.  Be that as it may, we want to catch the general use case with a simple rule. We could probably write a more thorough analyzer, but it would take a lot of time. Since all of this code still needs to go through code reviews, we don’t worry too much about it.

Other possibilities

The above example is something that was very convenient for our use case, but the possibilities are endless. Here are a few things you could achieve with this:

  • Ensure a user message is shown when HTTP calls are initiated and that they’re properly removed once it ends.
  • Ensure jQuery isn’t used when we’re using a SPA-framework (or only in certain modules)
  • When using jQuery, ensure you’re always calling event-handlers using the .on method instead of the shorthand ,click and similar

There are plenty of possibilities for custom EsLint rules and most of it depends on your project. What other ideas do you have?

Existing plugins

Of course, there are plenty of existing EsLint plugins for existing frameworks on NPM already. If you’re using one of these frameworks, it’s worth checking out the rules and see if you could benefit from enabling some of these

More information on writing custom EsLint rules can be found in the offical documentation

Comments are closed.