ricecoder_research/dependency_analyzer/
dotnet_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct DotNetParser;
11
12impl DotNetParser {
13 pub fn new() -> Self {
15 DotNetParser
16 }
17
18 pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20 let mut dependencies = Vec::new();
21
22 if let Ok(entries) = std::fs::read_dir(root) {
24 for entry in entries.flatten() {
25 let path = entry.path();
26 if path.extension().is_some_and(|ext| ext == "csproj") {
27 debug!("Parsing .NET dependencies from {:?}", path);
28 if let Ok(mut deps) = self.parse_csproj(&path) {
29 dependencies.append(&mut deps);
30 }
31 }
32 }
33 }
34
35 let packages_config_path = root.join("packages.config");
37 if packages_config_path.exists() {
38 debug!("Parsing .NET dependencies from {:?}", packages_config_path);
39 if let Ok(mut deps) = self.parse_packages_config(&packages_config_path) {
40 dependencies.append(&mut deps);
41 }
42 }
43
44 Ok(dependencies)
45 }
46
47 fn parse_csproj(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
49 let content =
50 std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
51 language: ".NET".to_string(),
52 path: Some(path.to_path_buf()),
53 reason: format!("Failed to read .csproj: {}", e),
54 })?;
55
56 let mut dependencies = Vec::new();
57
58 let dep_pattern =
61 regex::Regex::new(r#"<PackageReference\s+Include="([^"]+)"\s+Version="([^"]+)"#)
62 .unwrap();
63
64 for cap in dep_pattern.captures_iter(&content) {
65 let name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
66 let version = cap.get(2).map(|m| m.as_str()).unwrap_or("");
67
68 dependencies.push(Dependency {
69 name: name.to_string(),
70 version: version.to_string(),
71 constraints: Some(version.to_string()),
72 is_dev: false,
73 });
74 }
75
76 Ok(dependencies)
77 }
78
79 fn parse_packages_config(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
81 let content =
82 std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
83 language: ".NET".to_string(),
84 path: Some(path.to_path_buf()),
85 reason: format!("Failed to read packages.config: {}", e),
86 })?;
87
88 let mut dependencies = Vec::new();
89
90 let dep_pattern =
93 regex::Regex::new(r#"<package\s+id="([^"]+)"\s+version="([^"]+)"#).unwrap();
94
95 for cap in dep_pattern.captures_iter(&content) {
96 let name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
97 let version = cap.get(2).map(|m| m.as_str()).unwrap_or("");
98
99 dependencies.push(Dependency {
100 name: name.to_string(),
101 version: version.to_string(),
102 constraints: Some(version.to_string()),
103 is_dev: false,
104 });
105 }
106
107 Ok(dependencies)
108 }
109
110 pub fn has_manifest(&self, root: &Path) -> bool {
112 if let Ok(entries) = std::fs::read_dir(root) {
114 for entry in entries.flatten() {
115 if entry.path().extension().is_some_and(|ext| ext == "csproj") {
116 return true;
117 }
118 }
119 }
120
121 root.join("packages.config").exists()
123 }
124}
125
126impl Default for DotNetParser {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::fs;
136 use tempfile::TempDir;
137
138 #[test]
139 fn test_dotnet_parser_creation() {
140 let parser = DotNetParser::new();
141 assert!(true);
142 }
143
144 #[test]
145 fn test_dotnet_parser_no_manifest() {
146 let parser = DotNetParser::new();
147 let temp_dir = TempDir::new().unwrap();
148 let result = parser.parse(temp_dir.path()).unwrap();
149 assert!(result.is_empty());
150 }
151
152 #[test]
153 fn test_dotnet_parser_csproj() {
154 let parser = DotNetParser::new();
155 let temp_dir = TempDir::new().unwrap();
156
157 let csproj = r#"<?xml version="1.0"?>
158<Project Sdk="Microsoft.NET.Sdk">
159 <ItemGroup>
160 <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
161 <PackageReference Include="System.Net.Http" Version="4.3.4" />
162 </ItemGroup>
163</Project>"#;
164
165 fs::write(temp_dir.path().join("test.csproj"), csproj).unwrap();
166
167 let deps = parser.parse(temp_dir.path()).unwrap();
168 assert_eq!(deps.len(), 2);
169
170 let newtonsoft = deps.iter().find(|d| d.name == "Newtonsoft.Json").unwrap();
171 assert_eq!(newtonsoft.version, "13.0.1");
172 }
173
174 #[test]
175 fn test_dotnet_parser_has_manifest() {
176 let parser = DotNetParser::new();
177 let temp_dir = TempDir::new().unwrap();
178
179 assert!(!parser.has_manifest(temp_dir.path()));
180
181 fs::write(temp_dir.path().join("test.csproj"), "").unwrap();
182 assert!(parser.has_manifest(temp_dir.path()));
183 }
184}