finding

package module
v1.10.0 Latest Latest
Warning

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

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

README

go-finding

A Go library providing a unified data model and pipeline for static analysis tools.

CI Go Reference codecov Go Version

Why

Seven tools detect issues. Zero tools route them to remediation.

Each tool invents its own types for findings. There is no standardized way to apply fixes. The manual loop — run tool, read output, fix, re-run — is slow and error-prone.

go-finding solves this with:

  • Unified Finding type — Common model for all static analysis tools
  • Pipeline — Automated detect → triage → fix → verify loop with retry, partial success, and observability hooks
  • SARIF 2.1.0 — Standard interchange format for CI/CD integration
  • LSP diagnostics — IDE integration out of the box
  • Flight recorder — Go execution trace capture for pipeline diagnostics, with slow-stage auto-snapshot and manual checkpoints. See FlightRecorder Guide.
  • Finding groupsGroupID ties related findings together (e.g. clone groups), round-tripping through JSON, SARIF, and LSP
  • Per-finding fix outcomes — Know exactly what happened to every fix: applied, refused, conflict, invalid, or failed — with scoped per-file rollback by default

Installation

Prerequisite — Go 1.26+ with GOEXPERIMENT=jsonv2. This library uses encoding/json/v2 (experimental in Go 1.26). Enable it once globally:

go install golang.org/dl/go1.26@latest && go1.26 download   # if not already on 1.26
go env -w GOEXPERIMENT=jsonv2

Without this, go get fails with build constraints exclude all Go files. When Go stabilizes json/v2 (expected 1.27+), this step disappears.

Core types only:

go get github.com/larsartmann/go-finding

With pipeline (adds x/sync, gogenfilter):

go get github.com/larsartmann/go-finding/pipeline

CLI tool:

go install github.com/larsartmann/go-finding/cmd/go-finding@latest

Each module is an independent Go module and is versioned with its own git tag:

Module Import path Tag
Core github.com/larsartmann/go-finding v1.8.0
Pipeline github.com/larsartmann/go-finding/pipeline pipeline/v*
Analysis github.com/larsartmann/go-finding/analysis analysis/v*
CLI github.com/larsartmann/go-finding/cmd/go-finding cmd/go-finding/v*

See docs/release-procedure.md for details.

Quick Start

Recommended: Use the Builder API for validated findings with fix strategies, categories, and metadata. The Builder validates at construction time, preventing invalid findings from entering your pipeline.

package main

import (
    "fmt"
    "log"

    "github.com/larsartmann/go-finding"
)

func main() {
    // Builder API — validated, fluent, recommended for all new code.
    f, err := finding.NewBuilder(
        finding.RuleName("unused-var"), finding.ToolName("my-tool"),
        "variable x is unused",
        finding.SeverityWarning,
        finding.Pos("main.go", 42, 5),
    ).
        WithCategory(finding.CategoryCorrectness).
        WithConfidence(finding.ConfidenceHigh).
        WithFixStrategy(finding.FixStrategySuggest).
        WithSuggestion("remove the unused variable").
        Build()
    if err != nil {
        log.Fatal(err)
    }

    report := finding.NewReport(finding.ToolInfo{Name: "my-tool", Version: "1.0.0"})
    report.AddFinding(f)
    report.ComputeSummary()

    sarif, _ := report.ToSARIF()
    fmt.Println(string(sarif))
}

Lower-level: finding.NewFinding(...) skips validation and is intended for cases where you construct findings from trusted sources. Prefer NewBuilder unless you have a specific reason to skip validation.

One-shot detection (no fix loop)

For tools that only need detection without the fix pipeline:

detectors := []pipeline.Detector{myDetector1, myDetector2}
findings, err := pipeline.Detect(ctx, detectors...)
// → []finding.Finding, ready for Report/SARIF output
Applying fixes to in-memory content
result, applied := pipeline.ApplyToContent(fileContent, findings)
// result = modified []byte, applied = count of successful fixes
Per-finding fix outcomes (v1.7.0)

Know exactly what happened to each finding during a fix run — no more guessing whether a fix was applied, refused, conflicted, or failed:

result := engine.ApplyWithOutcomes(content, fixes)
for _, oc := range result.Outcomes {
    fmt.Println(oc.Finding.ID, oc.Status) // applied / no-change / refused / conflict / invalid / failed
    if oc.Err != nil {
        fmt.Println("  cause:", oc.Err)
    }
}

// Aggregate: pipeline.Metrics.RecordOutcome + OutcomeCounts feed the CLI's
// "Fix outcomes:" summary. Full guide: docs/guides/outcomes.md

Plan-before-apply: applier.ApplyDryRun(ctx, findings) (v1.8.0) returns the same report shape with zero writes.

Core Types

Type Purpose
Finding A single issue: ID, rule, severity, position, fix strategy
Report Thread-safe container for findings with summary statistics
Severity info / warning / error / critical
FixStrategy none / suggest / direct / ai
Position File, line, column location
Range Start and end positions with geometric operations
Category security, style, performance, correctness, etc.
Tag Multi-label classification (security, performance, ...)
Confidence Named float64 with IsValid(), Clamp(), String()
Suppression Expiring suppression with IsActive(now)
ID/RuleName/ToolName/FilePath Branded string types preventing field mixups at compile time

API Overview

┌─────────────────────────────────────────────────────────────┐
│  Detectors (govet, staticcheck, custom)                     │
│         ↓                                                   │
│  []finding.Finding                                          │
│         ↓                                                   │
│  Report (thread-safe container)                             │
│         ↓                                                   │
│  Filter / Group / Merge / Correlate / Diff                  │
│         ↓                                                   │
│  SARIF / JSON / LSP / Text / Markdown / CSV / TSV             │
└─────────────────────────────────────────────────────────────┘

Key packages:

  • finding — core types, filtering, grouping, merging, SARIF, LSP, formatting
  • pipeline — detect → triage → fix → verify loop
  • analysisgo/analysis.DiagnosticFinding conversion
  • cmd/go-finding — CLI tool with JSON/YAML config

Filtering

errors := finding.Filter(findings, finding.BySeverity(finding.SeverityError))

autoFixable := finding.Filter(findings, finding.ByFixStrategy(finding.FixStrategyDirect))

important := finding.Filter(findings,
    finding.BySeverityAtLeast(finding.SeverityWarning),
    finding.NotSuppressed,
    finding.WithFix,
)

byFile := finding.GroupByFile(findings)
bySeverity := finding.GroupBySeverity(findings)

Merging

Combine reports from multiple tools with deduplication:

merged := finding.Combine([]*finding.Report{govet, staticcheck, custom},
    finding.WithDeduplication(true),
)

Cross-tool correlation finds related findings:

correlations := finding.Correlate(allFindings)
for _, c := range correlations {
    fmt.Printf("%.1f: %s\n", c.Score, c.Reason)
}

Pipeline

The pipeline package runs a detect → process → triage → apply → verify loop:

detect ──→ process ──→ triage ──→ apply ──→ verify
  ↑                                      │
  └────────────── repeat ────────────────┘ (until stable or max iterations)

Each iteration runs registered detectors, applies FindingTransformer transforms, categorizes findings by FixStrategy, applies direct fixes with conflict detection, and optionally re-runs detectors to verify.

detector := pipeline.NamedDetectorFunc("my-tool", func(ctx context.Context) ([]finding.Finding, error) {
    return []finding.Finding{...}, nil
})

cfg := pipeline.Config{
    MaxIterations:     5,
    ParallelDetectors: true,
    Timeout:           10 * time.Minute,
    VerifyAfterFix:    true,
    GracefulDegradation: true,
    DryRun:            false,
}

p, err := pipeline.New(cfg, ".", detector)
if err != nil {
    log.Fatal(err)
}
result, err := p.Run(context.Background())

fmt.Printf("Iterations: %d, Findings: %d, Stable: %v\n",
    result.TotalIterations, result.TotalDetected, result.Stable())
Pipeline Features
Feature Description
Parallel detection errgroup-based concurrent detector execution
Finding processors Composable transforms between detection and triage
Custom triage Config.TriageFunc overrides default categorization
Byte-level conflict detection Config.ByteLevelConflictDetection filters overlapping edits
Fix provider chain Offset → Line → Substring, plus custom AST-aware providers
Fix application Byte-level edits with backup/rollback
Verification Re-run detectors to confirm fixes
Retry Exponential backoff for flaky detectors
Partial success Continue with findings from successful detectors
Metrics Optional timing and count collection with snapshots
Structured logging *slog.Logger integration
Stage hooks StageHooks before/after events with abort (replaces OnStage)
Dry run Detect + triage without applying fixes
Generated file filter Removes findings from auto-generated Go source files
Flight recorder Chrome Trace Event export for pipeline stage timing visualization
Custom Detector
type MyDetector struct{}

func (d *MyDetector) Name() string { return "my-detector" }

func (d *MyDetector) Detect(ctx context.Context) ([]finding.Finding, error) {
    findings := []finding.Finding{
        finding.NewFinding("RULE001", "my-detector", "issue found",
            finding.SeverityError,
            finding.Position{File: "main.go", Line: 10}, finding.ConfidenceHigh),
    }
    return findings, nil
}
Fix Providers

The pipeline resolves findings to byte-level edits via a composable provider chain:

// Default chain: OffsetProvider → LineProvider → SubstringProvider
applier, err := pipeline.NewFixApplier(rootDir)
defer applier.Close()

// Custom providers for AST-aware transformations
applier, err = pipeline.NewFixApplierWithProviders(rootDir, myASTProvider)

A built-in Go AST provider disambiguates BeforeCode occurrences structurally:

import "github.com/larsartmann/go-finding/pipeline/goast"

applier, err := pipeline.NewFixApplierWithProviders(rootDir, &goast.Provider{})

The SubstringProvider fallback uses nearest-position matching (line + column distance) to disambiguate multiple occurrences of the same text.

Diff and Compare
result := finding.Diff(before, after)
fmt.Println(result.Stats()) // "+2 -1 ~0 =3"
fmt.Println(result.HasChanges())

Tool Adapters

The ToolAdapter[O] generic wraps any external tool into a Detector:

detector := finding.NewToolAdapter("staticcheck",
    func(ctx context.Context) ([]byte, error) {
        return exec.CommandContext(ctx, "staticcheck", "-json", "./...").Output()
    },
    func(data []byte) ([]staticcheckIssue, error) {
        var issues []staticcheckIssue
        return issues, json.Unmarshal(data, &issues)
    },
    func(issue staticcheckIssue) finding.Finding {
        return finding.NewFinding(issue.Rule, "staticcheck", issue.Message,
            finding.SeverityError, finding.Pos(issue.File, issue.Line, 0),
            finding.ConfidenceHigh)
    },
)
CategoryForLinter

70+ built-in linter→category mappings:

cat := finding.CategoryForLinter("SA1000") // CategoryCorrectness
cat := finding.CategoryForLinter("G104")  // CategorySecurity
finding.RegisterLinterCategory("MY-RULE", finding.CategoryPerformance)

SARIF

// Export
sarifJSON, err := report.ToSARIF()

// Parse SARIF from another tool
findings, err := finding.FindingsFromSARIF(sarifJSON)

