lint/pkg/lint/catalog.go
Snider a36f835fe0 refactor: replace os.* and fmt.Errorf with go-io/go-log conventions
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-16 19:44:45 +00:00

114 lines
2.8 KiB
Go

package lint
import (
"io/fs"
"path/filepath"
"slices"
"strings"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
)
// severityOrder maps severity names to numeric ranks for threshold comparison.
var severityOrder = map[string]int{
"info": 0,
"low": 1,
"medium": 2,
"high": 3,
"critical": 4,
}
// Catalog holds a collection of lint rules loaded from YAML files.
type Catalog struct {
Rules []Rule
}
// LoadDir reads all .yaml files from the given directory and returns a Catalog.
func LoadDir(dir string) (*Catalog, error) {
entries, err := coreio.Local.List(dir)
if err != nil {
return nil, coreerr.E("Catalog.LoadDir", "loading catalog from "+dir, err)
}
var rules []Rule
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
raw, err := coreio.Local.Read(filepath.Join(dir, entry.Name()))
if err != nil {
return nil, coreerr.E("Catalog.LoadDir", "reading "+entry.Name(), err)
}
parsed, err := ParseRules([]byte(raw))
if err != nil {
return nil, coreerr.E("Catalog.LoadDir", "parsing "+entry.Name(), err)
}
rules = append(rules, parsed...)
}
return &Catalog{Rules: rules}, nil
}
// LoadFS reads all .yaml files from the given directory within an fs.FS and returns a Catalog.
func LoadFS(fsys fs.FS, dir string) (*Catalog, error) {
entries, err := fs.ReadDir(fsys, dir)
if err != nil {
return nil, coreerr.E("Catalog.LoadFS", "loading catalog from embedded "+dir, err)
}
var rules []Rule
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
data, err := fs.ReadFile(fsys, dir+"/"+entry.Name())
if err != nil {
return nil, coreerr.E("Catalog.LoadFS", "reading embedded "+entry.Name(), err)
}
parsed, err := ParseRules(data)
if err != nil {
return nil, coreerr.E("Catalog.LoadFS", "parsing embedded "+entry.Name(), err)
}
rules = append(rules, parsed...)
}
return &Catalog{Rules: rules}, nil
}
// ForLanguage returns all rules that apply to the given language.
func (c *Catalog) ForLanguage(lang string) []Rule {
var result []Rule
for _, r := range c.Rules {
if slices.Contains(r.Languages, lang) {
result = append(result, r)
}
}
return result
}
// AtSeverity returns all rules at or above the given severity threshold.
func (c *Catalog) AtSeverity(threshold string) []Rule {
minRank, ok := severityOrder[threshold]
if !ok {
return nil
}
var result []Rule
for _, r := range c.Rules {
if rank, ok := severityOrder[r.Severity]; ok && rank >= minRank {
result = append(result, r)
}
}
return result
}
// ByID returns the rule with the given ID, or nil if not found.
func (c *Catalog) ByID(id string) *Rule {
for i := range c.Rules {
if c.Rules[i].ID == id {
return &c.Rules[i]
}
}
return nil
}