1, 2, 3, linted

Preventing easy to catch JavaScript bugs using linters.

JavaScript is very dynamic language, and a silly mistake is simple to make but hard to catch. A misspelled variable can be undiscovered and crash the website in production. Exhaustive unit testing is exhaustive. Luckily many common errors are easily spotted using static source analysis tools commonly called linters. There are 3 popular ones

  • jshint - a modern replacement for the obsolete jslint
  • eslint - EcmaScript linter
  • jscs - JavaScript code style checker

These three tools are very popular: more than 3k GitHub stars each. JsHint is the oldest, with very stable API and large set of rules it can check. ESLint is newer, with a very flexible API based on the processing the syntax tree rather than the raw code. This allows one to write custom rules, for example here is my set: eslint-rules. Finally, jscs is more a style checker than a linter. It allows to enforce a common style on your code base, making sure the developers can read code written by others with ease.

All tools are implemented using NodeJS and can be installed using npm commands.

Getting started

I wrote a small example you can use to follow these steps along. Just grab the bahmutov/1-2-3-linted repo

git clone [email protected]:bahmutov/1-2-3-linted.git
cd 1-2-3-linted
npm install
git checkout initial

We have a single file index.js showing a pretty mediocre JavaScript

index.js
1
2
3
4
foo = 'foo';
if(foo=='foo') {
alert('foo is ok');
}

There is a variable foo we have forgotten to declare, making it global. The whitespacing is inconsistent, and the script uses alert to show a message, which is annoying. It also uses non-strict comparison operator == that can later lead to hard to debug bugs. Let us detect these problems.

Linting using NPM scripts

I now prefer linting my code directly from an NPM script. Let us install eslint and start linting.

npm install --save-dev eslint

There are several ways to configure eslint. I prefer creating a separate configuration file. We can let eslint create one for us by answering a few questions

$ ./node_modules/.bin/eslint --init
? How would you like to configure ESLint? Use a popular style guide
? Which style guide do you want to follow? AirBnB
? What format do you want your config file to be in? JSON
Successfully created .eslintrc.json file in /git/1-2-3-linted
Installing additional dependencies

I picked output JSON format and there is a new file .eslintrc.json in the current folder.

.eslintrc.json
1
2
3
4
5
6
{
"extends": "airbnb",
"plugins": [
"react"
]
}

Becase we are reusing AirBnB JS style guide, the .eslintrc.json file is very bare. We can still add separate rules as we go along. For now, let us setup the lint script in the package.json. Since there are no unit tests at all, let us also set npm test to run the linter. Some linting will be better than no testing at the start.

package.json
1
2
3
4
"scripts": {
"test": "npm run lint",
"lint": "eslint index.js"
}

Let us run the linter using either npm run lint or npm test, or if you are using npm-quick-run using nr l command.

$ nr l
running prefix l
spawning test process npm [ 'run', 'lint' ]
> [email protected] lint /1-2-3-linted
> eslint index.js
/1-2-3-linted/index.js
  1:1  error    "foo" is not defined                         no-undef
  2:3  error    Keyword "if" must be followed by whitespace  space-after-keywords
  2:4  error    "foo" is not defined                         no-undef
  2:7  error    Expected '===' and instead saw '=='          eqeqeq
  2:7  error    Infix operators must be spaced               space-infix-ops
  3:3  warning  Unexpected alert                             no-alert
✖ 6 problems (5 errors, 1 warning)

All the problems in the source code have been discovered. You can see the problems yourself by running the command from the following tag

git checkout eslint
npm run lint

Configuring rules

Let us say that we are ok with the missing space after the if keyword. Can we disable the corresponding eslint error? Yes, by ignoring this specific rule (the name of the rule is in the last column of the default eslint reporter). Change the .eslintrc.json and set the rule to 0 - meaning to ignore it.

.eslintrc.json
1
2
3
4
5
6
7
8
9
10
{
"extends": "airbnb",
"plugins": [
"react"
],
"rules": {
"space-after-keywords": 0,
"no-alert": 2
}
}

I also changed the rule no-alert from default (1 - warning) to be 2 - error, because I really do want to catch all alert calls in my code. Run the linter again to see the changes.

