package lint import ( "strings" "github.com/vladimir/goherence/internal/parser" ) // shellToModuleHints — известные паттерны команд, для которых есть // прямой декларативный модуль. Список расширяется по мере роста // internal/module/builtin — если добавляешь builtin-модуль, стоит // сразу подумать, какие shell-паттерны он делает лишними. var shellToModuleHints = map[string]string{ "apt-get install": "apt", "apt install": "apt", "yum install": "rpm_package", "dnf install": "rpm_package", "systemctl": "systemd", "mkdir -p": "file (state=directory)", "chmod": "file (mode=...)", "rm -rf": "file (state=absent)", } // ShellUsageRule ловит использование shell/command там, где уже есть // builtin-модуль с тем же эффектом, но идемпотентный по своей природе. // Единственный способ обойти правило — явная пометка allow_shell: true // прямо на таске: она остаётся видна в коде плейбука навсегда, так что // причина обхода на виду у любого, кто читает файл при code review. type ShellUsageRule struct{} func (r *ShellUsageRule) Name() string { return "shell-usage" } func (r *ShellUsageRule) Check(pb *parser.Playbook) []Violation { var out []Violation walkTasks(pb, func(loc string, t *parser.Task) { if t.Module != "shell" && t.Module != "command" { return } if t.AllowShell { return } cmd, _ := t.Args["cmd"].(string) for pattern, alt := range shellToModuleHints { if strings.Contains(cmd, pattern) { out = append(out, Violation{ Rule: r.Name(), Severity: Error, Location: loc, Message: "используй декларативный модуль " + alt + " вместо shell/command, либо явно пометь таск allow_shell: true: " + cmd, }) return // одного совпадения достаточно, не дублируем сообщение } } }) return out }