autoconfigure

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 10 Imported by: 0

README

linter-autoconfigure-sdk

Shared foundation for linter auto-configuration tools — config round-trip, finding emission for config issues, and a provider spec for BuildFlow integration.

CI Go Reference Go Report Card

pkg.go.dev


Why?

Two existing auto-configurers — golangci-lint-auto-configure and oxlint-auto-configure — diverge on their domain-specific concepts (Go project shape vs JS framework detection, 4-tier priority enum vs profile presets). Those differences are legitimate and stay in each tool.

What they reinvent identically is the surrounding plumbing:

Concern Before (per tool) After (this SDK)
Read/write config files Each tool hand-wraps os.ReadFile + parse + error handling ReadConfig(path) / LoadJSON[T](path) / SaveJSON(path, v)
Emit findings for config issues Each tool maps priority → Severity, fix → Suggestion, by hand FindingFromIssue(tool, ConfigIssue{...})
Wire into BuildFlow as Detector + Repairer Each tool writes its own adapter ProviderFromSpec → canonical toolsdk.Spec (go-finding)

linter-autoconfigure-sdk owns that plumbing once. Adding a third auto-configurer (e.g. biome-auto-configure) becomes a config-schema exercise, not a from-scratch build.


Installation

go get github.com/larsartmann/linter-autoconfigure-sdk

Resolves to the latest tagged release (v0.1.0 as of 2026-09-10); pin @v0.1.0 explicitly for reproducible builds.

Requires Go 1.26+ with GOEXPERIMENT=jsonv2 set: go-finding imports encoding/json/v2, which is experimental in Go 1.26 and standard in Go 1.27. Either export GOEXPERIMENT=jsonv2 or use a direnv-based .envrc.

Peer dependencies: the latest go-finding and go-atomic-write modules.


Usage

Read a config file
data, err := autoconfigure.ReadConfig(".golangci.yml")
// YAML parsing stays in each tool (different libraries: yaml.v3 vs go-yaml).
// This helper covers the shared read + existence check.
Round-trip JSON configs
type oxlintConfig struct {
    Plugins []string `json:"plugins"`
}

cfg, err := autoconfigure.LoadJSON[oxlintConfig](".oxlintrc.json")
// ... mutate cfg ...
changed, err := autoconfigure.SaveJSON(".oxlintrc.json", cfg) // creates parent dirs; changed=false when content already matched
Emit findings for config issues
issues := []autoconfigure.ConfigIssue{
    {
        Rule:       "missing-linter",
        Message:    "errcheck is not enabled",
        Severity:   finding.SeverityWarning,
        File:       ".golangci.yml",
        Line:       5,
        Suggestion: "add errcheck to enabled linters",
    },
}
findings := autoconfigure.FindingsFromIssues("golangci-autoconfigure", issues)
// → []finding.Finding with FixStrategySuggest attached where Suggestion != ""
BuildFlow provider shape
spec := autoconfigure.ProviderSpec{
    Name:        "golangci-autoconfigure",
    Description: "Optimize .golangci.yml",
    ConfigFile:  ".golangci.yml",
    Analyze: func(ctx context.Context) ([]autoconfigure.ConfigIssue, error) {
        // inspect config, return issues
    },
    Repair: func(ctx context.Context) (string, error) {
        // rewrite config, return description of changes
    },
}

ProviderFromSpec(spec) wraps this as the canonical BuildFlow provider contract — go-finding's toolsdk.Spec — ready for toolsdk.Register (adjust its Trigger / DependsOn on the returned value first if needed). The underlying analyze/repair closures also work standalone with no BuildFlow wiring.

provider, err := autoconfigure.ProviderFromSpec(spec)
// provider.Detect implements finding.Detector (issues become findings)
// provider.Repair implements toolsdk.Repairer (nil when spec.Repair is nil:
// the canonical suggest-only signal)

API

