finding

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: MIT Imports: 19 Imported by: 0

README

go-finding

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

CI Go Reference Go Report Card 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
  • SARIF 2.1.0 — Standard interchange format for CI/CD integration
  • LSP diagnostics — IDE integration out of the box

Installation

go get github.com/larsartmann/go-finding

Requires Go 1.26 or later.

Quick Start

package main

import (
    "fmt"
    "github.com/larsartmann/go-finding"
)

func main() {
    f := finding.NewFinding(
        "unused-var", "my-tool",
        "variable x is unused",
        finding.SeverityWarning,
        finding.Position{File: "main.go", Line: 42, Column: 5},
        0.95,
    )

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

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

Builder API

Construct findings fluently with the Builder:

f, err := finding.NewBuilder("nilcheck", "govet", "possible nil deref",
    finding.SeverityError, finding.Pos("main.go", 42, 5)).
    WithFixStrategy(finding.FixStrategyDirect).
    WithBeforeCode("x.foo").
    WithAfterCode("x.foo()").
    WithConfidence(0.95).
    Build()
if err != nil {
    log.Fatal(err)
}

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.

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.HasFix,
)

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

Merging

Combine reports from multiple tools with deduplication:

merged := finding.Merge([]*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.Confidence, c.Reason)
}

Pipeline

The pipeline package provides a detect → triage → fix → verify loop:

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 := pipeline.New(cfg, ".", detector)
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
Conflict detection Overlapping fixes filtered before application
Fix application AST-aware with text fallback, 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
Dry run Detect + triage without applying fixes
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}, 0.9),
    }
    return findings, nil
}

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" (SARIF 2.1.0 has no critical level); the original severity is stored in Properties["go-finding/severity"].

LSP Diagnostics

lspDiag := f.ToLSP()

// From LSP diagnostic
f := finding.FromLSP(lspDiag, "my-tool")

go/analysis Integration

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

// Note: Converting back to analysis.Diagnostic is not yet supported.

JSON

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

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

// Line-delimited JSON stream
data, dropped := finding.LineJSON(findings)

// 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.GetCategory(err) // "validation", "io", "conflict", etc.

Tools Using This SDK

CLI

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

go-finding run --format sarif --output results.sarif
go-finding run --format json --config config.yaml

Development

just test       # Run tests with -race
just bench      # Run benchmarks
just lint       # golangci-lint
just cover      # Coverage report
just check      # All checks (fmt + lint + test)

See CONTRIBUTING.md for guidelines.

Project Stats

Package Coverage
Root 99.4%
Pipeline 98.0%
Detectors 96.1%
CLI 96.2%
Total 95.5%

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)
  • Fix strategies (none, suggest, direct, ai)
  • Position tracking with range support
  • SARIF 2.1.0 output generation
  • LSP Diagnostic conversion
  • go/analysis integration
  • Report merging and filtering

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},
}

Create a report:

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

Output as SARIF:

sarifJSON, err := report.ToSARIF()

Core Types

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

  • Finding: A single issue detected by a tool
  • Report: Container for all findings from a tool run
  • Severity: info, warning, error, critical
  • FixStrategy: none, suggest, direct, ai
  • Position: File, line, column location
  • Range: Start and end positions

Filtering

Filter findings using predicates:

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

Converting from go/analysis

Convert from the standard Go analysis framework:

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

Pipeline

The package includes a pipeline for automated fixing:

  1. Detect: Run tools and collect findings
  2. Triage: Route by fix strategy
  3. Fix: Apply direct fixes, route AI fixes
  4. Verify: Re-run and validate

See the pipeline subpackage for details.

Cross-Tool Correlation

Correlate finds related findings across different tools using simple heuristics (same file, nearby lines). It is a standalone utility, not wired into the pipeline:

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

Known Limitations

SeverityCritical maps to SARIF level "error" (SARIF 2.1.0 has no "critical" level). The original severity is preserved in Properties["go-finding/severity"] for round-trip fidelity.

  • go/analysis: The standard Go analysis framework
  • SARIF 2.1.0: Static Analysis Results Interchange Format
  • LSP: Language Server Protocol
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.Merge([]*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.Merge([]*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 (
	LSPSeverityError   = 1 // Error
	LSPSeverityWarning = 2 // Warning
	LSPSeverityInfo    = 3 // Information
	LSPSeverityHint    = 4 // Hint
)

LSP severity level constants per the LSP specification.

View Source
const (
	VersionMajor = 0
	VersionMinor = 2
	VersionPatch = 1
	Version      = "0.2.1"
)

Version constants for programmatic version checking.

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 ErrInvalidBuilder = NewValidationError(
	"finding.Builder: cannot Build() an invalid Finding",
	nil,
)

ErrInvalidBuilder is returned when Builder.Build is called on a Finding that is missing required fields.

Functions

func FilterInvalid added in v0.2.0

func FilterInvalid(f Finding) bool

FilterInvalid returns true if the finding is invalid (has missing required fields).

func FormatDiagnostic

func FormatDiagnostic(d *analysis.Diagnostic, fset *token.FileSet, analyzerName string) string

FormatDiagnostic returns a formatted string for a go/analysis diagnostic. Similar to how go vet formats output.

func GenerateID

func GenerateID(toolName, rule string, pos Position) string

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(hashID))

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

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[string][]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 HasFix

func HasFix(f Finding) bool

HasFix returns a filter for findings with fixes.

func HasSuggestion

func HasSuggestion(f Finding) bool

HasSuggestion returns a filter for findings with suggestions.

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

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
package main

import (
	"fmt"

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

func main() {
	f, err := finding.NewBuilder("staticcheck", "SA1000", "invalid regex", finding.SeverityError, finding.Pos("pkg.go", 24, 8)).
		WithCategory(finding.CategoryCorrectness).
		WithConfidence(0.95).
		WithBeforeCode("oldPattern").
		WithAfterCode("newPattern").
		WithFixStrategy(finding.FixStrategyDirect).
		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, 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.

func (*Builder) Build added in v0.2.0

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

Build returns the constructed Finding. Returns an error if required fields are missing.

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 float64) *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) WithID added in v0.2.0

func (b *Builder) WithID(id string) *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) WithTag added in v0.2.0

func (b *Builder) WithTag(tag string) *Builder

WithTag sets the tag.

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"
)

Standard category constants for findings.

func (Category) IsStandard

func (c Category) IsStandard() bool

IsStandard returns true if the category is one of the predefined standard constants.

func (Category) IsValid

func (c Category) IsValid() bool

IsValid returns true if the category is a non-empty string. Custom categories (e.g. "go-vet") are valid. Use IsStandard to check for predefined constants only.

func (Category) String

func (c Category) String() string

String returns the string representation of the category.

type Correlation

type Correlation struct {
	FindingIDs []string `json:"findingIds"`
	Reason     string   `json:"reason"`     // Why they're correlated
	Confidence float64  `json:"confidence"` // 0.0-1.0
}

Correlation links related findings from different tools.

func Correlate

func Correlate(findings []Finding) []Correlation

Correlate finds potentially related findings across tools. Currently uses simple 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.

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.Confidence, c.Reason)
	}

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

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.

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 GetCategory

func GetCategory(err error) ErrorCategory

GetCategory 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. Custom categories are valid. Use specific constants for predefined values.

type FilterFunc

type FilterFunc func(Finding) bool

FilterFunc is a predicate for filtering findings.

func ByCategory

func ByCategory(cat Category) FilterFunc

ByCategory returns a filter for the given category.

func ByFile

func ByFile(file string) 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 string) 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 string) FilterFunc

ByTool returns a filter for the given tool name.

type Finding

type Finding struct {
	// Identity
	ID       string `json:"id"`       // Stable unique identifier (e.g., "tool:rule:file:42:5")
	Rule     string `json:"rule"`     // Rule/check name (e.g., "STRONG_ID", "clone-detected")
	ToolName string `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.
	// Deprecated: Use Tags instead.
	Tag  string `json:"tag,omitempty"`  // Sub-classification: "phantom-type", "clone", 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  float64      `json:"confidence,omitempty"`  // 0.0-1.0
	Related     []RelatedRef `json:"related,omitempty"`     // Related findings
	Suppression *Suppression `json:"suppression,omitempty"` // If suppressed

	// Extensibility
	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 FindingsFromSARIF

func FindingsFromSARIF(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.

func FromDiagnostic

func FromDiagnostic(
	d *analysis.Diagnostic,
	fset *token.FileSet,
	toolName, ruleCode string,
	defaultSeverity ...Severity,
) Finding

FromDiagnostic converts a go/analysis.Diagnostic to a Finding. The toolName parameter identifies which analyzer produced this. The ruleCode parameter provides a rule identifier (since go/analysis.Diagnostic doesn't have Code). The defaultSeverity is used because go/analysis.Diagnostics don't carry severity; if empty, SeverityWarning is used.

func FromJSON

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

FromJSON parses a Finding from JSON and validates required fields.

func FromLSP

func FromLSP(fileURI string, 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. The raw LSP severity integer is stored in Metadata under "go-finding/lsp-severity".

func NewFinding

func NewFinding(
	rule, toolName, message string,
	severity Severity,
	pos Position,
	confidence float64,
) 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) HasFix

func (f Finding) HasFix() bool

HasFix returns true if this finding has a fix available.

func (Finding) HasSuggestion

func (f Finding) HasSuggestion() bool

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

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.

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. If ID is set, it is returned; otherwise a deterministic key is built from ToolName, Position.File, Rule, and Message.

func (Finding) LineJSON

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

LineJSON returns compact JSON (single line).

func (Finding) NormalizedConfidence added in v0.2.0

func (f Finding) NormalizedConfidence() float64

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. Note: This is a lossy conversion - some fields (FixStrategy, Confidence, etc.) are lost.

func (Finding) Validate added in v0.2.1

func (f Finding) Validate() error

Validate performs comprehensive validation and returns an error if the finding is invalid. It checks all fields that IsValid checks plus additional constraints: FixStrategy validity, Confidence range, and structural consistency.

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     string        // 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.GetCategory(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) 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 (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 LSPDiagnostic

type LSPDiagnostic struct {
	Range    LSPRange         `json:"range"`
	Severity int              `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"`
	Related  []LSPRelatedInfo `json:"relatedInformation,omitempty"`
}

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

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 LSPRelatedInfo

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

LSPRelatedInfo provides related information for a diagnostic.

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 ParsedID

type ParsedID struct {
	Tool   string
	Rule   string
	File   string
	Line   int
	Column int
}

ParsedID holds the components of a parsed finding ID.

func ParseID

func ParseID(id string) 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   string `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. Line and Column are 1-based; 0 means not set. Offset is 0-based; -1 means not set (offset 0 = start of file is valid).

func FromTokenPosition

func FromTokenPosition(pos token.Position) Position

FromTokenPosition creates a Position from a token.Position.

func NodePosition

func NodePosition(fset *token.FileSet, node ast.Node) Position

NodePosition returns a Position from an AST node.

func Pos

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

Pos is a convenience constructor for Position. It creates a Position with the given file, line, and column.

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) HasOffset

func (p Position) HasOffset() bool

HasOffset reports whether the offset is set.

func (Position) IsValid

func (p Position) IsValid() bool

IsValid returns true if the position has a file set and non-negative line/column.

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 string, startLine, startCol, endLine, endCol int) Range

NewRange creates a Range with the given file, start/end lines, and columns.

func NewRangePtr

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

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

func NodeRange

func NodeRange(fset *token.FileSet, node ast.Node) Range

NodeRange returns a Range from an AST node.

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

type RelatedRef

type RelatedRef struct {
	FindingID string   `json:"findingId"` // ID of the related finding
	Relation  string   `json:"relation"`  // e.g., "clone-of", "wraps", "causes"
	Position  Position `json:"position"`  // Quick access to 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.

type Report

type Report struct {
	Tool     ToolInfo  `json:"tool"`     // Tool metadata
	Findings []Finding `json:"findings"` // All findings from this run
	Summary  Summary   `json:"summary"`  // Aggregated statistics
	// contains filtered or unexported fields
}

Report is the top-level container for a tool run. Use NewReport to create a thread-safe instance.

func Merge

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

Merge combines multiple reports into one. The merged report has: - Tool.Name = "merged" (unless there's only one report) - Findings from all reports - Summary computed from all findings Options control deduplication and conflict resolution.

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.Merge([]*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.Merge([]*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 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.

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 copies; modifications do not affect the report.

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.

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.

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.

func (*Report) ComputeSummary

func (r *Report) ComputeSummary()

ComputeSummary recalculates the summary from the current findings.

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.

func (*Report) FindByID

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

FindByID returns the finding with the given ID, or nil if not found. The returned Finding is a copy; modifications do not affect the report.

func (*Report) FindByRule

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

FindByRule returns all non-suppressed findings matching the given rule name.

func (*Report) Len added in v0.1.3

func (r *Report) Len() int

Len returns the number of findings in the report.

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.

func (*Report) PrettyJSON

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

PrettyJSON returns a formatted JSON representation of the report.

func (*Report) ToSARIF

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

ToSARIF converts a Report to SARIF 2.1.0 format.

Round-trip losses: SARIF export→import does not preserve:

  • Suppression data (suppressed findings are excluded from export)

All other fields are preserved via the "properties" bag or related location properties.

Example
package main

import (
	"encoding/json"
	"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) ToSARIFFiltered

func (r *Report) ToSARIFFiltered(minSeverity Severity) ([]byte, error)

ToSARIFFiltered converts non-suppressed findings with severity >= minSeverity to SARIF 2.1.0 format. It filters by BOTH suppression status and severity.

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.

func (*Report) WriteSARIF added in v0.2.1

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

WriteSARIF writes the report in SARIF 2.1.0 format directly to w. This avoids the intermediate buffer allocation of ToSARIF.

func (*Report) WriteSARIFFiltered added in v0.2.1

func (r *Report) WriteSARIFFiltered(w io.Writer, minSeverity Severity) error

WriteSARIFFiltered writes non-suppressed findings with severity >= minSeverity in SARIF 2.1.0 format directly to w.

type SarifArtifactChange

type SarifArtifactChange struct {
	ArtifactLocation SarifArtifactLocation `json:"artifactLocation"`
	Replacements     []SarifReplacement    `json:"replacements"`
}

SarifArtifactChange represents a change to an artifact.

type SarifArtifactLocation

type SarifArtifactLocation struct {
	URI string `json:"uri"`
}

SarifArtifactLocation represents the artifact URI.

type SarifDriver

type SarifDriver struct {
	Name    string `json:"name"`
	Version string `json:"version,omitempty"`
}

SarifDriver represents the main driver tool with version information.

type SarifFix

type SarifFix struct {
	Description SarifMessage          `json:"description"`
	Changes     []SarifArtifactChange `json:"artifactChanges"`
}

SarifFix represents a fix to be applied to the artifact.

type SarifLocation

type SarifLocation struct {
	PhysicalLocation SarifPhysicalLocation `json:"physicalLocation"`
}

SarifLocation represents a location in SARIF format.

type SarifLog

type SarifLog struct {
	Version string     `json:"version"`
	Schema  string     `json:"$schema"`
	Runs    []SarifRun `json:"runs"`
}

SarifLog represents a SARIF log file containing run results.

type SarifMessage

type SarifMessage struct {
	Text string `json:"text"`
}

SarifMessage represents a message in SARIF format.

type SarifPhysicalLocation

type SarifPhysicalLocation struct {
	ArtifactLocation SarifArtifactLocation `json:"artifactLocation"`
	Region           *SarifRegion          `json:"region,omitempty"`
}

SarifPhysicalLocation represents physical details of a location.

type SarifRegion

type SarifRegion struct {
	StartLine   int `json:"startLine,omitempty"`
	StartColumn int `json:"startColumn,omitempty"`
	EndLine     int `json:"endLine,omitempty"`
	EndColumn   int `json:"endColumn,omitempty"`
}

SarifRegion represents a code region in a text document.

type SarifRelatedLoc

type SarifRelatedLoc struct {
	PhysicalLocation SarifPhysicalLocation `json:"physicalLocation"`
	Message          SarifMessage          `json:"message"`
	Properties       map[string]any        `json:"properties,omitempty"`
}

SarifRelatedLoc represents a related location in SARIF.

type SarifReplacement

type SarifReplacement struct {
	DeletedRegion SarifRegion  `json:"deletedRegion"`
	InsertedText  SarifMessage `json:"insertedText"`
}

SarifReplacement represents a replacement of text in an artifact.

type SarifResult

type SarifResult struct {
	RuleID     string            `json:"ruleId"`
	Level      string            `json:"level"`
	Message    SarifMessage      `json:"message"`
	Locations  []SarifLocation   `json:"locations"`
	Fixes      []SarifFix        `json:"fixes,omitempty"`
	Related    []SarifRelatedLoc `json:"relatedLocations,omitempty"`
	Rank       float64           `json:"rank,omitempty"`
	Properties map[string]any    `json:"properties,omitempty"`
}

SarifResult represents a single finding in SARIF format.

type SarifRun

type SarifRun struct {
	Tool    SarifTool     `json:"tool"`
	Results []SarifResult `json:"results"`
}

SarifRun represents a single analysis run in a SARIF log.

type SarifTool

type SarifTool struct {
	Driver SarifDriver `json:"driver"`
}

SarifTool defines the static analysis tool that generated the results.

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 (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) 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) String

func (s Severity) String() string

String returns the string representation of the severity.

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
	DurationMs    int64               `json:"durationMs,omitempty"`    // Execution time
	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      string          `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) IsExpired

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

IsExpired returns true if the suppression has expired relative to now.

func (*Suppression) IsValid

func (s *Suppression) IsValid() bool

IsValid returns true if the suppression has a kind and rule.

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.

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.

Directories

Path Synopsis
analysis module
cmd
go-finding command
Package main implements the go-finding CLI tool.
Package main implements the go-finding CLI tool.
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.
pipeline command
pipeline demonstrates running the detection-fix-verify loop.
pipeline demonstrates running the detection-fix-verify loop.
internal
detectors
Package detectors provides built-in detector implementations that wrap external static analysis tools.
Package detectors provides built-in detector implementations that wrap external static analysis tools.
Package pipeline provides a detect → triage → fix → verify workflow for automated code remediation.
Package pipeline provides a detect → triage → fix → verify workflow for automated code remediation.
toolsdk module

Jump to

Keyboard shortcuts

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