Code Quality Tools Deep Dive · 1 of 6

Linters — Catching Bugs Before Reviewers Do

A linter scans source for patterns that are usually mistakes — unused variables, accidental shadowing, missing await, wrong comparison operators, broken regex — and flags them as you type or in CI. It catches a class of bugs in seconds that human review would catch in hours, and ends the never-quite-resolved arguments about style by making the tool the arbiter.

ESLintPylint / RuffRuboCopCheckstylegolangci-lintclippy
← Back to Testing
Quick Facts

What a Linter Is

Basic Concepts

  • Static analysis, light edition. Reads code without running it; flags suspicious patterns and style violations.
  • Two roles, one tool: bug detection ("you forgot await") and style enforcement ("two-space indent").
  • Configurable severity: off / warn / error. Errors fail CI; warnings clutter logs; off means you've disabled a rule and should write down why.
  • Auto-fix: most modern linters can fix simple violations in place. Run --fix in pre-commit and PRs become diffs about meaning, not whitespace.
  • The rule: a linter is for things tools should decide. Things humans should decide go in code review.
By Stack

The Default Linter for Each Ecosystem

StackLinter(s)Notes
JavaScript / TypeScriptESLint (with typescript-eslint), BiomeESLint is the standard. Biome is a faster all-in-one alternative (lint + format).
PythonRuff, Pylint, Flake8Ruff has eaten most of the others — written in Rust, 10–100× faster, configurable.
RubyRuboCopStyle + correctness. Strong defaults; team-tunable.
JavaCheckstyle, SpotBugs, PMD, ErrorProneOften used together; each catches different things. ErrorProne integrates with the compiler.
Kotlinktlint, detektktlint for style; detekt for code-smell detection.
.NET (C#)Roslyn analyzers, StyleCopRoslyn analyzers are first-class; the IDE shows them as you type.
Gogolangci-lint (aggregator), staticcheck, govetgolangci-lint runs many linters in parallel.
RustclippyHundreds of high-quality lints; standard equipment.
SwiftSwiftLintStandard for Swift codebases.
PHPPHPStan, Psalm, PHP_CodeSnifferPHPStan/Psalm are heavier static analyzers; CodeSniffer for style.
SQLSQLFluffLints SQL across multiple dialects; auto-formats.
ShellshellcheckCatches the dozen ways shell quoting hurts you.
YAML / Markdownyamllint, markdownlintDon't ship inconsistent docs.
Setting Up

Working Practices

Adopt a Recommended Preset

Don't write your own ruleset from scratch. Start from the project's recommended config (eslint:recommended, typescript-eslint/recommended, ESLint's airbnb or standard, Ruff's defaults). Adjust as the team finds rules that don't fit; document each disable.

Run Locally and in CI

IDE integration shows lint errors as you type. Pre-commit hooks (Husky / lint-staged / pre-commit) catch them before push. CI runs the same check on PRs. The redundancy is the point: the earliest catch is the cheapest fix.

Auto-Fix in Pre-Commit

For style and trivial fixes, run --fix automatically. Review-time diffs become about logic, not formatting. Pair with a formatter (see formatters) to remove every "missed a space" PR comment forever.

Treat Warnings as Errors in CI

"Warning" tier exists for migration periods. Long-term, a tolerated warning is just clutter — and gradually becomes a normal part of the build output, hiding real issues. Either fix the rule or disable it; don't leave it warning forever.

Document Disables

// eslint-disable-next-line no-explicit-any -- third-party API forces this. The -- reason is mandatory in many shops. A disable without a reason is a TODO that never gets done.

Custom Rules for Real Patterns

Some bugs are project-specific: "never call db.query directly outside the repository layer." A custom Semgrep rule, ESLint rule, or PHPStan extension catches it forever. Cheaper than writing it in the style guide and hoping people read it.

Where It Hurts

Common Failure Modes

  • Bikeshedding. Lint configuration debates eat days. Pick a sensible preset; move on; revisit when something demonstrably hurts.
  • Slow lint runs. Linters that take a minute aren't run by anyone locally. Modern fast linters (Ruff, Biome, golangci-lint with caching) bring this back to seconds.
  • Disabling rules everywhere. A repo with 800 eslint-disable comments has stopped lint protection. Fix the rule fit, or accept the rule, or actually fix the code.
  • Over-strict rules on legacy code. Adding a strict ruleset to a 5-year-old codebase produces 50,000 warnings. Use baseline files (golangci-lint baseline, ESLint --quiet + ratchet) to enforce on new code only.
Continue

Other Code Quality Tools