Documentation
¶
Overview ¶
Package goago enforces one way to write Go across a codebase.
A project selects the Go constructs that it accepts. Developers and coding agents use the same rule policy, and CI enforces it.
goago only ever rejects language constructs. It never adds syntax, never rewrites code, and never changes semantics. Code that passes goago is ordinary Go that builds with the stock toolchain.
Every rule is a golang.org/x/tools/go/analysis.Analyzer. Run them through the goago command, compose them in an analysis driver, or load the golangci-lint module plugin.
See github.com/agentstation/goago/cmd/goago for the command.
Index ¶
Constants ¶
const ( ConfigName = ".goago.yml" ConfigNameAlt = ".goago.yaml" )
ConfigName is the file goago looks for when the command line names no configuration. ConfigNameAlt is an equivalent spelling.
const ReportSchemaVersion = 1
ReportSchemaVersion is the current JSON report schema version.
Variables ¶
var RuleNoBlankImportOutsideMain = register(Rule{ Name: "no-blank-import-outside-main", Summary: `import _ "pkg" only allowed in package main`, Default: false, Severity: Error, Rationale: `A blank import registers a side effect through the import graph, which makes program behaviour depend on which packages the linker includes. Confining blank imports to package main keeps that dependency explicit and puts it where a reader looks for wiring. Off by default because some driver-registration patterns legitimately need a blank import from a library package. Files with a _test.go suffix are exempt. A test that blank-imports a driver registers it for that binary only.`, Analyzer: newAnalyzer("no-blank-import-outside-main", `reject import _ "pkg" outside package main`, checkBlankImport), })
RuleNoBlankImportOutsideMain confines side-effect imports to package main.
var RuleNoDotImport = register(Rule{ Name: "no-dot-import", Summary: `import . "pkg" is forbidden`, Default: true, Severity: Error, Rationale: `A dot import makes every identifier in the file ambiguous as to origin, which defeats both the reader and grep. The standard library uses dot imports in production code in exactly one pattern. The two type checkers dot-import a package that holds nothing but error-code constants, referenced densely enough that qualifying every use would be noise. That is the narrow case where this rule is wrong. Outside of it, only tests and assembly generators excluded from the build use dot imports in the standard library.`, Analyzer: newAnalyzer("no-dot-import", `reject import . "pkg"`, checkDotImport), })
RuleNoDotImport forbids dot imports.
var RuleNoEmbeddedField = register(Rule{ Name: "no-embedded-field", Summary: "struct embedding is forbidden. name your fields", Default: false, Severity: Error, Rationale: `Embedding promotes methods and fields implicitly, so a type's method set stops being visible at its declaration. Naming the field costs one selector per use and makes the delegation explicit. Off by default because embedding is thoroughly idiomatic Go. Turning this on is a real break with house Go, not a tidy-up. Interface embedding is not reported. Composing interfaces from smaller ones is the language's intended mechanism and has no field to name.`, Analyzer: newAnalyzer("no-embedded-field", "reject embedded struct fields. Name them", checkEmbeddedField), })
RuleNoEmbeddedField forbids struct embedding.
var RuleNoFBoundedConstraints = register(Rule{ Name: "no-f-bounded-constraints", Summary: "a type parameter may not appear in its own constraint", Default: false, Severity: Error, Rationale: `Reports F-bounded polymorphism. A type parameter appears inside its own constraint, as in type Node[T Cloneable[T]] or func algo[A Adder[A]](x A) A. This is not a version revert. Generics in Go 1.18 made this construct legal, and the standard library uses it. It is the most abstraction-dense shape the type system allows. An ordinary interface plus a concrete type usually reaches the same shape, so some codebases choose to ban it. That is a house-style decision. That is why the rule is off by default. A constraint that refers to a sibling type parameter, such as type Graph[N any, E Edge[N]], is not reported. The parameter does not appear in its own constraint.`, Analyzer: newAnalyzer("no-f-bounded-constraints", "reject a type parameter that appears in its own constraint", checkFBoundedConstraints), })
RuleNoFBoundedConstraints forbids F-bounded polymorphism.
var RuleNoGenericDecls = register(Rule{ Name: "no-generic-decls", Summary: "no type parameters on any func or type declaration", Reverts: "1.18", Default: false, Severity: Error, Rationale: `Reverts generics entirely. This is the strictest rule goago offers. It is the least likely to be right for a given codebase: it forbids type parameters on any func or type declaration. Reasonable for a codebase with no container libraries of its own. Hostile otherwise. It does not stop you calling generic standard library functions such as slices.Sort. Recognising a generic call requires resolving the callee to its declaration in another package.`, Analyzer: newAnalyzer("no-generic-decls", "reject type parameters on any func or type declaration (reverts Go 1.18)", checkGenericDecls), })
RuleNoGenericDecls forbids type parameters anywhere.
var RuleNoGenericMethods = register(Rule{ Name: "no-generic-methods", Summary: "methods may not declare their own type parameters", Reverts: "1.27", Default: true, Severity: Error, Rationale: `Go 1.27 lets a method declare type parameters of its own. Rewrite any such method as a package-level function that takes the receiver as its first argument. The feature buys call-site chaining and costs a second place to look for a type's operations. A method whose receiver carries type parameters, such as func (b *Box[T]) Get() T, is not affected. The rule reports only a method that introduces new type parameters. On a toolchain older than Go 1.27 this rule is dormant. The parser rejects the construct before any analyzer runs.`, Analyzer: newAnalyzer("no-generic-methods", "reject methods that declare their own type parameters (reverts Go 1.27)", checkGenericMethods), })
RuleNoGenericMethods forbids the Go 1.27 generic-method feature.
var RuleNoGoto = register(Rule{ Name: "no-goto", Summary: "goto is forbidden", Default: true, Severity: Error, Rationale: `Labelled break and continue cover the loop cases. A goto that jumps anywhere else is control flow the reader has to simulate by hand. This is a rule for application code, not a claim that goto is vestigial. The standard library's goto statements concentrate in the type checkers, the compiler, and the syscall exec paths. That code is either performance critical or a hand-written state machine. If you write that kind of code, turn this rule off rather than working around it.`, Analyzer: newAnalyzer("no-goto", "reject goto statements", checkGoto), })
RuleNoGoto forbids goto.
var RuleNoInitFunc = register(Rule{ Name: "no-init-func", Summary: "func init() is forbidden. initialize explicitly", Default: false, Severity: Error, Rationale: `init runs before main in an order determined by the import graph, which makes an initialisation failure hard to localise and hard to test. Explicit wiring from main puts the order in one readable place. Off by default because avoiding init entirely requires a wiring convention that a linter cannot supply.`, Analyzer: newAnalyzer("no-init-func", "reject func init(). Prefer explicit initialization", checkInitFunc), })
RuleNoInitFunc forbids package initialisation functions.
var RuleNoInvalidIgnore = register(Rule{ Name: "no-invalid-ignore", Summary: "every //goago:ignore must name a known rule and give a reason", Default: true, Severity: Error, Rationale: `A suppression that names no rule silences everything on the line, and a suppression that names a misspelled rule silences nothing at all. Both fail quietly, which is how a lint configuration rots. This rule requires the full form: //goago:ignore no-goto -- hand-written state machine, see docs/parser.md The reason is not decoration. It is the only record of why goago granted the exception. A reviewer or a coding agent reads it before deciding whether the exception still applies. Turning this rule off is possible but self-defeating. It is the rule that makes every other rule's escape hatch auditable.`, Analyzer: newAnalyzer("no-invalid-ignore", "reject //goago:ignore directives that name no known rule or give no reason", checkInvalidIgnore), })
RuleNoInvalidIgnore keeps suppression directives honest.
var RuleNoNakedReturn = register(Rule{ Name: "no-naked-return", Summary: "return statements must be explicit even with named results", Default: true, Severity: Error, Rationale: `A bare return in a function with named results forces the reader to scroll up to learn the returned values. It also silently returns whatever the result variables hold at that point. Naming the values costs nothing and survives later edits to the function body. The rule checks function literals against their own result list, not the enclosing function's. It reports a naked return inside a closure when the closure itself declares named results.`, Analyzer: newAnalyzer("no-naked-return", "reject bare return statements in a signature with named results", checkNakedReturn), })
RuleNoNakedReturn forbids bare returns from a signature with named results.
var RuleNoNewExpr = register(Rule{ Name: "no-new-expr", Summary: "new() takes a type, not an expression", Reverts: "1.26", Default: true, Severity: Error, Rationale: `Go 1.26 lets the built-in new take an expression that supplies the initial value, so new(yearsSince(born)) allocates and initialises in one step. The form collapses declaration, initialisation, and address-taking into a single expression. The two-line version with a named variable says the same thing and reads left to right: age := yearsSince(born) p := &age When type information is available the rule decides exactly whether the argument is a type. Without it the rule falls back to syntax. It reports only unambiguous expressions such as new(f(x)). A shadowed new or a variable that looks like a type name is not reported.`, Analyzer: newAnalyzer("no-new-expr", "reject new() applied to an expression rather than a type (reverts Go 1.26)", checkNewExpr), })
RuleNoNewExpr forbids the Go 1.26 new(expression) form.
var RuleNoRedundantShortDecl = register(Rule{ Name: "no-redundant-short-decl", Summary: "use var except where := is syntactically required", Default: false, Severity: Error, Rationale: `One way to introduce a variable instead of several. The rule does not report := where var is a syntax error: switch t := x.(type) { ... } // var is not allowed in a switch guard for i, v := range xs { ... } // no var form exists select { case v := <-ch: ... } // var is not allowed in a receive clause It reports := only in plain statement position, where var is a drop-in replacement. Moving an if, for, or switch header declaration to a preceding var is legal but widens the variable's scope. The rule does not report those declarations. A blanket ban would force adding syntax to the language. The tool will not do that.`, Analyzer: newAnalyzer("no-redundant-short-decl", "reject := in plain statement position, where var is a drop-in replacement", checkRedundantShortDecl), })
RuleNoRedundantShortDecl forbids := where var is a drop-in replacement.
var RuleNoSelfReferentialConstraints = register(Rule{ Name: "no-self-referential-constraints", Summary: "a generic type may not name itself in its own type parameter list", Reverts: "1.26", Default: true, Severity: Error, Rationale: `Go 1.26 lifted the restriction that a generic type may not refer to itself in its type parameter list. The type Adder[A Adder[A]] interface{ Add(A) A } now compiles. Before Go 1.26 the compiler rejected it as "invalid recursive type". This rule reports exactly that construct: the declared type's own name appearing inside its own type parameter list. It does not report F-bounded constraints written through a separate interface, such as type Node[T Cloneable[T]]. Generics in Go 1.18 made that shape legal. It is not what Go 1.26 changed. Use no-f-bounded-constraints for those. Detection is syntactic and catches direct self-reference. Mutual recursion across two declarations is not reported.`, Analyzer: newAnalyzer("no-self-referential-constraints", "reject a generic type that names itself in its own type parameter list (reverts Go 1.26)", checkSelfReferentialConstraints), })
RuleNoSelfReferentialConstraints forbids the Go 1.26 self-reference.
var Version = "dev"
Version is the module version. Release builds can set it with a linker value. Commands built through go install or go tool read it from Go build information. A local source build without a version reports "dev".
Functions ¶
func Analyzers ¶
Analyzers returns the analyzer for every rule, for use with multichecker, unitchecker, or a golangci-lint module plugin. Callers that want only the default set should filter with Rule.Default.
func DefaultNames ¶
func DefaultNames() []string
DefaultNames returns the canonical names of the rules that are on when no configuration selects a rule set, sorted.
func ExampleConfig ¶
func ExampleConfig() string
ExampleConfig returns the minimal policy that "goago -init" writes. The "default" meta-name follows the defaults in the pinned goago version.
Types ¶
type Config ¶
type Config struct {
// Version names the config schema. Zero accepts an unversioned config from
// ago v0.1. Version 1 is the current schema.
Version int `yaml:"version"`
// Enable lists rules to turn on. The special value "default" expands to
// the default rule set and "all" expands to every rule. An empty list
// means the default set.
Enable []string `yaml:"enable"`
// Disable lists rules to turn off after goago applies Enable.
Disable []string `yaml:"disable"`
// Tests reports whether goago checks _test.go files.
Tests bool `yaml:"tests"`
// Exclude lists path patterns. goago matches them with [path/filepath.Match]
// against each path element and skips those packages.
Exclude []string `yaml:"exclude"`
// contains filtered or unexported fields
}
A Config is the on-disk rule policy for a repository. A committed policy means every developer, every CI job, and every coding agent enforces the same subset without extra instruction.
func LoadConfig ¶
LoadConfig reads a config file. A path of "" searches dir and each parent directory for .goago.yml or .goago.yaml, including the legacy .ago names. Multiple policy files in one directory are an error. When the search finds no file it returns the default config and a nil error.
func (*Config) Enabled ¶
Enabled resolves the config into the set of rules to run, sorted in registration order. overrides, when non-empty, replaces Config.Enable and comes from the command line.
func (*Config) Path ¶
Path returns the file goago loaded the config from, or "" for a default config.
func (*Config) Skip ¶
Skip reports whether a file path matches any exclude pattern.
goago matches a pattern three ways, because each way catches a different surprise. It matches the whole slash-separated path, so "*.pb.go" works. It matches each path element, so "generated" excludes any directory with that name at any depth. It matches each leading path prefix, so "third_party/*" excludes the whole subtree rather than only the files directly inside it.
type Finding ¶
type Finding struct {
// Rule is the canonical kebab-case rule name.
Rule string `json:"rule"`
// Severity is the rule's severity.
Severity Severity `json:"severity"`
// Message states what is wrong and what to write instead.
Message string `json:"message"`
// File is the path as the caller passed it, slash separated.
File string `json:"file"`
// Line and Column are 1-based, matching go vet and gofmt.
Line int `json:"line"`
Column int `json:"column"`
// EndLine and EndColumn bound the offending syntax. They equal Line and
// Column when the rule reports a point rather than a range.
EndLine int `json:"endLine"`
EndColumn int `json:"endColumn"`
// DocURL points at the rule's section in the README.
DocURL string `json:"docURL"`
}
A Finding is one reported violation, in the form the report writers consume. It is deliberately a flat value rather than an analysis.Diagnostic so that the JSON schema is stable across x/tools upgrades.
type Format ¶
type Format string
Format names an output encoding.
const ( // FormatText is the vet-style file:line:col form that editors link on. FormatText Format = "text" // FormatJSON is a stable machine-readable document. It is the format to // use from a coding agent or any other program. FormatJSON Format = "json" // FormatSARIF is SARIF 2.1.0, which GitHub code scanning ingests. FormatSARIF Format = "sarif" // FormatGitHub is the GitHub Actions workflow-command form, which turns // findings into inline annotations on a pull request. FormatGitHub Format = "github" )
Supported output formats.
type Options ¶
type Options struct {
// Dir is the directory goago resolves patterns against. An empty Dir means
// the process working directory.
Dir string
// Patterns are go/packages patterns such as "./..." or a list of .go
// files. An empty Patterns means "./...".
Patterns []string
// Rules are the rules to run. An empty Rules means the default set.
Rules []Rule
// Tests reports whether goago analyzes _test.go files.
Tests bool
// Config supplies exclude patterns. It may be nil.
Config *Config
// ReportStaleIgnores reports //goago:ignore directives that suppressed
// nothing.
ReportStaleIgnores bool
}
Options control one Check run.
type Report ¶
type Report struct {
// SchemaVersion is the JSON report schema version.
SchemaVersion int `json:"schemaVersion"`
// Version is the goago version that produced the report.
Version string `json:"version"`
// Rules lists the canonical names of the rules that ran, sorted.
Rules []string `json:"rules"`
// Findings holds every violation, ordered by file, line, and column.
Findings []Finding `json:"findings"`
// StaleIgnores lists //goago:ignore directives that suppressed nothing.
StaleIgnores []StaleIgnore `json:"staleIgnores"`
// Errors holds load or parse failures. A non-empty Errors means the run
// did not finish and Findings may omit violations.
Errors []string `json:"errors"`
}
A Report is everything one goago run produced. The JSON encoding of this type is goago's machine-readable contract. Fields grow over time but existing fields keep their name and meaning.
func Check ¶
Check loads the named packages and runs the selected rules over them.
A load or type error does not abort the run. Check collects errors into Report.Errors and analysis continues over whatever parsed. A single unbuildable file cannot hide every finding in the repository.
type Rule ¶
type Rule struct {
// Name is the canonical kebab-case rule name, such as "no-goto".
Name string
// Summary is a single line shown by "goago -list".
Summary string
// Rationale explains why the rule exists and when to turn it off. It is
// the analyzer's Doc body and the text an agent reads to decide whether a
// violation is worth fixing or worth ignoring.
Rationale string
// Default reports whether goago enables the rule when no configuration
// selects a rule set explicitly.
Default bool
// Reverts names the Go release that introduced the construct this rule
// forbids, or "" when the construct is not tied to one release.
Reverts string
// Severity is the level reported for violations of this rule.
Severity Severity
// Analyzer enforces the rule.
Analyzer *analysis.Analyzer
}
A Rule is one restriction, paired with the analyzer that enforces it.
Name is the canonical kebab-case name used by the goago command, by .goago.yml, and by //goago:ignore directives. The Analyzer.Name field is the same name with the hyphens removed, because go/analysis requires analyzer names to be valid Go identifiers. The goago command accepts either spelling.
type Severity ¶
type Severity string
Severity classifies how strongly goago objects to a construct. Every rule currently reports at Error. The field exists so that report formats with a severity axis, such as SARIF, carry an honest value rather than a hardcoded one.
type StaleIgnore ¶
type StaleIgnore struct {
// Rules lists the rule names the directive claimed to suppress.
Rules []string `json:"rules"`
// Reason is the text the author gave for the exception.
Reason string `json:"reason"`
// File, Line, and Column locate the directive.
File string `json:"file"`
Line int `json:"line"`
Column int `json:"column"`
}
A StaleIgnore is a suppression directive that matched no finding. The command reports it separately from Findings because it is a maintenance signal rather than a Go-subset violation.
func (StaleIgnore) Position ¶
func (s StaleIgnore) Position() string
Position renders the directive location in file:line:col form.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
goago
command
Command goago enforces one way to write Go across a codebase.
|
Command goago enforces one way to write Go across a codebase. |
|
plugin
|
|
|
golangci
Package golangci registers goago as a golangci-lint module plugin.
|
Package golangci registers goago as a golangci-lint module plugin. |