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 tempfile::TempDir;

pub struct CsharpValidator;

impl CsharpValidator {
    fn validate_with_context(
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
        session: Option<&ValidationSession>,
    ) -> Result<(SnippetStatus, Option<String>)> {
        let temporary_directory = session.is_none().then(TempDir::new).transpose()?;
        let directory = match (session, temporary_directory.as_ref()) {
            (Some(value), _) => value.workspace_directory()?,
            (None, Some(value)) => value.path().to_path_buf(),
            (None, None) => unreachable!(),
        };
        let project_path = directory.join("Snippet.csproj");
        let reference = session
            .and_then(|value| value.manifest.as_ref())
            .map_or_else(String::new, |manifest| {
                format!(
                    "  <ItemGroup><ProjectReference Include={:?} /></ItemGroup>\n",
                    manifest.to_string_lossy()
                )
            });
        let project = format!(
            "<Project Sdk=\"Microsoft.NET.Sdk\"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup>{reference}</Project>\n"
        );
        std::fs::write(&project_path, project)?;
        std::fs::write(directory.join("Program.cs"), Self::wrap_if_fragment(&snippet.code))?;
        let mut command = std::process::Command::new("dotnet");
        match level {
            ValidationLevel::Syntax | ValidationLevel::Compile => {
                command.args(["build", "--nologo", "-v", "quiet"]);
            }
            ValidationLevel::TypeCheck => {
                command.args(["build", "--nologo", "-v", "quiet", "-warnaserror"]);
            }
            ValidationLevel::Run => {
                command.args(["run", "--nologo"]);
            }
        }
        command.current_dir(&directory);
        if let Some(session) = session {
            session.apply_environment(&mut command);
        }
        let (success, output) = run_command(&mut command, timeout_secs)?;
        Ok(if success {
            (SnippetStatus::Pass, None)
        } else {
            (SnippetStatus::Fail, Some(output))
        })
    }

    fn wrap_if_fragment(code: &str) -> String {
        let trimmed = code.trim();
        let only_comments = !trimmed.is_empty()
            && trimmed
                .lines()
                .all(|line| line.trim().is_empty() || line.trim().starts_with("//"));
        if only_comments {
            return format!("{trimmed}\n// snippet placeholder\nreturn;\n");
        }
        code.to_string()
    }

    fn is_dependency_error_text(output: &str) -> bool {
        output.contains("CS0246") || output.contains("CS0234") || output.contains("CS0103") || output.contains("CS5001")
    }
}

impl SnippetValidator for CsharpValidator {
    fn language(&self) -> Language {
        Language::Csharp
    }

    fn is_available(&self) -> bool {
        which::which("dotnet").is_ok()
    }

    fn validate(
        &self,
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
    ) -> Result<(SnippetStatus, Option<String>)> {
        Self::validate_with_context(snippet, level, timeout_secs, None)
    }

    fn validate_in_session(
        &self,
        snippet: &Snippet,
        level: ValidationLevel,
        timeout_secs: u64,
        session: Option<&ValidationSession>,
    ) -> Result<(SnippetStatus, Option<String>)> {
        Self::validate_with_context(snippet, level, timeout_secs, session)
    }

    fn max_level(&self) -> ValidationLevel {
        ValidationLevel::Run
    }

    fn is_dependency_error(&self, output: &str) -> bool {
        Self::is_dependency_error_text(output)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::snippets::types::{SnippetMetadata, SourceOrigin};
    use std::collections::BTreeMap;
    use std::path::PathBuf;

    #[test]
    fn session_manifest_adds_a_real_project_reference() {
        if which::which("dotnet").is_err() {
            return;
        }
        let root = tempfile::tempdir().expect("temporary root");
        let project = root.path().join("LocalFixture");
        let working = root.path().join("working");
        std::fs::create_dir_all(&project).expect("project directory");
        std::fs::create_dir_all(&working).expect("working directory");
        std::fs::write(
            project.join("LocalFixture.csproj"),
            "<Project Sdk=\"Microsoft.NET.Sdk\"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup></Project>",
        )
        .expect("project manifest");
        std::fs::write(
            project.join("Value.cs"),
            "namespace LocalFixture; public static class Values { public const int Value = 1; }",
        )
        .expect("project source");
        let session = ValidationSession {
            working_directory: working,
            manifest: Some(project.join("LocalFixture.csproj")),
            fingerprint: "fixture".into(),
            env: BTreeMap::from([("DOTNET_CLI_TELEMETRY_OPTOUT".into(), "1".into())]),
        };

        let (status, output) = CsharpValidator::validate_with_context(
            &snippet("using LocalFixture; System.Console.WriteLine(Values.Value);"),
            ValidationLevel::TypeCheck,
            60,
            Some(&session),
        )
        .expect("validation runs");
        assert_eq!(status, SnippetStatus::Pass, "{output:?}");
    }

    fn snippet(code: &str) -> Snippet {
        Snippet {
            id: None,
            path: PathBuf::from("snippet.cs"),
            language: Language::Csharp,
            title: None,
            code: code.into(),
            start_line: 1,
            block_index: 0,
            annotation: None,
            metadata: SnippetMetadata::default(),
            source_origin: SourceOrigin {
                path: PathBuf::from("snippet.cs"),
                line: 1,
                block_index: 0,
            },
        }
    }
}