vow is a Go linter that expresses and verifies value-level
obligations — properties that must hold for specific values as they
flow through code — through a declarative DSL and reusable presets.
The name reads as vow witnessing the obligation: every contract
is named, located, and proved (or reported) by the analyzer.
Typical obligations vow expresses:
- Nil safety — every parameter, receiver, and return position is statically classified as non-nil, nillable, or platform, so callers cannot pass
nilinto a non-nil slot and bodies cannot overwrite a non-nil binding (vow:nil). - Relational invariants between values — a function declares that one value's state determines another's: a parameter requirement pairs with a return requirement, or two expressions chain through an implication or biconditional. The analyzer enforces the invariant at every call site and every matching return (
vow:cond). - Must-consume values — a designated value must be checked through a recognised consumer (
errors.Is/errors.As, an equality compare, a switch case) before it leaves the function that produced it; propagation across a boundary requires explicit authorisation (vow:define @Sentinel,vow:emit,vow:use). - Lifecycle invariants — a designated resource must reach a discharge —
defer Close, goroutine hand-off, callback hand-off, storage write, or channel send — before its acquisition scope ends (vow:define @Closable,vow:emit).
The concept system extends the same enforcement machinery to
project-specific invariants (vow:define), and dedicated controls
let a function declare itself as an all-verification barrier or
suppress a single site with a reason (vow:discharged,
vow:suppress). See the Marker family table for placement and
payload details and the Concept system section for the
extensibility surface.
Rather than hard-coding each rule, vow reads YAML presets that
describe the obligation in a uniform DSL and dispatches them to
generic rule engines built on
go/analysis.
Requires Go 1.27 or later.
go install github.com/knowledge-work/go-vow/cmd/vow@latestDeclare a package-level sentinel and a function that propagates it under both an emit and a nil-safety contract:
package store
import "errors"
type Record struct{ /* ... */ }
func load(id *string) (*Record, error) { /* ... */ }
// vow:define @Sentinel
var ErrNotFound = errors.New("not found")
// vow:nil (!) !, ?
// vow:emit ErrNotFound
func Lookup(id *string) (*Record, error) {
if *id == "" {
return nil, ErrNotFound
}
return load(id)
}The vow:emit ErrNotFound marker authorises Lookup to hand the
named sentinel back; without it, return nil, ErrNotFound is
reported as a leak. The check reads references to the sentinel —
every place ErrNotFound is itself named. A reference in a
conditional position discharges the obligation, whether the branch
matches it with errors.Is / errors.As, compares it directly, or
lists it as a switch case; a reference anywhere else is reported,
including an errors.Is call whose result is returned instead of
branched on. A caller that only holds the error value Lookup
returned never names the sentinel, so the obligation is not carried
across the call.
The vow:nil (!) !, ? signature mirror pins id and the first
return as non-nil and the second return as nillable. A caller
passing nil to id is reported at the call site; a body that
overwrites id with nil is reported at the assignment.
Run the linter:
vow ./...| Marker | Purpose |
|---|---|
vow:nil |
Signature-mirror nil-safety contract. Per-position tokens ! (non-nil), ? (nillable), or unmarked (platform) pin the receiver, parameters, and returns inline with the Go signature. The keyword form nonnil / nil is accepted as input at every slot. |
vow:cond |
Relational constraint. The structural form pairs a parameter requirement with a return requirement through ->; the logical form pairs two expressions through => (implication) or <=> (biconditional). The payload supports value-level sum types, the [subject] scope qualifier, and lifecycle-pair invariants. |
vow:use |
Caller-authored discharge assertion: the marker claims the named subject is consumed at this scope and drops the must-consume diagnostic locally. Placed on a bool-returning function declaration, the same marker also promotes the function to transducer status so callers in conditional context discharge the obligation. |
vow:emit |
Declares the callee's output side. The payload has three shapes: name-based, positional ($N), and passthrough (name -> $N). A statement-scope vow:emit additionally discharges a Closable lifecycle hand-off and dispatches to five recognised destinations: goroutine, callback, storage, channel, or wrapped-return. |
vow:discharged |
Callee-authored marker on a function declaration. The annotated function counts as an all-verification barrier so callers may treat the call as discharging every obligation the function carries. |
vow:suppress |
Suppression marker. The payload must carry a reason string so the suppression survives review. |
vow:define |
Concept declaration. @Sentinel and @Closable are the built-in concepts; user-declared concepts attach behaviour to a var or type through preset configuration. |
vow:import |
Imports a preset rule namespace into the package: vow:import result "preset/std/result" makes the namespace available for inline reference (@result.OkErr[Result, error]). |
The Annotations reference documents each marker's full payload grammar, placement rules, and the analyzer behaviour it drives.
Every subject reference resolves into one of three classes:
- Observe — the reference sits in a conditional position and is
consumed by a recognised discharger there (
errors.Is/errors.As, an equality compare against the sentinel, or aswitchcase branch). The same discharger outside a conditional — its result returned or bound to a variable — does not observe. - Chain — the reference is propagated through a recognised
authorisation surface (the enclosing signature lists the subject
through
vow:emit, or the call expression resolves to avow:emit-declared callee). - Leak — every other reference. The analyzer reports the leak at the offending expression so the author either observes the subject, authorises the chain, or suppresses the diagnostic with a reason.
vow:define @<Concept> declares a tracked subject. The analyzer
ships two built-in concepts:
@Sentinel— a package-levelvar(typicallyerrors.New) becomes a must-consume subject. Thesentinel-errorpreset wires the observation surfaces.@Closable— atype(or a built-in standard-library type listed under theclosablepreset) becomes a lifecycle subject. Every acquisition must reach a discharge before its scope closes.
A vow:define @<Concept> declaration whose concept has no
behaviour registered surfaces an info-level diagnostic so the
author learns that preset configuration is pending. Adopters add
new concepts by extending a preset YAML and registering the
obligation pattern the concept follows.
| Preset | Purpose |
|---|---|
preset/sentinel-error.yaml |
Activates the must-consume obligation on every vow:define @Sentinel declaration. |
preset/closable.yaml |
Activates the lifecycle-discharge obligation on every vow:define @Closable type and on a curated list of built-in Closable Go types (*os.File, *net.TCPConn, *bufio.Writer, io.Closer). |
preset/std/result.yaml |
A reusable rule library for the (Result, error) return shape. Imported into a package with vow:import result "preset/std/result". |
example/werror.yaml |
A sample preset showing the wrapped-return shape vow:emit uses to recognise a wrap library's pass-through call as a chain-authorisation source. Adopters copy and adapt the shape for their own wrap helpers. |
See the Presets guide for the YAML schema and authoring walkthrough.
- Getting started — install, annotate, run.
- DSL reference — terms, qualifiers, sums, tuples, logical-arrow rules, signature-mirror grammar.
- Annotations — every
vow:marker and where it applies. - Presets — the bundled presets, the
std/resultrule library, and how to author your own. - CLI —
vowcommand, flags, exit codes. - Architecture — how detection, matching, and cross-package facts are implemented internally.
The analyzer's test fixtures under internal/analysis/testdata/src/
are also worked examples: each is a compiling Go package carrying
real annotations, and every // want comment states the exact
diagnostic the annotated code produces. To see how a marker behaves
in a shape the prose does not cover, read the fixture rather than
guess — go test ./... fails as soon as a // want stops matching
what the analyzer reports, so the examples cannot drift from the
implementation.
One package per marker family and scenario, named after the two:
| Prefix | Covers |
|---|---|
nilDecl* |
vow:nil — signature mirror, field declarations, receivers, flow, cross-package facts. |
cond* |
vow:cond — grammar, logical arrows, tag exhaustiveness, caller-side narrowing, lifecycle pairs. |
sentinels, vowUse, vowEmit, useHint, discharged, suppress, concept |
@Sentinel must-consume, and the markers that discharge or suppress it. |
lifecycle_*, closable* |
@Closable — the five discharge destinations. |
higher_order*, narrowScope*, qualified_*, importPkgScope |
Function values, scope qualifiers, and preset imports. |
A fixture whose name ends in XPkgCallee / XPkgCaller is one half
of a cross-package pair; read both to see which side exports the
fact and which side consumes it.
| Path | Purpose |
|---|---|
cmd/vow/ |
CLI entry point. |
internal/analysis/ |
Analyzer implementation built on go/analysis. |
internal/dsl/ |
YAML preset parser and annotation grammar. |
preset/ |
Bundled presets. |
preset/std/ |
Reusable rule libraries. |
example/ |
Sample presets demonstrating extension patterns. |
docs/ |
User-facing documentation. |
internal/analysis/testdata/src/ |
Fixtures consumed by analysistest, one package per marker family and scenario. |
go build ./...
go test ./...
go vet ./...MIT. See LICENSE.