#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SyntaxCheck {
pub has_error: bool,
pub first_error_line: Option<usize>,
}
#[cfg(feature = "tree-sitter")]
pub fn check_syntax(content: &str, ext: &str) -> Option<SyntaxCheck> {
use std::cell::RefCell;
use tree_sitter::Parser;
let language = crate::core::deep_queries::get_language(ext)?;
thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
let tree = PARSER.with(|p| {
let mut parser = p.borrow_mut();
parser.set_language(&language).ok()?;
parser.parse(content.as_bytes(), None)
})?;
let root = tree.root_node();
if !root.has_error() {
return Some(SyntaxCheck {
has_error: false,
first_error_line: None,
});
}
Some(SyntaxCheck {
has_error: true,
first_error_line: first_error_line(root),
})
}
#[cfg(not(feature = "tree-sitter"))]
pub fn check_syntax(_content: &str, _ext: &str) -> Option<SyntaxCheck> {
None
}
#[cfg(feature = "tree-sitter")]
fn first_error_line(node: tree_sitter::Node) -> Option<usize> {
if node.is_error() || node.is_missing() {
return Some(node.start_position().row + 1);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.is_error() || child.is_missing() {
return Some(child.start_position().row + 1);
}
if child.has_error()
&& let Some(line) = first_error_line(child)
{
return Some(line);
}
}
None
}
#[must_use]
pub fn gate_edit(ext: &str, old_content: &str, new_content: &str) -> Option<String> {
let pre = check_syntax(old_content, ext)?;
if pre.has_error {
return None;
}
let post = check_syntax(new_content, ext)?;
if !post.has_error {
return None;
}
let loc = post
.first_error_line
.map_or_else(String::new, |l| format!(" near line {l}"));
Some(format!(
"ERROR: edit rejected — it introduces a syntax error{loc} (.{ext}). \
The file parsed cleanly before this edit, so the change is malformed; \
no write was made. Fix the snippet and retry, or pass \
validate_syntax=false to override."
))
}
#[cfg(all(test, feature = "tree-sitter"))]
mod tests {
use super::*;
#[test]
fn clean_code_has_no_error() {
let c = check_syntax("fn main() {}\n", "rs").unwrap();
assert!(!c.has_error);
assert_eq!(c.first_error_line, None);
}
#[test]
fn broken_code_reports_error_with_line() {
let c = check_syntax("fn main() {\n let x =\n", "rs").unwrap();
assert!(c.has_error);
assert!(c.first_error_line.is_some());
}
#[test]
fn unsupported_extension_is_none() {
assert!(check_syntax("anything at all", "unknownext").is_none());
}
#[test]
fn gate_blocks_clean_to_broken() {
let old = "fn main() {}\n";
let new = "fn main() {\n"; let reason = gate_edit("rs", old, new).expect("clean→broken must be gated");
assert!(reason.contains("syntax error"));
assert!(reason.contains("validate_syntax=false"));
}
#[test]
fn gate_allows_broken_to_broken() {
let old = "fn main() {\n"; let new = "fn main( {\n"; assert!(gate_edit("rs", old, new).is_none());
}
#[test]
fn gate_allows_clean_to_clean() {
let old = "fn main() {}\n";
let new = "fn main() { let x = 1; }\n";
assert!(gate_edit("rs", old, new).is_none());
}
#[test]
fn gate_skips_unsupported_language() {
assert!(gate_edit("unknownext", "valid", "{[(").is_none());
}
#[test]
fn gate_allows_broken_being_fixed() {
let old = "fn main() {\n"; let new = "fn main() {}\n"; assert!(gate_edit("rs", old, new).is_none());
}
}