syncable_cli/analyzer/dclint/
mod.rs

1//! Dclint-RS: Native Rust Docker Compose Linter
2//!
3//! A Rust translation of the docker-compose-linter project.
4//!
5//! # Attribution
6//!
7//! This module is a derivative work based on [docker-compose-linter](https://github.com/zavoloklom/docker-compose-linter),
8//! originally written in TypeScript by Sergey Kupletsky.
9//!
10//! **Original Project:** <https://github.com/zavoloklom/docker-compose-linter>
11//! **Original License:** MIT
12//!
13//! # Features
14//!
15//! - Docker Compose YAML parsing with position tracking
16//! - 15 configurable linting rules (DCL001-DCL015)
17//! - Auto-fix capability for 8 rules
18//! - Multiple output formats (JSON, Stylish, GitHub Actions, etc.)
19//! - Comment-based rule disabling
20//!
21//! # Example
22//!
23//! ```rust,ignore
24//! use syncable_cli::analyzer::dclint::{lint, DclintConfig, LintResult};
25//!
26//! let compose = r#"
27//! services:
28//!   web:
29//!     image: nginx:latest
30//!     ports:
31//!       - "8080:80"
32//! "#;
33//!
34//! let config = DclintConfig::default();
35//! let result = lint(compose, &config);
36//!
37//! for failure in result.failures {
38//!     println!("{}: {} - {}", failure.line, failure.code, failure.message);
39//! }
40//! ```
41//!
42//! # Rules
43//!
44//! | Code   | Name                                    | Fixable | Description                                    |
45//! |--------|-----------------------------------------|---------|------------------------------------------------|
46//! | DCL001 | no-build-and-image                      | No      | Service cannot have both build and image       |
47//! | DCL002 | no-duplicate-container-names            | No      | Container names must be unique                 |
48//! | DCL003 | no-duplicate-exported-ports             | No      | Exported ports must be unique                  |
49//! | DCL004 | no-quotes-in-volumes                    | Yes     | Volume paths should not be quoted              |
50//! | DCL005 | no-unbound-port-interfaces              | No      | Ports should bind to specific interface        |
51//! | DCL006 | no-version-field                        | Yes     | Version field is deprecated                    |
52//! | DCL007 | require-project-name-field              | No      | Require top-level name field                   |
53//! | DCL008 | require-quotes-in-ports                 | Yes     | Port mappings should be quoted                 |
54//! | DCL009 | service-container-name-regex            | No      | Container name format validation               |
55//! | DCL010 | service-dependencies-alphabetical-order | Yes     | Sort depends_on alphabetically                 |
56//! | DCL011 | service-image-require-explicit-tag      | No      | Images need explicit tags                      |
57//! | DCL012 | service-keys-order                      | Yes     | Service keys in standard order                 |
58//! | DCL013 | service-ports-alphabetical-order        | Yes     | Sort ports alphabetically                      |
59//! | DCL014 | services-alphabetical-order             | Yes     | Sort services alphabetically                   |
60//! | DCL015 | top-level-properties-order              | Yes     | Top-level keys in standard order               |
61
62pub mod config;
63pub mod formatter;
64pub mod lint;
65pub mod parser;
66pub mod pragma;
67pub mod rules;
68pub mod types;
69
70// Re-export main types and functions
71pub use config::DclintConfig;
72pub use formatter::{OutputFormat, format_result, format_result_to_string, format_results};
73pub use lint::{LintResult, fix_content, fix_file, lint, lint_file, lint_with_path};
74pub use types::{CheckFailure, ConfigLevel, RuleCategory, RuleCode, RuleMeta, Severity};
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_lint_basic() {
82        let yaml = r#"
83services:
84  web:
85    image: nginx:1.25
86"#;
87        let result = lint(yaml, &DclintConfig::default());
88        assert!(result.parse_errors.is_empty());
89    }
90
91    #[test]
92    fn test_lint_with_errors() {
93        let yaml = r#"
94services:
95  web:
96    build: .
97    image: nginx
98"#;
99        let result = lint(yaml, &DclintConfig::default());
100        assert!(result.parse_errors.is_empty());
101        // Should catch DCL001 and DCL011
102        assert!(result.failures.iter().any(|f| f.code.as_str() == "DCL001"));
103    }
104
105    #[test]
106    fn test_config_ignore() {
107        let yaml = r#"
108services:
109  web:
110    build: .
111    image: nginx
112"#;
113        let config = DclintConfig::default().ignore("DCL001");
114        let result = lint(yaml, &config);
115        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DCL001"));
116    }
117
118    #[test]
119    fn test_format_json() {
120        let yaml = r#"
121services:
122  web:
123    image: nginx
124"#;
125        let result = lint(yaml, &DclintConfig::default());
126        let output = format_result(&result, OutputFormat::Json);
127        assert!(output.contains("filePath"));
128    }
129}