Skip to main content

code_kb_core/
syntax.rs

1use serde::Deserialize;
2use std::io::Write;
3use std::process::{Command, Stdio};
4use thiserror::Error;
5
6use crate::sync::find_julie_extract_binary;
7
8#[derive(Debug, Error, PartialEq)]
9pub enum SyntaxError {
10    #[error("Syntax error in {0}: {1}")]
11    ParseError(String, String),
12    #[error("Syntax check could not run: {0}")]
13    CheckFailed(String),
14}
15
16#[derive(Deserialize)]
17struct CheckReport {
18    status: String,
19    #[serde(default)]
20    errors: Vec<CheckDiagnostic>,
21}
22
23#[derive(Deserialize)]
24struct CheckDiagnostic {
25    message: String,
26}
27
28/// Validates `content` as the full text of `file_path` through `julie-extract check` before it is written to disk.
29/// Returns `Ok(true)` when the extractor parsed it cleanly, `Ok(false)` when the extractor has no grammar for the
30/// path, and `Err(SyntaxError::ParseError)` when the parse reported syntax errors.
31pub fn validate_syntax(file_path: &str, content: &str) -> Result<bool, SyntaxError> {
32    let bin = find_julie_extract_binary()
33        .ok_or_else(|| SyntaxError::CheckFailed("julie-extract binary not found".to_string()))?;
34    let mut child = Command::new(bin)
35        .args(["check", "--path", file_path, "--json"])
36        .stdin(Stdio::piped())
37        .stdout(Stdio::piped())
38        .stderr(Stdio::null())
39        .spawn()
40        .map_err(|e| SyntaxError::CheckFailed(e.to_string()))?;
41    let written = child
42        .stdin
43        .take()
44        .expect("stdin was piped")
45        .write_all(content.as_bytes());
46    let output = child
47        .wait_with_output()
48        .map_err(|e| SyntaxError::CheckFailed(e.to_string()))?;
49    written.map_err(|e| SyntaxError::CheckFailed(format!("could not send content: {e}")))?;
50    let report: CheckReport = serde_json::from_slice(&output.stdout)
51        .map_err(|e| SyntaxError::CheckFailed(format!("unreadable check report: {e}")))?;
52
53    match report.status.as_str() {
54        "ok" => Ok(true),
55        "unsupported" => Ok(false),
56        _ => {
57            let first = report
58                .errors
59                .first()
60                .map(|e| e.message.clone())
61                .unwrap_or_else(|| "syntax error".to_string());
62            let more = report.errors.len().saturating_sub(1);
63            let detail = if more > 0 {
64                format!("{first} (+{more} more)")
65            } else {
66                first
67            };
68            Err(SyntaxError::ParseError(file_path.to_string(), detail))
69        }
70    }
71}