use crate::error::{Result, SpliceError};
use crate::ingest::detect::{detect_language, Language};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationOutcome {
pub is_valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationError>,
pub tool_available: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationError {
pub file: String,
pub line: usize,
pub column: usize,
pub message: String,
pub code: Option<String>,
pub note: Option<String>,
}
pub fn validate_file(path: &Path) -> Result<ValidationOutcome> {
let language = detect_language(path).ok_or_else(|| {
SpliceError::Other(format!("Cannot detect language for file: {:?}", path))
})?;
match language {
Language::Rust => {
Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
})
}
Language::Python => validate_python(path),
Language::C => validate_c(path),
Language::Cpp => validate_cpp(path),
Language::Java => validate_java(path),
Language::JavaScript => validate_javascript(path),
Language::TypeScript => validate_typescript(path),
}
}
fn validate_python(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let output = Command::new("python")
.args(["-m", "py_compile", path_str])
.output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let errors = parse_python_errors(&stderr, path);
Ok(ValidationOutcome {
is_valid: false,
errors,
warnings: vec![],
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Err(SpliceError::Other(format!("Failed to run python: {}", e)))
}
}
}
fn validate_c(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let output = Command::new("gcc")
.args(["-fsyntax-only", "-c", path_str])
.output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let (errors, warnings) = parse_gcc_output(&stderr);
Ok(ValidationOutcome {
is_valid: errors.is_empty(),
errors,
warnings,
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Err(SpliceError::Other(format!("Failed to run gcc: {}", e)))
}
}
}
fn validate_cpp(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let output = Command::new("g++")
.args(["-fsyntax-only", "-c", path_str])
.output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let (errors, warnings) = parse_gcc_output(&stderr);
Ok(ValidationOutcome {
is_valid: errors.is_empty(),
errors,
warnings,
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Err(SpliceError::Other(format!("Failed to run g++: {}", e)))
}
}
}
fn validate_java(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let output = Command::new("javac").args([path_str]).output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let errors = parse_javac_errors(&stderr, path);
Ok(ValidationOutcome {
is_valid: false,
errors,
warnings: vec![],
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Err(SpliceError::Other(format!("Failed to run javac: {}", e)))
}
}
}
fn validate_javascript(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let output = Command::new("node").args(["--check", path_str]).output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let errors = parse_node_errors(&stderr, path);
Ok(ValidationOutcome {
is_valid: false,
errors,
warnings: vec![],
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
})
}
}
}
fn validate_typescript(path: &Path) -> Result<ValidationOutcome> {
let path_str = path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 path: {}", path.display())))?;
let parent_dir = path.parent().map(|p| p.as_ref()).unwrap_or(Path::new("."));
let output = Command::new("tsc")
.args(["--noEmit", path_str])
.current_dir(parent_dir)
.output();
match output {
Ok(result) => {
if result.status.success() {
return Ok(ValidationOutcome {
is_valid: true,
errors: vec![],
warnings: vec![],
tool_available: true,
});
}
let stderr = String::from_utf8_lossy(&result.stderr);
let stdout = String::from_utf8_lossy(&result.stdout);
let errors = parse_tsc_errors(&stderr, &stdout, path);
Ok(ValidationOutcome {
is_valid: errors.is_empty(),
errors,
warnings: vec![],
tool_available: true,
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
});
}
Err(SpliceError::Other(format!("Failed to run tsc: {}", e)))
}
}
}
fn parse_python_errors(output: &str, file: &Path) -> Vec<ValidationError> {
let mut errors = Vec::new();
let lines: Vec<&str> = output.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.contains("File \"") && line.contains(", line ") {
if let Some(line_end) = line.rfind(", line ") {
let after_line = &line[line_end + 7..];
let line_str = after_line.trim().trim_end_matches('"');
if let Ok(line_num) = line_str.parse::<usize>() {
let mut message = String::new();
for line in lines.iter().take(lines.len().min(i + 5)).skip(i + 1) {
if line.contains("SyntaxError:") {
if let Some(msg_start) = line.find("SyntaxError: ") {
message = line[msg_start + 12..].trim().to_string();
}
break;
}
}
if !message.is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: line_num,
column: 0,
message,
code: None,
note: None,
});
}
}
}
}
if line.contains("SyntaxError:") && errors.is_empty() {
if let Some(msg_start) = line.find("SyntaxError: ") {
let message = line[msg_start + 12..].trim().to_string();
errors.push(ValidationError {
file: file.display().to_string(),
line: 0,
column: 0,
message,
code: None,
note: None,
});
}
}
i += 1;
}
if errors.is_empty() && !output.trim().is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: 0,
column: 0,
message: output.trim().to_string(),
code: None,
note: None,
});
}
errors
}
fn parse_gcc_output(output: &str) -> (Vec<ValidationError>, Vec<ValidationError>) {
let mut errors = Vec::new();
let mut warnings = Vec::new();
for line in output.lines() {
if line.contains(": error: ") {
if let Some(error) = parse_gcc_line(line) {
errors.push(error);
}
}
else if line.contains(": warning: ") {
if let Some(warning) = parse_gcc_line(line) {
warnings.push(warning);
}
}
}
(errors, warnings)
}
fn parse_gcc_line(line: &str) -> Option<ValidationError> {
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() >= 4 {
let file = parts[0].trim();
let line_num = parts[1].trim().parse::<usize>().ok()?;
let column = parts[2].trim().parse::<usize>().ok()?;
let rest = parts[3..].join(":");
let message = rest.trim().to_string();
return Some(ValidationError {
file: file.to_string(),
line: line_num,
column,
message,
code: None,
note: None,
});
}
None
}
fn parse_javac_errors(output: &str, file: &Path) -> Vec<ValidationError> {
let mut errors = Vec::new();
for line in output.lines() {
if line.contains(": error: ") {
if let Some(colon_idx) = line.find(':') {
let after_file = &line[colon_idx + 1..];
if let Some(second_colon) = after_file.find(':') {
let line_str = &after_file[..second_colon];
if let Ok(line_num) = line_str.trim().parse::<usize>() {
let after_line = &after_file[second_colon + 1..];
if let Some(error_idx) = after_line.find("error: ") {
let message = after_line[error_idx + 7..].trim().to_string();
errors.push(ValidationError {
file: file.display().to_string(),
line: line_num,
column: 0,
message,
code: None,
note: None,
});
}
}
}
}
}
}
if errors.is_empty() && !output.is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: 0,
column: 0,
message: output.trim().to_string(),
code: None,
note: None,
});
}
errors
}
fn parse_node_errors(output: &str, file: &Path) -> Vec<ValidationError> {
let mut errors = Vec::new();
for line in output.lines() {
if line.contains(file.to_str().unwrap_or("")) {
if let Some(first_colon) = line.find(':') {
let after_file = &line[first_colon + 1..];
if let Some(space_idx) = after_file.find(' ') {
let line_str = &after_file[..space_idx];
if let Ok(line_num) = line_str.parse::<usize>() {
let after_line = &after_file[space_idx + 1..];
let (column, message) = if after_line.starts_with('(') {
if let Some(close_paren) = after_line.find(')') {
let col_str = &after_line[1..close_paren];
let col = col_str.parse::<usize>().unwrap_or(0);
let msg = &after_line[close_paren + 1..];
(col, msg.trim())
} else {
(0, after_line.trim())
}
} else {
(0, after_line.trim())
};
errors.push(ValidationError {
file: file.display().to_string(),
line: line_num,
column,
message: message.to_string(),
code: None,
note: None,
});
}
}
}
}
}
if errors.is_empty() && !output.is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: 0,
column: 0,
message: output.trim().to_string(),
code: None,
note: None,
});
}
errors
}
fn parse_tsc_errors(stderr: &str, stdout: &str, file: &Path) -> Vec<ValidationError> {
let mut errors = Vec::new();
let combined = format!("{}\n{}", stderr, stdout);
for line in combined.lines() {
if line.contains(file.to_str().unwrap_or(""))
&& (line.contains(": error ") || line.contains("TS"))
{
if let Some(open_paren) = line.find('(') {
let after_paren = &line[open_paren + 1..];
if let Some(comma) = after_paren.find(',') {
let line_str = &after_paren[..comma];
if let Ok(line_num) = line_str.trim().parse::<usize>() {
let after_comma = &after_paren[comma + 1..];
if let Some(close_paren) = after_comma.find(')') {
let col_str = &after_comma[..close_paren];
if let Ok(column) = col_str.trim().parse::<usize>() {
let after_close = &line[open_paren + close_paren + 2..];
let (code, message) = extract_ts_error(after_close);
if !message.is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: line_num,
column,
message,
code,
note: None,
});
}
}
}
}
}
}
}
}
if errors.is_empty() && !combined.trim().is_empty() {
errors.push(ValidationError {
file: file.display().to_string(),
line: 0,
column: 0,
message: combined.trim().to_string(),
code: None,
note: None,
});
}
errors
}
fn extract_ts_error(segment: &str) -> (Option<String>, String) {
if let Some(ts_idx) = segment.find("TS") {
if let Some(colon_idx) = segment[ts_idx..].find(':') {
let code = segment[ts_idx..ts_idx + colon_idx].trim().to_string();
let message = segment[ts_idx + colon_idx + 1..].trim().to_string();
return (Some(code), message);
}
}
if let Some(error_idx) = segment.find("error ") {
let message = segment[error_idx + 6..].trim().to_string();
return (None, message);
}
(None, segment.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_python_syntax_error() {
let output = " File \"test.py\", line 1\n SyntaxError: invalid syntax\n";
let path = Path::new("test.py");
let errors = parse_python_errors(output, path);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 1);
assert_eq!(errors[0].message, "invalid syntax");
}
#[test]
fn test_parse_gcc_error() {
let output = "test.c:3:5: error: expected ';' before '}'\n";
let (errors, _warnings) = parse_gcc_output(output);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 3);
assert_eq!(errors[0].column, 5);
assert!(errors[0].message.contains("expected ';'"));
}
#[test]
fn test_parse_gcc_warning() {
let output = "test.c:5:10: warning: unused variable 'x'\n";
let (_errors, warnings) = parse_gcc_output(output);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].line, 5);
assert_eq!(warnings[0].column, 10);
}
#[test]
fn test_parse_javac_error() {
let output = "Main.java:3: error: class, interface, or enum expected\n";
let path = Path::new("Main.java");
let errors = parse_javac_errors(output, path);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 3);
assert!(errors[0]
.message
.contains("class, interface, or enum expected"));
}
#[test]
fn test_parse_node_error() {
let output = "test.js:2 (5) SyntaxError: Unexpected token\n";
let path = Path::new("test.js");
let errors = parse_node_errors(output, path);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 2);
assert_eq!(errors[0].column, 5);
}
#[test]
fn test_validation_outcome_has_tool_available() {
let outcome = ValidationOutcome {
is_valid: false,
errors: vec![],
warnings: vec![],
tool_available: false,
};
assert!(!outcome.tool_available);
}
#[test]
fn test_parse_tsc_error() {
let output = "test.ts(2,5): error TS1002: Unterminated string literal\n";
let path = Path::new("test.ts");
let errors = parse_tsc_errors(output, "", path);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 2);
assert_eq!(errors[0].column, 5);
assert!(errors[0].message.contains("Unterminated string literal"));
}
#[test]
fn test_parse_tsc_error_with_stderr() {
let stderr = "";
let stdout = "test.ts(1,1): error TS2304: Cannot find name 'foo'\n";
let path = Path::new("test.ts");
let errors = parse_tsc_errors(stderr, stdout, path);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].line, 1);
assert_eq!(errors[0].column, 1);
assert!(errors[0].message.contains("Cannot find name"));
}
}