syncable_cli/analyzer/dclint/rules/
mod.rs

1//! Rule system framework for dclint.
2//!
3//! Provides the infrastructure for defining and running Docker Compose linting rules.
4//! Follows the hadolint-rs pattern with:
5//! - `Rule` trait for all rules
6//! - `SimpleRule` for stateless checks
7//! - `FixableRule` for rules that can auto-fix issues
8
9use crate::analyzer::dclint::parser::ComposeFile;
10use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, RuleCode, RuleMeta, Severity};
11
12// Rule modules
13pub mod dcl001;
14pub mod dcl002;
15pub mod dcl003;
16pub mod dcl004;
17pub mod dcl005;
18pub mod dcl006;
19pub mod dcl007;
20pub mod dcl008;
21pub mod dcl009;
22pub mod dcl010;
23pub mod dcl011;
24pub mod dcl012;
25pub mod dcl013;
26pub mod dcl014;
27pub mod dcl015;
28
29/// Context for linting a compose file.
30#[derive(Debug, Clone)]
31pub struct LintContext<'a> {
32    /// The parsed compose file.
33    pub compose: &'a ComposeFile,
34    /// The raw source content.
35    pub source: &'a str,
36    /// The file path (for error messages).
37    pub path: &'a str,
38}
39
40impl<'a> LintContext<'a> {
41    pub fn new(compose: &'a ComposeFile, source: &'a str, path: &'a str) -> Self {
42        Self {
43            compose,
44            source,
45            path,
46        }
47    }
48}
49
50/// A rule that can check Docker Compose files.
51pub trait Rule: Send + Sync {
52    /// Get the rule code (e.g., "DCL001").
53    fn code(&self) -> &RuleCode;
54
55    /// Get the human-readable rule name (e.g., "no-build-and-image").
56    fn name(&self) -> &str;
57
58    /// Get the default severity.
59    fn severity(&self) -> Severity;
60
61    /// Get the rule category.
62    fn category(&self) -> RuleCategory;
63
64    /// Get the rule metadata (description, URL).
65    fn meta(&self) -> &RuleMeta;
66
67    /// Whether this rule can auto-fix issues.
68    fn is_fixable(&self) -> bool {
69        false
70    }
71
72    /// Check the compose file and return any failures.
73    fn check(&self, context: &LintContext) -> Vec<CheckFailure>;
74
75    /// Auto-fix the source content (if fixable).
76    /// Returns the fixed content, or None if no fix was applied.
77    fn fix(&self, _source: &str) -> Option<String> {
78        None
79    }
80
81    /// Get a message for this rule violation.
82    fn get_message(&self, details: &std::collections::HashMap<String, String>) -> String {
83        self.meta().description.clone()
84    }
85}
86
87/// Base implementation for a simple (non-fixable) rule.
88pub struct SimpleRule<F>
89where
90    F: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
91{
92    code: RuleCode,
93    name: String,
94    severity: Severity,
95    category: RuleCategory,
96    meta: RuleMeta,
97    check_fn: F,
98}
99
100impl<F> SimpleRule<F>
101where
102    F: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
103{
104    pub fn new(
105        code: impl Into<RuleCode>,
106        name: impl Into<String>,
107        severity: Severity,
108        category: RuleCategory,
109        description: impl Into<String>,
110        url: impl Into<String>,
111        check_fn: F,
112    ) -> Self {
113        Self {
114            code: code.into(),
115            name: name.into(),
116            severity,
117            category,
118            meta: RuleMeta::new(description, url),
119            check_fn,
120        }
121    }
122}
123
124impl<F> Rule for SimpleRule<F>
125where
126    F: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
127{
128    fn code(&self) -> &RuleCode {
129        &self.code
130    }
131
132    fn name(&self) -> &str {
133        &self.name
134    }
135
136    fn severity(&self) -> Severity {
137        self.severity
138    }
139
140    fn category(&self) -> RuleCategory {
141        self.category
142    }
143
144    fn meta(&self) -> &RuleMeta {
145        &self.meta
146    }
147
148    fn check(&self, context: &LintContext) -> Vec<CheckFailure> {
149        (self.check_fn)(context)
150    }
151}
152
153/// Base implementation for a fixable rule.
154pub struct FixableRule<C, X>
155where
156    C: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
157    X: Fn(&str) -> Option<String> + Send + Sync,
158{
159    code: RuleCode,
160    name: String,
161    severity: Severity,
162    category: RuleCategory,
163    meta: RuleMeta,
164    check_fn: C,
165    fix_fn: X,
166}
167
168impl<C, X> FixableRule<C, X>
169where
170    C: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
171    X: Fn(&str) -> Option<String> + Send + Sync,
172{
173    pub fn new(
174        code: impl Into<RuleCode>,
175        name: impl Into<String>,
176        severity: Severity,
177        category: RuleCategory,
178        description: impl Into<String>,
179        url: impl Into<String>,
180        check_fn: C,
181        fix_fn: X,
182    ) -> Self {
183        Self {
184            code: code.into(),
185            name: name.into(),
186            severity,
187            category,
188            meta: RuleMeta::new(description, url),
189            check_fn,
190            fix_fn,
191        }
192    }
193}
194
195impl<C, X> Rule for FixableRule<C, X>
196where
197    C: Fn(&LintContext) -> Vec<CheckFailure> + Send + Sync,
198    X: Fn(&str) -> Option<String> + Send + Sync,
199{
200    fn code(&self) -> &RuleCode {
201        &self.code
202    }
203
204    fn name(&self) -> &str {
205        &self.name
206    }
207
208    fn severity(&self) -> Severity {
209        self.severity
210    }
211
212    fn category(&self) -> RuleCategory {
213        self.category
214    }
215
216    fn meta(&self) -> &RuleMeta {
217        &self.meta
218    }
219
220    fn is_fixable(&self) -> bool {
221        true
222    }
223
224    fn check(&self, context: &LintContext) -> Vec<CheckFailure> {
225        (self.check_fn)(context)
226    }
227
228    fn fix(&self, source: &str) -> Option<String> {
229        (self.fix_fn)(source)
230    }
231}
232
233/// Helper to create a check failure for a rule.
234pub fn make_failure(
235    code: &RuleCode,
236    name: &str,
237    severity: Severity,
238    category: RuleCategory,
239    message: impl Into<String>,
240    line: u32,
241    column: u32,
242    fixable: bool,
243) -> CheckFailure {
244    CheckFailure::new(
245        code.clone(),
246        name,
247        severity,
248        category,
249        message,
250        line,
251        column,
252    )
253    .with_fixable(fixable)
254}
255
256/// Get all enabled rules.
257pub fn all_rules() -> Vec<Box<dyn Rule>> {
258    vec![
259        Box::new(dcl001::rule()),
260        Box::new(dcl002::rule()),
261        Box::new(dcl003::rule()),
262        Box::new(dcl004::rule()),
263        Box::new(dcl005::rule()),
264        Box::new(dcl006::rule()),
265        Box::new(dcl007::rule()),
266        Box::new(dcl008::rule()),
267        Box::new(dcl009::rule()),
268        Box::new(dcl010::rule()),
269        Box::new(dcl011::rule()),
270        Box::new(dcl012::rule()),
271        Box::new(dcl013::rule()),
272        Box::new(dcl014::rule()),
273        Box::new(dcl015::rule()),
274    ]
275}
276
277/// Get rule definitions for documentation.
278pub fn rule_definitions() -> Vec<RuleDefinition> {
279    all_rules()
280        .iter()
281        .map(|r| RuleDefinition {
282            code: r.code().clone(),
283            name: r.name().to_string(),
284            severity: r.severity(),
285            category: r.category(),
286            description: r.meta().description.clone(),
287            url: r.meta().url.clone(),
288            fixable: r.is_fixable(),
289        })
290        .collect()
291}
292
293/// Rule definition for documentation/introspection.
294#[derive(Debug, Clone)]
295pub struct RuleDefinition {
296    pub code: RuleCode,
297    pub name: String,
298    pub severity: Severity,
299    pub category: RuleCategory,
300    pub description: String,
301    pub url: String,
302    pub fixable: bool,
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_all_rules_count() {
311        let rules = all_rules();
312        assert_eq!(rules.len(), 15, "Expected 15 rules");
313    }
314
315    #[test]
316    fn test_rule_codes_unique() {
317        let rules = all_rules();
318        let mut codes: Vec<String> = rules.iter().map(|r| r.code().to_string()).collect();
319        codes.sort();
320        codes.dedup();
321        assert_eq!(codes.len(), 15, "Rule codes should be unique");
322    }
323
324    #[test]
325    fn test_rule_names_unique() {
326        let rules = all_rules();
327        let mut names: Vec<String> = rules.iter().map(|r| r.name().to_string()).collect();
328        names.sort();
329        names.dedup();
330        assert_eq!(names.len(), 15, "Rule names should be unique");
331    }
332}