Documentation
¶
Overview ¶
Package seams provides a go/analysis analyzer enforcing the gomatic dependency-injection standard at authorship: outside a composition root, a package must not call an impure stdlib entry point — the clock, the global random source, the network, a subprocess — directly.
The standard's reasoning is testability, not purity: "Dependency injection everywhere. Functions and constructors accept their collaborators (filesystem, clock, readers, writers) as parameters or interfaces. Never reach for a global or a real OS resource where an injected abstraction works. This is what makes 100% coverage reachable." A `time.Now()` reading a branch depends on puts that branch out of a test's reach, so the 100%-coverage gate catches it — but only at the end, after the design is written. This rule catches the same defect at the call site.
The filesystem is named in the standard's sentence and is deliberately NOT on this rule's list; the boundary below says why.
The conforming forms, all silent by construction ¶
- An injected collaborator. `d.spawn(name)`, `clock.Now()` — the qualifier is a value, not an imported package, so nothing is reported.
- A package-level seam variable, which the standard blesses explicitly as the correct Go answer where a stdlib call has no other seam. `var now = time.Now` is a reference, never a call, so the declaration is silent; and `now()` at the use site is a call through an identifier, not through a package, so it is silent too.
- The same seam written as a closure, because the seam's signature differs from the stdlib's: `var runQuiet = func(name string) error { return exec.Command(name).Run() }`. A function literal that is the whole initializer of a NAMED package-level var is a seam declaration, not a call site. A literal bound to nothing (`var _ = func…`) or buried inside an initializer's expression (`var cached = sync.OnceValue(func…)`) is not: neither is a var a test can rebind, so both are walked like any other code.
- The real implementation BEHIND a seam. Every dependency-injected design bottoms out in one place that touches the world, and that place is ordinary Go. Two shapes are recognised, and each is recognised only when the seam it claims is one a test can actually substitute: a function the package HOLDS anywhere a test can write over — a package-level var bound at its declaration or by a later statement, a field, a registry entry, or an argument at a parameter declared with a function type (`var spawn commandRunner = execRun`, `client{fetch: httpGet}`, `New(git.run, git.exists)`) — and a method implementing an interface the package declares AND can be injected through, one that is exported or written as the type of a variable, a parameter, a result or a field (`type Clock interface{ Now() time.Time }` beside `func (System) Now() time.Time`). Both mean the seam already exists one level up. A reference that binds the function nowhere a test can reach — a local capture, a blank, an argument at an `any` parameter — and an interface nothing names are not seams and exempt nothing.
- Passing the function itself. `New(exec.Command)` is the injection this rule exists to encourage; only a CALL is reported.
Deliberate boundaries ¶
A rule that fires on legitimate code is worse than no rule, so every case this analyzer cannot decide reliably resolves to silence:
Composition roots are exempt wholesale, and one is a `main` PACKAGE. The import path is not consulted: anything beneath a `cmd` element used to be exempt too, which was a forgery costing one `git mv`, and measured across the fleet every such package is already `package main`. A library cannot be renamed to `main` without breaking every importer, so the clause is the half that cannot be had for free.
Test files are out of scope: reaching for a real resource in a test is the test's business. A test-SUPPORT package is out of scope for the same reason, recognised by the property that proves it — a NON-test file declaring an API that TAKES a testing harness (`*testing.T`, `*testing.B`, `*testing.F`, `testing.TB`). Nothing else has a reason to.
A non-test file merely IMPORTING `testing` used to be the marker, on the stated ground that doing so links the `-test.*` flag registration into every binary. That has not been true since Go 1.13 and is measurable: a binary whose only reference is `import _ "testing"` registers ZERO flags, so the exemption cost one blank line and silenced a whole package.
The package NAME is deliberately not consulted, because `pgtest` and `latest` are the same string shape and only one of them is a word.
The filesystem is not on the list. It was, and it was wrong: it produced 146 of 187 findings across 105 modules with no defect among them, because the rule's premise — the branches around the call cannot be reached from a test — is false of it. `t.TempDir` gives a test a real directory and a bad path reaches the failure branch, so a filesystem call site is already coverable without a seam. Where a genuinely unreachable branch exists (a rename failing midway), the package-level function var the standard sanctions is still available; it is an option, not a shape every call site owes.
TWO ENTRIES DO NOT MEET THAT CRITERION, measured and left standing because removing them is the owner's call rather than this file's. `os/exec.LookPath` and `net.Listen` are listed, and a test reaches BOTH branches of each with no seam at all: `t.Setenv("PATH", t.TempDir())` makes the lookup fail and a normal PATH makes it succeed, and `127.0.0.1:0` binds while `256.256.256.256:99999` cannot. A probe holding one function per call reports `coverage: 100.0% of statements` with both branches of both reached, and this rule still says the branches "cannot be reached from a test".
That is the identical argument that removed the filesystem, and it is recorded here rather than acted on because the two are not identical in kind: `t.TempDir` is guaranteed, while binding a loopback socket is a real resource a sandboxed runner may refuse. `LookPath` has no such caveat. The population could not be measured from inside this session -- the workspace holds ZERO seams findings of any kind -- so the number the decision wants is not in hand.
Only genuinely impure entry points are listed, and the list is narrower than the packages it draws from. Duration arithmetic over a `time.Time` the caller supplied — `from.Add(ttl)`, `a.Sub(b)`, `a.Compare(b)` — is a pure function of its arguments, and `rand.New(rand.NewPCG(…))` builds a generator from an explicit, reproducible source; neither is reported. `time.Since` and `time.Until` ARE listed, because each is shorthand for a `time.Now` the stdlib takes on the caller's behalf — so `time.Since(cut) > ttl` is the same unreachable branch as `time.Now().Sub(cut) > ttl`, and draws the same finding. What is never reported is a clock reading nothing branches on, whichever of the three spells it.
A dot-imported package (`. "time"`) makes `Now()` indistinguishable from a local seam at the call site, so it is not reported.
`os.Getenv` and friends are not listed. Process environment is read once at the edge often enough that flagging it would report configuration plumbing far more often than a testability defect.
An adapter that satisfies an interface from ANOTHER package is not recognised as one. Whether an exported type is someone else's collaborator cannot be decided from this package's syntax, so such a method is reported like any other.
Pushing the impurity to a thin public wrapper — `func Fetch(url string) (*http.Response, error) { return handle(http.Get(url)) }` over a fully-tested `handle` — is the right design and is still reported, because a one-line wrapper is not distinguishable from a domain function that reaches for the network. The clock is the exception, and it is the branching narrowing rather than a wrapper rule that makes it one: `func Generate(seed int) string { return generateAt(seed, time.Now()) }` consumes the reading as an ARGUMENT, which is a stamp, so it is silent.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Analyzer = &analysis.Analyzer{
Name: "seams",
Doc: "reports a direct call to an impure stdlib entry point (clock, global random source, " +
"network, subprocess) outside a composition root, where an injected " +
"collaborator or a package-level seam variable is required instead",
Run: run,
}
Analyzer reports direct calls to impure stdlib entry points outside a composition root.
var Registration = goyze.Registration{ Precision: goyze.PrecisionJudgment, Name: "seams", Categories: []goyze.Category{"tests", "patterns"}, URL: "https://docs.gomatic.dev/yze/seams", Analyzer: Analyzer, }
Registration declares this analyzer to the yze framework.
Functions ¶
This section is empty.
Types ¶
This section is empty.