Linter as Law
Every gate is an error, never a warning.
A standard nobody enforces is a standard nobody respects. The line that lives in CONTRIBUTING.md is a wish. The line that lives in eslint.config.mjs is law.
Opinion
I've worked on enough codebases that arrived with a glossy CONTRIBUTING.md and a `legacy/` folder the size of a small city to know which direction the gravity runs. The standard that cannot be broken is the only standard the codebase keeps. Every limit the team agrees to in a retrospective (“functions should be short,” “no `any`s, please,” “use `===`”) survives precisely as long as the next deadline. Encode it as a lint error and the deadline argues with the linter, not with the reviewer. The reviewer wins every time.
That is why so many of the principles a reader might expect to see as their own tenets do not appear. Strict equality is not a Style tenet because eqeqeq is one line of config. Annotated returns is not a Types tenet because @typescript-eslint/explicit-function-return-type already does the job. Direct imports is not an Architecture tenet because no-restricted-imports covers it. Type-file naming is not a Types tenet because unicorn/filename-case handles it. Tenets argue principles. The linter executes them. The two surfaces have different jobs.1
The corollary, the part Patterns and Architecture both keep colliding with, is that the linter has a ceiling.6 It cannot say that EmailNotificationServiceFactoryAdapter is the wrong name, but it can say the class exceeds the file-size cap. It cannot say the Singleton is hiding global state, but it can say the file has more than one default export. It cannot say that two functions with different signatures compute the same answer, but it can say they share twenty tokens. The work that judgement does (naming, decomposition, pattern fit) is what PT1 Patterns Are Vocabulary, AI2 Persistent Brief, P7 Short-Lived Branches with Review all carry. Name the boundary. Do not pretend the gate is the ceiling.
The third habit, the one I will die on the hill of: never disable a rule inline. The eslint-disable-next-line comment is the bug. The rule is doing its job. If the code earns the suppression, the rule is wrong: change it in eslint.config.mjs with a justification, with a scope, and with the name of the person who agreed. If the code does not earn it, fix the code. Either path is editable, audit-able, and reviewable. An inline comment is none of those things.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Anything not enforced will erode. Every quality gate is an error, never a warning. The limit goes in `eslint.config.mjs` before the first violation lands. If you reach for `eslint-disable`, fix the code instead. /tenet/linter-as-law/TA1
AI eyes only
Rule: tooling enforces what prose suggests. If the rule matters, encode it.
Reject: prose-only rules in AGENTS.md / CLAUDE.md that have a lint, type, or test equivalent. Reject: silencing a rule inline with eslint-disable, // @ts-ignore, or --no-verify. Reject: releasing with warnings unaddressed.
Generate: every load-bearing rule has a build gate (lint, type, test, format, dup, coverage). The brief teaches only what the gates cannot reach.
Diagnostic: the build must fail for any violation of a load-bearing rule. If the build passes for a violation, the rule is not a rule yet.
Why?
- Settle every recurring style argument once. The rule lives in
eslint.config.mjsand the team stops re-litigating function length, equality, return annotations or import direction in every pull request. - Catches the violation at the keystroke rather than the merge. The IDE underlines the line as you write it: per P4 Continuous Quality Feedback the cost of the fix is exponentially smaller before the PR opens.
- Makes the agent fix the code rather than negotiate the rule. A polite ask in AGENTS.md decays under length pressure. A build error does not.
- Removes the silent-suppression class of bug. The codebase has no
eslint-disable-next-linecomments to grep for, because every legitimate exception lives ineslint.config.mjswith a scope and a justification. - Compounds with every other tenet. S2 Cyclomatic Caps, S3 Parameter Object, T2 No Escape Hatches all rely on the same gate. One configured rule does the job all three principles ask for.
- Reviewer attention shifts from style to substance. The boring class of objection (“this function is too long”) never reaches review. The interesting class (“this function is doing two things”) is what the team argues.
- Draws an honest perimeter. The rule list is the floor. The things the linter cannot reach (pattern fit, naming taste, decomposition judgement) are flagged as the work of culture and review, not silently ignored.
Origins
The principle predates the linter. Brian Kernighan and P. J. Plauger's The Elements of Programming Style (1974) pushed the position that style is not taste but contract: code is a thing other humans must read and the rules that govern it are the rules that survive contact with the next maintainer. The book was written before continuous integration existed. The discipline still had to live in the head.2
Stephen C. Johnson's Lint, a C Program Checker (Bell Labs Computing Science Technical Report 65, 1977) is the artefact that gave the family its name. Lint stripped portability and correctness checks out of the C compiler so the compiler could stay fast while the policing got more aggressive.5 Every modern lint (ESLint, Ruff, Checkstyle, PHP-CS-Fixer) is the same shape: a rule engine that runs alongside the compiler, gates the build, and keeps the style decisions out of the reviewer's hands.
The treat-warnings-as-errors discipline was already an opt-in switch, /warnaserror, in Microsoft's C# compiler by 2008, off unless a project turned it on.7 Turned on, it reports every warning as an error and the build halts. Jimmy Hartzell later called warnings and linter errors the awkward middle children of a language: a model in which a program is simply valid or invalid rounds them away, yet many of Rust's important safety features are warnings, and he argues that obligatory warnings belong in any language spec.9 Rust's lint subsystem formalised the same shape with #[deny] attributes. The Go team decided to build in gofmt as a hard formatter rather than a warning so that there would be nothing left to debate.
Martin Fowler's Continuous Integration (2000, rewritten 2006 and 2024) made the self-testing commit build the check on every push to the mainline, held to a ten-minute guideline; the 2024 rewrite names linters among the additional verification tools a team should evaluate for it.8 The whole P4 Continuous Quality Feedback tradition runs through the article's observation that bugs are cumulative: the more there are, the harder each one is to remove.
Quotes
Lint is a command which examines C source programs, detecting a number of bugs and obscurities. It enforces the type rules of C more strictly than the C compilers.
Continuous Integration doesn’t get rid of bugs, but it does make them dramatically easier to find and remove.
The first step is fully admitting that the code you write is riddled with errors. That is a bitter pill to swallow for a lot of people, but without it, most suggestions for change will be viewed with irritation or outright hostility.
A function that has dead code after a return statement: this is a warning, but also a serious correctness issue.
Evidence
18 external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01Lint, a C Program CheckerSupportsThe progenitor: a rule engine alongside the compiler, gating on its own catalogue of policy. Establishes the architectural shape every modern lint inherits.
- 02Continuous IntegrationSupportsThe self-testing commit build, kept under ten minutes; the 2024 rewrite lists linters among the extra verification tools teams should evaluate. The exponential effect it names is that bugs are cumulative.
- 03Continuous DeliverySupportsCodifies the no-warning policy at pipeline level. Warnings rot. The gate either blocks or is silent. The entire commit-stage chapter underwrites this tenet.
- 04Practitioner-blog defence: warnings are tolerated, errors are fixed. The behavioural argument that the discipline only works at the error severity level.
- 05Argues that a language is not well described by a binary valid-or-invalid model, because many of its real safety rules ship as warnings rather than as errors. The warning is part of the standard, not an aside to it.
- 06Language-vendor's documentation for /warnaserror. Demonstrates the pattern at compiler level, predating ESLint by a generation.
- 07Recent quality-gates summary that situates lint as one stage of many. Useful for the modern restatement. Aligns with P3 + P4 in the cross-reference graph.
- 08The most-read style guide on the planet. Strongly opinionated but explicit that the rules are guidelines, not gates: kernel uses checkpatch.pl as advisory, not blocking. Useful as the principled middle position.
- 09Static Code AnalysisQualifiesid Software made Microsoft /analyze the default for 360 builds, on top of warning level 4 with warnings-as-errors; PVS-Studio and PC-Lint then found more in code already clean to /analyze, and Coverity was only demoed. Each new tool found errors the last had passed, so a clean run is not proof of correctness.
- 10-Werror is Not Your FriendOpposesNot against the discipline: the author runs a zero-warning policy that fails builds and blocks pull requests on CI. The objection is to hard-coding -Werror in the build files, which ties the project to specific toolchain vendors and versions.
- 11Chapter 17 (Consistency): the best way to enforce conventions is a tool that checks for violations, with code that cannot be committed unless it passes the checker. Chapter 9 cuts the other way on length caps: length by itself is rarely a good reason to split a method.
- 12Trunk-Based DevelopmentSupportsDORA's trunk-based development capability: short-lived branches merged to trunk at least daily, with fast automated tests that must pass before a merge. It does not mention lint.
- 13The execution surface for every claim in this tenet. Defines flat-config syntax, severity levels, per-glob overrides: the mechanism the discipline depends on.
- 14Ruff documentationSupportsPorts pylint, pycodestyle, eradicate, flake8-fixme, pydocstyle and many more under one binary and one TOML block. The Python-side referent for cross-language lint-as-law.
- 15Checkstyle documentationSupportsJava-side referent. Demonstrates the same discipline in a different ecosystem. The file-by-file XML override model parallels eslint.config.mjs's per-glob overrides.
- 16The lever that polices the inline-disable case: any eslint-disable directive without a justification fails the build. Concrete answer to the 'inline disable' objection.
- 17The #[deny] / #[forbid] severity attributes. Cross-language proof of the pattern: the strongest forbid level cannot be unset by inner scope, the closest analogue to a no-inline-disable policy at the language level.
- 18PHP-CS-Fixer documentationSupportsPHP-side referent. Auto-fixing fixer with hard error gating. Supports the cross-language claim that lint-as-law is not a JavaScript-only pattern.
Eighteen sources across supports, qualifiers and opposers. The supporters run from Johnson's 1977 lint paper, which gave the checks a program of their own beside the compiler, through Fowler on Continuous Integration and Humble & Farley on Continuous Delivery, to Carlos Schults and Jimmy Hartzell making the modern case for taking warnings seriously. The qualifiers are the Linux kernel style guide and Carmack. The opposers hold the steelman the reply has to address, Embedded Artistry's case against hard-coding -Werror into the build files, and keep the gate from drifting into the ceiling.
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-lines-per-function | ESLint core | functions over 15 lines: forces extraction rather than vertical sprawl. |
| max-lines | ESLint core | files over 150 lines: caps the unit of comprehension; the file becomes the chapter, not the book. |
| max-params | ESLint core | functions with more than 3 parameters: pushes toward parameter-object types. |
| complexity | ESLint core | cyclomatic complexity over 3: McCabe's metric encoded as a build-blocking gate. |
| eqeqeq | ESLint core | loose equality (== / !=): absorbs the old S5 Strict Equality candidate as a single rule. |
| @typescript-eslint/explicit-function-return-type | typescript-eslint | missing return-type annotations: absorbs the old T2 Annotate Returns candidate. |
| import/no-cycle | eslint-plugin-import | import cycles between modules: absorbs half of the old A6 Direct Imports candidate. |
| no-restricted-imports | ESLint core | barrel-file imports (index.ts re-exports): absorbs the second half of A6. |
| unicorn/filename-case | eslint-plugin-unicorn | kebab-case file and directory naming. It enforces case only, so it cannot require types to live in *.types.ts files. |
| @eslint-community/eslint-comments/require-description | @eslint-community/eslint-plugin-eslint-comments | eslint-disable directives without a justification: keeps the override path honest. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
import unicorn from 'eslint-plugin-unicorn';
import importPlugin from 'eslint-plugin-import';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs';
export default defineConfig(
eslintComments.recommended,
{
files: ['**/*.{ts,tsx}'],
extends: [tseslint.configs.base],
languageOptions: { parser: tseslint.parser },
settings: {
'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] },
'import/resolver': { node: { extensions: ['.ts', '.tsx', '.js'] } },
},
plugins: { '@typescript-eslint': tseslint.plugin, sonarjs, unicorn, import: importPlugin },
rules: {
'max-lines-per-function': ['error', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }],
'max-params': ['error', 3],
'complexity': ['error', { max: 3 }],
'max-depth': ['error', 3],
'eqeqeq': ['error', 'always'],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'import/no-cycle': 'error',
'no-restricted-imports': ['error', { patterns: ['**/index', '**/index.ts', '**/index.tsx'] }],
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'@eslint-community/eslint-comments/no-use': 'error',
'@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
'sonarjs/cognitive-complexity': ['error', 15],
},
},
);Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-lines-per-function | ESLint core | functions over 15 lines: forces extraction rather than vertical sprawl. |
| max-lines | ESLint core | files over 150 lines: caps the unit of comprehension; the file becomes the chapter, not the book. |
| max-params | ESLint core | functions with more than 3 parameters: pushes toward parameter-object types. |
| complexity | ESLint core | cyclomatic complexity over 3: McCabe's metric encoded as a build-blocking gate. |
| eqeqeq | ESLint core | loose equality (== / !=): absorbs the old S5 Strict Equality candidate as a single rule. |
| @typescript-eslint/explicit-function-return-type | typescript-eslint | missing return-type annotations: absorbs the old T2 Annotate Returns candidate. |
| import/no-cycle | eslint-plugin-import | import cycles between modules: absorbs half of the old A6 Direct Imports candidate. |
| no-restricted-imports | ESLint core | barrel-file imports (index.ts re-exports): absorbs the second half of A6. |
| unicorn/filename-case | eslint-plugin-unicorn | kebab-case file and directory naming. It enforces case only, so it cannot require types to live in *.types.ts files. |
| @eslint-community/eslint-comments/require-description | @eslint-community/eslint-plugin-eslint-comments | eslint-disable directives without a justification: keeps the override path honest. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
import unicorn from 'eslint-plugin-unicorn';
import importPlugin from 'eslint-plugin-import';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs';
export default defineConfig(
eslintComments.recommended,
{
files: ['**/*.{ts,tsx}'],
extends: [tseslint.configs.base],
languageOptions: { parser: tseslint.parser },
settings: {
'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] },
'import/resolver': { node: { extensions: ['.ts', '.tsx', '.js'] } },
},
plugins: { '@typescript-eslint': tseslint.plugin, sonarjs, unicorn, import: importPlugin },
rules: {
'max-lines-per-function': ['error', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }],
'max-params': ['error', 3],
'complexity': ['error', { max: 3 }],
'max-depth': ['error', 3],
'eqeqeq': ['error', 'always'],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'import/no-cycle': 'error',
'no-restricted-imports': ['error', { patterns: ['**/index', '**/index.ts', '**/index.tsx'] }],
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'@eslint-community/eslint-comments/no-use': 'error',
'@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
'sonarjs/cognitive-complexity': ['error', 15],
},
},
);Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-lines-per-function | ESLint core | functions over 15 lines. |
| max-lines | ESLint core | files over 150 lines. |
| max-params | ESLint core | functions with more than 3 parameters. |
| complexity | ESLint core | cyclomatic complexity over 3. |
| eqeqeq | ESLint core | loose equality. |
| playwright/no-wait-for-timeout | eslint-plugin-playwright | page.waitForTimeout calls in tests: absorbs the old TS12 Auto-Wait candidate. |
| @eslint-community/eslint-comments/require-description | @eslint-community/eslint-plugin-eslint-comments | eslint-disable directives without a justification. |
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
import playwright from 'eslint-plugin-playwright';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs';
export default defineConfig(
eslintComments.recommended,
{
files: ['**/*.{js,mjs,cjs}'],
rules: {
'max-lines-per-function': ['error', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }],
'max-params': ['error', 3],
'complexity': ['error', { max: 3 }],
'eqeqeq': ['error', 'always'],
'@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
},
},
{
files: ['tests/**/*.{js,mjs,cjs}'],
plugins: { playwright },
rules: { 'playwright/no-wait-for-timeout': 'error' },
},
);Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| C901 (complex-structure) | Ruff | functions over the configured complexity threshold: McCabe's metric, encoded as a hard error. |
| PLR0912 (too many branches) | Ruff | functions with too many branches: the cyclomatic-cap analogue. |
| PLR0913 (too many arguments) | Ruff | functions with more than 3 parameters: the parameter-object cap. |
| PLR0915 (too many statements) | Ruff | functions over the configured statement count: the line-count cap, statement-shaped. |
| N801-N818 (pep8-naming) | Ruff | class, function, argument and variable naming: the symbol-naming discipline. The file-name check is N999 (invalid-module-name). |
| C0301 (line-too-long) | Pylint | lines longer than the configured limit: the row-level companion to the file-length and function-length caps. |
| RUF100 (unused-noqa) | Ruff | noqa directives that no longer suppress anything: the Python-side analogue of the eslint-disable hygiene rule. |
pyproject.tomlconfiguration snippet
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"C90", # mccabe complexity
"N", # pep8-naming
"PL", # pylint
"RUF", # Ruff-specific rules
]
[tool.ruff.lint.mccabe]
max-complexity = 3
[tool.ruff.lint.pylint]
max-args = 3
max-statements = 10
max-branches = 3
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["PLR2004"]Apply these rules in checkstyle.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| MethodLength | Checkstyle | methods over 15 lines: the Java-side max-lines-per-function. |
| FileLength | Checkstyle | files over 150 lines: the file-size cap. |
| ParameterNumber | Checkstyle | methods with more than 3 parameters: pushes toward parameter-object types. |
| CyclomaticComplexity | Checkstyle | cyclomatic complexity over the configured cap: McCabe's metric in the Java toolchain. |
| EqualsAvoidNull | Checkstyle | equals() calls with a String literal as the argument: the literal belongs on the left, so a null variable cannot throw. |
| ExcessivePublicCount | PMD | classes with too many public members: the API-surface cap; pairs with A10 Interface Segregation. |
| AvoidThrowingRawExceptionTypes | PMD | throw new Exception() and similar: the Java-side type-discipline rule. |
checkstyle.xmlconfiguration snippet
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="severity" value="error"/>
<module name="FileLength">
<property name="max" value="150"/>
</module>
<module name="TreeWalker">
<module name="MethodLength">
<property name="max" value="15"/>
<property name="countEmpty" value="false"/>
</module>
<module name="ParameterNumber">
<property name="max" value="3"/>
</module>
<module name="CyclomaticComplexity">
<property name="max" value="3"/>
</module>
<module name="EqualsAvoidNull"/>
<module name="SuppressionCommentFilter">
<property name="checkFormat" value="$$0"/>
</module>
</module>
</module>Apply these rules in .php-cs-fixer.dist.php. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| Generic.Metrics.CyclomaticComplexity | PHP_CodeSniffer | cyclomatic complexity over the configured cap: McCabe's metric in PHP. |
| Generic.Files.LineLength | PHP_CodeSniffer | lines longer than the configured limit: the row-level cap. |
| Squiz.WhiteSpace.FunctionSpacing | PHP_CodeSniffer | the blank lines before and after each function: a spacing rule, not a length cap. |
| strict_comparison | PHP-CS-Fixer | loose comparisons (== / !=): absorbs the equality discipline candidate. |
| declare_strict_types | PHP-CS-Fixer | files missing declare(strict_types=1): the type-strictness baseline. |
| phpdoc_to_return_type | PHP-CS-Fixer | functions with a @return tag but no return-type declaration: writes the tag's type into the signature, and leaves a function with no @return tag alone. |
| ordered_imports | PHP-CS-Fixer | import disorder: pairs with the no-barrel-files PHP equivalent of grouped use statements. |
.php-cs-fixer.dist.phpconfiguration snippet
<?php
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
'strict_comparison' => true,
'declare_strict_types' => true,
'no_unused_imports' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'phpdoc_to_return_type' => true,
'return_type_declaration' => true,
])
->setFinder(
(new PhpCsFixer\Finder())->in(__DIR__ . '/src')
);AI rules
.cursor/rules/ta1-linter-as-law.mdc--- alwaysApply: true --- ## Prickles TA1: Linter as Law Make every quality gate an error, never a warning. A warning gets ignored within a sprint. An error stops the build. Encode the limit before the violation. Line count, complexity, parameter count, file size, equality, return annotations, type-file naming, barrel files, import direction: all in `eslint.config.mjs` or its language equivalent. When the linter trips, fix the code. Never disable the rule inline. The disable comment is the bug. The rule is doing its job. Where the linter cannot reach (pattern misuse, naming taste, decomposition judgement) culture, agent rules, code review, and test shapes take the load. Name that boundary so nobody pretends the gate is the ceiling.
Repo layout, CI, and ESLint wiring for these paths live on /implementation, not repeated on every tenet.
Counter-argument
The honest steelman is Embedded Artistry's.3 Phillip Johnston keeps a zero-warning policy, and his build server rejects pull requests and fails builds that carry a warning. What he objects to is hard-coding -Werror into the build files: “-Werror creates a project dependency on specific toolchain vendors and versions.” Every new compiler version adds warnings, so a user who changed nothing builds the project with a newer or different toolchain and watches it fail. Put the error in the configuration everyone builds with and the next tool upgrade breaks code nobody touched; enforce it in CI and the policy holds without that cost. Carmack's position on static analysis at id Software is adjacent: clear the backlog slowly before making the analyser the build default, do not pretend a clean lint pass means the code is correct.4
Counter-argument retort
Embedded Artistry's objection3 is doing something, but the conclusion does not hold once you separate two different kinds of lint.
New rule on existing code is the hard case, whether the team ratchets a previously-unenforced rule onto a hundred-thousand-line codebase or a tool upgrade brings new warnings, the break Embedded Artistry warns about. The post sets its own condition for using -Werror: note the versions the project is expected to work with, or capture the build environment. Pin the linter's version and a new rule arrives only in the pull request that upgrades it. Suppress the legacy violations through directory-scoped overrides in eslint.config.mjs; flip the rule to error for new code only. Let the green-zone grow as the legacy zone is paid down. The principle stays intact: errors, not warnings, in the green zone. The directory override is itself an audit trail.
Existing rule on new code, the principal case in this canon, is the simple case. The rule was at error from day one. The codebase grew up green. The cost of a new violation is fifteen seconds, not fifteen hours. This is the case Carmack's static- analysis essay endorses: ratchet up early, suppress with discipline, never trust a clean run as proof of correctness, but use the clean run as the floor.4
The Ousterhout-style steelman (lint is a proxy for the thing you care about, and proxies leak) is also right, and is exactly why the Patterns pillar exists. The reply is the cross-reference rather than the retreat. This tenet has been drawing the line that hands judgement back from the top: PT1 Patterns Are Vocabulary, AI2 Persistent Brief, P7 Short-Lived Branches with Review, F2 Intention-Revealing Names: the lint-unreachable work goes to those four.
The honest residue is the inline-disable case and the answer there is the rule: eslint-disable in the source file is not editable, not auditable, and not reviewable.10 Move the exception to eslint.config.mjs with a path scope and a reason. If you cannot justify it there, the rule was right and the code is wrong. The tooling gives you everything you need. Reach for the wrench, not for the silencer.
Notes
- [1]ESLint maintainers: ESLint flat config reference. The catalogue of rules and their severity levels (off / warn / error). The execution surface for every claim in this tenet.
- [2]Brian W. Kernighan & P. J. Plauger: The Elements of Programming Style (McGraw-Hill, 1974; 2nd ed. 1978). The earliest book-length statement that style is contract, not taste and the source of the rule numbers many later style guides cite directly.
- [3]Embedded Artistry: "-Werror is Not Your Friend" (2017). Phillip Johnston keeps a zero-warning policy and has CI fail any build or pull request with warnings. His objection is to hard-coding -Werror in the build files, because a newer or different compiler adds warnings and breaks the build for anyone who uses it.
- [4]John Carmack: "Static Code Analysis" (2011). On id Software's adoption of Microsoft /analyze on every 360 build, with PVS-Studio and PC-Lint run after it and Coverity only demoed: each new tool found errors in code already clean to the last, so a clean run is never proof of correctness.
- [5]Stephen C. Johnson: Lint, a C Program Checker (Bell Labs Computing Science Technical Report 65, 1977). The progenitor of every modern linter. Introduced the rule-engine-alongside-the-compiler shape that ESLint, Ruff, Checkstyle and PHP-CS-Fixer all inherit.
- [6]John Ousterhout: A Philosophy of Software Design (2018). Chapter 17 (Consistency): the best way to enforce conventions is a tool that checks for violations, with code that cannot be committed unless it passes. Chapter 9 adds that length by itself is rarely a good reason to split a method.
- [7]Microsoft C# documentation: Compiler options: errors and warnings. The language-vendor's own statement of the treat-warnings-as-errors discipline at the compiler level, predating ESLint by a generation.
- [8]Martin Fowler: Continuous Integration (2000, rewritten 2006 and 2024). The self-testing commit build kept to ten minutes; the 2024 rewrite names linters as one of the extra verification tools a team should evaluate for it.
- [9]Jimmy Hartzell (thecodedmessage): "Warnings and Linter Errors: The Awkward Middle Children" (2022). Argues that the binary valid-or-invalid model of a language is inadequate, because many of Rust's real safety rules are warnings: ignoring #[must_use], or dead code after a return.
- [10]ESLint plugin eslint-comments: @eslint-community/eslint-plugin-eslint-comments require-description rule. The lever that polices the inline-disable case: any eslint-disable directive without a justification fails the build.