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::TempDir;
pub struct RustValidator;
impl RustValidator {
fn validate_with_context(
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
session: Option<&ValidationSession>,
) -> Result<(SnippetStatus, Option<String>)> {
let dir = match session {
Some(session) => session.temp_dir()?,
None => TempDir::new()?,
};
let source_dir = dir.path().join("src");
std::fs::create_dir_all(&source_dir)?;
std::fs::write(dir.path().join("Cargo.toml"), Self::cargo_manifest(session)?)?;
let code = Self::wrap_if_fragment(&snippet.code);
let mut source_file = std::fs::File::create(source_dir.join("main.rs"))?;
source_file.write_all(code.as_bytes())?;
let args: &[&str] = match level {
ValidationLevel::Syntax | ValidationLevel::Compile | ValidationLevel::TypeCheck => &["check", "--quiet"],
ValidationLevel::Run => &["run", "--quiet"],
};
let mut command = std::process::Command::new("cargo");
command.args(args).current_dir(dir.path());
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 cargo_manifest(session: Option<&ValidationSession>) -> Result<String> {
let dependency = session.map(Self::path_dependency).transpose()?.unwrap_or_default();
Ok(format!(
"[workspace]\n\n[package]\nname = \"snippet-check\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{dependency}"
))
}
fn path_dependency(session: &ValidationSession) -> Result<String> {
let manifest = session
.manifest
.clone()
.unwrap_or_else(|| session.working_directory.join("Cargo.toml"));
let content = std::fs::read_to_string(&manifest)?;
let value: toml::Value = toml::from_str(&content).map_err(|error| {
crate::snippets::error::Error::Other(format!("parsing {}: {error}", manifest.display()))
})?;
let package = value
.get("package")
.and_then(|value| value.get("name"))
.and_then(toml::Value::as_str)
.ok_or_else(|| {
crate::snippets::error::Error::Other(format!("no package.name in {}", manifest.display()))
})?;
let crate_name = package.replace('-', "_");
Ok(format!(
"{crate_name} = {{ package = {package:?}, path = {:?} }}\n",
manifest
.parent()
.unwrap_or(&session.working_directory)
.to_string_lossy()
))
}
fn is_bare_signature(code: &str) -> bool {
let trimmed = code.trim();
trimmed.contains("fn ") && !trimmed.contains('{')
}
fn has_use_then_statements(code: &str) -> bool {
let trimmed = code.trim();
if !trimmed.starts_with("use ") {
return false;
}
for line in trimmed.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with("use ") {
continue;
}
if line.starts_with("let ")
|| line.starts_with("println!")
|| line.starts_with("eprintln!")
|| line.starts_with("assert")
|| line.starts_with("if ")
|| line.starts_with("for ")
|| line.starts_with("while ")
|| line.starts_with("match ")
|| line.starts_with("loop ")
|| line.starts_with("tokio::")
|| line.starts_with("std::")
|| line.starts_with("//")
{
return true;
}
return false;
}
false
}
fn split_uses(code: &str) -> (String, String) {
let mut uses = Vec::new();
let mut body = Vec::new();
let mut past_uses = false;
for line in code.lines() {
let trimmed = line.trim();
if !past_uses && (trimmed.starts_with("use ") || trimmed.is_empty()) {
uses.push(line);
} else {
past_uses = true;
body.push(line);
}
}
(uses.join("\n"), body.join("\n"))
}
fn wrap_if_fragment(code: &str) -> String {
let trimmed = code.trim();
if trimmed.contains("fn main()") {
return code.to_string();
}
if Self::is_bare_signature(trimmed) {
return format!("{code}\n\nfn main() {{}}");
}
if Self::has_use_then_statements(code) {
let (uses, body) = Self::split_uses(code);
return format!("{uses}\n\nfn main() {{\n{body}\n}}");
}
let has_top_level_items = trimmed.starts_with("use ")
|| trimmed.starts_with("fn ")
|| trimmed.starts_with("pub ")
|| trimmed.starts_with("struct ")
|| trimmed.starts_with("enum ")
|| trimmed.starts_with("impl ")
|| trimmed.starts_with("mod ")
|| trimmed.starts_with("trait ")
|| trimmed.starts_with("const ")
|| trimmed.starts_with("static ")
|| trimmed.starts_with("type ")
|| trimmed.starts_with("#[")
|| trimmed.starts_with("extern ")
|| trimmed.starts_with("unsafe ");
if has_top_level_items {
format!("{code}\n\nfn main() {{}}")
} else {
format!("fn main() {{\n{code}\n}}")
}
}
}
impl SnippetValidator for RustValidator {
fn language(&self) -> Language {
Language::Rust
}
fn is_available(&self) -> bool {
which::which("cargo").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 {
let patterns = [
"E0432", "E0433", "E0412", "E0405", "E0425", "E0463", "E0277", "E0599", "E0752", "E0308", "E0107", "E0609",
"E0061", "E0574", "E0583", "E0282", "E0728", "E0423",
];
let error_lines: Vec<&str> = output
.lines()
.filter(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("error")
|| trimmed.contains("aborting due to")
|| trimmed.starts_with("Some errors have")
|| trimmed.starts_with("For more information")
})
.collect();
if error_lines.is_empty() {
return false;
}
error_lines.iter().any(|line| {
patterns.iter().any(|pattern| line.contains(pattern))
|| line.contains("unresolved import")
|| line.contains("cannot find")
|| line.contains("not found in")
|| line.contains("could not compile")
|| line.contains("derive macro")
|| line.contains("proc-macro")
|| line.contains("main function not found")
|| line.contains("functions are not allowed in")
|| line.contains("expected one of")
|| line.contains("expected parameter name")
|| line.contains("not allowed to be `async`")
|| line.contains("expected item, found")
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::{SnippetMetadata, SourceOrigin};
use std::collections::BTreeMap;
#[test]
fn session_manifest_links_the_configured_local_crate() {
if which::which("cargo").is_err() {
return;
}
let project = tempfile::tempdir().expect("project directory");
std::fs::create_dir_all(project.path().join("src")).expect("source directory");
let manifest = project.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"sample-binding\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.expect("package manifest");
std::fs::write(project.path().join("src/lib.rs"), "pub const VALUE: usize = 1;\n").expect("package source");
let session = ValidationSession {
working_directory: project.path().to_path_buf(),
manifest: Some(manifest),
fingerprint: "neutral-project".into(),
env: BTreeMap::new(),
};
let snippet = Snippet {
id: None,
path: "snippet.rs".into(),
language: Language::Rust,
title: None,
code: "fn main() { assert_eq!(sample_binding::VALUE, 1); }".into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: "snippet.rs".into(),
line: 1,
block_index: 0,
},
};
let (status, output) =
RustValidator::validate_with_context(&snippet, ValidationLevel::TypeCheck, 30, Some(&session))
.expect("validation runs");
assert_eq!(status, SnippetStatus::Pass, "{output:?}");
}
}