ricecoder_research/dependency_analyzer/
nodejs_parser.rs

1//! Node.js dependency parser for package.json
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Node.js dependencies from package.json
9#[derive(Debug)]
10pub struct NodeJsParser;
11
12impl NodeJsParser {
13    /// Creates a new NodeJsParser
14    pub fn new() -> Self {
15        NodeJsParser
16    }
17
18    /// Parses dependencies from package.json
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let package_json_path = root.join("package.json");
21
22        if !package_json_path.exists() {
23            return Ok(Vec::new());
24        }
25
26        debug!("Parsing Node.js dependencies from {:?}", package_json_path);
27
28        let content = std::fs::read_to_string(&package_json_path).map_err(|e| {
29            ResearchError::DependencyParsingFailed {
30                language: "Node.js".to_string(),
31                path: Some(package_json_path.clone()),
32                reason: format!("Failed to read package.json: {}", e),
33            }
34        })?;
35
36        let package_json: serde_json::Value =
37            serde_json::from_str(&content).map_err(|e| ResearchError::DependencyParsingFailed {
38                language: "Node.js".to_string(),
39                path: Some(package_json_path.clone()),
40                reason: format!("Failed to parse package.json: {}", e),
41            })?;
42
43        let mut dependencies = Vec::new();
44
45        // Parse regular dependencies
46        if let Some(deps) = package_json.get("dependencies").and_then(|d| d.as_object()) {
47            for (name, value) in deps {
48                if let Some(version) = value.as_str() {
49                    dependencies.push(Dependency {
50                        name: name.clone(),
51                        version: version.to_string(),
52                        constraints: Some(version.to_string()),
53                        is_dev: false,
54                    });
55                }
56            }
57        }
58
59        // Parse dev dependencies
60        if let Some(deps) = package_json
61            .get("devDependencies")
62            .and_then(|d| d.as_object())
63        {
64            for (name, value) in deps {
65                if let Some(version) = value.as_str() {
66                    dependencies.push(Dependency {
67                        name: name.clone(),
68                        version: version.to_string(),
69                        constraints: Some(version.to_string()),
70                        is_dev: true,
71                    });
72                }
73            }
74        }
75
76        // Parse peer dependencies
77        if let Some(deps) = package_json
78            .get("peerDependencies")
79            .and_then(|d| d.as_object())
80        {
81            for (name, value) in deps {
82                if let Some(version) = value.as_str() {
83                    dependencies.push(Dependency {
84                        name: name.clone(),
85                        version: version.to_string(),
86                        constraints: Some(version.to_string()),
87                        is_dev: false,
88                    });
89                }
90            }
91        }
92
93        Ok(dependencies)
94    }
95
96    /// Checks if package.json exists
97    pub fn has_manifest(&self, root: &Path) -> bool {
98        root.join("package.json").exists()
99    }
100}
101
102impl Default for NodeJsParser {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::fs;
112    use tempfile::TempDir;
113
114    #[test]
115    fn test_nodejs_parser_creation() {
116        let parser = NodeJsParser::new();
117        assert!(true);
118    }
119
120    #[test]
121    fn test_nodejs_parser_no_manifest() {
122        let parser = NodeJsParser::new();
123        let temp_dir = TempDir::new().unwrap();
124        let result = parser.parse(temp_dir.path()).unwrap();
125        assert!(result.is_empty());
126    }
127
128    #[test]
129    fn test_nodejs_parser_simple_dependencies() {
130        let parser = NodeJsParser::new();
131        let temp_dir = TempDir::new().unwrap();
132
133        let package_json = r#"{
134  "name": "test",
135  "version": "1.0.0",
136  "dependencies": {
137    "express": "^4.18.0",
138    "react": "^18.0.0"
139  },
140  "devDependencies": {
141    "jest": "^29.0.0"
142  }
143}"#;
144
145        fs::write(temp_dir.path().join("package.json"), package_json).unwrap();
146
147        let deps = parser.parse(temp_dir.path()).unwrap();
148        assert_eq!(deps.len(), 3);
149
150        // Check express
151        let express = deps.iter().find(|d| d.name == "express").unwrap();
152        assert_eq!(express.version, "^4.18.0");
153        assert!(!express.is_dev);
154
155        // Check jest
156        let jest = deps.iter().find(|d| d.name == "jest").unwrap();
157        assert_eq!(jest.version, "^29.0.0");
158        assert!(jest.is_dev);
159    }
160
161    #[test]
162    fn test_nodejs_parser_has_manifest() {
163        let parser = NodeJsParser::new();
164        let temp_dir = TempDir::new().unwrap();
165
166        assert!(!parser.has_manifest(temp_dir.path()));
167
168        fs::write(temp_dir.path().join("package.json"), "{}").unwrap();
169        assert!(parser.has_manifest(temp_dir.path()));
170    }
171}