> [email protected] lint /1-2-3-linted
> eslint index.js
/1-2-3-linted/index.js
1:1  error  "foo" is not defined                 no-undef
2:4  error  "foo" is not defined                 no-undef
2:7  error  Expected '===' and instead saw '=='  eqeqeq
2:7  error  Infix operators must be spaced       space-infix-ops
3:3  error  Unexpected alert                     no-alert
✖ 5 problems (5 errors, 0 warnings)

Autofixing some problems

ESLint and JSCS have a great new feature - they can fix some problems automatically. Let us see what they can do for us right now. Let us configure an NPM script to fix our source

package.json
1
2
3
4
"scripts": {
"lint": "eslint index.js",
"fix": "eslint --fix index.js"
}

When we run it using npm run fix or nr f we see the following

/1-2-3-linted/index.js
1:1  error  "foo" is not defined            no-undef
2:4  error  "foo" is not defined            no-undef
2:7  error  Infix operators must be spaced  space-infix-ops
3:3  error  Unexpected alert                no-alert
✖ 4 problems (4 errors, 0 warnings)

As you can see, there are only 4 problems, rather than 5. The ESlint has been able to fix the problem eqeqeq, Expected '===' and instead saw '==' by automatically replacing if(foo == 'foo') with if(foo === 'foo') expression. In general the autofixing is a very conservative operation and does not modify the code too much.

Related: Autofix JavaScript style issues using jscs

Using linters on commit

You should never have to remember to run testing and linting commands manually, what is this - the 18th century? Instead, we want to try to make sure the code is linted automatically before it gets committed into the repository. This will make sure that any commit in your master branch is pointing at nice working code; the master branch should be very clean.

If you already have NPM lint command, it is very simple to run it on each commit. Let us install a Git hook module

npm install --save-dev pre-git

The pre-git module adds a config object to the package.json file. Let us add npm run lint command to the list of commands to execute before each commit is allowed to go through.

package.json
1
2
3
4
5
6
7
8
9
"config": {
"pre-git": {
"commit-msg": "validate-commit-msg",
"pre-commit": ["npm run lint"],
"pre-push": [],
"post-commit": [],
"post-merge": []
}
}

Let us try to land a commit - it should fail because we have not fixed the lint errors.

$ git commit -m "landing bad code"
executing task "npm run lint"
> [email protected] lint /1-2-3-linted
> eslint index.js
/1-2-3-linted/index.js
  1:1  error  "foo" is not defined            no-undef
  2:4  error  "foo" is not defined            no-undef
  2:7  error  Infix operators must be spaced  space-infix-ops
  3:3  error  Unexpected alert                no-alert
✖ 4 problems (4 errors, 0 warnings)
...
pre-commit You've failed to pass all the hooks.
pre-commit
pre-commit The "npm run lint" script failed.
pre-commit
pre-commit You can skip the git pre-commit hook by running:
pre-commit
pre-commit    git commit -n (--no-verify)
pre-commit
pre-commit But this is not advised as your tests are obviously failing.

The pre-commit script warns that the test commands have failed, but gives a work around. If you really need to land this code, you can skip the checks using git commit -n command line option.

Using linters from a build script

If you already are using a build tool, like grunt, gulp or broccoli, you can easily plug in a linter via a plugin. For example, there is grunt-contrib-jshint for people who are using grunt; and there is gulp-eslint for those who prefer gulp.

There is even gulp-lint-everything that I wrote that allows one to use all 3 linters at once without configuring each one separately.

1
2
3
4
5
6
7
8
9
// gulpfile.js
var gulp = require('gulp');
var lintAll = require('gulp-lint-everything')({
jshint: './configs/jshint.json',
eslint: './configs/eslint.json',
eslintRulePaths: ['./node_modules/eslint-rules'],
jscs: './configs/jscs.json'
});
gulp.task('default', lintAll('*.js'));

Additional links

Update 1 - standard

I tried using a linter / style checker called standard that is built on top of ESLint. The installation is very very simple.

npm install --save-dev standard

Then use standard just like you used eslint in the package script tag

1
2
3
"scripts": {
"lint": "standard index.js"
}

In our case the standard linter reports more errors - mostly because it does not allow semicolons (there is also same standard that allows semicolons, called semistandard).