Config I/O
Function Signature Purpose
ReadConfig(path) ([]byte, *ConfigError) Read raw bytes; YAML parsing stays tool-specific
LoadJSON[T](path) (*T, *ConfigError) Read + unmarshal a JSON config
SaveJSON(path, v) (changed bool, *ConfigError) Idempotent + crash-durable atomic write of indented JSON; creates parent dirs, skips the write when content is unchanged, and reports whether a write happened

All three return a *ConfigError (implements error) whose Op (typed: OpRead, OpUnmarshal, OpMarshal, OpMkdir, OpWrite), Path, and Err fields describe the failure. The wrapped cause is reachable via errors.Is / errors.AsType / errors.Unwrap against a *ConfigError (e.g. errors.Is(err, fs.ErrNotExist)), so callers can distinguish missing files from parse or I/O failures without parsing error strings.

Finding emission
Function Signature Purpose
FindingFromIssue(tool, issue) (finding.Finding, error) Convert one ConfigIssue to a finding.Finding (suggest-strategy auto-attached when Suggestion != "")
FindingsFromIssues(tool, issues) ([]finding.Finding, error) Slice version; propagates conversion errors
BuildFlow integration
Function Signature Purpose
ProviderFromSpec(spec) (toolsdk.Spec, error) Convert a ProviderSpec into go-finding's canonical toolsdk.Spec for toolsdk.Register; validates required fields
(*ProviderSpec).HasRepair() bool Whether the spec supports auto-repair
Types
Type Purpose
ConfigError {Op, Path, Err} — typed failure for config I/O; Op is a typed enum; supports Unwrap/Is/As for full error-chain traversal
ConfigIssue {Rule, Message, Severity, File, Line, Suggestion}Rule is finding.RuleName, File is finding.FilePath
ProviderSpec {Name, Description, ConfigFile, Analyze, Repair} — auto-configurer declaration; ConfigFile is finding.FilePath; HasRepair() reports repair support

Design notes

  • YAML parsing is NOT in this SDK. The two existing tools use different YAML libraries (yaml.v3 vs go-yaml) with different semantics. Forcing one would create friction. The SDK covers the shared byte-level read; YAML unmarshaling stays in each tool.
  • Branded types are adopted consistently. ConfigIssue.Rule is finding.RuleName, File is finding.FilePath, ProviderSpec.ConfigFile is finding.FilePath, and FindingFromIssue takes finding.ToolName. The SDK is coupled to go-finding; taking the branded types buys compile-time safety at no extra cost.
  • FindingFromIssue auto-attaches FixStrategySuggest when Suggestion != ""; otherwise sets FixStrategyNone explicitly to avoid the empty-string zero-value split brain. When Line == 0, a file-level position (finding.FilePos) is used instead of fabricating a line number.
  • No ProjectType enum. The two existing tools have incompatible concepts (Go shape vs JS framework); sharing one enum would force false convergence. Each tool keeps its own detection layer.

Consumers

Active:

  • oxlint-auto-configure — builds its BuildFlow provider via ProviderFromSpec (first migrated consumer, 2026-09-11).
  • golangci-lint-auto-configure — emits validate-command health findings via FindingFromIssue (2026-09-11). Its linter-recommendation conversion (missing-linter findings with per-linter categories/tags) intentionally stays app-side: those carry domain metadata ConfigIssue does not model.

Future: biome-auto-configure, etc.

Until the next SDK release is tagged, both consumers track this repo via a local replace directive; drop the replace and go get the tagged version once it is published.

Status

v0.1.0 — first tagged release. The config round-trip and finding-emission helpers have breaking signatures (typed Op enum, branded types, (Finding, error) and (bool, *ConfigError) returns) — breaking changes remain acceptable until v1. BuildFlow wiring is anchored to go-finding's canonical toolsdk contract (v1.10.0+). Requires GOEXPERIMENT=jsonv2 on Go 1.26 (see Installation).

License

MIT — see LICENSE.

Documentation

Overview

Package autoconfigure provides the shared foundation for linter auto-configuration tools (golangci-lint-auto-configure, oxlint-auto-configure, and future additions like biome-auto-configure).