Round-trip fidelity is preserved:

  • SeverityCritical maps to SARIF "error" (no critical level in SARIF 2.1.0); the original severity is stored in Properties["go-finding/severity"]
  • Finding.Snippet round-trips via region.snippet
  • RelatedRef.Range end positions round-trip via related location regions
  • Non-standard metadata preserved in the property bag with go-finding/* prefix

LSP Diagnostics

lspDiag := f.ToLSP()

// Related information includes proper LSPRange when RelatedRef.Range is set
for _, rel := range lspDiag.RelatedInformation {
    fmt.Println(rel.Range.Start.Line, rel.Range.End.Line)
}

// From LSP diagnostic
f := finding.FromLSP("file:///path/to/file.go", lspDiag)

LSP diagnostic tags (Unnecessary, Deprecated) are preserved in Finding.Metadata["go-finding/lsp-diagnostic-tags"]. Related information end positions reconstruct RelatedRef.Range.

go/analysis Integration

// From go/analysis Diagnostic (in analysis subpackage)
f := analysis.FromDiagnostic(diag, pass.Fset, "my-analyzer", "RULE001")

// Note: Converting back to analysis.Diagnostic is supported via ToDiagnostic().

JSON

// Serialize a single finding
data, err := f.LineJSON()

// Deserialize with validation (returns value type)
f, err := finding.FromJSON(data)

// Line-delimited JSON stream
line, err := f.LineJSON()

// Pretty-printed report
data, err := report.PrettyJSON()

Error Handling

Structured errors with categories:

err := finding.NewValidationError("invalid severity", nil)
err := finding.NewIOError("read file", cause).WithPosition(pos)
err := finding.NewConflictError("overlapping fixes", cause)

finding.IsFindingError(err)
finding.CategoryOf(err) // "validation", "io", "conflict", etc.

CLI

go install github.com/larsartmann/go-finding/cmd/go-finding@latest

go-finding -format sarif -output results.sarif
go-finding -format json -config config.yaml
go-finding -format csv -output findings.csv
go-finding -format markdown -min-severity warning
go-finding -filter-generated -fix-provider go-ast

Key flags: -format (text/markdown/csv/tsv/json/sarif), -min-severity, -config, -filter-generated, -fix-provider, -byte-level-conflict. Use -help for the full list.

Development

nix run .#test                     # Run tests (all modules)
nix run .#test-race                # Run tests with race detector
nix run .#bench                    # Run benchmarks
nix run .#lint                     # Lint

Requires GOEXPERIMENT=jsonv2. The project uses encoding/json/v2 (experimental in Go 1.26). All nix run .#* commands set this automatically. For direct go commands, export it first: export GOEXPERIMENT=jsonv2 && go test -race -count=1 ./...

See CONTRIBUTING.md for guidelines.

Support

go-finding is MIT-licensed and maintained on a best-effort basis by a single author.

  • Bugs & feature requests: open a GitHub Issue.
  • No SLA. Issues and PRs are reviewed as time permits.
  • Security vulnerabilities: see SECURITY.md for private reporting.
  • Supported versions: the latest minor release only. The API has been frozen since v1.0.0; breaking changes require a major version bump.
  • Community: be kind and constructive — see CODE_OF_CONDUCT.md.

Versioning

This project follows Semantic Versioning. The API has been frozen since v1.0.0 (2026-06-24). Breaking changes require a major version bump.

The current version is available programmatically:

fmt.Println(finding.Version) // "1.8.0"

Documentation

Document Purpose
FEATURES.md Honest feature inventory with status
ROADMAP.md Long-term direction and future ideas
TODO_LIST.md Short-term actionable tasks
CHANGELOG.md Versioned change history
docs/USAGE_GUIDE.md Comprehensive usage guide
docs/MIGRATION_v1.0.md v1.0 migration instructions
docs/guides/consumer-migration-v1.7.md Upgrade guide: outcomes, rollback default, groups
docs/guides/outcomes.md Per-finding fix outcomes, rollback semantics, metrics
docs/guides/fix-engine.md Fix engine patterns (providers, edits, conflicts)
docs/guides/fix-providers.md Writing custom fix providers
docs/guides/finding-groups.md Grouping related findings via SARIF/LSP
docs/guides/flight-recorder.md Pipeline trace flight recorder
docs/guides/configuration.md CLI flags, YAML/JSON config, precedence
docs/guides/troubleshooting.md Common errors and fixes
docs/ecosystem.md How go-finding relates to the surrounding SDKs and tools

go-finding is the hub of an ecosystem of SDKs and tools. See docs/ecosystem.md for the full architecture diagram and component comparison.

Ecosystem SDKs (shared plumbing for tools that emit findings):

  • go-linter-sdk — Rule + Registry scaffolding for linters
  • linter-autoconfigure-sdk — Config round-trip + finding emission for auto-configurers
  • go-checker-helpers — Finding builders, fix pipeline, and safe I/O for BuildFlow checkers

Tools using go-finding:

  • art-dupl — Code duplication detection
  • branching-flow — Go code quality analyzer
  • hierarchical-errors — Error handling pattern detector
  • go-auto-upgrade — Dependency upgrade automation

Standards:

  • SARIF 2.1.0 — Static Analysis Results Interchange Format
  • LSP — Language Server Protocol

License

MIT

Documentation

Overview

Package finding provides a unified data model and pipeline for static analysis tools.

The finding package solves the fragmentation problem in Go's static analysis ecosystem where each tool invents its own types for findings. It provides:

  • A common Finding type that all tools can use
  • Standard severity levels (info, warning, error, critical)
  • Named types for Confidence, Category, FixStrategy, Tag, SuppressionKind
  • Position tracking with range support
  • SARIF 2.1.0 output generation and import
  • LSP Diagnostic conversion
  • go/analysis integration (see github.com/larsartmann/go-finding/analysis module)
  • Report merging, deduplication, and cross-tool correlation
  • Diff to compare finding sets
  • Human-readable text and markdown formatting
  • A pipeline for automated detect → triage → fix → verify loops (see github.com/larsartmann/go-finding/pipeline module)
  • Finding grouping via GroupID (e.g. clone groups: N findings for one logical issue)
  • Per-finding fix outcomes and scoped rollback via the pipeline module's FixEngine

Quick Start

Create a finding:

f := finding.Finding{
    ID:       finding.GenerateID("my-tool", "unused-var", finding.Position{File: "main.go", Line: 5}),
    Rule:     "unused-var",
    ToolName: "my-tool",
    Message:  "variable x is unused",
    Severity: finding.SeverityWarning,
    Position: finding.Position{File: "main.go", Line: 5, Column: 2},
}

Or use the Builder API for construction with validation:

f, err := finding.NewBuilder("unused-var", "my-tool", "variable x is unused",
    finding.SeverityWarning, finding.Pos("main.go", 5, 2)).
    WithCategory(finding.CategoryUnused).
    WithConfidence(finding.ConfidenceHigh).
    Build()

Or use BuildOrDefault to skip error handling (returns Finding{} on error):

f := finding.NewBuilder("rule", "tool", "msg", finding.SeverityError, finding.Pos("a.go", 1, 1)).
    BuildOrDefault()

Create a report:

report := finding.NewReport(finding.ToolInfo{Name: "my-tool"})
report.AddFinding(f)
report.ComputeSummary()

Or in one step:

report := finding.NewReportFromFindings(finding.ToolInfo{Name: "my-tool"}, []finding.Finding{f})

Output as SARIF:

sarifJSON, err := report.ToSARIF()

Core Types

The main types are Finding, Report, and supporting named types:

  • Finding: A single issue detected by a tool
  • Report: Thread-safe container for all findings from a tool run
  • Severity: info, warning, error, critical (with comparison operators)
  • Confidence: Named float64 type with IsValid/Clamp/Compare/String/ParseConfidence, range [0.0, 1.0]
  • FixStrategy: none, suggest, direct, ai (ai is reserved)
  • Category: 16 predefined + custom (security, style, performance, etc.)
  • Tag: Multi-label classification (security, bug, deprecated, etc.)
  • Position: File, line, column, offset location
  • Range: Start and end positions with spatial operations (Contains, Overlaps, Adjacent)
  • Suppression: Mark findings as suppressed with kind, reason, and optional expiry
  • FixEdit: Byte-level edit operation (offset, length, replacement)

Validation

Every Finding can be validated with Validate() which returns detailed per-field errors:

if err := f.Validate(); err != nil {
    // err contains joined errors for each invalid field
}

Report and ToolInfo also have Validate methods. Builder calls Validate automatically on Build().

Filtering

Filter findings using composable predicates:

errors := finding.Filter(findings, finding.BySeverity(finding.SeverityError))
autoFixable := finding.Filter(findings, finding.ByFixStrategy(finding.FixStrategyDirect))
byFile := finding.GroupByFile(findings)

Combine with Negate for inverse filters, AnyOf for union:

nonAuto := finding.Filter(findings, finding.Negate(finding.WithFix))
warnOrErr := finding.Filter(findings, finding.AnyOf(
    finding.BySeverity(finding.SeverityWarning),
    finding.BySeverity(finding.SeverityError),
))

FilterInPlace modifies the slice in place (zeroes tail for GC safety). ByConfidence and ByConfidenceAtLeast filter on confidence values.

Merging and Deduplication

Merge reports from multiple tools with configurable deduplication:

merged := finding.Combine(reports,
    finding.WithDeduplication(true),
    finding.WithDeduplicateBy(finding.DeduplicateByPosition),
)

Three deduplication strategies: ByID (exact match), ByPosition (file:line:col), ByRule (rule+position). Combine always deep-clones findings. Report.Merge merges in-place with shallow copy.

Cross-Tool Correlation

Correlate finds related findings across different tools based on file proximity:

correlations := finding.Correlate(allFindings)
for _, c := range correlations {
    fmt.Printf("%s ↔ %s (score: %.2f): %s\n",
        c.FindingIDs[0], c.FindingIDs[1], float64(c.Score), c.Reason)
}

Diff

Compare two finding sets to categorize changes:

result := finding.Diff(before, after)
fmt.Println(result.Stats()) // "+2 -1 ~0 =3"

DiffResult contains Added, Removed, Modified (with before/after pairs), and Unchanged. Use HasChanges() for a quick check.

SARIF 2.1.0

Export and import SARIF format for CI/CD integration:

data, err := report.ToSARIF()                                  // all findings
data, err := report.ToSARIFWithOpts(finding.WithMinSeverity(sev)) // filtered by severity

Import back:

findings, err := finding.FindingsFromSARIF(ctx, data)
findings, err := finding.FindingsFromReader(ctx, reader) // streaming

WriteSARIF/WriteSARIFWithOpts stream directly to io.Writer. Report.WriteTo implements io.WriterTo for io.Copy compatibility.

go-finding-specific properties are preserved in the SARIF property bag for full round-trip fidelity.

LSP Diagnostics

Convert findings to LSP Diagnostics for IDE integration:

diags := f.ToLSP()

Convert back from LSP:

f := finding.FromLSP(uri, lspDiag)

LSP conversion preserves go-finding-specific fields via LSPDiagnosticData on diag.Data: FixStrategy, Confidence, BeforeCode, AfterCode, Suppression, Metadata, Category, Tags, and RelatedFindingIDs are round-tripped. Diagnostic tags (unnecessary, deprecated) are preserved via Metadata.

Error Handling

Structured error types with category-based classification:

err := finding.NewValidationError("missing field", nil)
errors.Is(err, finding.ErrValidation) // true

Five error categories: Validation, IO, Parse, Conflict, Internal. Use IsFindingError, CategoryOf, IsCategory for programmatic handling. FindingError supports WithFinding and WithPosition for attaching context. ErrorCode returns "finding.<category>" for use with go-error-family classification. ErrorFamily maps the category to an errorfamily.Family (Rejection, Conflict, etc.).

Suppression

Findings can be suppressed with optional TTL:

f.Suppression = &finding.Suppression{
    Kind:      finding.SuppressionInSource,
    Rule:      "unused-var",
    Reason:    "intentionally unused in test",
    ExpiresAt: &expiry,
}
f.IsSuppressed()                // true
f.Suppression.IsActive(time.Now()) // true if not expired

Use ActiveFindings() to get only non-suppressed findings from a Report.

Formatting

Human-readable output formats:

finding.FormatText(os.Stdout, findings)     // [SEVERITY] tag + suggestion
finding.FormatTextRich(os.Stdout, findings) // emoji badge + category + 💡 suggestion
finding.FormatTable(os.Stdout, findings)   // severity-badged table
finding.FormatMarkdown(os.Stdout, findings) // markdown table

Convenience APIs (v1.3.0)

Eliminate common boilerplate with these helper functions:

Stamp common fields once, build many findings:

tmpl := finding.NewTemplate("my-linter").
    WithCategory(finding.CategoryStyle).
    WithFixStrategy(finding.FixStrategySuggest)
f1 := tmpl.Build("R1", "msg 1", finding.SeverityInfo, finding.Pos("a.go", 1, 1))
f2 := tmpl.Build("R2", "msg 2", finding.SeverityWarning, finding.Pos("b.go", 2, 3))

For per-finding confidence/suggestion, use Template.Builder (returns *Builder):

f := tmpl.Builder("R1", "msg", finding.SeverityWarning, finding.Pos("a.go", 1, 1)).
    WithConfidence(finding.ConfidenceHigh).
    WithSuggestion("use foo.Bar() instead").
    MustBuild()

Confidence parsing (inverse of String):

c, err := finding.ParseConfidence("high") // → ConfidenceHigh

File-level positions (config files, project checks):

f := finding.NewBuilder("config", "tool", "missing field",
    finding.SeverityError, finding.FilePos("config.yaml")).BuildOrDefault()

Severity mapping from external tools:

sev := finding.SeverityFromLevel("warn", finding.SeverityInfo) // → SeverityWarning
priority := finding.SeverityError.PriorityString()              // → "high"

Simple BeforeCode→AfterCode fixes without the pipeline:

results := finding.ApplySimpleFixes(findingsWithFixes)

External tool integration:

path, err := finding.CheckBinary("golangci-lint")
output, err := finding.RunCmd(ctx, "golangci-lint", "run", "--out-format", "json", "./...")

JSON

JSON serialization with validation:

f, err := finding.FromJSON(data)       // single finding, validates
r, dropped, err := finding.ReportFromJSON(data) // report, drops invalid

PrettyJSON includes all findings; PrettyJSONFiltered excludes suppressed.

ID Generation

Stable, deterministic IDs:

id := finding.GenerateID("tool", "rule", finding.Position{File: "main.go", Line: 5})
parsed := finding.ParseID(id)

IDs are colon-separated with length-prefixed fields to prevent collisions.

Pipeline

The pipeline module (github.com/larsartmann/go-finding/pipeline) provides an automated detect → triage → fix → verify loop. Import it separately:

p, err := pipeline.New(pipeline.Config{
    MaxIterations:     3,
    ParallelDetectors: true,
    Timeout:           5 * time.Minute,
    VerifyAfterFix:    true,
}, rootDir, detector1, detector2)
result, err := p.Run(ctx)

Pipeline features:

  • Configurable iterations with early termination
  • Parallel or sequential detector execution
  • FindingTransformer chain between detection and triage
  • Customizable TriageFunc for categorizing findings
  • Byte-level FixEngine with composable FixProvider chain
  • Conflict detection (position-based or byte-level)
  • Post-fix verification by re-running detectors
  • Retry with exponential backoff for flaky detectors
  • Graceful degradation on detector failures
  • Structured logging via slog
  • Stage and iteration callbacks (StageHook with before/after events)
  • Metrics collection with snapshots
  • Flight recorder (Go execution trace via runtime/trace.FlightRecorder)

Fix Providers

The FixEngine resolves findings to byte-level edits via a provider chain:

  • OffsetProvider: Direct byte offset ranges
  • LineProvider: Line/column positions converted to byte offsets
  • SubstringProvider: BeforeCode text matching (fallback)

Register custom providers for domain-specific transformations:

applier, err := pipeline.NewFixApplierWithProviders(rootDir, myASTProvider)

A Go AST-aware provider (pipeline/goast.Provider) is available for .go files, using go/parser to disambiguate BeforeCode occurrences structurally.

Detector Registry

Register named detector constructors for plugin-style extensibility:

registry := finding.NewDetectorRegistry()
registry.MustRegister("my-tool", func() finding.Detector { ... })
det, err := registry.Build("my-tool")
all, err := registry.BuildAll() // sorted by name

Thread-safe. Use with ConfigFile.ResolveDetectors for config-driven pipelines.

Flight Recorder

The pipeline module includes a flight recorder (pipeline.FlightRecorderHook) that captures Go execution traces for diagnostics. It wraps runtime/trace.FlightRecorder and is registered as a StageHook:

hook, err := pipeline.NewFlightRecorderHook(pipeline.DefaultFlightRecorderConfig())
cfg.StageHooks = append(cfg.StageHooks, hook)

Snapshots are written on demand via hook.Snapshot(ctx, reason) or automatically when a stage exceeds SlowStageThreshold. The CLI exposes -trace, -trace-dir, and -trace-slow flags. YAML/JSON config files support a flightRecorder section with enabled, outputDir, slowStageThreshold, minAge, and maxBytes fields. For a complete walkthrough with examples, see the FlightRecorder guide in the project documentation.

Interval Index

Efficient overlap queries over half-open ranges in O(log n + k):

idx := finding.NewIntervalIndex(intervals)
overlaps := idx.Query(start, end)

Used internally by Correlate for spatial finding correlation.

Streaming Merge

MergeIter yields findings from multiple reports as an iterator, avoiding intermediate slice allocation:

for f := range finding.MergeIter(reports, finding.WithDeduplication(true)) {
    process(f)
}

Converting from go/analysis

Convert from the standard Go analysis framework using the analysis module (github.com/larsartmann/go-finding/analysis), imported separately:

f := analysis.FromDiagnostic(diag, pass.Fset, "my-analyzer", "RULE001")

Known Limitations

LSP conversion preserves go-finding-specific fields via LSPDiagnosticData. SeverityCritical maps to SARIF level "error" (SARIF 2.1.0 has no "critical" level). The original severity is preserved in the SARIF property bag for round-trip fidelity.

Report.findings is unexported for thread safety. Use AddFinding/AddFindings for writes, FindingsSnapshot/All/FindByID for reads.

Example (Basic)
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	// Create a finding
	f := finding.Finding{
		ID: finding.GenerateID(
			"my-linter",
			"unused-import",
			finding.Position{File: "main.go", Line: 5},
		),
		Rule:        "unused-import",
		ToolName:    "my-linter",
		Message:     "import \"fmt\" is unused",
		Severity:    finding.SeverityWarning,
		Position:    finding.Pos("main.go", 5, 2),
		Category:    finding.CategoryStyle,
		FixStrategy: finding.FixStrategyDirect,
		BeforeCode:  `import "fmt"`,
		AfterCode:   "",
	}

	// Create a report
	report := finding.NewReport(finding.ToolInfo{Name: "my-linter", Version: "1.0.0"})
	report.AddFinding(f)
	report.ComputeSummary()

	// Print summary
	fmt.Printf("Tool: %s\n", report.Tool.Name)
	fmt.Printf("Total findings: %d\n", report.Summary.Total)
	fmt.Printf("Warnings: %d\n", report.Summary.BySeverity[finding.SeverityWarning])

}
Output:
Tool: my-linter
Total findings: 1
Warnings: 1
Example (Filter)
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	findings := []finding.Finding{
		{ID: "1", Severity: finding.SeverityError, Rule: "nil-pointer", ToolName: "analyzer"},
		{ID: "2", Severity: finding.SeverityWarning, Rule: "unused-var", ToolName: "analyzer"},
		{ID: "3", Severity: finding.SeverityInfo, Rule: "comment-style", ToolName: "analyzer"},
	}

	// Filter for errors only
	errors := finding.Filter(findings, finding.BySeverity(finding.SeverityError))
	fmt.Printf("Errors: %d\n", len(errors))

	// Filter for severity >= warning
	warningsAndErrors := finding.Filter(
		findings,
		finding.BySeverityAtLeast(finding.SeverityWarning),
	)
	fmt.Printf("Warnings and Errors: %d\n", len(warningsAndErrors))

}
Output:
Errors: 1
Warnings and Errors: 2
Example (Merge)
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	// Reports from different tools
	r1 := finding.NewReport(finding.ToolInfo{Name: "linter-a"})
	r1.AddFinding(finding.Finding{
		ID:       "a:rule1:file.go:10:5",
		Severity: finding.SeverityError,
		Position: finding.Position{File: "file.go", Line: 10},
	})

	r2 := finding.NewReport(finding.ToolInfo{Name: "linter-b"})
	r2.AddFinding(finding.Finding{
		ID:       "b:rule2:file.go:20:3",
		Severity: finding.SeverityWarning,
		Position: finding.Position{File: "file.go", Line: 20},
	})

	// Merge reports
	merged := finding.Combine([]*finding.Report{r1, r2})
	merged.ComputeSummary()

	fmt.Printf("Total: %d\n", merged.Summary.Total)
	fmt.Printf("Files: %d\n", merged.Summary.FilesAffected)

}
Output:
Total: 2
Files: 1
Example (Merging)

Example_mergingShows unified report from multiple tools.

package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	// Tool A: Linter
	toolA := finding.NewReport(finding.ToolInfo{Name: "linter", Version: "1.0"})
	toolA.AddFinding(finding.Finding{
		ID:       "linter:unused:main.go:10",
		Rule:     "unused",
		ToolName: "linter",
		Message:  "unused variable",
		Severity: finding.SeverityWarning,
		Position: finding.Position{File: "main.go", Line: 10},
	})

	// Tool B: Security Scanner
	toolB := finding.NewReport(finding.ToolInfo{Name: "security", Version: "2.0"})
	toolB.AddFinding(finding.Finding{
		ID:       "security:sql-inject:db.go:45",
		Rule:     "sql-inject",
		ToolName: "security",
		Message:  "SQL injection vulnerability",
		Severity: finding.SeverityCritical,
		Position: finding.Position{File: "db.go", Line: 45},
	})

	// Merge into unified report
	merged := finding.Combine([]*finding.Report{toolA, toolB})
	merged.ComputeSummary()

	fmt.Printf("Unified Report:\n")
	fmt.Printf("Total: %d findings from %d tools\n", merged.Summary.Total, 2)
	fmt.Printf("By severity: critical=%d, warning=%d\n",
		merged.Summary.BySeverity[finding.SeverityCritical],
		merged.Summary.BySeverity[finding.SeverityWarning])

}
Output:
Unified Report:
Total: 2 findings from 2 tools
By severity: critical=1, warning=1
Example (SimpleCLI)

Example_simpleCLI demonstrates a simple CLI tool using the finding library.

package main

import (
	"fmt"
	"log"

	"github.com/larsartmann/go-finding"
)

func main() {
	// Simulate findings from a tool
	findings := []finding.Finding{
		{
			ID:          "linter:unused-import:main.go:3:2",
			Rule:        "unused-import",
			ToolName:    "my-linter",
			Message:     "import \"fmt\" is unused",
			Severity:    finding.SeverityWarning,
			Position:    finding.Pos("main.go", 3, 2),
			Category:    finding.CategoryStyle,
			FixStrategy: finding.FixStrategyDirect,
			BeforeCode:  `import "fmt"`,
			AfterCode:   "",
		},
		{
			ID:          "linter:unused-var:main.go:10:5",
			Rule:        "unused-var",
			ToolName:    "my-linter",
			Message:     "variable x is unused",
			Severity:    finding.SeverityWarning,
			Position:    finding.Pos("main.go", 10, 5),
			Category:    finding.CategoryStyle,
			FixStrategy: finding.FixStrategySuggest,
			Suggestion:  "Remove the variable or use it",
		},
		{
			ID:          "linter:nil-pointer:auth.go:45:12",
			Rule:        "nil-pointer",
			ToolName:    "my-linter",
			Message:     "potential nil pointer dereference",
			Severity:    finding.SeverityError,
			Position:    finding.Position{File: "auth.go", Line: 45, Column: 12},
			Category:    finding.CategorySecurity,
			FixStrategy: finding.FixStrategyNone,
		},
	}

	// Create report
	report := finding.NewReport(finding.ToolInfo{Name: "my-linter", Version: "1.0.0"})
	report.AddFindings(findings)
	report.ComputeSummary()

	// Filter for actionable items
	autoFixable := finding.Filter(findings, finding.ByFixStrategy(finding.FixStrategyDirect))
	suggestions := finding.Filter(findings, finding.ByFixStrategy(finding.FixStrategySuggest))

	// Output summary
	fmt.Printf("=== Analysis Summary ===\n")
	fmt.Printf("Total findings: %d\n", report.Summary.Total)
	fmt.Printf("Auto-fixable: %d\n", len(autoFixable))
	fmt.Printf("Need manual review: %d\n", len(suggestions))
	fmt.Printf("Errors: %d\n", report.Summary.BySeverity[finding.SeverityError])
	fmt.Printf("Warnings: %d\n", report.Summary.BySeverity[finding.SeverityWarning])

	// Output SARIF for CI integration
	sarif, err := report.ToSARIF()
	if err != nil {
		log.Fatal(err)
	}

	_ = sarif // In real tool, write to file

}
Output:
=== Analysis Summary ===
Total findings: 3
Auto-fixable: 1
Need manual review: 1
Errors: 1
Warnings: 2

Index

Examples

Constants

View Source
const LSPDiagnosticTagsKey = "go-finding/lsp-diagnostic-tags"

LSPDiagnosticTagsKey is the Metadata key for preserving LSP diagnostic tags.

View Source
const LSPSeverityKey = "go-finding/lsp-severity"

LSPSeverityKey is the Metadata key for preserving raw LSP severity codes.

View Source
const OffsetUnknown = -1

OffsetUnknown is the sentinel value for "no byte offset set." Use this instead of raw -1 literals to centralize the convention.

View Source
const VersionMajor = 1

VersionMajor is the major version number.

View Source
const VersionMinor = 9

VersionMinor is the minor version number.

View Source
const VersionPatch = 2

VersionPatch is the patch version number.

Variables

View Source
var (
	ErrValidation = errors.New("finding: validation error")
	ErrIO         = errors.New("finding: I/O error")
	ErrParse      = errors.New("finding: parse error")
	ErrConflict   = errors.New("finding: conflict error")
	ErrInternal   = errors.New("finding: internal error")
)

Sentinel errors for use with errors.Is.

View Source
var (
	ErrInvalidFinding = errors.New("invalid finding: missing required fields")
	ErrInvalidReport  = errors.New("invalid report: missing tool name")
)

Sentinel errors for JSON validation.

View Source
var (
	ErrDetectorRegistered = errors.New("detector already registered")
	ErrUnknownDetector    = errors.New("unknown detector")
)

Sentinel errors for the detector registry. Exported so callers can use errors.Is to distinguish registration conflicts from unknown detectors.

View Source
var DefaultLinterRegistry = NewLinterRegistry(map[string]Category{

	"gosec":         CategorySecurity,
	"noctx":         CategorySecurity,
	"errchkjson":    CategorySecurity,
	"bidichk":       CategorySecurity,
	"nosprintfhost": CategorySecurity,

	"govet":         CategoryCorrectness,
	"staticcheck":   CategoryCorrectness,
	"errcheck":      CategoryCorrectness,
	"nilerr":        CategoryCorrectness,
	"ineffassign":   CategoryCorrectness,
	"unconvert":     CategoryCorrectness,
	"bodyclose":     CategoryCorrectness,
	"contextcheck":  CategoryCorrectness,
	"durationcheck": CategoryCorrectness,
	"typecheck":     CategoryCorrectness,
	"gosimple":      CategoryCorrectness,
	"deadcode":      CategoryCorrectness,
	"varcheck":      CategoryCorrectness,
	"nakedret":      CategoryCorrectness,
	"rowserrcheck":  CategoryCorrectness,
	"sqlclosecheck": CategoryCorrectness,
	"wastedassign":  CategoryCorrectness,
	"exportloopref": CategoryCorrectness,
	"nilnesserr":    CategoryCorrectness,
	"recvcheck":     CategoryCorrectness,

	"prealloc":   CategoryPerformance,
	"perfsprint": CategoryPerformance,
	"unparam":    CategoryPerformance,

	"gocyclo":        CategoryComplexity,
	"cyclop":         CategoryComplexity,
	"gocognit":       CategoryComplexity,
	"maintidx":       CategoryComplexity,
	"funlen":         CategoryComplexity,
	"nestif":         CategoryComplexity,
	"interfacebloat": CategoryComplexity,
	"gocritic":       CategoryComplexity,

	"dupl":    CategoryDuplication,
	"goconst": CategoryDuplication,

	"wrapcheck": CategoryErrorHandling,
	"errorlint": CategoryErrorHandling,
	"errname":   CategoryErrorHandling,
	"nilnil":    CategoryErrorHandling,

	"misspell":       CategoryStyle,
	"revive":         CategoryStyle,
	"gofmt":          CategoryStyle,
	"gofumpt":        CategoryStyle,
	"goimports":      CategoryStyle,
	"gci":            CategoryStyle,
	"wsl":            CategoryStyle,
	"wsl_v5":         CategoryStyle,
	"dupword":        CategoryStyle,
	"godot":          CategoryStyle,
	"lll":            CategoryStyle,
	"whitespace":     CategoryStyle,
	"nlreturn":       CategoryStyle,
	"golint":         CategoryStyle,
	"dogsled":        CategoryStyle,
	"nolintlint":     CategoryStyle,
	"forbidigo":      CategoryStyle,
	"gomnd":          CategoryStyle,
	"varnamelen":     CategoryStyle,
	"tagalign":       CategoryStyle,
	"nonamedreturns": CategoryStyle,

	"paralleltest":     CategoryTesting,
	"thelper":          CategoryTesting,
	"testifylint":      CategoryTesting,
	"ginkgolinter":     CategoryTesting,
	"tparallel":        CategoryTesting,
	"testpackage":      CategoryTesting,
	"testableexamples": CategoryTesting,

	"exhaustive":      CategoryTypeSafety,
	"exhaustruct":     CategoryTypeSafety,
	"forcetypeassert": CategoryTypeSafety,
	"musttag":         CategoryTypeSafety,
	"gochecksumtype":  CategoryTypeSafety,
	"copyloopvar":     CategoryTypeSafety,
	"intrange":        CategoryTypeSafety,
	"tagliatelle":     CategoryTypeSafety,

	"sloglint":    CategoryStructure,
	"loggercheck": CategoryStructure,
	"unused":      CategoryUnused,

	"depguard": CategoryBestPractice,
	"mirror":   CategoryBestPractice,

	"gomodguard_v2": CategoryConfiguration,
})

DefaultLinterRegistry is the global registry with built-in linter→category mappings.

View Source
var ErrInvalidConfidence = fmt.Errorf("invalid confidence level: use %s, %s, %s, %s, %s, or a decimal in [0.0, 1.0]",
	ConfidenceNone, ConfidenceLow, ConfidenceMedium, ConfidenceHigh, ConfidenceFull)

ErrInvalidConfidence is returned by ParseConfidence when the input string is not a recognized confidence level name or a valid decimal number.

Version is the semantic version string, computed from components.

Functions

func ApplySimpleFixes added in v1.3.0

func ApplySimpleFixes(findings []Finding) map[FilePath][]SimpleFixResult

ApplySimpleFixes applies BeforeCode→AfterCode string replacements for all findings with FixStrategyDirect. Files are read, modified in memory, and written back. Findings without BeforeCode or AfterCode are skipped. This is the 80% case for consumers that don't need the full pipeline FixEngine.

Returns a map of file path to per-finding results. If a file cannot be read, all its findings are marked as not applied with the error reason.

func CheckBinary added in v1.3.0

func CheckBinary(name string) (string, error)

CheckBinary verifies that a binary exists in the system PATH. Returns the full path to the binary on success, or a wrapped NewIOError if the binary is not found. This standardizes the "run CLI tool → parse JSON" pattern that 4+ consumers independently implement.

func FilterInvalid deprecated added in v0.2.0

func FilterInvalid(f Finding) bool

FilterInvalid is a deprecated alias for IsInvalid. The name was misleading: it returns true when the finding IS invalid (should be filtered out), not when it should be kept.

Deprecated: Use IsInvalid instead.

func FormatMarkdown added in v0.4.0

func FormatMarkdown(w io.Writer, findings []Finding) error

FormatMarkdown writes a markdown table of findings to w.

Example
package main

import (
	"os"

	"github.com/larsartmann/go-finding"
)

func newExampleFinding(
	rule, tool, msg string,
	sev finding.Severity,
	file string,
	line, col int,
) finding.Finding {
	return finding.NewFinding(
		finding.RuleName(rule),
		finding.ToolName(tool),
		msg,
		sev,
		finding.Pos(finding.FilePath(file), line, col),
		0,
	)
}

func main() {
	findings := []finding.Finding{
		newExampleFinding(
			"nilcheck", "govet", "possible nil dereference",
			finding.SeverityError, "main.go", 42, 5,
		),
	}

	finding.FormatMarkdown(os.Stdout, findings) //nolint:errcheck

}
Output:
| Location | Severity | Rule | Message |
|----------|----------|------|--------|
| main.go:42:5 | error | nilcheck | possible nil dereference |

func FormatTable added in v1.3.0

func FormatTable(w io.Writer, findings []Finding) error

FormatTable writes a human-readable table of findings to w with severity badges, file:line, rule, message, and category columns.

func FormatText added in v0.4.0

func FormatText(w io.Writer, findings []Finding) error

FormatText writes a human-readable text representation of findings to w. Each finding is formatted as: file:line:col [SEVERITY] rule: message. Suggestion (when present) is shown on the next line with a "Suggestion:" prefix.

Example
package main

import (
	"os"

	"github.com/larsartmann/go-finding"
)

func newExampleFinding(
	rule, tool, msg string,
	sev finding.Severity,
	file string,
	line, col int,
) finding.Finding {
	return finding.NewFinding(
		finding.RuleName(rule),
		finding.ToolName(tool),
		msg,
		sev,
		finding.Pos(finding.FilePath(file), line, col),
		0,
	)
}

func main() {
	findings := []finding.Finding{
		newExampleFinding(
			"nilcheck",
			"govet",
			"possible nil dereference",
			finding.SeverityError,
			"main.go",
			42,
			5,
		),
		newExampleFinding(
			"unused",
			"staticcheck",
			"unused variable",
			finding.SeverityWarning,
			"util.go",
			10,
			3,
		),
	}

	finding.FormatText(os.Stdout, findings) //nolint:errcheck

}
Output:
main.go:42:5 [ERROR] nilcheck: possible nil dereference
util.go:10:3 [WARNING] unused: unused variable

func FormatTextRich added in v1.3.0

func FormatTextRich(w io.Writer, findings []Finding) error

FormatTextRich writes a human-readable text representation of findings to w with emoji severity badges and category display. Each finding is formatted as: file:line:col BADGE rule: message [category]. Severity badges use emoji + uppercase name (e.g., "🟠 ERROR"). Category is shown in brackets when present. Suggestion is prefixed with 💡.

func GroupBy

func GroupBy(findings []Finding, keyFn func(Finding) string) map[string][]Finding

GroupBy groups findings by a key extractor function.

func GroupByCategory

func GroupByCategory(findings []Finding) map[Category][]Finding

GroupByCategory groups findings by category.

func GroupByFile

func GroupByFile(findings []Finding) map[FilePath][]Finding

GroupByFile groups findings by file path.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	findings := []finding.Finding{
		{ID: "1", Position: finding.Position{File: "a.go"}},
		{ID: "2", Position: finding.Position{File: "b.go"}},
		{ID: "3", Position: finding.Position{File: "a.go"}},
	}

	byFile := finding.GroupByFile(findings)
	fmt.Println("a.go:", len(byFile["a.go"]))
	fmt.Println("b.go:", len(byFile["b.go"]))

}
Output:
a.go: 2
b.go: 1

func GroupBySeverity

func GroupBySeverity(findings []Finding) map[Severity][]Finding

GroupBySeverity groups findings by severity.

func IsCategory

func IsCategory(err error, cat ErrorCategory) bool

IsCategory returns true if err is a FindingError with the given category.

func IsFindingError

func IsFindingError(err error) bool

IsFindingError returns true if err is a *FindingError.

func IsHashID

func IsHashID(id string) bool

IsHashID returns true if the ID appears to be hash-based.

func IsInvalid added in v1.1.0

func IsInvalid(f Finding) bool

IsInvalid returns true if the finding is invalid (has missing required fields). Intended for use with slices.DeleteFunc.

func MergeIter added in v0.7.0

func MergeIter(reports []*Report, opts ...MergeOption) iter.Seq[Finding]

MergeIter returns an iterator that yields findings from multiple reports in streaming fashion, without loading all findings into memory at once. Supports optional deduplication via the same MergeOption as Combine.

Each report's findings are read under RLock; the iterator does not hold locks across reports.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	r1 := finding.NewReport(finding.ToolInfo{Name: "tool-a"})
	r1.AddFinding(finding.Finding{ID: "1", Rule: "r1", ToolName: "tool-a", Severity: finding.SeverityWarning})

	r2 := finding.NewReport(finding.ToolInfo{Name: "tool-b"})
	r2.AddFinding(finding.Finding{ID: "2", Rule: "r2", ToolName: "tool-b", Severity: finding.SeverityError})

	for f := range finding.MergeIter([]*finding.Report{r1, r2}) {
		fmt.Printf("%s: %s\n", f.ToolName, f.ID)
	}

}
Output:
tool-a: 1
tool-b: 2

func NotSuppressed

func NotSuppressed(f Finding) bool

NotSuppressed returns a filter for non-suppressed findings.

func RangeLinesEq

func RangeLinesEq(a, b Range) bool

RangeLinesEq checks if two ranges have equal start/end lines (ignoring columns/files).

func RegisterLinterCategory added in v0.7.0

func RegisterLinterCategory(name string, cat Category)

RegisterLinterCategory registers or overrides the Category for a linter name. The name is stored in lowercase for case-insensitive lookup. Safe for concurrent use.

func RegisterSeverityAlias added in v1.0.0

func RegisterSeverityAlias(name string, sev Severity)

RegisterSeverityAlias adds a custom severity alias for use by ParseSeverity. This is safe to call concurrently. If the alias already exists, it is overwritten.

func RunCmd added in v1.3.0

func RunCmd(ctx context.Context, name string, args ...string) ([]byte, error)

RunCmd executes a command with the given context and arguments, returning stdout output. Returns a wrapped NewIOError if the command fails. Use this with CheckBinary for the standard "run CLI tool → parse JSON" pattern.

func SortByPosition

func SortByPosition(findings []Finding)

SortByPosition sorts findings by file path, then line, then column.

func SortBySeverity

func SortBySeverity(findings []Finding)

SortBySeverity sorts findings by severity (most severe first).

func SortFindingsByID added in v0.5.0

func SortFindingsByID(findings []Finding)

SortFindingsByID sorts a slice of findings by ID.

func ValidateAll added in v1.5.0

func ValidateAll(findings []Finding) map[int]error

ValidateAll validates a batch of findings and returns a map of index → error for all invalid findings. Returns nil if all are valid.

func WithFix added in v0.9.0

func WithFix(f Finding) bool

WithFix returns a filter for findings with fixes.

func WithSuggestion added in v0.9.0

func WithSuggestion(f Finding) bool

WithSuggestion returns a filter for findings with suggestions.

func WithWorkingDir added in v1.2.1

func WithWorkingDir(ctx context.Context, dir string) context.Context

WithWorkingDir returns a context carrying dir as the working directory for a detection/repair/generation run. Downstream capabilities read it via WorkingDirFromContext. This is the canonical setter — consumer packages (BuildFlow runner, etc.) should delegate to it rather than define their own key so the value propagates across package boundaries.

func WorkingDirFromContext added in v1.2.1

func WorkingDirFromContext(ctx context.Context) string

WorkingDirFromContext extracts the working directory set by WithWorkingDir. Returns "" when no working directory is set (callers should then fall back to the process working directory or project root).

This is the canonical getter for the ecosystem: any Detector implementation can retrieve its target directory without coupling to a specific orchestrator.

Types

type Builder added in v0.2.0

type Builder struct {
	// contains filtered or unexported fields
}

Builder provides a fluent API for constructing Finding values. Use NewBuilder with the required fields, then chain With* methods for optional fields, and call Build to obtain the result.

Example:

f := NewBuilder("nilcheck", "govet", "possible nil deref", SeverityError, Pos("main.go", 42, 5)).
	WithFixStrategy(FixStrategyDirect).
	WithBeforeCode("x.foo").
	WithAfterCode("x.foo()").
	Build()
Example

ExampleBuilder demonstrates the fluent Finding builder API. This block intentionally mirrors examples/builder/main.go: the testable example feeds the godoc with a verifiable `// Output:` snapshot, while the demo program is a standalone binary for `go run`. Sharing the snippet between them would require an indirection that obscures both forms.

package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	builder := finding.NewBuilder(
		"staticcheck", "SA1000", "invalid regex",
		finding.SeverityError, finding.Pos("pkg/validate.go", 24, 8),
	)
	builder.WithCategory(finding.CategoryCorrectness)
	builder.WithConfidence(0.95)
	builder.WithBeforeCode("oldPattern")
	builder.WithAfterCode("newPattern")
	builder.WithFixStrategy(finding.FixStrategyDirect)

	f, err := builder.Build()
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	fmt.Println(f.Rule)
	fmt.Println(f.ToolName)
	fmt.Println(f.Category)
	fmt.Println(f.HasFix())

}
Output:
staticcheck
SA1000
correctness
true

func NewBuilder added in v0.2.0

func NewBuilder(rule RuleName, toolName ToolName, message string, severity Severity, pos Position) *Builder

NewBuilder creates a builder seeded with the required fields. The ID is auto-generated from the provided arguments. Confidence defaults to ConfidenceFull (1.0), appropriate for deterministic static analysis. Override with WithConfidence if needed.

func (*Builder) Build added in v0.2.0

func (b *Builder) Build() (Finding, error)

Build returns the constructed Finding. Returns a detailed validation error if required fields are missing or invalid. FixStrategy is normalized: empty string becomes FixStrategyNone.

func (*Builder) BuildOrDefault added in v1.3.0

func (b *Builder) BuildOrDefault() Finding

BuildOrDefault returns the constructed Finding, or a zero-value Finding{} if validation fails. This eliminates the error-swallowing boilerplate (SafeBuildFinding / buildFinding) that consumers universally reinvent. Use Build when you need to handle validation errors explicitly.

func (*Builder) MustBuild added in v0.2.1

func (b *Builder) MustBuild() Finding

MustBuild returns the constructed Finding or panics if required fields are missing. Use this only when the builder is fully configured and invalid state is a programmer error.

func (*Builder) WithAfterCode added in v0.2.0

func (b *Builder) WithAfterCode(code string) *Builder

WithAfterCode sets the code after the fix.

func (*Builder) WithBeforeCode added in v0.2.0

func (b *Builder) WithBeforeCode(code string) *Builder

WithBeforeCode sets the code before the fix.

func (*Builder) WithCategory added in v0.2.0

func (b *Builder) WithCategory(cat Category) *Builder

WithCategory sets the category.

func (*Builder) WithConfidence added in v0.2.0

func (b *Builder) WithConfidence(c Confidence) *Builder

WithConfidence sets the confidence level (clamped to [0.0, 1.0]).

func (*Builder) WithFixStrategy added in v0.2.0

func (b *Builder) WithFixStrategy(fs FixStrategy) *Builder

WithFixStrategy sets the fix strategy.

func (*Builder) WithGroupID added in v1.7.0

func (b *Builder) WithGroupID(g GroupID) *Builder

WithGroupID sets the logical group this finding belongs to (e.g., a clone group identifier).

func (*Builder) WithID added in v0.2.0

func (b *Builder) WithID(id ID) *Builder

WithID overrides the auto-generated ID.

func (*Builder) WithMetadata added in v0.2.0

func (b *Builder) WithMetadata(m map[string]string) *Builder

WithMetadata copies the given metadata into the finding.

func (*Builder) WithRange added in v0.2.0

func (b *Builder) WithRange(r Range) *Builder

WithRange sets the source range.

func (*Builder) WithRelated added in v0.2.0

func (b *Builder) WithRelated(refs ...RelatedRef) *Builder

WithRelated appends related references.

func (*Builder) WithSnippet added in v0.2.0

func (b *Builder) WithSnippet(s string) *Builder

WithSnippet sets the surrounding code context.

func (*Builder) WithSuggestion added in v0.2.0

func (b *Builder) WithSuggestion(s string) *Builder

WithSuggestion sets the human-readable fix suggestion.

func (*Builder) WithSuppression added in v0.2.0

func (b *Builder) WithSuppression(s Suppression) *Builder

WithSuppression sets the suppression info.

func (*Builder) WithTags added in v0.2.1

func (b *Builder) WithTags(tags ...Tag) *Builder

WithTags sets multiple tags.

type Category

type Category string

Category classifies the domain of a finding.

const (
	CategorySecurity      Category = "security"
	CategoryStyle         Category = "style"
	CategoryPerformance   Category = "performance"
	CategoryCorrectness   Category = "correctness"
	CategoryComplexity    Category = "complexity"
	CategoryDuplication   Category = "duplication"
	CategoryErrorHandling Category = "error-handling"
	CategoryMigration     Category = "migration"
	CategoryTypeSafety    Category = "type-safety"
	CategoryStructure     Category = "structure"
	CategoryConfiguration Category = "configuration"
	CategoryDocumentation Category = "documentation"
	CategoryTesting       Category = "testing"
	CategoryUnused        Category = "unused"
	CategoryBestPractice  Category = "best-practice"
	CategoryNaming        Category = "naming"
)

Standard category constants for findings.

func CategoryForLinter added in v0.7.0

func CategoryForLinter(name string) Category

CategoryForLinter returns the default Category for a well-known linter or analyzer name. The lookup is case-insensitive. If the name is not registered, it returns CategoryCorrectness. Register custom mappings with RegisterLinterCategory.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	cat := finding.CategoryForLinter("gosec")
	fmt.Println(cat)

	cat2 := finding.CategoryForLinter("gocyclo")
	fmt.Println(cat2)

	cat3 := finding.CategoryForLinter("unknown")
	fmt.Println(cat3)

}
Output:
security
complexity
correctness

func MustParseCategory added in v0.7.0

func MustParseCategory(s string) Category

MustParseCategory parses a string into a Category, panicking on invalid input.

func ParseCategory added in v0.7.0

func ParseCategory(s string) (Category, error)

ParseCategory parses a string into a Category. It accepts the standard category names (e.g., "security", "style", "correctness"). Returns an error if the string is not a valid category.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	cat, err := finding.ParseCategory("security")
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	fmt.Println(cat)

	cat2 := finding.MustParseCategory("error-handling")
	fmt.Println(cat2)

}
Output:
security
error-handling

func (Category) Compare added in v0.8.0

func (c Category) Compare(other Category) int

Compare returns -1, 0, or +1 depending on whether c is less than, equal to, or greater than other. Categories have no inherent priority ordering, so the comparison is lexicographic by string value, providing a stable total ordering suitable for deterministic sorting.

func (Category) IsSecurity added in v0.4.0

func (c Category) IsSecurity() bool

IsSecurity reports whether the category is security-related.

func (Category) IsStandard

func (c Category) IsStandard() bool

IsStandard returns true if the category is one of the predefined standard constants. Use IsStandard for allow-list filtering (e.g., "only show findings in known categories"). Use IsValid for input validation (accepts any well-formed custom category).

func (Category) IsValid

func (c Category) IsValid() bool

IsValid returns true if the category is a non-empty string matching the lowercase-hyphenated convention (e.g., "security", "go-vet"). This rejects typos like "Security" or "SOME_CATEGORY". Use IsValid for input validation (accepts custom categories). Use IsStandard to check for predefined constants only (allow-list filtering).

func (Category) String

func (c Category) String() string

String returns the string representation of the category.

type Confidence added in v0.4.0

type Confidence float64

Confidence represents the certainty level of a finding on a 0.0–1.0 scale. Use named constants (ConfidenceLow, ConfidenceMedium, ConfidenceHigh) for common values, or Confidence(f) for custom levels. The zero value is valid and represents no confidence information.

Be aware: Direct construction with Confidence values outside [0.0, 1.0] is possible (e.g., Finding{Confidence: 1.5}). The Validate() method catches this. For guaranteed-valid values, use the Builder API (WithConfidence) or NewFinding (both clamp automatically).

const (
	ConfidenceNone   Confidence = 0.0
	ConfidenceLow    Confidence = 0.25
	ConfidenceMedium Confidence = 0.5
	ConfidenceHigh   Confidence = 0.75
	ConfidenceFull   Confidence = 1.0
)

Standard confidence levels.

func ParseConfidence added in v1.6.0

func ParseConfidence(s string) (Confidence, error)

ParseConfidence converts a confidence level string to a Confidence value. It is the inverse of Confidence.String: every named level ("none", "low", "medium", "high", "full") maps back to its constant. Decimal strings (e.g. "0.42") are also accepted and clamped to [0.0, 1.0]. An empty string defaults to ConfidenceLow.

This eliminates the per-consumer switch statements that every CLI linter reinvents for its --min-confidence flag.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	c, err := finding.ParseConfidence("high")
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	fmt.Println(c)

	// Empty string defaults to low
	low, _ := finding.ParseConfidence("")
	fmt.Println(low)

	// Decimals work too
	custom, _ := finding.ParseConfidence("0.42")
	fmt.Println(custom)

}
Output:
high
low
0.42

func (Confidence) Clamp added in v0.4.0

func (c Confidence) Clamp() Confidence

Clamp returns the confidence clamped to [0.0, 1.0].

func (Confidence) Compare added in v0.4.0

func (c Confidence) Compare(other Confidence) int

Compare returns -1, 0, or +1 depending on whether c is less than, equal to, or greater than other.

func (Confidence) IsValid added in v0.4.0

func (c Confidence) IsValid() bool

IsValid returns true if the confidence is within [0.0, 1.0].

func (Confidence) String added in v0.4.0

func (c Confidence) String() string

String returns the confidence as a human-readable string. Named levels return their label (e.g., "medium"), custom values return a decimal.

type Correlation

type Correlation struct {
	FindingIDs []ID             `json:"findingIds"`
	Reason     string           `json:"reason"` // Why they're correlated
	Score      CorrelationScore `json:"score"`  // 0.0-1.0 correlation strength
}

Correlation represents a relationship between two or more findings.

func Correlate

func Correlate(findings []Finding) []Correlation

Correlate finds potentially related findings across tools. Uses two strategies depending on the data:

  • For findings with Range: uses IntervalIndex for O(n + k) overlap queries.
  • For point-only findings: uses line-proximity heuristics (same file + nearby lines).

This can be used standalone or enabled in Pipeline via Config.CorrelateFindings. When enabled, the pipeline populates PipelineResult.Correlations automatically.

Complexity

For range-based findings: O(n log n) to build the index, O(n + k) per query. For point-based findings: O(n) per file with sorted early-break.

The maxCorrelations constant (10,000) caps total output across both strategies.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	findings := []finding.Finding{
		{
			ID: "govet:printf:main.go:10:3", Rule: "printf",
			ToolName: "govet", Message: "format error",
			Severity: finding.SeverityWarning,
			Position: finding.Pos("main.go", 10, 3),
		},
		{
			ID: "staticcheck:SA1000:main.go:12:1", Rule: "SA1000",
			ToolName: "staticcheck", Message: "invalid regex",
			Severity: finding.SeverityError,
			Position: finding.Pos("main.go", 12, 1),
		},
	}

	correlations := finding.Correlate(findings)
	fmt.Println("Correlations:", len(correlations))

	for _, c := range correlations {
		fmt.Printf("%.1f: %s\n", c.Score, c.Reason)
	}

}
Output:
Correlations: 1
0.6: same file, nearby lines

type CorrelationScore added in v0.5.0

type CorrelationScore float64

CorrelationScore measures the strength of a correlation between findings. Unlike Confidence (which measures certainty of a single finding), CorrelationScore measures how strongly two findings are related.

func (CorrelationScore) IsValid added in v0.5.0

func (s CorrelationScore) IsValid() bool

IsValid returns true if the score is in the valid range [0.0, 1.0].

func (CorrelationScore) String added in v0.5.0

func (s CorrelationScore) String() string

String returns a human-readable representation of the correlation score.

type DeduplicateBy

type DeduplicateBy int

DeduplicateBy specifies what fields to use for deduplication.

const (
	DeduplicateByID       DeduplicateBy = iota // Exact ID matches.
	DeduplicateByPosition                      // File:line:column matching.
	DeduplicateByRule                          // Rule + position matching.
)

Deduplication strategies control how findings are matched during merge.

func (DeduplicateBy) String added in v0.6.0

func (d DeduplicateBy) String() string

String returns a human-readable name for the deduplication strategy.

type Detector added in v0.7.0

type Detector interface {
	// Name returns the detector's name.
	Name() string
	// Detect runs the detector and returns findings.
	Detect(ctx context.Context) ([]Finding, error)
}

Detector is the interface implemented by tools that can find issues.

func NamedDetectorFunc added in v0.7.0

func NamedDetectorFunc(name string, fn DetectorFunc) Detector

NamedDetectorFunc returns a Detector with the given name wrapping the provided function.

type DetectorFunc added in v0.7.0

type DetectorFunc func(ctx context.Context) ([]Finding, error)

DetectorFunc is an adapter to use ordinary functions as Detectors.

func (DetectorFunc) Detect added in v0.7.0

func (f DetectorFunc) Detect(ctx context.Context) ([]Finding, error)

Detect implements Detector.

func (DetectorFunc) Name added in v0.7.0

func (f DetectorFunc) Name() string

Name implements Detector. Returns "" — use NamedDetectorFunc for a named detector.

type DetectorRegistry added in v0.7.0

type DetectorRegistry struct {
	// contains filtered or unexported fields
}

DetectorRegistry manages named detector constructors. Create one with NewDetectorRegistry and register detectors with DetectorRegistry.Register. Use DetectorRegistry.Build to instantiate detectors by name.

The registry is safe for concurrent use.

func NewDetectorRegistry added in v0.7.0

func NewDetectorRegistry() *DetectorRegistry

NewDetectorRegistry creates an empty registry.

Example
package main

import (
	"context"
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	registry := finding.NewDetectorRegistry()
	registry.MustRegister("govet", func() finding.Detector {
		return finding.NamedDetectorFunc("govet", func(_ context.Context) ([]finding.Finding, error) {
			return []finding.Finding{{ID: "v1", Rule: "printf", Severity: finding.SeverityError}}, nil
		})
	})

	fmt.Println(registry.Has("govet"))
	fmt.Println(registry.Has("ghost"))
	fmt.Println(registry.Names())

	det, _ := registry.Build("govet")
	fmt.Println(det.Name())

}
Output:
true
false
[govet]
govet

func (*DetectorRegistry) Build added in v0.7.0

func (r *DetectorRegistry) Build(name string) (Detector, error)

Build instantiates a detector by name. Returns an error if not found.

func (*DetectorRegistry) BuildAll added in v0.7.0

func (r *DetectorRegistry) BuildAll() ([]Detector, error)

BuildAll instantiates all registered detectors in sorted name order.

func (*DetectorRegistry) Has added in v0.7.0

func (r *DetectorRegistry) Has(name string) bool

Has reports whether a detector with the given name is registered.

func (*DetectorRegistry) MustRegister added in v0.7.0

func (r *DetectorRegistry) MustRegister(name string, builder func() Detector)

MustRegister panics if registration fails.

func (*DetectorRegistry) Names added in v0.7.0

func (r *DetectorRegistry) Names() []string

Names returns registered detector names in sorted order.

func (*DetectorRegistry) Register added in v0.7.0

func (r *DetectorRegistry) Register(name string, builder func() Detector) error

Register adds a detector constructor under the given name. Returns an error if a detector with the same name is already registered.

type DiffResult added in v0.4.0

type DiffResult struct {
	Added     []Finding      // Present in "after" but not "before"
	Removed   []Finding      // Present in "before" but not "after"
	Modified  []ModifiedPair // Present in both but with different content
	Unchanged []Finding      // Present in both with identical content
}

DiffResult holds the difference between two finding sets.

func Diff added in v0.4.0

func Diff(before, after []Finding) DiffResult

Diff compares two finding sets by ID and categorizes them as added, removed, modified, or unchanged. Two findings with the same ID are considered "modified" if their content differs (per Equal()).

Note: Findings are keyed by ID. If the input contains duplicate IDs, only the last occurrence per ID is used (standard Go map semantics). Callers should deduplicate by ID before diffing if duplicates are expected. All result slices are sorted by ID.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func newExampleFinding(
	rule, tool, msg string,
	sev finding.Severity,
	file string,
	line, col int,
) finding.Finding {
	return finding.NewFinding(
		finding.RuleName(rule),
		finding.ToolName(tool),
		msg,
		sev,
		finding.Pos(finding.FilePath(file), line, col),
		0,
	)
}

func main() {
	before := []finding.Finding{
		newExampleFinding("rule-a", "tool", "msg a", finding.SeverityError, "a.go", 1, 1),
		newExampleFinding("rule-b", "tool", "msg b", finding.SeverityWarning, "b.go", 2, 1),
	}
	after := []finding.Finding{
		newExampleFinding("rule-a", "tool", "msg a", finding.SeverityError, "a.go", 1, 1),
		newExampleFinding("rule-c", "tool", "msg c", finding.SeverityInfo, "c.go", 3, 1),
	}

	result := finding.Diff(before, after)
	fmt.Println("Added:", len(result.Added))
	fmt.Println("Removed:", len(result.Removed))
	fmt.Println("Unchanged:", len(result.Unchanged))

}
Output:
Added: 1
Removed: 1
Unchanged: 1

func (DiffResult) HasChanges added in v0.4.0

func (d DiffResult) HasChanges() bool

HasChanges reports whether the diff contains any additions, removals, or modifications.

func (DiffResult) Stats added in v0.4.0

func (d DiffResult) Stats() string

Stats returns a human-readable summary of the diff counts.

type ErrorCategory

type ErrorCategory string

ErrorCategory categorizes errors for programmatic handling.

const (
	// ErrCategoryValidation indicates validation errors.
	ErrCategoryValidation ErrorCategory = "validation"
	// ErrCategoryIO indicates file system or network errors.
	ErrCategoryIO ErrorCategory = "io"
	// ErrCategoryParse indicates parsing errors.
	ErrCategoryParse ErrorCategory = "parse"
	// ErrCategoryConflict indicates conflicting operations.
	ErrCategoryConflict ErrorCategory = "conflict"
	// ErrCategoryInternal indicates internal logic errors.
	ErrCategoryInternal ErrorCategory = "internal"
)

func CategoryOf added in v1.0.0

func CategoryOf(err error) ErrorCategory

CategoryOf returns the category of the error, or empty string if not a FindingError.

func (ErrorCategory) IsValid

func (c ErrorCategory) IsValid() bool

IsValid returns true if the error category is a non-empty string matching the lowercase-hyphenated convention (e.g., "validation", "io").

type FilePath added in v1.0.0

type FilePath string

FilePath is a path to a source file.

type FilterFunc

type FilterFunc func(Finding) bool

FilterFunc is a predicate for filtering findings.

func AnyOf added in v0.4.0

func AnyOf(predicates ...FilterFunc) FilterFunc

AnyOf returns a filter that matches if ANY of the given predicates match. This is the complement of Filter, which requires ALL predicates to match.

func ByCategory

func ByCategory(cat Category) FilterFunc

ByCategory returns a filter for the given category.

func ByConfidence added in v0.4.0

func ByConfidence(c Confidence) FilterFunc

ByConfidence returns a filter for the exact confidence level.

func ByConfidenceAtLeast added in v0.4.0

func ByConfidenceAtLeast(c Confidence) FilterFunc

ByConfidenceAtLeast returns a filter for confidence >= the given level.

func ByFile

func ByFile(file FilePath) FilterFunc

ByFile returns a filter for findings in the given file.

func ByFixStrategy

func ByFixStrategy(fs FixStrategy) FilterFunc

ByFixStrategy returns a filter for the given fix strategy.

func ByRule

func ByRule(rule RuleName) FilterFunc

ByRule returns a filter for the given rule.

func BySeverity

func BySeverity(sev Severity) FilterFunc

BySeverity returns a filter for the given severity.

func BySeverityAtLeast

func BySeverityAtLeast(sev Severity) FilterFunc

BySeverityAtLeast returns a filter for severity >= the given level. Findings with invalid severity are excluded (return false).

func ByTool

func ByTool(tool ToolName) FilterFunc

ByTool returns a filter for the given tool name.

func Negate added in v0.4.0

func Negate(predicate FilterFunc) FilterFunc

Negate inverts a filter: returns findings that do NOT match the given predicate.

type Finding

type Finding struct {
	// Identity
	ID       ID       `json:"id"`       // Stable unique identifier (e.g., "tool:rule:file:42:5")
	Rule     RuleName `json:"rule"`     // Rule/check name (e.g., "STRONG_ID", "clone-detected")
	ToolName ToolName `json:"toolName"` // Source tool name (e.g., "branching-flow", "art-dupl")

	// Core
	Message  string   `json:"message"`  // Human-readable description
	Severity Severity `json:"severity"` // info, warning, error, critical
	Position Position `json:"position"` // Where the issue is

	// Classification
	Category Category `json:"category,omitempty"` // Domain: "security", "style", "duplication", etc.
	Tags     []Tag    `json:"tags,omitempty"`     // Multiple tags for richer classification
	// Fix
	FixStrategy FixStrategy `json:"fixStrategy"`          // none, suggest, direct, ai
	Suggestion  string      `json:"suggestion,omitempty"` // Human-readable fix description
	BeforeCode  string      `json:"beforeCode,omitempty"` // Code before the fix
	AfterCode   string      `json:"afterCode,omitempty"`  // Code after the fix

	// Context
	Range       *Range       `json:"range,omitempty"`       // For span-based findings
	Snippet     string       `json:"snippet,omitempty"`     // Surrounding code context
	Confidence  Confidence   `json:"confidence,omitempty"`  // 0.0-1.0
	GroupID     GroupID      `json:"groupId,omitempty"`     // Logical group this finding belongs to
	Related     []RelatedRef `json:"related,omitempty"`     // Related findings
	Suppression *Suppression `json:"suppression,omitempty"` // If suppressed

	// Extensibility
	// Design decision: We intentionally have only Metadata (map[string]string), NOT a
	// Properties map[string]any. Use string-valued metadata for extensibility.
	// If you need complex values, JSON-serialize them into a string value.
	// Rationale: keeps the struct simple, avoids type-assertion boilerplate,
	// and Metadata is fully typed as string→string which is lossless for
	// interchange (SARIF, JSON, CLI flags, env vars).
	//
	// Key namespacing: use "toolName.key" format to prevent collisions between
	// tools. For example, "govet.category" vs "staticcheck.category". The
	// "go-finding/" prefix is reserved for internal use (SARIF round-trip,
	// LSP diagnostic tags, etc.).
	Metadata map[string]string `json:"metadata,omitempty"` // Tool-specific key-value pairs
}

Finding represents a single issue detected by a static analysis tool.

func Filter

func Filter(findings []Finding, predicates ...FilterFunc) []Finding

Filter returns findings that match all predicates. If no predicates are provided, returns a copy of all findings.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	findings := []finding.Finding{
		{
			ID:       "1",
			Rule:     "R1",
			Severity: finding.SeverityInfo,
			Position: finding.Position{File: "a.go"},
		},
		{
			ID:       "2",
			Rule:     "R2",
			Severity: finding.SeverityError,
			Position: finding.Position{File: "b.go"},
		},
		{
			ID:       "3",
			Rule:     "R1",
			Severity: finding.SeverityWarning,
			Position: finding.Position{File: "a.go"},
		},
	}

	errors := finding.Filter(findings, finding.BySeverityAtLeast(finding.SeverityError))
	fmt.Println("Errors:", len(errors))

	fromA := finding.Filter(findings, finding.ByFile("a.go"))
	fmt.Println("In a.go:", len(fromA))

	combined := finding.Filter(
		findings,
		finding.ByRule("R1"),
		finding.ByFile("a.go"),
	)
	fmt.Println("R1 in a.go:", len(combined))

}
Output:
Errors: 1
In a.go: 2
R1 in a.go: 2

func FilterInPlace added in v0.2.1

func FilterInPlace(findings []Finding, predicates ...FilterFunc) []Finding

FilterInPlace filters findings in place, modifying the input slice. Returns the filtered slice (which may be a sub-slice of the input).

func FindingsFromJSON

func FindingsFromJSON(data []byte) ([]Finding, int, error)

FindingsFromJSON parses a slice of Findings from JSON and validates each one. Invalid findings are silently dropped. Use the returned count to detect data loss.

func FindingsFromReader added in v0.4.3

func FindingsFromReader(ctx context.Context, r io.Reader) ([]Finding, error)

FindingsFromReader parses SARIF JSON from an io.Reader and returns Findings. It extracts go-finding-specific properties for round-trip fidelity. The context is checked for cancellation before decoding begins. Prefer this over FindingsFromSARIF for large payloads to avoid buffering the entire input into memory.

func FindingsFromSARIF

func FindingsFromSARIF(ctx context.Context, data []byte) ([]Finding, error)

FindingsFromSARIF parses SARIF JSON and returns Findings. It extracts go-finding-specific properties for round-trip fidelity (severity, ID, tool name, etc.) and falls back to SARIF fields otherwise. The context is checked for cancellation before parsing begins.

func FromJSON

func FromJSON(data []byte) (Finding, error)

FromJSON parses a Finding from JSON and validates required fields.

func FromLSP

func FromLSP(fileURI FilePath, diag LSPDiagnostic) Finding

FromLSP creates a Finding from an LSP Diagnostic at the given file URI. Preserves end position in Range and related information when present. When the diagnostic's Data field is populated (by ToLSP), restores the original ID, FixStrategy, Confidence, Category, Tags, and code data. The raw LSP severity integer is stored in Metadata under LSPSeverityKey.

func NewFinding

func NewFinding(
	rule RuleName, toolName ToolName, message string,
	severity Severity,
	pos Position,
	confidence Confidence,
) Finding

NewFinding creates a Finding with an auto-generated ID, default fix strategy, and clamped confidence.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	pos := finding.Pos("main.go", 42, 5)
	f := finding.NewFinding(
		"nilcheck", "govet", "possible nil dereference",
		finding.SeverityError, pos, 0,
	)
	fmt.Println(f.ID)
	fmt.Println(f.Rule)
	fmt.Println(f.Severity)
	fmt.Println(f.Position)

}
Output:
govet:nilcheck:main.go:42:5
nilcheck
error
main.go:42:5

func (Finding) Clone

func (f Finding) Clone() Finding

Clone returns a deep copy of the finding.

func (Finding) Equal

func (f Finding) Equal(other Finding) bool

Equal reports whether two findings are identical, including all nested fields.

func (Finding) HasCategory added in v0.2.1

func (f Finding) HasCategory() bool

HasCategory returns true if this finding has a category set.

func (Finding) HasCodeChange added in v0.4.0

func (f Finding) HasCodeChange() bool

HasCodeChange reports whether the finding carries any code change data (BeforeCode or AfterCode). This is a low-level check used by the FixEngine; prefer HasFix() or IsAutoFixable() for higher-level fixability decisions.

func (Finding) HasFix

func (f Finding) HasFix() bool

HasFix reports whether this finding has any fix available. This is the canonical "is fixable?" check.

Fixability lattice:

  • IsAutoFixable() ⟹ HasFix() (strict subset)
  • HasFix() requires a valid FixStrategy AND code data where applicable

Returns true for FixStrategyDirect when BeforeCode or AfterCode is present. Returns true for FixStrategySuggest/FixStrategyAI when AfterCode is present. Returns false for FixStrategyNone, empty strategy, or any strategy missing required code data.

Note: HasFix() respects Validate() semantics — a FixStrategyDirect finding without code data returns false (it would fail validation).

func (Finding) HasRange added in v0.4.0

func (f Finding) HasRange() bool

HasRange reports whether the finding has a valid range set.

func (Finding) HasSuggestion

func (f Finding) HasSuggestion() bool

HasSuggestion returns true if this finding has a human-readable suggestion.

func (Finding) IsAutoFixable added in v0.4.0

func (f Finding) IsAutoFixable() bool

IsAutoFixable reports whether the pipeline can automatically apply this fix. Stricter than HasFix: requires FixStrategyDirect AND (BeforeCode or AfterCode). Use HasFix() to check if any fix exists; use IsAutoFixable() to check if the pipeline will attempt auto-application.

func (Finding) IsSuppressed

func (f Finding) IsSuppressed() bool

IsSuppressed returns true if this finding is suppressed at the current time.

func (Finding) IsSuppressedAt added in v0.2.0

func (f Finding) IsSuppressedAt(now time.Time) bool

IsSuppressedAt returns true if this finding is suppressed at the given time. Use this in tests for deterministic suppression checks. A finding is suppressed only if its Suppression is valid (correct Kind and non-empty Rule) and not expired, consistent with Suppression.IsActive.

func (Finding) IsValid

func (f Finding) IsValid() bool

IsValid returns true if the finding has required fields set.

func (Finding) Key added in v0.2.1

func (f Finding) Key() string

Key returns a stable identifier for the finding.

Canonical identity: Two findings are identical iff their GenerateID outputs are equal (see ADR #12). Key() is a fallback for findings without an ID, building a composite key from ToolName, Position.File, Rule, and Message.

Note: Key() includes Message in the fallback key, while GenerateID does not. This means two findings with different messages but the same position will have different Keys but the same GenerateID. Prefer ID (and thus GenerateID) as the canonical identity. Key() is primarily used by external dedup logic that predates GenerateID.

func (Finding) LineJSON

func (f Finding) LineJSON() (string, error)

LineJSON returns compact JSON (single line).

func (Finding) Normalized added in v0.9.0

func (f Finding) Normalized() Finding

Normalized returns a copy of the finding with FixStrategy normalized (empty string converted to FixStrategyNone). Use this when you need the canonical form after direct struct construction, since Validate() has a value receiver and cannot mutate the original.

func (Finding) NormalizedConfidence added in v0.2.0

func (f Finding) NormalizedConfidence() Confidence

NormalizedConfidence returns the confidence clamped to [0.0, 1.0].

func (Finding) Preview added in v0.2.1

func (f Finding) Preview() string

Preview returns a unified-diff-style preview of the fix, or empty string if the finding has no fixable code change (BeforeCode and AfterCode both empty).

func (Finding) String

func (f Finding) String() string

String returns a human-readable summary of the finding.

func (Finding) ToLSP

func (f Finding) ToLSP() LSPDiagnostic

ToLSP converts a Finding to LSP Diagnostic format. The Data field carries go-finding-specific fields (ID, FixStrategy, Confidence, Category, Tags, code data) for round-trip fidelity via FromLSP.

func (Finding) Validate added in v0.2.1

func (f Finding) Validate() error

Validate checks all fields of the Finding for correctness and returns detailed per-field errors. Use IsValid for a simple boolean check.

func (Finding) WriteJSON added in v0.2.1

func (f Finding) WriteJSON(w io.Writer) error

WriteJSON writes compact JSON directly to w. Avoids the intermediate string allocation of LineJSON.

type FindingError

type FindingError struct {
	Category ErrorCategory // Category of error
	Finding  *Finding      // Associated finding (may be nil)
	Message  string        // Human-readable message
	Cause    error         // Underlying cause (may be nil)
	File     FilePath      // File path (if applicable)
	Position *Position     // Position in file (if applicable)
}

FindingError provides structured error information with context.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	err := finding.NewValidationError("invalid input", nil)
	fmt.Println(finding.IsFindingError(err))
	fmt.Println(finding.CategoryOf(err))

	ioErr := finding.NewIOError("read file", errors.New("permission denied"))
	fmt.Println(ioErr.Error())

}
Output:
true
validation
[io] read file: permission denied

func NewConflictError

func NewConflictError(message string, cause error) *FindingError

NewConflictError creates a conflict error.

func NewIOError

func NewIOError(message string, cause error) *FindingError

NewIOError creates an IO error.

func NewInternalError

func NewInternalError(message string, cause error) *FindingError

NewInternalError creates an internal error.

func NewParseError

func NewParseError(message string, cause error) *FindingError

NewParseError creates a parse error.

func NewValidationError

func NewValidationError(message string, cause error) *FindingError

NewValidationError creates a validation error.

func (*FindingError) Error

func (e *FindingError) Error() string

Error implements the error interface.

func (*FindingError) ErrorCode added in v1.4.0

func (e *FindingError) ErrorCode() string

ErrorCode implements errorfamily.Coded.

func (*FindingError) ErrorFamily added in v1.4.0

func (e *FindingError) ErrorFamily() errorfamily.Family

ErrorFamily implements errorfamily.Classified, mapping ErrorCategory to go-error-family families for integration with Classify().

func (*FindingError) Is

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

Is supports errors.Is by matching sentinel errors.

func (*FindingError) Unwrap

func (e *FindingError) Unwrap() error

Unwrap returns the underlying cause for error inspection.

func (*FindingError) WithFinding

func (e *FindingError) WithFinding(f Finding) *FindingError

WithFinding sets the finding on a copy of the FindingError and returns it.

func (*FindingError) WithPosition

func (e *FindingError) WithPosition(pos Position) *FindingError

WithPosition sets the position on a copy of the FindingError and returns it.

type FixStrategy

type FixStrategy string

FixStrategy indicates how a finding can be remediated.

const (
	// FixStrategyNone indicates no fix is available.
	FixStrategyNone FixStrategy = "none"
	// FixStrategySuggest provides a human-readable suggestion.
	FixStrategySuggest FixStrategy = "suggest"
	// FixStrategyDirect can be automatically applied.
	FixStrategyDirect FixStrategy = "direct"
	// FixStrategyAI requires AI assistance.
	// Pipeline triage groups this with FixStrategySuggest (no auto-apply).
	// NeedsAI() is defined but no AI backend exists yet. Reserve this value
	// for future AI-powered remediation — do not remove.
	FixStrategyAI FixStrategy = "ai"
)

func NormalizeFixStrategy added in v0.9.0

func NormalizeFixStrategy(f FixStrategy) FixStrategy

NormalizeFixStrategy converts the empty string (the zero value) to FixStrategyNone. This ensures there is exactly one canonical "no fix" state, eliminating the split-brain where "" and "none" both meant "no fix" but compared unequal. All public entry points (Validate, Builder.Build, FromLSP, SARIF import) should call this.

func (FixStrategy) CanAutoApply

func (f FixStrategy) CanAutoApply() bool

CanAutoApply returns true if this fix strategy can be automatically applied.

func (FixStrategy) IsValid

func (f FixStrategy) IsValid() bool

IsValid returns true if the fix strategy is a valid value.

func (FixStrategy) NeedsAI

func (f FixStrategy) NeedsAI() bool

NeedsAI returns true if this fix strategy requires AI assistance.

func (FixStrategy) String

func (f FixStrategy) String() string

String returns the string representation of the fix strategy.

type Group added in v1.8.0

type Group struct {
	// ID is the group's GroupID.
	ID GroupID
	// Findings are the group members in report order.
	Findings []Finding
}

Group is one GroupID group of findings, as returned by Report.GroupFindingsSorted.

type GroupID added in v1.7.0

type GroupID string

GroupID groups findings that belong to the same logical set (e.g., a clone group of N duplicated code blocks).

A set GroupID must be a machine-safe identifier: no whitespace, no control characters, at most 128 bytes. The empty GroupID means "not grouped" and is always valid. See GroupID.IsValid.

func (GroupID) IsValid added in v1.8.0

func (g GroupID) IsValid() bool

IsValid reports whether the GroupID is usable as a machine identifier. The empty value ("not grouped") is valid. Non-empty values must not contain whitespace or control characters and must fit in 128 bytes. The charset is deliberately permissive (underscores, dots, uppercase are fine) — this guards the wire formats, not naming style.

type ID added in v1.0.0

type ID string

ID is the stable unique identifier for a finding (tool:rule:file:line:col).

func GenerateID

func GenerateID(toolName ToolName, rule RuleName, pos Position) ID

GenerateID creates a stable, unique identifier for a finding. Format: "tool:rule:file:line:col" (human-readable) If line is 0, uses hash-based ID for stability.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	pos := finding.Position{File: "main.go", Line: 42, Column: 5}
	id := finding.GenerateID("govet", "printf", pos)
	fmt.Println(id)

	// Hash-based ID when line is 0
	posNoLine := finding.Position{File: "main.go"}
	hashID := finding.GenerateID("govet", "printf", posNoLine)
	fmt.Println(finding.IsHashID(string(hashID)))

}
Output:
govet:printf:main.go:42:5
true

type Interval added in v0.7.0

type Interval[T any] struct {
	Start int
	End   int
	Value T
}

Interval represents a half-open range [Start, End) on a single axis.

type IntervalIndex added in v0.7.0

type IntervalIndex[T any] struct {
	// contains filtered or unexported fields
}

IntervalIndex supports efficient overlap queries over intervals. Create one with NewIntervalIndex. The index is immutable after construction.

Query complexity is O(n + k) where n is the number of intervals and k is the number of matching results. This is a sorted-slice implementation (not an augmented interval tree), which is fast enough for typical finding counts. For very large interval sets with adversarial distributions, consider implementing a true O(log n + k) interval tree.

Use cases include finding overlapping findings by line range, conflict detection, and spatial correlation queries.

func NewIntervalIndex added in v0.7.0

func NewIntervalIndex[T any](intervals []Interval[T]) *IntervalIndex[T]

NewIntervalIndex builds an interval index from the given intervals. If the input is empty, returns an index that answers all queries with nil.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	type Item struct{ Name string }

	idx := finding.NewIntervalIndex([]finding.Interval[Item]{
		{Start: 1, End: 10, Value: Item{"alpha"}},
		{Start: 5, End: 15, Value: Item{"beta"}},
		{Start: 20, End: 30, Value: Item{"gamma"}},
	})

	overlaps := idx.Query(8, 12)
	for _, iv := range overlaps {
		fmt.Println(iv.Value.Name)
	}

}
Output:
alpha
beta

func (*IntervalIndex[T]) Len added in v0.7.0

func (idx *IntervalIndex[T]) Len() int

Len returns the number of intervals in the index.

func (*IntervalIndex[T]) Query added in v0.7.0

func (idx *IntervalIndex[T]) Query(start, end int) []Interval[T]

Query returns all intervals that overlap the half-open range [start, end). Overlap means: iv.Start < end && start < iv.End. Returns nil if no intervals match.

type LSPDiagnostic

type LSPDiagnostic struct {
	Range    LSPRange           `json:"range"`
	Severity LSPSeverity        `json:"severity,omitempty"` // 1=Error, 2=Warning, 3=Info, 4=Hint
	Code     string             `json:"code,omitempty"`
	Source   string             `json:"source,omitempty"`
	Message  string             `json:"message"`
	Tags     []LSPDiagnosticTag `json:"tags,omitempty"`
	Related  []LSPRelated       `json:"relatedInformation,omitempty"`
	Data     *LSPDiagnosticData `json:"data,omitempty"`
}

LSPDiagnostic represents an LSP (Language Server Protocol) diagnostic. Used for converting Finding objects to LSP diagnostic format.

The Data field carries go-finding-specific metadata (finding ID, fix strategy, confidence, category, tags) for round-trip fidelity through LSP conversion. LSP clients that don't understand Data will simply ignore it.

type LSPDiagnosticData added in v1.1.0

type LSPDiagnosticData struct {
	ID                ID                `json:"id,omitempty"`
	Severity          Severity          `json:"severity,omitempty"`
	FixStrategy       FixStrategy       `json:"fixStrategy,omitempty"`
	Confidence        Confidence        `json:"confidence,omitempty"`
	Category          Category          `json:"category,omitempty"`
	GroupID           GroupID           `json:"groupId,omitempty"`
	Tags              []Tag             `json:"tags,omitempty"`
	BeforeCode        string            `json:"beforeCode,omitempty"`
	AfterCode         string            `json:"afterCode,omitempty"`
	Suggestion        string            `json:"suggestion,omitempty"`
	Snippet           string            `json:"snippet,omitempty"`
	Suppression       *Suppression      `json:"suppression,omitempty"`
	Metadata          map[string]string `json:"metadata,omitempty"`
	RelatedFindingIDs []string          `json:"relatedFindingIds,omitempty"`
}

LSPDiagnosticData carries go-finding-specific fields in the LSP diagnostic's data property. This enables round-trip fidelity for fields that the standard LSP diagnostic type cannot represent.

type LSPDiagnosticTag added in v0.5.0

type LSPDiagnosticTag int

LSPDiagnosticTag represents a diagnostic tag per the LSP specification (3.15+).

const (
	LSPDiagnosticTagUnnecessary LSPDiagnosticTag = 1 // Unnecessary code (e.g., unused, duplicate)
	LSPDiagnosticTagDeprecated  LSPDiagnosticTag = 2 // Deprecated code
)

LSP diagnostic tag constants per the LSP specification.

type LSPLocation

type LSPLocation struct {
	URI   string   `json:"uri"`
	Range LSPRange `json:"range"`
}

LSPLocation represents the location of a diagnostic.

type LSPPosition

type LSPPosition struct {
	Line      int `json:"line"`      // 0-based
	Character int `json:"character"` // 0-based
}

LSPPosition represents a 0-based position in a text document.

type LSPRange

type LSPRange struct {
	Start LSPPosition `json:"start"`
	End   LSPPosition `json:"end"`
}

LSPRange represents a 0-based character range in a text document.

type LSPRelated added in v1.0.0

type LSPRelated struct {
	Location LSPLocation `json:"location"`
	Message  string      `json:"message"`
}

LSPRelated provides related information for a diagnostic.

type LSPSeverity added in v0.4.0

type LSPSeverity int

LSPSeverity represents an LSP diagnostic severity level per the LSP specification.

const (
	LSPSeverityError   LSPSeverity = 1 // Error
	LSPSeverityWarning LSPSeverity = 2 // Warning
	LSPSeverityInfo    LSPSeverity = 3 // Information
	LSPSeverityHint    LSPSeverity = 4 // Hint
)

LSP severity level constants per the LSP specification.

type LinterRegistry added in v0.7.0

type LinterRegistry struct {
	// contains filtered or unexported fields
}

LinterRegistry maps linter/analyzer names to Categories. The zero value is ready to use. All methods are safe for concurrent use.

Use DefaultLinterRegistry for the global registry with built-in mappings, or create isolated registries for testing or custom tool chains.

func NewLinterRegistry added in v0.7.0

func NewLinterRegistry(items map[string]Category) *LinterRegistry

NewLinterRegistry creates a registry pre-populated with the given mappings.

func (*LinterRegistry) Clone added in v0.7.0

func (r *LinterRegistry) Clone() *LinterRegistry

Clone returns a deep copy of the registry.

func (*LinterRegistry) Lookup added in v0.7.0

func (r *LinterRegistry) Lookup(name string, fallback Category) Category

Lookup returns the Category for a linter name (case-insensitive). Returns the provided fallback if the name is not registered.

func (*LinterRegistry) Names added in v0.7.0

func (r *LinterRegistry) Names() []string

Names returns all registered linter names in sorted order.

func (*LinterRegistry) Register added in v0.7.0

func (r *LinterRegistry) Register(name string, cat Category)

Register adds or overrides a Category mapping for a linter name.

type MergeOption

type MergeOption func(*MergeOptions)

MergeOption is a functional option for configuring merge behavior.

func WithDeduplicateBy

func WithDeduplicateBy(by DeduplicateBy) MergeOption

WithDeduplicateBy sets the deduplication strategy.

func WithDeduplication

func WithDeduplication(enabled bool) MergeOption

WithDeduplication enables/disables deduplication.

type MergeOptions

type MergeOptions struct {
	Deduplicate   bool
	DeduplicateBy DeduplicateBy
}

MergeOptions controls how reports are merged.

type ModifiedPair added in v0.4.0

type ModifiedPair struct {
	Before Finding
	After  Finding
}

ModifiedPair holds both versions of a modified finding.

type ParsedID

type ParsedID struct {
	Tool   ToolName
	Rule   RuleName
	File   FilePath
	Line   int
	Column int
}

ParsedID holds the components of a parsed finding ID.

func ParseID

func ParseID(id ID) ParsedID

ParseID parses a finding ID and extracts its components.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	p := finding.ParseID("govet:printf:main.go:42:5")
	if !p.OK() {
		fmt.Println("invalid ID")

		return
	}

	fmt.Printf("tool=%s rule=%s file=%s line=%d col=%d\n", p.Tool, p.Rule, p.File, p.Line, p.Column)

}
Output:
tool=govet rule=printf file=main.go line=42 col=5

func (ParsedID) OK

func (p ParsedID) OK() bool

OK returns true if the ID was successfully parsed.

type Position

type Position struct {
	File   FilePath `json:"file"`             // Required: file path
	Line   int      `json:"line,omitempty"`   // 1-based line number; 0 = not set
	Column int      `json:"column,omitempty"` // 1-based column number; 0 = not set
	Offset int      `json:"offset,omitempty"` // 0-based byte offset; -1 = not set
}

Position represents a location in source code.

Sentinel values:

  • Line, Column: 0 means "not set" (1-based, so 0 is never valid).
  • Offset: -1 means "not set" (0-based, so 0 means "start of file" which IS valid).

Constructors (Pos, NewRange, NewFinding, FromLSP, SARIF import) set Offset to -1 when no byte offset is available. The zero value Position{} has Offset=0, which means "byte offset 0" (start of file) — NOT "unset". This is a deliberate design choice: Offset=0 is a valid byte position, and the zero value should not lie about having data.

File-level positions: A Position with a file path but Line=0 is valid for findings that apply to an entire file (config issues, project checks). Use FilePos to create such positions. Position.IsValid still requires Line>0 for backward compatibility; use Position.HasFile to check only whether a file is set.

Use HasLocation() to check for a meaningful position (file + line). Use HasOffset() to check whether a byte offset is set (Offset >= 0). Use IsZero() to check for the completely-uninitialized state (all fields at their "unset" sentinels: empty File, Line=0, Column=0, Offset=-1).

func FilePos added in v1.3.0

func FilePos(file FilePath) Position

FilePos creates a Position for a file-level finding (no line/column). Use this for findings that apply to an entire file, such as configuration issues, project-level checks, or file-wide linting rules. The Offset is set to OffsetUnknown (-1) since no byte position applies.

func Pos

func Pos(file FilePath, line, column int) Position

Pos is a convenience constructor for Position. It creates a Position with the given file, line, and column. Offset is set to -1 (unset) since byte offset is not provided.

func (Position) Compare

func (p Position) Compare(other Position) int

Compare returns -1, 0, or 1 depending on whether p is less than, equal to, or greater than other. Positions are ordered by file, then line, then column, then offset. This is consistent with Equal: Compare returns 0 iff Equal returns true.

func (Position) Equal

func (p Position) Equal(other Position) bool

Equal reports whether two positions are identical.

func (Position) HasFile added in v1.1.0

func (p Position) HasFile() bool

HasFile reports whether the position has a file path set, regardless of line/column completeness.

func (Position) HasLocation added in v0.4.0

func (p Position) HasLocation() bool

HasLocation reports whether the position has a file and line number.

func (Position) HasOffset

func (p Position) HasOffset() bool

HasOffset reports whether the byte offset is set (not the -1 sentinel). Position{} (the Go zero value) has Offset=0, which HasOffset reports as true — byte 0 is a valid offset. Constructors (Pos, NewRange, FromLSP, SARIF import) set Offset to -1 when no byte offset is available, so HasOffset returns false.

func (Position) IsValid

func (p Position) IsValid() bool

IsValid returns true if the position has a file set and a non-zero line number. Line 0 means "not set" per the sentinel convention, so IsValid returns false for positions that lack a line number.

For checking only whether a file path is present, use Position.HasFile.

func (Position) IsZero added in v0.4.0

func (p Position) IsZero() bool

IsZero reports whether the position is completely uninitialized (all fields at their "unset" sentinel values: empty File, Line=0, Column=0, Offset=-1).

Note: Position{} (the Go zero value) has Offset=0 (byte 0), NOT Offset=-1 (unset), so Position{}.IsZero() returns false. This is correct: Position{} has a valid byte offset of 0, even though it lacks a file path. Use HasLocation() or IsValid() to check for a meaningful position.

func (Position) String

func (p Position) String() string

String returns a human-readable representation.

type Range

type Range struct {
	Start Position `json:"start"` // Required: start position
	End   Position `json:"end"`   // Optional: end position
}

Range represents a span in source code from Start to End.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	r := finding.NewRange("main.go", 10, 1, 15, 20)

	p := finding.Position{File: "main.go", Line: 12, Column: 5}
	fmt.Println("Contains:", r.Contains(p))
	fmt.Println("Valid:", r.IsValid())
	fmt.Println("HasEnd:", r.HasEnd())

}
Output:
Contains: true
Valid: true
HasEnd: true

func NewRange

func NewRange(file FilePath, startLine, startCol, endLine, endCol int) Range

NewRange creates a Range with the given file, start/end lines, and columns. Offsets are set to -1 (unset) since byte offsets are not provided.

func NewRangePtr

func NewRangePtr(file FilePath, startLine, startCol, endLine, endCol int) *Range

NewRangePtr creates a pointer to a Range with the given file, start/end lines, and columns.

func (Range) Adjacent

func (r Range) Adjacent(other Range) bool

Adjacent reports whether this range is immediately adjacent to another range. Adjacent means one range ends exactly where the other begins.

func (Range) Compare

func (r Range) Compare(other Range) int

Compare returns -1, 0, or 1 depending on whether r is less than, equal to, or greater than other. Ranges are ordered by start position, then end position.

func (Range) Contains

func (r Range) Contains(p Position) bool

Contains reports whether the position is within the range. Checks same file, line range, and offset when line ranges aren't available.

func (Range) EndOffsetOrStart added in v1.1.0

func (r Range) EndOffsetOrStart() int

EndOffsetOrStart returns the effective end byte offset of the range. A range with no offset end (End.Offset < 0) is treated as a single point at Start. This is the offset-based counterpart to EndOrStart.

func (Range) EndOrStart added in v1.1.0

func (r Range) EndOrStart() Position

EndOrStart returns the effective end position of the range. A range with no end (End.Line == 0) is treated as a single point at Start. This matches the convention used for line-based range arithmetic (overlap, intersection, extension), where a single-point range's effective end equals its start.

func (Range) Equal

func (r Range) Equal(other Range) bool

Equal reports whether two ranges are identical.

func (Range) HasEnd

func (r Range) HasEnd() bool

HasEnd returns true if the range has an end position set.

func (Range) Intersection

func (r Range) Intersection(other Range) *Range

Intersection returns the overlapping region of two ranges, or nil if they don't overlap.

func (Range) IsInverted added in v0.4.0

func (r Range) IsInverted() bool

IsInverted returns true if the range has both Start and End lines set and End is before Start.

func (Range) IsSingleLine added in v0.4.0

func (r Range) IsSingleLine() bool

IsSingleLine reports whether the range spans exactly one line.

func (Range) IsValid

func (r Range) IsValid() bool

IsValid returns true if the range has a valid start position.

func (Range) Length

func (r Range) Length() int

Length returns the byte length of the range (End.Offset - Start.Offset). Returns 0 if either offset is not set. Returns 0 if End < Start.

func (Range) LineCount

func (r Range) LineCount() int

LineCount returns the number of lines spanned by the range. Returns 1 if End is not set (single-line range). Returns 0 if Start has no line info. For inverted ranges (End.Line < Start.Line), returns the absolute span.

func (Range) Overlaps

func (r Range) Overlaps(other Range) bool

Overlaps reports whether this range overlaps with another range. Two ranges overlap if they share at least one position.

func (Range) SameFile added in v1.1.0

func (r Range) SameFile() bool

SameFile reports whether the range's Start and End are in the same file. Returns true when End.File is empty (single-file convention) or matches Start.File.

type RelatedRef

type RelatedRef struct {
	FindingID ID           `json:"findingId"`       // ID of the related finding
	Relation  RelationKind `json:"relation"`        // e.g., RelationCloneOf, RelationCauses
	Position  Position     `json:"position"`        // Quick access to related location
	Range     *Range       `json:"range,omitempty"` // Span of the related location
}

RelatedRef links to another finding.

func (RelatedRef) IsValid

func (r RelatedRef) IsValid() bool

IsValid returns true if the reference has a non-empty FindingID and a valid Relation.

type RelationKind added in v0.5.0

type RelationKind string

RelationKind describes the type of relationship between two findings.

const (
	RelationCloneOf RelationKind = "clone-of"
	RelationCauses  RelationKind = "causes"
	RelationWraps   RelationKind = "wraps"
	RelationRelated RelationKind = "related"
)

Standard relation kinds for relating findings.

func (RelationKind) IsValid added in v1.1.0

func (r RelationKind) IsValid() bool

IsValid returns true if the relation kind is a recognized standard value.

type Report

type Report struct {
	Tool ToolInfo `json:"tool"` // Tool metadata

	Summary Summary `json:"summary"` // Aggregated statistics
	// contains filtered or unexported fields
}

Report is the top-level container for a tool run. The zero value is safe for concurrent use. Use NewReport to create a Report with pre-allocated findings. All methods are safe for concurrent use. Read methods (FindByID, Len, ActiveFindings, etc.) acquire a read lock; write methods (AddFinding, AddFindings, MergeInto) acquire a write lock.

func Combine added in v0.5.0

func Combine(reports []*Report, opts ...MergeOption) *Report

Combine merges multiple reports into a new report with optional deduplication. The resulting report has:

  • Tool.Name = mergedToolName (unless there's only one report)
  • Findings from all reports
  • Summary computed from all findings

Use Report.MergeInto(other) to combine two reports without mutation.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	r1 := finding.NewReport(finding.ToolInfo{Name: "tool-a"})
	r1.AddFinding(finding.Finding{
		ID:       "govet:printf:main.go:10:3",
		Rule:     "printf",
		ToolName: "govet",
		Message:  "fmt.Printf format error",
		Severity: finding.SeverityWarning,
		Position: finding.Pos("main.go", 10, 3),
	})

	r2 := finding.NewReport(finding.ToolInfo{Name: "tool-b"})
	r2.AddFinding(finding.Finding{
		ID:       "staticcheck:SA1000:main.go:20:1",
		Rule:     "SA1000",
		ToolName: "staticcheck",
		Message:  "invalid regular expression",
		Severity: finding.SeverityError,
		Position: finding.Pos("main.go", 20, 1),
	})

	merged := finding.Combine([]*finding.Report{r1, r2})
	fmt.Println("Total:", merged.Summary.Total)

}
Output:
Total: 2
Example (Deduplication)
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	duplicate := finding.Finding{
		ID:       "same-id",
		Rule:     "R1",
		Position: finding.Position{File: "a.go"},
	}

	r1 := finding.NewReport(finding.ToolInfo{Name: "tool-a"})
	r1.AddFinding(duplicate)

	r2 := finding.NewReport(finding.ToolInfo{Name: "tool-b"})
	r2.AddFinding(duplicate)

	merged := finding.Combine([]*finding.Report{r1, r2})
	fmt.Println("After dedup:", merged.Summary.Total)

}
Output:
After dedup: 1

func NewReport

func NewReport(tool ToolInfo) *Report

NewReport creates a new report with the given tool info.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	report := finding.NewReport(finding.ToolInfo{Name: "mytool", Version: "1.0.0"})
	report.AddFinding(finding.Finding{
		ID:          "mytool:RULE001:main.go:5:1",
		Rule:        "RULE001",
		ToolName:    "mytool",
		Message:     "unused variable",
		Severity:    finding.SeverityWarning,
		Category:    finding.CategoryCorrectness,
		FixStrategy: finding.FixStrategySuggest,
		Suggestion:  "Remove the unused variable",
		Position:    finding.Pos("main.go", 5, 1),
	})
	report.ComputeSummary()

	fmt.Println("Total:", report.Summary.Total)
	fmt.Println("Files:", report.Summary.FilesAffected)

}
Output:
Total: 1
Files: 1

func NewReportFromFindings added in v1.3.0

func NewReportFromFindings(tool ToolInfo, findings []Finding) *Report

NewReportFromFindings creates a report from tool info and a findings slice, calling AddFindings and ComputeSummary in one step. This eliminates the repetitive 4-line NewReport + AddFindings + ComputeSummary boilerplate duplicated across consumers.

func ReportFromJSON

func ReportFromJSON(data []byte) (*Report, int, error)

ReportFromJSON parses a Report from JSON and validates required fields. Invalid findings are silently dropped. Use the returned count to detect data loss.

func (*Report) ActiveFindings

func (r *Report) ActiveFindings() []Finding

ActiveFindings returns all non-suppressed findings. Uses time.Now() for suppression expiry checks. For deterministic results in tests, filter Findings directly with IsSuppressedAt. Safe for concurrent use.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	report := finding.NewReport(finding.ToolInfo{Name: "tool"})
	report.AddFinding(finding.Finding{
		ID: "1", Rule: "R1", Position: finding.Position{File: "a.go"},
	})
	report.AddFinding(finding.Finding{
		ID: "2", Rule: "R2", Position: finding.Position{File: "b.go"},
		Suppression: &finding.Suppression{Kind: finding.SuppressionInSource, Rule: "R2"},
	})

	active := report.ActiveFindings()
	fmt.Println("Active:", len(active))

}
Output:
Active: 1

func (*Report) AddFinding

func (r *Report) AddFinding(f Finding)

AddFinding adds a finding to the report. Safe for concurrent use.

func (*Report) AddFindings

func (r *Report) AddFindings(findings []Finding)

AddFindings adds multiple findings to the report. Safe for concurrent use.

func (*Report) All added in v0.2.0

func (r *Report) All() iter.Seq[Finding]

All returns all findings in the report (including suppressed). The yielded Finding values are shallow copies; modifications to value fields do not affect the report, but mutations to slice/map fields (Tags, Related, Metadata) will be shared. Use Clone() for a deep copy.

IMPORTANT: The returned iterator holds a read lock for the duration of iteration. You MUST exhaust the iterator (e.g., with a break or range) to release the lock. If you need a snapshot without holding the lock, call ActiveFindings() or use Filter.

func (*Report) ByCategory

func (r *Report) ByCategory(cat Category) []Finding

ByCategory returns findings filtered by category, excluding suppressed. For composable filtering, use filter.ByCategory with filter.NotSuppressed instead. Safe for concurrent use.

func (*Report) ByFixStrategy

func (r *Report) ByFixStrategy(fs FixStrategy) []Finding

ByFixStrategy returns findings filtered by fix strategy, excluding suppressed. For composable filtering, use filter.ByFixStrategy with filter.NotSuppressed instead. Safe for concurrent use.

func (*Report) BySeverity

func (r *Report) BySeverity(sev Severity) []Finding

BySeverity returns findings filtered by severity, excluding suppressed. For composable filtering, use filter.BySeverity with filter.NotSuppressed instead. Safe for concurrent use.

func (*Report) ComputeSummary

func (r *Report) ComputeSummary()

ComputeSummary recalculates the summary from the current findings. Uses time.Now() for suppression expiry checks. For deterministic results in tests, use ComputeSummaryAt. Safe for concurrent use with AddFinding/AddFindings.

func (*Report) ComputeSummaryAt added in v0.4.0

func (r *Report) ComputeSummaryAt(now time.Time)

ComputeSummaryAt recalculates the summary using the given time for suppression expiry checks. Use this in tests for deterministic results.

func (*Report) CountBySeverity added in v0.4.0

func (r *Report) CountBySeverity(sev Severity) int

CountBySeverity returns the count of findings for the given severity, including suppressed findings. Uses the pre-computed summary.

func (*Report) Filter added in v0.2.1

func (r *Report) Filter(predicates ...FilterFunc) *Report

Filter returns a new report containing only findings that match all predicates. Safe for concurrent use.

func (*Report) FindByID

func (r *Report) FindByID(id ID) *Finding

FindByID returns the finding with the given ID, or nil if not found. The returned Finding is a shallow copy; modifications to value fields do not affect the report, but mutations to slice/map fields (Tags, Related, Metadata) will be shared. Use Clone() for a deep copy. Safe for concurrent use.

func (*Report) FindByRule

func (r *Report) FindByRule(rule RuleName) []Finding

FindByRule returns all non-suppressed findings matching the given rule name. Safe for concurrent use.

func (*Report) FindingsSnapshot added in v0.5.0

func (r *Report) FindingsSnapshot() []Finding

FindingsSnapshot returns a deep copy of all findings in the report. The returned slice is safe for concurrent use without holding any lock, making it suitable for v1.0 migration from direct Findings slice access. Each Finding is fully cloned via Clone(), so mutations are isolated.

func (*Report) GroupFindings added in v1.7.0

func (r *Report) GroupFindings() map[GroupID][]Finding

GroupFindings returns findings grouped by GroupID, excluding suppressed. Findings without a GroupID are omitted. Map iteration order is unspecified; when deterministic group order matters (tests, serialization, stable CLI output), use Report.GroupFindingsSorted. Findings within a group preserve report order in both variants. Safe for concurrent use.

func (*Report) GroupFindingsSorted added in v1.8.0

func (r *Report) GroupFindingsSorted() []Group

GroupFindingsSorted returns the GroupID groups in deterministic order (sorted by GroupID), excluding suppressed findings. It is the ordered variant of Report.GroupFindings for tests, serialization, and stable output. Returns nil when no grouped findings exist. Safe for concurrent use.

Example

ExampleReport_GroupFindingsSorted demonstrates deterministic grouping of findings by GroupID (e.g. clone groups from art-dupl): groups come back sorted by GroupID, findings inside a group keep report order, and findings without a GroupID are omitted.

package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	report := finding.NewReport(finding.ToolInfo{Name: "clone-detector"})

	build := func(group, file string, line int) finding.Finding {
		return finding.NewBuilder(
			finding.RuleName("duplicate-code"), finding.ToolName("clone-detector"),
			"duplicated block",
			finding.SeverityInfo,
			finding.Pos(finding.FilePath(file), line, 1),
		).WithGroupID(finding.GroupID(group)).BuildOrDefault()
	}

	report.AddFinding(build("grp-2", "b.go", 10))
	report.AddFinding(build("grp-1", "a.go", 5))
	report.AddFinding(build("grp-2", "c.go", 1))
	report.AddFinding(build("", "solo.go", 3)) // no group -> omitted

	groups := report.GroupFindingsSorted()

	for _, g := range groups {
		fmt.Printf("%s:", g.ID)

		for _, f := range g.Findings {
			fmt.Printf(" %s:%d", f.Position.File, f.Position.Line)
		}

		fmt.Println()
	}

}
Output:
grp-1: a.go:5
grp-2: b.go:10 c.go:1

func (*Report) JSON added in v1.2.1

func (r *Report) JSON() (string, error)

JSON returns a compact JSON representation of the report. Shorthand for MarshalJSON that returns a string.

func (*Report) Len added in v0.1.3

func (r *Report) Len() int

Len returns the number of findings in the report. Safe for concurrent use.

func (*Report) Map added in v0.2.1

func (r *Report) Map(fn func(Finding) Finding) *Report

Map returns a new report with the given function applied to each finding. Safe for concurrent use.

func (*Report) MarshalJSON added in v1.0.0

func (r *Report) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (*Report) MergeInto added in v0.5.0

func (r *Report) MergeInto(other *Report) *Report

MergeInto returns a new Report containing findings from both r and other. Neither receiver nor other is modified. The new report uses r's ToolInfo.

func (*Report) PrettyJSON

func (r *Report) PrettyJSON() (string, error)

PrettyJSON returns a formatted JSON representation of the report. Includes all findings, including suppressed ones.

func (*Report) PrettyJSONFiltered added in v0.5.0

func (r *Report) PrettyJSONFiltered() (string, error)

PrettyJSONFiltered returns a formatted JSON representation with only active (non-suppressed) findings. Unlike PrettyJSON, this excludes suppressed findings from the output.

func (*Report) ToSARIF

func (r *Report) ToSARIF() ([]byte, error)

ToSARIF converts a Report to SARIF 2.1.0 format. Suppressed findings are excluded by default.

For full round-trip fidelity including suppression data, use ToSARIFWithOpts(WithIncludeSuppressed()).

Example
package main

import (
	"encoding/json/v2"
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	report := finding.NewReport(finding.ToolInfo{Name: "mytool", Version: "1.0.0"})
	pos := finding.Pos("main.go", 1, 1)
	f := finding.Finding{
		Severity: finding.SeverityWarning,
		ID:       "mytool:R1:main.go:1:1",
		Rule:     "R1",
		ToolName: "mytool",
		Message:  "test finding",
		Position: pos,
	}
	report.AddFinding(f)

	data, err := report.ToSARIF()
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	var log struct {
		Version string `json:"version"`
	}

	_ = json.Unmarshal(data, &log)
	fmt.Println("SARIF version:", log.Version)

}
Output:
SARIF version: 2.1.0

func (*Report) ToSARIFWithOpts added in v1.1.0

func (r *Report) ToSARIFWithOpts(opts ...SARIFOption) ([]byte, error)

ToSARIFWithOpts converts a Report to SARIF 2.1.0 format with the given options. See WithIncludeSuppressed and WithMinSeverity.

func (*Report) UnmarshalJSON added in v1.0.0

func (r *Report) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. Safe for concurrent use — acquires a write lock.

func (*Report) Validate added in v0.4.0

func (r *Report) Validate() error

Validate returns an error if the Report is invalid. It checks Tool info and validates each finding, returning joined errors. Safe for concurrent use.

func (*Report) WithFinding added in v1.2.1

func (r *Report) WithFinding(f Finding) *Report

WithFinding adds a finding and returns the report for chaining. Example: report.WithFinding(f1).WithFinding(f2).

func (*Report) WriteJSON added in v0.2.1

func (r *Report) WriteJSON(w io.Writer) error

WriteJSON writes pretty-printed JSON directly to w. Avoids the intermediate string allocation of PrettyJSON. Safe for concurrent use.

func (*Report) WriteSARIF added in v0.2.1

func (r *Report) WriteSARIF(ctx context.Context, w io.Writer) error

WriteSARIF writes the report in SARIF 2.1.0 format directly to w. Streams via json.Encoder, avoiding the intermediate []byte buffer of ToSARIF. The context is checked for cancellation before encoding begins. Suppressed findings are excluded by default.

For full round-trip fidelity including suppression data, use WriteSARIFWithOpts(w, WithIncludeSuppressed()).

func (*Report) WriteSARIFWithOpts added in v1.1.0

func (r *Report) WriteSARIFWithOpts(ctx context.Context, w io.Writer, opts ...SARIFOption) error

WriteSARIFWithOpts writes the report in SARIF 2.1.0 format with the given options. See WithIncludeSuppressed and WithMinSeverity.

func (*Report) WriteTo added in v0.4.0

func (r *Report) WriteTo(w io.Writer) (int64, error)

WriteTo writes the report in SARIF 2.1.0 format to w and returns the bytes written. Implements io.WriterTo, enabling use with io.Copy for streaming SARIF output.

For context-aware cancellation, prefer WriteSARIF directly.

type RuleName added in v1.0.0

type RuleName string

RuleName is the rule or check name (e.g., "nilcheck", "STRONG_ID").

type SARIFOption added in v1.1.0

type SARIFOption func(*sarifExportConfig)

SARIFOption configures SARIF export behavior.

func WithIncludeSuppressed added in v1.1.0

func WithIncludeSuppressed() SARIFOption

WithIncludeSuppressed causes SARIF export to include suppressed findings with their suppression metadata emitted as SARIF suppression entries, rather than dropping them entirely. This enables full round-trip fidelity for suppression data through SARIF export→import.

func WithMinSeverity added in v1.1.0

func WithMinSeverity(sev Severity) SARIFOption

WithMinSeverity filters findings below the given severity level.

type Severity

type Severity string

Severity represents the severity level of a finding.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	fmt.Println(finding.SeverityInfo)
	fmt.Println(finding.SeverityWarning)
	fmt.Println(finding.SeverityError)
	fmt.Println(finding.SeverityCritical)

	fmt.Println(finding.SeverityError.GreaterThan(finding.SeverityWarning))
	fmt.Println(finding.SeverityInfo.LessThan(finding.SeverityCritical))

}
Output:
info
warning
error
critical
true
true
const (
	SeverityInfo     Severity = "info"
	SeverityWarning  Severity = "warning"
	SeverityError    Severity = "error"
	SeverityCritical Severity = "critical"
)

Severity levels for findings, ordered by urgency.

func FromSARIFLevel

func FromSARIFLevel(level string) Severity

FromSARIFLevel converts a SARIF level back to Severity. Lossy: both SeverityCritical and SeverityError map to SARIF "error", so FromSARIFLevel("error") returns SeverityError. For full fidelity, read the "go-finding/severity" property from the result instead.

func LookupSeverityAlias added in v1.0.0

func LookupSeverityAlias(name string) (Severity, bool)

LookupSeverityAlias returns the canonical Severity for the given alias, if it exists. This is safe to call concurrently.

func MustParseSeverity added in v0.4.0

func MustParseSeverity(s string) Severity

MustParseSeverity parses a string into a Severity, panicking on invalid input.

func ParseSeverity added in v0.4.0

func ParseSeverity(s string) (Severity, error)

ParseSeverity parses a string into a Severity. It accepts the canonical names (info, warning, error, critical) and common aliases (warn, high, medium, low, fatal, note, advice, suggestion) registered via RegisterSeverityAlias. Returns an error if the string is not a valid severity level or alias.

Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	s, err := finding.ParseSeverity("warn")
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	fmt.Println(s)

	s2 := finding.MustParseSeverity("high")
	fmt.Println(s2)

}
Output:
warning
error

func SeverityFromLevel added in v1.3.0

func SeverityFromLevel(level string, fallback Severity) Severity

SeverityFromLevel maps a severity level string to a canonical Severity. It tries canonical names first, then registered aliases, and falls back to the provided fallback if the level is unrecognized. This eliminates the severity-mapping switch statements that every consumer independently writes.

func (Severity) Badge added in v0.6.0

func (s Severity) Badge() string

Badge returns a human-readable severity badge with emoji (e.g., "🔴 CRITICAL"). Derived from Severity.Emoji and the uppercased severity name. Returns the string value unchanged for unknown severities.

func (Severity) Compare added in v0.1.3

func (s Severity) Compare(other Severity) int

Compare returns -1, 0, or 1 depending on whether s is less than, equal to, or greater than other. Invalid severities rank below all valid ones. Two different invalid severities are ordered lexicographically to ensure a total ordering.

func (Severity) Emoji added in v0.6.0

func (s Severity) Emoji() string

Emoji returns the emoji representing the severity level. Returns an empty string for unknown severities.

func (Severity) GreaterThan

func (s Severity) GreaterThan(other Severity) bool

GreaterThan returns true if this severity is greater than the other. Order: info < warning < error < critical.

func (Severity) GreaterThanOrEqual

func (s Severity) GreaterThanOrEqual(other Severity) bool

GreaterThanOrEqual returns true if this severity is greater than or equal to the other.

func (Severity) IsValid

func (s Severity) IsValid() bool

IsValid returns true if the severity is a valid value.

func (Severity) LessThan

func (s Severity) LessThan(other Severity) bool

LessThan returns true if this severity is less than the other.

func (Severity) LessThanOrEqual

func (s Severity) LessThanOrEqual(other Severity) bool

LessThanOrEqual returns true if this severity is less than or equal to the other.

func (Severity) PriorityString added in v1.3.0

func (s Severity) PriorityString() string

PriorityString returns a priority label for the severity, which is the reverse mapping of severity levels to common priority terms. Critical→"critical", Error→"high", Warning→"medium", Info→"low". Returns the string value unchanged for unknown severities.

func (Severity) String

func (s Severity) String() string

String returns the string representation of the severity.

type SimpleFixResult added in v1.3.0

type SimpleFixResult struct {
	FindingID ID     // The finding that was processed
	Applied   bool   // Whether the replacement was applied
	Reason    string // Why it was skipped (when Applied is false)
}

SimpleFixResult records the outcome of applying a single BeforeCode→AfterCode fix.

type Summary

type Summary struct {
	Total         int                 `json:"total"`                   // Total findings
	BySeverity    map[Severity]int    `json:"bySeverity"`              // Count by severity
	ByCategory    map[Category]int    `json:"byCategory,omitempty"`    // Count by category
	ByFixStrategy map[FixStrategy]int `json:"byFixStrategy,omitempty"` // Count by fix strategy
	FilesAffected int                 `json:"filesAffected,omitempty"` // Unique files with findings
	FilesScanned  int                 `json:"filesScanned,omitempty"`  // Total files scanned (including clean files)
	Suppressed    int                 `json:"suppressed,omitempty"`    // Count of suppressed findings
}

Summary contains aggregated statistics for a report.

type Suppression

type Suppression struct {
	Kind      SuppressionKind `json:"kind"`                // Where the suppression is defined
	Rule      RuleName        `json:"rule"`                // Which rule is suppressed
	Reason    string          `json:"reason"`              // Why it's suppressed
	ExpiresAt *time.Time      `json:"expiresAt,omitempty"` // Optional expiry
}

Suppression represents a suppressed finding.

func (*Suppression) IsActive added in v0.4.0

func (s *Suppression) IsActive(now time.Time) bool

IsActive returns true if the suppression is valid and not expired. This combines IsValid and !IsExpired into a single check.

func (*Suppression) IsExpired

func (s *Suppression) IsExpired(now time.Time) bool

IsExpired returns true if the suppression has expired relative to now. A suppression is expired when now is strictly after ExpiresAt. At the exact ExpiresAt instant, the suppression is still considered active (valid through that moment, expired any time after).

func (*Suppression) IsValid

func (s *Suppression) IsValid() bool

IsValid returns true if the suppression has a valid kind and a non-empty rule. The kind must be one of the predefined SuppressionKind constants (checked via Kind.IsValid), not just any non-empty string. This prevents typos like "in-source" from silently passing validation.

type SuppressionKind

type SuppressionKind string

SuppressionKind indicates where a suppression was defined.

const (
	SuppressionInSource SuppressionKind = "in-source" // e.g., //nolint, //lint:ignore
	SuppressionInConfig SuppressionKind = "in-config" // Config file rules
	SuppressionInReview SuppressionKind = "in-review" // Accepted as false positive
)

Suppression kinds indicate where a suppression was defined.

func (SuppressionKind) IsValid

func (k SuppressionKind) IsValid() bool

IsValid returns true if the suppression kind is a recognized value.

type Tag added in v0.2.1

type Tag string

Tag is a sub-classification label for a finding.

const (
	TagSecurity      Tag = "security"
	TagPerformance   Tag = "performance"
	TagStyle         Tag = "style"
	TagCorrectness   Tag = "correctness"
	TagBug           Tag = "bug"
	TagDeprecated    Tag = "deprecated"
	TagDocumentation Tag = "documentation"
	TagComplexity    Tag = "complexity"
	TagTest          Tag = "test"
	TagBuild         Tag = "build"
)

Standard tag constants for common classification labels.

func (Tag) IsStandard added in v0.4.0

func (t Tag) IsStandard() bool

IsStandard returns true if the tag is one of the predefined standard constants. Use IsStandard for allow-list filtering (e.g., "only show findings with known tags"). Use IsValid for input validation (accepts any well-formed custom tag).

func (Tag) IsValid added in v0.4.0

func (t Tag) IsValid() bool

IsValid returns true if the tag is a non-empty lowercase-hyphenated string. This rejects typos like "Security" or "SOME_TAG". Custom tags are valid as long as they match the format. Use IsValid for input validation (accepts custom values). Use IsStandard to check for predefined constants only (allow-list filtering).

func (Tag) String added in v0.4.0

func (t Tag) String() string

String returns the string representation of the tag.

type Template added in v1.3.0

type Template struct {
	Tool        ToolName
	Category    Category
	FixStrategy FixStrategy
	Tags        []Tag
	GroupID     GroupID
}

Template is a pre-configured builder factory: stamp common fields (tool name, category, fix strategy, tags) once, then build many findings with varying rule/message/severity/position. This eliminates the newMigrationFinding / buildFixableFinding / IssueBuilderFactory patterns that consumers reinvent for batch finding creation.

func NewTemplate added in v1.3.0

func NewTemplate(toolName ToolName) *Template

NewTemplate creates a Template with the given tool name. Chain WithCategory, WithFixStrategy, WithTags to configure common fields, then call Build for each finding.

func (*Template) Build added in v1.3.0

func (t *Template) Build(rule RuleName, message string, severity Severity, pos Position) Finding

Build creates a Finding from the template, stamping the pre-configured tool name, category, fix strategy, and tags. Returns a zero-value Finding if validation fails (delegates to Builder.BuildOrDefault).

For per-finding confidence, suggestion, or other overrides, use Template.Builder instead — it returns a *Builder for further chaining before terminal Build.

func (*Template) Builder added in v1.6.0

func (t *Template) Builder(rule RuleName, message string, severity Severity, pos Position) *Builder

Builder creates a pre-configured *Builder from the template, stamping the pre-configured tool name, category, fix strategy, and tags. Unlike [Build], which returns a final Finding, Builder returns the intermediate *Builder so the caller can chain additional per-finding fields (confidence, suggestion, before/after code, metadata, etc.) before calling Builder.Build, Builder.MustBuild, or Builder.BuildOrDefault.

This eliminates the per-consumer factory wrapper (e.g. makeFindingWithConfidence) that every linter reinvents when it needs both template-level defaults AND per-finding confidence/suggestion:

tmpl := finding.NewTemplate("my-linter").
    WithCategory(finding.CategoryStyle).
    WithFixStrategy(finding.FixStrategySuggest)

f := tmpl.Builder(rule, msg, finding.SeverityWarning, pos).
    WithConfidence(finding.ConfidenceHigh).
    WithSuggestion("use foo.Bar() instead").
    MustBuild()
Example
package main

import (
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	tmpl := finding.NewTemplate("my-linter").
		WithCategory(finding.CategoryStyle).
		WithFixStrategy(finding.FixStrategySuggest)

	f := tmpl.Builder("R1", "bad pattern", finding.SeverityWarning, finding.Pos("demo.go", 42, 3)).
		WithConfidence(finding.ConfidenceHigh).
		WithSuggestion("use humanize.Bytes instead").
		MustBuild()

	fmt.Println(f.Rule)
	fmt.Println(f.ToolName)
	fmt.Println(f.Category)
	fmt.Println(f.Confidence)
	fmt.Println(f.Suggestion)

}
Output:
R1
my-linter
style
high
use humanize.Bytes instead

func (*Template) WithCategory added in v1.3.0

func (t *Template) WithCategory(cat Category) *Template

WithCategory sets the category on the template.

func (*Template) WithFixStrategy added in v1.3.0

func (t *Template) WithFixStrategy(fs FixStrategy) *Template

WithFixStrategy sets the fix strategy on the template.

func (*Template) WithGroupID added in v1.8.0

func (t *Template) WithGroupID(g GroupID) *Template

WithGroupID sets the logical group on the template. Every finding built from it joins the same group (e.g. N occurrences of one cloned block). Callers that need per-finding groups should use Builder.WithGroupID on the per-finding builder instead of a template-level stamp.

func (*Template) WithTags added in v1.3.0

func (t *Template) WithTags(tags ...Tag) *Template

WithTags sets tags on the template. These are stamped onto every finding built from this template.

type ToolAdapter added in v0.7.0

type ToolAdapter[O any] struct {
	// contains filtered or unexported fields
}

ToolAdapter converts output from an external tool into Findings. It implements the Detector interface, so it can be used directly with the pipeline package.

Type parameter O is the tool-specific output type that JSON decodes into.

Example
package main

import (
	"context"
	"encoding/json/v2"
	"fmt"

	"github.com/larsartmann/go-finding"
)

func main() {
	type lintOutput struct {
		Diagnostics []struct {
			Rule    string `json:"rule"`
			Message string `json:"message"`
			File    string `json:"file"`
			Line    int    `json:"line"`
		} `json:"diagnostics"`
	}

	parse := func(data []byte) (lintOutput, error) {
		var out lintOutput

		err := json.Unmarshal(data, &out)

		return out, err
	}

	convert := func(out lintOutput) ([]finding.Finding, error) {
		findings := make([]finding.Finding, 0, len(out.Diagnostics))
		for _, d := range out.Diagnostics {
			findings = append(findings, finding.Finding{
				ID: finding.GenerateID(
					"mylint",
					finding.RuleName(d.Rule),
					finding.Position{File: finding.FilePath(d.File), Line: d.Line},
				),
				Rule:     finding.RuleName(d.Rule),
				ToolName: "mylint",
				Message:  d.Message,
				Severity: finding.SeverityError,
				Category: finding.CategoryForLinter(d.Rule),
				Position: finding.Position{File: finding.FilePath(d.File), Line: d.Line},
			})
		}

		return findings, nil
	}

	run := func(_ context.Context) ([]byte, error) {
		return json.Marshal(lintOutput{
			Diagnostics: []struct {
				Rule    string `json:"rule"`
				Message string `json:"message"`
				File    string `json:"file"`
				Line    int    `json:"line"`
			}{
				{Rule: "no-unused", Message: "unused variable", File: "main.go", Line: 10},
			},
		})
	}

	adapter := finding.NewToolAdapter("mylint", run, parse, convert)

	findings, err := adapter.Detect(context.Background())
	if err != nil {
		fmt.Println("error:", err)

		return
	}

	fmt.Println("count:", len(findings))
	fmt.Println("rule:", findings[0].Rule)

}
Output:
count: 1
rule: no-unused

func NewToolAdapter added in v0.7.0

func NewToolAdapter[O any](
	name string,
	run ToolRunFunc,
	parse func([]byte) (O, error),
	convert func(O) ([]Finding, error),
) *ToolAdapter[O]

NewToolAdapter creates a ToolAdapter that:

  1. Runs the tool via run,
  2. Parses raw bytes into type O via parse,
  3. Converts O into Findings via convert.

All three functions must be non-nil.

func (*ToolAdapter[O]) Detect added in v0.7.0

func (a *ToolAdapter[O]) Detect(ctx context.Context) ([]Finding, error)

Detect runs the tool, parses its output, and returns Findings. Returns nil (not an empty slice) when the tool produces no output.

func (*ToolAdapter[O]) Name added in v0.7.0

func (a *ToolAdapter[O]) Name() string

Name returns the tool's name.

type ToolInfo

type ToolInfo struct {
	Name    string `json:"name"`              // Tool name
	Version string `json:"version,omitempty"` // Tool version
}

ToolInfo contains metadata about the tool that generated the report.

func (ToolInfo) Validate added in v0.4.0

func (t ToolInfo) Validate() error

Validate returns an error if the ToolInfo is invalid. A valid ToolInfo requires a non-empty Name.

type ToolName added in v1.0.0

type ToolName string

ToolName is the source tool name (e.g., "govet", "staticcheck").

type ToolRunFunc added in v0.7.0

type ToolRunFunc func(ctx context.Context) ([]byte, error)

ToolRunFunc executes an external tool and returns its raw output. The function receives the context for cancellation and timeout propagation.

Directories

Path Synopsis
analysis module
cmd
go-finding module
examples
basic command
basic demonstrates creating a Finding and Report from scratch.
basic demonstrates creating a Finding and Report from scratch.
builder command
builder demonstrates the fluent Finding builder API.
builder demonstrates the fluent Finding builder API.
Package gotoken provides shared utilities for working with go/token and go/ast.
Package gotoken provides shared utilities for working with go/token and go/ast.
Package lockutil provides generic helpers for sync.Mutex- and sync.RWMutex-protected critical sections.
Package lockutil provides generic helpers for sync.Mutex- and sync.RWMutex-protected critical sections.
pipeline module
toolsdk module

Jump to

Keyboard shortcuts

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