Quote

Creating custom rules for ESLint

Creating custom rules for ESLint is one of the more attractive way of building continuity tests. This allows you to set up organization or project specific rules that are unique to your code.

Continuity Tests

The idea of testing is generally split between unit tests and integration tests, where unit tests test specific functions or module and integration tests are higher level abstract tests, often done via QA testers in a mostly manual process.

The third and often overlooked set of testing is continuity testing, also known as static code analysis.

ESLint

ESLint is a powerful linting utility for Javascript and is highly configurable and extendable. Through the use of plugins you can write your own custom set of rules to use in your project.

Creating a custom rule set involves creating a new eslint-plugin-{name} project. There is a Yeoman generator that gets you a quick scaffold to get started.

Were going to setup a new project from scratch.

$ mkdir eslint-plugin-sounds && cd eslint-plugin-sounds
$ npm init

Create your project and accept all the defaults for now.

$ npm install eslint --save-dev

We install eslint as a dev dependency since the plugin itself is is called by eslint so we do not need to package eslint with our plugin.

Create the following directories

$ mkdir lib
$ mkdir lib/rules
$ mkdir tests
$ mkdir tests/lib
$ mkdir tests/lib/rules

I’m not going to cover the rest of a normal github/npm package with readme.md. If you are not familiar with creating projects and publishing to npm I suggest you read GitHub Getting Started and NPM Publishing Packages.

Let’s create our first rule

$ cd lib/rules
$ touch first-rule.js

"use strict";
module.exports = function(context) {
    return {
        // Rule methods - AST Node Type
    }
}
module.schema = [];

The module returns a simple object with methods as properties. Each property is an AST node, ie Identity, CallExpression, MemberExpression.

First we need to know what part of the static tree we are going to lint.

Head over to ASTExplorer.net and put in your code snippit and see the tree.

ice_screenshot_20151208-143351

 

 

 

 

We want to write a rule that checks this method to see if we are sending arguments and the first argument is not null.

We see from the AST we are going to use the CallExpression node.

In that tree we have a MemberExpression with the object (sounds) and property (get) inside the callee property. sounds.call() is the method that would be called.

ice_screenshot_20151208-143656    ice_screenshot_20151208-143713

 

return {
    "CallExpression": function(node) {
        // node = {CallExpression}
        var callee = node.callee;
        // callee.object.name === 'sounds'
        // callee.property.name === 'get'
    }
}

So we now know that we want to test this method so we need to check against the object and property name.

if(callee.object.name === "sounds" &&
    callee.property.name === "get") {
  // Now we only match the methods that we are looking for
}

Lastly we want to check to see if that method is being called without arguments or a null first argument.

ice_screenshot_20151208-143824    ice_screenshot_20151208-143805

 

 

if (!node.arguments[0] || node.arguments[0].value === null) {
    //Report the error
}

Let put it all together

"use strict";

module.exports = function(context) {
    return {
        "CallExpression": function(node) {
            var callee = node.callee;
            if(callee.object.name === "sounds" &&
                callee.property.name === "get" &&
                node.arguments[0] &&
                node.arguments[0].value ==== null) {
                    context.report(node, "Method sounds.get() called without argument or first argument is null");
            }
        }
    };
};

module.schema = [];

There’s your first rule. It matches the method signature sounds.get() and checks to see if the first argument is defined and not null.

To implement your new rule you need to include your plugin in your node_modules and in your eslint config you include your plugin and rule.

{
  "plugins": [
    "sounds"
  ],
  "rules": {
    "sounds/first-rule": 1
  }
}

Notice that in the plugins section we only put our project name (sounds) not the full module name (eslint-plugin-sounds) as eslint expects eslint-plugin-{name} is the plugin path.

From here you should create a test to try out possible valid and invalid code snippets to make sure that everything is still valid. Check out this guide on how to write a test for your new rule.

There are dozens upon dozens of plugins and examples out there with more complex examples; see eslint-plugin-react for some really good ones.

Today we learned how to create your first ESLint plugin and custom rule, and implement it in your lint config file.