Skip to content

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:

Terminal window
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.

Terminal window
# Detect (default: BD001 + BD002): scan and print findings
brandid-lint ./myproject/
# Autofix: insert the suggested Name() stub for every unsuppressed finding
brandid-lint -fix ./myproject/
# SARIF output for CI dashboards
brandid-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.

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" }

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 fixed
func 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.

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 key
type 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.

  1. Walks .go files (skipping .git, vendor, node_modules, testdata) and parses the AST.
  2. Finds id.ID[Brand, Value] and ID[Brand, Value] usages (both qualified and same-package).
  3. Checks if the brand type (must be an empty struct) has a Name() string method — any body counts (literal, constant, or computed return).
  4. Derives a suggested name by stripping Brand/ID suffixes and leading T prefix.
Input Suggested Name
UserBrand User
OrderID Order
TProduct Product
EventBrandID Event
Brand Brand (fallback to original when result is empty)

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.

Terminal window
cd linter && go run ./cmd/brandid-lint ..

The library’s own test brands carry Name() stubs, so the linter runs clean on this repository.