ESLint Part 1: Exploration
2016 Jun 14
I recently spent some time to review my ESLint setup. It’s got lots of rules, and a healthy ecosystem of plugins. It’s intimidating! Maybe I can help you make some sense of it…
Why ESLint?
For a long time, JSLint, then JSHint and JSCS were the primary Javascript linting tools. They were important, because Javascript is such an easy language to make mistakes in. To wit, JSLint originally came from the author of Javascript: The Good Parts, which famously covers a small subset of the language. Of course, the rest is suggested to be ‘the bad parts’ - JSLint was designed to help you avoid them!
There are three major factors that caused ESLint to become the gold standard for linting:
1. Support for modern Javascript
React was released in early 2013, and with it came a return to compiling something other than pure Javascript into its final browser-ready form. CoffeeScript was popular for a while, but seemed more like unnecessary syntactic sugar. React, however, was substantially easier to use with JSX. To sweeten the pot, tools to translate JSX also included support for ES2015. And none of the leading linters could handle JSX or modern Javascript syntax.
2. Plugin architecture
While the previous linting tools did have a large set of options to configure, adding something like JSX support required a fork of the project. In ESLint, however, support for something totally new could be added via plugin. All sorts of plugins are available today, vastly expanding upon ESLint’s core value proposition.
3. No rules on by default
Of course, ESLint does have a substantial and very useful set of built-in rules. When ESLint started out in June 2013 it was just like its predecessors in that it was quite opinionated, preloaded with a default set of enabled rules. But all rules are now off by default, as of the version 1.0.0, released in mid-2015. ESLint is no longer an opinionated tool, but a blank slate, ready to be molded into something useful.
Jumping in
I was a late adopter of ESLint. I got away with it because I didn’t really like JSX when I was first using React, so JSHint and JSCS were good enough for me. Thus, ESLint was a big project for indeterminate gain. Eventually I gave in, took the plunge, and upgraded my standard approach: Webpack, Babel, and ESLint instead of RequireJS and JSHint.
I figured that the easiest way to jump in was a preset. The AirBnB ESLint configuration had a pretty good ethos: prefer the ES2015 way of doing things. What better way to jump into the new syntax with both feet?
This served me well for a while, but I slowly realized that I was being kept from detail I cared about. Yes, because of ESLint’s excellent support for building upon others’ configurations, I could make any customization I wanted on top of the config I was extending. But it encouraged me to take what was given to me. And I was sure I was missing something.
Making it my own
Recently, instead of upgrading to the latest AirBnB config, I embarked on my own odyssey of ESLint discovery. My first step was copying all of AirBnB’s configuration into a single .eslintrc.js
file. It felt really good having it all in front of me like that - I could clearly see how many rules were disabled!
And then I went, rule by rule, attempting to turn everything on. But I didn’t do it in a vacuum; I used an existing project with a couple thousand lines of code to test each change. In some cases I absolutely changed a lot of code ('comma-dangle': ['error', 'always-multiline']
) and in others I wanted to turn it on but the current codebase didn’t really allow it ('max-statements': 'off'
).
When I was done, I wanted to be sure. I went looking for something that would guarantee that I had made a decision for every rule, and found the find-eslint-rules
node module. Once installed, I had a new npm
script to tell me whether I had configured, one way or another, every available rule available.
Expanding horizons with plugins
I felt good, but I wasn’t done. A large amount of the value in the world of ESLint comes from its ecosystem. I needed to explore the world of ESLint plugins. I had encountered some via the AirBnB config, some via newsletters and social media, but I wanted to do a sweep myself.
And so, I went deep into the NPM registry with a generic ‘eslint-plugin’ search. Page after page, collecting plugins that looked interesting. Here are some I found notable…
General
eslint-plugin-import
is an amazing plugin, alerting you to missing or malformed require()
or import
statements. Before, you had to run your code to catch incorrect references! It made me realize the kinds of high-value things ESLint could do.
eslint-plugin-security
is similarly creative. The Node.js Security Project reviews node modules and their nsp
tool checks for vulnerable node modules in your dependencies. This plugin helps them do that job, and it can help you with your project as well. Note: it will fire false positives!
eslint-plugin-better
draws from more Douglas Crockford wisdom, attempting to limit you to the ‘good parts’ of Javascript. If you have some features of Javascript you want to avoid in your project, this plugin might be for you!
eslint-plugin-filenames
makes filenames consistent across your project. It’s about more than just consistency - incorrect casing can cause problems on different OSes, since some are case-sensitive, some aren’t. You definitely don’t want something that works locally to break only the server, right?
React
eslint-plugin-react
is a must-have if you’re using React with JSX. It will communicate that variables used inside of JSX are used globally, so ESLint’s no-unused-vars
rule stops firing. It can also push you towards simple Stateless Components when appropriate, and definitely helps with standardizing JSX formatting across your project.
eslint-plugin-jsx-a11y
helps you remember to include accessibility attributes in your JSX. It’s so easy to forget about this stuff. I very much recommend using this plugin.
Functional design
eslint-plugin-immutable
is a simple little plugin that keeps me honest when I’m trying to use functional design in my projects. No let
keyword, no mutation of objects, and no this
. Sadly, it doesn’t work very well with CommonJS.
eslint-plugin-no-loops
takes it even further, pushing you towards map()
and reduce()
instead of iteration. It’s a great exercise to implement something without loops when you’d usually reach for a loop. Quite frequently I find that the logic gets simpler and easier to reason about.
Testing
eslint-plugin-chai-expect
is a tiny little plugin. It checks for common assertion mistakes. My favorite is terminating-properties
which reminds you whether it should be expect().to.exist
or expect().to.exist()
. It’s the former. :0)
eslint-plugin-bdd
, another plugin, protects against something I’ve accidentally included in pull requests a couple times. Without an easy way to --grep
your tests runs, it’s common to use .skip()
or .only()
to exclude or run a subset of your tests. Use it, but don’t commit it!
Funny
eslint-plugin-json
is not a plugin I’d recommend. I like JSON format checking as much as the next guy, but this one pulls in all of JSHint just to check for well-formed JSON!
eslint-plugin-no-js
is very opinionated. It recommends that you dispense with Javascript entirely. As it says, “your code sucks.” :0)
Close, but not quite
eslint-plugin-graphql
was so close! I really like GraphQL and I use it. I just don’t use it in a way that would make using this plugin easy since I’m not using Relay. I’ll probably get there, especially if I submit a PR!
eslint-plugin-promise
is needed. I see incorrect, error-swallowing use of promises all the time. But this plugin has some key bugs. Again, I’ll probably come back to this one later.
So many!
I tried others that didn’t make the cut, and there are certainly plugins I missed entirely. Let me know if there’s one you find useful that’s not in this list!
Announcing: My Configuration!
After all my explorations, including tests in a number of projects, I decided to bring all that learning together into one core project: @scottnonnenberg/eslint-config-thehelp
, available via npm
now! It greatly shrank my .eslintrc.js
file in each project, and makes it far easy to start up a new project.
One install
I wanted to make it a single install, so my configuration includes ESLint and all needed plugins. And this means, given ESLint plugin resolution rules, that you need npm
version 3 and its flat node_modules
directory structure. It seems like a reasonable tradeoff. peerDependencies
are messy.
Principles
As I was coming up with my ruleset, I kept a couple key principles in mind. First:
Rules specify the 98% case.
eslint-disable
will cover the rest. I can periodically search for those exceptions and re-verify their need.
Additional more-specific principles emerged as I refined my approach:
- Always use ‘error’ or ‘off’ instead of 0 and 2. Numbers are for real config values.
- No warnings. Either disallow it completely, or don’t worry about it at all.
- All ‘off’ rules must have a reason mentioned in comments.
- Don’t include any configuration if that configuration is the same as the defaults.
- Rules are in alphabetical order: first core ESLint, then plugins sorted by name
- All rules from core and plugins must be included
Multiple configurations
In my explorations I discovered a couple different types of configurations I’d need, so I included this set in the package:
@scottnonnenberg/thehelp/core
- node modules targeting downlevel versions of Node.js, or without precompilation before the browser.@scottnonnenberg/thehelp/es2015
- modern Javascript, preferring new techniques when applicable.@scottnonnenberg/thehelp/functional
- when I want to use functional design principles.@scottnonnenberg/thehelp
(default) - the combination of all three above configurations@scottnonnenberg/thehelp/react
- when writing React applications@scottnonnenberg/thehelp/scripts
- relaxed rules for a project’sscripts/
directory@scottnonnenberg/thehelp/test
- additional and relaxed rules for a project’stest/
directory
With ESLint it’s really easy to choose one or more of these to merge into your final configuration:
{
extends: [
'@scottnonnenberg/thehelp/core',
'@scottnonnenberg/thehelp/es2015',
'@scottnonnenberg/thehelp/react',
],
}
Testing
It’s worth talking about my test strategy for the package. Sadly, I have had to release new versions because ESLint plugin resolution rules don’t work with npm link
. Thus it’s a bit hard to test.
But I can do these things, and I’d recommend them for you too:
- Check for missing rules - I include a
eslint-find-rules
run in my overallnpm test
command, which ensures that I’ve configured everything. When new rules are released, you’ll be sure to catch them! - Check for well-formed configuration - I discovered ESLint’s built-in validator via 1000ch’s
eslint-config
tests. You’ll at least be sure that your configuration loads successfully.
Optimizations
While digging pretty deep into ESLint, I discovered a couple things I didn’t know before. Maybe you’ll find them useful?
Hierarchical configuration
You probably have one .eslintrc.js
or eslintrc.yml
in your root project folder. But did you know that you can put one of these files in any folder, and eslint
will pick it up? What’s more, ESLint will merge configuration in parent directories with that for the current directory. That’s why I added the test
and scripts
configurations to @scottnonnenberg/eslint-config-thehelp
.
For example, in test/.eslintrc.js
I can include this:
{
extends: [
'@scottnonnenberg/thehelp/test',
],
}
All of my global project configurations will apply, and the changes in the test
configuration will be merged in. Fancy!
Editor Integration
I resisted adding a linter into my project for a long time, preferring to wait to do a test/lint run later. But the additional value ESLint provides over classic linting is useful real-time. And with SublimeText 3, SublimeLinter, and its ESLint plugin it’s all very fluid, no configuration fuss or undue delays.
I love getting immediate feedback about incorrect paths in require()
or import
. The red everywhere when I start a new file can be annoying, but I think it’s worth it.
Gotchas with babel-eslint
babel-eslint
is an alternate parser which can be useful if you’re using bleeding edge Babel language constructs. In my case, I like to use static properties in my React components for propTypes
.
It doesn’t always work smoothly, however. Sometimes bugs are fixed for the default parser but not for babel-eslint
.
I ran into some performance issues as well. If ESLint is slow for you with babel-eslint
, first ensure you installed everything with npm
3.x, which automatically de-dupes modules. Then, double-check the versions of all your top-level Babel-related dependencies. I saw a big improvement when I made a couple key versions consistent:
Before (17.8 seconds)
{
"babel-cli": "6.7.7",
"babel-eslint": "6.0.4",
"babel-preset-es2015": "6.6.0",
}
After (4.9 seconds)
{
"babel-cli": "6.9.0",
"babel-eslint": "6.0.4",
"babel-preset-es2015": "6.9.0",
}
Use it!
ESLint is an amazingly useful tool. If you’re not using it today, I recommend that you start. If you already are, I hope this article helps you leverage it fully!
And you can use @scottnonnenberg/eslint-config-thehelp
directly, submit pull requests to improve it, or fork it and make it your own!
But first check out the next post in the series: ESLint Part 2: Contribution, about contributing back to the ESLint ecosystem. Or skip to the final post, ESLint Part 3: Analysis!
NEXT:
ESLint Part 2: Contribution 2016 Jun 16
I recently wrote about my ESLint exploration and configuration node module. But I went further than that - I contributed back to the ESLint community! I submitted several pull requests and released a... Read more »
PREVIOUS:
Customizing Agile 2016 Jun 07
Ever since the term Agile started to take hold in larger organizations, there’s been a growing backlash among developers. Sadly, this is to be expected. As I pointed out in my last post on Agile... Read more »