use crate::snippets::error::Result;
use crate::snippets::scratch::ScratchDir;
use crate::snippets::session::ValidationSession;
use crate::snippets::types::{Language, Snippet, SnippetStatus, ValidationLevel};
use crate::snippets::validators::{SnippetValidator, run_command};
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct SwiftModuleLookup {
package_root: std::path::PathBuf,
environment: Vec<(String, String)>,
}
#[derive(Default)]
pub struct SwiftValidator {
module_directories: Mutex<HashMap<SwiftModuleLookup, std::result::Result<Vec<std::path::PathBuf>, String>>>,
}
impl SnippetValidator for SwiftValidator {
fn language(&self) -> Language {
Language::Swift
}
fn is_available(&self) -> bool {
which::which("swiftc").is_ok()
}
fn validate(
&self,
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
let dir = ScratchDir::isolated()?;
let file = dir.path().join("snippet.swift");
std::fs::write(&file, snippet.code.trim())?;
let mut command = std::process::Command::new("swiftc");
match level {
ValidationLevel::Syntax => {
command.args(["-parse"]).arg(&file);
}
ValidationLevel::Compile => {
let out = dir.path().join("snippet");
command.args(["-o"]).arg(&out).arg(&file);
}
ValidationLevel::TypeCheck => {
command.args(["-typecheck", "-warnings-as-errors"]).arg(&file);
}
ValidationLevel::Run => {
let out = dir.path().join("snippet");
command.args(["-o"]).arg(&out).arg(&file);
}
}
let (success, output) = run_command(&mut command, timeout_secs)?;
if success {
Ok((SnippetStatus::Pass, None))
} else {
Ok((SnippetStatus::Fail, Some(output)))
}
}
fn max_level(&self) -> ValidationLevel {
ValidationLevel::TypeCheck
}
fn validate_in_session(
&self,
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
session: Option<&ValidationSession>,
) -> Result<(SnippetStatus, Option<String>)> {
let Some(session) = session else {
return self.validate(snippet, level, timeout_secs);
};
let dir = session.scratch_dir()?;
let file = dir.path().join("snippet.swift");
std::fs::write(&file, snippet.code.trim())?;
let module_directories = self.module_directories(session)?;
let mut command = std::process::Command::new("swiftc");
match level {
ValidationLevel::Syntax => {
command.arg("-parse");
}
ValidationLevel::TypeCheck => {
command.args(["-typecheck", "-warnings-as-errors"]);
}
ValidationLevel::Compile => {
command.arg("-typecheck");
}
ValidationLevel::Run => {
command.arg("-o").arg(dir.path().join("snippet"));
}
}
for directory in &module_directories {
command.arg("-I").arg(directory);
}
if let Some(binary_directory) = module_directories.first().and_then(|path| path.parent()) {
command.arg("-L").arg(binary_directory);
}
command.arg(&file);
session.apply(&mut command);
let (success, output) = run_command(&mut command, timeout_secs)?;
Ok(if success {
(SnippetStatus::Pass, None)
} else {
(SnippetStatus::Fail, Some(output))
})
}
fn is_dependency_error(&self, output: &str) -> bool {
output.contains("no such module")
}
}
impl SwiftValidator {
fn module_directories(&self, session: &ValidationSession) -> Result<Vec<std::path::PathBuf>> {
self.cached_module_directories(session, || swift_module_directories(session))
}
fn cached_module_directories(
&self,
session: &ValidationSession,
resolve: impl FnOnce() -> Result<Vec<std::path::PathBuf>>,
) -> Result<Vec<std::path::PathBuf>> {
let lookup = swift_module_lookup(session);
let mut cache = self.module_directories.lock().map_err(|error| {
crate::snippets::error::Error::Other(format!("locking Swift module-directory cache: {error}"))
})?;
if let Some(cached) = cache.get(&lookup) {
return cached.clone().map_err(crate::snippets::error::Error::Other);
}
let resolved = resolve().map_err(|error| error.to_string());
cache.insert(lookup, resolved.clone());
resolved.map_err(crate::snippets::error::Error::Other)
}
}
fn swift_module_lookup(session: &ValidationSession) -> SwiftModuleLookup {
let package_root = session
.manifest
.as_deref()
.and_then(std::path::Path::parent)
.unwrap_or(&session.working_directory)
.to_path_buf();
SwiftModuleLookup {
package_root,
environment: session
.env
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect(),
}
}
fn swift_module_directories(session: &ValidationSession) -> Result<Vec<std::path::PathBuf>> {
let mut command = std::process::Command::new("swift");
command.args(["build", "--show-bin-path"]);
session.apply(&mut command);
if let Some(package_root) = session.manifest.as_deref().and_then(std::path::Path::parent) {
command.current_dir(package_root);
}
let output = command.output()?;
if !output.status.success() {
return Err(crate::snippets::error::Error::Other(
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
));
}
let binary_directory = std::path::PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
swift_module_directories_in(&binary_directory)
}
fn swift_module_directories_in(binary_directory: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
let mut directories = vec![binary_directory.join("Modules")];
let Ok(entries) = std::fs::read_dir(binary_directory) else {
return Ok(directories);
};
for entry in entries {
let path = entry?.path();
if path.join("module.modulemap").is_file() || path.join("include/module.modulemap").is_file() {
directories.push(path);
}
}
Ok(directories)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn batching_is_declined_because_one_module_cannot_hold_two_top_level_snippets() {
let first = swift_snippet("print(\"one\")\n");
let second = swift_snippet("print(\"two\")\n");
let validator = SwiftValidator::default();
assert!(!validator.supports_batching());
for level in [
ValidationLevel::Syntax,
ValidationLevel::Compile,
ValidationLevel::TypeCheck,
ValidationLevel::Run,
] {
let declined = validator.validate_batch_in_session(&[&first, &second], level, 10, None);
assert!(
declined.is_none(),
"{level:?} must fall back to one process per snippet"
);
}
}
#[test]
fn a_session_resolves_its_swiftpm_module_directories_once() {
let validator = SwiftValidator::default();
let first_session = ValidationSession {
language: Language::Swift,
working_directory: PathBuf::from("package"),
manifest: Some(PathBuf::from("package/Package.swift")),
fingerprint: "first-target-identity".into(),
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
};
let mut second_session = first_session.clone();
second_session.fingerprint = "second-target-identity".into();
let invocations = AtomicUsize::new(0);
let expected = vec![PathBuf::from(".build/debug/Modules")];
for session in [&first_session, &second_session] {
let resolved = validator
.cached_module_directories(session, || {
invocations.fetch_add(1, Ordering::SeqCst);
Ok(expected.clone())
})
.expect("module directories resolve");
assert_eq!(resolved, expected);
}
assert_eq!(invocations.load(Ordering::SeqCst), 1);
}
fn swift_snippet(code: &str) -> crate::snippets::types::Snippet {
crate::snippets::types::Snippet {
id: None,
path: std::path::PathBuf::from("snippet.swift"),
language: Language::Swift,
title: None,
code: code.into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: crate::snippets::types::SnippetMetadata::default(),
source_origin: crate::snippets::types::SourceOrigin {
path: std::path::PathBuf::from("snippet.swift"),
line: 1,
block_index: 0,
},
}
}
#[test]
fn missing_swiftpm_bin_directory_is_not_an_io_error() {
let directory = tempfile::tempdir().expect("temp directory");
let missing = directory.path().join("not-built");
assert_eq!(
swift_module_directories_in(&missing).expect("missing bin directory is tolerated"),
vec![missing.join("Modules")]
);
}
}