dockerfile_roast/
linter.rs1use 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 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
22pub 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 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
68pub 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}