vtcode_core/tools/
ast_grep.rs1use anyhow::{Context, Result};
7use serde_json::{Value, json};
8use std::collections::HashMap;
9use tokio;
10
11pub struct AstGrepEngine {
13 sgrep_path: String,
15}
16
17impl AstGrepEngine {
18 pub fn new() -> Result<Self> {
20 let sgrep_path = if cfg!(target_os = "windows") {
22 "ast-grep.exe"
23 } else {
24 "ast-grep"
25 };
26
27 let output = std::process::Command::new(sgrep_path)
29 .arg("--version")
30 .output()
31 .context("Failed to execute ast-grep")?;
32
33 if !output.status.success() {
34 return Err(anyhow::anyhow!(
35 "ast-grep not found or not working properly"
36 ));
37 }
38
39 Ok(Self {
40 sgrep_path: sgrep_path.to_string(),
41 })
42 }
43
44 fn map_language(language: &str) -> &str {
46 match language.to_lowercase().as_str() {
47 "rust" => "rust",
48 "rs" => "rust",
49 "python" => "python",
50 "py" => "python",
51 "javascript" => "javascript",
52 "js" => "javascript",
53 "typescript" => "typescript",
54 "ts" => "typescript",
55 "tsx" => "tsx",
56 "go" => "go",
57 "golang" => "go",
58 "java" => "java",
59 "cpp" => "cpp",
60 "c++" => "cpp",
61 "c" => "c",
62 "html" => "html",
63 "css" => "css",
64 "json" => "json",
65 "yaml" => "yaml",
66 "yml" => "yaml",
67 _ => language,
68 }
69 }
70
71 pub async fn search(
73 &self,
74 pattern: &str,
75 path: &str,
76 language: Option<&str>,
77 context_lines: Option<usize>,
78 max_results: Option<usize>,
79 ) -> Result<Value> {
80 let sgrep_path = self.sgrep_path.clone();
81 let pattern = pattern.to_string();
82 let path = path.to_string();
83 let language = language.map(|s| s.to_string());
84 let _context_lines = context_lines.unwrap_or(0);
85 let _max_results = max_results.unwrap_or(100);
86
87 let handle = tokio::task::spawn_blocking(move || {
88 let mut cmd = std::process::Command::new(&sgrep_path);
89 cmd.arg("run")
90 .arg("--pattern")
91 .arg(&pattern)
92 .arg("--json")
93 .arg("--context")
94 .arg(&_context_lines.to_string())
95 .arg(&path);
96
97 if let Some(lang) = language {
98 cmd.arg("--lang").arg(Self::map_language(&lang));
99 }
100
101 cmd.output()
102 });
103
104 let output = handle
105 .await
106 .context("Failed to spawn ast-grep search task")?
107 .context("Failed to execute ast-grep search")?;
108
109 if !output.status.success() {
110 let stderr = String::from_utf8_lossy(&output.stderr);
111 return Err(anyhow::anyhow!("ast-grep search failed: {}", stderr));
112 }
113
114 let stdout = String::from_utf8_lossy(&output.stdout);
115 let results: Value =
116 serde_json::from_str(&stdout).context("Failed to parse ast-grep search results")?;
117
118 Ok(json!({ "success": true, "matches": results }))
119 }
120
121 pub async fn transform(
123 &self,
124 pattern: &str,
125 replacement: &str,
126 path: &str,
127 language: Option<&str>,
128 preview_only: bool,
129 update_all: bool,
130 ) -> Result<Value> {
131 let sgrep_path = self.sgrep_path.clone();
132 let pattern = pattern.to_string();
133 let replacement = replacement.to_string();
134 let path = path.to_string();
135 let language = language.map(|s| s.to_string());
136
137 let handle = tokio::task::spawn_blocking(move || {
138 let mut cmd = std::process::Command::new(&sgrep_path);
139 cmd.arg("run")
140 .arg("--pattern")
141 .arg(&pattern)
142 .arg("--rewrite")
143 .arg(&replacement)
144 .arg("--json")
145 .arg(&path);
146
147 if let Some(lang) = language {
148 cmd.arg("--lang").arg(Self::map_language(&lang));
149 }
150
151 if update_all && !preview_only {
154 cmd.arg("--update-all");
155 }
156
157 cmd.output()
158 });
159
160 let output = handle
161 .await
162 .context("Failed to spawn ast-grep transform task")?
163 .context("Failed to execute ast-grep transform")?;
164
165 if !output.status.success() {
166 let stderr = String::from_utf8_lossy(&output.stderr);
167 return Err(anyhow::anyhow!("ast-grep transform failed: {}", stderr));
168 }
169
170 let stdout = String::from_utf8_lossy(&output.stdout);
171 let results: Value =
172 serde_json::from_str(&stdout).context("Failed to parse ast-grep transform results")?;
173
174 Ok(json!({ "success": true, "changes": results }))
175 }
176
177 pub async fn lint(
179 &self,
180 path: &str,
181 language: Option<&str>,
182 severity_filter: Option<&str>,
183 custom_rules: Option<Vec<HashMap<String, Value>>>,
184 ) -> Result<Value> {
185 let sgrep_path = self.sgrep_path.clone();
186 let path = path.to_string();
187 let language = language.map(|s| s.to_string());
188 let _severity_filter = severity_filter.map(|s| s.to_string());
189 let _custom_rules = custom_rules.clone();
190
191 let handle = tokio::task::spawn_blocking(move || {
192 let mut cmd = std::process::Command::new(&sgrep_path);
193 cmd.arg("run")
194 .arg("--pattern")
195 .arg("// TODO: $$")
196 .arg("--json")
197 .arg(&path);
198
199 if let Some(lang) = language {
200 cmd.arg("--lang").arg(Self::map_language(&lang));
201 }
202
203 cmd.output()
204 });
205
206 let output = handle
207 .await
208 .context("Failed to spawn ast-grep lint task")?
209 .context("Failed to execute ast-grep lint")?;
210
211 let stdout = String::from_utf8_lossy(&output.stdout);
214 if !stdout.trim().is_empty() {
215 if let Ok(results) = serde_json::from_str::<Value>(&stdout) {
216 return Ok(json!({ "success": true, "issues": results }));
217 }
218 }
219
220 if !output.status.success() {
222 let stderr = String::from_utf8_lossy(&output.stderr);
223 if stderr.contains("No ast-grep project configuration") {
225 return Ok(json!({
226 "success": true,
227 "issues": [],
228 "warning": "No ast-grep project configuration found. Linting may be limited."
229 }));
230 }
231 return Err(anyhow::anyhow!("ast-grep lint failed: {}", stderr));
232 }
233
234 Ok(json!({ "success": true, "issues": [] }))
235 }
236
237 pub async fn refactor(
239 &self,
240 path: &str,
241 language: Option<&str>,
242 refactor_type: &str,
243 ) -> Result<Value> {
244 let (pattern, replacement) = match refactor_type {
246 "extract_function" => (
247 "function $func($) { $$ }",
248 "// TODO: Extract function $func to separate module\nfunction $func($) { $$ }",
249 ),
250 "remove_console_logs" => ("console.log($$)", ""),
251 "simplify_conditions" => ("if ($cond) { true } else { false }", "$cond"),
252 "extract_constants" => (
253 "$NUM", "const MY_CONSTANT = $NUM;\n// TODO: Replace $NUM with MY_CONSTANT",
255 ),
256 "modernize_syntax" => ("var $VAR = $$", "let $VAR = $$"),
257 _ => ("$$", "// TODO: Consider refactoring this code"),
258 };
259
260 let sgrep_path = self.sgrep_path.clone();
261 let path = path.to_string();
262 let language = language.map(|s| s.to_string());
263 let pattern = pattern.to_string();
264 let replacement = replacement.to_string();
265
266 let handle = tokio::task::spawn_blocking(move || {
267 let mut cmd = std::process::Command::new(&sgrep_path);
268 cmd.arg("run")
269 .arg("--pattern")
270 .arg(&pattern)
271 .arg("--rewrite")
272 .arg(&replacement)
273 .arg("--json")
274 .arg(&path);
275
276 if let Some(lang) = language {
277 cmd.arg("--lang").arg(Self::map_language(&lang));
278 }
279
280 cmd.output()
281 });
282
283 let output = handle
284 .await
285 .context("Failed to spawn ast-grep refactor task")?
286 .context("Failed to execute ast-grep refactor")?;
287
288 if !output.status.success() {
289 let stderr = String::from_utf8_lossy(&output.stderr);
290 return Err(anyhow::anyhow!("ast-grep refactor failed: {}", stderr));
291 }
292
293 let stdout = String::from_utf8_lossy(&output.stdout);
294 let results: Value =
295 serde_json::from_str(&stdout).context("Failed to parse ast-grep refactor results")?;
296
297 Ok(json!({ "success": true, "suggestions": results }))
298 }
299
300 pub async fn run_custom(
302 &self,
303 pattern: &str,
304 path: &str,
305 language: Option<&str>,
306 rewrite: Option<&str>,
307 context_lines: Option<usize>,
308 max_results: Option<usize>,
309 interactive: bool,
310 update_all: bool,
311 ) -> Result<Value> {
312 let sgrep_path = self.sgrep_path.clone();
313 let pattern = pattern.to_string();
314 let path = path.to_string();
315 let language = language.map(|s| s.to_string());
316 let rewrite = rewrite.map(|s| s.to_string());
317 let context_lines = context_lines.unwrap_or(0);
318 let max_results = max_results.unwrap_or(100);
319
320 let handle = tokio::task::spawn_blocking(move || {
321 let mut cmd = std::process::Command::new(&sgrep_path);
322 cmd.arg("run")
323 .arg("--pattern")
324 .arg(&pattern)
325 .arg("--json")
326 .arg("--context")
327 .arg(context_lines.to_string())
328 .arg("--max-results")
329 .arg(max_results.to_string())
330 .arg(&path);
331
332 if let Some(lang) = language {
333 cmd.arg("--lang").arg(Self::map_language(&lang));
334 }
335
336 if let Some(rewrite_pattern) = rewrite {
337 cmd.arg("--rewrite").arg(&rewrite_pattern);
338 }
339
340 if interactive {
341 cmd.arg("--interactive");
342 } else if update_all {
343 cmd.arg("--update-all");
344 }
345
346 cmd.output()
347 });
348
349 let output = handle
350 .await
351 .context("Failed to spawn ast-grep custom task")?
352 .context("Failed to execute ast-grep custom command")?;
353
354 if !output.status.success() {
355 let stderr = String::from_utf8_lossy(&output.stderr);
356 return Err(anyhow::anyhow!(
357 "ast-grep custom command failed: {}",
358 stderr
359 ));
360 }
361
362 let stdout = String::from_utf8_lossy(&output.stdout);
363 let results: Value =
364 serde_json::from_str(&stdout).context("Failed to parse ast-grep custom results")?;
365
366 Ok(json!({ "success": true, "results": results }))
367 }
368}