contracts

package
v0.62.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package contracts is GoFastr's semantic analysis layer: the rules that say whether a codebase is a *good GoFastr application*, not merely a compiling one.

The compiler verifies correctness. `go vet` verifies suspicious constructs. Neither can answer "does this route have auth", "is this screen covered by a test", "did someone hand-roll CSS the design system already provides", or "is the dependency direction still intact". Those are framework semantics, and this package is where they live.

The three moving parts

  • A Rule is the *documentation*: a stable ID (GOFASTR1002), a capability, a default severity, and — mandatory — the Why, the Fix, and a bad/good example pair. Rules are data. They are readable without running anything, which is what makes the catalog useful to an agent over MCP.
  • An Analyzer is the *detector*: it declares which rules it can emit and walks a Pass to produce [Diagnostic]s. Analyzers never invent messages for rules they did not declare — Run rejects that, so the catalog can never drift from what actually fires.
  • A Config is the *relaxation*: strict is the zero value. Every rule in the catalog is enforced at its declared severity unless configuration explicitly turns it down. There is no opt-in; there is only visible, reviewable opt-out.

Strictness is the default; config is the only way to change it

This inverts the usual linter posture, deliberately. A rule that must be switched on is a rule nobody switches on. The cost of the inversion is that adding a rule to the catalog can break existing builds — which is the point, and why every rule ships with a `Fix` and a suppression path.

Precisely: nothing is enforced *less* than the catalog declares unless someone writes it down. Configuration can move a rule either way — a team wanting `routing/untested-route` to be an error rather than a warning is making a real choice and should be able to — but the default is never quieter than declared, and every change is listed in the report footer whichever direction it goes.

The two escape hatches are:

# gofastr.contracts.yml — visible in review, applies to a whole tree
contracts:
  performance:
    severity: warn
  rules:
    GOFASTR1003: off

//gofastr:allow(GOFASTR1003) covered by the e2e suite in examples/site

Both require a human to write a reason down. A suppression that stops matching anything is itself reported (GOFASTR0002), so the escape hatches cannot silently accumulate.

Output is for agents first

Report renders as text for humans, JSON for agents, and SARIF for IDEs and code scanning. The JSON form carries the whole rule — Why, Fix, examples, doc URL — beside each diagnostic, so a coding agent that receives one has everything it needs to make the change without a second round-trip.

Index

Constants

View Source
const (
	RuleSuppressionNoReason    = "GOFASTR0001"
	RuleSuppressionStale       = "GOFASTR0002"
	RuleSuppressionUnknownRule = "GOFASTR0003"
	RuleSuppressionMalformed   = "GOFASTR0004"
)

Meta rules — the contract system reporting on itself.

View Source
const (
	RuleDuplicateRoute   = "GOFASTR1001"
	RuleColonPathParam   = "GOFASTR1002"
	RuleUntestedRoute    = "GOFASTR1003"
	RuleStateAsRoute     = "GOFASTR1004"
	RuleNonUppercaseVerb = "GOFASTR1005"
)

Routing rules.

View Source
const (
	RuleRouteNotExercised      = "GOFASTR1101"
	RulePermissionNotExercised = "GOFASTR1102"
	RuleEntityNotExercised     = "GOFASTR1103"
	RuleCoverageBelowMinimum   = "GOFASTR1104"
	RuleDisabledTest           = "GOFASTR1105"
	RuleNoCoverageManifest     = "GOFASTR1106"
	RuleHookNotFired           = "GOFASTR1107"
	RuleEventNotEmitted        = "GOFASTR1108"
	RuleRoleNotExercised       = "GOFASTR1109"
	RuleCoverageManifestBroken = "GOFASTR1110"
)

Testing rules.

View Source
const (
	RuleMissingAlt            = "GOFASTR1201"
	RuleMissingAccessibleName = "GOFASTR1202"
	RuleUnnamedLandmark       = "GOFASTR1203"
	RuleIncompleteFormControl = "GOFASTR1204"
	RuleImplicitHeadingLevel  = "GOFASTR1205"
	RuleMissingElementMeta    = "GOFASTR1206"
)

Accessibility rules.

View Source
const (
	RuleLayerViolation  = "GOFASTR1301"
	RuleForbiddenImport = "GOFASTR1302"
)

Architecture rules.

View Source
const (
	RuleSQLStringConcat = "GOFASTR1401"
	RuleFormWithoutCSRF = "GOFASTR1402"
	RuleHTMLConcat      = "GOFASTR1403"
	RuleInsecureCookie  = "GOFASTR1404"
	RuleHardcodedSecret = "GOFASTR1405"
)

Security rules.

View Source
const (
	RuleRegexpCompilePerCall = "GOFASTR1501"
	RuleQueryInLoop          = "GOFASTR1502"
	RuleReflectionPerRequest = "GOFASTR1503"
)

Performance rules.

View Source
const (
	RuleMCPWithoutCRUD = "GOFASTR1701"
	RulePublicEntity   = "GOFASTR1702"
)

Entity rules.

View Source
const (
	RuleBespokeCSS         = "GOFASTR1801"
	RuleHardNavigation     = "GOFASTR1802"
	RuleBespokeEventSource = "GOFASTR1803"
	RuleInlineStyle        = "GOFASTR1804"
	RuleInlineScript       = "GOFASTR1805"
)

