1#![warn(rust_2024_compatibility, clippy::all)]
2
3mod 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#[derive(Debug, Clone)]
17pub struct RustConfig {
18 pub max_lines: usize,
19 pub min_edition: Option<String>,
21 pub min_rust_version: Option<String>,
23 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#[must_use]
40pub fn lint_source(source: &str) -> Diagnostics {
41 lint_source_with_configs(source, &RustConfig::default(), &SupremeConfig::default())
42}
43
44#[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#[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 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 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 if filename == "Cargo.toml" {
116 return cargo_toml::lint_cargo_toml(source, &self.config);
117 }
118
119 let mut diags = lint_source_with_configs(source, &self.config, &self.supreme);
121
122 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#[must_use]
158pub fn init_decree_with_config(config: RustConfig) -> BoxDecree {
159 Box::new(RustDecree::new(config, SupremeConfig::default()))
160}
161
162#[must_use]
164pub fn init_decree_with_configs(config: RustConfig, supreme: SupremeConfig) -> BoxDecree {
165 Box::new(RustDecree::new(config, supreme))
166}
167
168#[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(); 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}