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}