stickler

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 17 Imported by: 0

README

stickler

The gomatic lint runner — a stickler for the rules. It executes a set of analyzer tools (the yze suite, golangci-lint, and others) to completion, normalizes their findings into one diagnostic schema, writes the report to stdout in the chosen format, prints a pass/fail status to stderr, and reports pass/fail via the process exit code. Any finding or any tool error is a stickler failure; every tool still runs to completion first.

stickler [--format human|json|github] [root]
  • Runner model — each tool is an executable plus an output adapter (Runner). yze is read via its native stickler-json (no adapter); golangci-lint's JSON is adapted. New tools are added as more Runners.
  • --formathuman (default, one line per finding), json (the normalized result), github (Actions annotations). SARIF output is planned.
  • Exit code0 only when every tool ran cleanly with zero findings; non-zero on any finding or tool error.

Configuration (layered merge)

stickler is a runner, so its configuration is about configuring the tools it runs. Two layers merge, global first then repo:

  • Global: $XDG_CONFIG_HOME/stickler/config.yaml (or ~/.config/stickler/config.yaml).
  • Repo: .stickler.yaml at the target (or --root).
runners: [yze, golangci-lint]   # which tools to run
format: human                   # default output (overridable by --format)
analyzers:                      # per-analyzer settings (forwarded to yze)
  ptrrecv:
    allow: [mypkg.MyMutexType]

A repo layer can replace, add, or remove relative to the global layer. A list written as a sequence replaces; a list written as a mapping adds/removes:

runners:
  add: [revive]
  remove: [golangci-lint]

Maps (the analyzers tree) deep-merge; each setting's list follows the same replace/add/remove rules. Precedence for output format is --format flag > config > human.

Per-tool config overlays (config:)

A config: block carries a generic, per-tool configuration overlay keyed by runner name. Each overlay is deep-merged onto that tool's own base config file at run time, so per-repo lint deltas live in .stickler.yaml instead of in a divergent, unmanaged base config. The merge is generic — golangci-lint is just one configured tool — and follows the same polymorphism as everything else: a mapping deep-merges, a scalar or sequence replaces, and a mapping written with only add/remove/replace keys mutates the base list.

config:
  golangci-lint:                  # merged onto the repo's managed .golangci.yaml
    linters:
      settings:
        gosec:
          excludes: { add: [G204] }   # add a per-repo exclude on top of the central config

At lint time stickler reads the tool's base config (for golangci-lint, the repo's .golangci.yaml/.golangci.yml), folds the overlays onto it, writes the effective config to a temp file, and runs the tool with --config pointing at it. With no overlay the tool is run as before (its own config discovery). This is how a repo keeps the uniform, managed base config while still expressing the handful of rules it genuinely needs to differ on.

