alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
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);
    }
}