> [email protected] standard /1-2-3-linted
> standard index.js
standard: Use JavaScript Standard Style (https://github.com/feross/standard)
  /1-2-3-linted/index.js:1:1: "foo" is not defined.
  /1-2-3-linted/index.js:1:12: Extra semicolon.
  /1-2-3-linted/index.js:2:1: Keyword "if" must be followed by whitespace.
  /1-2-3-linted/index.js:2:4: "foo" is not defined.
  /1-2-3-linted/index.js:2:7: Infix operators must be spaced.
  /1-2-3-linted/index.js:3:3: "alert" is not defined.
  /1-2-3-linted/index.js:3:21: Extra semicolon.

By adding --verbose flag, you can also see the names of the errors, making it simple to ignore certain ones. For example, to ignore a particular error on a particular line, add a comment

1
2
3
function demo () { // eslint-disable-line no-unused-vars
...
}

It also has a rule that rubs me in a wrong way a little: a named function should have space after the name

1
2
3
4
// good
function foo () { return 'foo' }
// bad
function foo() { return 'foo' }

Aside from this, the Standard is pretty similar to what I like, and the additional configuration is possible because under the covers it is ESLint.

You can let everyone know that your project follows this code style standard by using one of the two badges:

badge1 badge2

I love badges!

Update 2 - Automatic formatting

When using standard, a good companion is automatic reformatter released by the same author standard-format. Just install it together and add the command to reformat before linting.

1
npm install --save-dev standard standard-format

Then setup the scripts in the package.json

1
2
3
4
5
6
"scripts": {
"test": "mocha test/*-spec.js",
"lint": "standard --verbose index.js test/*.js",
"format": "standard-format -w index.js test/*.js",
"pretest": "npm run format && npm run lint"
}

Every time we run npm test, the tools will fix simple linting issues and will show any remaining ones. If there are no lint errors, the tests are run. You can also add common global variable names to the list. Just add the names to the package.json

1
2
3
4
5
{
"standard": {
"globals": ["describe", "it", "beforeEach"]
}
}

To hide a specific warning you can add a comment in a single file /*eslint-disable no-eval */ or at a specific line

1
eval(source)  // eslint-disable-line no-eval

See a project using this obind.

Update 3 - Automatic CSS formatting and linting

In addition to the automatic JavaScript formatting, I have started using similar tools for linting my CSS with stylelint and stylefmt. I am using stylelint-config-standard that specifies the linting settings and is shared between the formatter and linter (via package.json property).

1
npm install --save-dev stylelint stylefmt stylelint-config-standard

and set the config and script commands in the package.json

1
2
3
4
5
6
7
8
9
{
"scripts": {
"stylelint": "stylelint src/*.css",
"stylefmt": "stylefmt src/*.css"
},
"stylelint": {
"extends": "stylelint-config-standard"
}
}

Typically, my styles do not change very often, so I run CSS format and lint commands on each pre-commit hook, instead of running them before each test.

Update 4

I have described how I setup linting in August of 2017 in linters gonna lint

Update 5 - Shared ESLint config

Assuming you have a lot of custom ESLint rules and settings, you probably want to share them among the projects. It is straightforward to move the settings into a separate NPM module, which you can then install as a dev dependency. For example, here is ESLint module with all linting rules we use at Cypress.io in eslint-plugin-cypress-dev.

You just install this module with npm install --save-dev eslint-plugin-cypress-dev and then specify in your .eslintrc file which collection of rules you want to bring. There are general, test (for your Mocha unit tests) and react sets. Most of the time just add general and you will be all set

1
2
3
4
5
6
// .eslintc
{
"extends": [
"plugin:cypress-dev/general"
]
}

See how these rules are specified in the eslint-plugin-cypress-dev in the exported file index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
configs: {
general: {
env: {
commonjs: true,
},
parserOptions: {
ecmaVersion: 6,
},
rules: {
'array-bracket-spacing': ['error', 'never'],
'arrow-parens': ['error', 'always']
// lots more rules
}
}
}
}

That's it - you just export a named object with rules, just like you would code it in .eslintrc file.

Update 6 - linting in a monorepo

If you have a large repo with lots of subprojects, you do not have to install eslint in every subfolder. Instead, install it in the root, and then "find it" using bin-up tool, which you should install in each subfolder (but bin-up is very small).

1
npm i -D bin-up

Then setup the lint script like this

1
2
3
4
5
{
"scripts": {
"lint": "$(bin-up eslint) *.js"
}
}

bin-up will go up the folder tree, until it either finds the specified tool in one of node_modules/.bin folders, or reaches Git root.