laconic /ləˈkɒnɪk/ adj. — using few words; terse. After Laconia, the region around Sparta, whose people were famed for it: Philip II of Macedon is said to have written "If I invade Laconia, I will level Lacedaemon to the ground," and to have been answered, "If."
A golangci-lint linter that forces your Clanker to write shorter code comments.
Two limits, both configurable:
- a comment line longer than
max-line-length(default 120) - a comment group taller than
max-lines(default 5)
A comment group is a run of comments with no blank line and no other tokens
between them, so 6 consecutive // lines are one group, not 6 comments.
foo.go:80:1: comment line is 158 characters long, limit is 120 (laconiccomments)
bar.go:192:1: comment spans 89 lines, limit is 5 (laconiccomments)
Findings you cannot act on are worse than no findings, so:
- Tooling directives —
//go:generate,//nolint:...,//lint:ignore. Reflowing these breaks them. - Generated files — anything carrying the
// Code generated ... DO NOT EDIT.marker from go.dev/s/generatedcode. - File headers — copyright, license and build-constraint blocks above the
packageclause. Boilerplate you did not write and cannot shorten. The package doc comment is not a header: it sits directly onpackagewith no blank line, and gets checked like the prose it is. - Unbreakable tokens — a line whose excess comes from a single token no rewrap could shorten: a URL, a commit hash, a template literal. Without this exemption, every reference link in a codebase is a finding with no fix. On the first codebase this ran against, 2 of 5 findings in a module were bare reference links.
//nolint:laconiccomments works too, because golangci-lint applies nolint
directives to plugins like any other linter.
golangci-lint has no runtime plugin loading, so a linter has to be compiled in. That is what module plugins are, and it takes two files plus one command.
.custom-gcl.yml — the build recipe:
version: v2.13.2 # the golangci-lint release to build from
name: custom-gcl
destination: ./bin
plugins:
- module: github.com/hidalgopl/laconiccomments
version: v0.1.0
import: github.com/hidalgopl/laconiccomments/plugin.golangci.yml — enable it like any built-in:
version: "2"
linters:
enable:
- laconiccomments
settings:
custom:
laconiccomments:
type: module
description: Forces your Clanker to write shorter code comments.
settings:
max-line-length: 120
max-lines: 5Then:
golangci-lint custom # writes ./bin/custom-gcl
./bin/custom-gcl run./bin/custom-gcl is golangci-lint plus this linter. Use it everywhere you used
golangci-lint, including in CI.
Error: build linters: plugin(laconiccomments): plugin "laconiccomments" not found
A plain golangci-lint refuses to start on a config that names a plugin it
was not built with. The config and the binary travel together — wire the
golangci-lint custom step into whatever builds your lint target, so nobody has
to remember.
Swap version: for a local checkout; relative paths work:
plugins:
- module: github.com/hidalgopl/laconiccomments
path: ../laconiccomments
import: github.com/hidalgopl/laconiccomments/pluginIt is an ordinary go/analysis analyzer, so it also runs standalone or under
go vet. You lose nolint directives, which are a golangci-lint feature.
On Go 1.24 or newer, a tool directive is the shortest path in: the version is
pinned in your own go.mod, there is no binary to install, and CI needs no
build step.
go get -tool github.com/hidalgopl/laconiccomments/cmd/laconiccomments@v0.1.0
go tool laconiccomments ./...
go tool laconiccomments -max-line-length=80 ./...go get -tool records the tool in your go.mod and its dependencies in your
go.sum, so golang.org/x/tools joins your module graph.
Without a tool directive, install the binary yourself:
go install github.com/hidalgopl/laconiccomments/cmd/laconiccomments@latest
laconiccomments -max-line-length=80 ./...
go vet -vettool=$(which laconiccomments) ./...make test # go test ./...
make cover # statement coverage over every package, plugin wiring included
make lint # builds bin/custom-gcl, then dogfoods this linter on itselfTests use analysistest over testdata/src, a GOPATH-style tree, matching each
diagnostic against a // want "regexp" comment on the same line. Two
consequences shape that testdata, and will bite you if you extend it:
- The annotation counts toward its own line's length, so patterns use
\d+rather than a fixed character count. - A
wantinside a/* ... */registers on the line of the opening/*, and everything afterwantin that comment is parsed as further expectations. A multi-line block comment therefore cannot carry its own annotation;testdata/src/groupscovers block comments with a run of consecutive one-line/* */comments instead.
Those two limits are why analyzer_test.go exists alongside them: it runs the
analyzer over in-memory sources through a hand-built analysis.Pass, so a case
can assert the exact line, column and character count, block comments included.
Reach for it when the position or the number in the message is the point, and
for testdata when the case reads better as a real Go file.
MIT. See LICENSE.