This document outlines a language-agnostic source code commenting system that I evolved over the years. A major feature of it addresses the problem that you often can’t quickly and reliably know how far down the scope of a comment reaches, i.e., where exactly its applicability ends. In general, the system works against unclarity, ambiguities, and directed-attention fatigue.
-
Traditional line comments are headings that apply on their indentation level until superseded.
-
Headings may be empty to simply limit the scope of the previous heading. Empty headings must be written like regular headings, just without text—so, with an empty line before, not after.
-
Comments with the
.
marker like//. Text.
are special chunk headings that only apply until the next empty line. -
Info comments like
//i Text.
are like end-of-line comments that were wrapped to a new line. While having them after the code they’re applying to is preferred—because they aren’t headings, and are comparable to long end-of-line comments—with good reason, they can actually be placed freely, even on their own without an adjacent code line. They belong to the code that they touch; but if this is ambiguous, because they touch code at their start and end, their text must casually refer to what code they belong to. They’re not meant to directly annotate long runs of code; they may indirectly do that using their standalone form.
It’s assumed that you have an accurate understanding of the terms "line comment", "block comment", "inline line comment", and "inline block comment" (see also the "Common attributes" section of the Wikipedia article on comments).
If available in the language, you’d normally use line comments for the purposes of this system.
Use empty lines before headings in regular runs of code (except at the start of the indentation level). Generally sensibly group lines of code using empty lines. Just like paragraph breaks, periods, semicolons, and commas provide different levels of separators in natural language, this system extends and clears up the levels of separators that are available for use in code. Empty lines are part of these different levels of separators.
Almost always write comments in regular sentence style, including capitalization and periods. An exception are short end-of-line comments coming across as especially technical, like case 0x0060: // Character: `
.
The system ascribes certain meanings to comments. Most of these comment types require a specific marker, which is a single character directly and without space after the comment’s opening delimiter. (The ESLint rule spaced-comment
is an example for how markers can be supported.)
When multiple lines in succession are needed for a comment, repeat the marker for every line comment (yet undecided for block comments). When this is simply about wrapping prose, though, I like to rely on the IDE wrapping it for me, because maintaining wraps for changing prose is too much to ask for. (It would be great if there was a VS Code extension that displayed ghost repetitions of the comment delimiter and marker.)
The scope of a comment always ends by the end of its indentation level. It also ends with two consecutive empty lines, should your language and project use them.
Marker: :
(colon)
Headings are the only comment type where the marker is optional. You’d normally omit the marker. But you may decide to be explicit about it—like perhaps when heavily mixing chunk and non-chunk headings (the latter meaning the currently described heading type).
Headings divide the indentation level into segments, and optionally label those segments. The label provides a bird’s-eye view on the code by summarizing it as concisely as possible, which is more necessary the greater a run of code’s vertical length. Even though it’s called a heading, imperative language is still okay, although not required.
A heading is normally preceded by an empty line (if not at the start of the indentation level). An exception can be a context where people generally avoid taking up too much space, like a multiline if-condition.
In edge cases of short code, the difference between headings and info comments can be blurry and not as important. Technically, they’re still headings, though, and should have at least somewhat of a heading flair (use an info comment otherwise).
If the segment of a heading isn’t labeled, the heading is empty. In the case of line comments, this means having nothing but the line comment delimiter on the line. Even if a heading is empty, it still always stands directly before a line of code and is preceded by an empty line, separating it from the previous heading’s code.
Empty headings are used to simply limit the scope of the previous heading, which could in turn be empty. It may additionally be preparation for a non-empty heading that you intend to write later.
(If there’s no heading at the start of an indentation level, you could say that it starts with an implicit empty heading.)
Of course, the general recommendation is to avoid overly long functions, which is why subheadings are rarely used. But in a case like the following, you can gain some clarity by making use of subheadings:
-
You’re first writing long code, and don’t yet know how to best split it into multiple functions.
-
You’re preparing old code to do exactly that.
-
It appears unwise to stray from a rare flow state.
-
It’s currently unrealistic for other reasons, like with already difficult-to-maintain code that you edit and try to clarify.
Heading levels exist per indentation level. A deeper level is created by adding a space and additional delimiter to the line comment delimiter. (Subheadings weren’t defined for block comments yet.) The explicit marker :
, if used, is added after the last delimiter in the sequence of delimiters.
Avoid the segments becoming too small and too many. If a final return
doesn’t really fit in with the previous heading, it may be okay to separate it with an empty heading, but it’s not a problem to introduce variables in an upper heading, which starts to work with them, that you continue to use in a lower heading. It’s like a person building on the work of a pioneer.
When wanting to label an else-block, you can split the common } else {
(e.g., of the One True Brace Style) before the else
, even if you wouldn’t do that without a heading. If appropriate, it can generally also be nice to linguistically connect two consecutive headings on the same indentation level using ellipses:
// Foo...
if needs_foo_xor_bar {
foo();
}
// ...or bar.
else {
bar();
}
Marker: .
(dot) •
Mnemonic: less than :
Chunk headings are headings which only apply to the chunk of code before the next empty line (or, as always, the end of the indentation level).
Just like with non-chunk headings, chunk headings can indicate levels. This is how they interact with each other:
-
A chunk heading always ends the previous non-chunk heading of the same or a deeper level. (This means chunk headings are the only way that code can become headingless again after having used a non-chunk heading: The code after the chunk belonging to a top-level chunk heading will always be headingless, if there’s no new heading.)
-
Higher non-chunk heading levels continue to apply in the scope of and after the nested chunk heading.
With chunk headings, in contrast to non-chunk headings, there’s no general recommendation against subheadings.
(A possible extension for rare cases may be to use the marker |
without anything after it, and without an empty line before the line comment to connect two chunks of code, so that the previous chunk heading applies to both chunks.)
Marker: i
•
Mnemonic: the typical i-symbol of an info desk
Info comments provide additional information, or put code in a certain light. First and foremost, empty lines are what control what chunk of code they’re associated with. If this is ambiguous, because there isn’t an empty line neither before nor after the comment, the comment’s text must casually and concisely semantically reference the respective adjacent chunk of code. If an info comment is surrounded by two empty lines (or an empty line and the start/end of the indentation level), this is its standalone form, which provides information in the general context of the indentation level.
Since info comments aren’t headings, and they’re comparable to and were derived from end-of-line comments, the preferred placement of info comments is after their associated chunk of code. Info comments can actually be freely placed, though; so, if you think you have a good reason, you may also place them before their associated chunk of code. You shouldn’t disregard the general preference, though.
You can continue to use short end-of-line comments, which are written without a marker and have info comment flair. If an end-of-line comment gets too long, you can transform it into an info comment with i
marker on its own line. Keep in mind that this may necessitate to reference the associated chunk of code in its text.
If you loosely want to annotate an algorithm, or a point in its flow, use a standalone info comment. If you want to rigidly annotate a longer run of code, use a heading instead. Note that there’s the possibility to use a heading, directly followed by an info comment, but that headings may also contain small amounts of info text after their concise summary (in parentheses for secondary, circumstantial info).
Marker: >
•
Mnemonic: used in terminal apps (at least their icons), browser consoles, and the VS Code command palette
To quickly summarize future code, you can use a placeholder comment with the >
marker, and pretend it gets executed. This enables you to, e.g., develop an algorithm on a higher abstraction level and flesh it out later.
The absent space between the opening comment delimiter and the marker character is an integral part of the system. If your language requires a space after the comment delimiter, or the marker is the same character as the last character of the comment delimiter, you can use |
at the earliest position possible after the comment delimiter, and pretend it’s the last character of the comment delimiter.
Windows Batch example:
echo foo
echo bar
rem |i Note the difference to "baz".
HTML example (>
as first character is disallowed; space before |
makes it more readable, but isn’t required):
<foo />
<!-- |> Something planned here. -->
<bar />
When your language supports inline block comments, use them directly after numeric literals to specify their unit, if applicable:
setTimeout(() => console.log("timeout"), 1000 /*ms*/);
setTimeout(() => console.log("timeout"), 1 /*min*/ * 60 * 1000);
canvas.width = 500 /*px*/;
You can make use of multicharacter comment markers to have an easier time keeping track of your work, which is especially important in chaotic situations, or when working at the edge of your cognitive capacity. They’re not meant to be committed to version control.
A VS Code extension like Better Comments can be used to make these comments more salient:
Relevant Better Comments settings
{
"tag": "HIER",
"color": "#ed02c2",
"backgroundColor": "#ed02c228",
"bold": true,
},
{
"tag": "dbg",
"color": "#5ab9f0",
"backgroundColor": "#5ab9f028",
},
{
"tag": "-dbg",
"color": "#5ab9f0",
"backgroundColor": "#5ab9f028",
},
{
"tag": "teils dbg",
"color": "#5ab9f0",
"backgroundColor": "#5ab9f028",
},
(These markers could theoretically be replaced with more unique ones by researching using a language dataset for English or many Latin-alphabet languages at once to find the rarest three-letter combinations, which would eliminate more false positives when searching for the markers.)
I use comments with just the text HIER
, which is German for "here", to keep track of what places exactly I’m currently working on. When jumping between different places, and between code and documentation, this helps me avoid or reduce directed-attention fatigue.
When rewriting a run of code with considerable vertical length, the HIER
comment has a special role, and could be called a "progressor" (I previously called it a "scanner", but these semantics were flawed). In each iteration that you work on a piece of code, the HIER
comment travels or progresses over the code from top to bottom. The code above it has the status of being fully rewritten in the context of the current iteration. Avoid moving it up again; this should be a rare exception. Below it, you may still jump up and down a little; but whenever you come back to the code, the code above the progressor can immediately be disregarded.
When debugging a program with possible breaks, e.g., at breakpoints, you can also use HIER
comments to easily get back to spots where the cursor has jumped away from.
Additionally, there’s the dbg
marker, which can be read as "debugwise". It can be used in multiple ways:
-
To comment out code debugwise, i.e., temporarily to check how the program functions without it, add the word
dbg
after the opening comment delimiter of the first comment in a vertical sequence of comments. To save effort, you don’t need to repeat the marker for every following line comment; the first marker marks the whole sequence of comments until the last commented-out code line. If you need to comment out lines debugwise that reach down right before a commented-out code line that wasn’t commented out debugwise, you need to usedbg{
in the first anddbg}
in the last line. -
To insert code debugwise, i.e., temporarily to check how the program functions with it—possibly as a replacement for code commented out debugwise—add a line comment with just the text
dbg
at the end of a line. Should the language not support inline line comments, you can use-dbg
, which contains the marker character-
, which I used in the past (but don’t anymore) for other postpositioned end-of-line comments that were wrapped to a new line. To save effort, you can also enclose multiple lines inside line comments with just the textdbg{
anddbg}
, or usedbg:
, which reaches to the end of the indentation level. -
To change a line of code when the change’s reversion is obvious, you can take the shortcut of adding an end-of-line comment with just the text
teils dbg
. "teils" is German for "partly". If you’d translate and abbreviate it, you’d haveptly dbg
.
These means enable you to recover code that you had to edit heavily for debug purposes at various smaller to larger spots, even if it was already changed after the last commit to version control.
If you want to defer addressing a TODO
comment until a certain event occurred, use an all-caps prefix. This enables you to easily exclude them using regular expressions (based on the first two letters), or specifically search for them in the project.
-
When there’s no specific event to wait for, and you just need to let some time pass, you can simply add some time to the current date, and possibly round it coarsely, like up to the next half-year. Month accuracy should normally suffice and avoid to bewilder your future self. The resulting point in time is the earliest that the todo can be addressed, but there’s no obligation to do it exactly then.
-
When awaiting an upcoming version of software that the code depends on, don’t guess the next version number, but write
AFTER
together with the current version number. -
When initially writing the bulk of the code of a project, a prefix like
LATE
can be used to defer todos to a late stage of the early life of the project.
-
Dr. David J. Pearce, author of the Whiley programming language, apparently segments functions, and uses lines with just
//
as separators that limit the scope of the previous comment (just without empty line before). See, e.g., here and here, in each case to the end of the file. These separators also made their way into the code of his employer Consensys. -
In the VS Code codebase, I found a place in a TypeScript file where a bare block statement (
{ … }
) was used with comments inside that resemble subheadings.