winx_code_agent/utils/
syntax.rs1use std::path::Path;
2use std::process::Command;
3
4use tree_sitter::Parser;
5
6pub fn syntax_warning(path: &Path, content: &str) -> Option<String> {
7 let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or_default();
8 let file_name = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
9
10 match ext {
11 "json" => serde_json::from_str::<serde_json::Value>(content)
12 .err()
13 .map(|error| format!("Syntax warning: JSON parser reported: {error}")),
14 "toml" => toml::from_str::<toml::Value>(content)
15 .err()
16 .map(|error| format!("Syntax warning: TOML parser reported: {error}")),
17 "rs" => {
18 let language = tree_sitter_rust::LANGUAGE.into();
19 tree_sitter_warning(content, &language, "Rust")
20 }
21 "js" | "mjs" | "cjs" | "jsx" => {
22 let language = tree_sitter_javascript::LANGUAGE.into();
23 tree_sitter_warning(content, &language, "JavaScript")
24 }
25 "ts" => {
26 let language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into();
27 tree_sitter_warning(content, &language, "TypeScript")
28 }
29 "tsx" => {
30 let language = tree_sitter_typescript::LANGUAGE_TSX.into();
31 tree_sitter_warning(content, &language, "TSX")
32 }
33 "sh" | "bash" | "zsh" if ext != "zsh" => {
34 let language = tree_sitter_bash::LANGUAGE.into();
35 tree_sitter_warning(content, &language, "bash")
36 }
37 "py" => python_warning(path),
38 _ if matches!(file_name, "Dockerfile" | "Makefile") => None,
39 _ => None,
40 }
41}
42
43fn tree_sitter_warning(
44 content: &str,
45 language: &tree_sitter::Language,
46 language_name: &str,
47) -> Option<String> {
48 let mut parser = Parser::new();
49 if let Err(error) = parser.set_language(language) {
50 return Some(format!("Syntax warning: failed to load {language_name} parser: {error}"));
51 }
52
53 let tree = parser.parse(content, None)?;
54 tree.root_node()
55 .has_error()
56 .then(|| format!("Syntax warning: tree-sitter reported {language_name} syntax errors."))
57}
58
59fn python_warning(path: &Path) -> Option<String> {
60 let python = if Command::new("python3").arg("--version").output().is_ok() {
61 "python3"
62 } else if Command::new("python").arg("--version").output().is_ok() {
63 "python"
64 } else {
65 return None;
66 };
67
68 let output = Command::new(python).args(["-m", "py_compile"]).arg(path).output().ok()?;
69 (!output.status.success()).then(|| {
70 let stderr = String::from_utf8_lossy(&output.stderr);
71 format!("Syntax warning: Python parser reported:\n{}", stderr.trim())
72 })
73}
74
75#[cfg(test)]
76mod tests {
77 use super::syntax_warning;
78 use std::path::Path;
79
80 #[test]
81 fn reports_invalid_json() {
82 assert!(syntax_warning(Path::new("bad.json"), "{").is_some());
83 }
84
85 #[test]
86 fn accepts_valid_rust() {
87 assert!(syntax_warning(Path::new("lib.rs"), "fn main() {}\n").is_none());
88 }
89
90 #[test]
91 fn reports_invalid_bash() {
92 assert!(syntax_warning(Path::new("script.sh"), "if true; then\n").is_some());
93 }
94}