Rendering rules.

View Source
const (
	RuleUnscopedPII       = "GOFASTR1901"
	RuleUnguardedMutation = "GOFASTR1902"
	RuleAuthNotWired      = "GOFASTR1903"
)

Permission rules.

View Source
const (
	RuleHandrolledCRUD    = "GOFASTR2001"
	RuleHandrolledBattery = "GOFASTR2002"
	RuleRawSQLOverRepo    = "GOFASTR2003"
)

AI-guidance rules.

View Source
const BaselineFileName = ".gofastr-contracts-baseline.json"

BaselineFileName is the conventional location for a recorded baseline. It lives in the repository, not under `.gofastr/`: unlike the coverage manifest this is a reviewed decision about what debt is accepted, and it belongs in the diff where a reader can see it shrink.

View Source
const BaselineSchemaVersion = 1

BaselineSchemaVersion guards the on-disk shape.

View Source
const JSONSchemaVersion = 1

JSONSchemaVersion is bumped when the machine-readable shape changes in a way a consumer must notice. It is the first field in the document so a reader can branch on it before parsing anything else.

View Source
const RuleIgnoredExec = "GOFASTR1601"

Data rules.

Variables

View Source
var ConfigFileNames = []string{
	"gofastr.contracts.yml",
	"gofastr.contracts.yaml",
	"gofastr.yml",
	"gofastr.yaml",
}

ConfigFileNames are the files LoadConfig looks for, in order. The first two are dedicated; the third is the blueprint, which may carry a top-level `contracts:` block so a small project needs one file rather than two.

Functions

func ChangedFiles

func ChangedFiles(root, ref string) (map[string]bool, error)

ChangedFiles returns the repository-relative paths that differ from ref, as a set suitable for Report.RestrictTo.

Why this exists: verify is all-or-nothing otherwise. A pre-commit hook, a dev-loop rebuild, and a PR review all want the same narrower question — "what did *this change* break" — and answering it by reading a 200-line whole-repo report is answering a different question.

The analysis itself still runs over the whole tree, and must: the route table, the entity list, and the coverage manifest are only meaningful whole. Only the *reporting* narrows. A duplicate route introduced by editing one file is still found, because the other half of the pair was analysed too.

ref may be a branch, a commit, or "" for the working tree against HEAD. A repository-less directory returns (nil, nil) — not an error, because "not in git" is a legitimate state and the caller should simply not narrow.

func FormatCatalogJSON

func FormatCatalogJSON(rules []Rule) ([]byte, error)

FormatCatalogJSON renders the rule catalog — what `contracts_list` returns over MCP and what `gofastr verify --list --json` prints. An agent reads this once and knows every contract the framework enforces.

func FormatExplain

func FormatExplain(r Rule, color bool) string

FormatExplain renders one catalog entry in full — the `--explain` view and the body of the `contracts_explain` MCP tool.

func FormatJSON

func FormatJSON(r *Report) ([]byte, error)

FormatJSON renders the report as indented JSON. Every diagnostic carries its full rule — Why, Fix, examples, doc URL — so an agent consuming one finding has everything it needs to make the change without a second call. That redundancy costs bytes and saves round trips, which is the right trade for the consumer this exists for.

func FormatSARIF

func FormatSARIF(r *Report, version string) ([]byte, error)

FormatSARIF renders SARIF 2.1.0 — the format GitHub code scanning and every major IDE already consume, which is how `gofastr verify` gets inline squiggles and PR annotations without anyone writing an extension.

func FormatText

func FormatText(r *Report, opts TextOptions) string

FormatText renders a report for a terminal. The shape is deliberate: findings grouped by rule rather than by file, because the unit of *action* is the rule — you learn it once and then fix every instance — while a file-grouped list makes the reader re-derive the lesson at each stop.

func IsGeneratedSource

func IsGeneratedSource(body []byte) bool

