use std::path::Path;
use crate::errors::{Result, TokenSaveError};
use super::common::{Dep, DepKind, Member, Workspace};
const ECOSYSTEM: &str = "swift";
pub fn detect(root: &Path) -> bool {
root.join("Package.swift").exists()
}
pub fn parse(root: &Path) -> Result<Workspace> {
let path = root.join("Package.swift");
let raw = std::fs::read_to_string(&path).map_err(|e| TokenSaveError::Config {
message: format!("failed to read {}: {e}", path.display()),
})?;
let name = extract_package_name(&raw).unwrap_or_else(|| "swift-package".to_string());
let deps = extract_dependencies(&raw);
Ok(Workspace {
ecosystem: ECOSYSTEM,
root: root.to_path_buf(),
members: vec![Member {
path: "Package.swift".to_string(),
name,
license: None,
deps,
}],
patches: Vec::new(),
})
}
fn extract_package_name(raw: &str) -> Option<String> {
let idx = raw.find("name:")?;
let after = &raw[idx + "name:".len()..];
let quote_start = after.find('"')? + 1;
let rest = &after[quote_start..];
let quote_end = rest.find('"')?;
Some(rest[..quote_end].to_string())
}
fn extract_dependencies(raw: &str) -> Vec<Dep> {
let mut out = Vec::new();
let mut search_pos = 0;
while let Some(idx) = raw[search_pos..].find(".package(") {
let start = search_pos + idx + ".package(".len();
let Some(end) = find_matching_paren(&raw[start..]) else {
break;
};
let body = &raw[start..start + end];
if let Some(dep) = parse_package_call(body) {
out.push(dep);
}
search_pos = start + end + 1;
}
out
}
fn parse_package_call(body: &str) -> Option<Dep> {
let url = extract_string_arg(body, "url:")
.or_else(|| extract_string_arg(body, "path:"));
let name_arg = extract_string_arg(body, "name:");
let name = name_arg
.clone()
.or_else(|| url.as_ref().map(|u| extract_repo_name(u)))?;
let version = extract_string_arg(body, "from:")
.or_else(|| extract_string_arg(body, "exact:"))
.or_else(|| extract_string_arg(body, "branch:"))
.or_else(|| extract_string_arg(body, "revision:"))
.or_else(|| {
extract_first_quoted(body).filter(|s| s != url.as_deref().unwrap_or(""))
});
let local_path = if body.contains("path:") {
url.clone()
} else {
None
};
Some(Dep {
name,
resolved: None,
version,
features: Vec::new(),
optional: false,
local_path,
kind: DepKind::Normal,
})
}
fn extract_string_arg(body: &str, key: &str) -> Option<String> {
let idx = body.find(key)?;
let after = &body[idx + key.len()..];
let quote_start = after.find('"')? + 1;
let rest = &after[quote_start..];
let quote_end = rest.find('"')?;
Some(rest[..quote_end].to_string())
}
fn extract_first_quoted(body: &str) -> Option<String> {
let start = body.find('"')? + 1;
let rest = &body[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn extract_repo_name(url: &str) -> String {
let trimmed = url.trim_end_matches('/');
let last = trimmed.rsplit('/').next().unwrap_or(trimmed);
last.strip_suffix(".git").unwrap_or(last).to_string()
}
fn find_matching_paren(body: &str) -> Option<usize> {
let mut depth = 1usize;
for (i, c) in body.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn parses_package_swift_basic() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("Package.swift"),
r#"
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLib",
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-log", .upToNextMajor(from: "1.0.0")),
.package(name: "LocalLib", path: "../LocalLib"),
]
)
"#,
)
.unwrap();
let ws = parse(dir.path()).unwrap();
assert_eq!(ws.ecosystem, "swift");
let m = &ws.members[0];
assert_eq!(m.name, "MyLib");
let nio = m.deps.iter().find(|d| d.name == "swift-nio").unwrap();
assert_eq!(nio.version.as_deref(), Some("2.0.0"));
assert!(m.deps.iter().any(|d| d.name == "swift-log"));
let local = m.deps.iter().find(|d| d.name == "LocalLib").unwrap();
assert!(local.local_path.is_some());
}
#[test]
fn extracts_repo_name_strips_git_suffix() {
assert_eq!(extract_repo_name("https://github.com/foo/bar.git"), "bar");
assert_eq!(extract_repo_name("https://github.com/foo/bar"), "bar");
assert_eq!(extract_repo_name("https://github.com/foo/bar/"), "bar");
}
}