Skip to main content

dictator_rust/
lib.rs

1#![warn(rust_2024_compatibility, clippy::all)]
2
3//! decree.rust - Rust structural rules.
4
5mod cargo_toml;
6mod counting;
7mod structure;
8mod visibility;
9
10use dictator_decree_abi::{BoxDecree, Decree, Diagnostics};
11use dictator_supreme::SupremeConfig;
12
13pub use cargo_toml::lint_cargo_toml;
14
15/// Configuration for rust decree
16#[derive(Debug, Clone)]
17pub struct RustConfig {
18    pub max_lines: usize,
19    /// Minimum required Rust edition (e.g., "2024"). None = disabled.
20    pub min_edition: Option<String>,
21    /// Minimum required rust-version/MSRV (e.g., "1.83"). None = disabled.
22    pub min_rust_version: Option<String>,
23    /// When true, `line-too-long` violations on comment lines are suppressed.
24    pub ignore_comments: bool,
25}
26
27impl Default for RustConfig {
28    fn default() -> Self {
29        Self {
30            max_lines: 400,
31            min_edition: None,
32            min_rust_version: None,
33            ignore_comments: false,
34        }
35    }
36}
37
38/// Lint Rust source for structural violations.
39#[must_use]
40pub fn lint_source(source: &str) -> Diagnostics {
41    lint_source_with_configs(source, &RustConfig::default(), &SupremeConfig::default())
42}
43
44/// Lint with custom configuration
45#[must_use]
46pub fn lint_source_with_config(source: &str, config: &RustConfig) -> Diagnostics {
47    let mut diags = Diagnostics::new();
48
49    counting::check_file_line_count(source, config.max_lines, &mut diags);
50    visibility::check_visibility_ordering(source, &mut diags);
51
52    diags
53}
54
55/// Lint with custom config + supreme config (merged from decree.supreme + decree.rust)
56#[must_use]
57pub fn lint_source_with_configs(
58    source: &str,
59    rust_config: &RustConfig,
60    supreme_config: &SupremeConfig,
61) -> Diagnostics {
62    let mut diags = Diagnostics::new();
63
64    let supreme_diags =
65        dictator_supreme::lint_source_with_owner(source, supreme_config, "rust");
66
67    if rust_config.ignore_comments {
68        // Filter out line-too-long violations on comment lines
69        let lines: Vec<&str> = source.lines().collect();
70        diags.extend(supreme_diags.into_iter().filter(|d| {
71            if d.rule == "rust/line-too-long" {
72                let line_idx = source[..d.span.start].matches('\n').count();
73                !lines
74                    .get(line_idx)
75                    .is_some_and(|line| line.trim_start().starts_with("//"))
76            } else {
77                true
78            }
79        }));
80    } else {
81        diags.extend(supreme_diags);
82    }
83
84    // Rust-specific rules
85    diags.extend(lint_source_with_config(source, rust_config));
86
87    diags
88}
89
90#[derive(Default)]
91pub struct RustDecree {
92    config: RustConfig,
93    supreme: SupremeConfig,
94}
95
96impl RustDecree {
97    #[must_use]
98    pub const fn new(config: RustConfig, supreme: SupremeConfig) -> Self {
99        Self { config, supreme }
100    }
101}
102
103impl Decree for RustDecree {
104    fn name(&self) -> &'static str {
105        "rust"
106    }
107
108    fn lint(&self, path: &str, source: &str) -> Diagnostics {
109        let filename = std::path::Path::new(path)
110            .file_name()
111            .and_then(|f| f.to_str())
112            .unwrap_or("");
113
114        // Cargo.toml gets edition check only (no supreme formatting rules)
115        if filename == "Cargo.toml" {
116            return cargo_toml::lint_cargo_toml(source, &self.config);
117        }
118
119        // Regular Rust files get full treatment
120        let mut diags = lint_source_with_configs(source, &self.config, &self.supreme);
121
122        // Check mod.rs structure (needs filesystem access)
123        structure::check_mod_rs_structure(path, &mut diags);
124
125        diags
126    }
127
128    fn metadata(&self) -> dictator_decree_abi::DecreeMetadata {
129        dictator_decree_abi::DecreeMetadata {
130            abi_version: dictator_decree_abi::ABI_VERSION.to_string(),
131            decree_version: env!("CARGO_PKG_VERSION").to_string(),
132            description: "Rust structural rules".to_string(),
133            dectauthors: Some(env!("CARGO_PKG_AUTHORS").to_string()),
134            supported_extensions: vec!["rs".to_string()],
135            supported_filenames: vec![
136                "Cargo.toml".to_string(),
137                "build.rs".to_string(),
138                "rust-toolchain".to_string(),
139                "rust-toolchain.toml".to_string(),
140                ".rustfmt.toml".to_string(),
141                "rustfmt.toml".to_string(),
142                "clippy.toml".to_string(),
143                ".clippy.toml".to_string(),
144            ],
145            skip_filenames: vec!["Cargo.lock".to_string()],
146            capabilities: vec![dictator_decree_abi::Capability::Lint],
147        }
148    }
149}
150
151#[must_use]
152pub fn init_decree() -> BoxDecree {
153    Box::new(RustDecree::default())
154}
155
156/// Create decree with custom config
157#[must_use]
158pub fn init_decree_with_config(config: RustConfig) -> BoxDecree {
159    Box::new(RustDecree::new(config, SupremeConfig::default()))
160}
161
162/// Create decree with custom config + supreme config (merged from decree.supreme + decree.rust)
163#[must_use]
164pub fn init_decree_with_configs(config: RustConfig, supreme: SupremeConfig) -> BoxDecree {
165    Box::new(RustDecree::new(config, supreme))
166}
167
168/// Convert `DecreeSettings` to `RustConfig`
169#[must_use]
170pub fn config_from_decree_settings(settings: &dictator_core::DecreeSettings) -> RustConfig {
171    RustConfig {
172        max_lines: settings.max_lines.unwrap_or(400),
173        min_edition: settings.min_edition.clone(),
174        min_rust_version: settings.min_rust_version.clone(),
175        ignore_comments: settings.ignore_comments.unwrap_or(false),
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn ignores_long_comment_lines_when_configured() {
185        let long_comment = format!("// {}\n", "x".repeat(150));
186        let src = format!("fn main() {{\n{long_comment}}}\n");
187        let config = RustConfig {
188            ignore_comments: true,
189            ..Default::default()
190        };
191        let supreme = SupremeConfig {
192            max_line_length: Some(120),
193            ..Default::default()
194        };
195        let diags = lint_source_with_configs(&src, &config, &supreme);
196        assert!(!diags.iter().any(|d| d.rule == "rust/line-too-long"));
197    }
198
199    #[test]
200    fn detects_long_comment_lines_when_not_configured() {
201        let long_comment = format!("// {}\n", "x".repeat(150));
202        let src = format!("fn main() {{\n{long_comment}}}\n");
203        let config = RustConfig::default(); // ignore_comments = false
204        let supreme = SupremeConfig {
205            max_line_length: Some(120),
206            ..Default::default()
207        };
208        let diags = lint_source_with_configs(&src, &config, &supreme);
209        assert!(diags.iter().any(|d| d.rule == "rust/line-too-long"));
210    }
211
212    #[test]
213    fn still_detects_long_code_lines_with_ignore_comments() {
214        let long_code = format!("    let x = \"{}\";\n", "a".repeat(150));
215        let src = format!("fn main() {{\n{long_code}}}\n");
216        let config = RustConfig {
217            ignore_comments: true,
218            ..Default::default()
219        };
220        let supreme = SupremeConfig {
221            max_line_length: Some(120),
222            ..Default::default()
223        };
224        let diags = lint_source_with_configs(&src, &config, &supreme);
225        assert!(diags.iter().any(|d| d.rule == "rust/line-too-long"));
226    }
227}