use std::path::{Path, PathBuf};
use syn::spanned::Spanned;
use syn::visit::Visit;
use super::{MetaMutation, MetaMutationError, MetaOperator};
#[inline]
pub fn apply(
vyre_conform_root: &Path,
mutation: MetaMutation,
) -> Result<Vec<PathBuf>, MetaMutationError> {
match mutation {
MetaMutation::Syntactic {
component,
operator,
line,
column,
} => apply_syntactic(vyre_conform_root, component, operator, line, column),
_ => Err(MetaMutationError::NotYetWired),
}
}
fn apply_syntactic(
vyre_conform_root: &Path,
component: &str,
operator: MetaOperator,
line: usize,
column: usize,
) -> Result<Vec<PathBuf>, MetaMutationError> {
let relative = component.replace("::", "/");
let mut source_path = vyre_conform_root.to_path_buf();
source_path.push("src");
source_path.push(relative);
source_path.set_extension("rs");
let source =
std::fs::read_to_string(&source_path).map_err(|e| MetaMutationError::TargetMissing {
message: format!("Fix: could not read {}: {}", source_path.display(), e),
})?;
let new_source = match operator {
MetaOperator::SwapComparison => apply_swap_comparison(&source, line, column)?,
MetaOperator::OffByOne => apply_off_by_one(&source, line, column)?,
_ => apply_line_replacement(&source, component, operator, line)?,
};
std::fs::write(&source_path, new_source).map_err(|e| MetaMutationError::TargetMissing {
message: format!("Fix: could not write {}: {}", source_path.display(), e),
})?;
Ok(vec![source_path])
}
fn apply_line_replacement(
source: &str,
component: &str,
operator: MetaOperator,
line: usize,
) -> Result<String, MetaMutationError> {
let mut lines: Vec<String> = source.lines().map(|s| s.to_string()).collect();
if line == 0 || line > lines.len() {
return Err(MetaMutationError::TargetMissing {
message: format!("Fix: line {} out of bounds for {}", line, component),
});
}
let line_content = &lines[line - 1];
let (original, replacement) = find_replacement(line_content, operator)?;
let new_line = line_content.replacen(original, replacement, 1);
lines[line - 1] = new_line;
Ok(lines.join("\n"))
}
fn find_replacement(
line: &str,
operator: MetaOperator,
) -> Result<(&'static str, &'static str), MetaMutationError> {
match operator {
MetaOperator::SwapComparison | MetaOperator::OffByOne => {
unreachable!("handled by syn-based apply path")
}
MetaOperator::WeakenBound => {
if line.contains("<") && !line.contains("<=") {
Ok(("<", "<="))
} else if line.contains(">") && !line.contains(">=") {
Ok((">", ">="))
} else {
Err(MetaMutationError::TargetMissing {
message: "Fix: no strict inequality found on line".into(),
})
}
}
MetaOperator::DropAssertion => {
if line.contains("assert!") {
Ok(("assert!", "// assert!"))
} else if line.contains("assert_eq!") {
Ok(("assert_eq!", "// assert_eq!"))
} else {
Err(MetaMutationError::TargetMissing {
message: "Fix: no assertion found on line".into(),
})
}
}
MetaOperator::SwallowError => {
if line.contains("?") {
Ok(("?", "/* ? */.unwrap_or_default()"))
} else {
Err(MetaMutationError::TargetMissing {
message: "Fix: no ? operator found on line".into(),
})
}
}
}
}
fn apply_swap_comparison(
source: &str,
target_line: usize,
target_column: usize,
) -> Result<String, MetaMutationError> {
let syntax = syn::parse_file(source).map_err(|e| MetaMutationError::TargetMissing {
message: format!("Fix: failed to parse source: {}", e),
})?;
struct Visitor {
target_line: usize,
target_column: usize,
match_span: Option<proc_macro2::Span>,
replacement: Option<&'static str>,
}
impl<'ast> Visit<'ast> for Visitor {
fn visit_expr_binary(&mut self, i: &'ast syn::ExprBinary) {
match i.op {
syn::BinOp::Eq(_) | syn::BinOp::Ne(_) | syn::BinOp::Lt(_) | syn::BinOp::Gt(_) => {
let span = i.op.span();
let start = span.start();
if start.line == self.target_line && start.column == self.target_column {
self.match_span = Some(span);
self.replacement = Some(match i.op {
syn::BinOp::Eq(_) => "!=",
syn::BinOp::Ne(_) => "==",
syn::BinOp::Lt(_) => ">",
syn::BinOp::Gt(_) => "<",
_ => unreachable!(),
});
}
}
_ => {}
}
syn::visit::visit_expr_binary(self, i);
}
}
let mut visitor = Visitor {
target_line,
target_column,
match_span: None,
replacement: None,
};
visitor.visit_file(&syntax);
if let (Some(span), Some(replacement)) = (visitor.match_span, visitor.replacement) {
replace_at_span(source, span, replacement)
} else {
Err(MetaMutationError::TargetMissing {
message: "Fix: no comparison operator found at target location".into(),
})
}
}
fn apply_off_by_one(
source: &str,
target_line: usize,
target_column: usize,
) -> Result<String, MetaMutationError> {
let syntax = syn::parse_file(source).map_err(|e| MetaMutationError::TargetMissing {
message: format!("Fix: failed to parse source: {}", e),
})?;
struct Visitor {
target_line: usize,
target_column: usize,
match_span: Option<proc_macro2::Span>,
replacement: Option<&'static str>,
}
impl<'ast> Visit<'ast> for Visitor {
fn visit_lit_int(&mut self, i: &'ast syn::LitInt) {
if let Ok(val) = i.base10_parse::<u64>() {
if val == 0 || val == 1 {
let span = i.span();
let start = span.start();
if start.line == self.target_line && start.column == self.target_column {
self.match_span = Some(span);
self.replacement = Some(if val == 0 { "1" } else { "0" });
}
}
}
syn::visit::visit_lit_int(self, i);
}
}
let mut visitor = Visitor {
target_line,
target_column,
match_span: None,
replacement: None,
};
visitor.visit_file(&syntax);
if let (Some(span), Some(replacement)) = (visitor.match_span, visitor.replacement) {
replace_at_span(source, span, replacement)
} else {
Err(MetaMutationError::TargetMissing {
message: "Fix: no 0 or 1 literal found at target location".into(),
})
}
}
fn replace_at_span(
source: &str,
span: proc_macro2::Span,
replacement: &str,
) -> Result<String, MetaMutationError> {
let start = span.start();
let end = span.end();
let byte_start =
byte_offset_for_line_column(source, start.line, start.column).ok_or_else(|| {
MetaMutationError::TargetMissing {
message: "Fix: could not map span start to byte offset".into(),
}
})?;
let byte_end = byte_offset_for_line_column(source, end.line, end.column).ok_or_else(|| {
MetaMutationError::TargetMissing {
message: "Fix: could not map span end to byte offset".into(),
}
})?;
let mut result =
String::with_capacity(source.len() - (byte_end - byte_start) + replacement.len());
result.push_str(&source[..byte_start]);
result.push_str(replacement);
result.push_str(&source[byte_end..]);
Ok(result)
}
fn byte_offset_for_line_column(
source: &str,
target_line: usize,
target_column: usize,
) -> Option<usize> {
let mut line = 1;
let mut column = 1;
for (byte_offset, ch) in source.char_indices() {
if line == target_line && column == target_column {
return Some(byte_offset);
}
if ch == '\n' {
line += 1;
column = 1;
} else {
column += ch.len_utf8();
}
}
if line == target_line && column == target_column {
return Some(source.len());
}
None
}