Skip to main content

cargo_dokita/
lib.rs

1//! # Cargo Dokita
2//!
3//! A comprehensive Rust project analysis tool that performs static analysis on Rust projects
4//! to identify potential issues, security vulnerabilities, and code quality problems.
5//!
6//! ## Features
7//!
8//! - **Dependency Analysis**: Check for outdated dependencies and known security vulnerabilities
9//! - **Code Quality Checks**: Analyze code patterns and project structure
10//! - **Manifest Validation**: Validate Cargo.toml metadata and configuration
11//! - **Configurable Rules**: Support for custom configuration through `.dokita.toml` files
12//! - **Multiple Output Formats**: Support for both human-readable and JSON output
13//!
14//! ## Usage
15//!
16//! The main entry point for analysis is the [`analyze_project`] function:
17//!
18//! ```rust,no_run
19//! use cargo_dokita::analyze_project;
20//!
21//! // Analyze a Rust project with default text output
22//! match analyze_project("./my-rust-project", "text") {
23//!     Ok(()) => println!("Analysis completed successfully"),
24//!     Err(e) => eprintln!("Analysis failed: {:?}", e),
25//! }
26//! ```
27//!
28//! ## Modules
29//!
30//! - [`manifest`] - Cargo.toml parsing and validation
31//! - [`diagnostics`] - Core diagnostic types and severity levels
32//! - [`dependency_analysis`] - Dependency checking and vulnerability scanning
33//! - [`crates_io_api`] - Integration with crates.io API
34//! - [`code_checks`] - Static code analysis and pattern detection
35//! - [`config`] - Configuration file handling and settings
36
37// filepath: /home/sally-nwamama/Desktop/rust_projects/cargo-dokita/src/lib.rs
38use dependency_analysis::check_vulnerability;
39use diagnostics::{Finding, Severity};
40use reqwest::blocking::Client as HttpClient;
41use std::io::Write; // For termcolor
42use std::{fs, path::Path, process};
43use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
44
45/// Cargo.toml manifest parsing and validation functionality.
46pub mod manifest;
47
48/// Core diagnostic types, severity levels, and finding structures.
49pub mod diagnostics;
50
51/// Dependency analysis including vulnerability scanning and outdated package detection.
52pub mod dependency_analysis;
53
54/// Integration with the crates.io API for package information retrieval.
55pub mod crates_io_api;
56
57/// Static code analysis and project structure validation.
58pub mod code_checks;
59
60/// Configuration file handling and project settings management.
61pub mod config;
62
63/// Error types that can occur during project analysis.
64#[derive(Debug)]
65pub enum MyError {
66    /// The specified directory is not a valid Rust project (missing Cargo.toml).
67    NotRustProject,
68    /// The provided project path could not be resolved or canonicalized.
69    UnresolvableProjectPath,
70    /// Analysis completed but found issues. Contains the list of findings for test purposes.
71    HasIssues(Vec<Finding>), // For test purposes
72}
73
74/// Analyzes a Rust project for potential issues and vulnerabilities.
75///
76/// This function performs a comprehensive analysis of a Rust project, including:
77/// - Dependency vulnerability scanning
78/// - Code pattern analysis
79/// - Project structure validation
80/// - Manifest (Cargo.toml) checks
81/// - Configuration validation
82///
83/// # Arguments
84///
85/// * `project_path` - Path to the root directory of the Rust project to analyze
86/// * `output_format` - Output format for results ("json" for JSON output, anything else for human-readable text)
87///
88/// # Returns
89///
90/// Returns `Ok(())` if analysis completes successfully (even if issues are found).
91/// Returns `Err(MyError)` if:
92/// - The project path cannot be resolved ([`MyError::UnresolvableProjectPath`])
93/// - The directory is not a valid Rust project ([`MyError::NotRustProject`])
94///
95/// # Behavior
96///
97/// - If no issues are found, prints a success message in green
98/// - If issues are found, outputs them according to the specified format
99/// - Calls `process::exit(1)` if any errors or warnings are found
100/// - Supports parallel execution of some analysis phases for improved performance
101///
102/// # Examples
103///
104/// ```rust,no_run
105/// use cargo_dokita::analyze_project;
106///
107/// // Analyze with text output
108/// analyze_project("./my-project", "human").unwrap();
109///
110/// // Analyze with JSON output
111/// analyze_project("./my-project", "json").unwrap();
112/// ```
113///
114/// # Panics
115///
116/// This function may panic if there are issues with terminal color output,
117/// but such panics are handled gracefully with `unwrap_or_default()`.
118pub fn analyze_project(project_path: &str, output_format: &str) -> Result<(), MyError> {
119    let mut findings: Vec<Finding> = Vec::new();
120    let project_path = match fs::canonicalize(project_path) {
121        Ok(path) => path,
122        Err(e) => {
123            eprint!("Error Could not resolve project path - {e:?}");
124            return Err(MyError::UnresolvableProjectPath);
125        }
126    };
127
128    let mut stdout = StandardStream::stdout(ColorChoice::Auto);
129
130    let config = match config::Config::load_from_project_root(&project_path) {
131        Ok(cfg) => {
132            if project_path.join(config::CONFIG_FILE_NAME).exists() {
133                println!("Loaded configuration from {}", config::CONFIG_FILE_NAME);
134            }
135            cfg
136        }
137        Err(e) => {
138            println!(
139                "Warning: Could not load or parse {}: {}. Using default configuration.",
140                config::CONFIG_FILE_NAME,
141                e
142            );
143            // Optionally add a Finding for bad config
144            config::Config::default()
145        }
146    };
147
148    let rust_files = code_checks::collect_rust_files(project_path.as_path());
149    findings.extend(code_checks::check_code_patterns(&rust_files, &project_path));
150
151    if !is_rust_project(&project_path) {
152        eprintln!("This is not a rust project");
153        return Err(MyError::NotRustProject);
154    }
155
156    let cargo_toml_path = project_path.join("Cargo.toml");
157
158    let cargo_manifest = manifest::CargoManifest::parse(cargo_toml_path.as_path());
159
160    if let Ok(data) = &cargo_manifest {
161        findings.extend(code_checks::check_project_structure(
162            &project_path,
163            Some(data),
164        ));
165    }
166
167    let http_client = HttpClient::new();
168
169    let (manifest_findings, dep_findings) = rayon::join(
170        || {
171            let mut f = Vec::new();
172            // md is Option<CargoManifest>
173            if let Ok(md) = cargo_manifest {
174                f.extend(manifest::check_missing_metadata(&md, &config));
175                f.extend(manifest::check_dependency_versions(&md, &config));
176                f.extend(manifest::check_rust_edition(&md));
177            }
178            f
179        },
180        || {
181            let mut f = Vec::new();
182            match dependency_analysis::get_project_metadata(cargo_toml_path.as_path()) {
183                Ok(metadata) => {
184                    let outdated_dependencies_findings =
185                        dependency_analysis::check_outdated_dependencies(&metadata, &http_client);
186                    f.extend(outdated_dependencies_findings);
187                }
188                Err(e) => {
189                    println!("{e:?}");
190                }
191            }
192            let vulnerability_findings = check_vulnerability(project_path.as_path());
193            f.extend(vulnerability_findings);
194            f
195        },
196    );
197
198    findings.extend(manifest_findings);
199    findings.extend(dep_findings);
200
201    findings.extend(code_checks::check_missing_denied_lints(
202        project_path.as_path(),
203        &config,
204    ));
205
206    if findings.is_empty() {
207        stdout
208            .set_color(ColorSpec::new().set_fg(Some(Color::Green)))
209            .unwrap_or_default();
210        writeln!(
211            &mut stdout,
212            "No issues found. Your project looks healthy (based on current checks)!"
213        )
214        .unwrap_or_default();
215        stdout.reset().unwrap_or_default();
216    } else {
217        if output_format == "json" {
218            match serde_json::to_string_pretty(&findings) {
219                Ok(json_output) => println!("{json_output}",),
220                Err(e) => {
221                    eprintln!("Error serializing findings to JSON: {e:?}");
222                    process::exit(1);
223                }
224            }
225        } else {
226            for finding in &findings {
227                // Basic output, can be improved with termcolor later
228                let severity_str = match finding.severity {
229                    Severity::Error => "ERROR",
230                    Severity::Warning => "WARNING",
231                    Severity::Note => "NOTE",
232                };
233
234                stdout
235                    .set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))
236                    .unwrap_or_default();
237                write!(&mut stdout, "[{severity_str}]").unwrap_or_default();
238                stdout.reset().unwrap_or_default();
239
240                let file_info = finding.file_path.as_deref().unwrap_or("N/A");
241                let line_info = finding
242                    .line_number
243                    .map_or("".to_string(), |l| format!("{l}"));
244
245                stdout
246                    .set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))
247                    .unwrap_or_default();
248                write!(&mut stdout, " ({})", finding.code).unwrap_or_default();
249                stdout.reset().unwrap_or_default();
250
251                writeln!(
252                    &mut stdout,
253                    ": {} [{}{}]",
254                    finding.message, file_info, line_info
255                )
256                .unwrap_or_default();
257            }
258        }
259
260        writeln!(&mut stdout, "\nFound {} issues:", findings.len()).unwrap_or_default();
261    }
262
263    if findings
264        .iter()
265        .any(|f| matches!(f.severity, Severity::Error | Severity::Warning))
266    {
267        process::exit(1);
268    }
269
270    Ok(())
271}
272
273/// Test-friendly version of [`analyze_project`] that returns findings instead of calling `process::exit`.
274///
275/// This function performs the same analysis as [`analyze_project`] but is designed for use in tests
276/// and other scenarios where you need programmatic access to the findings without side effects.
277///
278/// # Arguments
279///
280/// * `project_path` - Path to the root directory of the Rust project to analyze
281/// * `_output_format` - Output format parameter (currently unused in this function)
282///
283/// # Returns
284///
285/// Returns:
286/// - `Ok(Vec<Finding>)` - Analysis completed successfully with the list of findings (may be empty)
287/// - `Err(MyError::UnresolvableProjectPath)` - The project path could not be resolved
288/// - `Err(MyError::NotRustProject)` - The directory is not a valid Rust project
289///
290/// # Differences from `analyze_project`
291///
292/// - Does not print output to stdout/stderr
293/// - Does not call `process::exit()`
294/// - Returns findings as a vector for programmatic inspection
295/// - Suitable for use in unit tests and integration tests
296///
297/// # Examples
298///
299/// ```rust,no_run
300/// use cargo_dokita::analyze_project_for_test;
301///
302/// match analyze_project_for_test("./test-project", "json") {
303///     Ok(findings) => {
304///         println!("Found {} issues", findings.len());
305///         for finding in findings {
306///             println!("Issue: {}", finding.message);
307///         }
308///     },
309///     Err(e) => eprintln!("Analysis failed: {:?}", e),
310/// }
311/// ```
312pub fn analyze_project_for_test(
313    project_path: &str,
314    _output_format: &str,
315) -> Result<Vec<Finding>, MyError> {
316    let mut findings: Vec<Finding> = Vec::new();
317    let project_path = match fs::canonicalize(project_path) {
318        Ok(path) => path,
319        Err(e) => {
320            eprint!("Error Could not resolve project path - {e:?}");
321            return Err(MyError::UnresolvableProjectPath);
322        }
323    };
324
325    let config = config::Config::load_from_project_root(&project_path).unwrap_or_default();
326
327    // Code checks first (before checking if it's a Rust project)
328    let rust_files = code_checks::collect_rust_files(project_path.as_path());
329    findings.extend(code_checks::check_code_patterns(&rust_files, &project_path));
330
331    if !is_rust_project(&project_path) {
332        return Err(MyError::NotRustProject);
333    }
334
335    let cargo_toml_path = project_path.join("Cargo.toml");
336    let cargo_manifest = manifest::CargoManifest::parse(cargo_toml_path.as_path());
337
338    if let Ok(data) = &cargo_manifest {
339        findings.extend(code_checks::check_project_structure(
340            &project_path,
341            Some(data),
342        ));
343    }
344
345    let http_client = HttpClient::new();
346
347    let (manifest_findings, dep_findings) = rayon::join(
348        || {
349            let mut f = Vec::new();
350            if let Ok(md) = cargo_manifest {
351                f.extend(manifest::check_missing_metadata(&md, &config));
352                f.extend(manifest::check_dependency_versions(&md, &config));
353                f.extend(manifest::check_rust_edition(&md));
354            }
355            f
356        },
357        || {
358            let mut f = Vec::new();
359            if let Ok(metadata) =
360                dependency_analysis::get_project_metadata(cargo_toml_path.as_path())
361            {
362                let outdated_dependencies_findings =
363                    dependency_analysis::check_outdated_dependencies(&metadata, &http_client);
364                f.extend(outdated_dependencies_findings);
365            }
366            let vulnerability_findings = check_vulnerability(project_path.as_path());
367            f.extend(vulnerability_findings);
368            f
369        },
370    );
371
372    findings.extend(manifest_findings);
373    findings.extend(dep_findings);
374    findings.extend(code_checks::check_missing_denied_lints(
375        project_path.as_path(),
376        &config,
377    ));
378
379    Ok(findings)
380}
381
382/// Checks if the given path represents a valid Rust project.
383///
384/// A directory is considered a valid Rust project if:
385/// 1. The path points to an existing directory
386/// 2. The directory contains a `Cargo.toml` file
387///
388/// # Arguments
389///
390/// * `project_path` - Path to the directory to check
391///
392/// # Returns
393///
394/// Returns `true` if the path is a valid Rust project directory, `false` otherwise.
395///
396/// # Examples
397///
398/// ```rust,no_run
399/// use std::path::PathBuf;
400/// # use cargo_dokita::*;
401///
402/// let valid_project = PathBuf::from("./my-rust-project");
403/// let invalid_project = PathBuf::from("./not-a-rust-project");
404///
405/// // This would be true if ./my-rust-project contains Cargo.toml
406/// // let is_valid = is_rust_project(&valid_project);
407/// ```
408fn is_rust_project(project_path: &Path) -> bool {
409    if !project_path.is_dir() {
410        return false;
411    }
412
413    project_path.join("Cargo.toml").is_file()
414}
415
416/// Unit tests for the library functionality.
417///
418/// This module contains tests for the core analysis functions and helper utilities.
419/// Tests use the [`analyze_project_for_test`] function to avoid side effects.
420#[cfg(test)]
421mod tests {}