ricecoder_research/dependency_analyzer/
kotlin_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct KotlinParser;
11
12impl KotlinParser {
13 pub fn new() -> Self {
15 KotlinParser
16 }
17
18 pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20 let mut dependencies = Vec::new();
21
22 let gradle_kts_path = root.join("build.gradle.kts");
24 if gradle_kts_path.exists() {
25 debug!("Parsing Kotlin dependencies from {:?}", gradle_kts_path);
26 if let Ok(mut deps) = self.parse_gradle_kts(&gradle_kts_path) {
27 dependencies.append(&mut deps);
28 }
29 }
30
31 let pom_path = root.join("pom.xml");
33 if pom_path.exists() {
34 debug!("Parsing Kotlin dependencies from {:?}", pom_path);
35 if let Ok(mut deps) = self.parse_pom(&pom_path) {
36 dependencies.append(&mut deps);
37 }
38 }
39
40 Ok(dependencies)
41 }
42
43 fn parse_gradle_kts(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
45 let content =
46 std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
47 language: "Kotlin".to_string(),
48 path: Some(path.to_path_buf()),
49 reason: format!("Failed to read build.gradle.kts: {}", e),
50 })?;
51
52 let mut dependencies = Vec::new();
53
54 let dep_pattern = regex::Regex::new(
57 r#"(?:implementation|testImplementation|api|testApi|compileOnly|testCompileOnly)\s*\(\s*["\']([^:]+):([^:]+):([^"\']+)["\']\s*\)"#
58 ).unwrap();
59
60 for cap in dep_pattern.captures_iter(&content) {
61 let group_id = cap.get(1).map(|m| m.as_str()).unwrap_or("");
62 let artifact_id = cap.get(2).map(|m| m.as_str()).unwrap_or("");
63 let version = cap.get(3).map(|m| m.as_str()).unwrap_or("");
64
65 let name = format!("{}:{}", group_id, artifact_id);
66
67 dependencies.push(Dependency {
68 name,
69 version: version.to_string(),
70 constraints: Some(version.to_string()),
71 is_dev: false,
72 });
73 }
74
75 Ok(dependencies)
76 }
77
78 fn parse_pom(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
80 let content =
81 std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
82 language: "Kotlin".to_string(),
83 path: Some(path.to_path_buf()),
84 reason: format!("Failed to read pom.xml: {}", e),
85 })?;
86
87 let mut dependencies = Vec::new();
88
89 let dep_pattern = regex::Regex::new(
91 r"<dependency>\s*<groupId>([^<]+)</groupId>\s*<artifactId>([^<]+)</artifactId>\s*<version>([^<]+)</version>(?:\s*<scope>([^<]+)</scope>)?"
92 ).unwrap();
93
94 for cap in dep_pattern.captures_iter(&content) {
95 let group_id = cap.get(1).map(|m| m.as_str()).unwrap_or("");
96 let artifact_id = cap.get(2).map(|m| m.as_str()).unwrap_or("");
97 let version = cap.get(3).map(|m| m.as_str()).unwrap_or("");
98 let scope = cap.get(4).map(|m| m.as_str()).unwrap_or("compile");
99
100 let name = format!("{}:{}", group_id, artifact_id);
101 let is_dev = scope == "test";
102
103 dependencies.push(Dependency {
104 name,
105 version: version.to_string(),
106 constraints: Some(version.to_string()),
107 is_dev,
108 });
109 }
110
111 Ok(dependencies)
112 }
113
114 pub fn has_manifest(&self, root: &Path) -> bool {
116 root.join("build.gradle.kts").exists() || root.join("pom.xml").exists()
117 }
118}
119
120impl Default for KotlinParser {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::fs;
130 use tempfile::TempDir;
131
132 #[test]
133 fn test_kotlin_parser_creation() {
134 let parser = KotlinParser::new();
135 assert!(true);
136 }
137
138 #[test]
139 fn test_kotlin_parser_no_manifest() {
140 let parser = KotlinParser::new();
141 let temp_dir = TempDir::new().unwrap();
142 let result = parser.parse(temp_dir.path()).unwrap();
143 assert!(result.is_empty());
144 }
145
146 #[test]
147 fn test_kotlin_parser_gradle_kts() {
148 let parser = KotlinParser::new();
149 let temp_dir = TempDir::new().unwrap();
150
151 let gradle_kts = r#"
152dependencies {
153 implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0")
154 testImplementation("junit:junit:4.13.2")
155}
156"#;
157
158 fs::write(temp_dir.path().join("build.gradle.kts"), gradle_kts).unwrap();
159
160 let deps = parser.parse(temp_dir.path()).unwrap();
161 assert_eq!(deps.len(), 2);
162 }
163
164 #[test]
165 fn test_kotlin_parser_has_manifest() {
166 let parser = KotlinParser::new();
167 let temp_dir = TempDir::new().unwrap();
168
169 assert!(!parser.has_manifest(temp_dir.path()));
170
171 fs::write(temp_dir.path().join("build.gradle.kts"), "").unwrap();
172 assert!(parser.has_manifest(temp_dir.path()));
173 }
174}