ricecoder_research/dependency_analyzer/
php_parser.rs

1//! PHP dependency parser for composer.json
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses PHP dependencies from composer.json
9#[derive(Debug)]
10pub struct PhpParser;
11
12impl PhpParser {
13    /// Creates a new PhpParser
14    pub fn new() -> Self {
15        PhpParser
16    }
17
18    /// Parses dependencies from composer.json
19    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        // Parse require dependencies
46        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        // Parse require-dev dependencies
62        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    /// Checks if composer.json exists
79    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}