use std::path::Path;
pub(crate) const REPAIR_NOTE: &str =
"auto-corrected literal newline/tab escapes in the payload to real characters";
fn decode_literal_escapes_inner(s: &str, decode_quotes: bool) -> Option<String> {
if !s.contains('\\') {
return None;
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
let mut changed = false;
while let Some(c) = chars.next() {
if c == '\\' {
match chars.peek() {
Some('n') => {
out.push('\n');
chars.next();
changed = true;
}
Some('t') => {
out.push('\t');
chars.next();
changed = true;
}
Some('r') => {
out.push('\r');
chars.next();
changed = true;
}
Some('"') if decode_quotes => {
out.push('"');
chars.next();
changed = true;
}
Some('\'') if decode_quotes => {
out.push('\'');
chars.next();
changed = true;
}
_ => out.push('\\'),
}
} else {
out.push(c);
}
}
changed.then_some(out)
}
pub(crate) fn decode_literal_escapes(s: &str) -> Option<String> {
decode_literal_escapes_inner(s, false)
}
pub(crate) fn decode_literal_escapes_incl_quotes(s: &str) -> Option<String> {
decode_literal_escapes_inner(s, true)
}
pub(crate) enum RepairResult {
Clean(String),
Repaired(String),
Introduced(String),
}
impl RepairResult {
pub(crate) fn into_content(self) -> String {
match self {
RepairResult::Clean(c) | RepairResult::Repaired(c) | RepairResult::Introduced(c) => c,
}
}
}
pub(crate) fn finalize_edit_content<F>(
path: &Path,
original: &str,
candidate: String,
new_fragment: &str,
reapply_decoded: F,
) -> RepairResult
where
F: FnOnce(&str) -> String,
{
let Some(lang) = crate::ast::detect_language(path) else {
return RepairResult::Clean(candidate);
};
if !crate::ast::has_syntax_errors(&candidate, lang) {
return RepairResult::Clean(candidate);
}
if crate::ast::has_syntax_errors(original, lang) {
return RepairResult::Clean(candidate);
}
if let Some(decoded) = decode_literal_escapes(new_fragment) {
let repaired = reapply_decoded(&decoded);
if !crate::ast::has_syntax_errors(&repaired, lang) {
return RepairResult::Repaired(repaired);
}
}
RepairResult::Introduced(candidate)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn decode_literal_escapes_decodes_n_t_r() {
assert_eq!(decode_literal_escapes("a\\nb").as_deref(), Some("a\nb"));
assert_eq!(decode_literal_escapes("a\\tb").as_deref(), Some("a\tb"));
assert_eq!(decode_literal_escapes("a\\rb").as_deref(), Some("a\rb"));
}
#[test]
fn decode_literal_escapes_leaves_other_escapes_intact() {
assert_eq!(
decode_literal_escapes("a\\nb\\\"c").as_deref(),
Some("a\nb\\\"c")
);
assert_eq!(decode_literal_escapes("a\\\"b"), None);
}
#[test]
fn decode_literal_escapes_none_when_nothing_to_decode() {
assert_eq!(decode_literal_escapes("plain text"), None);
}
#[test]
fn decode_incl_quotes_decodes_escaped_quotes() {
assert_eq!(
decode_literal_escapes_incl_quotes("a\\\"b").as_deref(),
Some("a\"b")
);
assert_eq!(
decode_literal_escapes_incl_quotes("a\\'b").as_deref(),
Some("a'b")
);
assert_eq!(decode_literal_escapes("a\\\"b"), None);
}
#[test]
fn decode_incl_quotes_decodes_newline_and_quotes_together() {
assert_eq!(
decode_literal_escapes_incl_quotes("x\\nassert(\\\"m\\\")").as_deref(),
Some("x\nassert(\"m\")")
);
}
#[test]
fn decode_incl_quotes_leaves_doubled_backslash_intact() {
assert_eq!(decode_literal_escapes_incl_quotes("a\\\\b"), None);
}
#[test]
fn finalize_clean_when_candidate_parses() {
let r = finalize_edit_content(
Path::new("x.rs"),
"fn a() {}\n",
"fn a() {}\nfn b() {}\n".to_string(),
"fn b() {}",
|d| format!("fn a() {{}}\n{d}\n"),
);
assert!(matches!(r, RepairResult::Clean(_)));
}
#[test]
fn finalize_repairs_introduced_error_via_decode() {
let candidate = "fn a() {}\nfn b() {\\n let x = 1;\\n}\n".to_string();
let r = finalize_edit_content(
Path::new("x.rs"),
"fn a() {}\n",
candidate,
"fn b() {\\n let x = 1;\\n}",
|decoded| format!("fn a() {{}}\n{decoded}\n"),
);
match r {
RepairResult::Repaired(c) => {
assert!(
!c.contains("\\n"),
"decoded content must use real newlines: {c}"
);
assert!(c.contains("let x = 1;"));
}
_ => panic!("expected Repaired"),
}
}
#[test]
fn finalize_introduced_when_unrepairable() {
let r = finalize_edit_content(
Path::new("x.rs"),
"fn a() {}\n",
"fn a() {}\nfn b() {\n".to_string(),
"fn b() {",
|d| d.to_string(),
);
assert!(matches!(r, RepairResult::Introduced(_)));
}
#[test]
fn finalize_clean_when_original_already_broken() {
let r = finalize_edit_content(
Path::new("x.rs"),
"fn a() {\n",
"fn a() {\nfn b() {\n".to_string(),
"fn b() {",
|d| d.to_string(),
);
assert!(matches!(r, RepairResult::Clean(_)));
}
}