pub mod bash;
pub mod c;
pub mod csharp;
pub mod dart;
pub mod documentation;
pub mod elixir;
pub mod go;
pub mod java;
pub mod json_validator;
pub mod kotlin;
pub mod php;
pub mod python;
pub mod r;
pub mod ruby;
pub mod rust;
pub mod swift;
pub mod toml_validator;
pub mod typescript;
pub mod yaml_validator;
pub mod zig;
use crate::snippets::error::Result;
use crate::snippets::scratch::ScratchDir;
use crate::snippets::session::ValidationSession;
use crate::snippets::types::{Language, Snippet, SnippetStatus, ValidationLevel};
use std::collections::HashMap;
use std::io::Write;
mod process;
mod termination;
pub use process::run_command;
#[cfg(test)]
#[path = "dependency_error_classification_tests.rs"]
mod dependency_error_classification_tests;
#[cfg(test)]
pub(crate) fn jvm_toolchain_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub type SnippetValidation = (SnippetStatus, Option<String>);
pub type BatchValidation = Vec<SnippetValidation>;
pub trait SnippetValidator: Send + Sync {
fn language(&self) -> Language;
fn is_available(&self) -> bool;
fn is_available_at(&self, _level: ValidationLevel) -> bool {
self.is_available()
}
fn validate(&self, snippet: &Snippet, level: ValidationLevel, timeout_secs: u64) -> Result<SnippetValidation>;
fn validate_in_session(
&self,
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
session: Option<&ValidationSession>,
) -> Result<SnippetValidation> {
if session.is_some() {
return Err(crate::snippets::error::Error::Other(format!(
"{} validator does not support binding-aware sessions",
self.language()
)));
}
self.validate(snippet, level, timeout_secs)
}
fn validate_batch_in_session(
&self,
_snippets: &[&Snippet],
_level: ValidationLevel,
_timeout_secs: u64,
_session: Option<&ValidationSession>,
) -> Option<Result<BatchValidation>> {
None
}
fn max_level(&self) -> ValidationLevel;
fn achievable_level(&self, _requested: ValidationLevel) -> ValidationLevel {
ValidationLevel::Run
}
fn achievable_level_is_structural(&self, _requested: ValidationLevel) -> bool {
false
}
fn is_dependency_error(&self, _error_output: &str) -> bool {
false
}
fn requires_session_exclusivity(&self) -> bool {
false
}
fn supports_batching(&self) -> bool {
false
}
}
pub struct ValidatorRegistry {
validators: HashMap<Language, Box<dyn SnippetValidator>>,
}
impl ValidatorRegistry {
#[must_use]
pub fn new() -> Self {
let mut registry = Self {
validators: HashMap::new(),
};
registry.register(Box::new(rust::RustValidator));
registry.register(Box::new(python::PythonValidator));
registry.register(Box::new(typescript::TypeScriptValidator));
registry.register(Box::new(php::PhpValidator));
registry.register(Box::new(ruby::RubyValidator));
registry.register(Box::new(elixir::ElixirValidator));
registry.register(Box::new(bash::BashValidator));
registry.register(Box::new(toml_validator::TomlValidator));
registry.register(Box::new(c::CValidator));
registry.register(Box::new(csharp::CsharpValidator));
registry.register(Box::new(dart::DartValidator));
registry.register(Box::new(go::GoValidator));
registry.register(Box::new(java::JavaValidator));
registry.register(Box::new(kotlin::KotlinValidator));
registry.register(Box::new(swift::SwiftValidator::default()));
registry.register(Box::new(zig::ZigValidator));
registry.register(Box::new(json_validator::JsonValidator));
registry.register(Box::new(yaml_validator::YamlValidator));
registry.register(Box::new(r::RValidator));
registry.register(Box::new(documentation::TextValidator));
registry.register(Box::new(documentation::MermaidValidator));
registry.register(Box::new(documentation::PowerShellValidator));
registry.register(Box::new(documentation::XmlValidator));
registry.register(Box::new(documentation::DockerValidator));
registry
}
pub(crate) fn register(&mut self, validator: Box<dyn SnippetValidator>) {
self.validators.insert(validator.language(), validator);
}
#[must_use]
pub fn get(&self, language: Language) -> Option<&dyn SnippetValidator> {
self.validators.get(&language).map(Box::as_ref)
}
#[must_use]
pub fn languages(&self) -> Vec<Language> {
let mut languages: Vec<Language> = self.validators.keys().copied().collect();
languages.sort_unstable();
languages
}
}
impl Default for ValidatorRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn run_script(
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
session: Option<&ValidationSession>,
suffix: &str,
program: &str,
syntax_arguments: &[&str],
) -> Result<(SnippetStatus, Option<String>)> {
let scratch_dir = session.map(ScratchDir::for_session).transpose()?;
let mut source = match &scratch_dir {
Some(dir) => tempfile::Builder::new().suffix(suffix).tempfile_in(dir.path())?,
None => tempfile::Builder::new().suffix(suffix).tempfile()?,
};
source.write_all(snippet.code.as_bytes())?;
source.flush()?;
let mut command = std::process::Command::new(program);
if level == ValidationLevel::Run {
command.arg(source.path());
} else {
command.args(syntax_arguments).arg(source.path());
}
if let Some(value) = session {
value.apply(&mut command);
command.env("RUBYLIB", &value.working_directory);
command.env("R_LIBS_USER", &value.working_directory);
}
let (success, output) = run_command(&mut command, timeout_secs)?;
Ok(if success {
(SnippetStatus::Pass, None)
} else {
(SnippetStatus::Fail, Some(output))
})
}
#[cfg(all(test, unix))]
mod tests {
fn script_session(working_directory: std::path::PathBuf) -> crate::snippets::session::ValidationSession {
crate::snippets::session::ValidationSession {
language: crate::snippets::types::Language::Bash,
working_directory,
manifest: None,
fingerprint: "run-script-scratch-fixture".into(),
env: std::collections::BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: std::collections::BTreeMap::new(),
}
}
fn script_snippet(code: &str) -> crate::snippets::types::Snippet {
crate::snippets::types::Snippet {
id: None,
path: "example.md".into(),
language: crate::snippets::types::Language::Bash,
title: None,
code: code.to_string(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: crate::snippets::types::SnippetMetadata::default(),
source_origin: crate::snippets::types::SourceOrigin {
path: "example.md".into(),
line: 1,
block_index: 0,
},
}
}
#[test]
fn run_script_resolves_scratch_under_the_cache_root_not_directly_in_working_directory() {
let working = tempfile::tempdir().expect("working directory");
let session = script_session(working.path().to_path_buf());
let snippet = script_snippet("true\n");
let (status, _) = super::run_script(
&snippet,
super::ValidationLevel::Syntax,
5,
Some(&session),
".sh",
"true",
&[],
)
.expect("run_script runs");
assert_eq!(status, super::SnippetStatus::Pass);
let top_level_entries: Vec<_> = std::fs::read_dir(working.path())
.expect("read working directory")
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name())
.filter(|name| name != ".alef")
.collect();
assert!(
top_level_entries.is_empty(),
"run_script must not leave any scratch entry directly in working_directory: {top_level_entries:?}"
);
}
#[test]
fn run_script_removes_scratch_after_a_run_that_fails() {
let working = tempfile::tempdir().expect("working directory");
let session = script_session(working.path().to_path_buf());
let snippet = script_snippet("false\n");
let (status, _) = super::run_script(
&snippet,
super::ValidationLevel::Syntax,
5,
Some(&session),
".sh",
"false",
&[],
)
.expect("run_script runs");
assert_eq!(status, super::SnippetStatus::Fail);
let scratch_root = working.path().join(".alef/snippets/tmp");
let remaining = std::fs::read_dir(&scratch_root)
.map(|entries| entries.filter_map(|entry| entry.ok()).count())
.unwrap_or(0);
assert_eq!(
remaining, 0,
"scratch left behind under the cache root after a failing snippet validation"
);
}
#[test]
fn run_script_removes_scratch_when_the_run_returns_an_error() {
let working = tempfile::tempdir().expect("working directory");
let session = script_session(working.path().to_path_buf());
let snippet = script_snippet("true\n");
let error = super::run_script(
&snippet,
super::ValidationLevel::Syntax,
5,
Some(&session),
".sh",
"alef-nonexistent-toolchain-for-scratch-test",
&[],
)
.expect_err("a missing toolchain must surface as an error, not a status");
assert!(matches!(error, crate::snippets::error::Error::Other(_)));
let scratch_root = session.scratch_root();
let remaining = std::fs::read_dir(&scratch_root)
.map(|entries| entries.flatten().count())
.unwrap_or(0);
assert_eq!(
remaining,
0,
"scratch left behind under {} after run_script returned an error",
scratch_root.display()
);
}
}