Fine-Tuning AirBnB's ESLint Config

ESLint was launched in June 2013 and has rapidly become the most popular JavaScript linter. It offers a number of advantages over other linters including fine-grained, configurable rules and plugin support. This flexibility can also be a weakness, however, as you can easily get lost in the multitude of options.

For this reason, many developers have turned to a predefined rule set such as eslint-config-airbnb, based on the AirBnB JavaScript Style Guide.

But why have such a flexible linter if we end up using a prepackaged configuration? Instead, let's take the predefined rule set and improve it with a few modifications. In the rest of this post, I describe my personal changes to the AirBnB rules and the reasoning behind each change.

{
  "extends": "airbnb",
  "rules": {
    "max-len": [1, 120, 2, {ignoreComments: true}],
    "quote-props": [1, "consistent-as-needed"],
    "no-cond-assign": [2, "except-parens"],
    "radix": 0,
    "space-infix-ops": 0,
    "no-unused-vars": [1, {"vars": "local", "args": "none"}],
    "default-case": 0,
    "no-else-return": 0,
    "no-param-reassign": 0,
    "quotes": 0
  }
}

In general, I find the original AirBnB style guide to be too strict. And I don't like writing worse code just to satisfy linting rules. You can disable rules using code comments for individual cases, but this is impractical for more frequent violations. So almost all my changes are about relaxing rules. The exceptions are max-len and quote-props, which I felt were lacking from the AirBnB rules.

Let's go through these changes one by one:

max-len [1, 120, 2, {ignoreComments: true}]

Although I have a ruler in my editor, I still like to see an explicit warning when a line is especially long. The traditional 80 characters is too short for my taste, however. (Try to configure AngularUI with this setting!). I currently have my editor ruler set to 100 characters and the linter warning to 120.

quote-props [1, "consistent-as-needed"]

consistent-as-needed is exactly what I want, i.e. don't use quotes for object keys if they are unnecessary, but if you need to quote one then quote them all. This disallows { foo: 1, 'class': 2} and similar.

no-cond-assign [2, "except-parens"]

except-parens mimicks default JSHint behavior. It prevent you from accidental assignment in an if or while condition, but still allows assignment if you explicitly wrap it in parentheses:

let matches;
if ((matches = args.match(/regexp1/))) { 
  ...
} else if ((matches = args.match(/regexp2/))) {  
  ...
} else if ((matches = args.match(/regexp3/))) {  
  ...
}

or

while ((item = queue.pop()) { ... }

Some may prefer to use the in-place directive /*eslint no-cond-assign: 0 */ instead of parentheses, but personally I find this too verbose.

radix 0

If the radix is not specified, JavaScript assumes 16 for arguments starting with 0x and 10 otherwise. Historically there was a problem with ES3 behavior, which treated a string as an octal number if the argument started with 0. This could lead to unexpected results if a decimal number happened to have a leading zero.

ES5 removed this odd behavior, and as far I know last browser to implement it was IE8. And no one uses IE8 anymore, right? Right! So I've chosen to save a bit of typing and omit explicit radixes.

space-infix-ops 0

I turned this off because it strikes me as overly general and simplistic. You might want to use spaces for:

const foo = x + 3;

But you will probably prefer to omit some spaces in some more complex expressions:

const foo = 2*x + 1;
const c = (a+b) * (a-b);

I find that I get more readable expressions if I have the freedom to format them as I see fit.

no-unused-vars [1, {"vars": "local", "args": "none"}]

Firstly, I changed the whole rule from an error to a warning because unused variables may exist during
development or refactoring just because the code is not finished yet.

Secondly, unused variables are allowed for function arguments. I often use unused args to conform to some standard API, such as:

new Promise(function(resolve, reject) {
  setTimeout(resolve(1), 100);
});

Another example is keeping the arguments from the parent class in an overridden method even if the child class doesn't need them.

default-case 0

I omit the default clause from switch statements more often than not because switch is generally used with an internal variable that can never contain values not covered by case clauses. You can use default to throw an exception, but your unit tests are a much better place to make these checks. Or you can write special comments like // no default at the end of each switch statement just to keep the linter happy. But that is annoying.

no-else-return 0

At first glance this seems reasonable, but I realized that in many cases omitting the else branch looks odd or is simply less readable. As a rule of thumb, I use else when both branches represent more or less equally valid code paths. When the if condition is more of an exceptional state, I omit the else statement.

no-param-reassign 0

Unfortunately there is no rule that allows reassigning arguments only at the begining of function scope, and I want to use reassigment to set defaults.

function (data) {
   data = data || 0;
   ...
}

This can be avoided in ES6 with default arguments, but they can't used in all cases, for instance making a defensive copy:

function (data) {
   data = _.clone(data);
   ...
}

You can use a _ prefix, but I find this ugly.

function (_data) {
   const data = _.clone(_data);
   ...
}

Renaming the variable to dataCopy or the like is even uglier, and in any case the reassignment ensures that the function cannot accidentally access the original parameter. So I just disabled no-param-reassign. Ideally ESLint would be extended with an option similar to vars-on-top so I filed an issue proposing this.

quotes 0

I disabled the quotes check since I've adopted the following style:

  • single quotes: anything with "identifier" semantics (keys, constants, etc.)
  • double quotes: human-readable text messages

In practice this produces quoting quite close to original AirBnB setting [2, "single", "avoid-escape"], at least in English, because English text messages often contains an apostrophe so you need to use double quotes to avoid escaping.

I prefer a more relaxed configuration because if you have a list of messages, some of which contain apostrophes, you can consistently use double quotes:

consts ERRORS = {
  E0: "Request timed out",
  E1: "Sorry, we can’t change your account right now.",
}

Which is better than:

consts ERRORS = {
  E0: 'Request timed out',
  E1: "Sorry, we can’t change your account right now.",
}

new-cap

In addition my ESLint configuration contains project-specific settings, e.g. to allow uppercase functions from ImmutableJS:

"new-cap": [2, {
  "capIsNewExceptions": ["Immutable.Map", "Immutable.Set", "Immutable.List"]
}]
Roman Krejčík

Roman Krejčík