use crate::error::{Diagnostic, DiagnosticLevel, Result, SpliceError};
use crate::io_ext;
use crate::symbol::Language as SymbolLanguage;
use crate::validate::{self, AnalyzerMode};
use std::path::{Path, PathBuf};
pub(crate) fn run_validation_gates(
file_path: &Path,
workspace_dir: &Path,
language: SymbolLanguage,
analyzer_mode: AnalyzerMode,
) -> Result<()> {
gate_tree_sitter_reparse(file_path, language)?;
gate_compiler_validation(file_path, workspace_dir, language)?;
if language == SymbolLanguage::Rust {
use crate::validate::gate_rust_analyzer;
gate_rust_analyzer(workspace_dir, analyzer_mode)?;
}
Ok(())
}
pub(crate) fn gate_tree_sitter_reparse(file_path: &Path, language: SymbolLanguage) -> Result<()> {
let source = io_ext::read(file_path)?;
let mut parser = tree_sitter::Parser::new();
let tree_sitter_lang = get_tree_sitter_language(language);
parser
.set_language(&tree_sitter_lang)
.map_err(|e| SpliceError::Parse {
file: file_path.to_path_buf(),
message: format!("Failed to set language: {:?}", e),
})?;
let tree = parser
.parse(&source, None)
.ok_or_else(|| SpliceError::ParseValidationFailed {
file: file_path.to_path_buf(),
message: "Parse failed - no tree returned".to_string(),
})?;
if tree.root_node().has_error() {
return Err(SpliceError::ParseValidationFailed {
file: file_path.to_path_buf(),
message: format!(
"Tree-sitter detected syntax errors in patched {} file",
language.as_str()
),
});
}
Ok(())
}
fn get_tree_sitter_language(language: SymbolLanguage) -> tree_sitter::Language {
match language {
SymbolLanguage::Rust => tree_sitter_rust::LANGUAGE.into(),
SymbolLanguage::Python => tree_sitter_python::LANGUAGE.into(),
SymbolLanguage::C => tree_sitter_c::LANGUAGE.into(),
SymbolLanguage::Cpp => tree_sitter_cpp::LANGUAGE.into(),
SymbolLanguage::Java => tree_sitter_java::LANGUAGE.into(),
SymbolLanguage::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
SymbolLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
}
}
pub(crate) fn gate_compiler_validation(
file_path: &Path,
workspace_dir: &Path,
language: SymbolLanguage,
) -> Result<()> {
match language {
SymbolLanguage::Rust => {
gate_cargo_check(workspace_dir)?;
}
_ => {
use crate::validate::gates::validate_file;
let outcome = validate_file(file_path)?;
let tool_metadata = tool_invocation_for_language(language)
.map(|inv| validate::collect_tool_metadata(inv.binary, inv.version_args));
if !outcome.is_valid {
if !outcome.tool_available {
log::warn!(
"Compiler validation tool not available for {}, skipping validation",
language.as_str()
);
return Ok(());
}
let mut diagnostics = Vec::new();
let tool_name = format!("{}-compiler", language.as_str());
for err in outcome.errors {
let remediation = err
.code
.as_deref()
.and_then(validate::remediation_link_for_code);
diagnostics.push(
Diagnostic::new(&tool_name, DiagnosticLevel::Error, err.message)
.with_file(file_for_diagnostic(&err.file, file_path))
.with_position(nonzero(err.line), nonzero(err.column))
.with_code(err.code.clone())
.with_note(err.note.clone())
.with_tool_metadata(tool_metadata.as_ref())
.with_remediation(remediation),
);
}
for warn in outcome.warnings {
let remediation = warn
.code
.as_deref()
.and_then(validate::remediation_link_for_code);
diagnostics.push(
Diagnostic::new(&tool_name, DiagnosticLevel::Warning, warn.message)
.with_file(file_for_diagnostic(&warn.file, file_path))
.with_position(nonzero(warn.line), nonzero(warn.column))
.with_code(warn.code.clone())
.with_note(warn.note.clone())
.with_tool_metadata(tool_metadata.as_ref())
.with_remediation(remediation),
);
}
return Err(SpliceError::CompilerValidationFailed {
file: file_path.to_path_buf(),
language: language.as_str().to_string(),
diagnostics,
});
}
}
}
Ok(())
}
fn file_for_diagnostic(reported: &str, fallback: &Path) -> PathBuf {
if reported.is_empty() {
fallback.to_path_buf()
} else {
PathBuf::from(reported)
}
}
fn nonzero(value: usize) -> Option<usize> {
if value == 0 {
None
} else {
Some(value)
}
}
struct ToolInvocation {
binary: &'static str,
version_args: &'static [&'static str],
}
fn tool_invocation_for_language(language: SymbolLanguage) -> Option<ToolInvocation> {
match language {
SymbolLanguage::Python => Some(ToolInvocation {
binary: "python",
version_args: &["--version"],
}),
SymbolLanguage::C => Some(ToolInvocation {
binary: "gcc",
version_args: &["--version"],
}),
SymbolLanguage::Cpp => Some(ToolInvocation {
binary: "g++",
version_args: &["--version"],
}),
SymbolLanguage::Java => Some(ToolInvocation {
binary: "javac",
version_args: &["-version"],
}),
SymbolLanguage::JavaScript => Some(ToolInvocation {
binary: "node",
version_args: &["--version"],
}),
SymbolLanguage::TypeScript => Some(ToolInvocation {
binary: "tsc",
version_args: &["--version"],
}),
_ => None,
}
}
pub(crate) fn gate_cargo_check(workspace_dir: &Path) -> Result<()> {
use std::process::Command;
use std::thread;
use std::time::Duration;
let workspace_path = workspace_dir.to_path_buf();
let thread_workspace = workspace_path.clone();
let (tx, rx) = std::sync::mpsc::channel();
thread::spawn(move || {
let output = Command::new("cargo")
.args(["check", "--color=never"])
.current_dir(&thread_workspace)
.output();
let _ = tx.send(output);
});
let output = match rx.recv_timeout(Duration::from_secs(120)) {
Ok(result) => result.map_err(|source| SpliceError::Io {
path: workspace_path.clone(),
source,
})?,
Err(_) => {
return Err(SpliceError::Other(
"cargo check timed out after 120 seconds".to_string(),
));
}
};
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stderr, stdout);
if output.status.success() {
return Ok(());
}
let compiler_errors = validate::parse_cargo_output(&stderr);
let mut diagnostics = Vec::new();
let cargo_meta = validate::collect_tool_metadata("cargo", &["--version"]);
if compiler_errors.is_empty() {
diagnostics.push(
Diagnostic::new("cargo-check", DiagnosticLevel::Error, combined.clone())
.with_file(workspace_dir.to_path_buf())
.with_tool_metadata(Some(&cargo_meta)),
);
} else {
for err in compiler_errors {
let remediation = err
.code
.as_deref()
.and_then(validate::remediation_link_for_code);
diagnostics.push(
Diagnostic::new("cargo-check", DiagnosticLevel::from(err.level), err.message)
.with_file(PathBuf::from(err.file))
.with_position(nonzero(err.line), nonzero(err.column))
.with_code(err.code.clone())
.with_note(err.note.clone())
.with_tool_metadata(Some(&cargo_meta))
.with_remediation(remediation),
);
}
}
Err(SpliceError::CargoCheckFailed {
workspace: workspace_dir.to_path_buf(),
output: combined,
diagnostics,
})
}