Documentation
¶
Overview ¶
Package config is stickler's layered configuration: the documents that declare which checks run, how each tool is invoked, and which findings are permitted to be soft.
It is a leaf. A tool is DATA here (see RunnerSpec) — the package knows how a tool's configuration is spelled and merged, never how one is executed.
Index ¶
- Constants
- func DefaultRunnerSpecs() map[string]RunnerSpec
- func MergeSpecs(defaults, defined map[string]RunnerSpec, native []string) (map[string]RunnerSpec, error)
- type Config
- type FileReader
- type Layer
- type Merger
- type Overlay
- type ParserName
- type Path
- type RepoRoot
- type Resolved
- type RunnerName
- type RunnerSelection
- type RunnerSpec
- type Spec
- type StringList
- type TempWriter
Constants ¶
const ( PlaceholderRoot = "{root}" PlaceholderConfig = "{config}" // PlaceholderCache, in a RunnerSpec's Env entries, expands to a fresh // per-invocation temporary directory removed when the run finishes. It // exists so a tool's result cache can be made hermetic: a shared mutable // cache is a channel through which a stale entry silently suppresses a // real finding (observed in the field — a poisoned global golangci-lint // cache hid a genuine exhaustive violation across many runs until the // file's content changed), and a gate that can be greened by ambient // state is not a gate. PlaceholderCache = "{cache}" )
Argument placeholders substituted in a RunnerSpec's Args at run time.
const ( ToolYze = "yze" ToolGolangci = "golangci-lint" )
Built-in tool literals (named so the spec table and adapters share one source).
Variables ¶
This section is empty.
Functions ¶
func DefaultRunnerSpecs ¶
func DefaultRunnerSpecs() map[string]RunnerSpec
DefaultRunnerSpecs is the built-in tool set as pure data: yze (native stickler-json, config merged from the repo's .yze.yaml plus the resolved `analyzers:` settings) and golangci-lint (adapted JSON, config merged from the repo's .golangci.yaml). A .stickler.yaml `define:` block overrides or extends this map without touching Go.
func MergeSpecs ¶
func MergeSpecs(defaults, defined map[string]RunnerSpec, native []string) (map[string]RunnerSpec, error)
MergeSpecs adds config-defined runner specs to the built-in defaults, and REFUSES one that would replace a built-in.
Defining a NEW tool is the feature: a tool is data here, and adding one is configuration rather than a recompile. Redefining an existing one is not the same act. It used to be allowed, and it deleted any check from the gate silently: three lines pointing `yze` at a script that echoes `{"diagnostics":[]}` took a tree reporting a real violation and exiting 1 to empty output and exit 0, with no warning anywhere.
That is precisely the channel [parseLayer] refuses at the top of this package, and for the identical reason -- a gate lowered by configuration reads to everyone downstream exactly like a gate that passed. `soft:` was refused while this did the same thing more completely: soft demotes one rule's findings, this removes a whole tool.
A built-in that genuinely needs different arguments is configured through `config:`, which merges into what the tool reads rather than replacing what the tool IS.
Types ¶
type Config ¶
type Config struct {
Analyzers map[string]map[string]StringList `yaml:"analyzers"`
Config map[string]Overlay `yaml:"config"`
Define map[string]RunnerSpec `yaml:"define"`
Format string `yaml:"format"`
Runners StringList `yaml:"runners"`
}
Config is one configuration layer (global or repo). It holds per-tool config overlays keyed by runner name (the `config:` block), each deep-merged onto that tool's own base config file at run time; the analyzer settings the suite passes through; the runners the layer selects; and any runner it defines.
WHAT IT NO LONGER HOLDS is the point. `soft:`, `soft-baseline:` and `probe:` each lowered what the gate demanded -- report without gating, cap how many findings were allowed, never gate at any count -- and they are refused at parse time now rather than folded here (see reducingKeys). Every setting that survives changes what is LOOKED AT or how a tool is invoked; none changes what a finding means.
func LoadLayers ¶
func LoadLayers(read FileReader, sources ...Layer) ([]Config, error)
LoadLayers reads and parses each existing config path into a layer. A path the reader cannot open is treated as an absent layer and skipped; a path that parses badly is an error.
type FileReader ¶
FileReader reads a file's bytes; injected so config merging is testable without a real base config on disk.
type Layer ¶ added in v0.12.2
type Layer struct {
Path Path
}
Layer is one configuration source. It is a struct with one field on purpose: a layer is a PLACE a configuration comes from, and the set of places is a decision this file owns rather than a shape its callers assemble.
func Layers ¶ added in v0.12.2
Layers is the configuration a run reads: the repository's own .stickler.yaml, and nothing else.
Every input to a verdict now travels with the repository being judged, which is what makes one run reproducible from another machine. Nothing is read from a home directory, an environment variable, or an image -- so there is no file a developer can hold a stale copy of, and no answer that depends on where the binary happened to be run.
type Merger ¶
type Merger struct {
Flag string
BaseDir string
Read FileReader
Temp TempWriter
BaseNames []string
Overlays []Overlay
}
Merger is the generic, per-tool config-merge capability: given a tool's base config filenames, its config-flag prefix, the per-repo overlays, and the repo directory, it produces the tool's --config argument pointing at an effective config (base + overlays). It is reused by every config-file tool — golangci-lint is just one configured instance — so no tool is special-cased in the merge logic.
Read and Temp are the filesystem seams. Both default to the real filesystem when unset, so a caller wiring the production merger supplies neither and only a test names them.
func (Merger) Args ¶
Args builds the tool's config argument: with overlays it merges them onto the base and writes the effective config to a temp file, returning the config flag and a cleanup; with no overlays it returns no extra args (and a no-op cleanup), leaving config discovery to the tool itself — the pre-merge behavior.
type Overlay ¶
Overlay is one configuration layer's overlay for a single tool, taken from that tool's entry under the `config:` block of a .stickler.yaml. It is deep-merged onto the tool's own base config file at run time, so per-repo tool-config deltas live in .stickler.yaml instead of in a divergent, unmanaged base config. A mapping value deep-merges; a scalar or sequence replaces; a mapping written with only add/remove/replace keys mutates the base list (the StringList polymorphism).
type ParserName ¶
type ParserName string
ParserName selects the output parser a runner's stdout is read with.
const ( ParserSticklerJSON ParserName = "stickler-json" ParserGolangciJSON ParserName = "golangci-json" )
Built-in parser names.
type RepoRoot ¶
type RepoRoot string
RepoRoot is the directory whose .stickler.yaml supplies the repository configuration layer.
type Resolved ¶
type Resolved struct {
Analyzers map[string]map[string][]string
Config map[string][]Overlay
Define map[string]RunnerSpec
Format string
Runners RunnerSelection
}
Resolved is the concrete configuration after all layers are folded. Config maps each runner name to the ordered list of its per-layer overlays (global first, repo last); a config-file runner folds them onto its base config in the repo at run time, since that base lives in the repo, not in any stickler layer.
type RunnerName ¶ added in v0.15.0
type RunnerName string
RunnerName is one runner's key in the registry -- the name a .stickler.yaml selects it by, and the name its config overlays are filed under.
type RunnerSelection ¶ added in v0.15.0
type RunnerSelection struct {
// contains filtered or unexported fields
}
RunnerSelection is what the configuration SAYS about which runners run: each layer's directives, in order, held until the default set they apply to is known. It is not a list of names, because the directives cannot be resolved where they are read -- the defaults include runners a repository's own `define:` block adds, which is itself part of the configuration being folded.
Holding them is what makes `remove` mean what it reads as. It used to be applied to an EMPTY base, because no layer states `runners:` and the fold started from nothing; the result was empty, an empty result meant "nothing was configured", and "nothing was configured" meant every default. So `runners: {remove: [yze]}` re-enabled every runner in the registry, yze included, and said nothing about having done so -- a line that reads as a disablement delivering the opposite. Applied to the defaults, a removal removes, and a selection that ends up empty is an error rather than a fresh start.
func Select ¶ added in v0.15.0
func Select(names ...RunnerName) RunnerSelection
Select is an explicit selection, for a caller that has its own list rather than a configuration file: a test, or a command that names its runners. It is a REPLACE, so it answers whatever the defaults are, and Select() with no names selects none.
func (RunnerSelection) Apply ¶ added in v0.15.0
func (s RunnerSelection) Apply(defaults []RunnerName) []RunnerName
Apply resolves the selection against the runners available by default, applying each layer's directives in the order they were folded. A layer that never wrote `runners:` carries no directives and leaves the list alone, so an unconfigured selection is exactly the defaults.
A name that survives twice runs ONCE. `add: [yze]` where yze is already a default appended a second entry, and Build builds one runner per entry, so the tool ran twice and every finding it reports was counted twice -- which is not a disablement but distorts a soft baseline in the direction that hides the next real finding. First occurrence wins, so the order a configuration asked for is the order it gets.
type RunnerSpec ¶
type RunnerSpec struct {
Name string `yaml:"name"`
Format ParserName `yaml:"format"`
Config *Spec `yaml:"config"`
Command []string `yaml:"command"`
Args []string `yaml:"args"`
// Instructions is the argument template that makes this tool emit its own
// rule catalog. It is DATA for the same reason the tool itself is: what a
// tool's rules mean is the tool's to say, and prose about them living in
// stickler's source would be a second source of truth that drifts from the
// analyzer that actually decides. A spec that declares none simply cannot
// explain itself, and is reported as such rather than guessed at.
Instructions []string `yaml:"instructions"`
// Env entries are appended to the subprocess environment, shadowing
// same-keyed ambient values. An entry may carry the `{cache}` placeholder,
// which expands to a fresh per-invocation temporary directory (removed
// after the run) — the hermetic-cache seam.
Env []string `yaml:"env"`
// Requires names the files that must exist at the repository root for this
// runner to have anything to say. A runner whose requirement is absent is
// SKIPPED and the skip is reported; it is not run and then forgiven.
//
// It is data for the same reason the command is. golangci-lint cannot load a
// package pattern in a tree with no go.mod -- it exits 7 with "directory
// prefix . does not contain main module" and takes the whole run with it --
// so every docs repository in the fleet failed its gate on a fact about the
// Go toolchain rather than about the repository. A runner that declares what
// it needs can be absent from a tree without being an error, and a tool
// added later declares its own requirement without touching this file.
Requires []string `yaml:"requires"`
}
RunnerSpec is the declarative definition of a tool stickler runs: its command, its argument template (with `{root}`/`{config}` placeholders), the parser its stdout is read with, and optionally how its config file is wired. A tool is data here, not code — adding one is configuration, not a recompile. Fields are ordered for struct-field alignment (slices last); the YAML schema is unaffected since decoding is by tag.
type Spec ¶
Spec declares, as data, how a tool takes its configuration file: the base config filename candidates (first found wins) the stickler overlays are merged onto, and the flag template (`{path}` substituted) that passes the effective config. It carries no tool-specific behavior — the merge is generic.
type StringList ¶
type StringList struct {
// contains filtered or unexported fields
}
StringList is a list-valued setting in one configuration layer. It merges onto the value accumulated from lower layers either by replacing it (when written as a YAML sequence) or by adding and removing entries (when written as a mapping with add/remove/replace keys). An absent setting leaves the lower value intact.
func (*StringList) UnmarshalYAML ¶
func (l *StringList) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts a sequence (replace) or an add/remove/replace mapping, rejecting any unknown mapping key (a config typo such as `addd`). Every other node kind is named explicitly rather than swept into the default: a scalar where a list belongs is the most likely way to mis-write this setting, and it must be refused rather than silently accepted as an empty list. The pointer receiver and *yaml.Node parameter are dictated by the yaml.Unmarshaler interface, which a polymorphic (sequence-or-mapping) setting must implement.
type TempWriter ¶
TempWriter writes data to a fresh temporary file and returns its path plus a cleanup that removes it; injected so the effective-config write is testable.