package linter import ( "fmt" "strconv" "strings" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/pkg/errors" ) type Config struct { ExperimentalAll bool ExperimentalRules []string ReturnAsError bool SkipAll bool SkipRules []string Warn LintWarnFunc } type Linter struct { CalledRules []string ExperimentalAll bool ExperimentalRules map[string]struct{} ReturnAsError bool SkipAll bool SkippedRules map[string]struct{} Warn LintWarnFunc } func New(config *Config) *Linter { toret := &Linter{ SkippedRules: map[string]struct{}{}, ExperimentalRules: map[string]struct{}{}, CalledRules: []string{}, Warn: config.Warn, } toret.SkipAll = config.SkipAll toret.ExperimentalAll = config.ExperimentalAll toret.ReturnAsError = config.ReturnAsError for _, rule := range config.SkipRules { toret.SkippedRules[rule] = struct{}{} } for _, rule := range config.ExperimentalRules { toret.ExperimentalRules[rule] = struct{}{} } return toret } func (lc *Linter) Run(rule LinterRuleI, location []parser.Range, txt ...string) { if lc == nil || lc.Warn == nil || rule.IsDeprecated() { return } rulename := rule.RuleName() if rule.IsExperimental() { _, experimentalOk := lc.ExperimentalRules[rulename] if !(lc.ExperimentalAll || experimentalOk) { return } } else { _, skipOk := lc.SkippedRules[rulename] if lc.SkipAll || skipOk { return } } lc.CalledRules = append(lc.CalledRules, rulename) rule.Run(lc.Warn, location, txt...) } func (lc *Linter) Error() error { if lc == nil || !lc.ReturnAsError { return nil } if len(lc.CalledRules) == 0 { return nil } var rules []string uniqueRules := map[string]struct{}{} for _, r := range lc.CalledRules { uniqueRules[r] = struct{}{} } for r := range uniqueRules { rules = append(rules, r) } return errors.Errorf("lint violation found for rules: %s", strings.Join(rules, ", ")) } type LinterRuleI interface { RuleName() string Run(warn LintWarnFunc, location []parser.Range, txt ...string) IsDeprecated() bool IsExperimental() bool } type LinterRule[F any] struct { Name string Description string Deprecated bool Experimental bool URL string Format F } func (rule *LinterRule[F]) RuleName() string { return rule.Name } func (rule *LinterRule[F]) Run(warn LintWarnFunc, location []parser.Range, txt ...string) { if len(txt) == 0 { txt = []string{rule.Description} } short := strings.Join(txt, " ") warn(rule.Name, rule.Description, rule.URL, short, location) } func (rule *LinterRule[F]) IsDeprecated() bool { return rule.Deprecated } func (rule *LinterRule[F]) IsExperimental() bool { return rule.Experimental } func LintFormatShort(rulename, msg string, line int) string { msg = fmt.Sprintf("%s: %s", rulename, msg) if line > 0 { msg = fmt.Sprintf("%s (line %d)", msg, line) } return msg } type LintWarnFunc func(rulename, description, url, fmtmsg string, location []parser.Range) func ParseLintOptions(checkStr string) (*Config, error) { checkStr = strings.TrimSpace(checkStr) if checkStr == "" { return &Config{}, nil } parts := strings.SplitN(checkStr, ";", 3) var skipSet, experimentalSet []string var errorOnWarn, skipAll, experimentalAll bool for _, p := range parts { k, v, ok := strings.Cut(p, "=") if !ok { return nil, errors.Errorf("invalid check option %q", p) } k = strings.TrimSpace(k) switch k { case "skip": v = strings.TrimSpace(v) if v == "all" { skipAll = true } else { skipSet = strings.Split(v, ",") for i, rule := range skipSet { skipSet[i] = strings.TrimSpace(rule) } } case "experimental": v = strings.TrimSpace(v) if v == "all" { experimentalAll = true } else { experimentalSet = strings.Split(v, ",") for i, rule := range experimentalSet { experimentalSet[i] = strings.TrimSpace(rule) } } case "error": v, err := strconv.ParseBool(strings.TrimSpace(v)) if err != nil { return nil, errors.Wrapf(err, "failed to parse check option %q", p) } errorOnWarn = v default: return nil, errors.Errorf("invalid check option %q", k) } } return &Config{ ExperimentalAll: experimentalAll, ExperimentalRules: experimentalSet, SkipRules: skipSet, SkipAll: skipAll, ReturnAsError: errorOnWarn, }, nil }