Linting setup
A linter tuned to shout about every stylistic preference trains the team to
ignore it, and the one rule that would have caught a real bug scrolls past
in the noise. A useful lint config is triaged: bug-finding rules are
errors, style is left to the formatter, and every suppression is narrow and
justified.
Method
- Split rules into bugs, style, and noise. Set bug-finding rules to
error (
no-unused-vars, no-floating-promises,
react-hooks/exhaustive-deps, eqeqeq, clang-tidy bugprone-*). Hand
pure-style rules to the formatter and turn them off. Turn opinion-only
rules off, not warn: warnings accumulate and get ignored.
- Start from a vetted base, then subtract. Extend
eslint:recommended, @typescript-eslint/recommended, ruff's default
set, or golangci-lint's defaults, and disable the specific rules that
fight your codebase rather than assembling a config from nothing. Record
why each disable exists in a comment beside it.
- Make warnings fail or make them nothing. Run with
--max-warnings 0
in CI so warning and error collapse into one bar: the build passes or it
does not. A pipeline that tolerates 400 warnings is a pipeline with no
linter.
- Suppress narrowly and with a reason. A suppression names one rule on
one line with a why:
// eslint-disable-next-line no-await-in-loop -- sequential rate limit. Ban file-wide eslint-disable and bare
# noqa; those switch the linter off for everything, not the one case.
- Enable type-aware rules where they exist.
@typescript-eslint with
type information, ruff's flake8-bugbear set, and clang-tidy's analysis
catch defects a syntactic pass cannot: unhandled promises, mutable
default arguments, use-after-move. They cost CI seconds and earn them.
- Ratchet on legacy code, do not boil the ocean. Lint only changed
files in CI (lint-staged or a diff filter) so new code meets the bar
while the backlog shrinks with each touch. A one-shot fix of 5000
findings gets reverted; a ratchet holds.
Signals
- Does a lint error reliably mean a bug or a real risk, not a style
opinion?
- Does every suppression in the tree name a rule and a reason?
- Is the CI warning budget zero, or is it quietly unbounded?
Boundaries
Linting overlaps a real type checker but does not replace one: for deep
correctness lean on tsc, mypy, or the compiler. Formatting stays with the
formatter. When a rule and the team's actual practice disagree, decide once
and encode the decision in config, not in repeated review comments.
1---2name: linting-setup3description: Triage lint rules so bug-catchers are errors, style is left to the formatter, and every suppression carries a reason. Use when configuring a linter, taming warning noise, or setting a suppressions policy.4---56# Linting setup78A linter tuned to shout about every stylistic preference trains the team to9ignore it, and the one rule that would have caught a real bug scrolls past10in the noise. A useful lint config is triaged: bug-finding rules are11errors, style is left to the formatter, and every suppression is narrow and12justified.1314## Method15161. **Split rules into bugs, style, and noise.** Set bug-finding rules to17 error (`no-unused-vars`, `no-floating-promises`,18 `react-hooks/exhaustive-deps`, `eqeqeq`, clang-tidy `bugprone-*`). Hand19 pure-style rules to the formatter and turn them off. Turn opinion-only20 rules off, not warn: warnings accumulate and get ignored.212. **Start from a vetted base, then subtract.** Extend22 `eslint:recommended`, `@typescript-eslint/recommended`, ruff's default23 set, or golangci-lint's defaults, and disable the specific rules that24 fight your codebase rather than assembling a config from nothing. Record25 why each disable exists in a comment beside it.263. **Make warnings fail or make them nothing.** Run with `--max-warnings 0`27 in CI so warning and error collapse into one bar: the build passes or it28 does not. A pipeline that tolerates 400 warnings is a pipeline with no29 linter.304. **Suppress narrowly and with a reason.** A suppression names one rule on31 one line with a why: `// eslint-disable-next-line no-await-in-loop --32 sequential rate limit`. Ban file-wide `eslint-disable` and bare33 `# noqa`; those switch the linter off for everything, not the one case.345. **Enable type-aware rules where they exist.** `@typescript-eslint` with35 type information, ruff's flake8-bugbear set, and clang-tidy's analysis36 catch defects a syntactic pass cannot: unhandled promises, mutable37 default arguments, use-after-move. They cost CI seconds and earn them.386. **Ratchet on legacy code, do not boil the ocean.** Lint only changed39 files in CI (lint-staged or a diff filter) so new code meets the bar40 while the backlog shrinks with each touch. A one-shot fix of 500041 findings gets reverted; a ratchet holds.4243## Signals4445- Does a lint error reliably mean a bug or a real risk, not a style46 opinion?47- Does every suppression in the tree name a rule and a reason?48- Is the CI warning budget zero, or is it quietly unbounded?4950## Boundaries5152Linting overlaps a real type checker but does not replace one: for deep53correctness lean on `tsc`, mypy, or the compiler. Formatting stays with the54formatter. When a rule and the team's actual practice disagree, decide once55and encode the decision in config, not in repeated review comments.