Skip to main content

provenant/parsers/
swift_show_dependencies.rs

1//! Parser for Swift show-dependencies deplock files.
2//!
3//! Extracts dependency information from `swift-show-dependencies.deplock` files
4//! created by the dependency-inspector tool.
5//!
6//! # Supported Formats
7//! - `swift-show-dependencies.deplock` - Swift dependency graph JSON
8//!
9//! # Implementation Notes
10//! - Format: JSON with nested dependency tree
11//! - Generated by deplock tool or `swift package show-dependencies`
12//! - Extracts full dependency graph with versions (beyond Python which only extracts name)
13//! - Spec: https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154
14
15use std::fs;
16use std::path::Path;
17
18use crate::parser_warn as warn;
19use serde::{Deserialize, Serialize};
20
21use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
22
23use super::PackageParser;
24
25const PACKAGE_TYPE: PackageType = PackageType::Swift;
26
27fn default_package_data() -> PackageData {
28    PackageData {
29        package_type: Some(PACKAGE_TYPE),
30        primary_language: Some("Swift".to_string()),
31        datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
32        ..Default::default()
33    }
34}
35
36pub struct SwiftShowDependenciesParser;
37
38#[derive(Debug, Deserialize, Serialize)]
39struct SwiftDeplock {
40    name: Option<String>,
41    version: Option<String>,
42    url: Option<String>,
43    #[serde(default)]
44    dependencies: Vec<SwiftDependency>,
45}
46
47#[derive(Debug, Deserialize, Serialize, Clone)]
48struct SwiftDependency {
49    identity: Option<String>,
50    name: Option<String>,
51    version: Option<String>,
52    url: Option<String>,
53    #[serde(default)]
54    dependencies: Vec<SwiftDependency>,
55}
56
57impl PackageParser for SwiftShowDependenciesParser {
58    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
59
60    fn is_match(path: &Path) -> bool {
61        path.file_name()
62            .and_then(|name| name.to_str())
63            .is_some_and(|name| name == "swift-show-dependencies.deplock")
64    }
65
66    fn extract_packages(path: &Path) -> Vec<PackageData> {
67        let content = match fs::read_to_string(path) {
68            Ok(c) => c,
69            Err(e) => {
70                warn!(
71                    "Failed to read swift-show-dependencies.deplock {:?}: {}",
72                    path, e
73                );
74                return vec![default_package_data()];
75            }
76        };
77
78        vec![parse_swift_show_dependencies(&content)]
79    }
80}
81
82pub(crate) fn parse_swift_show_dependencies(content: &str) -> PackageData {
83    let data: SwiftDeplock = match serde_json::from_str(content) {
84        Ok(d) => d,
85        Err(e) => {
86            warn!("Failed to parse swift-show-dependencies.deplock: {}", e);
87            return default_package_data();
88        }
89    };
90
91    let dependencies = flatten_dependencies(&data.dependencies);
92    let version = normalize_version(data.version);
93    let homepage_url = normalize_remote_url(data.url);
94    let purl = create_root_purl(data.name.as_deref(), version.as_deref());
95
96    PackageData {
97        package_type: Some(PACKAGE_TYPE),
98        primary_language: Some("Swift".to_string()),
99        name: data.name,
100        version,
101        homepage_url,
102        dependencies,
103        datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
104        purl,
105        ..Default::default()
106    }
107}
108
109fn flatten_dependencies(deps: &[SwiftDependency]) -> Vec<Dependency> {
110    let mut result = Vec::new();
111    for dep in deps {
112        flatten_dependency(dep, true, &mut result);
113    }
114
115    result
116}
117
118fn flatten_dependency(dep: &SwiftDependency, is_direct: bool, result: &mut Vec<Dependency>) {
119    if let Some(dependency) = build_dependency(dep, is_direct) {
120        result.push(dependency);
121    }
122
123    for child in &dep.dependencies {
124        flatten_dependency(child, false, result);
125    }
126}
127
128fn build_dependency(dep: &SwiftDependency, is_direct: bool) -> Option<Dependency> {
129    let name = dep.name.as_ref()?.clone();
130    let version = normalize_version(dep.version.clone());
131    let purl = create_dependency_purl(dep, &name, version.as_deref());
132    let nested_dependencies = dep
133        .dependencies
134        .iter()
135        .filter_map(|child| build_dependency(child, true))
136        .collect();
137
138    Some(Dependency {
139        purl: Some(purl.clone()),
140        extracted_requirement: version.clone(),
141        scope: Some("dependencies".to_string()),
142        is_runtime: None,
143        is_optional: Some(false),
144        is_pinned: Some(version.is_some()),
145        is_direct: Some(is_direct),
146        resolved_package: Some(Box::new(ResolvedPackage {
147            primary_language: Some("Swift".to_string()),
148            download_url: None,
149            sha1: None,
150            sha256: None,
151            sha512: None,
152            md5: None,
153            is_virtual: true,
154            extra_data: None,
155            dependencies: nested_dependencies,
156            repository_homepage_url: None,
157            repository_download_url: None,
158            api_data_url: None,
159            datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
160            purl: None,
161            ..ResolvedPackage::new(
162                PACKAGE_TYPE,
163                extract_namespace(dep.url.as_deref()).unwrap_or_default(),
164                name,
165                version.clone().unwrap_or_default(),
166            )
167        })),
168        extra_data: None,
169    })
170}
171
172fn create_dependency_purl(
173    dep: &SwiftDependency,
174    fallback_name: &str,
175    version: Option<&str>,
176) -> String {
177    if let Some(url) = dep.url.as_deref()
178        && let Some((namespace, name)) = parse_url_namespace_and_name(url)
179    {
180        let mut purl = format!("pkg:swift/{}/{}", namespace, name);
181        if let Some(version) = version {
182            purl.push('@');
183            purl.push_str(version);
184        }
185        return purl;
186    }
187
188    let mut purl = format!("pkg:swift/{}", fallback_name);
189    if let Some(version) = version {
190        purl.push('@');
191        purl.push_str(version);
192    }
193    purl
194}
195
196fn create_root_purl(name: Option<&str>, version: Option<&str>) -> Option<String> {
197    let name = name?.trim();
198    if name.is_empty() {
199        return None;
200    }
201
202    let mut purl = format!("pkg:swift/{}", name);
203    if let Some(version) = version {
204        purl.push('@');
205        purl.push_str(version);
206    }
207    Some(purl)
208}
209
210fn normalize_version(version: Option<String>) -> Option<String> {
211    version.and_then(|v| {
212        let trimmed = v.trim();
213        if trimmed.is_empty() || trimmed == "unspecified" {
214            None
215        } else {
216            Some(trimmed.to_string())
217        }
218    })
219}
220
221fn normalize_remote_url(url: Option<String>) -> Option<String> {
222    url.and_then(|value| {
223        let trimmed = value.trim();
224        if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
225            Some(trimmed.to_string())
226        } else {
227            None
228        }
229    })
230}
231
232fn extract_namespace(url: Option<&str>) -> Option<String> {
233    parse_url_namespace_and_name(url?).map(|(namespace, _)| namespace)
234}
235
236fn parse_url_namespace_and_name(url: &str) -> Option<(String, String)> {
237    let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
238    let without_scheme = trimmed
239        .strip_prefix("https://")
240        .or_else(|| trimmed.strip_prefix("http://"))?;
241    let mut parts = without_scheme.split('/');
242    let host = parts.next()?;
243    let owner = parts.next()?;
244    let repo = parts.next()?;
245
246    Some((format!("{}/{}", host, owner), repo.to_string()))
247}
248
249crate::register_parser!(
250    "Swift show-dependencies deplock file",
251    &["*swift-show-dependencies.deplock"],
252    "swift",
253    "Swift",
254    Some(
255        "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154"
256    ),
257);