DEV Community

Daniel Pertu
Daniel Pertu

Posted on

6,308 comment lines in 72,683, zero TODOs, and the 26-line comment above a single boolean

Nakodo is a Next.js app that runs outreach campaigns for brands. Every post I write about it starts by counting something, and this time the thing I counted is the comments, because I have strong habits there and had never checked whether they were real.

files            582
lines         72,683
code          61,860
blank          4,515
comment lines  6,308   (8.7%)
Enter fullscreen mode Exit fullscreen mode

Plus 7,085 // occurrences against 6,308 comment-only lines, so about 780 of them are trailing comments on a line of code, and 101 /* */ blocks, nearly all of them inside hand-drawn SVG components where they label a shape.

Zero TODOs

$ grep -rn "TODO\|FIXME\|HACK\|XXX" src
src/lib/research/brief.test.ts:28:  const brief = sanitizeBrief({ ...base, targetMarkets: [{ countryCode: "XXX", ... }] });
Enter fullscreen mode Exit fullscreen mode

One hit in 582 files, and it is an invalid ISO country code in a test.

This is not discipline, it is a different place to put them. Deferred work lives in markdown at the root of the repo and in four files under docs/, which get read when the question comes up again. A TODO in the code is a note addressed to whoever opens this file next, and on a one-developer codebase that is a note addressed to nobody, with no date, no reason and no way to find it except by opening the file it is already too late to be reading.

What that leaves in the code is comments that are not tasks. Three measurements of what they are instead:

comments that name another file path (src/...)   250
comments that contain a URL                       68
comments containing the word "because"            41
Enter fullscreen mode Exit fullscreen mode

The 250 are the interesting number. They are hyperlinks written by hand:

// One plan per user; limits are defined in src/lib/plans.ts. Paid plans mirror
// a Stripe subscription (src/lib/stripe/sync.ts), which is the source of truth:
// this row is rewritten from Stripe on every webhook, never edited by hand.
Enter fullscreen mode Exit fullscreen mode

A path in a comment is the only cross-reference a TypeScript file can make to a fact that lives in a different file and is not an import. The type system cannot say "this column is rewritten by that webhook and you must not edit it", and 250 of these say something of that shape. They also rot, which is the honest cost, and the mitigation is that a path is greppable: moving a file is a grep -rn "old/path" away from being consistent again, which a prose description of the same relationship is not.

The 68 URLs point almost entirely at other people's rules: a regulator's guidance on disclosure, a platform's API terms, a provider's usage policy. A comment is the right home for those because the code can only encode the conclusion, and the conclusion looks arbitrary without the link.

The density map says comments cluster around other people's systems

Comment lines as a share of total lines, by directory:

src/lib        230 files   4,089 / 28,042 lines   14.6%     26 files with none
src/db/schema.ts 1 file      278 / 1,266 lines    22.0%
src/content     79 files                          mostly none, 60 files with none
src/components/ui 24 files                        16 files with none
Enter fullscreen mode Exit fullscreen mode

And the six densest files in the repo, over 100 lines:

0.47  47/101   src/lib/stripe/managed-payments.ts
0.44  75/172   src/lib/publisher.ts
0.35  45/128   src/lib/seo/indexnow.ts
0.32  68/210   src/lib/limits/counters.ts
0.32  42/131   src/lib/support/kinds.ts
0.32  74/233   src/lib/proposal/negotiate.ts
Enter fullscreen mode Exit fullscreen mode

Every one of those files is a place where the code is the smaller half of the decision. Stripe's merchant-of-record service and which countries it is on in. The rules five sibling sites follow about naming their publisher. A search engine's submission protocol. How a plan's allowance is counted when there is no counter column. Which support questions a model is allowed to answer. What a negotiation may concede.

None of that is derivable from reading the code, and all of it is the reason the code is shaped as it is. Meanwhile the files with no comments at all are dominated by two kinds of file that genuinely have nothing to explain: generated components, and data.

