Self-Documenting Code
If it looks like a hedgehog and acts like a hedgehog, it MUST be a hedgehog.
If the comment is doing the explaining, then the code isn't. Make the shape of the code tell the story so the comment becomes redundant.
Opinion
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.
Don't add a comment that explains what the code does — make the code itself clear instead. If the meaning isn't obvious, rename the function, split it, or improve the names of its parameters. Comments are for explaining *why* something works the way it does, not *what* it does. /tenet/self-documenting-code/F5
AI eyes only
Rule: code carries the meaning. A comment that explains what code does is a defect.
Reject: comments that paraphrase the next line. Reject: JSDoc / docstring that restates parameter names or types the type system already declares. Reject: “this function does X” preambles.
Generate: better names. If a comment is genuinely needed, it explains why — a constraint, an external bug, a non-obvious invariant — never what.
Diagnostic: delete the comment. If the code now reads ambiguously, rename or split the function until it does not. Re-add only when the comment is the sole place a non-obvious why lives.
Why?
- Forces developers and AI agents to honour F1 Single Responsibility Principle.
- Faster to write — naming the function honestly per F2 Intention-Revealing Names is faster than naming it dishonestly and then explaining the gap.
- Comments rot. The code changes; the comment beside it doesn't. By the third commit the prose lies about the line above it, and the next reader either trusts the comment and ships a bug, or distrusts every comment they meet. Cutting the comment cuts the rot at source.
- Removes bloat. Every comment is a line a reviewer must read, an LLM must token-spend, and a future editor must keep aligned. Strip the prose; let the names carry the meaning.
- Coding agents weight code more than comments. When the comment and the code disagree, the agent follows the code — the comment becomes dead text the model still pays tokens to read.
- Cuts review time — names and shape do the explaining instead of prose.
- Removes a class of merge conflicts (the kind where the comment lies about the new code).
Origins
The rule is older than most of the developers reading it. Brian Kernighan and P. J. Plauger's The Elements of Programming Style (1974) sets it down in a single line: “Don't comment bad code — rewrite it.”6The Elements of Programming Style (McGraw-Hill, 1974; 2nd ed. 1978). Rule 64: “Don’t comment bad code — rewrite it.” The earliest published statement of the principle, half a century before Clean Code put it in front of a generation of working developers. Decades before Clean Code, the discipline was already in print: the comment is the symptom, the rewrite is the cure.
Robert C. Martin's Clean Code (2008) argued the position again, in chapter four, with no patience left for the steelman: comments are almost always a sign of failure; the code could not say it itself, so a human had to step in and translate.1Clean Code (Prentice Hall, 2008), ch. 4 “Comments”: “The proper use of comments is to compensate for our failure to express ourselves in code.” Martin allows good comments (legal headers, public-API explainers, intent the language cannot express) but they are vastly outnumbered by the bad ones, and the bad ones do active harm by drifting out of sync with the code they purport to describe.
The position is best understood by reading it against its steelman opposite, written a generation earlier. Donald Knuth's 1984 essay Literate Programming proposed the inverse discipline: write the prose first, in narrative order, and let the source code fall out of it like footnotes from a paragraph.2“Literate Programming”, The Computer Journal 27 (2), 1984. The steelman case for prose alongside code, written for typeset documents — a different production model than the modern editor and reviewer. Knuth's target was the typeset document, not the editor pane. The production-model difference is doing most of the work in the disagreement. When the artefact is a printed paper, the comment is the artefact. When the artefact is a feature shipped at speed by half-a-dozen authors and an LLM, the comment is debt.
Martin Fowler later catalogued the position in Refactoring, 2nd edition (2018), listing “Comments” among the bad smells: an explanatory comment is usually a symptom that the name is wrong or the function is doing too much.3Refactoring, 2nd ed. (Addison-Wesley, 2018). Lists Comments under Bad Smells in Code: “a comment is often used as a deodorant”. Fix the underlying code and the smell goes with it.
Quotes
The proper use of comments is to compensate for our failure to express ourselves in code. Note that I used the word failure. I meant it. Comments are always failures.
Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.
When you feel the need to write a comment, first try to refactor the code so that any comment becomes superfluous.
Evidence
Twenty 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.
- 01Clean Code, ch. 4 “Comments”SupportsThe most-cited source for the principle. Comments are a failure of expression; refactor the code rather than annotating it.
- 02“Necessary Comments”QualifiesMartin steel-manning the exception: reaffirms comments-as-failure but admits a narrow class — intricate algorithms, timing diagrams.
- 03Three chapters defend comments. Argues “self-documenting code” is wrong: rationale, units, edge cases and interface contracts can’t live in code alone.
- 04Comments should explain why — purpose, intent, constraints — not what. The what should live in the code itself.
- 05Formalised the term. Treats programming style as documentation, since the code often becomes the only documentation that survives.
Twenty sources, three stances. The supports cluster on the Martin canon, qualified by McConnell and the Pragmatic Programmer line between explaining the what and the why. The opposing voice is Ousterhout: rationale, edge cases and interface contracts cannot live in code alone. The honest argument is between the qualifiers and the opposers, not between the supporters and the sceptics.
Examples
// How many spines does the hedgehog havefunction handle(patchCm2: number, pricklesPerCm2: number): number { // number of spines = body area times the number of spines per square cm const scratch = patchCm2 * pricklesPerCm2; // return the number of spines return scratch;}
function countHedgehogSpines(bodyAreaCm2: number, spinesPerCm2: number): number { return bodyAreaCm2 * spinesPerCm2;}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/ban-ts-comment | typescript-eslint | @ts-ignore / @ts-expect-error / @ts-nocheck without a minimum-length description. |
| no-inline-comments | ESLint core | any comment on the same line as code — forces comments to stand alone, exposing how much they're paraphrasing. |
| multiline-comment-style | ESLint core | inconsistent block-comment styling; with separate-lines it bans /* ... */ clusters used as code dividers. |
| spaced-comment | ESLint core | //foo style — stops //commented-out-code patterns slipping through formatters. |
| capitalized-comments | ESLint core | comments that look like sentence fragments scribbled mid-thought. |
| no-warning-comments | ESLint core | TODO / FIXME / XXX markers left in main. |
| sonarjs/no-commented-code | eslint-plugin-sonarjs | heuristic detection of commented-out code. |
| jsdoc/require-description | eslint-plugin-jsdoc | JSDoc blocks that have tags but no human description — i.e. the comment exists only to repeat the signature. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
import jsdoc from 'eslint-plugin-jsdoc';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs, jsdoc },
rules: {
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-ignore': 'allow-with-description',
'ts-expect-error': 'allow-with-description',
minimumDescriptionLength: 10,
}],
'no-inline-comments': 'error',
'multiline-comment-style': ['error', 'separate-lines'],
'spaced-comment': ['error', 'always'],
'capitalized-comments': ['error', 'always'],
'no-warning-comments': ['warn', { terms: ['todo', 'fixme', 'xxx'], location: 'anywhere' }],
'sonarjs/no-commented-code': 'error',
'jsdoc/require-description': 'warn',
}
});AI rules
.cursor/rules/f5-self-documenting-code.mdc---
description: Prickles F5 — Self-Documenting Code
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles F5 — Self-Documenting Code
Do not add comments that explain what the code does. If a reader needs help, improve names, extract functions, add types, or add tests until the code carries the story.
Do not use a comment as a substitute for refactoring. If you are about to comment, refactor first.
Allowed without debate: SPDX license headers, autogenerated file banners, and narrow linter suppressions that match your repo's required shape (e.g. eslint-disable-next-line with rule id).
Refuse to "just add a comment" instead of fixing unclear code.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The honest steelman is Knuth's, sharpened by a modern librarian's instinct. There is a class of code (public-API surface, cryptographic primitives, numerical routines) whose contract is what the caller must understand, and the contract is irreducibly prose. No name carries the bit-level guarantees of a hashing function; no signature explains why a particular sort is stable. Knuth's literate-programming case2“Literate Programming”, The Computer Journal 27 (2), 1984. The steelman case for prose alongside code, written for typeset documents — a different production model than the modern editor and reviewer. is that for these artefacts the prose is the deliverable, not decoration. Hillel Wayne extends the point to the why-not-what comment: the line of code can be obvious and still need a sentence saying this exists because the vendor's clock skews on leap seconds.4“Why Not Comments” (buttondown.com/hillelwayne, 2024). Argues the rule needs an exception for negative-information comments — recording what was not done and why.
Counter-argument retort
On reflection, neither concession holds. The strongest cases the research can muster all reduce.
Knuth's literate-programming case2“Literate Programming”, The Computer Journal 27 (2), 1984. The steelman case for prose alongside code, written for typeset documents — a different production model than the modern editor and reviewer. was written for typeset documents whose deliverable was a printed paper. Modern cryptographic primitives ship as self-documenting code; the proof is a separate paper on arXiv, peer-reviewed, with its own citation. Two artefacts, not one.
Hillel Wayne's negative-information case4“Why Not Comments” (buttondown.com/hillelwayne, 2024). Argues the rule needs an exception for negative-information comments — recording what was not done and why. (recording what was not done and why) looks irreducible at first. A regression test whose name is the rationale ( “does not double-validate merchantId since the upstream webhook is authoritative” ) plus an ADR for the architectural decision carries the same load with a stronger audit trail; neither artefact rots silently the way a comment does.
Ousterhout's rationale-and-edge-cases case5A Philosophy of Software Design (2018, 2021). The strongest steelman for explanatory comments — defends rationale, units, edge cases and interface contracts as belonging in prose. The reply argues each of those reduces to types, tests or ADRs. is mostly types waiting to be written; see T1 Domain-Driven Types. A Result<T, E> forces every error path; a Branded<string, “UserId”> removes the ambiguity the comment was patching. Where types cannot carry the constraint, a property-based test usually can.
The genuine residue is not what the canon means by “comment”: SPDX-License-Identifier headers, AUTO-GENERATED markers that tooling depends on, ESLint-disable directives that already carry a justification. These are metadata interfaces between the source and external systems, not explanation aimed at a human reader. Police their format with a linter; do not call them comments.
In production code the comment that explains is the code that lies. The next reader trusts the comment over the code; the comment is wrong; a bug ships. Refactor: name the function, type the parameter, write the test, file the ADR, ship the doc. The discipline is irreducible. The comment is not.
Notes
- [1]Robert C. Martin — Clean Code (Prentice Hall, 2008), ch. 4 “Comments”: “The proper use of comments is to compensate for our failure to express ourselves in code.”
- [2]Donald E. Knuth — “Literate Programming”, The Computer Journal 27 (2), 1984. The steelman case for prose alongside code, written for typeset documents — a different production model than the modern editor and reviewer.
- [3]Martin Fowler — Refactoring, 2nd ed. (Addison-Wesley, 2018). Lists Comments under Bad Smells in Code: “a comment is often used as a deodorant”.