ricecoder_research/dependency_analyzer/
java_parser.rs

1//! Java dependency parser for pom.xml and build.gradle
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Java dependencies from pom.xml and build.gradle
9#[derive(Debug)]
10pub struct JavaParser;
11
12impl JavaParser {
13    /// Creates a new JavaParser
14    pub fn new() -> Self {
15        JavaParser
16    }
17
18    /// Parses dependencies from pom.xml or build.gradle
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let mut dependencies = Vec::new();
21
22        // Try pom.xml first
23        let pom_path = root.join("pom.xml");
24        if pom_path.exists() {
25            debug!("Parsing Java dependencies from {:?}", pom_path);
26            if let Ok(mut deps) = self.parse_pom(&pom_path) {
27                dependencies.append(&mut deps);
28            }
29        }
30
31        // Try build.gradle
32        let gradle_path = root.join("build.gradle");
33        if gradle_path.exists() {
34            debug!("Parsing Java dependencies from {:?}", gradle_path);
35            if let Ok(mut deps) = self.parse_gradle(&gradle_path) {
36                dependencies.append(&mut deps);
37            }
38        }
39
40        Ok(dependencies)
41    }
42
43    /// Parses dependencies from pom.xml
44    fn parse_pom(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
45        let content =
46            std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
47                language: "Java".to_string(),
48                path: Some(path.to_path_buf()),
49                reason: format!("Failed to read pom.xml: {}", e),
50            })?;
51
52        let mut dependencies = Vec::new();
53
54        // Simple regex-based parsing for dependencies
55        // Look for <dependency> blocks
56        let dep_pattern = regex::Regex::new(
57            r"<dependency>\s*<groupId>([^<]+)</groupId>\s*<artifactId>([^<]+)</artifactId>\s*<version>([^<]+)</version>(?:\s*<scope>([^<]+)</scope>)?"
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            let scope = cap.get(4).map(|m| m.as_str()).unwrap_or("compile");
65
66            let name = format!("{}:{}", group_id, artifact_id);
67            let is_dev = scope == "test";
68
69            dependencies.push(Dependency {
70                name,
71                version: version.to_string(),
72                constraints: Some(version.to_string()),
73                is_dev,
74            });
75        }
76
77        Ok(dependencies)
78    }
79
80    /// Parses dependencies from build.gradle
81    fn parse_gradle(&self, path: &Path) -> Result<Vec<Dependency>, ResearchError> {
82        let content =
83            std::fs::read_to_string(path).map_err(|e| ResearchError::DependencyParsingFailed {
84                language: "Java".to_string(),
85                path: Some(path.to_path_buf()),
86                reason: format!("Failed to read build.gradle: {}", e),
87            })?;
88
89        let mut dependencies = Vec::new();
90
91        // Parse dependencies block
92        // Look for patterns like: implementation 'group:artifact:version'
93        let dep_pattern = regex::Regex::new(
94            r#"(?:implementation|testImplementation|api|testApi|compileOnly|testCompileOnly)\s+['"]([^:]+):([^:]+):([^'"]+)['"]"#
95        ).unwrap();
96
97        for cap in dep_pattern.captures_iter(&content) {
98            let group_id = cap.get(1).map(|m| m.as_str()).unwrap_or("");
99            let artifact_id = cap.get(2).map(|m| m.as_str()).unwrap_or("");
100            let version = cap.get(3).map(|m| m.as_str()).unwrap_or("");
101
102            let name = format!("{}:{}", group_id, artifact_id);
103
104            dependencies.push(Dependency {
105                name,
106                version: version.to_string(),
107                constraints: Some(version.to_string()),
108                is_dev: false,
109            });
110        }
111
112        Ok(dependencies)
113    }
114
115    /// Checks if Java manifest files exist
116    pub fn has_manifest(&self, root: &Path) -> bool {
117        root.join("pom.xml").exists() || root.join("build.gradle").exists()
118    }
119}
120
121impl Default for JavaParser {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use std::fs;
131    use tempfile::TempDir;
132
133    #[test]
134    fn test_java_parser_creation() {
135        let parser = JavaParser::new();
136        assert!(true);
137    }
138
139    #[test]
140    fn test_java_parser_no_manifest() {
141        let parser = JavaParser::new();
142        let temp_dir = TempDir::new().unwrap();
143        let result = parser.parse(temp_dir.path()).unwrap();
144        assert!(result.is_empty());
145    }
146
147    #[test]
148    fn test_java_parser_pom_xml() {
149        let parser = JavaParser::new();
150        let temp_dir = TempDir::new().unwrap();
151
152        let pom = r#"<?xml version="1.0"?>
153<project>
154    <dependencies>
155        <dependency>
156            <groupId>junit</groupId>
157            <artifactId>junit</artifactId>
158            <version>4.13.2</version>
159            <scope>test</scope>
160        </dependency>
161        <dependency>
162            <groupId>org.springframework</groupId>
163            <artifactId>spring-core</artifactId>
164            <version>5.3.0</version>
165        </dependency>
166    </dependencies>
167</project>"#;
168
169        fs::write(temp_dir.path().join("pom.xml"), pom).unwrap();
170
171        let deps = parser.parse(temp_dir.path()).unwrap();
172        assert_eq!(deps.len(), 2);
173
174        let junit = deps.iter().find(|d| d.name.contains("junit")).unwrap();
175        assert!(junit.is_dev);
176    }
177
178    #[test]
179    fn test_java_parser_has_manifest() {
180        let parser = JavaParser::new();
181        let temp_dir = TempDir::new().unwrap();
182
183        assert!(!parser.has_manifest(temp_dir.path()));
184
185        fs::write(temp_dir.path().join("pom.xml"), "").unwrap();
186        assert!(parser.has_manifest(temp_dir.path()));
187    }
188}