mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
233 lines
9.7 KiB
Markdown
233 lines
9.7 KiB
Markdown
# Code comments
|
|||
|
|
|
||
|
|
A comment must carry information the code cannot. If a reader could derive it from
|
||
|
|
the code in front of them, delete it: a redundant comment still has to be
|
||
|
|
maintained, will eventually contradict the code, and dilutes the comments that
|
||
|
|
matter.
|
||
|
|
|
||
|
|
The operative rules are in `AGENTS.md`, kept short so they stay in an agent's
|
||
|
|
context. This document is the reasoning and the worked examples behind them, plus
|
||
|
|
how to run the linter.
|
||
|
|
|
||
|
|
## Comment the current state
|
||
|
|
|
||
|
|
Describe the code as it is. Not what it used to be, not what changed, not why it
|
||
|
|
changed. A comment that narrates history is stale the moment the next change
|
||
|
|
lands, and git already holds that record.
|
||
|
|
|
||
|
|
When you know the history and it explains the shape of the code, the useful half is
|
||
|
|
the reason, not the sequence. State the reason:
|
||
|
|
|
||
|
|
```java
|
||
|
|
// Don't:
|
||
|
|
// This used to reimplement the modal internals, which is how the procurement
|
||
|
|
// dialogs drifted from the billing ones.
|
||
|
|
|
||
|
|
// Do:
|
||
|
|
// Thin wrapper over the shared Modal: duplicating its portal and focus trap is
|
||
|
|
// how dialogs drift apart.
|
||
|
|
```
|
||
|
|
|
||
|
|
Future state is the exception, and it belongs in a TODO with an issue.
|
||
|
|
|
||
|
|
## The four jobs
|
||
|
|
|
||
|
|
**Contract.** What a caller must know that the signature cannot say:
|
||
|
|
preconditions, invariants, units, ownership and lifetime, thread-safety, error
|
||
|
|
semantics, side effects.
|
||
|
|
|
||
|
|
The bound is the surface, not the volume: document the contract of everything a
|
||
|
|
caller outside the file can reach, and nothing else. Inside that surface say
|
||
|
|
whatever a caller needs; outside it a comment earns its place on the same terms as
|
||
|
|
any other.
|
||
|
|
|
||
|
|
```java
|
||
|
|
/**
|
||
|
|
* Authority on which filesystem locations a policy may read or write. Fail-closed
|
||
|
|
* in order: denied entirely under the saas profile; Stirling's own config dir is
|
||
|
|
* always rejected; the path must resolve within policies.allowedFolderRoots.
|
||
|
|
*
|
||
|
|
* <p>Compared after normalisation so {@code ..} cannot escape a root. Symlink
|
||
|
|
* escape is not defended: an operator who roots an allowlist on a symlink to a
|
||
|
|
* sensitive location is trusted.
|
||
|
|
*/
|
||
|
|
```
|
||
|
|
|
||
|
|
**Why.** The constraint the code satisfies, the bug it avoids, the alternative
|
||
|
|
rejected and the reason.
|
||
|
|
|
||
|
|
```java
|
||
|
|
// whenComplete runs on the worker thread after the run finishes, so the
|
||
|
|
// terminal event never races the step events.
|
||
|
|
handle.completion()
|
||
|
|
```
|
||
|
|
|
||
|
|
A reference is supplementary, never load-bearing: the comment must survive
|
||
|
|
deleting it. `// See #1234` is a dead end.
|
||
|
|
|
||
|
|
```java
|
||
|
|
// flatten() reads the annotation list that save() clears, so saving first loses
|
||
|
|
// every annotation (#6865).
|
||
|
|
document.flatten(annotations);
|
||
|
|
```
|
||
|
|
|
||
|
|
Prefer a spec (`RFC 3161`, `ISO 4217`) or a CVE where one applies. Both are
|
||
|
|
immutable; a ticket can be closed, moved or made private.
|
||
|
|
|
||
|
|
**Hazard.** "Must stay in sync with X." "Order matters because Y." "Do not remove,
|
||
|
|
it prevents Z."
|
||
|
|
|
||
|
|
**Map.** A short orientation at the head of a genuinely complex file: what it owns,
|
||
|
|
and what it deliberately does not.
|
||
|
|
|
||
|
|
## The test that decides it
|
||
|
|
|
||
|
|
A comment earns its place when it sits at a different level of detail than the line
|
||
|
|
below it: lower, stating a precise fact the code implies but does not say, or
|
||
|
|
higher, giving intent a reader would otherwise assemble from ten lines.
|
||
|
|
Same-altitude is the definition of redundant.
|
||
|
|
|
||
|
|
- **Delete it.** Is any information lost? If not, it stays deleted.
|
||
|
|
- **Could a name carry it instead?** A better identifier, an extracted function or
|
||
|
|
a named constant beats a comment. Prefer the code change.
|
||
|
|
|
||
|
|
## What not to write
|
||
|
|
|
||
|
|
| Don't | Instead |
|
||
|
|
| --- | --- |
|
||
|
|
| `// Handle drag start` above `handleDragStart` | Nothing. The name already says it. |
|
||
|
|
| `// ─── Types ───`, `// Helpers`, `// ====` | If a file needs internal signposting, split the file. |
|
||
|
|
| `// Step 1:` narrating a function body | Extract functions. If the steps need labels they need names. |
|
||
|
|
| `// No longer needed`, `// Previously this used X` | State why the code is as it is now, or nothing. |
|
||
|
|
| Commented-out code | Delete it. Git remembers. |
|
||
|
|
| `@param blob - The blob to download` | Omit the tag rather than pad it. |
|
||
|
|
| Docs on a self-explanatory member | Nothing, unless there is a real constraint to state. |
|
||
|
|
|
||
|
|
Step numbering is fine where it labels a genuinely numbered thing, such as a wizard
|
||
|
|
step or a step in a written test procedure. It is narration when it numbers the
|
||
|
|
lines of one function.
|
||
|
|
|
||
|
|
## Comments at the end of a line
|
||
|
|
|
||
|
|
A trailing comment usually does a different job from one above the code: it decodes
|
||
|
|
the line it sits on. Those are worth keeping, and the linter leaves them alone.
|
||
|
|
|
||
|
|
```java
|
||
|
|
byte[] pdfBytes = {0x25, 0x50, 0x44, 0x46}; // "%PDF"
|
||
|
|
long maxAttachmentSize = 50L * 1024 * 1024; // 50 MB
|
||
|
|
double buffer = 0.10; // 10% headroom
|
||
|
|
default -> toBytes(value, 2); // MB
|
||
|
|
```
|
||
|
|
|
||
|
|
Each overlaps in words with the code and each adds the interpretation the code
|
||
|
|
leaves implicit, which is the lower-altitude case the test above asks for. So
|
||
|
|
`CMT001` does not judge trailing comments; on this codebase it would have been
|
||
|
|
wrong about roughly six in seven of them.
|
||
|
|
|
||
|
|
What still applies is anything that does not depend on the code below: a trailing
|
||
|
|
`// TODO fix this` is as unowned as one on its own line, and a trailing
|
||
|
|
`// this used to run before the flush` narrates history wherever it sits.
|
||
|
|
|
||
|
|
A comment block over about 12 lines, outside a file or type header, is usually a
|
||
|
|
sign the code needs restructuring. If it is genuinely product documentation, it
|
||
|
|
belongs in the docs repo.
|
||
|
|
|
||
|
|
## TODOs
|
||
|
|
|
||
|
|
A TODO needs an issue, because an issue is the only part that will close it:
|
||
|
|
|
||
|
|
```java
|
||
|
|
// TODO(#1234): re-enable the checkout gate once account syncing lands
|
||
|
|
```
|
||
|
|
|
||
|
|
An owner is not a substitute: a username goes stale when someone changes team and
|
||
|
|
means nothing to an outside contributor. If the work is not worth an issue it is
|
||
|
|
not worth a TODO, and the options are to do it now or leave the code alone. A
|
||
|
|
question is not a TODO.
|
||
|
|
|
||
|
|
## Per language
|
||
|
|
|
||
|
|
**Java.** Google Java Style, which this repo already formats to. Its §7.3.1
|
||
|
|
exception applies: omit Javadoc on a self-explanatory member where there is
|
||
|
|
genuinely nothing to add, but do not cite it to skip something a reader needs.
|
||
|
|
Summary fragments are noun or verb phrases, not sentences starting "This method
|
||
|
|
returns".
|
||
|
|
|
||
|
|
**TypeScript.** JSDoc on the `@app/*` seams, exported hooks, and anything crossing
|
||
|
|
a layer boundary. No `@param`/`@returns` that restates a typed signature. JSX
|
||
|
|
comments follow the same rules as any other.
|
||
|
|
|
||
|
|
**Python.** Docstrings on modules, public functions and Pydantic models where the
|
||
|
|
contract is not obvious from the type.
|
||
|
|
|
||
|
|
## The linter
|
||
|
|
|
||
|
|
```bash
|
||
|
|
task comment-lint # what the working tree adds over HEAD
|
||
|
|
task comment-lint:branch # what the branch adds over origin/main (BASE=<ref> to change)
|
||
|
|
task pre-commit:comment-lint:ci # the fixture corpus, then the diff
|
||
|
|
```
|
||
|
|
|
||
|
|
`comment-lint` is the pre-commit question, so it reports nothing once you have
|
||
|
|
committed; on a CI pull request it compares against the target branch via
|
||
|
|
`GITHUB_BASE_REF`. `comment-lint:branch` is the review question. The corpus checks
|
||
|
|
the rules themselves rather than the code under review, so it runs on CI and before
|
||
|
|
a rule change, not on every local commit.
|
||
|
|
|
||
|
|
`task comment-lint` also runs inside `task pre-commit`, and as a Claude Code `Stop`
|
||
|
|
hook, so an agent is told before it finishes a turn and fixes the comment inside
|
||
|
|
that turn. Stop rather than per file write: a run costs the same for one file as for
|
||
|
|
twenty-five, and half of all writes in a turn go to a file already written in it.
|
||
|
|
|
||
|
|
Findings are scoped to comment text that is new, not to lines git calls new, so
|
||
|
|
reindenting or moving code does not resurface comments you did not write.
|
||
|
|
|
||
|
|
The rules are the `RULES` object in
|
||
|
|
[`scripts/lint/comment-rules.mjs`](../scripts/lint/comment-rules.mjs); the exact
|
||
|
|
condition for each is the predicate of the same name in that file, with the
|
||
|
|
readings it deliberately excludes beside it.
|
||
|
|
|
||
|
|
**Every rule blocks.** A rule that only warns is a rule nobody acts on. So a
|
||
|
|
finding you believe is wrong is a bug in the rule, not something to live with:
|
||
|
|
narrow the rule, or mark the line and say why.
|
||
|
|
|
||
|
|
Every comment form the repo writes is covered: `//` and `/* */`, Javadoc and JSDoc,
|
||
|
|
JSX comments, `#`, and Python docstrings. `CMT007` reads all three parameter
|
||
|
|
conventions in use here, Javadoc/JSDoc `@param`, Sphinx `:param name:` and Google
|
||
|
|
`name: description` under `Args:`.
|
||
|
|
|
||
|
|
Two engines, one rule set. `.ts`/`.tsx`/`.mjs` go to an oxlint JS plugin, so
|
||
|
|
comments come from the parser: a `//` inside a string is not a comment, and JSX
|
||
|
|
`{/* … */}` is. `.java`/`.py` go to a line scanner. Neither reads the other's
|
||
|
|
files, so they cannot disagree about one file. `scripts/lint/fixtures/` is the
|
||
|
|
corpus that keeps them meaning the same thing.
|
||
|
|
|
||
|
|
### When a finding is wrong
|
||
|
|
|
||
|
|
Name the rule on the line above:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// comment-lint-allow: CMT002
|
||
|
|
// ─── kept deliberately, because <reason> ───
|
||
|
|
```
|
||
|
|
|
||
|
|
There is no form that disables every rule, and the directive has to earn its
|
||
|
|
place. `CMT008` reports one that names something which is not a rule, and one that
|
||
|
|
silences nothing, so a typo does not read as a suppression and a stale
|
||
|
|
suppression does not sit there blinding the line. The whole comment must be the
|
||
|
|
directive; prose that mentions the syntax is just prose.
|
||
|
|
|
||
|
|
If you reach for this more than occasionally the rule is wrong: fix it in
|
||
|
|
`comment-rules.mjs` and update the fixture corpus in the same commit, so the diff
|
||
|
|
shows what moved.
|
||
|
|
|
||
|
|
### The existing backlog
|
||
|
|
|
||
|
|
`task pre-commit:comment-lint:all` reports the whole tree and never fails. There is
|
||
|
|
a standing backlog being cleared by directory; diff scoping is what keeps it off
|
||
|
|
whoever touches a file first.
|
||
|
|
|
||
|
|
To turn the editor hook off, put `{ "env": { "COMMENT_LINT_HOOK": "0" } }` in
|
||
|
|
`.claude/settings.local.json`. The commit-time gate still applies, so you lose the
|
||
|
|
early warning rather than the check.
|