Built on the go-yze diagnostic schema. Forwarding the merged analyzers settings to yze --config, and --fix (delegating to each tool's fixer), are the next steps.

Documentation

Overview

Package stickler is the gomatic lint runner: it executes a set of analyzer tools (the yze suite, golangci-lint, and others) to completion, normalizes their findings into one diagnostic schema, and reports pass/fail via the process exit code. Any finding or any tool error is a stickler failure.

Index

Constants

View Source
const (
	// ErrConfig reports a stickler config file that cannot be parsed.
	ErrConfig errs.Const = "cannot load stickler config"
	// ErrBadListSetting reports a list setting that is neither a sequence nor an
	// add/remove/replace mapping.
	ErrBadListSetting errs.Const = "list setting must be a sequence or an add/remove/replace mapping"
)

Configuration errors.

View Source
const (
	// ErrRunnerFailed reports that a runner could not be run, its config could not
	// be built, or its output could not be parsed — distinct from a clean pass that
	// merely reported findings.
	ErrRunnerFailed errs.Const = "runner failed"
	// ErrExec reports that a subprocess could not be started or exited non-zero; it
	// carries the captured stderr so the failure's real reason is reported.
	ErrExec errs.Const = "command execution failed"
)

Runner failures.

View Source
const ErrRunner errs.Const = "lint runner failed"

ErrRunner wraps a failure from one tool runner, tagged with the runner name.

View Source
const ErrUnknownOutput errs.Const = "unknown output format"

ErrUnknownOutput reports an output format stickler does not support.

Variables

This section is empty.

Functions

func ConfigPaths added in v0.2.0

func ConfigPaths(getenv func(string) string, home HomeDir, repoRoot RepoRoot) []string

ConfigPaths returns the ordered config layer paths: the global config, then the repository's .stickler.yaml.

func DefaultRunnerSpecs added in v0.5.2

func DefaultRunnerSpecs() map[string]RunnerSpec

DefaultRunnerSpecs is the built-in tool set as pure data: yze (native stickler-json, no config file) 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 ExecCommand

func ExecCommand(ctx context.Context, name RunnerName, args ...Arg) ([]byte, error)

ExecCommand is the default Command, executing a real subprocess. On failure the returned error wraps ErrExec with the captured stderr so the underlying reason (config error, panic, load failure) reaches the caller's message.

func Format

func Format(w io.Writer, format OutputFormat, result Result) error

Format writes the result to w in the named format.

func MarshalTree added in v0.3.0

func MarshalTree(tree map[string]any) ([]byte, error)

MarshalTree renders an effective configuration tree as a YAML document.

func MergeSpecs added in v0.5.2

func MergeSpecs(defaults, defined map[string]RunnerSpec) map[string]RunnerSpec

MergeSpecs overlays config-defined runner specs onto the built-in defaults, returning a new map; a defined spec replaces the default of the same name. This is what lets a .stickler.yaml `define:` block add or override a tool without a recompile.

func MergeTree added in v0.3.0

func MergeTree(base map[string]any, overlays []Overlay) map[string]any

MergeTree folds each overlay, in layer order, onto base and returns the effective configuration tree. base is never mutated.

func OSTempWriter added in v0.3.0

func OSTempWriter(data []byte) (string, func(), error)

OSTempWriter is the production TempWriter, writing the effective config to a uniquely-named temp file and returning a cleanup that removes it.

func ParseTree added in v0.3.0

func ParseTree(data []byte) (map[string]any, error)

ParseTree decodes a base config document into a generic tree, or an empty tree when data is empty or null. A malformed document is a configuration error.

Types

type Arg added in v0.2.1

type Arg string

Arg is one command-line argument passed to a runner's binary.

type Command

type Command func(ctx context.Context, name RunnerName, args ...Arg) ([]byte, error)

Command runs an external tool and returns its stdout. A non-nil error includes a non-zero exit; callers that can still parse the stdout (linters exit non-zero when they report findings) treat the output as authoritative.

type Config added in v0.2.0

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"`
	Soft      StringList                       `yaml:"soft"`
}

Config is one configuration layer (global or repo). Config holds per-tool config overlays keyed by runner name (the `config:` block); each is deep-merged onto that tool's own base config file at run time. Soft lists runner names and/or rule identifiers whose findings are reported but do NOT fail the run (a soft-fail ratchet: a whole tool like `yze`, or a single analyzer like `yze/ptrrecv`).

func LoadLayers added in v0.2.0

