Skip to content

Instantly share code, notes, and snippets.

@jareware
Last active January 30, 2024 03:15
Show Gist options
  • Save jareware/7179093 to your computer and use it in GitHub Desktop.
Save jareware/7179093 to your computer and use it in GitHub Desktop.
Project-specific lint rules with ESLint

⇐ back to the gist-blog at jrw.fi

Project-specific lint rules with ESLint

A quick introduction

First there was JSLint, and there was much rejoicing. The odd little language called JavaScript finally had some static code analysis tooling to go with its many quirks and surprising edge cases. But people gradually became annoyed with having to lint their code according to the rules dictated by Douglas Crockford, instead of their own.

So JSLint got forked into JSHint, and there was much rejoicing. You could set it up to only complain about the things you didn't want to allow in your project, and shut up about the rest. JSHint has been the de-facto standard JavaScript linter for a long while, and continues to do so. Yet there will always be things your linter could check for you, but doesn't: your team has agreed on some convention that makes sense for them, but JSHint doesn't have an option to enforce it. You could submit a pull request for each such option, but eventually they'll be rejected as too specific/generic/weird. And that makes sense; you really can't bundle every lint rule anyone ever thought up into a single tool.

When you think about the audience for these tools, it's actually kind of silly they need to be configured using simple, pre-set on/off switches. If you're configuring a static code analysis tool for your project, you very likely know how to write some code as well. So the next step in the evolution of JavaScript linters seems obvious: exposing an easy mechanism for running custom lint code, allowing you to check for whatever you want. No need to be tied to what the upstream decides (not) to support.

Enter ESLint

As is usually the case with good ideas, someone else already came up with it. ESLint (by the great @nzakas & friends) is a recent alternative to JSHint, with a very flexible architecture: every lint rule is an independent, pluggable module, and more can be added at runtime. The project also ships with a growing set of default rules, which you can either selectively use or completely opt-out of. (This is in fact also where the JSHint project is going, but AFAIK it'll be a while til it's done). Install it with (for example):

$ npm install -g eslint

A word of warning, though: while the ESLint project is aiming for feature-parity with JSHint, it's not there yet. Also, there's currently only a "pre-alpha" version available, so it's not exactly production-ready. That said, having used & hacked at it a bit it seems quite ready to be added to your toolchain, perhaps to fill in the gaps left by JSHint; remember, you can always run both tools side-by-side and only enable specific rules from ESLint.

Adding custom rules

I came across ESLint while shopping around for a JavaScript style checker, which JSHint decidedly isn't. It's not really the core mission of ESLint either, but its architecture makes it remarkably easy to use it as one. To demonstrate, let's decide we want to enforce if statements that have the curly brace on the same line as the condition, preceded by a single space (ahh, the arguments we've all had about this!).

ESLint uses Esprima for parsing the JavaScript source and producing its Abstract Syntax Tree (AST). It then allows lint rule modules to register interest in specific types of nodes in this tree, and then make their assertions. The type of node we're interested in is IfStatement. To see what we'll be operating on, go to the interactive Esprima demo and paste in:

if (true)
{
    console.log('yep...');
}

You should also write this into a file called sample-file.js so we can test our new lint rule against it.

To register a lint rule that enforces the aforementioned convention, put the following into eslint-rules/if-curly-formatting.js:

module.exports = function(context) {
    return {
        IfStatement: function(node) {
            var source = context.getSource(node.test, 0, 3);
            if (!source.match(/ {$/)) {
                context.report(node, "Found improperly formatted if-statement");
            }
        }
    };
};

As ESLint traverses the AST of the source file, the inner function we defined will be invoked for each IfStatement encountered. If you were to console.log(node), you'd see the AST information about the subtree we're currently visiting. That alone can be enough to make certain kinds of assertions, but invoking context.getSource(node) will additionally give us the corresponding source code in the original file.

But the source string for the complete IfStatement contains lots of unnecessary things for our simple assertion (the entire conditional code block, for example). Luckily, each IfStatement node also has a test subnode representing the condition being tested (in our sample file just true). We can then use context.getSource() with additional arguments, telling it to give us the source for the test node and the 3 characters that immediately followed that node in the original source. In a compliant case, that would be something like "true) {". Now it's a simple matter of whipping up a regular expression that ONLY matches the allowed case.

Confused? Don't worry. Just do some console.log()s within your linter function. The pieces are all there, for you to do whatever with!

Including our new rule

We still need to tell ESLint we want to enforce our newly created rule. Create a eslint-config.json with:

{
    "rules": {
        "if-curly-formatting": 1
    }
}

The format of this file is explained here and the available built-in rules listed here. You can turn other rules on/off from here as well. Once done, run:

$ eslint --config eslint-config.json --rulesdir eslint-rules/ sample-file.js

You should see your custom rule complain about if formatting! (You'll likely also see a few other built-in warnings about irresponsible use of console.log etc, which you can turn off from eslint-config.json if you don't like them.)

Grunt integration

Many JavaScript projects use Grunt for build automation these days, and ESLint integrates very painlessly to your build process. If you've installed it via npm, it's a three-liner in your Gruntfile.js:

grunt.registerTask('eslint', 'Lint source files with ESLint', function() {
    require('eslint/lib/cli').execute([ '--config', 'eslint-config.json', '--rulesdir', 'eslint-rules', 'your/scripts/path' ]);
});

Running $ grunt eslint should now work its magic.

And that's it!

Questions/comments? Leave a comment!

![GA](https://ssl.google-analytics.com/__utm.gif?utmwv=5.4.3&utmn=30456&utmhn=gist.github.com&utmdt=Project-specific%20lint%20rules%20with%20ESLint&utmr=-&utmp=%2Fjareware%2F7179093&utmac=UA-42176157-3&utmcc=__utma%3D1.1828258468.1374783534.1374783534.1374783534.1%3B%2B__utmz%3D1.1374783534.1.1.utmcsr%3D(direct\)%7Cutmccn%3D(direct\)%7Cutmcmd%3D(none\)%3B)

@joseph-allen
Copy link

joseph-allen commented Dec 20, 2016

I have a .eslintrc file as well bringing in the airbnb/legacy linting rules. When I have this I get errors. The definition for the rule can not be found and I think this might be because we look in the airbnb config. Any ideas?

great guide by the way!

@vvscode
Copy link

vvscode commented Dec 21, 2016

can't find how I can define rulesdir at config file. Any solution?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment