ricecoder_research/dependency_analyzer/
swift_parser.rs

1//! Swift dependency parser for Package.swift
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Swift dependencies from Package.swift
9#[derive(Debug)]
10pub struct SwiftParser;
11
12impl SwiftParser {
13    /// Creates a new SwiftParser
14    pub fn new() -> Self {
15        SwiftParser
16    }
17
18    /// Parses dependencies from Package.swift
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let package_swift_path = root.join("Package.swift");
21
22        if !package_swift_path.exists() {
23            return Ok(Vec::new());
24        }
25
26        debug!("Parsing Swift dependencies from {:?}", package_swift_path);
27
28        let content = std::fs::read_to_string(&package_swift_path).map_err(|e| {
29            ResearchError::DependencyParsingFailed {
30                language: "Swift".to_string(),
31                path: Some(package_swift_path.clone()),
32                reason: format!("Failed to read Package.swift: {}", e),
33            }
34        })?;
35
36        let mut dependencies = Vec::new();
37
38        // Parse .package declarations
39        // Pattern: .package(url: "...", from: "1.0.0")
40        //          .package(url: "...", .upToNextMajor(from: "1.0.0"))
41        let package_pattern = regex::Regex::new(
42            r#"\.package\(url:\s*"([^"]+)"[^)]*(?:from|\.upToNextMajor|\.upToNextMinor):\s*"([^"]+)"[^)]*\)"#
43        ).unwrap();
44
45        for cap in package_pattern.captures_iter(&content) {
46            let url = cap.get(1).map(|m| m.as_str()).unwrap_or("");
47            let version = cap.get(2).map(|m| m.as_str()).unwrap_or("");
48
49            // Extract package name from URL
50            let name = if let Some(last_slash) = url.rfind('/') {
51                let name_with_ext = &url[last_slash + 1..];
52                if let Some(stripped) = name_with_ext.strip_suffix(".git") {
53                    stripped
54                } else {
55                    name_with_ext
56                }
57            } else {
58                url
59            };
60
61            dependencies.push(Dependency {
62                name: name.to_string(),
63                version: version.to_string(),
64                constraints: Some(version.to_string()),
65                is_dev: false,
66            });
67        }
68
69        Ok(dependencies)
70    }
71
72    /// Checks if Package.swift exists
73    pub fn has_manifest(&self, root: &Path) -> bool {
74        root.join("Package.swift").exists()
75    }
76}
77
78impl Default for SwiftParser {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::fs;
88    use tempfile::TempDir;
89
90    #[test]
91    fn test_swift_parser_creation() {
92        let parser = SwiftParser::new();
93        assert!(true);
94    }
95
96    #[test]
97    fn test_swift_parser_no_manifest() {
98        let parser = SwiftParser::new();
99        let temp_dir = TempDir::new().unwrap();
100        let result = parser.parse(temp_dir.path()).unwrap();
101        assert!(result.is_empty());
102    }
103
104    #[test]
105    fn test_swift_parser_simple_dependencies() {
106        let parser = SwiftParser::new();
107        let temp_dir = TempDir::new().unwrap();
108
109        let package_swift = r#"
110// swift-tools-version:5.5
111import PackageDescription
112
113let package = Package(
114    name: "MyPackage",
115    dependencies: [
116        .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
117        .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0"))
118    ]
119)
120"#;
121
122        fs::write(temp_dir.path().join("Package.swift"), package_swift).unwrap();
123
124        let deps = parser.parse(temp_dir.path()).unwrap();
125        assert_eq!(deps.len(), 2);
126
127        let nio = deps.iter().find(|d| d.name.contains("nio")).unwrap();
128        assert_eq!(nio.version, "2.0.0");
129    }
130
131    #[test]
132    fn test_swift_parser_has_manifest() {
133        let parser = SwiftParser::new();
134        let temp_dir = TempDir::new().unwrap();
135
136        assert!(!parser.has_manifest(temp_dir.path()));
137
138        fs::write(temp_dir.path().join("Package.swift"), "").unwrap();
139        assert!(parser.has_manifest(temp_dir.path()));
140    }
141}