config

package
v0.15.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 7 Imported by: 0

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

View Source
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.

View Source
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

type FileReader func(path string) ([]byte, error)

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

func Layers(repoRoot RepoRoot) []Layer

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

func (m Merger) Args() ([]string, func(), error)

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

type Overlay map[string]any

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 Path

type Path string

Path is the path of one stickler config layer.

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.

func Resolve

func Resolve(layers ...Config) Resolved

Resolve folds the layers in order (global first, repo last), applying each layer's add/remove/replace directives onto the accumulated result.

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

type Spec struct {
	Flag string   `yaml:"flag"`
	Base []string `yaml:"base"`
}

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

type TempWriter func(data []byte) (path string, cleanup func(), err error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL