ricecoder_research/dependency_analyzer/
kotlin_parser.rs

1//! Kotlin dependency parser for build.gradle.kts and pom.xml
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Kotlin dependencies from build.gradle.kts and pom.xml
9#[derive(Debug)]
10pub struct KotlinParser;
11
12impl KotlinParser {
13    /// Creates a new KotlinParser
14    pub fn new() -> Self {
15        KotlinParser
16    }
17
18    /// Parses dependencies from build.gradle.kts or pom.xml
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let mut dependencies = Vec::new();
21
22        // Try build.gradle.kts first
23        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        // Try pom.xml
32        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    /// Parses dependencies from build.gradle.kts
44    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        // Parse dependencies block
55        // Look for patterns like: implementation("group:artifact:version")
56        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    /// Parses dependencies from pom.xml
79    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        // Simple regex-based parsing for dependencies
90        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    /// Checks if Kotlin manifest files exist
115    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}