The 26-line comment above a boolean

The densest file opens with the longest comment in the repo. Trimmed, because the full thing is 26 lines:

// Managed Payments, Stripe's merchant of record. With it on, Link (Stripe's
// reseller service) is the legal seller for that sale: it works out, collects,
// files and pays the VAT, sends the receipts, and handles disputes. It costs
// 3.5% on top of card processing, and the tax comes out of the payout.
//
// It's on only where a UK seller of digital services owes tax from the first
// sale, which is the EU. Everywhere else it stays off, so we don't pay the fee
// or charge tax that isn't due:
// - UK: off while under the £90k VAT registration threshold. Turning it on would
//   charge 20% UK VAT from the first sale. Revisit past the threshold.
// - US: off. No sales tax nexus at our volume.
// ...

// The master switch. Managed Payments has to be turned on in the Stripe
// Dashboard, with its terms accepted, in each mode (test and live). Until then
// Stripe rejects these sessions, and Checkout opens without it instead.
export const MANAGED_PAYMENTS_ENABLED = true;
Enter fullscreen mode Exit fullscreen mode

Twenty-six lines of comment and one boolean. Every number in the comment is a fact about tax law or a price list, none of it is checkable by a compiler or a test, and all of it is what a future reader needs in order to decide whether the boolean should still be true. The test I apply to a long comment is exactly that: if the next person's job is to decide whether this line should change, the comment has to contain the inputs to that decision.

The second instinct worth naming is that the comment includes the reason it is off everywhere else. A comment that says what the code does is noise. A comment that says what the code deliberately does not do is often the only record that the alternative was considered.

The schema is 22 per cent comments, and that is where they earn most

$ grep -c "^\s*//" src/db/schema.ts
278
Enter fullscreen mode Exit fullscreen mode

Two hundred and seventy-eight comment lines in a 1,266-line schema. The reason is simple: a column name has about four words to say what a value means, and the interesting part of a nullable column is usually what the null means.

viewCount: bigint({ mode: "number" }),
likeCount: bigint({ mode: "number" }), // null when hidden
commentCount: bigint({ mode: "number" }), // null when comments are off
Enter fullscreen mode Exit fullscreen mode

Three columns of the same type. Two of them carry six words that stop a reader rendering 0 likes on a post whose likes are hidden. There is no type that distinguishes them, and the alternative to those comments is a wrong number on a screen.

Findings

134 files have no comment at all. Broken down, that is 22 guide pages and 38 other content data files, 16 of the 24 generated UI components, and 21 files behind the login. Only four of the 134 are over 100 lines:

143  src/content/tools/sponsorship-cost-calculator.ts
128  src/app/(app)/nav.tsx
115  src/components/ui/table.tsx
102  src/components/ui/card.tsx
Enter fullscreen mode Exit fullscreen mode

The two ui files came from a CLI and are the only code in the repo with no semicolons, which is its own tell. The 143-line one is the copy and the figures behind the sponsorship cost calculator, and it is prose: every line already says what it is.

src/app/(app)/nav.tsx is the real gap. It is 128 hand-written lines, it decides what every signed-in person sees in the sidebar, and it says nothing about itself. That one gets a comment.

No lint rule enforces any of this. The whole ESLint config is 18 lines: two presets from eslint-config-next and an ignore list, with no comment or JSDoc rules in it at all, the 101 block comments are not JSDoc in any meaningful sense, and I would not add a rule: a rule can only require a comment, which produces comments that restate the function name. The thing that keeps the density at 8.7 per cent is that I write the comment first when I do not yet know what the code is.

The ratio I would watch. src/lib at 14.6 per cent and src/content at nearly zero is the shape I want: explanation where decisions live, silence where data lives. If src/lib ever drops toward the repo average, it means decisions have moved into files that are not explaining themselves, which is a thing a one-line script can now tell me, because I finally wrote it.

Top comments (0)