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