113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
// 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
|
||
}
|