Skip to main content

dockerfile_roast/
linter.rs

1/// Top-level linting orchestration.
2
3use std::path::Path;
4use anyhow::{Context, Result};
5
6use crate::parser;
7use crate::rules::{self, Finding, Severity};
8
9pub struct LintOptions {
10    pub skip_rules: Vec<String>,
11    /// When non-empty, only rules whose IDs appear in this list are run.
12    pub only_rules: Vec<String>,
13    pub min_severity: Severity,
14    pub check_dockerignore: bool,
15}
16
17pub struct LintResult {
18    pub file: String,
19    pub findings: Vec<Finding>,
20}
21
22/// Lint Dockerfile content that has already been read into a string.
23///
24/// `filename` is used only for display and for locating `.dockerignore`.
25/// Pass `"<stdin>"` when linting content read from standard input.
26pub fn lint_content(content: &str, filename: &str, opts: &LintOptions) -> LintResult {
27    let instructions = parser::parse(content);
28    let mut findings: Vec<Finding> = Vec::new();
29
30    for rule in rules::all_rules() {
31        if !opts.only_rules.is_empty() && !opts.only_rules.iter().any(|s| s.eq_ignore_ascii_case(rule.id)) {
32            continue;
33        }
34        if opts.skip_rules.iter().any(|s| s.eq_ignore_ascii_case(rule.id)) {
35            continue;
36        }
37        let mut rule_findings = (rule.func)(&instructions, content);
38        rule_findings.retain(|f| f.severity >= opts.min_severity);
39        findings.extend(rule_findings);
40    }
41
42    if opts.check_dockerignore {
43        // For stdin, check cwd; otherwise check the directory of the file.
44        let dir = if filename == "<stdin>" {
45            Path::new(".")
46        } else {
47            Path::new(filename).parent().unwrap_or(Path::new("."))
48        };
49        if !dir.join(".dockerignore").exists() && Severity::Info >= opts.min_severity {
50            findings.push(Finding {
51                rule: "DF033",
52                severity: Severity::Info,
53                line: 0,
54                message: "No .dockerignore file found in the same directory".to_string(),
55                roast: "No .dockerignore? You're COPY-ing your entire build context including \
56                        node_modules, .git, test fixtures, and possibly your diary. \
57                        A .dockerignore takes 5 minutes to write and saves you from \
58                        shipping your secrets to production.".to_string(),
59            });
60        }
61    }
62
63    findings.sort_by(|a, b| a.line.cmp(&b.line).then(b.severity.cmp(&a.severity)));
64
65    LintResult { file: filename.to_string(), findings }
66}
67
68/// Read `path` from disk and lint it. Thin wrapper around `lint_content`.
69pub fn lint_file(path: &Path, opts: &LintOptions) -> Result<LintResult> {
70    let content = std::fs::read_to_string(path)
71        .with_context(|| format!("Failed to read '{}'", path.display()))?;
72    Ok(lint_content(&content, &path.display().to_string(), opts))
73}
74
75pub fn has_errors(findings: &[Finding]) -> bool {
76    findings.iter().any(|f| f.severity == Severity::Error)
77}