The two existing tools diverge on their domain-specific concepts — Go project shape (CLI/Library/Web) vs JS framework (React/Next/Vue) — and on their priority models (4-tier enum vs profile presets). Those differences are legitimate and stay in each tool.

What they reinvent identically is the surrounding plumbing:

  • Reading and writing YAML/JSON config files (round-trip)
  • Emitting findings for config issues (priority → Severity, fix → suggestion)
  • Wiring into BuildFlow as a Detector + Repairer

This package owns that plumbing once. Adding a third auto-configurer becomes a config-schema exercise, not a from-scratch build.

Build with Go 1.26 requires GOEXPERIMENT=jsonv2 (encoding/json/v2 becomes standard in Go 1.27).

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNameRequired        = errors.New("autoconfigure: ProviderFromSpec: Name must not be empty")
	ErrDescriptionRequired = errors.New("autoconfigure: ProviderFromSpec: Description must not be empty")
	ErrAnalyzeRequired     = errors.New("autoconfigure: ProviderFromSpec: Analyze must not be nil")
)

Validation sentinels returned by ProviderFromSpec when a required field is missing. The messages are identical to the pre-sentinel dynamic errors, so callers matching on text keep working; new callers should match with errors.Is instead.

View Source
var ErrNoRepair = errors.New("autoconfigure: tool does not support auto-repair")

ErrNoRepair is the pre-bridge sentinel for "this tool does not support auto-repair", kept so pre-v1 code referencing it keeps compiling.

Deprecated: the canonical suggest-only signal is structural. ProviderFromSpec leaves toolsdk.Spec.Repair nil when ProviderSpec.Repair is nil, and BuildFlow reads that directly. This SDK never returns ErrNoRepair, and returning it from a custom Repairer is a repair failure for BuildFlow, not a suggest-only signal. Removed at v1.

Functions

func FindingFromIssue

func FindingFromIssue(toolName finding.ToolName, issue ConfigIssue) (finding.Finding, error)

FindingFromIssue converts a ConfigIssue to a finding.Finding with the given tool name. The fix strategy is issue.FixStrategy when set; otherwise a non-empty Suggestion yields FixStrategySuggest (so BuildFlow's repair loop can surface it) and an empty one yields FixStrategyNone, set explicitly to avoid the empty-string zero-value split brain.

When issue.Line is 0 (unknown), the finding receives a file-level Position via finding.FilePos rather than a fabricated line number.

Example
issue := ConfigIssue{
	Rule:       finding.RuleName("missing-linter"),
	Message:    "errcheck is not enabled",
	Severity:   finding.SeverityWarning,
	File:       finding.FilePath(".golangci.yml"),
	Line:       5,
	Suggestion: "add errcheck to enabled linters",
}

f, err := FindingFromIssue(finding.ToolName("golangci-autoconfigure"), issue)
if err != nil {
	fmt.Println("error:", err)

	return
}

fmt.Println(f.Rule, f.Severity, f.FixStrategy)
Output:
missing-linter warning suggest

func FindingsFromIssues

func FindingsFromIssues(toolName finding.ToolName, issues []ConfigIssue) ([]finding.Finding, error)

FindingsFromIssues converts a slice of ConfigIssues to findings. If any issue fails to convert, the entire batch fails and the cause is wrapped.

func ProviderFromSpec

func ProviderFromSpec(spec ProviderSpec) (toolsdk.Spec, error)

ProviderFromSpec converts a ProviderSpec into the canonical BuildFlow provider contract: go-finding's toolsdk.Spec (module github.com/larsartmann/go-finding/toolsdk). The result can be handed to toolsdk.Register for BuildFlow discovery, or its Trigger / DependsOn fields can be adjusted first — the returned Spec is a plain value.

Field mapping:

  • Name, Description pass through verbatim; Name also becomes the tool name stamped onto every finding the Detect adapter emits.
  • ConfigFile becomes Inputs (the config file is what the tool reads).
  • Analyze is wrapped as a finding.Detector that converts each ConfigIssue via FindingFromIssue.
  • A non-nil Repair is wrapped as a toolsdk.Repairer; a nil Repair stays nil, the canonical signal for a suggest-only tool (suggestions still flow through findings carrying FixStrategySuggest).
  • Trigger and DependsOn have no ProviderSpec equivalent and stay zero; set them on the returned Spec when needed.

Validation errors name the offending field: Name, Description, and Analyze are all required.

Example
spec := ProviderSpec{
	Name:        "golangci-autoconfigure",
	Description: "keeps .golangci.yml aligned with the project shape",
	ConfigFile:  ".golangci.yml",
	Analyze: func(ctx context.Context) ([]ConfigIssue, error) {
		return []ConfigIssue{{Rule: "missing-linter", Message: "errcheck is not enabled"}}, nil
	},
	Repair: func(ctx context.Context) (string, error) {
		return "enabled errcheck", nil
	},
}

provider, err := ProviderFromSpec(spec)
if err != nil {
	fmt.Println("error:", err)

	return
}

fmt.Println(provider.Name, provider.Inputs, provider.Repair != nil)
fmt.Println(provider.Detect.Name())
Output:
golangci-autoconfigure [.golangci.yml] true
golangci-autoconfigure

Types

type ConfigError

type ConfigError struct {
	// Op is the operation that failed.
	Op Op
	// Path is the config file path involved.
	Path string
	// Err is the underlying cause, never nil for a returned ConfigError.
	Err error
}

ConfigError describes a failure while reading, parsing, or writing a linter config file. It wraps the underlying cause with the operation attempted and the file path, so callers can produce precise diagnostics or branch with errors.Is / errors.As without parsing error strings.

Op follows os.PathError's convention: "read", "unmarshal", "marshal", "mkdir", or "write". The underlying cause is reachable via the exported Err field and through the Is / As methods (see below), so both errors.Is(err, fs.ErrNotExist) and errors.AsType[*jsontext.SyntacticError](err) still work against a *ConfigError. (The jsonv2 unmarshaler emits *jsontext.SyntacticError for malformed input; v1's json.SyntaxError is the legacy equivalent.)

