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 GoValidator;
impl GoValidator {
fn validate_with_context(
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
session: Option<&ValidationSession>,
) -> Result<(SnippetStatus, Option<String>)> {
let dir = match session {
Some(session) => tempfile::Builder::new()
.prefix(".alef-snippet-")
.tempdir_in(Self::project_directory(session))?,
None => TempDir::new()?,
};
let file = dir.path().join("snippet.go");
std::fs::write(&file, Self::wrap_if_fragment(&snippet.code))?;
if session.is_none() && level != ValidationLevel::Syntax {
std::fs::write(dir.path().join("go.mod"), "module snippet\n\ngo 1.21\n")?;
}
let mut command = match level {
ValidationLevel::Syntax => {
let mut command = std::process::Command::new("gofmt");
command.args(["-e", "-l"]).arg(&file);
command
}
ValidationLevel::Compile => {
let mut command = std::process::Command::new("go");
command.args(["build", "-o", "/dev/null"]).arg(&file);
command
}
ValidationLevel::TypeCheck => {
let mut command = std::process::Command::new("go");
command.arg("vet").arg(&file);
command
}
ValidationLevel::Run => {
let mut command = std::process::Command::new("go");
command.arg("run").arg(&file);
command
}
};
match session {
Some(value) => {
command.current_dir(Self::project_directory(value));
value.apply_environment(&mut command);
}
None => {
command.current_dir(dir.path());
}
}
let (success, output) = run_command(&mut command, timeout_secs)?;
Ok(if success {
(SnippetStatus::Pass, None)
} else {
(SnippetStatus::Fail, Some(output))
})
}
fn project_directory(session: &ValidationSession) -> &std::path::Path {
session
.manifest
.as_deref()
.and_then(std::path::Path::parent)
.unwrap_or(&session.working_directory)
}
fn wrap_if_fragment(code: &str) -> String {
let trimmed = code.trim();
if trimmed.starts_with("package ") {
return code.to_string();
}
let (imports, body) = Self::split_imports(trimmed);
let body_trimmed = body.trim();
let only_comments = !body_trimmed.is_empty()
&& body_trimmed
.lines()
.all(|line| line.trim().is_empty() || line.trim().starts_with("//"));
if body_trimmed.is_empty() || only_comments {
let imports_block = if imports.trim().is_empty() {
String::new()
} else {
format!("{imports}\n\n")
};
return format!("package main\n\n{imports_block}func main() {{\n{body_trimmed}\n_ = 0\n}}\n");
}
let imports_block = if imports.trim().is_empty() {
String::new()
} else {
format!("{imports}\n\n")
};
format!("package main\n\n{imports_block}func main() {{\n{body}\n}}\n")
}
fn split_imports(code: &str) -> (String, String) {
let mut imports = Vec::new();
let mut body = Vec::new();
let mut lines = code.lines().peekable();
while let Some(line) = lines.peek() {
let trimmed = line.trim();
if trimmed.is_empty() {
imports.push(*line);
lines.next();
continue;
}
if trimmed.starts_with("import (") {
for import_line in lines.by_ref() {
imports.push(import_line);
if import_line.trim() == ")" {
break;
}
}
continue;
}
if let Some(stripped) = trimmed.strip_prefix("import ") {
let stripped = stripped.trim();
if stripped.starts_with('"') || stripped.starts_with('`') {
imports.push(*line);
lines.next();
continue;
}
}
break;
}
for line in lines {
body.push(line);
}
(imports.join("\n"), body.join("\n"))
}
}
impl SnippetValidator for GoValidator {
fn language(&self) -> Language {
Language::Go
}
fn is_available(&self) -> bool {
which::which("go").is_ok() || which::which("gofmt").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 {
output.contains("undefined:") || output.contains("cannot find package") || output.contains("no required module")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::{SnippetMetadata, SourceOrigin};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[test]
fn session_manifest_resolves_a_local_module_outside_the_working_directory() {
if which::which("go").is_err() {
return;
}
let root = tempfile::tempdir().expect("temporary root");
let working = root.path().join("working");
let project = root.path().join("project");
std::fs::create_dir_all(&working).expect("working directory");
std::fs::create_dir_all(project.join("localpkg")).expect("local package directory");
std::fs::write(project.join("go.mod"), "module example.test/local\n\ngo 1.24\n").expect("go manifest");
std::fs::write(project.join("localpkg/value.go"), "package localpkg\nconst Value = 1\n").expect("go package");
let snippet =
snippet("package main\nimport \"example.test/local/localpkg\"\nfunc main() { _ = localpkg.Value }");
let session = ValidationSession {
working_directory: working,
manifest: Some(project.join("go.mod")),
fingerprint: "fixture".into(),
env: BTreeMap::from([(
"GOCACHE".into(),
root.path().join("go-cache").to_string_lossy().into_owned(),
)]),
};
let (status, output) =
GoValidator::validate_with_context(&snippet, ValidationLevel::TypeCheck, 30, Some(&session))
.expect("validation runs");
assert_eq!(status, SnippetStatus::Pass, "{output:?}");
}
fn snippet(code: &str) -> Snippet {
Snippet {
id: None,
path: PathBuf::from("snippet.go"),
language: Language::Go,
title: None,
code: code.into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: PathBuf::from("snippet.go"),
line: 1,
block_index: 0,
},
}
}
}