pub(crate) fn swift_module_name(raw: &str) -> Option<String> {
let rest = raw.strip_prefix("@testable ").unwrap_or(raw).trim_start();
let rest = rest.strip_prefix("import ")?.trim();
const DECLARATION_KINDS: &[&str] = &[
"struct",
"class",
"enum",
"protocol",
"typealias",
"func",
"let",
"var",
"actor",
];
let mut token = rest.split_whitespace().next()?;
if DECLARATION_KINDS.contains(&token) {
token = rest.split_whitespace().nth(1)?;
}
let name = token.split('.').next()?.trim_end_matches(';');
if name.is_empty() {
return None;
}
Some(name.to_string())
}
pub(super) fn swift_module_root(path: &str) -> Option<String> {
if !path.ends_with(".swift") {
return None;
}
let segments: Vec<&str> = path.split('/').collect();
if segments.len() < 3 {
return None;
}
let last_container = segments.len() - 2;
let anchor = segments[..=last_container]
.iter()
.rposition(|seg| *seg == "Sources" || *seg == "Tests")?;
if anchor + 1 > last_container {
return None;
}
Some(segments[..=anchor + 1].join("/"))
}
#[cfg(test)]
pub(in crate::code_index) mod tests {
use std::collections::HashMap;
use super::super::file_table::FileId;
use super::super::module_index::ModuleIndex;
use super::*;
pub(in crate::code_index) fn swift_workspace() -> HashMap<String, FileId> {
let mut paths: HashMap<String, FileId> = HashMap::new();
paths.insert("Sources/Core/Client.swift".to_string(), 1);
paths.insert("Sources/Core/Net/Session.swift".to_string(), 2);
paths.insert("Sources/App/Main.swift".to_string(), 3);
paths.insert("Tests/CoreTests/ClientTests.swift".to_string(), 4);
paths.insert("Package.swift".to_string(), 5);
paths.insert("Sources/Loose.swift".to_string(), 6);
paths
}
#[test]
fn swift_target_membership_needs_no_import_statement() {
let paths = swift_workspace();
let modules = ModuleIndex::build(&paths);
assert_eq!(modules.siblings_of(1), vec![2]);
assert_eq!(modules.siblings_of(2), vec![1]);
assert!(modules.siblings_of(3).is_empty());
}
#[test]
fn swift_targets_do_not_leak_across_sources_and_tests() {
let paths = swift_workspace();
let modules = ModuleIndex::build(&paths);
assert_eq!(modules.files_named("CoreTests"), vec![4]);
assert!(!modules.siblings_of(4).contains(&1));
}
#[test]
fn swift_files_outside_a_target_directory_join_no_module() {
let paths = swift_workspace();
let modules = ModuleIndex::build(&paths);
assert!(modules.siblings_of(5).is_empty());
assert!(modules.siblings_of(6).is_empty());
assert!(modules.files_named("Sources").is_empty());
}
#[test]
fn swift_module_root_reads_the_nearest_target_anchor() {
assert_eq!(
swift_module_root("Sources/Core/Net/Session.swift").as_deref(),
Some("Sources/Core")
);
assert_eq!(
swift_module_root("vendor/Dep/Sources/DepCore/A.swift").as_deref(),
Some("vendor/Dep/Sources/DepCore")
);
assert_eq!(swift_module_root("Package.swift"), None);
assert_eq!(swift_module_root("Sources/Loose.swift"), None);
assert_eq!(swift_module_root("Sources/Core/Client.rs"), None);
}
#[test]
fn swift_import_spellings_all_name_the_same_target() {
assert_eq!(swift_module_name("import Core").as_deref(), Some("Core"));
assert_eq!(
swift_module_name("@testable import Core").as_deref(),
Some("Core")
);
assert_eq!(
swift_module_name("import Core.Net").as_deref(),
Some("Core")
);
assert_eq!(
swift_module_name("import struct Core.Box").as_deref(),
Some("Core")
);
assert_eq!(swift_module_name("import"), None);
assert_eq!(swift_module_name("class Foo {"), None);
}
}