# Code comment style How a comment in this repository is written: what earns one, what belongs inside it, how it is marked up, and the shapes that quietly turn a comment into a liability. This covers both kinds — the `//` comment that explains local mechanics, and the JSDoc doc comment that states a contract. Neighboring guidance: [`../README.md`](../README.md) governs the documentation tree, and [`DEVELOPMENT.md`](DEVELOPMENT.md) carries the rest of the coding standards — the 80-column line width among them, and the [word choice](DEVELOPMENT.md#word-choice) a comment is held to, American spelling included. The `describe()` and `it()` description strings of a test have their own guide, [`unit-test-coding-style.md`](unit-test-coding-style.md), which governs how they are worded. What they take from here is the markup below, and the rule that they describe the system as it stands. Not every comment in the tree follows this guide, so a neighboring file is not evidence of what to write. New code follows it closely. An edit to existing code conforms the area around the edit; converting a whole file is its own change, made deliberately rather than as a side effect. ## What earns a comment The code already says what it does. A comment earns its keep by carrying what the code cannot say on its own: an invariant that is not visible locally, the reason a constraint exists, a trade-off deliberately taken, a pointer to the part of a specification that decides the behavior. Two things beat a comment whenever they apply. A well-named helper or a better identifier says the same thing in the code itself, where it cannot drift out of agreement with what it describes. When a chunk of logic needs an inline comment to be readable, first ask whether naming it would do the job instead. And when the reasoning outgrows what a comment can hold, it belongs in a document under [`../features/`](../features/README.md), with the comment naming that document. Several invariants here are held that way. The pairing is what keeps a subtle constraint from being edited away by someone who never saw the argument for it. A stale comment is worse than no comment, because it is believed. Changing code means changing the comments that describe it. ## Describe the system as it is A comment describes the system as it stands, for a reader who has the code in front of them and no other context — not the conversation that produced the code, and not the one that produced the comment. That one rule generates most of what follows, and the sections below are the shapes it rules out, each of which feels helpful while being written. ### Not the code's own past Do not compare the code to a previous version of itself. "Preserving prior behavior", "this used to collapse to `{}`", "no longer coerced", "now simplified" — all of it is history. History is valuable, and its home is the commit message and the pull request body, which is where a reader goes when the question is how the code got this way. State the present-tense fact and stop: ```ts // Shown as alternative snippets. // Wrong: half of this describes code that is not here. /** * Freezes `donut` if it is not already frozen. Previously this returned * `undefined` for an already-frozen donut; it now returns the donut itself, * preserving the old caller's expectations. */ function freezeDonut(donut: object): object { return Object.isFrozen(donut) ? donut : Object.freeze(donut); } ``` ```ts // Shown as alternative snippets. // Right: what a caller needs, and nothing about how it came to be. /** * Freezes `donut` if it is not already frozen, and returns it either way. */ function freezeDonut(donut: object): object { return Object.isFrozen(donut) ? donut : Object.freeze(donut); } ``` This reaches test files, and the description strings in them as much as the comments. A test added in response to a bug reads like every other test: it states the behavior being pinned. "Regression test for the case where …" explains why the test was written, which is again pull request material. ### Not the road not taken State the requirement. The argument for it against some alternative that was considered and rejected is a record of a decision, and belongs where decisions are recorded. "Being total is also what keeps it honest, since the alternative is collapsing an unknown flavor into a catch-all" is a sentence about a discussion. "Every flavor maps to a distinct key; a collision here would silently merge two orders" is a sentence about the code. ### Not the neighbors A comment is about the code it sits next to. Whatever is true of another module is that module's business, and a criticism of it planted here is a claim nobody maintaining either file will think to revisit. "`glazeOf()` tolerates some of these today, but that tolerance is a bug rather than a contract" is a comment in the wrong file, and possibly a bug report that was never filed. Noting a genuine present-tense inconsistency _within_ the current system is fine, because that is the system as it is: "the opposite of the `undefined ≈ true` convention used elsewhere in this file" tells a reader something true and useful. ### Not a survey of the rest of the system Claims about the wider codebase rot silently, because nothing about editing the far end of the claim brings anyone back here. "As all current callers do", "the only caller", "there are three such sites" — each is one refactor away from being false, and the refactor will not fail any check. If a property matters, assert it locally: guard it, type it, or state it as the contract the code enforces. Code search answers the cross-reference questions. A doc comment may mention callers only when they are local to its own file — that is, when the thing is not exported and the file bounds the claim. ### Not a rollout plan Terms like "Stage 3", "the expand-acceptor step", or "parallel-change" name a plan for getting somewhere. They have no referent for someone reading the code later, at which point the plan is either finished or abandoned. The same goes for issue numbers, tracker identifiers, and pull request numbers: the commit message and the branch name carry that trail, and they stay accurate when the tracker is replaced. What survives the plan is why the code has the shape it has. Write that, and give the eventual cleanup a `TODO` phrased as a condition on the world: ```ts // Shown at module scope. // The legacy branch exists so that orders naming a glaze by number still // resolve. // TODO(DonutFry99): Remove once every deployed fryer names glazes by string. export const acceptNumberedGlazes = true; ``` Note the shape of that `TODO`: it names the fix and names the blocker. Removing process residue is subtraction, and subtraction alone can leave a comment worse than it found it — a vague `TODO(DonutFry99): Fix the problem.` is what remains when a tracker identifier is deleted and nothing takes its place. Say the thing, rather than only striking the part that cannot stay. Note the tag, too. Every `TODO` names whoever is on the hook for it, and a bare `TODO:` fails `deno lint`, which runs the `ban-untagged-todo` rule across the repository. That rule also accepts an issue reference in the tag position, which does not make one a good idea here: an issue number is exactly what this section says to leave out of a comment. Tag a person. ### The check that finds these Self-review does not reliably catch any of the above, because each shape reads as helpful in the moment it is written. Grep the added comment lines of the diff instead: ```text before|used to|previously|no longer|formerly|the alternative|rather than the old ``` That list is a starting point, not a specification of the rule. It leans toward the past-tense shapes, and has nothing in it that would catch a rollout stage, a count of callers, or a complaint about a neighbor; the sections above are the rule. Nor is every hit wrong — "before" has honest uses. Each one is a place to ask which system the sentence describes: the one in the file, or one that is gone. The list is meant to grow. When a comment of one of these shapes gets past it, add the wording that would have caught it. ## Removing code removes its comments When a conditional or a block goes away, the comments that annotate it go with it. Comments that annotate surviving context nearby stay. The discriminator is whether the comment describes the thing being removed or something else that is still there. - A `// frozen means terminal` premise on an `if (Object.isFrozen(donut))` short-circuit goes when the short-circuit goes. - A `// TODO(DonutFry99): This copy should not be necessary.` on a `copyIfNecessary(...)` call goes when the call goes. - A comment describing the unbox-fill-rebox cycle around them stays, because it describes behavior that remains. A premise left behind after its conditional is deleted is the most misleading comment there is: it is well written, it is in the right place, and it is about nothing. ## Markup Comments are marked up as Markdown, and so are error messages and log messages (see below). **Backticks around code and code-like things.** This covers identifiers, literal values (`914`, `NaN`, `undefined`, `true`), keywords and operators (`export`, `===`), and snippets (`flavor != "chocolate"`, `fryTime: 200`). Backtick a keyword or a constant whenever it can reasonably be read as naming the language construct rather than as plain English; on a close call, backtick it. A literal string is written without its quotes, so `frosted` rather than `"frosted"`, unless dropping them would be confusing in context. Avoid two backticked spans in a row. They are hard to read and often ambiguous, and there is nearly always a single-span phrasing that says the same thing: write ``when `name === null` `` rather than ``with a `null` `name` ``. **Underscores for emphasis.** ```ts // Shown at module scope. // Three donuts is not enough, but _four_ should be sufficient. export const donutCount = 4; ``` **Callables get trailing parentheses, properties get a leading dot.** Write `eat()` for a function or method. Write `.name` for a getter or non-method property being named without a salient receiver in context. **The royal "we" for process.** The reader and the code are on a journey together: "we count the `donuts` here, because one might have gone missing while we were not looking." ## Section markers Section markers separate major portions of a file or class. They are not headers for individual functions and their helpers — doc comments already carry that structure, and a reader navigating by rendered documentation never sees the marker at all. They are not, as a rule, headers for individual test blocks either; [`unit-test-coding-style.md`](unit-test-coding-style.md#commenting-a-block) says where a comment about one of those goes. Where `//` comments can be used, mark a section of a file or a class with a `//` comment block that opens and closes with a line holding nothing but `//`. It carries a noun-phrase title, and optionally a fully grammatical description after a blank line: ```ts // Shown at module scope. // // Exported donut handlers // // These cover all currently-known types of donuts, including crullers and // fritters. // export function fry(): void {} ``` A blank line sets the frame off above as well as below. Without the one above, the marker runs into whatever comment precedes it and the two read as one comment; without the one below, the frame reads as a note on the declaration it touches rather than as a heading over what follows. Directly under an opening bracket there is nothing above to separate from, and `deno fmt` removes a blank line there in any case. A frame there takes no blank line above it, and reads as the first region of the block rather than as a note on it. Where the block opens with shared setup instead, the region's first marker goes after it. **A region has an end, and writing the marker is choosing it.** A marker opens a region that runs to the next marker at the same level, or to the end of the enclosing block or file. Read what sits between the marker and that point and check the title covers all of it. Where the material the title owns stops before the enclosing block does, put a marker there titling what follows. A file whose last marker does not head the file's last section has a region whose end nobody chose. **A title is a claim about everything in its region**, not an introduction to what comes next. The same words that pass as a label above the first of a run become false as a title over a region holding more than they name: "the two things that are not values" is a fact about a region with two of them in it. When a region grows, its title is read against it again. A title can be wrong by being too narrow as easily as too wide. Where the subject it names also appears outside its region, either the region is in the wrong place or the title names something smaller than its subject. **A region holds more than its blocks.** Everything between the marker and the region's end falls inside it: a helper function, a fixture constant, a `beforeEach()`. A helper that serves the whole file but happens to sit inside one region reads as belonging to that region, so shared setup goes above the first marker. **CSS takes a block comment.** CSS has no line comments, so a section marker in a `.css` file, a `css` template literal, or a `