func LoadLayers(read func(path string) ([]byte, error), paths ...string) ([]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 ConfigMerger added in v0.3.0

type ConfigMerger struct {
	Flag      string
	BaseDir   string
	Read      FileReader
	Temp      TempWriter
	BaseNames []string
	Overlays  []Overlay
}

ConfigMerger 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.

func (ConfigMerger) Args added in v0.3.0

func (m ConfigMerger) Args() ([]Arg, 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 ConfigSpec added in v0.5.2

type ConfigSpec struct {
	Flag string
	Base []string
}

ConfigSpec 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 FileReader added in v0.3.0

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 HomeDir added in v0.2.1

type HomeDir string

HomeDir is the current user's home directory, the base for the default global config location.

type OutputFormat

type OutputFormat string

OutputFormat names how a Result is rendered.

const (
	OutputHuman  OutputFormat = "human"
	OutputJSON   OutputFormat = "json"
	OutputGitHub OutputFormat = "github"
	OutputSARIF  OutputFormat = "sarif"
)

The output formats stickler supports.

type Overlay added in v0.3.0

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 Parser added in v0.5.2

type Parser func(out []byte) ([]goyze.Diagnostic, error)

Parser turns a tool's stdout into normalized diagnostics. A non-nil error means the tool self-reported a fatal problem (bad config, internal error). This is the only per-tool code in the runner layer.

type ParserName added in v0.5.2

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 added in v0.2.1

type RepoRoot string

RepoRoot is the directory whose .stickler.yaml supplies the repository configuration layer.

type Resolved added in v0.2.0

type Resolved struct {
	Analyzers map[string]map[string][]string
	Config    map[string][]Overlay
	Define    map[string]RunnerSpec
	Format    string
	Runners   []string
	Soft      []string
}

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 added in v0.2.0

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 Result

type Result struct {
	Diagnostics []goyze.Diagnostic
	Errors      []error
}

Result aggregates every runner's findings and failures from one stickler pass.

func Orchestrate

func Orchestrate(ctx context.Context, root Root, runners []Runner) Result

Orchestrate runs every runner concurrently to completion over root, collecting all diagnostics and wrapping any runner error with its name. One runner's failure never prevents the others from running, and the merged result is deterministic: diagnostics are grouped by runner order then sorted within the pass.

func (Result) Failed

func (r Result) Failed(soft Soft) bool

Failed reports whether the pass should fail the build: any runner error, or any HARD (not soft-listed) diagnostic. Soft diagnostics are still reported by the formatter; they just do not gate.

type Root added in v0.2.1

type Root string

Root is the directory or package pattern a runner analyzes (e.g. "./..." or a path); it is the target every Runner operates over.

type Runner

type Runner interface {
	Name() string
	Run(ctx context.Context, root Root) ([]goyze.Diagnostic, error)
}

Runner executes one analyzer tool over a root directory and returns its findings as normalized diagnostics.

func BuildRunners added in v0.2.0

func BuildRunners(command Command, specs map[string]RunnerSpec, names []string, ctx RunnerContext) []Runner

BuildRunners resolves the named runners against the spec registry (built-in defaults overlaid with any config-defined specs) into generic runners. Names default to every defined spec. An unknown name, or a spec naming an unknown parser, is skipped.

type RunnerContext added in v0.3.0

type RunnerContext struct {
	Config  map[string][]Overlay
	BaseDir string
}

RunnerContext carries what config-file runners need to build their effective configuration: the repo directory holding the base config files and the resolved per-tool overlays keyed by runner name.

type RunnerName added in v0.2.1

type RunnerName string

RunnerName identifies a runner's binary (the first word of its command).

type RunnerSpec added in v0.5.2

type RunnerSpec struct {
	Name    string      `yaml:"name"`
	Format  ParserName  `yaml:"format"`
	Config  *ConfigSpec `yaml:"config"`
	Command []string    `yaml:"command"`
	Args    []string    `yaml:"args"`
}

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 Soft added in v0.4.0

type Soft []string

Soft is the set of soft-fail identifiers: a diagnostic whose tool (e.g. "yze") or rule (e.g. "yze/ptrrecv") is listed is reported but does NOT fail the run. It is the rollout ratchet — a tool starts soft and is moved to hard, whole or analyzer by analyzer, as a repo cleans up.

type StringList added in v0.2.0

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 added in v0.2.0

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`). 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 added in v0.3.0

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.

Directories

Path Synopsis
cmd
stickler command
Command stickler is the gomatic lint runner: it executes the yze suite and golangci-lint to completion, normalizes their findings, writes them to stderr in the chosen format, and exits non-zero if any finding or tool error occurred.
Command stickler is the gomatic lint runner: it executes the yze suite and golangci-lint to completion, normalizes their findings, writes them to stderr in the chosen format, and exits non-zero if any finding or tool error occurred.

Jump to

Keyboard shortcuts

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