use crate::error::Result;
use crate::io_ext;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
pub struct PostVerificationResult {
pub syntax_ok: bool,
pub compiler_ok: bool,
pub semantic_ok: bool,
pub before_checksum: String,
pub after_checksum: String,
pub warnings: Vec<String>,
pub errors: Vec<String>,
}
impl PostVerificationResult {
pub fn new(
syntax_ok: bool,
compiler_ok: bool,
before_checksum: String,
after_checksum: String,
) -> Self {
Self {
syntax_ok,
compiler_ok,
semantic_ok: true, before_checksum,
after_checksum,
warnings: Vec::new(),
errors: Vec::new(),
}
}
pub fn add_warning(&mut self, warning: impl Into<String>) {
self.warnings.push(warning.into());
}
pub fn add_error(&mut self, error: impl Into<String>) {
self.errors.push(error.into());
}
pub fn file_changed(&self) -> bool {
self.before_checksum != self.after_checksum
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChecksumDiff {
pub changed: bool,
pub estimated_delta: i64,
}
pub fn checksum_diff(before_checksum: &str, after_checksum: &str) -> ChecksumDiff {
let changed = before_checksum != after_checksum;
let estimated_delta = 0;
ChecksumDiff {
changed,
estimated_delta,
}
}
pub fn verify_after_patch(
file_path: &Path,
workspace_root: &Path,
expected_before: &str,
analyzer_mode: crate::validate::AnalyzerMode,
) -> Result<PostVerificationResult> {
use crate::checksum::checksum_file;
let mut result = PostVerificationResult::new(
false, false, expected_before.to_string(),
String::new(), );
match checksum_file(file_path) {
Ok(after) => {
result.after_checksum = after.as_hex().to_string();
if !result.file_changed() {
result.add_warning("File checksum unchanged - no modification detected");
}
}
Err(e) => {
result.add_error(format!("Failed to compute after checksum: {}", e));
return Ok(result); }
}
use crate::syntax_validator::validate_syntax;
match std::fs::read(file_path) {
Ok(source) => {
result.syntax_ok = match validate_syntax(file_path, &source) {
Ok(valid) => {
if !valid {
result.add_error(
"Syntax validation failed: tree-sitter detected parse errors"
.to_string(),
);
}
valid
}
Err(e) => {
log::warn!("Syntax validation error: {}", e);
false
}
};
}
Err(e) => {
result.add_error(format!("Failed to read file for syntax validation: {}", e));
result.syntax_ok = false;
}
}
result.compiler_ok = match run_cargo_check(workspace_root) {
Ok(output) => {
if output.status.success() {
log::info!("Cargo check passed");
true
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
result.add_error(format!("Cargo check failed: {}", stderr));
log::warn!("Cargo check failed: {}", stderr);
false
}
}
Err(e) => {
result.add_warning(format!("Unable to run cargo check: {}", e));
log::debug!("Unable to run cargo check: {}", e);
true }
};
if workspace_root.join("Cargo.toml").exists()
&& analyzer_mode != crate::validate::AnalyzerMode::Off
{
match crate::validate::run_rust_analyzer(workspace_root, &analyzer_mode) {
Ok(diagnostics) => {
let (errors, warnings): (Vec<_>, Vec<_>) = diagnostics
.iter()
.partition(|d| d.level == crate::validate::ErrorLevel::Error);
for warn in &warnings {
result.add_warning(format!(
"rust-analyzer [{}] at {}:{}: {}",
warn.code.as_deref().unwrap_or("warning"),
warn.file,
warn.line,
warn.message
));
}
for err in &errors {
result.add_error(format!(
"rust-analyzer [{}] at {}:{}: {}",
err.code.as_deref().unwrap_or("error"),
err.file,
err.line,
err.message
));
}
result.semantic_ok = errors.is_empty();
}
Err(e) => {
result.add_warning(format!("rust-analyzer unavailable: {}", e));
result.semantic_ok = true; }
}
} else {
result.semantic_ok = true;
}
Ok(result)
}
pub fn verify_localized_change(
file_path: &Path,
replaced_content: &[u8],
target_span: (usize, usize),
) -> Result<bool> {
let current = io_ext::read(file_path)?;
if target_span.0 > 0 && target_span.0 <= replaced_content.len() {
let before_replaced = &replaced_content[..target_span.0];
let before_current = current.get(..target_span.0);
if before_current != Some(before_replaced) {
log::warn!("File modified before target span");
return Ok(false);
}
}
let after_start = target_span.1.min(replaced_content.len());
if after_start < replaced_content.len() {
let after_replaced = &replaced_content[after_start..];
let after_current = current.get(after_start..);
if after_current != Some(after_replaced) {
log::warn!("File modified after target span");
return Ok(false);
}
}
Ok(true)
}
fn run_cargo_check(workspace_dir: &Path) -> Result<std::process::Output> {
use std::process::Command;
if !workspace_dir.join("Cargo.toml").exists() {
return Err(crate::error::SpliceError::IoContext {
context: format!(
"Cannot run cargo check outside a Rust package: {}",
workspace_dir.display()
),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "Cargo.toml not found"),
});
}
let output = Command::new("cargo")
.args(["check", "--quiet", "--color=never"])
.current_dir(workspace_dir)
.output()
.map_err(|e| crate::error::SpliceError::IoContext {
context: format!("Failed to run cargo check: {}", e),
source: e,
})?;
Ok(output)
}