brandid-lint
brandid-lint scans Go source files for brand types (empty structs used as
id.ID[Brand, Value] type arguments) that are missing the Name() string
method (rule BD001), and can insert the missing stubs for you. The
opt-in rule BD003 flags primitive-typed identifiers that should become
branded ID types.
It lives in the separate linter/ module of this repository:
go run github.com/larsartmann/go-branded-id/linter/cmd/brandid-lint@latest ./myproject/When migrating to named brands, every brand type needs a Name() method to get
debug-visible IDs ("User:abc123" instead of "abc123"). Finding and writing
these by hand across a large codebase is tedious and error-prone.
# Detect (default: BD001 + BD002): scan and print findingsbrandid-lint ./myproject/
# Autofix: insert the suggested Name() stub for every unsuppressed findingbrandid-lint -fix ./myproject/
# SARIF output for CI dashboardsbrandid-lint -format sarif ./myproject/
# Also run BD003 (primitive-typed IDs)brandid-lint -rules BD001,BD003 ./myproject/Exit codes: 0 = clean, 1 = findings remain, 2 = invocation or runtime
error. Detection is a dry run by default; -fix is the only mode that writes
files, and it never touches suppressed brands.
Output Format
Section titled “Output Format”main.go:12:6 WARNING BD001: brand type UserBrand is used with id.ID but has no Name() string method 💡 func (UserBrand) Name() string { return "User" }Rule BD003: Primitive-Typed IDs (Opt-In)
Section titled “Rule BD003: Primitive-Typed IDs (Opt-In)”BD003 flags string/int/uint/bool parameters, named return values, and
struct fields whose name marks them as an identifier — userId, order_id,
PID — and suggests the branded type to adopt instead. It never runs by
default (brandid-lint runs fleet-wide, and repos that deliberately silenced
the old branching-flow strong-id rule must not be flooded by an upgrade):
select it with -rules BD003 (alone or with BD001).
main.go:8:6 ERROR BD003: Replace parameter 'userId' (string) with strong ID type UserID in getUser 💡 Replace with strong ID type UserID metadata: typeDefinition = type UserID = id.ID[UserBrand, string] type UserBrand struct{}Detection is ported from branching-flow strong-id (finding parity is gated
by scripts/parity-bd003.sh), so counts carry over when migrating. BD003 has
no autofix: a real fix rewrites the declaration and every call site, which is
a codemod’s job. Severities rank the domain (user/order/product →
error in signatures, config/item → warning, the rest → info), and the
scan skips cmd/, ui/, output, test-helper, and tooling paths plus
_test.go files, and generated files (the standard
Code generated ... DO NOT EDIT. header — sqlc, templ, protobuf, mocks) are
skipped for every rule, exactly like the rule’s branching-flow ancestor.
String/Error method declarations are exempt (interface
contracts stay primitive).
Suppress a BD003 finding where the decision lives:
// brandid-lint:ignore(BD003) external API contract, name is fixedfunc keep(userId string)The directive may sit on the declaration line, directly above it, or on the enclosing function or type declaration (a doc directive covers all of that declaration’s ID params, returns, and fields). The reason is mandatory, and a directive that suppresses nothing is reported as BD002.
Suppressing a Finding
Section titled “Suppressing a Finding”Some brands deliberately skip Name() — for example CQRS marker types whose
String() output is used directly as a storage key. Suppress the finding in
source, where the decision lives:
// brandid-lint:ignore(BD001) String() output is the stream name keytype StreamMarker struct{}The directive may sit on the declaration line or as the last line of the
comment group directly above it. The reason is mandatory. A directive that
cannot do its job — malformed syntax, missing reason, unknown rule, stale
(the brand has Name() by now), duplicate, or annotating no declaration —
is reported as a BD002 finding instead of silently doing nothing.
How It Works
Section titled “How It Works”- Walks
.gofiles (skipping.git,vendor,node_modules,testdata) and parses the AST. - Finds
id.ID[Brand, Value]andID[Brand, Value]usages (both qualified and same-package). - Checks if the brand type (must be an empty struct) has a
Name() stringmethod — any body counts (literal, constant, or computed return). - Derives a suggested name by stripping
Brand/IDsuffixes and leadingTprefix.
Name Derivation Rules
Section titled “Name Derivation Rules”| Input | Suggested Name |
|---|---|
UserBrand |
User |
OrderID |
Order |
TProduct |
Product |
EventBrandID |
Event |
Brand |
Brand (fallback to original when result is empty) |
BuildFlow Integration
Section titled “BuildFlow Integration”The linter/provider package self-registers the tool as a BuildFlow provider
(blank-import it), so buildflow runs detection — and --fix runs repair —
with zero orchestrator-side glue. Findings are go-finding values, so they
merge with the rest of the linter stack.
To run BD003 through BuildFlow instead of shelling out to the CLI, set the
rules tool option in .buildflow.yml (linter v0.5.0+):
tool_options: brandid-lint: rules: "BD001,BD003"The value is parsed by the same ParseRuleSet the CLI -rules flag uses, so
an invalid entry (unknown rule ID, BD002, empty token) is a hard Detect
error — a config typo cannot quietly scan with the wrong rule set. Without
the option the provider runs the default rule set (BD001+BD002), identical to
every earlier release. Repair stays scoped to BD001 Name() stub insertion
regardless of the selected rules.
Running on This Library
Section titled “Running on This Library”cd linter && go run ./cmd/brandid-lint ..The library’s own test brands carry Name() stubs, so the linter runs clean
on this repository.