Skip to main content

cargo_dokita/
code_checks.rs

1//! Code quality analysis and project structure checks for Rust projects.
2//!
3//! This module provides comprehensive static analysis capabilities for Rust codebases,
4//! focusing on code quality, best practices, and project structure validation.
5//!
6//! # Features
7//!
8//! ## Code Pattern Analysis
9//! - Detects potentially problematic patterns like `.unwrap()` and `.expect()` in library code
10//! - Identifies debug macros (`println!`, `dbg!`) that should be removed before release
11//! - Finds TODO/FIXME/XXX comments that need attention
12//! - Supports parallel processing for improved performance on large codebases
13//!
14//! ## Project Structure Validation
15//! - Validates presence of essential files (README.md, LICENSE)
16//! - Checks for proper source file organization (src/lib.rs, src/main.rs, src/bin/)
17//! - Integrates with Cargo manifest data for context-aware analysis
18//!
19//! ## Lint Configuration Checks
20//! - Verifies presence of recommended `#![deny(...)]` attributes
21//! - Configurable through the project's configuration system
22//!
23//! # Usage
24//!
25//! ```rust,no_run
26//! use std::path::Path;
27//! use cargo_dokita::code_checks::{collect_rust_files, check_code_patterns, check_project_structure};
28//!
29//! let project_root = Path::new("./my_project");
30//! let rust_files = collect_rust_files(project_root);
31//! let findings = check_code_patterns(&rust_files, project_root);
32//!
33//! for finding in findings {
34//!     println!("{}: {}", finding.code, finding.message);
35//! }
36//! ```
37//!
38//! The module is designed to integrate seamlessly with cargo-dokita's diagnostic system
39//! and configuration management, providing actionable feedback for Rust developers.
40
41use crate::config::Config;
42use crate::diagnostics::{Finding, Severity};
43use crate::manifest::CargoManifest;
44use once_cell::sync::Lazy;
45use rayon::prelude::*;
46use regex::Regex;
47use std::fs;
48use std::path::{Path, PathBuf};
49use walkdir::{DirEntry, WalkDir};
50
51static UNWRAP_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\.unwrap\(\)").unwrap());
52static EXPECT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\.expect\s*\("#).unwrap());
53static PRINTLN_DBG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(println!|dbg!)\s*\(").unwrap());
54static TODO_COMMENT_REGEX: Lazy<Regex> =
55    Lazy::new(|| Regex::new(r"//\s*(TODO|FIXME|XXX)").unwrap());
56static DENY_LINT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"#!\[deny\(([^)]+)\)\]").unwrap());
57
58fn is_rust_file(entry: &DirEntry) -> bool {
59    entry.file_type().is_file() && entry.path().extension().is_some_and(|ext| ext == "rs")
60}
61
62pub fn collect_rust_files(project_root: &Path) -> Vec<PathBuf> {
63    let mut rust_files = Vec::new();
64    let source_roots = [
65        project_root.join("src"),
66        project_root.join("tests"),
67        project_root.join("examples"),
68        project_root.join("benches"),
69    ];
70
71    for root in source_roots.iter().filter(|p| p.exists() && p.is_dir()) {
72        WalkDir::new(root)
73            .into_iter()
74            .filter_map(Result::ok) // Ignore errors during walk, or handle them
75            .filter(is_rust_file)
76            .for_each(|entry| rust_files.push(entry.path().to_path_buf()));
77    }
78    rust_files
79}
80
81fn is_library_file(file_path: &Path, project_root: &Path) -> bool {
82    let src_lib_path = project_root.join("src").join("lib.rs");
83
84    file_path.starts_with(project_root.join("src"))
85        && file_path != src_lib_path // Allow in lib.rs if it's a binary-only crate's "main" logic
86        && !file_path.ends_with("main.rs") // Check if it's literally main.rs
87        && file_path.components().all(|c| c.as_os_str() != "bin") // Not in a src/bin/ subdirectory
88        && file_path != project_root.join("build.rs") // Not build script
89}
90
91pub fn check_code_patterns(rust_files: &[PathBuf], project_root: &Path) -> Vec<Finding> {
92    let findings_from_all_files: Vec<Finding> = rust_files
93        .par_iter()
94        .flat_map(|file_path_ref| {
95            let file_path = &**file_path_ref;
96            let mut per_file_findings: Vec<Finding> = Vec::new();
97
98            let is_lib_context = is_library_file(file_path, project_root);
99
100
101        // Skip build.rs for some checks like unwrap/expect, as they are common there
102        if file_path.ends_with("build.rs") {
103            // Could have specific checks for build.rs if desired
104            // continue; // Or let specific checks decide
105        }
106
107
108        let content = match fs::read_to_string(file_path) {
109            Ok(c) => c,
110            Err(e) => {
111                per_file_findings.push(Finding::new(
112                    "IO001", // File Read Error
113                    format!("Failed to read file {file_path:?}: {e}"),
114                    Severity::Warning,
115                    Some(file_path.to_string_lossy().into_owned()),
116                ));
117                return per_file_findings;
118            }
119        };
120
121        // Iterate over lines to get line numbers for findings
122        for (line_num, line_content) in content.lines().enumerate() {
123            let line_number_for_finding = line_num + 1; // 1-indexed
124
125            // Check for .unwrap() in library context
126            if is_lib_context && UNWRAP_REGEX.is_match(line_content) && !file_path.ends_with("build.rs") {
127                per_file_findings.push(Finding::new(
128                    "CODE001",
129                    "'.unwrap()' used in library context. Consider using '?' or pattern matching.".to_string(),
130                    Severity::Warning,
131                    Some(file_path.to_string_lossy().into_owned()),
132                ).with_line(line_number_for_finding));
133            }
134
135            // Check for .expect() in library context
136            if is_lib_context && EXPECT_REGEX.is_match(line_content) && !file_path.ends_with("build.rs") {
137                per_file_findings.push(Finding::new(
138                    "CODE002",
139                    "'.expect()' used in library context. While better than unwrap, prefer '?' or specific error handling.".to_string(),
140                    Severity::Note, // expect is slightly better than unwrap
141                    Some(file_path.to_string_lossy().into_owned()),
142                ).with_line(line_number_for_finding));
143            }
144
145            // Check for println!/dbg! in library context
146            if is_lib_context && PRINTLN_DBG_REGEX.is_match(line_content) && !file_path.ends_with("build.rs") {
147                 // Further refine: allow in main fn of examples, benches.
148                 // This check is tricky without knowing the exact role of the file.
149                 // For now, broad check on `is_lib_context`.
150                per_file_findings.push(Finding::new(
151                    "CODE003",
152                    "Diagnostic macro (println! or dbg!) found in library context. Remove before release.".to_string(),
153                    Severity::Note,
154                    Some(file_path.to_string_lossy().into_owned()),
155                ).with_line(line_number_for_finding));
156            }
157
158            // Check for TODO/FIXME comments (applies to all files)
159            if TODO_COMMENT_REGEX.is_match(line_content) {
160                let comment_type = TODO_COMMENT_REGEX.captures(line_content).unwrap().get(1).unwrap().as_str();
161                per_file_findings.push(Finding::new(
162                    "CODE004",
163                    format!("Found '{comment_type}' comment. Address or create an issue for it."),
164                    Severity::Note,
165                    Some(file_path.to_string_lossy().into_owned()),
166                ).with_line(line_number_for_finding));
167            }
168        }
169
170            per_file_findings
171        }).collect();
172
173    findings_from_all_files
174}
175
176pub fn check_project_structure(
177    project_root: &Path,
178    manifest_data: Option<&CargoManifest>, // Pass the parsed Cargo.toml
179                                           // _metadata: Option<&Metadata>, // Pass metadata, not used yet but good for future
180) -> Vec<Finding> {
181    let mut findings = Vec::new();
182
183    let has_lib_rs = project_root.join("src").join("lib.rs").is_file();
184    let has_main_rs = project_root.join("src").join("main.rs").is_file();
185    let has_bin_dir = project_root.join("src").join("bin").is_dir();
186
187    // Heuristic: If there's a [lib] section or no explicit [[bin]] targets and no main.rs,
188    // it's likely intended to be a library.
189    let is_likely_library = manifest_data.is_some_and(|m| {
190        // Check if [lib] path is specified, or if name is specified (implies lib)
191        // This part of `toml` parsing for manifest.lib might need to be added to `CargoManifest` struct
192        m.package.as_ref().is_some_and(|p| {
193            // A common pattern is that the package name is used for the lib if not specified
194            // This is still a heuristic. cargo_metadata is better.
195            let default_lib_name = p.name.replace('-', "_");
196            project_root
197                .join("src")
198                .join(format!("{default_lib_name}.rs"))
199                .exists()
200                || has_lib_rs
201        })
202    });
203
204    if let Some(_pkg) = manifest_data.and_then(|m| m.package.as_ref()) {
205        // If it has a `[package]` section and is not a virtual workspace manifest
206        if !is_likely_library && !has_main_rs && !has_bin_dir {
207            findings.push(Finding::new(
208                "STRUCT001",
209                "Project has neither src/lib.rs, src/main.rs, nor src/bin/ directory. Is it a virtual workspace or missing source files?".to_string(),
210                Severity::Warning,
211                Some("Cargo.toml".to_string()), // Related to project structure defined by Cargo.toml implicitly
212            ));
213        }
214
215        // If it has main.rs, it's likely a binary.
216        // If it has lib.rs, it's likely a library.
217        // If it has both, it's a common pattern for a crate that is both a lib and has a default binary.
218
219        // Check for README.md (could also be in manifest checks if `readme` field points to it)
220        let readme_path_md = project_root.join("README.md");
221        let readme_path_rst = project_root.join("README.rst"); // Less common in Rust but possible
222        if !readme_path_md.is_file() && !readme_path_rst.is_file() {
223            // Check if Cargo.toml specifies `readme = false` or a custom readme file
224            let readme_specified_and_false_or_exists = manifest_data
225                .and_then(|m| m.package.as_ref())
226                .and_then(|p| p.readme.as_ref())
227                .is_some_and(|r_val| {
228                    // if r_val is a bool and false, then it's fine
229                    if let Some(b) = r_val.as_bool() {
230                        // Assuming readme can be bool or string
231                        !b
232                    } else if let Some(s) = r_val.as_str() {
233                        project_root.join(s).is_file()
234                    } else {
235                        false // Not a bool or string, odd.
236                    }
237                });
238
239            if !readme_specified_and_false_or_exists {
240                findings.push(Finding::new(
241                    "STRUCT002",
242                    "Missing README.md file in project root. Consider adding one.".to_string(),
243                    Severity::Note,
244                    Some(readme_path_md.to_string_lossy().into_owned()),
245                ));
246            }
247        }
248
249        // Check for LICENSE file (could also be in manifest checks if `license-file` points to it)
250        // Common names: LICENSE, LICENSE.txt, LICENSE-MIT, LICENSE-APACHE, COPYING
251        let license_files = [
252            "LICENSE",
253            "LICENSE.txt",
254            "LICENSE-MIT",
255            "LICENSE-APACHE",
256            "COPYING",
257            "UNLICENSE",
258        ];
259        let has_license_file = license_files.iter().any(|name| {
260            project_root.join(name).is_file()
261                || project_root.join(name.to_uppercase()).is_file()
262                || project_root.join(name.to_lowercase()).is_file()
263        });
264
265        if !has_license_file {
266            // Check if Cargo.toml specifies `license-file`
267            let license_file_specified = manifest_data
268                .and_then(|m| m.package.as_ref())
269                .is_some_and(|p| {
270                    // Assuming you add `license_file: Option<String>` to your Package struct
271                    // p.license_file.as_ref().map_or(false, |lf| project_root.join(lf).is_file())
272                    // For now, let's assume it's not specified if Package struct doesn't have it
273                    p.license.is_some() && p.license.as_deref() != Some("") // If license is specified, a file is good practice
274                });
275
276            if !license_file_specified
277                || manifest_data
278                    .and_then(|m| m.package.as_ref())
279                    .is_none_or(|p| p.license.is_none())
280            {
281                findings.push(Finding::new(
282                    "STRUCT003",
283                    "Missing LICENSE file in project root. Consider adding one (e.g., LICENSE-MIT or LICENSE-APACHE).".to_string(),
284                    Severity::Warning, // More important than README
285                    Some("Project Root".to_string()), // Generic path
286                ));
287            }
288        }
289    }
290
291    findings
292}
293
294// src/code_checks.rs
295
296pub fn check_missing_denied_lints(
297    project_root: &Path,
298    config: &Config, // Config could specify which lints are recommended
299) -> Vec<Finding> {
300    let mut findings = Vec::new();
301    // Example: Check src/lib.rs if it exists
302    let lib_rs_path = project_root.join("src/lib.rs");
303    let main_rs_path = project_root.join("src/main.rs");
304    let files_to_check = [&lib_rs_path, &main_rs_path];
305
306    let recommended_denials = vec!["warnings"]; // Could come from config
307    // or clippy::all, clippy::pedantic
308
309    for file_path in files_to_check.iter().filter(|p| p.exists()) {
310        if !config.is_check_enabled("LINT001") {
311            continue;
312        } // Example check code
313
314        if let Ok(content) = fs::read_to_string(file_path) {
315            let mut found_denials = std::collections::HashSet::new();
316            for cap in DENY_LINT_REGEX.captures_iter(&content) {
317                // cap[1] contains the comma-separated list of lints
318                for lint in cap[1].split(',').map(|s| s.trim()) {
319                    found_denials.insert(lint.to_string());
320                }
321            }
322
323            for rec_denial in &recommended_denials {
324                if !found_denials.contains(*rec_denial) {
325                    findings.push(Finding::new(
326                        "LINT001",
327                        format!("Consider adding `#![deny({})]` to the top of {:?} for stricter linting.", rec_denial, file_path.file_name().unwrap_or_default()),
328                        Severity::Note,
329                        Some(file_path.to_string_lossy().into_owned())
330                    ));
331                }
332            }
333        }
334    }
335    findings
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use crate::config::{ChecksConfig, Config};
342    use crate::diagnostics::Severity;
343    use crate::manifest::{CargoManifest, Package};
344    use std::collections::HashMap;
345    use std::fs;
346    use tempfile::TempDir;
347
348    // Helper function to create a temporary directory with test files
349    fn create_test_dir() -> TempDir {
350        TempDir::new().expect("Failed to create temp dir")
351    }
352
353    // Helper function to create a basic config
354    fn create_test_config() -> Config {
355        let mut enabled_checks = HashMap::new();
356        enabled_checks.insert("LINT001".to_string(), true);
357
358        Config {
359            general: Default::default(),
360            checks: ChecksConfig {
361                enabled: enabled_checks,
362            },
363        }
364    }
365
366    // Helper function to create a basic CargoManifest
367    fn create_test_manifest(name: &str) -> CargoManifest {
368        CargoManifest {
369            package: Some(Package {
370                name: name.to_string(),
371                version: "0.1.0".to_string(),
372                license: Some("MIT".to_string()),
373                readme: None,
374                description: Some("Test package".to_string()),
375                edition: Some("2021".to_string()),
376                repository: None,
377            }),
378            dependencies: None,
379            dev_dependencies: None,
380            build_dependencies: None,
381        }
382    }
383
384    #[test]
385    fn test_is_rust_file() {
386        let temp_dir = create_test_dir();
387        let rust_file = temp_dir.path().join("test.rs");
388        let non_rust_file = temp_dir.path().join("test.txt");
389
390        fs::write(&rust_file, "fn main() {}").unwrap();
391        fs::write(&non_rust_file, "hello").unwrap();
392
393        let rust_entry = WalkDir::new(&rust_file)
394            .into_iter()
395            .next()
396            .unwrap()
397            .unwrap();
398        let non_rust_entry = WalkDir::new(&non_rust_file)
399            .into_iter()
400            .next()
401            .unwrap()
402            .unwrap();
403
404        assert!(is_rust_file(&rust_entry));
405        assert!(!is_rust_file(&non_rust_entry));
406    }
407
408    #[test]
409    fn test_collect_rust_files() {
410        let temp_dir = create_test_dir();
411        let project_root = temp_dir.path();
412
413        // Create directory structure
414        let src_dir = project_root.join("src");
415        let tests_dir = project_root.join("tests");
416        let examples_dir = project_root.join("examples");
417
418        fs::create_dir_all(&src_dir).unwrap();
419        fs::create_dir_all(&tests_dir).unwrap();
420        fs::create_dir_all(&examples_dir).unwrap();
421
422        // Create Rust files
423        fs::write(src_dir.join("lib.rs"), "// lib content").unwrap();
424        fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
425        fs::write(tests_dir.join("integration_test.rs"), "// test content").unwrap();
426        fs::write(examples_dir.join("example.rs"), "fn main() {}").unwrap();
427
428        // Create non-Rust file (should be ignored)
429        fs::write(src_dir.join("config.toml"), "[package]").unwrap();
430
431        let rust_files = collect_rust_files(project_root);
432
433        assert_eq!(rust_files.len(), 4);
434        assert!(rust_files.iter().any(|p| p.ends_with("lib.rs")));
435        assert!(rust_files.iter().any(|p| p.ends_with("main.rs")));
436        assert!(
437            rust_files
438                .iter()
439                .any(|p| p.ends_with("integration_test.rs"))
440        );
441        assert!(rust_files.iter().any(|p| p.ends_with("example.rs")));
442    }
443
444    #[test]
445    fn test_is_library_file() {
446        let temp_dir = create_test_dir();
447        let project_root = temp_dir.path();
448        let src_dir = project_root.join("src");
449        let bin_dir = src_dir.join("bin");
450
451        fs::create_dir_all(&src_dir).unwrap();
452        fs::create_dir_all(&bin_dir).unwrap();
453
454        let lib_rs = src_dir.join("lib.rs");
455        let main_rs = src_dir.join("main.rs");
456        let module_rs = src_dir.join("module.rs");
457        let bin_file = bin_dir.join("binary.rs");
458        let build_rs = project_root.join("build.rs");
459
460        // Test various file types
461        assert!(!is_library_file(&lib_rs, project_root)); // lib.rs is allowed
462        assert!(!is_library_file(&main_rs, project_root)); // main.rs is not library
463        assert!(is_library_file(&module_rs, project_root)); // module in src/ is library
464        assert!(!is_library_file(&bin_file, project_root)); // bin/ files are not library
465        assert!(!is_library_file(&build_rs, project_root)); // build.rs is not library
466    }
467
468    #[test]
469    fn test_check_code_patterns_unwrap_detection() {
470        let temp_dir = create_test_dir();
471        let project_root = temp_dir.path();
472        let src_dir = project_root.join("src");
473
474        fs::create_dir_all(&src_dir).unwrap();
475
476        let test_file = src_dir.join("module.rs");
477        let test_content = r#"
478fn test_function() {
479    let result = some_operation().unwrap(); // This should be flagged
480    let other = another_op().expect("failed"); // This should be flagged too
481    println!("Debug output"); // This should be flagged in library context
482    // TODO: Fix this later // This should be flagged
483}
484"#;
485        fs::write(&test_file, test_content).unwrap();
486
487        let rust_files = vec![test_file];
488        let findings = check_code_patterns(&rust_files, project_root);
489
490        // Should find unwrap, expect, println, and TODO
491        assert!(findings.iter().any(|f| f.code == "CODE001")); // unwrap
492        assert!(findings.iter().any(|f| f.code == "CODE002")); // expect
493        assert!(findings.iter().any(|f| f.code == "CODE003")); // println
494        assert!(findings.iter().any(|f| f.code == "CODE004")); // TODO
495
496        // Check that line numbers are correct
497        let unwrap_finding = findings.iter().find(|f| f.code == "CODE001").unwrap();
498        assert_eq!(unwrap_finding.line_number, Some(3));
499    }
500
501    #[test]
502    fn test_check_code_patterns_build_script_exclusion() {
503        let temp_dir = create_test_dir();
504        let project_root = temp_dir.path();
505
506        let build_rs = project_root.join("build.rs");
507        let build_content = r#"
508fn main() {
509    let result = some_operation().unwrap(); // This should NOT be flagged in build.rs
510    println!("Building..."); // This should NOT be flagged in build.rs
511}
512"#;
513        fs::write(&build_rs, build_content).unwrap();
514
515        let rust_files = vec![build_rs];
516        let findings = check_code_patterns(&rust_files, project_root);
517
518        // Should not find CODE001, CODE002, or CODE003 in build.rs
519        assert!(!findings.iter().any(|f| f.code == "CODE001"));
520        assert!(!findings.iter().any(|f| f.code == "CODE002"));
521        assert!(!findings.iter().any(|f| f.code == "CODE003"));
522
523        // But should still find TODO comments
524        // (if we add one to the test content)
525    }
526
527    #[test]
528    fn test_check_code_patterns_file_read_error() {
529        let temp_dir = create_test_dir();
530        let project_root = temp_dir.path();
531
532        // Create a path to a non-existent file
533        let non_existent_file = project_root.join("nonexistent.rs");
534        let rust_files = vec![non_existent_file];
535
536        let findings = check_code_patterns(&rust_files, project_root);
537
538        assert_eq!(findings.len(), 1);
539        assert_eq!(findings[0].code, "IO001");
540        assert_eq!(findings[0].severity, Severity::Warning);
541    }
542
543    #[test]
544    fn test_check_project_structure_missing_source_files() {
545        let temp_dir = create_test_dir();
546        let project_root = temp_dir.path();
547
548        // Create Cargo.toml but no source files
549        let manifest = create_test_manifest("test-project");
550
551        let findings = check_project_structure(project_root, Some(&manifest));
552
553        assert!(findings.iter().any(|f| f.code == "STRUCT001"));
554    }
555
556    #[test]
557    fn test_check_project_structure_missing_readme() {
558        let temp_dir = create_test_dir();
559        let project_root = temp_dir.path();
560        let src_dir = project_root.join("src");
561
562        fs::create_dir_all(&src_dir).unwrap();
563        fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
564
565        let manifest = create_test_manifest("test-project");
566
567        let findings = check_project_structure(project_root, Some(&manifest));
568
569        assert!(findings.iter().any(|f| f.code == "STRUCT002"));
570    }
571
572    #[test]
573    fn test_check_project_structure_missing_license() {
574        let temp_dir = create_test_dir();
575        let project_root = temp_dir.path();
576        let src_dir = project_root.join("src");
577
578        fs::create_dir_all(&src_dir).unwrap();
579        fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
580
581        let mut manifest = create_test_manifest("test-project");
582        manifest.package.as_mut().unwrap().license = None; // No license specified
583
584        let findings = check_project_structure(project_root, Some(&manifest));
585
586        assert!(findings.iter().any(|f| f.code == "STRUCT003"));
587    }
588
589    #[test]
590    fn test_check_project_structure_with_readme_and_license() {
591        let temp_dir = create_test_dir();
592        let project_root = temp_dir.path();
593        let src_dir = project_root.join("src");
594
595        fs::create_dir_all(&src_dir).unwrap();
596        fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
597        fs::write(project_root.join("README.md"), "# Test Project").unwrap();
598        fs::write(project_root.join("LICENSE"), "MIT License").unwrap();
599
600        let manifest = create_test_manifest("test-project");
601
602        let findings = check_project_structure(project_root, Some(&manifest));
603
604        // Should not flag missing README or LICENSE
605        assert!(!findings.iter().any(|f| f.code == "STRUCT002"));
606        assert!(!findings.iter().any(|f| f.code == "STRUCT003"));
607    }
608
609    #[test]
610    fn test_check_missing_denied_lints_missing_warnings() {
611        let temp_dir = create_test_dir();
612        let project_root = temp_dir.path();
613        let src_dir = project_root.join("src");
614
615        fs::create_dir_all(&src_dir).unwrap();
616
617        let lib_rs = src_dir.join("lib.rs");
618        let lib_content = r#"
619// No deny attributes here
620pub fn hello() {
621    println!("Hello, world!");
622}
623"#;
624        fs::write(&lib_rs, lib_content).unwrap();
625
626        let config = create_test_config();
627        let findings = check_missing_denied_lints(project_root, &config);
628
629        assert!(findings.iter().any(|f| f.code == "LINT001"));
630    }
631
632    #[test]
633    fn test_check_missing_denied_lints_has_warnings() {
634        let temp_dir = create_test_dir();
635        let project_root = temp_dir.path();
636        let src_dir = project_root.join("src");
637
638        fs::create_dir_all(&src_dir).unwrap();
639
640        let lib_rs = src_dir.join("lib.rs");
641        let lib_content = r#"
642#![deny(warnings)]
643
644pub fn hello() {
645    println!("Hello, world!");
646}
647"#;
648        fs::write(&lib_rs, lib_content).unwrap();
649
650        let config = create_test_config();
651        let findings = check_missing_denied_lints(project_root, &config);
652
653        // Should not flag missing warnings denial
654        assert!(!findings.iter().any(|f| f.code == "LINT001"));
655    }
656
657    #[test]
658    fn test_check_missing_denied_lints_disabled_check() {
659        let temp_dir = create_test_dir();
660        let project_root = temp_dir.path();
661        let src_dir = project_root.join("src");
662
663        fs::create_dir_all(&src_dir).unwrap();
664
665        let lib_rs = src_dir.join("lib.rs");
666        fs::write(&lib_rs, "pub fn hello() {}").unwrap();
667
668        // Create config with LINT001 disabled
669        let mut config = Config::default();
670        config.checks.enabled.insert("LINT001".to_string(), false);
671
672        let findings = check_missing_denied_lints(project_root, &config);
673
674        // Should not flag anything when check is disabled
675        assert!(!findings.iter().any(|f| f.code == "LINT001"));
676    }
677
678    #[test]
679    fn test_regex_patterns() {
680        // Test the static regex patterns
681        assert!(UNWRAP_REGEX.is_match("result.unwrap()"));
682        assert!(UNWRAP_REGEX.is_match("some_func().unwrap()"));
683        assert!(!UNWRAP_REGEX.is_match("unwrap_or_default()"));
684
685        assert!(EXPECT_REGEX.is_match(r#"result.expect("failed")"#));
686        assert!(EXPECT_REGEX.is_match("result.expect("));
687        assert!(!EXPECT_REGEX.is_match("expected_value"));
688
689        assert!(PRINTLN_DBG_REGEX.is_match("println!(\"hello\")"));
690        assert!(PRINTLN_DBG_REGEX.is_match("dbg!(value)"));
691        assert!(!PRINTLN_DBG_REGEX.is_match("print_value()"));
692
693        assert!(TODO_COMMENT_REGEX.is_match("// TODO: fix this"));
694        assert!(TODO_COMMENT_REGEX.is_match("// FIXME: broken"));
695        assert!(TODO_COMMENT_REGEX.is_match("//XXX urgent"));
696        assert!(!TODO_COMMENT_REGEX.is_match("// NOTE: this is fine"));
697
698        assert!(DENY_LINT_REGEX.is_match("#![deny(warnings)]"));
699        assert!(DENY_LINT_REGEX.is_match("#![deny(clippy::all, warnings)]"));
700    }
701
702    #[test]
703    fn test_parallel_processing() {
704        let temp_dir = create_test_dir();
705        let project_root = temp_dir.path();
706        let src_dir = project_root.join("src");
707
708        fs::create_dir_all(&src_dir).unwrap();
709
710        // Create multiple files to test parallel processing
711        for i in 0..10 {
712            let file_path = src_dir.join(format!("module_{i}.rs"));
713            let content = format!(
714                r#"
715// TODO: implement module {i}
716pub fn function_{i}() {{
717    let result = some_operation().unwrap();
718}}
719"#
720            );
721            fs::write(&file_path, content).unwrap();
722        }
723
724        let rust_files = collect_rust_files(project_root);
725        let findings = check_code_patterns(&rust_files, project_root);
726
727        // Should find issues in all files
728        assert_eq!(findings.iter().filter(|f| f.code == "CODE001").count(), 10); // unwrap
729        assert_eq!(findings.iter().filter(|f| f.code == "CODE004").count(), 10); // TODO
730    }
731}