Documentation
¶
Overview ¶
Package linter provides the shared scaffolding for LarsArtmann Go linters (branching-flow, erraudit, go-structure-linter, and future additions).
Each of those linters independently reinvented the same three layers:
- A Rule interface (Name/Description/Severity/Check)
- A registry of rules
- A converter from their native domain type to finding.Finding
The last layer — the converter — is the most expensive duplication: branching-flow ships 1,871 LOC of converters, erraudit 1,214 LOC, just to bridge their Violation/ErrorViolation types to finding.Finding. go-structure-linter eliminated this entirely by aliasing `type Issue = finding.Finding`.
This SDK codifies the go-structure-linter pattern: a Rule emits finding.Finding directly, so there is NO converter layer. A linter that adopts this SDK ships a rules.go file and a main.go one-liner — the registry, detection, and exit codes are shared.
Design constraint: depends only on go-finding (the ecosystem hub) so any linter — CLI, library, LSP server, golangci plugin — can adopt it.
Index ¶
- Variables
- func DetectorFromRegistry(registry *Registry, toolName string) finding.Detector
- func DetectorsFromRegistry(registry *Registry) []finding.Detector
- func ExitCodeFromReport(report *finding.Report) int
- type Category
- type Registry
- type Rule
- type RuleError
- type RuleFunc
- func (r RuleFunc) Category() Category
- func (r RuleFunc) Check(ctx context.Context, dir string) ([]finding.Finding, error)
- func (r RuleFunc) Description() string
- func (r RuleFunc) ID() string
- func (RuleFunc) IsEnabledByDefault() bool
- func (r RuleFunc) Name() string
- func (r RuleFunc) Severity() finding.Severity
- type RuleMeta
Constants ¶
This section is empty.
Variables ¶
var ErrRuleFailed = errors.New("linter: rule failed")
ErrRuleFailed is the sentinel error for a rule execution failure. Use errors.Is(err, linter.ErrRuleFailed) to check whether a Registry.Run or DetectorFromRegistry call failed because a rule returned an error.
Functions ¶
func DetectorFromRegistry ¶
DetectorFromRegistry adapts a registry to the canonical finding.Detector interface, so a linter plugs into BuildFlow's DAG with zero glue (via the tool-sdk Spec.Detect field). The working directory is read from ctx via finding.WorkingDirFromContext.
For go-finding/pipeline integration with per-rule parallelism, use DetectorsFromRegistry instead — it returns one detector per rule rather than collapsing all rules into a single opaque detector.
func DetectorsFromRegistry ¶
DetectorsFromRegistry returns one finding.Detector per registered rule, so a pipeline (go-finding/pipeline) can run them with per-detector parallelism, timeouts, and error isolation. Each detector is named after the rule's ID for pipeline metric attribution.
Unlike DetectorFromRegistry, which collapses all rules into a single opaque detector, this function preserves per-rule granularity. The working directory is read from ctx via finding.WorkingDirFromContext.
pipeline.Detector is a type alias for finding.Detector, so the returned slice plugs directly into pipeline.New(config, rootDir, detectors...).
func ExitCodeFromReport ¶
ExitCodeFromReport returns the process exit code for a lint run: 0 when there are no findings, 1 otherwise. Critical-severity findings could map to a different code, but the ecosystem convention is binary (clean / not clean).
Types ¶
type Category ¶
type Category string
Category classifies what kind of issue a rule detects, for filtering and reporting. Maps to finding.Category at the finding boundary.
const ( CategoryDesign Category = "design" // design smells (coupling, cohesion) CategoryStructure Category = "structure" // file/package layout CategoryErrorHandling Category = "error-handling" CategoryCorrectness Category = "correctness" CategoryStyle Category = "style" CategoryPerformance Category = "performance" CategorySecurity Category = "security" CategoryConfiguration Category = "configuration" )
Category values classify what kind of issue a rule detects, for filtering and reporting. Each maps to finding.Category at the finding boundary.
These are RECOMMENDED values, not a closed set. Define your own for domain-specific taxonomies:
const CategoryAPI Category = "api" // your domain category
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds a set of rules. A linter registers all its rules (typically in a rules.go via init() or a constructor) and the registry drives both standalone execution and BuildFlow integration.
func (*Registry) Register ¶
Register adds a rule. Panics if a rule with the same ID is already registered — duplicate IDs are a programming error that should surface at startup, not silently shadow at runtime. Panics if the rule's ID is empty.
type Rule ¶
type Rule interface {
ID() string
Name() string
Description() string
Category() Category
Severity() finding.Severity
IsEnabledByDefault() bool
Check(ctx context.Context, dir string) ([]finding.Finding, error)
}
Rule is a single lint check. A linter is a collection of rules. Each rule declares its identity (ID/Name/Description/Category/Severity) and a Check function that emits findings for a target directory.
Dual identity: ID vs Name ¶
Every rule has two identity fields with distinct stability contracts:
- ID() is the STABLE identifier. It never changes once published. Used for registry deduplication, suppression matching (//tool:ignore ID), filter config (--enable ID), and the finding.RuleName field. Think "G001", "C001", "no-unused-vars".
- Name() is the DISPLAY name. Human-readable, can change across versions without breaking suppression, filtering, or health-score computation. Think "missing transaction commit", "unused variable".
Rules emit finding.Finding DIRECTLY — there is no intermediate Violation or Issue type to convert. This is the key design decision that eliminates the thousands of LOC of converter code the existing linters carry. Use finding.NewBuilder(...) to construct each finding.
Building findings with Confidence and FixStrategy ¶
finding.Finding already carries Confidence (finding.ConfidenceLow/Medium/High) and FixStrategy (finding.FixStrategyNone/Suggest/Direct/AI) at the per-finding level. Set these on individual findings via finding.NewBuilder(...):
finding.NewBuilder(ruleID, toolName, msg, sev, pos).
WithConfidence(finding.ConfidenceHigh).
WithFixStrategy(finding.FixStrategyDirect).
MustBuild()
Per-finding values are strictly more expressive than rule-level defaults: a single rule can emit findings with different confidence levels and fix strategies depending on the matched pattern. RuleMeta.Sev is the default severity — individual findings can override it via the Builder.
type RuleError ¶
RuleError wraps an error from a specific rule, preserving the rule's stable ID so callers can identify which rule in a registry failed. Returned by Registry.Run, RuleFunc.Check, DetectorFromRegistry, and DetectorsFromRegistry when a rule's execution fails.
Use errors.AsType (Go 1.26+) to extract the rule ID:
ruleErr, ok := errors.AsType[*linter.RuleError](err)
if ok {
log.Printf("rule %s failed", ruleErr.RuleID)
}
To check whether any rule failed (regardless of which), match the sentinel:
if errors.Is(err, linter.ErrRuleFailed) {
// a rule returned an error
}
func NewRuleError ¶
NewRuleError wraps cause as a RuleError for the rule identified by ruleID.
type RuleFunc ¶
type RuleFunc struct {
Meta RuleMeta
Run func(ctx context.Context, dir string) ([]finding.Finding, error)
}
RuleFunc adapts a function to the Rule interface, filling in the identity fields from the supplied RuleMeta. This is the common case: most rules are a metadata header plus a check closure.
func (RuleFunc) Check ¶
Check implements Rule. If Run returns an error, it is wrapped into a *RuleError carrying the rule's stable ID, so callers of Registry.Run and DetectorFromRegistry can identify which rule failed via errors.As.
func (RuleFunc) IsEnabledByDefault ¶
IsEnabledByDefault returns true for RuleFunc. Rules created via RuleFunc are enabled by default — they run unless a consumer explicitly disables them. For opt-in rules, use OptIn().
type RuleMeta ¶
RuleMeta is the declarative identity of a rule: ID, name, description, category, severity. Supplied as a struct literal so rules read like data.
ID is REQUIRED — it is the stable identifier that never changes once published. It is used for registry deduplication, suppression matching, filter config, and the finding.RuleName field.