use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::config::Settings;
use crate::project_resolver::{
ResolutionResult, Sha256Hash,
helpers::{compute_config_shas, extract_language_config_paths, is_language_enabled},
memo::ResolutionMemo,
persist::{ResolutionPersistence, ResolutionRules},
provider::ProjectResolutionProvider,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SwiftPackagePath(PathBuf);
impl SwiftPackagePath {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn as_path(&self) -> &PathBuf {
&self.0
}
}
pub struct SwiftProvider {
#[allow(dead_code)] memo: ResolutionMemo<HashMap<SwiftPackagePath, Sha256Hash>>,
}
impl Default for SwiftProvider {
fn default() -> Self {
Self::new()
}
}
impl SwiftProvider {
pub fn new() -> Self {
Self {
memo: ResolutionMemo::new(),
}
}
pub fn module_path_for_file(&self, file_path: &Path) -> Option<String> {
let codanna_dir = Path::new(crate::init::local_dir_name());
let persistence = ResolutionPersistence::new(codanna_dir);
let index = persistence.load("swift").ok()?;
let canon_file = file_path.canonicalize().ok()?;
let config_path = index.get_config_for_file(&canon_file)?;
let rules = index.rules.get(config_path)?;
for root_path in rules.paths.keys() {
let root = Path::new(root_path);
let canon_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
if let Ok(relative) = canon_file.strip_prefix(&canon_root) {
let module_path = relative
.parent()? .to_string_lossy()
.replace(['/', '\\'], ".");
return Some(module_path);
}
}
None
}
fn parse_package_swift(&self, package_path: &Path) -> ResolutionResult<Vec<PathBuf>> {
use std::fs;
let content = fs::read_to_string(package_path).map_err(|e| {
crate::project_resolver::ResolutionError::IoError {
path: package_path.to_path_buf(),
cause: e.to_string(),
}
})?;
let mut source_roots = Vec::new();
let project_dir = package_path.parent().unwrap_or(Path::new("."));
let mut found_custom_paths = false;
for line in content.lines() {
if let Some(path_start) = line.find("path:") {
if let Some(quote_start) = line[path_start..].find('"') {
let after_quote = &line[path_start + quote_start + 1..];
if let Some(quote_end) = after_quote.find('"') {
let custom_path = &after_quote[..quote_end];
source_roots.push(project_dir.join(custom_path));
found_custom_paths = true;
}
}
}
}
if !found_custom_paths {
let sources_dir = project_dir.join("Sources");
let tests_dir = project_dir.join("Tests");
if sources_dir.exists() {
source_roots.push(sources_dir);
} else {
source_roots.push(project_dir.to_path_buf());
}
if tests_dir.exists() {
source_roots.push(tests_dir);
}
}
Ok(source_roots)
}
fn build_rules_for_config(&self, config_path: &Path) -> ResolutionResult<ResolutionRules> {
let source_roots = self.parse_package_swift(config_path)?;
let mut paths = HashMap::new();
for root in source_roots {
paths.insert(root.to_string_lossy().to_string(), Vec::new());
}
Ok(ResolutionRules {
base_url: None,
paths,
})
}
}
impl ProjectResolutionProvider for SwiftProvider {
fn language_id(&self) -> &'static str {
"swift"
}
fn is_enabled(&self, settings: &Settings) -> bool {
is_language_enabled(settings, "swift")
}
fn config_paths(&self, settings: &Settings) -> Vec<PathBuf> {
extract_language_config_paths(settings, "swift")
}
fn compute_shas(&self, configs: &[PathBuf]) -> ResolutionResult<HashMap<PathBuf, Sha256Hash>> {
compute_config_shas(configs)
}
fn rebuild_cache(&self, settings: &Settings) -> ResolutionResult<()> {
use crate::project_resolver::persist::ResolutionIndex;
let config_paths = self.config_paths(settings);
if config_paths.is_empty() {
return Ok(());
}
let persistence = ResolutionPersistence::new(Path::new(crate::init::local_dir_name()));
let mut index = ResolutionIndex::new();
for config_path in &config_paths {
if !config_path.exists() {
continue;
}
let rules = self.build_rules_for_config(config_path)?;
let project_dir = config_path.parent().unwrap_or(Path::new("."));
let pattern = format!("{}/**/*.swift", project_dir.display());
index.mappings.insert(pattern, config_path.clone());
index.rules.insert(config_path.clone(), rules);
}
let shas = self.compute_shas(&config_paths)?;
for (path, sha) in shas {
index.hashes.insert(path, sha.0);
}
persistence.save("swift", &index)?;
Ok(())
}
fn select_affected_files(&self, _settings: &Settings) -> Vec<PathBuf> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_package_swift_default_sources() {
let temp_dir = TempDir::new().unwrap();
let package_path = temp_dir.path().join("Package.swift");
let package_content = r#"// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "MyPackage",
targets: [
.target(name: "MyLib"),
.testTarget(name: "MyLibTests", dependencies: ["MyLib"]),
]
)
"#;
fs::write(&package_path, package_content).unwrap();
fs::create_dir_all(temp_dir.path().join("Sources")).unwrap();
fs::create_dir_all(temp_dir.path().join("Tests")).unwrap();
let provider = SwiftProvider::new();
let roots = provider.parse_package_swift(&package_path).unwrap();
assert!(!roots.is_empty(), "Should have at least Sources directory");
assert!(
roots.iter().any(|r| r.ends_with("Sources")),
"Should have Sources directory"
);
}
#[test]
fn test_parse_package_swift_custom_path() {
let temp_dir = TempDir::new().unwrap();
let package_path = temp_dir.path().join("Package.swift");
let package_content = r#"// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "MyPackage",
targets: [
.target(name: "MyLib", path: "CustomSources/MyLib"),
]
)
"#;
fs::write(&package_path, package_content).unwrap();
let provider = SwiftProvider::new();
let roots = provider.parse_package_swift(&package_path).unwrap();
assert!(
roots
.iter()
.any(|r| r.to_string_lossy().contains("CustomSources/MyLib")),
"Should have custom source path"
);
}
#[test]
#[ignore = "Requires filesystem isolation (changes cwd, conflicts with parallel tests)"]
fn test_rebuild_cache_creates_resolution_json() {
let temp_dir = TempDir::new().unwrap();
let package_path = temp_dir.path().join("Package.swift");
let codanna_dir = temp_dir.path().join(crate::init::local_dir_name());
let package_content = r#"// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "MyPackage",
targets: [
.target(name: "MyLib"),
]
)
"#;
fs::write(&package_path, package_content).unwrap();
fs::create_dir_all(temp_dir.path().join("Sources")).unwrap();
let settings_content = format!(
r#"
[languages.swift]
enabled = true
config_files = ["{}"]
"#,
package_path.display()
);
let settings: Settings = toml::from_str(&settings_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
let provider = SwiftProvider::new();
std::env::set_current_dir(&temp_dir).unwrap();
fs::create_dir_all(&codanna_dir).unwrap();
provider.rebuild_cache(&settings).unwrap();
std::env::set_current_dir(&original_dir).unwrap();
let cache_path = codanna_dir.join("index/resolvers/swift_resolution.json");
assert!(
cache_path.exists(),
"Cache file should be created at {}",
cache_path.display()
);
let cache_content = fs::read_to_string(&cache_path).unwrap();
assert!(
cache_content.contains("Sources") || cache_content.contains("sources"),
"Cache should contain source root path"
);
}
#[test]
fn test_provider_language_id() {
let provider = SwiftProvider::new();
assert_eq!(provider.language_id(), "swift");
}
}