Adding a lint rule
A rule is not done when Apply works — it has to be registered, made
configurable, and documented, or it is invisible to users. Copy the closest
existing rule rather than starting from scratch.
Files to touch
| File | Always? | What goes in |
|---|---|---|
internal/addon/rules/<camelCase>Rule.go |
yes | the rule |
internal/addon/rules/<camelCase>Rule_test.go |
yes | table-driven tests |
internal/cmd/subcmds/rules.go |
yes | register in newAllInternalRules |
internal/linter/config/rulesOption.go |
yes | one field on RulesOption |
README.md |
yes | a row in the ## Rules table |
_example/config/.protolint.yaml |
yes | a commented example entry |
internal/linter/config/<name>Option.go (+ test) |
only if configurable | a dedicated option type |
_testdata/rules/<name>/*.proto |
when fixtures help | input protos |
Naming is mechanical: rule ID FIELD_NUMBERS_ORDER_ASCENDING → type
FieldNumbersOrderAscendingRule → file fieldNumbersOrderAscendingRule.go →
config key field_numbers_order_ascending.
The rule type
Embed RuleWithSeverity and implement:
ID() string—UPPER_SNAKE_CASE, uniquePurpose() string— one sentence; it is user-facing and goes in the README verbatimIsOfficial() bool— this decides whether the rule is on by default
Rules.Default() in internal/linter/rule/rules.go keeps only rules where
IsOfficial() is true. Return true only for rules from the
official style guide; anything
opinionated returns false and users opt in via .protolint.yaml.
Apply(proto *parser.Proto) ([]report.Failure, error) walks the AST with a
visitor from linter/visitor.
Fixable and auto-disable
Both are opt-in and both show up as columns in the README table.
- Fixable — build the visitor with
visitor.NewBaseFixableVisitorand take afixMode boolin the constructor. Users get it via-fix. - AutoDisable — take
autoDisableType autodisable.PlacementType. The constructor must forcefixMode = falsewhenautoDisableType != autodisable.Noop; the two cannot both apply. Users get it via-auto_disable.
Rules whose fix cannot break wire compatibility are marked *1 in the README
instead of supporting auto-disable — for those, fixing is always safe.
Configuration
If severity is the only knob, use the shared CustomizableSeverityOption in
rulesOption.go. Add a dedicated option type only when the rule needs its own
settings, and give it a test — these types are parsed from YAML, JSON and TOML,
so all three struct tags must be present and identical.
Verify
make test/lint && make test
make run/cmd/protolint ARG="lint _example/proto"
Confirm the rule appears where users will look for it:
go run cmd/protolint/main.go list | grep YOUR_RULE_ID
A rule missing from that list was implemented but never registered.