anatomy of an eslintrc

2020-12-28

 | 

~5 min read

 | 

919 words

Anatomy of an .eslintrc

I’ve written about ESLint a few times in the past - normally in the context of something breaking or setting up a new project, but I’ve never spent much time discussing the anatomy of an ESLint config file.

I want to change that. The docs do a great job of actually explaining things, however, if you’re new and don’t know what you’re looking at or for, they can be intimidating (they were for me!).

Consider the following javascript file:

main.js
"use strict"

const username = "freddy"
typeof username === "strng" // whoops, typo!

if (!"serviceWorker" in navigator) {
  // you have an old browser :-(
}
const foo = undefined
const greeting = "hello" // hmm, this seems unused
console.log("hello, world!")[(1, 2, 3)].forEach((x) => console.log(x)) // unexpected new line!

Even assuming that we’ll use babel to transpile our code, there are a few problems here that a static analysis tool like ESLint can help us catch. We just need to configure it to help us!

Here’s an example .eslintrc.js file that I saved to the root of the project:

.eslintrc.js
module.exports = {
  parserOptions: {
    ecmaVersion: 12,
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
  },
  // extends: "eslint:recommended",
  rules: {
    strict: ["error", "never"],
    "valid-typeof": "warn",
    "no-unsafe-negation": "error",
    "no-unused-vars": "error",
    "no-unexpected-multiline": "error",
    "no-undefined": "error",
  },
}

I’ve organized this example to loosely follow the Doc’s Table of Contents. Looking just at the first few sections of the table of contents, we can get a good idea of what constitutes an eslintrc file:

  1. Parser Options
  2. Parser
  3. Processor
  4. Environments
  5. Globals
  6. Plugins
  7. Rules

There are many additional features beyond this. Even this list may be more than what’s necessary to start getting value out of ESLint as I’ll focus on Parser Options, Environments, and Rules to begin.

Parser Options

First up is the Parser Options. This specifies which language to support as well as features. In our case, we’re supporting the latest and greatest and trusting Babel to do its job - ecmaVersion: 12 is equivalent to supporting ES2021.

Instead of scripts, we’ll be using modules to split up the code and I’m planning to use JSX later, so I’ve enabled that.

Environments

The env block specifies the environment in which the code will run. This is important for determining which global variables will be available. For example, window and console are available in the browser. They’re not in node.

You can have multiple environments targeted, however, this will then make it an author time responsibility to ensure that globals are accessed appropriately based on where the code will run (i.e. don’t try to access window on a backend file).

.eslintrc.js
{
    "env": {
        "browser": true,
        "node": true
    }
}

All of the available globals are listed in the docs here.

Rules

When it comes to which rules we actually want to enforce, I’ve included three different approaches:

.eslintrc.js
{
    rules: {
        strict: ["error", "never"],
        "valid-typeof": "warn",
        "no-unsafe-negation": "error",
        "no-unused-vars": "error",
        "no-unexpected-multiline": "error",
        "no-undefined": "error",
    },
}

Rules typically have three options:

  1. "off"/0,
  2. "warn"/1,
  3. and "error"/2
Using numeric values instead of text for rules

In the example above, I opted to type out how I wanted to treat the rules with the full text, however, the following would have been equivalent:

.eslintrc.js
{
    rules: {
        strict: [2, "never"],
        "valid-typeof": 1,
        "no-unsafe-negation": 2,
        "no-unused-vars": 2,
        "no-unexpected-multiline": 2,
        "no-undefined": 2,
    },
}

Some rules, like strict are configurable and have options. When that happens, the first value is how you want ESLint to respond to the rule, and the other values in the tuple the options.

Rule Sets

While being able to specify rules is nice, there are a lot of rules (here is the full list of rules). Fortunately there are different rule sets that we can apply to get a baseline (by default, no rules are enabled).

.eslintrc.js
{
    extends: ["eslint:recommended"],
    rules: {
        strict: ["error", "never"],
    },
}

Here, I’ve enabled the ESLint recommended rules and then specified how to treat strict, which is not part of the basic set.

If we had multiple rule sets, we could add them to the extends keyword. Order does matter. In the event of overlap, later rule sets override previous ones.

.eslintrc.js
{
    extends: ["eslint:recommended", ...],
    rules: {
        strict: ["error", "never"],
    },
}

Similarly, rules will override any rules set in the extends.

Testing Our Static Analysis

At this point, we’ve seen how to configure a basic ESLint file to specify certain rules. But how do we actually use it?

We could add a script to our package.json:

package.json
{
  "scripts": {
    "lint": "eslint ."
  }
}

Or, we could run it ad hoc from the command line:

$ npx eslint .

or

$ node_modules/.bin/eslint .

This script will error if violations of rules are found (and ESLint was configured to view these violations as errors, not warnings). As a result, ESLint can be inserted into a continuous integration / continuous deployment pipeline to ensure that no errors slip through (at least errors in the class ESLint can detect).

Conclusion

While I’ve used ESLint many times before, it’s taken me until now to really dig into the documentation to understand how it works and feel comfortable configuring my own. As I noted above, there’s still a ton left to learn. All in due time.


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!