feat(linter): glint explain, GL042 rules:if: reachability, GL043 inherit completeness
- `glint explain <RULE>`: new subcommand printing rule description, rationale, bad-YAML example and fix for every GL001–GL043 rule. `glint explain` (no arg) lists all rules with ID, severity, title. Rule IDs are case-insensitive. - GL042 (rules:if: evaluated reachability): warns when every rules:if: condition evaluates to false given the values of variables declared in the pipeline YAML, making the job statically unreachable. Conservative: only fires when all referenced variables are declared in YAML; predefined CI_* / GITLAB_* variables are skipped to avoid false positives. - GL043 (inherit: completeness): warns when inherit: default: is declared but there is no default: block in the pipeline (dead declaration), or when the list form names fields not set in the default: block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package linter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.k3nny.fr/glint/internal/model"
|
||||
)
|
||||
|
||||
// checkInheritCompleteness (GL043): warn when a job's 'inherit: default:'
|
||||
// declaration is dead — either because there is no 'default:' block in the
|
||||
// pipeline, or because the list form references fields that are not defined
|
||||
// in the 'default:' block.
|
||||
func checkInheritCompleteness(p *model.Pipeline) []Finding {
|
||||
var findings []Finding
|
||||
for name, job := range p.Jobs {
|
||||
findings = append(findings, checkJobInheritCompleteness(p, name, job)...)
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
func checkJobInheritCompleteness(p *model.Pipeline, name string, job model.Job) []Finding {
|
||||
if job.Inherit == nil {
|
||||
return nil
|
||||
}
|
||||
m, ok := job.Inherit.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defaultVal, hasDefault := m["default"]
|
||||
if !hasDefault {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Case 1: no default: block at all — the entire declaration is a no-op.
|
||||
if p.Default == nil {
|
||||
return []Finding{{
|
||||
Severity: Warning,
|
||||
Rule: RuleInheritNoDefault,
|
||||
Job: name,
|
||||
File: job.File,
|
||||
Line: job.Line,
|
||||
Message: "'inherit: default:' is declared but the pipeline has no 'default:' block — declaration has no effect",
|
||||
}}
|
||||
}
|
||||
|
||||
// Case 2: list form — check for field names not set in default:.
|
||||
list, ok := defaultVal.([]any)
|
||||
if !ok {
|
||||
// bool form (true/false) with a non-nil default: block is always valid.
|
||||
return nil
|
||||
}
|
||||
|
||||
var dead []string
|
||||
for _, item := range list {
|
||||
field, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !defaultBlockHasField(p.Default, field) {
|
||||
dead = append(dead, field)
|
||||
}
|
||||
}
|
||||
if len(dead) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []Finding{{
|
||||
Severity: Warning,
|
||||
Rule: RuleInheritNoDefault,
|
||||
Job: name,
|
||||
File: job.File,
|
||||
Line: job.Line,
|
||||
Message: fmt.Sprintf(
|
||||
"'inherit: default: [%s]': %s not defined in the 'default:' block — %s",
|
||||
strings.Join(dead, ", "),
|
||||
pluralIs(len(dead)),
|
||||
"these entries have no effect",
|
||||
),
|
||||
}}
|
||||
}
|
||||
|
||||
// defaultBlockHasField reports whether the given field name has a non-zero value
|
||||
// in the default: block. Unknown field names return true (conservative).
|
||||
func defaultBlockHasField(d *model.DefaultConfig, field string) bool {
|
||||
switch field {
|
||||
case "image":
|
||||
return d.Image != nil
|
||||
case "before_script":
|
||||
return d.BeforeScript != nil
|
||||
case "after_script":
|
||||
return d.AfterScript != nil
|
||||
case "cache":
|
||||
return d.Cache != nil
|
||||
case "artifacts":
|
||||
return d.Artifacts != nil
|
||||
case "retry":
|
||||
return d.Retry != nil
|
||||
case "timeout":
|
||||
return d.Timeout != ""
|
||||
case "tags":
|
||||
return len(d.Tags) > 0
|
||||
}
|
||||
// Unknown field name — conservatively assume it might be defined.
|
||||
return true
|
||||
}
|
||||
|
||||
func pluralIs(n int) string {
|
||||
if n == 1 {
|
||||
return "this field is"
|
||||
}
|
||||
return "these fields are"
|
||||
}
|
||||
Reference in New Issue
Block a user