ricecoder_research/dependency_analyzer/
dotnet_parser.rs

1//! .NET dependency parser for .csproj and packages.config
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses .NET dependencies from .csproj and packages.config
9#[derive(Debug)]
10pub struct DotNetParser;
11
12impl DotNetParser {
13    /// Creates a new DotNetParser
14    pub fn new() -> Self {
15        DotNetParser
16    }
17
18    /// Parses dependencies from .csproj or packages.config
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let mut dependencies = Vec::new();
21
22        // Try .csproj files
23        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        // Try packages.config
36        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    /// Parses dependencies from .csproj
48    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        // Look for PackageReference elements
59        // <PackageReference Include="PackageName" Version="1.0.0" />
60        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    /// Parses dependencies from packages.config
80    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        // Look for package elements
91        // <package id="PackageName" version="1.0.0" />
92        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    /// Checks if .NET manifest files exist
111    pub fn has_manifest(&self, root: &Path) -> bool {
112        // Check for .csproj files
113        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        // Check for packages.config
122        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}