ricecoder_research/dependency_analyzer/
php_parser.rs1use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct PhpParser;
11
12impl PhpParser {
13 pub fn new() -> Self {
15 PhpParser
16 }
17
18 pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20 let composer_json_path = root.join("composer.json");
21
22 if !composer_json_path.exists() {
23 return Ok(Vec::new());
24 }
25
26 debug!("Parsing PHP dependencies from {:?}", composer_json_path);
27
28 let content = std::fs::read_to_string(&composer_json_path).map_err(|e| {
29 ResearchError::DependencyParsingFailed {
30 language: "PHP".to_string(),
31 path: Some(composer_json_path.clone()),
32 reason: format!("Failed to read composer.json: {}", e),
33 }
34 })?;
35
36 let composer_json: serde_json::Value =
37 serde_json::from_str(&content).map_err(|e| ResearchError::DependencyParsingFailed {
38 language: "PHP".to_string(),
39 path: Some(composer_json_path.clone()),
40 reason: format!("Failed to parse composer.json: {}", e),
41 })?;
42
43 let mut dependencies = Vec::new();
44
45 if let Some(deps) = composer_json.get("require").and_then(|d| d.as_object()) {
47 for (name, value) in deps {
48 if name != "php" {
49 if let Some(version) = value.as_str() {
50 dependencies.push(Dependency {
51 name: name.clone(),
52 version: version.to_string(),
53 constraints: Some(version.to_string()),
54 is_dev: false,
55 });
56 }
57 }
58 }
59 }
60
61 if let Some(deps) = composer_json.get("require-dev").and_then(|d| d.as_object()) {
63 for (name, value) in deps {
64 if let Some(version) = value.as_str() {
65 dependencies.push(Dependency {
66 name: name.clone(),
67 version: version.to_string(),
68 constraints: Some(version.to_string()),
69 is_dev: true,
70 });
71 }
72 }
73 }
74
75 Ok(dependencies)
76 }
77
78 pub fn has_manifest(&self, root: &Path) -> bool {
80 root.join("composer.json").exists()
81 }
82}
83
84impl Default for PhpParser {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::fs;
94 use tempfile::TempDir;
95
96 #[test]
97 fn test_php_parser_creation() {
98 let parser = PhpParser::new();
99 assert!(true);
100 }
101
102 #[test]
103 fn test_php_parser_no_manifest() {
104 let parser = PhpParser::new();
105 let temp_dir = TempDir::new().unwrap();
106 let result = parser.parse(temp_dir.path()).unwrap();
107 assert!(result.is_empty());
108 }
109
110 #[test]
111 fn test_php_parser_simple_dependencies() {
112 let parser = PhpParser::new();
113 let temp_dir = TempDir::new().unwrap();
114
115 let composer_json = r#"{
116 "name": "test/project",
117 "require": {
118 "php": ">=7.4",
119 "laravel/framework": "^9.0",
120 "guzzlehttp/guzzle": "^7.0"
121 },
122 "require-dev": {
123 "phpunit/phpunit": "^9.0"
124 }
125}"#;
126
127 fs::write(temp_dir.path().join("composer.json"), composer_json).unwrap();
128
129 let deps = parser.parse(temp_dir.path()).unwrap();
130 assert_eq!(deps.len(), 3);
131
132 let laravel = deps.iter().find(|d| d.name == "laravel/framework").unwrap();
133 assert_eq!(laravel.version, "^9.0");
134 assert!(!laravel.is_dev);
135
136 let phpunit = deps.iter().find(|d| d.name == "phpunit/phpunit").unwrap();
137 assert!(phpunit.is_dev);
138 }
139
140 #[test]
141 fn test_php_parser_has_manifest() {
142 let parser = PhpParser::new();
143 let temp_dir = TempDir::new().unwrap();
144
145 assert!(!parser.has_manifest(temp_dir.path()));
146
147 fs::write(temp_dir.path().join("composer.json"), "{}").unwrap();
148 assert!(parser.has_manifest(temp_dir.path()));
149 }
150}