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,104 @@
|
||||
package linter
|
||||
|
||||
import (
|
||||
"git.k3nny.fr/glint/internal/cicontext"
|
||||
"git.k3nny.fr/glint/internal/model"
|
||||
)
|
||||
|
||||
// checkRulesIfReachability (GL042): warn when every rule in a job's rules: block
|
||||
// can be statically proven to never activate given the values of variables
|
||||
// declared in the pipeline YAML. Only fires when ALL variables referenced by the
|
||||
// rules:if: expressions are declared in the pipeline or job variables: blocks —
|
||||
// predefined CI_* / GITLAB_* variables or variables not declared in YAML are
|
||||
// treated as unknown (could have any runtime value) so the check is skipped
|
||||
// conservatively to avoid false positives.
|
||||
func checkRulesIfReachability(p *model.Pipeline) []Finding {
|
||||
if len(p.Jobs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect scalar string values from pipeline-level variables.
|
||||
pipelineVars := make(map[string]string, len(p.Variables))
|
||||
for k, v := range p.Variables {
|
||||
if s, ok := cicontext.ScalarString(v); ok {
|
||||
pipelineVars[k] = s
|
||||
}
|
||||
}
|
||||
|
||||
var findings []Finding
|
||||
for name, job := range p.Jobs {
|
||||
if len(job.Rules) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge pipeline vars with job-level variable overrides.
|
||||
jobVars := make(map[string]string, len(pipelineVars)+len(job.Variables))
|
||||
for k, v := range pipelineVars {
|
||||
jobVars[k] = v
|
||||
}
|
||||
for k, v := range job.Variables {
|
||||
if s, ok := cicontext.ScalarString(v); ok {
|
||||
jobVars[k] = s
|
||||
}
|
||||
}
|
||||
|
||||
if f := evalRulesReachability(name, job, jobVars); f != nil {
|
||||
findings = append(findings, *f)
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
// evalRulesReachability returns a GL042 finding when the job's rules: block
|
||||
// can never activate given the provided variable map, or nil if the job
|
||||
// might activate (or the check cannot be applied conservatively).
|
||||
func evalRulesReachability(name string, job model.Job, jobVars map[string]string) *Finding {
|
||||
lookup := func(k string) string { return jobVars[k] }
|
||||
|
||||
for _, rule := range job.Rules {
|
||||
// Explicit when: never — this rule is provably dead; continue.
|
||||
if rule.When == "never" {
|
||||
continue
|
||||
}
|
||||
|
||||
// No if: condition → this rule always matches → job CAN activate.
|
||||
if rule.If == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check whether all variables referenced in the if: expression are
|
||||
// declared in the pipeline YAML. If any are not declared (predefined
|
||||
// CI vars, project settings, etc.), we cannot evaluate the expression
|
||||
// and must conservatively assume the job might activate.
|
||||
refs := extractIfVars(rule.If)
|
||||
allDeclared := true
|
||||
for _, ref := range refs {
|
||||
if _, ok := jobVars[ref]; !ok {
|
||||
// Variable not in YAML (predefined or unknown) → cannot evaluate.
|
||||
allDeclared = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allDeclared {
|
||||
return nil // conservative: job might activate
|
||||
}
|
||||
|
||||
// All referenced variables are declared; evaluate the expression.
|
||||
// Use strict mode (unparse → false) to avoid matching unparseable expressions.
|
||||
if cicontext.EvalIfStrict(rule.If, lookup) {
|
||||
// This rule's condition evaluates to true → job CAN activate.
|
||||
return nil
|
||||
}
|
||||
// Expression evaluated to false → this rule cannot activate; continue.
|
||||
}
|
||||
|
||||
// No rule could activate the job.
|
||||
return &Finding{
|
||||
Severity: Warning,
|
||||
Rule: RuleStaticDeadRules,
|
||||
Job: name,
|
||||
File: job.File,
|
||||
Line: job.Line,
|
||||
Message: "rules: block can never activate: all if: conditions evaluate to false given the declared pipeline variables",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user