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
1 | foo = 'foo'; |
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.
1 | { |
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.
1 | "scripts": { |
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.
1 | { |
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
1 | "scripts": { |
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.
1 | "config": { |
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 | // gulpfile.js |
Additional links
- The example source code used in this blog post is at bahmutov/1-2-3-linted
- Tightening project with grunt build
- Lint like it's 2015
- Linting JavaScript inside HTML
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 | "scripts": { |
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 | 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 | // good |
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:
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 | "scripts": { |
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 | { |
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 | { |
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
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 | // .eslintc |
See how these rules are specified in the eslint-plugin-cypress-dev
in the exported file
index.js
1 | module.exports = { |
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 | { |
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.