IsGeneratedSource reports whether body carries the conventional generated-code header (https://pkg.go.dev/cmd/go#hdr-Generate_Go_files). Only the first 512 bytes are inspected, so a doc comment mentioning the phrase further down does not exempt a hand-written file.

func MatchPath

func MatchPath(pattern, path string) bool

MatchPath is [matchGlob] exported for analyzers that need the same dialect on something other than a file path — import paths, most obviously, which are slash-separated in exactly the same way.

func Register

func Register(as ...*Analyzer)

Register adds analyzers to the process-wide set. Panics on a duplicate name or a rule the catalog does not know — both are wiring errors that belong at init, not in a user's terminal.

func RegisterRules

func RegisterRules(rules ...Rule)

RegisterRules adds rules to the process-wide catalog, validating each one. It panics on a malformed or duplicate rule: the catalog is compiled-in data, so a violation is a programming error that should never reach a user's terminal, and failing at init is how it stays that way.

func SortedFiles

func SortedFiles(files map[string]bool) []string

SortedFiles renders a file set deterministically, for reporting.

func SuggestRules

func SuggestRules(idOrSlug string) []string

SuggestRules returns catalog entries whose ID or slug is close to the given string — the "did you mean" behind an unknown-rule config error.

func WriteBaseline

func WriteBaseline(path string, b *Baseline) error

WriteBaseline saves a baseline as indented JSON, sorted so a regenerated file diffs cleanly against the previous one.

Types

type Analyzer

type Analyzer struct {
	// Name is the stable identifier used by `--analyzer` and in timings.
	Name string
	// Doc is one line describing what the analyzer looks at.
	Doc string
	// Rules are the IDs this analyzer may emit. [Run] rejects a
	// diagnostic naming any other rule — an analyzer cannot smuggle in an
	// undocumented finding, which is what keeps the catalog honest.
	Rules []string
	// Run inspects the pass. Returning an error aborts only this
	// analyzer; the rest of the run continues and the error is reported.
	Run func(*Pass) ([]Diagnostic, error)
}

Analyzer is one detector. It is a struct rather than an interface for the same reason golang.org/x/tools/go/analysis uses one: the interesting part is the data (which rules can this emit), and a struct keeps that declaration next to the function instead of scattered across methods.

func Analyzers

func Analyzers() []*Analyzer

Analyzers returns every registered analyzer, name-sorted.

func (*Analyzer) Capabilities

func (a *Analyzer) Capabilities() []Capability

Capabilities are the distinct capabilities this analyzer's rules cover.

type AnalyzerTiming

type AnalyzerTiming struct {
	Name        string        `json:"name"`
	Duration    time.Duration `json:"-"`
	Millis      float64       `json:"ms"`
	Diagnostics int           `json:"diagnostics"`
	Error       string        `json:"error,omitempty"`
}

AnalyzerTiming records how long one analyzer took, for `--json` output and for finding the analyzer that made the pipeline slow.

type ArchitectureConfig

type ArchitectureConfig struct {
	Layers []LayerRule
	Forbid []ForbidRule
}

ArchitectureConfig describes the dependency direction the architecture analyzer enforces. Layers are ordered: a package in layer N may import its own layer and anything below it, never above. That single rule covers "core must not import framework" and "domain must not import UI" without listing every pair.

func (ArchitectureConfig) Configured

func (a ArchitectureConfig) Configured() bool

Configured reports whether the project described a dependency shape. No layers means the analyzer has nothing to enforce and stays quiet — inventing a layering for someone else's package tree would be noise.

type Baseline

type Baseline struct {
	Schema int `json:"schema"`
	// Generated is when the baseline was recorded, for the report's "this
	// is N months old" nudge.
	Generated string `json:"generated"`
	// Note is free text explaining why this debt is accepted.
	Note string `json:"note,omitempty"`
	// Counts maps rule ID → file → number of accepted findings.
	Counts map[string]map[string]int `json:"counts"`
}

Baseline is the debt an existing codebase has agreed to carry.

It exists because strict-by-default and adoption pull against each other. A mature app switching `gofastr verify` on gets hundreds of findings at once; nobody fixes hundreds of findings at once, so the realistic outcomes are "turn the tool off" or "downgrade every rule to warn", and both end with nothing being enforced. A baseline gives the third option: accept what is there, fail on what is added.

Counts are keyed by (rule, file) rather than by line, because line numbers churn on every edit and a baseline that goes stale on a reformat is a baseline people delete. Moving a finding within a file keeps it accepted; adding one more of the same rule to that file does not.

func NewBaseline

func NewBaseline(r *Report, now time.Time, note string) *Baseline

NewBaseline records the report's *gating* diagnostics as accepted.

Findings below the run's fail-on severity are deliberately left out. A baseline exists to unblock a gate; recording something that cannot fail the run is not just noise in the file, it is actively harmful — the entry absorbs the finding on every later run, so an informational signal the project wanted to keep watching disappears instead.

This matters for the semantic-coverage rules in particular. They are environment-dependent (they record which tests ran), so a project will often downgrade them to info rather than let them gate; without this filter, `--baseline-write` would then silence the very findings the downgrade was meant to keep visible.

func ReadBaseline

func ReadBaseline(path string) (*Baseline, error)

ReadBaseline loads a baseline. A missing file returns (nil, nil) — no baseline is the normal state, not an error.

func (*Baseline) Total

func (b *Baseline) Total() int

Total is the number of accepted findings across every rule and file.

type BaselineDelta

type BaselineDelta struct {
	RuleID   string
	File     string
	Baseline int
	Current  int
}

BaselineDelta is one (rule, file) whose accepted count is now too high.

type BaselineResult

type BaselineResult struct {
	// Accepted is how many findings the baseline absorbed.
	Accepted int
	// Fixed lists (rule, file) pairs where fewer findings occur now than
	// the baseline records — debt that was paid down. Reported so the
	// baseline visibly shrinks instead of quietly over-accepting forever.
	Fixed []BaselineDelta
}

BaselineResult is what applying a baseline did to a report.

type Capability

type Capability string

Capability is the area of the framework a rule speaks about. It is the unit users filter by (`gofastr verify routing`) and the unit config relaxes by, so the set is deliberately small and stable — a new capability is an API change, a new rule inside one is not.

const (
	// CapMeta covers the contract system talking about itself:
	// unparsable config, suppressions that no longer match anything.
	CapMeta Capability = "meta"
	// CapRouting covers the route table — duplicates, auth, reachability,
	// and registrations that bypass the framework's own helpers.
	CapRouting Capability = "routing"
	// CapTesting covers semantic coverage: which routes, permissions, and
	// entity operations a test run actually exercised.
	CapTesting Capability = "testing"
	// CapAccessibility covers the static WCAG floor the type system can
	// see. The runtime half lives in `gofastr audit a11y --url`.
	CapAccessibility Capability = "accessibility"
	// CapArchitecture covers dependency direction and package layering.
	CapArchitecture Capability = "architecture"
	// CapSecurity covers CSRF, injection, cookie flags, and unscoped data.
	CapSecurity Capability = "security"
	// CapPerformance covers work done per-request that belongs at init.
	CapPerformance Capability = "performance"
	// CapData covers the persistence layer — ignored writes, raw SQL.
	CapData Capability = "data"
	// CapEntities covers entity declarations and their exposure surface.
	CapEntities Capability = "entities"
	// CapPermissions covers who can reach what: owner scoping, RBAC.
	CapPermissions Capability = "permissions"
	// CapRendering covers the UI contract — one styling surface, no hard
	// navigation, no bespoke event streams.
	CapRendering Capability = "rendering"
	// CapAI covers idiomatic-usage guidance: the hand-rolled shape an
	// agent reaches for when a framework primitive already exists.
	CapAI Capability = "ai"
)

func Capabilities

func Capabilities() []Capability

Capabilities returns every capability in report order.

func ParseCapability

func ParseCapability(s string) (Capability, error)

ParseCapability resolves a user-typed capability name. It accepts the canonical name plus the aliases people actually type — `a11y` for accessibility, `sec` for security, `perf` for performance — because a CLI that rejects `gofastr verify a11y` after shipping `gofastr audit a11y` for a year is just being rude.

func (Capability) Order

func (c Capability) Order() int

Order is the capability's position in report order. Unknown capabilities sort last so a future rule never silently jumps the queue.

func (Capability) String

func (c Capability) String() string

func (Capability) Title

func (c Capability) Title() string

Title is the capability rendered for a section header.

func (Capability) Valid

func (c Capability) Valid() bool

Valid reports whether c is a capability the catalog knows.

type CapabilitySummary

type CapabilitySummary struct {
	Capability Capability `json:"capability"`
	Errors     int        `json:"errors"`
	Warnings   int        `json:"warnings"`
	Infos      int        `json:"infos"`
}

CapabilitySummary is the per-capability tally shown in the report footer — the "which area of this app is drifting" view.

func (CapabilitySummary) Total

func (c CapabilitySummary) Total() int

Total is every diagnostic in this capability.

type Config

type Config struct {
	// Path is the file this came from, "" when defaults.
	Path string
	// Exempt are path globs no analyzer looks at.
	Exempt []string
	// FailOn is the severity floor that makes a run fail. Default
	// SeverityError; `strict: true` lowers it to SeverityWarn.
	FailOn Severity

	Coverage     CoverageConfig
	Architecture ArchitectureConfig
	// contains filtered or unexported fields
}

Config is the resolved configuration for a verify run. The zero value (via DefaultConfig) enforces every rule in the catalog at its declared severity — nothing is quieter than declared unless someone writes it down. Most fields exist to turn something down; severity may also be raised, which is a real choice a team is entitled to make.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig is strict: nothing exempted, nothing downgraded, every coverage demand on.

func LoadConfig

func LoadConfig(root, explicit string) (*Config, error)

LoadConfig resolves configuration for a project root. An explicit path must exist; otherwise the first of ConfigFileNames present in root is used, and defaults apply when none is. A file that exists but is malformed is an error — falling back to defaults there would silently re-enable rules the author believed they had turned off.

func (*Config) Enabled

func (c *Config) Enabled(r Rule) bool

Enabled reports whether a rule can fire at all in this run.

func (*Config) ExemptFor

func (c *Config) ExemptFor(r Rule, rel string) bool

ExemptFor reports whether a specific rule is exempt at a path, taking global, capability, and rule-level exemptions together.

func (*Config) ExemptPath

func (c *Config) ExemptPath(rel string) bool

ExemptPath reports whether a path is globally exempt.

func (*Config) Relaxations

func (c *Config) Relaxations() []string

Relaxations lists every severity override and path exemption this config applies, so a report states plainly what was changed rather than quietly honouring it. The name reflects the common case; an escalation is listed too, because the footer's job is "what did config change", not "what did it weaken".

func (*Config) SeverityFor

func (c *Config) SeverityFor(r Rule) Severity

SeverityFor is the effective severity of a rule after configuration. Rule overrides beat capability overrides, which beat the rule's declared default — most specific wins.

Configuration may move a severity in either direction. "Strict by default" is a statement about the *default*, not a ceiling: a team that wants a warning to be an error is making a real, reviewable choice.

type CoverageConfig

type CoverageConfig struct {
	// Minimum is the line-coverage percentage floor. Zero disables the
	// check; the generated config comments this in at 90.
	Minimum float64
	// MinimumSet distinguishes "not configured" from "configured to 0".
	MinimumSet bool
	// Routes demands every discovered route be exercised by a test.
	Routes bool
	// Permissions demands every declared permission be exercised.
	Permissions bool
	// Entities demands every entity's CRUD surface be exercised.
	Entities bool
	// Profile is the path to the `go test -coverprofile` output, relative
	// to the project root. Empty means the default location.
	Profile string
}

CoverageConfig governs the testing capability's thresholds. Zero values are the strict setting throughout: every demand on, and a line-coverage floor that is only enforced once a number is set (there is no honest default percentage — a floor nobody chose is a floor nobody meets).

type Diagnostic

type Diagnostic struct {
	// RuleID identifies the catalog entry. Analyzers set this; Slug,
	// Capability, and Severity are filled in from the catalog during
	// [Run], so an analyzer cannot claim a severity its rule did not
	// declare.
	RuleID string `json:"rule"`
	// Slug mirrors Rule.Slug for readability.
	Slug string `json:"slug,omitempty"`
	// Capability mirrors Rule.Capability.
	Capability Capability `json:"capability,omitempty"`
	// Severity is the *effective* severity after config relaxation.
	Severity Severity `json:"severity,omitempty"`

	// File is relative to the pass root, slash-separated.
	File string `json:"file"`
	// Line is 1-indexed. Zero means the finding is about the project as a
	// whole rather than a location (a missing manifest, say).
	Line int `json:"line,omitempty"`
	// Column is 1-indexed, zero when unknown.
	Column int `json:"column,omitempty"`
	// EndLine bounds a multi-line finding. Zero means single-line.
	EndLine int `json:"endLine,omitempty"`

	// Message is the instance-specific statement — it names the actual
	// route, field, or import, where Rule.Summary states the class.
	Message string `json:"message"`
	// Suggestion is the instance-specific remedy, naming the concrete
	// file to create or call to add. Falls back to Rule.Fix when empty.
	Suggestion string `json:"suggestion,omitempty"`
	// Snippet is the offending source line, trimmed.
	Snippet string `json:"snippet,omitempty"`
	// RedactSnippet stops [Run] from filling Snippet in from the source.
	// Set by rules whose whole subject is a value that must not be echoed
	// — a report that prints the committed credential back into a
	// terminal, a CI log, and a SARIF artifact has made things worse.
	RedactSnippet bool `json:"-"`
	// Evidence carries analyzer-specific structured detail — the route
	// pattern, the two conflicting registrations, the import edge. Agents
	// read this; the text reporter ignores it.
	Evidence map[string]string `json:"evidence,omitempty"`
	// Fix is the mechanical edit, when the analyzer can produce one.
	Fix *SuggestedFix `json:"fix,omitempty"`

	// Rule is the catalog entry, attached during [Run]. Present in JSON
	// output so a consumer needs no second lookup.
	Rule *Rule `json:"ruleDoc,omitempty"`
}

Diagnostic is one violation at one place. It carries enough context to be actioned standalone: the rule's Why and Fix are attached at report time (see [Report.rules]) so a single JSON object is a complete work item.

func (Diagnostic) Location

func (d Diagnostic) Location() string

Location renders "file:line:col", omitting the parts that are unknown.

type Example

type Example struct {
	// Caption is the one-line framing ("a POST route with no access
	// declaration"). Optional.
	Caption string `json:"caption,omitempty"`
	// Bad is the code that trips the rule.
	Bad string `json:"bad"`
	// Good is the code that satisfies it.
	Good string `json:"good"`
}

Example is a bad/good pair shown under a rule. Both halves are required: "don't do this" without "do this instead" is the failure mode that makes linters feel adversarial, and it is exactly the half an agent needs in order to produce a fix on the first attempt.

type FixedDiagnostic

type FixedDiagnostic struct {
	Rule string `json:"rule"`
	File string `json:"file"`
	Line int    `json:"line"`
}

FixedDiagnostic is one applied autofix, in the wire format.

type ForbidRule

type ForbidRule struct {
	From   string
	To     string
	Reason string
}

ForbidRule is one banned import edge. `From` and `To` are import-path globs matched the same way as LayerRule.Packages.

type LayerRule

type LayerRule struct {
	// Name identifies the layer in diagnostics ("core", "domain", "ui").
	Name string
	// Packages are import-path globs, matched against the *suffix* of an
	// import path so `core/**` matches
	// `github.com/you/app/core/render` without repeating the module path.
	Packages []string
}

LayerRule is one named tier of the dependency graph.

type Pass

type Pass struct {
	// Root is the absolute directory being analysed.
	Root string
	// ModulePath is the go.mod module path, empty when there is none.
	ModulePath string
	// Config is the resolved configuration for this run.
	Config *Config
	// contains filtered or unexported fields
}

Pass is the shared context every analyzer runs against. It owns file discovery, source reading, and AST parsing so a twelve-analyzer run parses each file once rather than twelve times, and it carries the resolved Config so analyzers can honour path exemptions without each re-implementing the matching.

func NewPass

func NewPass(root string, cfg *Config) (*Pass, error)

NewPass discovers every source file under root and returns a pass ready for analyzers. Discovery is eager (one walk) but reading and parsing are lazy, so an analyzer that only cares about .yml files pays nothing for the Go tree.

func (*Pass) AST

func (p *Pass) AST(rel string) (*ast.File, bool)

AST parses a discovered file once and caches the result. Comments are retained because suppression directives live in them. A file that fails to parse returns (nil, false) rather than an error: a project mid-edit should still get findings from every file that *does* parse.

func (*Pass) AppFiles

func (p *Pass) AppFiles() []SourceFile

AppFiles returns the files analyzers care about by default: non-test, non-generated Go source that configuration has not exempted.

func (*Pass) FileSet

func (p *Pass) FileSet() *token.FileSet

FileSet is the shared position table. Every AST the pass hands out is positioned against it.

func (*Pass) Files

func (p *Pass) Files() []SourceFile

Files returns every discovered Go file, in path order.

func (*Pass) Line

func (p *Pass) Line(rel string, n int) string

Line returns the trimmed 1-indexed source line, or "" when out of range.

func (*Pass) Lines

func (p *Pass) Lines(rel string) []string

Lines splits a discovered file into lines, for snippet extraction.

func (*Pass) Memo

func (p *Pass) Memo(key string, compute func() any) any

Memo computes a value once per pass and shares it across analyzers. This is how the routing analyzer's discovered route table reaches the testing and permissions analyzers without an ordering constraint between them: whoever asks first pays, everyone else reads the cache.

func (*Pass) Position

func (p *Pass) Position(pos token.Pos) token.Position

Position resolves an AST position into the pass's line/column space.

func (*Pass) Rel

func (p *Pass) Rel(abs string) string

Rel converts an absolute path to the pass-relative, slash-separated form diagnostics use.

func (*Pass) Source

func (p *Pass) Source(rel string) ([]byte, bool)

Source returns the bytes of a discovered file. The returned slice is the pass's own buffer — analyzers must not mutate it.

func (*Pass) TestFiles

func (p *Pass) TestFiles() []SourceFile

TestFiles returns non-generated _test.go files that configuration has not exempted.

func (*Pass) Unparsed

func (p *Pass) Unparsed() map[string]string

Unparsed returns the files whose source could not be parsed, keyed by path, with the parser's message. A tree mid-edit is the normal case for the dev loop, so this is reported rather than fatal — but it is reported.

type Report

type Report struct {
	// Vet records the `go vet` stage, when the caller ran one.
	Vet *VetResult `json:"vet,omitempty"`
	// Root is the absolute directory analysed.
	Root string `json:"root"`
	// ConfigPath is the config file used, "" when defaults applied.
	ConfigPath string `json:"config,omitempty"`
	// Capabilities is the requested filter, empty when everything ran.
	Capabilities []Capability `json:"capabilities,omitempty"`
	// Diagnostics is every surviving finding, worst first.
	Diagnostics []Diagnostic `json:"diagnostics"`
	// Suppressed counts findings silenced by a `//gofastr:allow`
	// directive. Reported so a clean run still admits what it skipped.
	Suppressed int `json:"suppressed"`

	// Baselined counts findings absorbed by a recorded baseline —
	// pre-existing debt the project agreed to carry.
	Baselined int `json:"baselined,omitempty"`
	// BaselineFixed counts (rule, file) buckets whose baseline allowance
	// is now larger than the findings that remain. Debt that was paid
	// down and should be re-recorded so the baseline keeps shrinking.
	BaselineFixed int `json:"baselineFixed,omitempty"`
	// Notices are human-facing remarks the CLI would otherwise print to
	// stdout — "this rule has no autofix", "not a git repository". In text
	// mode they are printed; in JSON they belong here, because anything
	// printed alongside the document corrupts it, and dropping them
	// silently would lose the run's own account of what it did.
	Notices []string `json:"notices,omitempty"`
	// Fixed lists the diagnostics --fix resolved, so a JSON consumer
	// driving verify → fix → verify can see what changed.
	Fixed []FixedDiagnostic `json:"fixed,omitempty"`
	// Unparsed counts files the parser rejected. Those files produced no
	// findings from any analyzer, so without this a mid-edit tree reads as
	// clean for exactly the files nobody could read.
	Unparsed int `json:"unparsed,omitempty"`
	// OutsideChange counts findings dropped by --changed because they sit
	// in files this change did not touch. Reported so a narrowed run
	// never reads as a whole-repository all-clear.
	OutsideChange int `json:"outsideChange,omitempty"`
	// Errors are analyzer failures — a broken analyzer, not a broken app.
	Errors []string `json:"analyzerErrors,omitempty"`
	// Timings is per-analyzer wall time, slowest first.
	Timings []AnalyzerTiming `json:"timings,omitempty"`
	// Relaxations lists every configured downgrade, so `verify` cannot
	// pass quietly on a config that turned the interesting half off.
	Relaxations []string `json:"relaxations,omitempty"`
	// FailOn is the severity floor that decides the exit code.
	FailOn Severity `json:"failOn"`
	// Duration is the whole run's wall time.
	Duration time.Duration `json:"-"`

	// Summary is the per-capability tally, in capability order.
	Summary []CapabilitySummary `json:"summary"`
	// Counts are the run-wide totals.
	Counts struct {
		Errors   int `json:"errors"`
		Warnings int `json:"warnings"`
		Infos    int `json:"infos"`
	} `json:"counts"`
	// contains filtered or unexported fields
}

Report is the outcome of a verify run.

func Run

func Run(p *Pass, opts RunOptions) (*Report, error)

Run executes the selected analyzers against the pass and assembles a Report: diagnostics normalized against the catalog, relaxed by config, filtered by suppression, deduplicated, and sorted.

func (*Report) Apply

func (r *Report) Apply() ([]Diagnostic, error)

Apply writes every suggested fix in the report to disk and returns the diagnostics it resolved.

Edits are byte offsets captured when the file was read, so Apply re-reads each file and refuses to write when an edit no longer fits — out-of-range offsets always, and for edits that record their expected text (TextEdit.Old), any file whose content at those offsets changed since analysis. A stale offset silently applied is a corrupted source file, which is a far worse outcome than "run verify again".

func (*Report) ApplyBaseline

func (r *Report) ApplyBaseline(b *Baseline) BaselineResult

ApplyBaseline removes baselined findings from the report and returns what it absorbed.

Diagnostics are dropped worst-first within each (rule, file) bucket, so when a file has three accepted findings and gains a fourth, the one left visible is the most severe — not whichever happened to sort last.

func (*Report) ExitCode

func (r *Report) ExitCode() int

ExitCode is the process status for a CLI wrapping this report: 0 clean, 1 findings at or above the fail-on floor.

func (*Report) Fixable

func (r *Report) Fixable() []Diagnostic

Fixable returns the diagnostics carrying a mechanical fix.

func (*Report) Only

func (r *Report) Only(rules ...string) *Report

Only returns a shallow copy of the report holding just the diagnostics for the given rules, accepted as IDs or slugs. It exists so a caller can fix one rule at a time — Apply writes every fix in the report, which is the wrong granularity when an agent has decided to accept one rule's edits and review another's by hand.

The counters (Suppressed, Baselined, …) are deliberately NOT carried over: they describe the whole run, and copying them onto a narrowed report would state that this rule alone silenced that many findings.

func (*Report) OnlyFiles

func (r *Report) OnlyFiles(files map[string]bool) *Report

OnlyFiles returns a shallow copy of the report holding just the diagnostics in the given file set. Unlike Report.RestrictTo it does not mutate the receiver, which is what makes it usable for narrowing a fix without also narrowing the report that gets printed.

A nil set returns a copy holding everything: "no restriction" and "restrict to nothing" are different requests, and conflating them would make an unfiltered run silently fix nothing.

func (*Report) Passed

func (r *Report) Passed() bool

func (*Report) RestrictTo

func (r *Report) RestrictTo(files map[string]bool) int

RestrictTo drops every diagnostic outside the given file set and returns how many it removed.

Diagnostics with no file — the ones about the project as a whole, like a missing coverage manifest — are dropped too. They are not about the change under review, and surfacing them on every narrowed run is the noise that makes people stop reading.

type Rule

type Rule struct {
	// ID is the stable identifier — "GOFASTR1002". Assigned from the
	// capability's number block (see catalog.go) and never reused, so a
	// suppression written today keeps meaning the same thing.
	ID string `json:"id"`
	// Slug is the human-readable name — "routing/missing-auth". Accepted
	// anywhere an ID is, because `//gofastr:allow(routing/missing-auth)`
	// reads better in a diff than a number.
	Slug string `json:"slug"`
	// Title is the short noun phrase shown as the finding's headline.
	Title string `json:"title"`
	// Capability is the area this rule belongs to.
	Capability Capability `json:"capability"`
	// Severity is the default severity. Config may lower it; nothing
	// raises it.
	Severity Severity `json:"severity"`
	// Summary is one sentence stating what was detected.
	Summary string `json:"summary"`
	// Why explains the consequence — what breaks, for whom, when. This is
	// the field that turns a lint error into a lesson.
	Why string `json:"why"`
	// Fix is the concrete remedy, in imperative voice, naming the exact
	// API or file to reach for.
	Fix string `json:"fix"`
	// Examples are bad/good pairs. Optional but strongly encouraged.
	Examples []Example `json:"examples,omitempty"`
	// Doc is the `gofastr docs` topic that covers this rule in depth
	// (e.g. "reactivity"). Rendered as a URL by DocURL.
	Doc string `json:"doc"`
	// Autofix reports whether an analyzer can produce a mechanical edit
	// for this rule. Advisory: a rule may be marked autofixable and still
	// decline to fix a particular instance it cannot rewrite safely.
	Autofix bool `json:"autofix"`
}

Rule is the documentation half of a contract: everything a human or an agent needs to understand a diagnostic without opening the source of the analyzer that produced it.

Every field except Autofix and Examples is mandatory, and RegisterRules enforces that. A rule with an empty Fix is a rule that will be suppressed rather than fixed.

func AllRules

func AllRules() []Rule

AllRules returns the whole catalog sorted by capability order then ID — the order `gofastr verify --list` prints and the MCP catalog returns.

func LookupRule

func LookupRule(idOrSlug string) (Rule, bool)

LookupRule resolves a rule by ID or slug, case-insensitively on the ID.

func RulesFor

func RulesFor(c Capability) []Rule

RulesFor returns every catalog rule in the given capability.

func (Rule) DocCommand

func (r Rule) DocCommand() string

DocCommand is the offline equivalent of DocURL — the docs are embedded in the binary, so an agent with no network still has the full text.

func (Rule) DocURL

func (r Rule) DocURL() string

DocURL is the published location of the rule's doc topic.

type RunOptions

type RunOptions struct {
	// Capabilities restricts the run to analyzers covering at least one
	// of these. Empty runs them all.
	Capabilities []Capability
	// Analyzers restricts the run by analyzer name. Empty runs them all.
	Analyzers []string
	// Parallel caps concurrent analyzers. Zero uses GOMAXPROCS.
	Parallel int
}

RunOptions narrows a run. Both filters are additive-empty: an empty slice means "everything", which is the strict default.

type Severity

type Severity int

Severity is how loudly a diagnostic lands. A config may move a rule either way along this list; what the ordering buys is that every move *down* from the catalog default is a relaxation, which the report names out loud (see Config.Relaxations) — turning a check off is a decision the whole team gets to see, not a line in a YAML file.

const (
	// SeverityOff suppresses the rule entirely. Only reachable through
	// configuration — no rule declares it.
	SeverityOff Severity = iota
	// SeverityInfo reports without affecting the exit code.
	SeverityInfo
	// SeverityWarn reports and affects the exit code only under
	// `--severity=warn` (or `strict: true` in the AI section).
	SeverityWarn
	// SeverityError fails the verify run.
	SeverityError
)

func ParseSeverity

func ParseSeverity(s string) (Severity, error)

ParseSeverity resolves a config or flag value. `warning` and `err` are accepted alongside the canonical spellings; anything else is an error rather than a silent default, because a typo'd severity that quietly means "error" would make a relaxation look applied when it is not.

func (Severity) MarshalText

func (s Severity) MarshalText() ([]byte, error)

MarshalText makes Severity round-trip through JSON as its name.

func (Severity) String

func (s Severity) String() string

func (*Severity) UnmarshalText

func (s *Severity) UnmarshalText(b []byte) error

UnmarshalText parses the name form written by MarshalText.

type SourceFile

type SourceFile struct {
	// Rel is the slash-separated path from the pass root.
	Rel string
	// Abs is the absolute path on disk.
	Abs string
	// IsTest is true for _test.go files.
	IsTest bool
	// IsGenerated is true when the file carries a `Code generated … DO NOT
	// EDIT` header. Analyzers skip these by default: the developer cannot
	// fix a finding there, only the generator can.
	IsGenerated bool
	// Package is the Go import path of the file's directory, derived from
	// the module path. Empty when the root has no go.mod.
	Package string
}

SourceFile is one file the pass discovered, with the classification every analyzer needs before deciding whether to look at it.

type SuggestedFix

type SuggestedFix struct {
	// Description says what the edit does, in imperative voice.
	Description string `json:"description"`
	// Edits are applied together or not at all.
	Edits []TextEdit `json:"edits"`
}

SuggestedFix is a mechanical edit an analyzer is willing to apply. Only attach one when the rewrite is unambiguous — "add the missing Alt: \"\" field", not "restructure this handler". Anything requiring a judgment call belongs in Rule.Fix as prose.

type TextEdit

type TextEdit struct {
	File  string `json:"file"`
	Start int    `json:"start"`
	End   int    `json:"end"`
	New   string `json:"new"`
	// Old is the text this edit expects to find at [Start, End). When
	// set, [Report.Apply] refuses the edit if the file no longer carries
	// it — a file edited since analysis can pass every bounds check and
	// still put the offsets in the middle of something else entirely.
	// Leave it empty only for pure insertions (Start == End), where there
	// is nothing to expect.
	Old string `json:"old,omitempty"`
}

TextEdit replaces the byte range [Start, End) of File with New. Offsets are byte offsets into the file as it was read during the pass, which is why Report.Apply re-reads and re-verifies before writing: an edit computed against a stale buffer must fail loudly, not corrupt a file.

type TextOptions

type TextOptions struct {
	// Color emits ANSI escapes. Callers set this from a TTY check.
	Color bool
	// Verbose prints the rule's Why and an example under every finding
	// rather than only under the first of each rule. The compact default
	// exists because twenty instances of one rule should teach the lesson
	// once, not twenty times.
	Verbose bool
	// Timings appends the per-analyzer wall times.
	Timings bool
}

TextOptions tunes the human report.

type VetResult

type VetResult struct {
	// Ran is false when the stage was skipped.
	Ran bool `json:"ran"`
	// Passed is meaningful only when Ran, and the wire format enforces
	// that: see MarshalJSON.
	Passed bool `json:"passed"`
	// Skipped explains why the stage did not run, e.g. "--no-vet".
	Skipped string `json:"skipped,omitempty"`
	// Output is vet's diagnostic text when it failed.
	Output string `json:"output,omitempty"`
}

VetResult records what the `go vet` stage did. It is set by the CLI, not by Run — vet is a pipeline stage around the analyzers rather than one of them — but it rides on the report because the report is what gets serialised, and a consumer that cannot tell whether the code even compiles is reading the analyzers' findings without their precondition.

func (*VetResult) MarshalJSON

func (v *VetResult) MarshalJSON() ([]byte, error)

MarshalJSON omits the verdict when the stage never ran. A skipped stage serialising `"passed": false` is "we did not check" reading as "it failed" — a consumer keying on vet.passed alone would fail every --no-vet run. The plain omitempty cannot express this (it would also drop a genuine ran-and-failed false), so the shaping is explicit.

Directories

Path Synopsis
Package analyzers holds every detector behind `gofastr verify`.
Package analyzers holds every detector behind `gofastr verify`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL