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)
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()
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 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, range [0.0, 1.0]
- FixStrategy: none, suggest, direct, ai (ai is reserved)
- Category: 14 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.ToSARIFFiltered(sev) // filtered by severity + suppression
Import back:
findings, err := finding.FindingsFromSARIF(ctx, data) findings, err := finding.FindingsFromReader(ctx, reader) // streaming
WriteSARIF/WriteSARIFFiltered 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 is lossy: FixStrategy, Confidence, BeforeCode, AfterCode, Suppression, Metadata, Category, and Tags are not preserved through LSP round-trips. 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, GetCategory, IsCategory for programmatic handling. FindingError supports WithFinding and WithPosition for attaching context.
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) // single-line per finding finding.FormatMarkdown(os.Stdout, findings) // markdown table
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
- Metrics collection with snapshots
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.
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 ¶
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.
LSP conversion is lossy: FixStrategy, Confidence, BeforeCode, AfterCode, Suppression, Metadata, Category, and Tags are not preserved through LSP round-trips.
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 ¶
- Constants
- Variables
- func FilterInvalid(f Finding) booldeprecated
- func FormatMarkdown(w io.Writer, findings []Finding) error
- func FormatText(w io.Writer, findings []Finding) error
- func GroupBy(findings []Finding, keyFn func(Finding) string) map[string][]Finding
- func GroupByCategory(findings []Finding) map[Category][]Finding
- func GroupByFile(findings []Finding) map[FilePath][]Finding
- func GroupBySeverity(findings []Finding) map[Severity][]Finding
- func IsCategory(err error, cat ErrorCategory) bool
- func IsFindingError(err error) bool
- func IsHashID(id string) bool
- func IsInvalid(f Finding) bool
- func MergeIter(reports []*Report, opts ...MergeOption) iter.Seq[Finding]
- func NotSuppressed(f Finding) bool
- func RangeLinesEq(a, b Range) bool
- func RegisterLinterCategory(name string, cat Category)
- func RegisterSeverityAlias(name string, sev Severity)
- func SortByPosition(findings []Finding)
- func SortBySeverity(findings []Finding)
- func SortFindingsByID(findings []Finding)
- func WithFix(f Finding) bool
- func WithSuggestion(f Finding) bool
- type Builder
- func (b *Builder) Build() (Finding, error)
- func (b *Builder) MustBuild() Finding
- func (b *Builder) WithAfterCode(code string) *Builder
- func (b *Builder) WithBeforeCode(code string) *Builder
- func (b *Builder) WithCategory(cat Category) *Builder
- func (b *Builder) WithConfidence(c Confidence) *Builder
- func (b *Builder) WithFixStrategy(fs FixStrategy) *Builder
- func (b *Builder) WithID(id ID) *Builder
- func (b *Builder) WithMetadata(m map[string]string) *Builder
- func (b *Builder) WithRange(r Range) *Builder
- func (b *Builder) WithRelated(refs ...RelatedRef) *Builder
- func (b *Builder) WithSnippet(s string) *Builder
- func (b *Builder) WithSuggestion(s string) *Builder
- func (b *Builder) WithSuppression(s Suppression) *Builder
- func (b *Builder) WithTags(tags ...Tag) *Builder
- type Category
- type Confidence
- type Correlation
- type CorrelationScore
- type DeduplicateBy
- type Detector
- type DetectorFunc
- type DetectorRegistry
- func (r *DetectorRegistry) Build(name string) (Detector, error)
- func (r *DetectorRegistry) BuildAll() ([]Detector, error)
- func (r *DetectorRegistry) Has(name string) bool
- func (r *DetectorRegistry) MustRegister(name string, builder func() Detector)
- func (r *DetectorRegistry) Names() []string
- func (r *DetectorRegistry) Register(name string, builder func() Detector) error
- type DiffResult
- type ErrorCategory
- type FilePath
- type FilterFunc
- func AnyOf(predicates ...FilterFunc) FilterFunc
- func ByCategory(cat Category) FilterFunc
- func ByConfidence(c Confidence) FilterFunc
- func ByConfidenceAtLeast(c Confidence) FilterFunc
- func ByFile(file FilePath) FilterFunc
- func ByFixStrategy(fs FixStrategy) FilterFunc
- func ByRule(rule RuleName) FilterFunc
- func BySeverity(sev Severity) FilterFunc
- func BySeverityAtLeast(sev Severity) FilterFunc
- func ByTool(tool ToolName) FilterFunc
- func Negate(predicate FilterFunc) FilterFunc
- type Finding
- func Filter(findings []Finding, predicates ...FilterFunc) []Finding
- func FilterInPlace(findings []Finding, predicates ...FilterFunc) []Finding
- func FindingsFromJSON(data []byte) ([]Finding, int, error)
- func FindingsFromReader(ctx context.Context, r io.Reader) ([]Finding, error)
- func FindingsFromSARIF(ctx context.Context, data []byte) ([]Finding, error)
- func FromJSON(data []byte) (Finding, error)
- func FromLSP(fileURI FilePath, diag LSPDiagnostic) Finding
- func NewFinding(rule RuleName, toolName ToolName, message string, severity Severity, ...) Finding
- func (f Finding) Clone() Finding
- func (f Finding) Equal(other Finding) bool
- func (f Finding) HasCategory() bool
- func (f Finding) HasCodeChange() bool
- func (f Finding) HasFix() bool
- func (f Finding) HasRange() bool
- func (f Finding) HasSuggestion() bool
- func (f Finding) IsAutoFixable() bool
- func (f Finding) IsSuppressed() bool
- func (f Finding) IsSuppressedAt(now time.Time) bool
- func (f Finding) IsValid() bool
- func (f Finding) Key() string
- func (f Finding) LineJSON() (string, error)
- func (f Finding) Normalized() Finding
- func (f Finding) NormalizedConfidence() Confidence
- func (f Finding) Preview() string
- func (f Finding) String() string
- func (f Finding) ToLSP() LSPDiagnostic
- func (f Finding) Validate() error
- func (f Finding) WriteJSON(w io.Writer) error
- type FindingError
- func NewConflictError(message string, cause error) *FindingError
- func NewIOError(message string, cause error) *FindingError
- func NewInternalError(message string, cause error) *FindingError
- func NewParseError(message string, cause error) *FindingError
- func NewValidationError(message string, cause error) *FindingError
- type FixStrategy
- type ID
- type Interval
- type IntervalIndex
- type LSPDiagnostic
- type LSPDiagnosticData
- type LSPDiagnosticTag
- type LSPLocation
- type LSPPosition
- type LSPRange
- type LSPRelated
- type LSPSeverity
- type LinterRegistry
- type MergeOption
- type MergeOptions
- type ModifiedPair
- type ParsedID
- type Position
- type Range
- func (r Range) Adjacent(other Range) bool
- func (r Range) Compare(other Range) int
- func (r Range) Contains(p Position) bool
- func (r Range) EndOffsetOrStart() int
- func (r Range) EndOrStart() Position
- func (r Range) Equal(other Range) bool
- func (r Range) HasEnd() bool
- func (r Range) Intersection(other Range) *Range
- func (r Range) IsInverted() bool
- func (r Range) IsSingleLine() bool
- func (r Range) IsValid() bool
- func (r Range) Length() int
- func (r Range) LineCount() int
- func (r Range) Overlaps(other Range) bool
- func (r Range) SameFile() bool
- type RelatedRef
- type RelationKind
- type Report
- func (r *Report) ActiveFindings() []Finding
- func (r *Report) AddFinding(f Finding)
- func (r *Report) AddFindings(findings []Finding)
- func (r *Report) All() iter.Seq[Finding]
- func (r *Report) ByCategory(cat Category) []Finding
- func (r *Report) ByFixStrategy(fs FixStrategy) []Finding
- func (r *Report) BySeverity(sev Severity) []Finding
- func (r *Report) ComputeSummary()
- func (r *Report) ComputeSummaryAt(now time.Time)
- func (r *Report) CountBySeverity(sev Severity) int
- func (r *Report) Filter(predicates ...FilterFunc) *Report
- func (r *Report) FindByID(id ID) *Finding
- func (r *Report) FindByRule(rule RuleName) []Finding
- func (r *Report) FindingsSnapshot() []Finding
- func (r *Report) Len() int
- func (r *Report) Map(fn func(Finding) Finding) *Report
- func (r *Report) MarshalJSON() ([]byte, error)
- func (r *Report) MergeInto(other *Report) *Report
- func (r *Report) PrettyJSON() (string, error)
- func (r *Report) PrettyJSONFiltered() (string, error)
- func (r *Report) ToSARIF() ([]byte, error)
- func (r *Report) ToSARIFWithOpts(opts ...SARIFOption) ([]byte, error)
- func (r *Report) UnmarshalJSON(data []byte) error
- func (r *Report) Validate() error
- func (r *Report) WriteJSON(w io.Writer) error
- func (r *Report) WriteSARIF(ctx context.Context, w io.Writer) error
- func (r *Report) WriteSARIFWithOpts(ctx context.Context, w io.Writer, opts ...SARIFOption) error
- func (r *Report) WriteTo(w io.Writer) (int64, error)
- type RuleName
- type SARIFOption
- type Severity
- func (s Severity) Badge() string
- func (s Severity) Compare(other Severity) int
- func (s Severity) Emoji() string
- func (s Severity) GreaterThan(other Severity) bool
- func (s Severity) GreaterThanOrEqual(other Severity) bool
- func (s Severity) IsValid() bool
- func (s Severity) LessThan(other Severity) bool
- func (s Severity) LessThanOrEqual(other Severity) bool
- func (s Severity) String() string
- type Summary
- type Suppression
- type SuppressionKind
- type Tag
- type ToolAdapter
- type ToolInfo
- type ToolName
- type ToolRunFunc
Examples ¶
- Package (Basic)
- Package (Filter)
- Package (Merge)
- Package (Merging)
- Package (SimpleCLI)
- Builder
- CategoryForLinter
- Combine
- Combine (Deduplication)
- Correlate
- Diff
- Filter
- FindingError
- FormatMarkdown
- FormatText
- GenerateID
- GroupByFile
- MergeIter
- NewDetectorRegistry
- NewFinding
- NewIntervalIndex
- NewReport
- ParseCategory
- ParseID
- ParseSeverity
- Range
- Report.ActiveFindings
- Report.ToSARIF
- Severity
- ToolAdapter
Constants ¶
const LSPDiagnosticTagsKey = "go-finding/lsp-diagnostic-tags"
LSPDiagnosticTagsKey is the Metadata key for preserving LSP diagnostic tags.
const LSPSeverityKey = "go-finding/lsp-severity"
LSPSeverityKey is the Metadata key for preserving raw LSP severity codes.
const OffsetUnknown = -1
OffsetUnknown is the sentinel value for "no byte offset set." Use this instead of raw -1 literals to centralize the convention.
const VersionMajor = 1
VersionMajor is the major version number.
const VersionMinor = 2
VersionMinor is the minor version number.
const VersionPatch = 0
VersionPatch is the patch version number.
Variables ¶
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.
var ( ErrInvalidFinding = errors.New("invalid finding: missing required fields") ErrInvalidReport = errors.New("invalid report: missing tool name") )
Sentinel errors for JSON validation.
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.
var DefaultLinterRegistry = NewLinterRegistry(map[string]Category{ "gosec": CategorySecurity, "noctx": CategorySecurity, "errchkjson": 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, "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, "goimports": CategoryStyle, "gci": CategoryStyle, "wsl_v5": CategoryStyle, "dupword": CategoryStyle, "godot": CategoryStyle, "lll": CategoryStyle, "whitespace": CategoryStyle, "nlreturn": CategoryStyle, "golint": 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, "sloglint": CategoryStructure, "loggercheck": CategoryStructure, "unused": CategoryUnused, "gomodguard_v2": CategoryConfiguration, })
DefaultLinterRegistry is the global registry with built-in linter→category mappings.
var Version = fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch)
Version is the semantic version string, computed from components.
Functions ¶
func FilterInvalid
deprecated
added in
v0.2.0
func FormatMarkdown ¶ added in v0.4.0
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 FormatText ¶ added in v0.4.0
FormatText writes a human-readable text representation of findings to w. Each finding is formatted as: file:line:col [SEVERITY] rule: message.
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 GroupByCategory ¶
GroupByCategory groups findings by category.
func GroupByFile ¶
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 ¶
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 ¶
IsFindingError returns true if err is a *FindingError.
func IsInvalid ¶ added in v1.1.0
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 ¶
NotSuppressed returns a filter for non-suppressed findings.
func RangeLinesEq ¶
RangeLinesEq checks if two ranges have equal start/end lines (ignoring columns/files).
func RegisterLinterCategory ¶ added in v0.7.0
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
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 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 WithSuggestion ¶ added in v0.9.0
WithSuggestion returns a filter for findings with suggestions.
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.
func (*Builder) Build ¶ added in v0.2.0
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) MustBuild ¶ added in v0.2.1
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
WithAfterCode sets the code after the fix.
func (*Builder) WithBeforeCode ¶ added in v0.2.0
WithBeforeCode sets the code before the fix.
func (*Builder) WithCategory ¶ added in v0.2.0
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) WithMetadata ¶ added in v0.2.0
WithMetadata copies the given metadata into the finding.
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
WithSnippet sets the surrounding code context.
func (*Builder) WithSuggestion ¶ added in v0.2.0
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.
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
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
MustParseCategory parses a string into a Category, panicking on invalid input.
func ParseCategory ¶ added in v0.7.0
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
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
IsSecurity reports whether the category is security-related.
func (Category) IsStandard ¶
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 ¶
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).
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 (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(log 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(log 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
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.
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()). 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 FilterFunc ¶
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 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
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 ¶
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
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 ¶
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 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) Equal ¶
Equal reports whether two findings are identical, including all nested fields.
func (Finding) HasCategory ¶ added in v0.2.1
HasCategory returns true if this finding has a category set.
func (Finding) HasCodeChange ¶ added in v0.4.0
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 ¶
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
HasRange reports whether the finding has a valid range set.
func (Finding) HasSuggestion ¶
HasSuggestion returns true if this finding has a human-readable suggestion.
func (Finding) IsAutoFixable ¶ added in v0.4.0
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 ¶
IsSuppressed returns true if this finding is suppressed at the current time.
func (Finding) IsSuppressedAt ¶ added in v0.2.0
IsSuppressedAt returns true if this finding is suppressed at the given time. Use this in tests for deterministic suppression checks.
func (Finding) Key ¶ added in v0.2.1
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) Normalized ¶ added in v0.9.0
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
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) 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.
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) 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 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 ¶
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
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"`
FixStrategy FixStrategy `json:"fixStrategy,omitempty"`
Confidence Confidence `json:"confidence,omitempty"`
Category Category `json:"category,omitempty"`
Tags []Tag `json:"tags,omitempty"`
BeforeCode string `json:"beforeCode,omitempty"`
AfterCode string `json:"afterCode,omitempty"`
Suggestion string `json:"suggestion,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 ¶
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
ModifiedPair holds both versions of a modified finding.
type ParsedID ¶
ParsedID holds the components of a parsed finding ID.
func ParseID ¶
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
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.
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 Pos ¶
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 ¶
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) HasFile ¶ added in v1.1.0
HasFile reports whether the position has a file path set, regardless of line/column completeness.
func (Position) HasLocation ¶ added in v0.4.0
HasLocation reports whether the position has a file and line number.
func (Position) HasOffset ¶
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 ¶
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
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.
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 ¶
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 ¶
NewRangePtr creates a pointer to a Range with the given file, start/end lines, and columns.
func (Range) Adjacent ¶
Adjacent reports whether this range is immediately adjacent to another range. Adjacent means one range ends exactly where the other begins.
func (Range) Compare ¶
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 ¶
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
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
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) Intersection ¶
Intersection returns the overlapping region of two ranges, or nil if they don't overlap.
func (Range) IsInverted ¶ added in v0.4.0
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
IsSingleLine reports whether the range spans exactly one line.
func (Range) Length ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
AddFinding adds a finding to the report. Safe for concurrent use.
func (*Report) AddFindings ¶
AddFindings adds multiple findings to the report. Safe for concurrent use.
func (*Report) All ¶ added in v0.2.0
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 ¶
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 ¶
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
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
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 ¶
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 ¶
FindByRule returns all non-suppressed findings matching the given rule name. Safe for concurrent use.
func (*Report) FindingsSnapshot ¶ added in v0.5.0
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) Len ¶ added in v0.1.3
Len returns the number of findings in the report. Safe for concurrent use.
func (*Report) Map ¶ added in v0.2.1
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
MarshalJSON implements json.Marshaler.
func (*Report) MergeInto ¶ added in v0.5.0
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 ¶
PrettyJSON returns a formatted JSON representation of the report. Includes all findings, including suppressed ones.
func (*Report) PrettyJSONFiltered ¶ added in v0.5.0
PrettyJSONFiltered returns a formatted JSON representation with only active (non-suppressed) findings. Unlike PrettyJSON, this excludes suppressed findings from the output.
func (*Report) ToSARIF ¶
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"
"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
UnmarshalJSON implements json.Unmarshaler.
func (*Report) Validate ¶ added in v0.4.0
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) WriteJSON ¶ added in v0.2.1
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
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
WriteSARIFWithOpts writes the report in SARIF 2.1.0 format with the given options. See WithIncludeSuppressed and WithMinSeverity.
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 ¶
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
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
MustParseSeverity parses a string into a Severity, panicking on invalid input.
func ParseSeverity ¶ added in v0.4.0
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 (Severity) Badge ¶ added in v0.6.0
Badge returns a human-readable severity badge with emoji (e.g., "🔴 CRITICAL"). Returns the string value unchanged for unknown severities.
func (Severity) Compare ¶ added in v0.1.3
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
Emoji returns the emoji representing the severity level. Returns an empty string for unknown severities.
func (Severity) GreaterThan ¶
GreaterThan returns true if this severity is greater than the other. Order: info < warning < error < critical.
func (Severity) GreaterThanOrEqual ¶
GreaterThanOrEqual returns true if this severity is greater than or equal to the other.
func (Severity) LessThanOrEqual ¶
LessThanOrEqual returns true if this severity is less than or equal to the other.
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.
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
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
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).
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"
"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:
- Runs the tool via run,
- Parses raw bytes into type O via parse,
- 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.
Source Files
¶
- adapter.go
- branded_types.go
- category.go
- category_linter.go
- confidence.go
- correlate.go
- detector.go
- diff.go
- doc.go
- errors.go
- filter.go
- finding.go
- finding_builder.go
- finding_equal.go
- finding_methods.go
- finding_validate.go
- fix_strategy.go
- format.go
- id.go
- interval_index.go
- json.go
- lsp.go
- merge.go
- position.go
- range.go
- range_overlap.go
- registry.go
- report.go
- report_query.go
- sarif_export.go
- sarif_import.go
- sarif_types.go
- severity.go
- suppression.go
- tag.go
- validate_helpers.go
- version.go
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
|