Documentation
¶
Overview ¶
Package discover finds the mutation candidates in a snapshot.
Discovery is the first phase that needs a Go toolchain and the first that needs types. It loads the snapshot with golang.org/x/tools/go/packages, walks the syntax of every file the module owns, and produces two things: the candidates a later phase will instrument, and a recorded reason for every place it deliberately did not produce one. Nothing is dropped silently — "why is there no mutant here?" is a question `--explain` has to be able to answer without re-running anything.
A compiling tree is a precondition ¶
Any package that fails to load or type-check stops discovery with CodePackageErrors. This is deliberate. Every rule in this package is type-directed to some degree — a boolean literal is only a candidate when it really is the universe constant, a type argument is only recognisable as a type through types.Info — and a partially typed tree would silently produce a different, smaller catalog rather than an error. Since the run would fail at the baseline build minutes later anyway, failing here is both faster and more precise: the message names the first few errors and where they are.
The single exception is a package that imports "C". Those are excluded from mutation wholesale (v1 limitation), so their own build failures are not something the user has to fix before mutation testing can start; whatever depends on them still fails the gate, because that dependency is real.
What is never mutated ¶
Test files are structural: `_test.go` is built, type-checked, and run, and never mutated. That is not recorded as a skip, because it is not a decision about a particular file.
Two more things are never candidates and are never skips either, for the same reason: neither was a decision about a place. A call to the builtin `panic` is not deleted, because deleting a terminating panic manufactures a missing return rather than a mutant; and a return value already spelled as its own replacement — `return 0` from a function returning an int — produces nothing, because the mutation and the source would be the same program.
Everything else that is passed over is recorded as a Skip with a SkipReason: whole files (generated code, cgo packages, files the include and exclude patterns removed), individual expressions sitting in a context that instrumentation cannot rewrite — constant declarations, array lengths, case labels, package-level variable initialisers, and type parameter lists or explicit type arguments — and edits whose rewrite site none of the three guard forms can express, which are SkipUnnameableDeclType. The reason reported for an expression is the outermost suppressed region containing it: that is the region a walker would have declined to descend into, so it is the reason that stays true no matter what happens inside it.
SkipUnnameableDeclType is the widest of those, and deliberately so: it is one reason with one string, and it covers every site v1's guard forms cannot express. Beyond the type that cannot be spelled that it is named for, the declaration form refuses a site whose initialiser mentions a name the site itself declares — hoisting the declaration out in front of it would rebind the reference to a zero value and change what the program computes — and one whose declaration tokens cannot be cut out without moving a line. Guard enumerates all of them.
The guard site hint ¶
Every candidate carries a Guard: which of the three rewrite forms the instrumentation phase has to use for it, over which bytes, and — for the declaration form — the source spelling of every type the site declares.
It lives here because this is where the type information is. Whether an expression is the universe `bool` or a named boolean type, what `x := f()` declares, whether a value is an `error`: each is a go/types question, and internal/instrument deliberately parses the snapshot without type checking it, so that a byte rewriter can be tested with no toolchain in the loop. Guard documents the contract in full; what belongs here is that the type gates and the site hint are two uses of one type check, done once.
What the operator families ask of the types ¶
Every family below the comparison family is decided by what the operands *are* rather than by how they are spelled, and every gate reads through a named type to its underlying one: `type Celsius float64` adds and subtracts exactly like a float64. String concatenation is not an integer-arithmetic candidate because its operands are strings, complex arithmetic is out of scope because the float gate asks for a floating-point type, and a type parameter gates out of all of them because its underlying type is its constraint. docs/operators.md is the table.
What the build configuration decides ¶
Discovery mutates what the build contains, and the build is a function of GOOS, GOARCH, and build tags. Two consequences are worth stating out loud, because neither produces a skip:
A file excluded by a build constraint — `foo_linux.go` on Windows, anything behind a tag that is not set — is not part of any package here, has no type information, and is not mutated. A package whose files are *all* excluded is not even matched by `./...`, which is what happens to a pure cgo package when CGO_ENABLED is 0: the go command does not build it, does not test it, and does not list it, so there is nothing for discovery to skip. A cgo package that also holds ordinary Go files does exist under either setting, and is then skipped whole, every file named.
Determinism ¶
Two discoveries over the same bytes produce identical results, field for field. Candidates are emitted in (path, span start, rule registry position) order and skips in (path, reason) order, both compared byte-wise with no locale involved. Nothing downstream — the catalog, the dense runtime indices, a shard assignment — can drift because a map was ranged over or a directory was read in a different order.
The go command ¶
go/packages shells out to `go list`, and it finds that executable on the *process* PATH rather than on the environment handed to it. Options.Toolchain is therefore prepended to the child environment's PATH — which is what makes the child resolve GOROOT and toolchain lines consistently — but a `go` that is not on this process's PATH at all cannot be reached from here. Callers that manage their toolchain out of band (mise, asdf) already run go-mutants through it; when they have not, CodeLoadFailed says so.
The child also runs with GOWORK=off. A snapshot is meant to be the whole truth about what is being tested, and a `go.work` in one of its parent directories or named by $GOWORK is a file the snapshot does not contain; a workspace at the snapshot root itself is a different matter and is refused outright with CodeWorkspace.
Index ¶
Constants ¶
const WorkspaceFile = "go.work"
WorkspaceFile is the name of the file whose presence at the snapshot root makes a tree a multi-module workspace, which v1 refuses.
Variables ¶
This section is empty.
Functions ¶
func BuildCatalog ¶
BuildCatalog feeds a result into the catalogue builder.
It is a convenience and nothing more: mutation.Builder sorts, deduplicates, and indexes on its own, so the ordering Discover produces is not load bearing here. Rule selection has already happened — it is Options.Rules — which is why this takes no selection argument.
func CompilePatterns ¶
CompilePatterns compiles include or exclude patterns, reporting the first one that does not parse as a CodePattern error.
It exists so that every pattern a run uses is compiled in one place, with one code, whether it came from `.go-mutants.toml` or from a flag. The underlying *glob.SyntaxError stays reachable with errors.As, so a caller that wants to underline the offending byte still can.
func SupportedRules ¶
SupportedRules returns the rules this phase implements, in canonical registry order.
It is derived from the operator tables rather than listed, and it is the one answer to "which rules can be discovered today". A caller may hand Discover a whole profile's selection without consulting it: a registered rule this phase does not implement is ignored rather than refused.
Types ¶
type Code ¶
type Code string
A Code is a stable, user-facing diagnostic code.
This package owns the GOM41xx block. Like the orchestration codes it does not re-code the failures of the packages it uses: a toolchain that cannot be located is reported by internal/gocmd with its own code, because two identifiers for one condition means a user searching for the wrong one.
const ( // CodeSnapshotRoot reports a snapshot root that is empty, cannot be // resolved, or is not a directory. It is a caller mistake rather than a // fact about the tree under test. CodeSnapshotRoot Code = "GOM4101" // CodeWorkspace reports a `go.work` file at the snapshot root. Multi-module // workspaces are not supported in v1: one module path, one set of // module-relative identities, one baseline. Saying so is the honest answer; // mutating the first module and quietly ignoring the rest is not. // // A workspace file outside the snapshot is neither reported nor obeyed. The // loader runs with GOWORK=off, so a snapshot that happens to sit below // somebody else's `go.work` — a temporary directory inside one, say — is // still discovered as the single module it contains. CodeWorkspace Code = "GOM4102" // CodePattern reports an include or exclude pattern that does not compile. // It is allocated here, and not in internal/engine, because which files are // worth mutating is discovery's question — the retired GOM4002 was the same // condition asked in the wrong place. CodePattern Code = "GOM4103" // CodeLoadFailed reports that the package loader itself could not run: // no `go` command reachable, a driver that failed, a cancelled context. // Nothing is known about the tree yet at this point. CodeLoadFailed Code = "GOM4110" // CodePackageErrors reports packages that failed to load or type-check. // Discovery requires a compiling tree; see the package documentation for // why this is an error and not a warning. CodePackageErrors Code = "GOM4111" // CodeModuleNotFound reports a snapshot root that no loaded package calls // its module root: an empty directory, a directory inside somebody else's // module, or a tree with no Go packages at all. CodeModuleNotFound Code = "GOM4112" // CodeUnknownRule reports a requested rule that the canonical registry does // not know, or knows with different metadata. Rules the registry knows but // this phase does not implement yet are ignored instead; see // [SupportedRules]. CodeUnknownRule Code = "GOM4120" // CodeSpanMismatch reports that a candidate's byte span does not cover the // text the rule says it replaces. It is an internal invariant violation and // always a bug in this package: the alternative to failing loudly is // splicing the wrong bytes into somebody's source in a later phase. CodeSpanMismatch Code = "GOM4130" // CodeInvalidCandidate reports a candidate that internal/mutation refused. // Same category as [CodeSpanMismatch], caught one layer further down. CodeInvalidCandidate Code = "GOM4131" // CodeFileUnreadable reports a source file that could not be read, or that // is too large to address with the 32-bit span offsets identities use. CodeFileUnreadable Code = "GOM4140" )
The discovery codes.
func CodeOf ¶
CodeOf returns the Code carried by err, or the empty Code if err did not come from this package.
type DeclType ¶
type DeclType struct {
// Name is the identifier as it is spelled in the declaration.
Name string
// Type is the type as it must be written in this file.
Type string
}
A DeclType is one identifier a Form D site declares, together with the source spelling of its type.
Type is what types.TypeString produced against a qualifier built from the file's own import declarations, so it can be written into that file verbatim. Discovery never invents an import to make a type nameable: a type that cannot be spelled with what the file already imports makes the whole candidate a SkipUnnameableDeclType skip instead.
type Error ¶
type Error struct {
// Code is the stable diagnostic code.
Code Code
// Message states the problem in one line, without the code.
Message string
// Err is the underlying cause, or nil. It stays reachable through
// errors.Is, which is how the command line recognises a cancellation.
Err error
}
An Error is one discovery failure carrying a stable Code.
It mirrors the shape internal/engine and internal/gocmd use — code, one-line message, optional cause — so a single renderer can lay all three out the same way, without the three packages sharing an error identity.
type Guard ¶
type Guard struct {
// Form is the rewrite shape to use.
Form GuardForm
// SiteSpan is the byte range the guard replaces: the bool expression for
// Form C, the statement for Form S and Form D. It always contains the
// candidate's own span.
SiteSpan mutation.Span
// DeclTypes are the identifiers a Form D site declares, in source order,
// with the type each one must be declared as. It is empty for Form C and
// Form S, and may be empty for a Form D site whose every name is the blank
// identifier, which declares nothing.
DeclTypes []DeclType
}
A Guard is the Form D site hint: the contract between discovery, which has the type information, and instrumentation, which has none.
Why the hint is computed here ¶
Choosing a guard form needs answers only a type checker holds — is this expression the universe `bool` or a named boolean type, what type does `x := f()` declare, is this value an `error` — and instrumentation deliberately parses the snapshot without type checking it. Handing the decision down as data keeps that split: the instrumenter stays a byte rewriter that can be tested with no toolchain in the loop, and the phase that already paid for go/types answers the questions once.
How the form is chosen ¶
Walking outward from the edit, in this order:
- The nearest enclosing expression whose static type is exactly the universe `bool` — `types.Typ[types.Bool]`, or an untyped bool that materialised as one — and that sits in a position where a parenthesised expression is legal, is a GuardFormC site. The search stops at the first ancestor that is not an expression, so it never crosses out of a function literal into the expression the literal sits in.
- Otherwise the nearest enclosing statement, which must be an expression statement, a `return`, an assignment that is not `:=`, an `++`/`--`, a send, a `defer` or a `go` for GuardFormS, or a `:=` or a `var` declaration for GuardFormD. The search stops at the enclosing function, for the same reason.
Anything else is refused, and a refused candidate is never emitted. The refusals are all reported as SkipUnnameableDeclType, which this phase reads as "v1's guard forms cannot express this site":
- the nearest statement is one no form covers — a `switch` tag, a `range` clause, an `if` whose condition is a named boolean type;
- the statement sits where a block is not legal Go, which is an `if`, `switch` or `for` initialiser, a `for` post statement, or a type switch guard: `for i := 0; i < n; if __gm.M[3] { … }` does not parse;
- a Form D site declares a type that cannot be spelled with the file's own imports;
- a `:=` redeclares an existing variable instead of declaring every name on its left afresh. Form D would have to know which names to declare and which to leave alone, so v1 declines the whole site;
- an initialiser of a Form D site mentions a name that same site declares. Go begins a declared name's scope at the end of its own specification, so `total := total * 2` and `err := fmt.Errorf("…: %w", err)` read the enclosing declaration; hoisting the new one out in front would rebind them to a zero value and quietly change what the program computes;
- a Form D site is a `var` whose declaration tokens cannot be cut without moving a line: a spec with no initialiser, or a spelled-out type, written across more than one line. The whole of the first and the type of the second are what the rewrite removes, and removing a line break moves every line after it.
type GuardForm ¶
type GuardForm string
A GuardForm names one of the three rewrite shapes instrumentation composes a dormant mutant from. The design plan calls them Form S, Form C, and Form D, and these are those three and no others.
const ( // GuardFormC is the bool selector: // // (__gm.M[3] && (<mutated>) || !(__gm.M[3]) && (<original>)) // // It wraps an expression whose static type is exactly the universe `bool`, // so that both branches are ordinary expressions in the site's own context // and the compiler settles typing, evaluation order, and short-circuiting. // A named boolean type is deliberately not a Form C site: the selector // evaluates to `bool`, which is not assignable to `type Flag bool`. GuardFormC GuardForm = "C" // GuardFormS is the statement guard: // // if __gm.M[7] { <mutated statement, flattened> } else { <original bytes> } // // It is used where the edit is not inside any bool-valued expression. The // site is a statement that declares nothing, so wrapping it in a block // changes no scope. GuardFormS GuardForm = "S" // GuardFormD is the declaration rewrite: // // var x T; if __gm.M[9] { x = <mutated> } else { x = <original> } // // It is used where the site is a statement that *does* declare something — // `x := e` or `var x = e` — because Form S would bury those declarations // inside a block and the code after them would stop compiling. The declared // types the rewrite needs are in [Guard.DeclTypes]; discovery computes them // because it is the only phase that has the type information. GuardFormD GuardForm = "D" )
The three guard forms.
type Located ¶
type Located struct {
mutation.Candidate
// Line is the 1-based line the candidate's span starts on.
Line int
// Column is the 1-based byte offset of the span's start within that line.
// Bytes, not runes and not display cells: it is what `file:line:col`
// consumers — editors, `::warning file=`, a jump-to-mutant — expect.
Column int
// Package is the import path of the package owning the file, with the
// " [pkg.test]" suffix of a test variant removed.
Package string
// Guard is the rewrite site the instrumenter has to use for this candidate.
// Every candidate carries one; a candidate for which no guard form could be
// determined is not emitted at all, it is a [SkipUnnameableDeclType] skip.
Guard Guard
}
A Located is one candidate plus where a human would look for it.
The embedded mutation.Candidate is the whole truth for identity and instrumentation; the line, column, and package are for the console, the report, and the GitHub annotations, none of which can do anything with a byte offset. Located.Guard is neither: it is the site hint the instrumentation phase consumes, and it is documented as a contract on Guard.
type Options ¶
type Options struct {
// SnapshotRoot is the absolute or relative path of the module root to
// discover in. It is the snapshot, never the user's working tree: the
// digests candidates carry are of the bytes found here.
SnapshotRoot string
// Toolchain is the located Go toolchain. Its directory is prepended to the
// child environment's PATH; see the package documentation for what that
// does and does not achieve.
Toolchain gocmd.Toolchain
// Env is the complete base environment used by the package loader. Nil
// inherits the current process environment. Discovery still forces
// GOWORK=off and prepends the located toolchain's directory to PATH. The
// field allows a long-lived public workspace to freeze all other build
// inputs at Open time instead of observing later process-global changes.
Env []string
// Rules selects the operators to apply. Empty means every rule this phase
// implements, which is what [SupportedRules] returns. Rules the canonical
// registry does not know are an error; rules it knows but this phase has
// not implemented yet are ignored, so a caller may pass a whole profile's
// selection without tracking which families have landed.
Rules []mutation.Rule
// Include lists the patterns a file must match to be considered, matched
// against its '/'-normalized module-relative path. Empty includes
// everything; a file matching none of a non-empty set is recorded as an
// "excluded" skip.
Include []glob.Pattern
// Exclude lists the patterns that remove a file again. Excludes are
// applied after includes, so an exclude always wins.
Exclude []glob.Pattern
}
Options configures Discover.
The zero value is not usable: Options.SnapshotRoot has no sensible default, since discovery must never be pointed at the user's own tree by accident.
type Result ¶
type Result struct {
// Candidates are the proposed edits, in (path, span start, rule registry
// position) order.
Candidates []Located
// Skips are the recorded reasons, in (path, reason) order.
Skips []Skip
// ModulePath is the module path of the main module at the snapshot root.
ModulePath string
// GoVersion is that module's `go` directive — "1.26", not "go1.26.5". It
// is deliberately not the toolchain version: the caller passed the
// toolchain in and already knows that. An empty string means the module
// declares no `go` directive, which is reported as the empty string rather
// than filled in from somewhere else.
GoVersion string
}
A Result is everything one discovery pass learned.
func Discover ¶
Discover finds every mutation candidate in the snapshot.
The sequence is fixed: refuse what cannot be discovered at all (a bad root, a workspace, an unknown rule), load, prove the tree compiles, and only then walk syntax. Each step's failure has its own code, so a user never has to guess which half of the phase went wrong.
type Skip ¶
type Skip struct {
// Path is the '/'-normalized module-relative path of the file.
Path string
// Reason is why discovery passed it over.
Reason SkipReason
// Count is the number of suppressed candidates, or 1 for a whole file.
Count int
}
A Skip is one recorded reason, aggregated per file.
Count means one of two things, depending on the reason, and the distinction is worth stating: for a whole-file reason (SkipGenerated, SkipCgo, SkipExcluded) it is 1, because the file was never opened and counting candidates in it would mean guessing. For a context reason it is the number of candidates that really were suppressed there.
type SkipReason ¶
type SkipReason string
A SkipReason names why discovery passed something over. Reasons are part of the report format and of `--explain` output, so each string is fixed.
const ( // SkipGenerated marks a file whose leading comments claim it is generated. // Mutating generated code measures the generator's test suite, not this // project's, and the edit would be overwritten by the next run of it. SkipGenerated SkipReason = "generated" // SkipCgo marks every file of a package that imports "C". The instrumented // build would have to survive the cgo preprocessor, which v1 does not // attempt. SkipCgo SkipReason = "cgo" // SkipExcluded marks a file the include and exclude patterns removed. When // more than one whole-file reason applies to a file, this is the one // reported: the others are facts about the code, and this one is the // user's own decision, which is the answer they are looking for. SkipExcluded SkipReason = "excluded" // SkipConstDecl marks an expression inside a `const` declaration. A // constant must stay constant, and an `iota` block is one edit away from // renumbering everything after it. SkipConstDecl SkipReason = "const-decl" // SkipArrayLength marks an expression inside an array length. It is part of // a type, evaluated by the compiler and never at run time. SkipArrayLength SkipReason = "array-length" // SkipCaseLabel marks an expression in the label list of a `switch` case or // in the communication clause of a `select`. Case *bodies* are ordinary // code and are mutated; v2 revisits the labels themselves. SkipCaseLabel SkipReason = "case-label" // SkipPackageVarInit marks an expression in a package-level variable // initialiser, `//go:embed` declarations included. Initialisation order is // a global property that a per-mutant guard cannot express in v1. SkipPackageVarInit SkipReason = "package-var-init" // SkipTypeParam marks an expression inside a type parameter list, a // constraint, or an explicit type argument. Those positions hold types, not // values, however much a constant array length inside one may look like a // value. SkipTypeParam SkipReason = "type-param" // SkipUnnameableDeclType marks a candidate whose rewrite site none of the // three guard forms can express. The name comes from the case the design // plan called out — a Form D declaration whose type cannot be spelled with // the imports the file already has — and it has since become the single // reason for every such refusal, because they are one fact for a user: // go-mutants knows what it would like to mutate here and cannot say it in // Go. [Guard] enumerates them. SkipUnnameableDeclType SkipReason = "unnameable-decl-type" )
The v1 skip reasons: three that remove a whole file, five that suppress an expression because of the context it sits in, and one that refuses a site no guard form can express.
func AllSkipReasons ¶
func AllSkipReasons() []SkipReason
AllSkipReasons returns every reason discovery can emit, in the declaration order of the constants above. The slice is freshly allocated, so a caller may sort or filter it without disturbing anyone else.
This list MUST name every SkipReason this package emits. It is the canonical enumeration the rest of the tree checks itself against: the package's own tests parse these sources and fail when a Skip* constant is declared without being listed here, and the tests of internal/report check the `reason` enumeration of the run report schema against it. A reason missing from this list is a reason nothing guards.
func (SkipReason) Explanation ¶
func (r SkipReason) Explanation() string
Explanation is one sentence saying what a reason means, or "" for a reason this build does not define.
The empty answer is for a document rather than for a run: `--explain` reads reasons out of a report, which may have been written by another version, and a reason nobody here recognises is still a row worth printing with its counts.