ricecoder_research/dependency_analyzer/
nodejs_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct NodeJsParser;
11
12impl NodeJsParser {
13 pub fn new() -> Self {
15 NodeJsParser
16 }
17
18 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 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 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 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 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 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 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}