Skip to main content

garbage_code_hunter/deps_shamer/
mod.rs

1//! Dependency Shame module.
2//!
3//! Scans project dependency files, identifies problematic patterns,
4//! and generates a roasting report about dependency management quality.
5//!
6//! Supported ecosystems: Rust (Cargo.toml), Node (package.json),
7//! Go (go.mod), Python (requirements.txt, pyproject.toml).
8
9pub mod parser;
10pub mod report;
11pub mod rules;
12pub mod types;
13
14use anyhow::Result;
15use std::path::Path;
16
17use parser::detect_and_parse;
18use report::{format_json, format_terminal};
19use rules::check_dep_file;
20use types::DepFile;
21
22pub use crate::common::OutputFormat;
23
24/// Run dependency analysis on the given project path.
25///
26/// Detects dependency files, parses them, applies rules,
27/// and returns a formatted report string.
28pub fn run(project_path: &Path, format: &OutputFormat) -> Result<String> {
29    let dep_files = detect_and_parse(project_path)?;
30
31    if dep_files.is_empty() {
32        return match format {
33            OutputFormat::Terminal => Ok(format!(
34                "\n{}\n\n  No dependency files found in {}\n\n  Supported: Cargo.toml, package.json, go.mod, requirements.txt, pyproject.toml\n",
35                "\u{1f4e6} Dependency Shame Report \u{1f4e6}",
36                project_path.display()
37            )),
38            OutputFormat::Json => Ok(r#"{"score":100,"total_deps":0,"issues":[],"files":[]}"#.to_string()),
39        };
40    }
41
42    let all_issues: Vec<_> = dep_files.iter().flat_map(check_dep_file).collect();
43
44    let output = match format {
45        OutputFormat::Terminal => format_terminal(&dep_files, &all_issues),
46        OutputFormat::Json => format_json(&dep_files, &all_issues),
47    };
48
49    Ok(output)
50}
51
52/// Run analysis and return structured data (for CLI integration).
53pub fn analyze(project_path: &Path) -> Result<(Vec<DepFile>, Vec<types::DepIssue>)> {
54    let dep_files = detect_and_parse(project_path)?;
55    let all_issues: Vec<_> = dep_files.iter().flat_map(check_dep_file).collect();
56    Ok((dep_files, all_issues))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::fs;
63    use tempfile::TempDir;
64
65    #[test]
66    fn test_run_no_dep_files() {
67        let dir = TempDir::new().unwrap();
68        let output = run(dir.path(), &OutputFormat::Terminal).unwrap();
69        assert!(output.contains("No dependency files found"));
70    }
71
72    #[test]
73    fn test_run_no_dep_files_json() {
74        let dir = TempDir::new().unwrap();
75        let output = run(dir.path(), &OutputFormat::Json).unwrap();
76        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
77        assert_eq!(parsed["total_deps"], 0);
78    }
79
80    #[test]
81    fn test_run_with_cargo_toml() {
82        let dir = TempDir::new().unwrap();
83        fs::write(
84            dir.path().join("Cargo.toml"),
85            r#"
86[dependencies]
87serde = "1.0"
88my-git-crate = { git = "https://github.com/foo/bar" }
89wildcard = "*"
90"#,
91        )
92        .unwrap();
93
94        let output = run(dir.path(), &OutputFormat::Terminal).unwrap();
95        assert!(output.contains("Dependency Shame Report"));
96        // Should detect wildcard and git dep issues
97        assert!(output.contains("wildcard") || output.contains("git"));
98    }
99
100    #[test]
101    fn test_run_json_format() {
102        let dir = TempDir::new().unwrap();
103        fs::write(
104            dir.path().join("Cargo.toml"),
105            "[dependencies]\nserde = \"1.0\"\n",
106        )
107        .unwrap();
108
109        let output = run(dir.path(), &OutputFormat::Json).unwrap();
110        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
111        assert!(parsed["score"].as_f64().unwrap() > 0.0);
112    }
113
114    #[test]
115    fn test_analyze_returns_structured_data() {
116        let dir = TempDir::new().unwrap();
117        fs::write(
118            dir.path().join("Cargo.toml"),
119            "[dependencies]\nserde = \"1.0\"\n",
120        )
121        .unwrap();
122
123        let (dep_files, issues) = analyze(dir.path()).unwrap();
124        assert_eq!(dep_files.len(), 1);
125        assert_eq!(dep_files[0].ecosystem, types::Ecosystem::Rust);
126        assert_eq!(dep_files[0].dependencies.len(), 1);
127        // serde = "1.0" should not trigger any issues
128        assert!(issues.is_empty());
129    }
130
131    #[test]
132    fn test_run_multiple_ecosystems() {
133        let dir = TempDir::new().unwrap();
134        fs::write(
135            dir.path().join("Cargo.toml"),
136            "[dependencies]\nserde = \"1.0\"\n",
137        )
138        .unwrap();
139        fs::write(
140            dir.path().join("package.json"),
141            r#"{"dependencies": {"express": "^4.0"}}"#,
142        )
143        .unwrap();
144
145        let (dep_files, _) = analyze(dir.path()).unwrap();
146        assert_eq!(dep_files.len(), 2);
147        assert!(dep_files
148            .iter()
149            .any(|f| f.ecosystem == types::Ecosystem::Rust));
150        assert!(dep_files
151            .iter()
152            .any(|f| f.ecosystem == types::Ecosystem::Node));
153    }
154}