1use std::path::{Path, PathBuf};
6use std::process::Command;
7
8#[derive(Debug, Clone)]
10pub struct FileAuditResult {
11 pub matches: Vec<PathBuf>,
13 pub scanned: usize,
15 pub patterns: Vec<String>,
17}
18
19impl FileAuditResult {
20 pub fn has_violations(&self) -> bool {
22 !self.matches.is_empty()
23 }
24
25 pub fn violation_count(&self) -> usize {
27 self.matches.len()
28 }
29}
30
31const EXCLUDED_SCRIPT_DIRS: &[&str] = &[
33 "node_modules",
34 "venv",
35 ".venv",
36 "__pycache__",
37 "/target/",
38 "/dist/",
39 "/examples/",
40 "/migrations/",
41 "/book/",
42 "/docs/",
43 "/fixtures/",
44 "/testdata/",
45];
46
47fn is_excluded_script_path(path_str: &str) -> bool {
49 EXCLUDED_SCRIPT_DIRS.iter().any(|ex| path_str.contains(ex))
50}
51
52pub fn audit_scripting_files(project_path: &Path) -> FileAuditResult {
54 let patterns = vec![
55 "**/*.py".to_string(),
56 "**/*.js".to_string(),
57 "**/*.ts".to_string(),
58 "**/*.lua".to_string(),
59 "**/*.rb".to_string(),
60 ];
61
62 let mut matches = Vec::new();
63 let mut scanned = 0;
64
65 for pattern in &patterns {
66 let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) else {
67 continue;
68 };
69 for entry in entries.flatten() {
70 scanned += 1;
71 if !is_excluded_script_path(&entry.to_string_lossy()) {
72 matches.push(entry);
73 }
74 }
75 }
76
77 FileAuditResult { matches, scanned, patterns }
78}
79
80pub fn audit_test_frameworks(project_path: &Path) -> FileAuditResult {
82 let patterns = vec![
83 "**/*.test.js".to_string(),
85 "**/*.spec.js".to_string(),
86 "**/*.test.ts".to_string(),
87 "**/*.spec.ts".to_string(),
88 "**/jest.config.*".to_string(),
89 "**/vitest.config.*".to_string(),
90 "**/test_*.py".to_string(),
92 "**/*_test.py".to_string(),
93 "**/conftest.py".to_string(),
94 "**/pytest.ini".to_string(),
95 ];
96
97 let mut matches = Vec::new();
98 let mut scanned = 0;
99
100 for pattern in &patterns {
101 if let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) {
102 for entry in entries.flatten() {
103 scanned += 1;
104 let path_str = entry.to_string_lossy();
105
106 if !path_str.contains("node_modules") && !path_str.contains("venv") {
107 matches.push(entry);
108 }
109 }
110 }
111 }
112
113 FileAuditResult { matches, scanned, patterns }
114}
115
116pub fn audit_yaml_configs(project_path: &Path) -> FileAuditResult {
118 let patterns = vec![
119 "*.yaml".to_string(),
120 "*.yml".to_string(),
121 "config/**/*.yaml".to_string(),
122 "config/**/*.yml".to_string(),
123 "examples/**/*.yaml".to_string(),
124 "examples/**/*.yml".to_string(),
125 ];
126
127 let mut matches = Vec::new();
128 let mut scanned = 0;
129
130 for pattern in &patterns {
131 if let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) {
132 for entry in entries.flatten() {
133 scanned += 1;
134 matches.push(entry);
135 }
136 }
137 }
138
139 FileAuditResult { matches, scanned, patterns }
140}
141
142#[derive(Debug, Clone)]
144pub struct DependencyAuditResult {
145 pub forbidden: Vec<String>,
147 pub checked: Vec<String>,
149 pub cargo_tree_output: Option<String>,
151}
152
153impl DependencyAuditResult {
154 pub fn has_violations(&self) -> bool {
156 !self.forbidden.is_empty()
157 }
158}
159
160pub fn audit_cargo_dependencies(project_path: &Path, forbidden: &[&str]) -> DependencyAuditResult {
162 let cargo_toml = project_path.join("Cargo.toml");
163 let mut found_forbidden = Vec::new();
164 let mut checked = Vec::new();
165
166 if cargo_toml.exists() {
167 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
168 for dep in forbidden {
169 checked.push((*dep).to_string());
170
171 if content.contains(&format!("{} =", dep))
173 || content.contains(&format!("{}=", dep))
174 || content.contains(&format!("\"{}\"", dep))
175 {
176 found_forbidden.push((*dep).to_string());
177 }
178 }
179 }
180 }
181
182 let cargo_tree_output = Command::new("cargo")
184 .args(["tree", "--edges", "no-dev", "-p"])
185 .current_dir(project_path)
186 .output()
187 .ok()
188 .and_then(|output| {
189 if output.status.success() {
190 String::from_utf8(output.stdout).ok()
191 } else {
192 None
193 }
194 });
195
196 DependencyAuditResult { forbidden: found_forbidden, checked, cargo_tree_output }
197}
198
199pub fn has_rust_tests(project_path: &Path) -> bool {
201 project_path.join("tests").exists()
202 || super::helpers::files_contain_pattern(
203 project_path,
204 &["src/**/*.rs"],
205 &["#[test]", "#[cfg(test)]"],
206 )
207}
208
209pub fn has_wasm_support(project_path: &Path) -> WasmSupport {
211 let cargo_toml = project_path.join("Cargo.toml");
212 let mut support = WasmSupport::default();
213
214 if cargo_toml.exists() {
215 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
216 support.has_wasm_feature = content.contains("[features]")
217 && (content.contains("wasm") || content.contains("web"));
218
219 support.has_wasm_bindgen = content.contains("wasm-bindgen");
220 support.has_web_sys = content.contains("web-sys");
221 support.has_wasm_pack = content.contains("wasm-pack");
222 }
223 }
224
225 support.has_wasm_module = project_path.join("src/wasm.rs").exists();
227
228 let cargo_config = project_path.join(".cargo/config.toml");
230 if cargo_config.exists() {
231 if let Ok(content) = std::fs::read_to_string(&cargo_config) {
232 support.has_wasm_target = content.contains("wasm32");
233 }
234 }
235
236 support
237}
238
239#[derive(Debug, Clone, Default)]
241pub struct WasmSupport {
242 pub has_wasm_feature: bool,
243 pub has_wasm_bindgen: bool,
244 pub has_web_sys: bool,
245 pub has_wasm_pack: bool,
246 pub has_wasm_module: bool,
247 pub has_wasm_target: bool,
248}
249
250impl WasmSupport {
251 pub fn is_supported(&self) -> bool {
253 self.has_wasm_feature
254 || self.has_wasm_bindgen
255 || self.has_web_sys
256 || self.has_wasm_pack
257 || self.has_wasm_module
258 }
259
260 pub fn level(&self) -> &'static str {
262 if self.has_wasm_bindgen && self.has_wasm_module {
263 "Full"
264 } else if self.has_wasm_bindgen || self.has_wasm_feature {
265 "Partial"
266 } else if self.has_wasm_module {
267 "Basic"
268 } else {
269 "None"
270 }
271 }
272}
273
274fn scan_deserialize_structs(project_path: &Path) -> (bool, bool) {
276 let mut has_deserialize = false;
277 let mut has_config = false;
278 let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
279 return (false, false);
280 };
281 for entry in entries.flatten() {
282 let Ok(content) = std::fs::read_to_string(&entry) else {
283 continue;
284 };
285 if content.contains("#[derive") && content.contains("Deserialize") {
286 has_deserialize = true;
287 if content.to_lowercase().contains("config") {
288 has_config = true;
289 }
290 }
291 }
292 (has_deserialize, has_config)
293}
294
295pub fn has_serde_config(project_path: &Path) -> SerdeConfigSupport {
297 let cargo_toml = project_path.join("Cargo.toml");
298 let content = std::fs::read_to_string(&cargo_toml).unwrap_or_default();
299 let (has_deserialize_structs, has_config_struct) = scan_deserialize_structs(project_path);
300
301 SerdeConfigSupport {
302 has_serde: content.contains("serde"),
303 has_serde_yaml: content.contains("serde_yaml")
304 || content.contains("serde_yml")
305 || content.contains("serde_yaml_ng"),
306 has_serde_json: content.contains("serde_json"),
307 has_toml: content.contains("toml"),
308 has_validator: content.contains("validator") || content.contains("garde"),
309 has_deserialize_structs,
310 has_config_struct,
311 }
312}
313
314#[derive(Debug, Clone, Default)]
316pub struct SerdeConfigSupport {
317 pub has_serde: bool,
318 pub has_serde_yaml: bool,
319 pub has_serde_json: bool,
320 pub has_toml: bool,
321 pub has_validator: bool,
322 pub has_deserialize_structs: bool,
323 pub has_config_struct: bool,
324}
325
326impl SerdeConfigSupport {
327 pub fn has_typed_config(&self) -> bool {
329 self.has_serde && self.has_config_struct
330 }
331
332 pub fn has_validation(&self) -> bool {
334 self.has_validator || self.has_serde }
336}
337
338#[cfg(test)]
339#[path = "auditors_tests.rs"]
340mod tests;