use crate::snippets::error::Result;
use crate::snippets::session::ValidationSession;
use crate::snippets::types::{Language, Snippet, SnippetStatus, ValidationLevel};
use crate::snippets::validators::{SnippetValidator, run_command};
use std::io::Write;
use tempfile::NamedTempFile;
pub struct CValidator;
fn compiler() -> Option<String> {
for candidate in ["cc", "clang", "gcc"] {
if which::which(candidate).is_ok() {
return Some(candidate.to_string());
}
}
None
}
impl SnippetValidator for CValidator {
fn language(&self) -> Language {
Language::C
}
fn is_available(&self) -> bool {
compiler().is_some()
}
fn validate(
&self,
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
let Some(cc) = compiler() else {
return Ok((SnippetStatus::Unavailable, Some("no C compiler on PATH".into())));
};
let mut source = NamedTempFile::with_suffix(".c")?;
source.write_all(snippet.code.as_bytes())?;
source.flush()?;
let source_path = source.path().to_string_lossy().to_string();
let mut command = std::process::Command::new(&cc);
match level {
ValidationLevel::Syntax => {
command.args(["-fsyntax-only", &source_path]);
}
ValidationLevel::TypeCheck => {
command.args(["-fsyntax-only", "-Wall", "-Werror", &source_path]);
}
ValidationLevel::Compile | ValidationLevel::Run => {
let out = NamedTempFile::new()?;
let out_path = out.path().to_string_lossy().to_string();
drop(out);
command.args(["-o", &out_path, &source_path]);
let (success, output) = run_command(&mut command, timeout_secs)?;
if !success {
return Ok((SnippetStatus::Fail, Some(output)));
}
if matches!(level, ValidationLevel::Run) {
let mut run = std::process::Command::new(&out_path);
let (ran_ok, run_output) = run_command(&mut run, timeout_secs)?;
let _ = std::fs::remove_file(&out_path);
return Ok(if ran_ok {
(SnippetStatus::Pass, None)
} else {
(SnippetStatus::Fail, Some(run_output))
});
}
let _ = std::fs::remove_file(&out_path);
return Ok((SnippetStatus::Pass, None));
}
}
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::Run
}
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 Some(cc) = compiler() else {
return Ok((SnippetStatus::Unavailable, Some("no C compiler on PATH".into())));
};
let mut source = tempfile::Builder::new()
.suffix(".c")
.tempfile_in(&session.working_directory)?;
source.write_all(snippet.code.as_bytes())?;
source.flush()?;
let output = session.working_directory.join(".alef-snippet-output");
let mut command = std::process::Command::new(cc);
let include_directory = session
.manifest
.as_deref()
.and_then(std::path::Path::parent)
.unwrap_or(&session.working_directory);
command.arg("-I").arg(include_directory);
if level == ValidationLevel::Syntax {
command.arg("-fsyntax-only");
}
if level == ValidationLevel::TypeCheck {
command.args(["-fsyntax-only", "-Wall", "-Werror"]);
}
if level == ValidationLevel::Compile {
command.arg("-c").arg("-o").arg(&output);
} else if level == ValidationLevel::Run {
command.arg("-o").arg(&output);
}
command.arg(source.path());
session.apply(&mut command);
let (success, message) = run_command(&mut command, timeout_secs)?;
if !success {
return Ok((SnippetStatus::Fail, Some(message)));
}
if level != ValidationLevel::Run {
let _ = std::fs::remove_file(&output);
return Ok((SnippetStatus::Pass, None));
}
let mut run = std::process::Command::new(&output);
session.apply(&mut run);
let (success, message) = run_command(&mut run, timeout_secs)?;
let _ = std::fs::remove_file(&output);
Ok(if success {
(SnippetStatus::Pass, None)
} else {
(SnippetStatus::Fail, Some(message))
})
}
fn is_dependency_error(&self, output: &str) -> bool {
output.contains("file not found")
|| output.contains("No such file or directory")
|| output.contains("undeclared identifier")
|| output.contains("implicit declaration")
|| output.contains("unknown type name")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::{Snippet, SnippetMetadata, SourceOrigin};
use std::path::PathBuf;
fn snippet(code: &str) -> Snippet {
Snippet {
id: None,
path: PathBuf::from("test.c"),
language: Language::C,
title: None,
code: code.to_string(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: PathBuf::from("test.c"),
line: 1,
block_index: 0,
},
}
}
#[test]
fn syntax_ok() {
let v = CValidator;
if !v.is_available() {
return;
}
let s = snippet("int main(void) { return 0; }\n");
let (status, _) = v.validate(&s, ValidationLevel::Syntax, 30).unwrap();
assert_eq!(status, SnippetStatus::Pass);
}
#[test]
fn syntax_fail() {
let v = CValidator;
if !v.is_available() {
return;
}
let s = snippet("int main(void) { @@@ }\n");
let (status, _) = v.validate(&s, ValidationLevel::Syntax, 30).unwrap();
assert_eq!(status, SnippetStatus::Fail);
}
}