Base
Principles
Syntax
- Using Prettier for better formatting.
- Syntactic homogeneity. Semantically identical constructions must use the same syntax. To achieve that we fix spacings, paddings, quotes, shorthand techniques, etc. E.g.,
curly
anddot-notation
. - Minimization of git diffs when code is being changed or refactored. Changing or extending code must change minimal count of symbols which aren't connected with the change in logic. By the most part this is achieved by using Prettier.
- Avoid disabling linting rules. We believe that linting rules shouldn't interfere with writing code. Disabling rules is allowed as an exception and a temporary measure. Because of this, we don't use frequently disabled rules like
no-mixed-operators
,prefer-arrow-callback
,no-useless-return
, etc. - No warnings. All kinds of undesirable constructions must be fixed. Warning add noise to the result of project building and quickly become being ignored. As an exception warnings are allowed in a situation of a long refactoring but only as a temporary measure. [~]
Semantics
- Explicit is better than implicit. JS has many implicit defaults so to avoid confusion we prefer being explicit and not using ambiguous constructions. E.g.,
no-implicit-coercion
. - Minimization of errors. JavaScript is an interpreted language so all the incorrect or meaningless constructions can be detected only during execution. We try to minimize the amount of constructions that can lead to runtime errors. E.g.,
guard-for-in
andeqeqeq
. - Make code readable without limiting developers too much. We don't want for linting to be in the way of writing logic so we don't use rules like
complexity
ormax-depth
. But we still try to restrict semantic constructions which make reading code difficult and use rules likemax-params
orno-nested-ternary
. - Make sure code is ready for production environment. Code shouldn't contain temporary or debug construction. E.g.,
no-eval
,no-console
,no-debugger
, etc. [~]
Rules overview
Prettier
eslint-plugin-prettier
is used for integration of Prettier and ESLint.
eslint-config-prettier
turns off all the rules from ESLint and its plugins that might conflict with Prettier.
[~]
ESLint rules
We are using eslint:recommended
as starting point. You can see list of included rules here marked with a check mark.
[~]
camelcase
// Fail
const my_var = { prop_name: 1 }
// Pass
const myVar = { propName: 1 }
Names starting with UNSAFE_
are allowed to support React's legacy lifecycle methods:
class MyComponent extends React.Component {
UNSAFE_componentWillUpdate() {}
}
curly
// Fail
if (true) return null
// Pass
if (true) { return null }
default-param-last
// Fail
function f(foo = 0, bar) {}
// Pass
function f(bar, foo = 0) {}
dot-notation
// Fail
obj['param']
// Pass
obj.param
eqeqeq
Prefer strict equals to avoid implicit checks.
// Fail
if (x == null) {}
// Pass
const isNil = (v) => v === null || v === undefined
if (isNil(x)) {}
guard-for-in
For iterating over object keys use Object.keys
instead of for ... in
as it returns object's own property names and ignores inherited ones.
// Fail
for (key in obj) { }
// Pass
Object.keys(obj).forEach((key) => {})
max-params
Functions may have maximum 3 parameters:
// Fail
const myFn = (a, b, c, d) => {}
// Pass
const myFn = ({ a, b, c, d }) => {}
If you're using reselect
and errors are reported for createSelector
with too many params, switch to createStructuredSelector
.
[~]
no-bitwise
Quite often bitwise operator is just a typo in logical operator:
// Fail
const x = y | z
// Pass
const x = y || z
If you need to use bitwise operators, disable this rule for the lines containing them. [~]
no-console
Remove console.log
from production code:
// Fail
console.log('Debug info')
// Pass
console.info('Info')
console.warn('Warning')
console.error('Error')
no-eval
Forbid usages of eval
as potentially dangerous:
// Fail
eval("var a = 0")
no-implicit-coercion
All implicit coercions except !!
are disallowed:
// Fail
+foo
1 * foo
'' + foo
`${foo}`
~foo.indexOf(bar)
// Pass
!!foo
no-nested-ternary
// Fail
return a ? b ? c : d : e
// Pass
if (a) {
return b ? c : d
} else {
return e
}
no-new-wrappers
// Fail
const n = new Number(1)
// Pass
const n = 1
no-param-reassign
// Fail
function f(foo) {
foo++
}
// Pass
function f(foo) {
const bar = foo + 1
}
no-shadow
// Fail
rows.forEach((item) => {
item.columns.forEach((item) => {
// `item` is shadowed
})
})
// Pass
rows.forEach((row) => {
item.columns.forEach((column) => {
})
})
no-template-curly-in-string
// Fail
"Hello, ${name}"
// Pass
`Hello, ${name}`
no-throw-literal
// Fail
throw 'Error'
// Pass
throw new Error('Error')
no-undef-init
// Fail
let foo = undefined
// Pass
let foo
no-underscore-dangle
// Fail
const _private = 1
// Pass
items.map((_item, idx) => idx)
no-unneeded-ternary
// Fail
const isYes = answer === 1 ? true : false
// Pass
const isYes = answer === 1
no-unused-expressions
// Fail
a || b
(function f() {})
// Pass
a && b()
a ? b() : c()
no-unused-vars
// Fail
const { foo, bar } = props
console.log(foo)
// Pass
const { foo } = props
console.log(foo)
Hanging unused parameters in functions are not allowed:
// Fail
function f(a, b, c, d) {
console.log(b)
}
// Pass
function f(a, b) {
console.log(b)
}
Omitted params in object destructuring can be left unused:
// Pass
const { foo, ...rest } = props
console.log(rest)
no-var
// Fail
var foo
// Pass
let foo
object-shorthand
// Fail
const a = { x : x }
// Pass
const a = { x }
one-var
// Fail
let foo, bar
// Pass
let foo
let bar
operator-assignment
// Fail
x = x + y
// Pass
x += y
padding-line-between-statements
We only configure paddings between exports and other statements to emphasize exported values. You may customize this rule in your project depending on your needs.
// Fail
const a = 1
export const b = 2
// Pass
const a = 1
export const b =2
prefer-const
// Fail
let a = 3
// Pass
const a = 3
radix
ES5 no longer treats strings with leading 0 as octal literal, so there's no need to pass the radix 10:
// Fail
const n = parseInt('071', 10)
// Pass
const n = parseInt('071')
Disabled rules
no-irregular-whitespace
We disable this rule as it can't properly allow using non-breaking whitespaces inside JSX tags.
It forces to use
instead:
<>Push the button</>
Which leads to a problem when converting JSX text node to a string:
<>{'Push the button'}</>
If developer don't pay attention or text is too big to look through it properly,
moves into string without any warning and shows up in UI as a group of symbols, not a whitespace.
[~]
no-use-before-define
We don't enable this rule as it is often more practical to place usages before definitions. E.g., in React component it is better to place component code first and helper functions below. That way the most important thing in the file comes first. [~]
Unicorn
Additional rules from eslint-config-unicorn
.
[~]
unicorn/explicit-length-check
Checking for .length
of an empty array can lead to rendering 0
instead of not rendering anything. This rule forces to always explicitly check for array length by comparing it:
// Fail
return items.length && <Items />
// Pass
return items.length > 0 && <Items />
unicorn/filename-case
// Fail
my-component.js
my_component.js
// Pass
myComponent.js
MyComponent.js
en-US.js
unicorn/no-abusive-eslint-disable
// Fail
// eslint-disable-next-line
console.log(message);
// Pass
// eslint-disable-next-line no-console
console.log(message);
unicorn/throw-new-error
By design Error(msg)
and new Error(msg)
can be used interchangeably but for the sake of homogeneity it is better to always use only one form.
// Fail
throw Error()
// Pass
throw new Error()
We prefer new Error
for consistency with custom subclassed errors which cannot be created without new
:
class MyError extends Error {}
throw MyError() // Uncaught TypeError
throw new MyError() // OK
Rules for asynchronous code
Using eslint-config-promise
.
[~]
prefer-promise-reject-errors
// Fail
Promise.reject('Error')
// Pass
Promise.reject(new Error('Error'))
require-await
// Fail
async function f() {
doSomethingSynchronously()
}
// Pass
async function f() {
await doSomethingAsynchronously()
}
promise/catch-or-return
// Fail
myPromise.then(doSomething)
// Pass
myPromise.then(doSomething).catch(handleError)
promise/no-return-in-finally
// Fail
myPromise.finally(() => {
return 'done'
})
// Pass
myPromise.finally(() => {
alert('done')
})
promise/no-return-wrap
// Fail
myPromise.then((val) => Promise.resolve(val * 2))
myPromise.then(() => Promise.reject(new Error()))
// Pass
myPromise.then((val) => val * 2)
myPromise.then(() => {
throw new Error()
})
promise/param-names
// Fail
new Promise((reject, resolve) => {})
// Pass
new Promise((resolve, reject) => {})
Import rules
Using eslint-config-import
and eslint-config-unicorn
.
[~]
Extending plugin:import/errors
.
[~]
import/no-duplicates
// Fail
import { App } from './App'
import { SOME_CONST } from './App'
// Pass
import { App, SOME_CONST } from './App'
import/first
// Fail
init()
import { App } from './App'
// Pass
import { App } from './App'
init()
import/newline-after-import
// Fail
import { App } from './App'
render(App)
// Pass
import { App } from './App'
render(App)
import/no-cycle
// Fail
// a.js
import { b } from './b'
export const a = 1
// b.js
import { a } from './a'
export const b = 2
import/no-default-export
// Fail
export default function foo() {}
// Pass
export function foo() {}
import/order
Group imports by module type and sort them alphabetically inside a group.
Order of import groups: [~]
builtin
import * from 'path'
external
import * from 'lodash'
internal
import * from 'src/utils'
import * from '@/utils'
import * from '~/utils'
parent
import * from '../'
sibling
import * from './sibling'
index
import * from '.'
sort-imports
Sort import members:
// Fail
import { c, a, b } from 'module'
// Pass
import { a, b, c } from 'module'
unicorn/import-index
// Fail
import * from './index'
// Pass
import * from '.'
Rules for regular expressions
prefer-named-capture-group
We use named capture groups as they are more explicit while matching or replacing:
// Fail
const regexp = /(ba[rz])/
const groupResult = regexp.exec('bar')[1] // 'bar'
'foobar'.replace(regexp, '($1)') // 'foo(bar)'
// Pass
const regexp = /(?<named>ba[rz])/
const groupResult = regexp.exec('bar').groups.named // 'bar'
'foobar'.replace(regexp, '($<named>)') // 'foo(bar)'
If you don't need the result of capturing, mark group as a non-capturing:
// Pass
const regexp = /(?:ba[rz])/
prefer-regex-literals
// Fail
new RegExp("abc")
// Pass
/abc/
new RegExp(someVariable)
new RegExp(`${prefix}abc`)
unicorn/better-regex
// Fail
/[0-9]/
// Pass
/\d/