use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use tree_sitter::{Language, Query};
use super::{Adapter, Import, Target, captured_nodes, parse, relative};
const QUERY: &str = "(import_declaration) @node";
const KINDS: &[&str] = &[
"typealias",
"struct",
"class",
"enum",
"protocol",
"let",
"var",
"func",
];
pub struct Swift {
language: Language,
query: Query,
modules: BTreeMap<String, PathBuf>,
}
impl Swift {
pub fn new(root: &Path) -> Result<Self> {
let language: Language = tree_sitter_swift::LANGUAGE.into();
let query = Query::new(&language, QUERY)?;
let mut modules = BTreeMap::new();
for entry in crate::scan::walker(root).build().flatten() {
if entry.file_name() != "Package.swift" {
continue;
}
let Some(package) =
relative(root, entry.path()).and_then(|rel| rel.parent().map(Path::to_path_buf))
else {
continue;
};
let Ok(sources) = std::fs::read_dir(root.join(&package).join("Sources")) else {
continue;
};
for dir in sources.flatten().filter(|d| d.path().is_dir()) {
let name = dir.file_name().to_string_lossy().to_string();
modules
.entry(name.clone())
.or_insert_with(|| package.join("Sources").join(name));
}
}
Ok(Self {
language,
query,
modules,
})
}
}
fn specifier(declaration: &str) -> String {
let words: Vec<&str> = declaration
.split_whitespace()
.skip_while(|w| *w != "import")
.skip(1)
.collect();
let name = match words.first() {
Some(first) if KINDS.contains(first) => words.get(1),
other => other,
};
name.map(|n| n.trim_end_matches(';').to_string())
.unwrap_or_default()
}
impl Adapter for Swift {
fn name(&self) -> &'static str {
"swift"
}
fn handles(&self, path: &Path) -> bool {
path.extension().is_some_and(|e| e == "swift")
}
fn imports(&self, path: &Path, source: &str) -> Result<Vec<Import>> {
let tree = parse(&self.language, path, source)?;
Ok(captured_nodes(&self.query, &tree, source)
.into_iter()
.map(|node| Import {
specifier: specifier(node.utf8_text(source.as_bytes()).unwrap_or_default()),
line: node.start_position().row + 1,
})
.collect())
}
fn resolve(&self, _file: &Path, specifier: &str) -> Target {
let module = specifier.split('.').next().unwrap_or(specifier);
match self.modules.get(module) {
Some(dir) => Target::Internal(dir.clone()),
None => Target::External(specifier.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_every_import_form() {
let swift = Swift::new(Path::new("/nonexistent")).unwrap();
let src = "import Foundation\nimport struct Acme.Models.User\n@testable import Core\nclass A {}\n";
let got: Vec<_> = swift
.imports(Path::new("A.swift"), src)
.unwrap()
.into_iter()
.map(|i| (i.specifier, i.line))
.collect();
assert_eq!(
got,
[
("Foundation".to_string(), 1),
("Acme.Models.User".to_string(), 2),
("Core".to_string(), 3),
]
);
}
#[test]
fn resolves_modules_of_the_package() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
for path in [
"Package.swift",
"Sources/Core/User.swift",
"Sources/App/main.swift",
"Packages/Net/Package.swift",
"Packages/Net/Sources/Net/Client.swift",
] {
let path = root.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "").unwrap();
}
let swift = Swift::new(root).unwrap();
let go = |spec: &str| swift.resolve(Path::new("A.swift"), spec);
assert_eq!(go("Core"), Target::Internal("Sources/Core".into()));
assert_eq!(go("Core.User"), Target::Internal("Sources/Core".into()));
assert_eq!(
go("Net"),
Target::Internal("Packages/Net/Sources/Net".into())
);
assert_eq!(go("Foundation"), Target::External("Foundation".into()));
}
}