A linter is a static analysis tool that examines source code without executing it and reports likely bugs, suspicious constructs, security risks and violations of agreed coding conventions. Among development tools, the linter is the fastest automated check a programmer gets: it runs in the editor while code is being typed, in pre-commit hooks and in continuous integration, long before tests or code review.
The name comes from lint, a Unix program written at Bell Labs in the late 1970s that flagged questionable C code the compiler accepted. Today almost every language has one or more linters, and each project typically configures a rule set that reflects its own standards.
How a linter analyzes code
A linter works by parsing source code into a syntax tree and then running a set of rules over that tree. Most rules are pattern checks: a rule visits specific node types, such as comparisons or function declarations, and reports a problem when the node matches a known risky pattern, for example comparing with == where strict equality is expected.
More advanced linters also build scope and data-flow information. With it they can detect an unused variable, a variable used before assignment, unreachable code, or a value that may be null when dereferenced. Linters with access to type information, such as typed rules for TypeScript, can catch misuse of promises or incorrect API calls that purely syntactic rules miss.
Each finding carries a rule identifier, a location and a severity. Many rules also provide an automatic fix, so the linter can rewrite simple problems itself, such as removing unused imports or adding a missing semicolon.
| Rule category | Example finding | Why it matters |
|---|---|---|
| Possible errors | Variable used before assignment | Runtime failure or wrong result |
| Suspicious patterns | Assignment inside a condition | Usually a typo for comparison |
| Unused code | Unused variable or import | Dead code hides real intent |
| Security | Use of eval or string-built SQL | Injection vulnerabilities |
| Complexity | Function with very high cyclomatic complexity | Hard to test and maintain |
| Style and conventions | Naming that breaks project rules | Inconsistent, harder-to-read code |
Linters for common languages
Linters are usually language-specific because they depend on the language’s grammar and semantics. Many ecosystems have one dominant tool, and several now offer extremely fast implementations written in compiled languages.
| Language | Common linters | Notable characteristics |
|---|---|---|
| JavaScript and TypeScript | ESLint, Biome | Plugin-based rules, type-aware checks |
| Python | Ruff, Pylint, Flake8 | Ruff combines many rule sets with high speed |
| Go | go vet, staticcheck, golangci-lint | Aggregators run many analyzers together |
| Rust | Clippy | Hundreds of lints integrated with the compiler |
| Java | Checkstyle, PMD, Error Prone | Style, bug patterns and compile-time checks |
| Shell scripts | ShellCheck | Quoting, portability and word-splitting errors |
| CSS | Stylelint | Invalid properties and convention checks |
Rule sets are stored in a configuration file in the repository, so every developer and every CI run applies the same rules. Projects typically start from a shared preset and then enable, disable or adjust individual rules.
Linter compared with formatter, compiler and type checker
A linter overlaps with several other development tools, and modern pipelines use them together. The compiler rejects code that is invalid in the language, while a linter flags code that is valid but probably wrong or unwise. A formatter such as Prettier or Black only rewrites layout, whereas a linter reasons about meaning.
| Tool | Main question answered | Changes code | Example |
|---|---|---|---|
| Linter | Is this code risky or against conventions | Optional automatic fixes | ESLint, Ruff |
| Formatter | Is the layout consistent | Yes, always | Prettier, Black, gofmt |
| Type checker | Are types used correctly | No | TypeScript compiler, mypy |
| Compiler | Is this valid code | No, produces output | javac, rustc |
| Deep static analyzer | Are there complex defects across files | No | Taint and data-flow scanners |
A practical rule is to let the formatter own layout and the linter own correctness. Running both with overlapping style rules produces conflicting fixes, which is why many linters now ship configurations that disable purely stylistic rules.
Integrating a linter into the workflow
A linter is most effective when it runs at several points with the same configuration. Editor integration, such as extensions in Visual Studio Code or other IDEs, shows findings as the code is typed. A pre-commit hook blocks commits that introduce new violations, and the CI pipeline fails a pull request when the rules are broken.
- Treat errors and warnings differently: errors fail the build, warnings mark areas to improve.
- Lint only changed files in pre-commit hooks to keep feedback within a few seconds.
- Pin the linter version so an upgrade with new rules does not unexpectedly break builds.
- Require a reason for inline suppressions so disabled rules can be reviewed later.
Automating style and simple bug checks also improves code review, because reviewers no longer spend comments on spacing or unused variables and can focus on design and logic.
Limitations and common pitfalls
A linter can only check what its rules describe, and it never proves that code is correct. False positives are the main practical problem: when a linter reports many findings that developers consider irrelevant, they start ignoring all of them, including the important ones.
| Pitfall | Effect | Remedy |
|---|---|---|
| Enabling every rule at once | Thousands of findings, team ignores output | Start with a recommended preset |
| Linting legacy code without a baseline | Old violations block all work | Record a baseline, enforce on new code |
| Blanket suppressions | Real problems hidden | Suppress per line with a reason |
| Conflicting formatter rules | Fixes undo each other | Disable stylistic lint rules |
| Local and CI versions differ | Passes locally, fails in CI | Pin versions in the lockfile |
Stripped to the essentials
A linter is like a spelling and grammar checker for computer code. It reads what a programmer has written and underlines anything that looks like a mistake. It also flags anything that breaks the team’s agreed writing rules. It does this without running the program.
Some warnings point to real bugs, like a value that is never used or a typo that changes the meaning. Others are about keeping the code tidy and consistent. Catching these problems early is cheaper than finding them after the software is in use.
Where it is applied
A team of eight developers maintains a TypeScript web application of about 150,000 lines. Production incidents keep tracing back to the same causes: promises that are created but never awaited, and leftover debugging code that ships to users. Code reviews take long because comments focus on formatting.
The team adopts ESLint with type-aware rules that flag floating promises, adds Prettier for layout, and records a baseline of existing violations so only new code must comply. The linter runs in the editor, in a pre-commit hook on changed files and in the CI pipeline. Within a few months, incidents caused by unawaited promises stop recurring, and review discussions shift from spacing to design decisions.