func LoadJSON

func LoadJSON[T any](path string) (*T, *ConfigError)

LoadJSON reads and unmarshals a JSON config file into a new *T.

Example
dir, _ := os.MkdirTemp("", "example")
defer func() { _ = os.RemoveAll(dir) }()

path := filepath.Join(dir, ".oxlintrc.json")
_ = os.WriteFile(path, []byte(`{"linters":["errcheck","gofmt"]}`), 0o644)

cfg, err := LoadJSON[exampleLintConfig](path)
if err != nil {
	fmt.Println("error:", err)

	return
}

fmt.Println(cfg.Linters)
Output:
[errcheck gofmt]

func ReadConfig

func ReadConfig(path string) ([]byte, *ConfigError)

ReadConfig reads a config file's raw bytes. YAML parsing is deliberately left to each tool (different YAML libraries: golangci uses yaml.v3 / v4, oxlint may use go-yaml) — this helper covers the shared read + existence check so both tools stop hand-writing os.ReadFile with the same error wrapping.

func SaveJSON

func SaveJSON(path string, v any) (bool, *ConfigError)

SaveJSON marshals v to indented JSON and writes it to path atomically, creating parent directories. The write is idempotent: if the marshalled content is byte-identical to the existing file, the write is skipped entirely (no mtime bump, no spurious diff). Otherwise the file is replaced via an fsync'd temp-file + atomic rename, so a crash cannot truncate the config. Race-safe: a concurrent modification between the content check and the rename surfaces as a non-nil *ConfigError wrapping atomicwrite.ErrConcurrentModification.

The changed return reports whether the file was actually written: false means the on-disk content already matched and the write was skipped. Repair flows use it to distinguish "config updated" from "config already correct".

Indented output is used because linter configs are typically human-edited.

Example
dir, _ := os.MkdirTemp("", "example")
defer func() { _ = os.RemoveAll(dir) }()

path := filepath.Join(dir, "nested", ".oxlintrc.json")

