winx_code_agent/utils/
syntax.rs1use std::fmt::Write as FmtWrite;
2use std::path::Path;
3use std::process::Command;
4
5use tree_sitter::{Node, Parser};
6
7pub fn syntax_warning(path: &Path, content: &str) -> Option<String> {
8 let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or_default();
9 let file_name = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
10
11 match ext {
12 "json" => serde_json::from_str::<serde_json::Value>(content)
13 .err()
14 .map(|error| format!("Syntax warning: JSON parser reported: {error}")),
15 "toml" => toml::from_str::<toml::Value>(content)
16 .err()
17 .map(|error| format!("Syntax warning: TOML parser reported: {error}")),
18 "rs" => {
19 let language = tree_sitter_rust::LANGUAGE.into();
20 tree_sitter_warning(content, &language, "Rust")
21 }
22 "js" | "mjs" | "cjs" | "jsx" => {
23 let language = tree_sitter_javascript::LANGUAGE.into();
24 tree_sitter_warning(content, &language, "JavaScript")
25 }
26 "ts" => {
27 let language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into();
28 tree_sitter_warning(content, &language, "TypeScript")
29 }
30 "tsx" => {
31 let language = tree_sitter_typescript::LANGUAGE_TSX.into();
32 tree_sitter_warning(content, &language, "TSX")
33 }
34 "sh" | "bash" | "zsh" => {
37 let language = tree_sitter_bash::LANGUAGE.into();
38 tree_sitter_warning(content, &language, "shell")
39 }
40 "go" => {
41 let language = tree_sitter_go::LANGUAGE.into();
42 tree_sitter_warning(content, &language, "Go")
43 }
44 "c" => {
45 let language = tree_sitter_c::LANGUAGE.into();
46 tree_sitter_warning(content, &language, "C")
47 }
48 "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" | "h" => {
50 let language = tree_sitter_cpp::LANGUAGE.into();
51 tree_sitter_warning(content, &language, "C++")
52 }
53 "java" => {
54 let language = tree_sitter_java::LANGUAGE.into();
55 tree_sitter_warning(content, &language, "Java")
56 }
57 "rb" => {
58 let language = tree_sitter_ruby::LANGUAGE.into();
59 tree_sitter_warning(content, &language, "Ruby")
60 }
61 "css" => {
62 let language = tree_sitter_css::LANGUAGE.into();
63 tree_sitter_warning(content, &language, "CSS")
64 }
65 "html" | "htm" => {
66 let language = tree_sitter_html::LANGUAGE.into();
67 tree_sitter_warning(content, &language, "HTML")
68 }
69 "php" => {
70 let language = tree_sitter_php::LANGUAGE_PHP.into();
71 tree_sitter_warning(content, &language, "PHP")
72 }
73 "cs" => {
74 let language = tree_sitter_c_sharp::LANGUAGE.into();
75 tree_sitter_warning(content, &language, "C#")
76 }
77 "lua" => {
78 let language = tree_sitter_lua::LANGUAGE.into();
79 tree_sitter_warning(content, &language, "Lua")
80 }
81 "py" | "pyi" => python_warning(path),
82 _ if matches!(file_name, "Dockerfile" | "Makefile") => None,
83 _ => None,
84 }
85}
86
87fn tree_sitter_warning(
88 content: &str,
89 language: &tree_sitter::Language,
90 language_name: &str,
91) -> Option<String> {
92 let mut parser = Parser::new();
93 if let Err(error) = parser.set_language(language) {
94 return Some(format!("Syntax warning: failed to load {language_name} parser: {error}"));
95 }
96
97 let tree = parser.parse(content, None)?;
98 let root = tree.root_node();
99 if !root.has_error() {
100 return None;
101 }
102
103 let mut message =
104 format!("Syntax warning: tree-sitter reported {language_name} syntax errors.");
105 if let Some(row) = first_error_row(root) {
106 let _ = write!(message, "\n{}", error_context(content, row));
107 }
108 Some(message)
109}
110
111fn first_error_row(node: Node<'_>) -> Option<usize> {
115 if node.is_error() || node.is_missing() {
116 return Some(node.start_position().row);
117 }
118 if !node.has_error() {
119 return None;
120 }
121 let mut cursor = node.walk();
122 for child in node.children(&mut cursor) {
123 if let Some(row) = first_error_row(child) {
124 return Some(row);
125 }
126 }
127 None
128}
129
130fn error_context(content: &str, error_row: usize) -> String {
133 let lines: Vec<&str> = content.lines().collect();
134 if lines.is_empty() {
135 return String::new();
136 }
137 let start = error_row.saturating_sub(10);
138 let end = (error_row + 11).min(lines.len());
139 let mut out = String::from("<snippet>\n");
140 for (offset, line) in lines[start..end].iter().enumerate() {
141 let line_no = start + offset + 1;
142 let marker = if start + offset == error_row { '>' } else { ' ' };
143 let _ = writeln!(out, "{marker}{line_no} {line}");
144 }
145 out.push_str("</snippet>");
146 out
147}
148
149fn python_warning(path: &Path) -> Option<String> {
150 let python = if Command::new("python3").arg("--version").output().is_ok() {
151 "python3"
152 } else if Command::new("python").arg("--version").output().is_ok() {
153 "python"
154 } else {
155 return None;
156 };
157
158 let output = Command::new(python).args(["-m", "py_compile"]).arg(path).output().ok()?;
159 (!output.status.success()).then(|| {
160 let stderr = String::from_utf8_lossy(&output.stderr);
161 format!("Syntax warning: Python parser reported:\n{}", stderr.trim())
162 })
163}
164
165#[cfg(test)]
166mod tests {
167 use super::syntax_warning;
168 use std::path::Path;
169
170 #[test]
171 fn reports_invalid_json() {
172 assert!(syntax_warning(Path::new("bad.json"), "{").is_some());
173 }
174
175 #[test]
176 fn accepts_valid_rust() {
177 assert!(syntax_warning(Path::new("lib.rs"), "fn main() {}\n").is_none());
178 }
179
180 #[test]
181 fn reports_invalid_bash() {
182 assert!(syntax_warning(Path::new("script.sh"), "if true; then\n").is_some());
183 }
184
185 #[test]
186 fn reports_invalid_zsh() {
187 assert!(syntax_warning(Path::new("script.zsh"), "if true; then\n").is_some());
189 }
190
191 #[test]
192 fn error_warning_includes_snippet() {
193 let warning = syntax_warning(Path::new("lib.rs"), "fn main() {\n").unwrap_or_default();
194 assert!(warning.contains("<snippet>"), "expected snippet, got: {warning}");
195 }
196
197 #[test]
198 fn checks_go() {
199 assert!(syntax_warning(Path::new("main.go"), "package main\nfunc main() {}\n").is_none());
200 assert!(syntax_warning(Path::new("main.go"), "package main\nfunc main( {\n").is_some());
201 }
202
203 #[test]
204 fn checks_php() {
205 assert!(syntax_warning(Path::new("a.php"), "<?php echo 1; ?>\n").is_none());
206 assert!(syntax_warning(Path::new("a.php"), "<?php echo (1; ?>\n").is_some());
207 }
208}