README
¶
commentlen
A configurable size and shape linter for Go comments, usable as a golangci-lint module plugin or as a standalone binary.
Existing linters check one thing each: lll measures any line, godot wants a
period, revive wants a doc comment to exist. None of them can say "an inline
note may be two lines, an exported doc comment may be six, and neither may be
longer than the code it explains". That is what this one does.
foo.go:41:2: inline comment is 4 lines long, max 2
foo.go:58:2: inline comment is 3 lines for 2 lines of code, max ratio 1 — say less, or extract the code
foo.go:73:14: trailing comments are not allowed
bar.go:12:1: doc comment should start with "Config"
bar.go:19:1: TODO marker left in a comment: finish it or ask, do not park it here
bar.go:88:2: commented-out code (3 lines): delete it, git remembers
What it checks
Every comment is classified into exactly one kind, and each kind has its own limits:
| kind | what it is |
|---|---|
package |
the package doc, directly above package x |
exported |
doc comment on an exported top-level declaration |
unexported |
doc comment on an unexported top-level declaration |
field |
comment on a struct field, interface method, or const/var element |
inline |
comment on its own line inside a function body |
trailing |
same-line comment after code inside a function body |
other |
everything else — floating, inside literals, end of file |
A method counts as exported only when its receiver type is exported too, the
way golint defines the exported API. A doc comment on a local var inside a
function is classified inline, because that is how it reads.
On top of the per-kind size limits:
- ratio — a comment must not be longer than the code it describes. For an inline comment the described code is the next statement plus the statements following it with no blank line or comment in between; for a doc comment it is the declaration.
- max-inline-per-func — a budget of inline comments per function body. Consecutive comment lines count as one comment. Function literals get their own budget.
- style — banned tags (
TODO,FIXME, …), banned phrases (hedging preambles, the vague-abstraction phrases, configurable), decorative separators, authorship/change-history metadata, and commented-out code. - godoc — the doc comment opens with the symbol name, is capitalized, ends with a period, and spells a bool-returning function's contract "reports whether" rather than "returns true if".
What is never reported
- Generated files — anything with a
// Code generated … DO NOT EDIT.marker above the package clause (skip-generated, on by default), plus whatevergenerated-extraadds. - Directives —
//go:generate,//nolint,//+build,//go:build,//easyjson:jsonand any//word:wordcomment, pergo/ast's own rule (ignore-directives, on by default). Legacy spellings and tool-specific prefixes are covered bydirective-prefixes-extra. - godoc code blocks — indented or ``` fenced blocks inside a doc comment are
exempt from both width and line count (
ignore-code-blocks). - Blank comment lines —
//on its own does not count towardsmax-linesunlesscount-blank-linesis set. - Long links — a line over the width limit only because of one unbreakable
token (URL, import path, email) is left alone (
ignore-urls).
Install
As a golangci-lint plugin
golangci-lint loads plugins by building a binary that includes them. Add
.custom-gcl.yml to your repository:
version: v2.11.4 # your golangci-lint version
name: golangci-lint-commentlen
destination: ./bin
plugins:
- module: github.com/swchck/commentlen
version: v0.2.0
Then build it and enable the linter in .golangci.yml:
$ golangci-lint custom
$ ./bin/golangci-lint-commentlen run ./...
version: "2"
linters:
enable:
- commentlen
settings:
custom:
commentlen:
type: module
description: size and shape limits for Go comments
settings:
preset: strict
The whole settings: block below commentlen is what this README documents.
Unknown keys are an error, so a typo fails the run instead of being ignored.
While developing the plugin itself, point at the checkout instead of a version:
plugins:
- module: github.com/swchck/commentlen
path: /path/to/commentlen
As a standalone binary
$ go install github.com/swchck/commentlen/cmd/commentlen@latest
$ commentlen -preset strict ./...
$ commentlen -config .commentlen.yml ./...
With no -config, the binary looks for .commentlen.yml, .commentlen.yaml
and .commentlen.json in the working directory. The file holds the same keys as
the golangci-lint settings: block, unindented — see .commentlen.yml in this
repository.
Presets
A preset is the baseline; every key you set is layered on top of it, so
preset: strict plus kinds.inline.max-lines: 3 means "strict, but three lines
inline".
| kind / setting | balanced (default) |
strict |
loose |
|---|---|---|---|
package |
∞ / 100 | ∞ / 100 | ∞ / ∞ |
exported |
10 / 100 | 6 / 100 | ∞ / 120 |
unexported |
6 / 100 | 2 / 100 | ∞ / 120 |
field |
3 / 100 | 2 / 100 | ∞ / 120 |
inline |
3 / 100 | 2 / 100 | 6 / 120 |
trailing |
1 / 80 | forbidden | 1 / 120 |
other |
4 / 100 | 2 / 100 | ∞ / 120 |
max-inline-per-func |
off | 2 | off |
ratio |
off | on | off |
style |
on | on | off |
godoc |
off | on | off |
Cells are max-lines / max-width; ∞ is 0, meaning unlimited.
Settings reference
File selection
| key | default | meaning |
|---|---|---|
skip-generated |
true |
skip files with a generated-code marker above the package clause |
generated-extra |
— | extra regexps that mark a file as generated |
skip-tests |
false |
skip _test.go entirely |
exclude-files |
— | regexps matched against the slash-separated path |
Comment selection
| key | default | meaning |
|---|---|---|
exclude-patterns |
— | regexps matched against a comment's text; a match exempts the comment from every check |
ignore-directives |
true |
exempt tool directives |
directive-prefixes-extra |
— | additional directive prefixes (e.g. ffjson:) |
ignore-urls |
true |
let a single unbreakable token overflow the width |
ignore-code-blocks |
true |
exempt godoc code blocks |
count-blank-lines |
false |
count // lines towards max-lines |
width-includes-indent |
true |
measure width from column 1, not from the marker |
Sizes
defaults: # applied to every kind first
max-width: 100
kinds: # then refined per kind
inline:
max-lines: 2
max-width: 100
trailing:
forbidden: true # report the comment whatever its size
other:
disabled: true # no checks at all for this kind
max-lines: 0 and max-width: 0 mean unlimited. Width is counted in runes, so
non-ASCII prose is measured the way it looks.
Ratio
ratio:
enabled: true
max: 1.0 # comment lines / code lines
min-code-lines: 2 # skip when the code is shorter, so a one-liner may carry an explanation
kinds: [inline, unexported, other]
Style
style:
tags: [TODO, FIXME, HACK, XXX, OPTIMIZE, BUG] # replaces the default list
tags-enabled: true
patterns: [] # replaces the built-in phrase list
patterns-extra: # appends to it
- pattern: '\b[A-Z]{2,10}-\d{1,6}\b'
message: ticket key in a comment — explain the reason instead
use-default-patterns: true
banners: true # ===== separators and the like
metadata: true # @author, "Modified by:", change history — not copyright headers
commented-code: true
commented-code-min-lines: 2
A tag is only reported at the head of a line or before a colon, so "the todos of a user" is prose, not an abandoned task. The built-in phrase list covers hedging preambles ("Note that", "It's important to note"), the "not just X, but Y" construction, vague abstraction ("for performance reasons", "to ensure correctness") and signature-restating openings.
Per-path overrides
An override is a settings layer applied to the files whose path matches, and the first matching entry wins — order them from most to least specific. Anything a layer does not mention is inherited, so limits can be relaxed without losing the content rules:
preset: strict
overrides:
# test prose is longer by nature, but a TODO or the wrong language is just as
# unwelcome there as in production code
- path: '_test\.go$'
defaults:
max-lines: 0 # 0 lifts every length cap at once
ratio:
enabled: false
max-inline-per-func: 0
# a package that documents a protocol may keep long doc comments
- path: '^internal/wire/'
kinds:
exported:
max-lines: 20
An override accepts every key of the top-level settings except preset,
overrides themselves, and the four that select which files are looked at at
all (skip-generated, skip-tests, exclude-files, generated-extra) — those
stay global. Overrides can tighten as well as relax.
Note the difference from golangci-lint's own exclusions: those filter finished
diagnostics by message text, which breaks when the wording changes, and they do
not exist at all when the standalone binary runs. An override changes what the
linter looks for in the first place.
Godoc
godoc:
enabled: true
starts-with-name: true # "Quote returns …", "Package p …"
capitalized: true # skipped when the symbol's own name is lower case
ends-with-period: true # a colon is accepted when a list or code block follows
reports-whether: true # flags "returns true if" on bool-returning functions
scope: exported # or "all", to include unexported symbols and fields
The naming and capitalization rules accept the usual variations: an article in
front ("A Buffer is …"), the name used as a call ("Quote(s) returns …"), and a
generic instantiation. Two cases are waived entirely, because godoc never renders
them: a package main doc, whose convention is "Command foo", and the
Test/Benchmark/Fuzz/Example functions of a _test.go file. The sentence
rules — capital, final period — still apply there.
Turning it off in source
//commentlen:disable-file // the whole file
//commentlen:disable
// … anything here is unchecked …
//commentlen:enable
//commentlen:ignore // this declaration only
// a doc comment that may be as long as it likes
func Documented() {}
//nolint:commentlen // honoured by both runners
//commentlen:ignore in a doc comment covers the comment and the declaration it
documents. Everything after the directive word is free text, so you can say why:
//commentlen:ignore the doc has to quote the phrases it bans
Inside golangci-lint you also have the usual linters.exclusions.rules for
paths and message texts — see examples/golangci.strict.yml.
Performance
The linter asks for syntax only — never type information — so golangci-lint keeps it in the fast load mode and never has to type-check a package for it. Per file it does one AST walk plus one pass over the comment list.
On an 86 KB generated file (200 functions, ~1600 comment lines), Apple M3 Pro:
| what | time | vs parsing | allocs |
|---|---|---|---|
go/parser with comments (the unavoidable baseline) |
1.14 ms | 1.0× | 19645 |
| commentlen, sizes + ratio only | 0.63 ms | 0.55× | 21 |
commentlen, balanced (sizes + style) |
1.78 ms | 1.6× | 426 |
commentlen, strict (everything) |
1.83 ms | 1.6× | 637 |
Three things keep it there:
- No allocation per comment. Lines are substrings of the source, and the line buffer, the prose buffer and the per-file maps and slices are reused across comments and across files.
- Regexps behind a prefilter. Every pattern is parsed once to derive the literals it cannot match without; a comment holding none of them skips the regexp entirely. This alone took the full check from 14.7 ms to 1.7 ms. The prefilter is fuzz-tested against the property that it never hides a match.
- Nothing computed that is not asked for. Statement spans are collected only when the ratio check is on; a disabled kind is dropped before its comment is even split into lines.
Development
$ make test # unit tests
$ make race # the same under -race
$ make bench # benchmarks
$ make fuzz # fuzz the prefilter invariant
$ make build # standalone binary
$ make custom # golangci-lint binary with the plugin compiled in
The linter runs on its own source under preset: strict and is expected to stay
clean:
$ make custom && ./bin/golangci-lint-commentlen run ./...
Documentation
¶
Overview ¶
Package commentlen registers the commentlen linter as a golangci-lint module plugin.
Build a custom golangci-lint binary that includes it by listing this module in .custom-gcl.yml and running `golangci-lint custom`; then enable the linter under linters.settings.custom in .golangci.yml. The settings block is decoded into analyzer.Settings, and unknown keys are rejected, so a typo in the configuration fails the run instead of being silently ignored.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package analyzer implements commentlen, a configurable size and style linter for Go comments.
|
Package analyzer implements commentlen, a configurable size and style linter for Go comments. |
|
cmd
|
|
|
commentlen
command
Command commentlen runs the comment linter on its own, outside golangci-lint.
|
Command commentlen runs the comment linter on its own, outside golangci-lint. |