cfg := exampleLintConfig{Linters: []string{"errcheck", "gofmt"}}

changed, err := SaveJSON(path, cfg)
if err != nil {
	fmt.Println("error:", err)

	return
}

fmt.Println("changed:", changed)

data, _ := os.ReadFile(path)
fmt.Println(string(data))
Output:
changed: true
{
  "linters": [
    "errcheck",
    "gofmt"
  ]
}

func (*ConfigError) As

func (e *ConfigError) As(target any) bool

func (*ConfigError) Error

func (e *ConfigError) Error() string

func (*ConfigError) Is

func (e *ConfigError) Is(target error) bool

func (*ConfigError) Unwrap

func (e *ConfigError) Unwrap() error

Unwrap, Is, and As expose the wrapped cause for standard error-chain traversal. Callers can use errors.Is(err, fs.ErrNotExist), errors.AsType[*jsontext.SyntacticError](err), or errors.Unwrap(err) interchangeably.

type ConfigIssue

type ConfigIssue struct {
	// Rule is the issue's rule identifier (e.g. "missing-linter", "wrong-priority").
	Rule finding.RuleName

	// Message describes the problem for the user.
	Message string

	// Severity rates how serious the issue is.
	Severity finding.Severity

	// File is the config file path (for Position).
	File finding.FilePath

	// Line is the 1-based line number in the config file (0 if unknown).
	Line int

	// Suggestion is the recommended fix text (empty if no auto-fix).
	Suggestion string

	// Confidence is the issue's confidence. Zero value (ConfidenceNone == 0)
	// means unset: the finding keeps the builder default (ConfidenceFull),
	// matching pre-extension behavior. Note this makes an explicit
	// ConfidenceNone unrepresentable through ConfigIssue; adjust the finding
	// afterwards if that level is genuinely needed.
	Confidence finding.Confidence

	// FixStrategy overrides the default strategy selection: nil (the default)
	// picks FixStrategySuggest when Suggestion is non-empty and
	// FixStrategyNone otherwise. Set it for issues whose repair is directly
	// applicable (FixStrategyDirect) rather than suggestion-only.
	FixStrategy *finding.FixStrategy
}

ConfigIssue describes a single problem found in a linter config file. Auto-configurers produce a slice of these; FindingFromIssue converts each to a finding.Finding that BuildFlow can aggregate and gate repairs on.

type Op

type Op string

Op identifies the operation that failed during config I/O. It mirrors the convention from os.PathError: a short, lowercase verb.

const (
	OpRead      Op = "read"
	OpUnmarshal Op = "unmarshal"
	OpMarshal   Op = "marshal"
	OpMkdir     Op = "mkdir"
	OpWrite     Op = "write"
)

type ProviderSpec

type ProviderSpec struct {
	Name        string
	Description string
	// ConfigFile is the config file path the tool manages (e.g. ".golangci.yml").
	ConfigFile finding.FilePath
	// Analyze inspects the config and returns issues. The working directory
	// is available via finding.WorkingDirFromContext(ctx).
	Analyze func(ctx context.Context) ([]ConfigIssue, error)
	// Repair, if non-nil, rewrites the config to fix the issues. Returns a
	// human-readable description of what changed. When nil, the spec is
	// suggest-only: ProviderFromSpec leaves toolsdk.Spec.Repair nil, the
	// canonical signal that BuildFlow should not attempt repairs.
	Repair func(ctx context.Context) (string, error)
}

ProviderSpec is the shape an auto-configurer supplies to wire into BuildFlow. It speaks the auto-configurer's domain language: Analyze reports ConfigIssues against the managed config file, Repair describes what was rewritten.

ProviderFromSpec converts a ProviderSpec into the canonical BuildFlow provider contract, go-finding's toolsdk.Spec, ready for toolsdk.Register. The Analyze and Repair closures are also usable standalone without any BuildFlow wiring.

func (ProviderSpec) HasRepair

func (s ProviderSpec) HasRepair() bool

HasRepair reports whether this spec supports auto-repair.

Jump to

Keyboard shortcuts

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