goherence/internal/lint/lint.go
2026-09-11 10:17:25 +03:00

113 lines
3.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package lint реализует guardrails против превращения декларативных
// плейбуков в императивный код. Никакой плагинной системы для правил —
// они часть бинарника и расширяются через PR, а не через рантайм-загрузку
// (в отличие от internal/module/external, который расширяем намеренно).
package lint
import (
"strconv"
"github.com/vladimir/goherence/internal/parser"
)
type Severity int
const (
Error Severity = iota // блокирует запуск, требует --force
Warning // печатается, но не блокирует
)
func (s Severity) String() string {
if s == Error {
return "ERROR"
}
return "WARNING"
}
type Violation struct {
Rule string
Severity Severity
Location string
Message string
}
// Rule — контракт, которому подчиняются все проверки.
type Rule interface {
Name() string
Check(pb *parser.Playbook) []Violation
}
// Linter — фиксированный, не настраиваемый снаружи список правил.
type Linter struct {
rules []Rule
}
// New возвращает линтер с полным набором правил первой версии.
// Пороги (MaxNesting/MaxBranches) — тут единственное, что параметризовано,
// и то как явные поля структуры, а не внешний конфиг-файл.
func New() *Linter {
return &Linter{rules: []Rule{
&WhenGrammarRule{},
&ShellUsageRule{},
&SetFactStateRule{},
&DynamicIncludeRule{},
&ComplexityRule{MaxNesting: 3, MaxBranches: 8},
}}
}
// Run прогоняет все правила по плейбуку и возвращает все нарушения сразу —
// принципиально не останавливается на первом, чтобы за один прогон
// показать разработчику всё, что нужно поправить.
func (l *Linter) Run(pb *parser.Playbook) []Violation {
var out []Violation
for _, r := range l.rules {
out = append(out, r.Check(pb)...)
}
return out
}
// HasError — есть ли среди нарушений хотя бы одно уровня Error.
func HasError(violations []Violation) bool {
for _, v := range violations {
if v.Severity == Error {
return true
}
}
return false
}
// walkTasks — общий обход всех тасков плейбука (в play и во всех
// подключённых ролях), с человекочитаемой Location для каждого.
// Вынесен сюда как единственная точка обхода, чтобы правила не
// дублировали логику "как пройтись по всем таскам плейбука".
func walkTasks(pb *parser.Playbook, fn func(location string, t *parser.Task)) {
for _, play := range pb.Plays {
for i := range play.Tasks {
t := &play.Tasks[i]
fn(locationFor(play.Name, "", i, t), t)
}
for _, roleName := range play.Roles {
role, ok := pb.Roles[roleName]
if !ok {
continue
}
for i := range role.Tasks {
t := &role.Tasks[i]
fn(locationFor(play.Name, roleName, i, t), t)
}
}
}
}
func locationFor(playName, roleName string, idx int, t *parser.Task) string {
loc := "play " + playName
if roleName != "" {
loc += ": role " + roleName
}
if t.Name != "" {
loc += ": task " + t.Name
} else {
loc += ": task #" + strconv.Itoa(idx)
}
return loc
}