Skip to main content

limit_cli/tools/
analysis.rs

1use ast_grep_core::Pattern;
2use ast_grep_language::{LanguageExt, SupportLang};
3use async_trait::async_trait;
4use ignore::WalkBuilder;
5use limit_agent::error::AgentError;
6use limit_agent::Tool;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13use tracing::{debug, info};
14
15const GREP_MAX_RESULTS: usize = 1000;
16const GREP_CONTEXT_LINES: usize = 3;
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct Position {
20    pub line: u32,
21    pub character: u32,
22}
23pub struct GrepTool;
24
25impl GrepTool {
26    pub fn new() -> Self {
27        GrepTool
28    }
29}
30
31impl Default for GrepTool {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37#[async_trait]
38impl Tool for GrepTool {
39    fn name(&self) -> &str {
40        "grep"
41    }
42
43    async fn execute(&self, args: Value) -> Result<Value, AgentError> {
44        let pattern: String = serde_json::from_value(args["pattern"].clone())
45            .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
46
47        if pattern.trim().is_empty() {
48            return Err(AgentError::ToolError(
49                "pattern argument cannot be empty".to_string(),
50            ));
51        }
52
53        // Validate regex pattern
54        Regex::new(&pattern)
55            .map_err(|e| AgentError::ToolError(format!("Invalid regex pattern: {}", e)))?;
56
57        let default_path = std::env::current_dir()
58            .unwrap_or_else(|_| PathBuf::from("."))
59            .to_string_lossy()
60            .to_string();
61        let path = args
62            .get("path")
63            .and_then(|v| v.as_str())
64            .unwrap_or(&default_path);
65
66        // Validate path exists
67        if !Path::new(path).exists() {
68            return Err(AgentError::ToolError(format!("Path not found: {}", path)));
69        }
70
71        // Use grep command-line tool
72        let mut cmd = Command::new("grep");
73        cmd.arg("-r")
74            .arg("-n")
75            .arg("-I") // Ignore binary files
76            .arg("--color=never")
77            .args(["-C", &GREP_CONTEXT_LINES.to_string()])
78            .arg(&pattern)
79            .arg(path);
80
81        let output = cmd
82            .output()
83            .map_err(|e| AgentError::ToolError(format!("Failed to execute grep: {}", e)))?;
84
85        if !output.status.success() {
86            // grep returns non-zero if no matches found, but that's not an error
87            let stderr = String::from_utf8_lossy(&output.stderr);
88            if !stderr.is_empty() && !stderr.contains("No such file") {
89                return Err(AgentError::ToolError(format!("grep failed: {}", stderr)));
90            }
91        }
92
93        let stdout = String::from_utf8_lossy(&output.stdout);
94        let lines: Vec<&str> = stdout.lines().collect();
95
96        // Limit results
97        let limited_lines = if lines.len() > GREP_MAX_RESULTS {
98            lines[..GREP_MAX_RESULTS].to_vec()
99        } else {
100            lines
101        };
102
103        // Parse grep output
104        let mut matches = Vec::new();
105        for line in limited_lines {
106            // Parse grep output format: "filename:line_number:content"
107            if let Some((rest, content)) = line.split_once(':') {
108                if let Some((file_path, line_number)) = rest.split_once(':') {
109                    if let Ok(line_num) = line_number.parse::<usize>() {
110                        matches.push(serde_json::json!({
111                            "file": file_path,
112                            "line": line_num,
113                            "content": content
114                        }));
115                    }
116                }
117            }
118        }
119
120        Ok(serde_json::json!({
121            "matches": matches,
122            "count": matches.len(),
123            "pattern": pattern
124        }))
125    }
126}
127
128pub struct AstGrepTool;
129
130impl AstGrepTool {
131    pub fn new() -> Self {
132        AstGrepTool
133    }
134
135    fn get_language_support(lang: &str) -> Result<SupportLang, AgentError> {
136        lang.parse()
137            .map_err(|_| AgentError::ToolError(format!("Unsupported language: {}. Use a valid language name or alias (e.g., rs, py, js, rust, python, javascript).", lang)))
138    }
139
140    async fn execute_search(&self, args: Value) -> Result<Value, AgentError> {
141        let pattern: String = serde_json::from_value(args["pattern"].clone())
142            .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
143
144        if pattern.trim().is_empty() {
145            return Err(AgentError::ToolError(
146                "pattern argument cannot be empty".to_string(),
147            ));
148        }
149
150        let language: String = serde_json::from_value(args["language"].clone())
151            .map_err(|e| AgentError::ToolError(format!("Invalid language argument: {}", e)))?;
152
153        let lang = Self::get_language_support(&language)?;
154
155        let default_path = std::env::current_dir()
156            .unwrap_or_else(|_| PathBuf::from("."))
157            .to_string_lossy()
158            .to_string();
159        let path = args
160            .get("path")
161            .and_then(|v| v.as_str())
162            .unwrap_or(&default_path);
163
164        let path_obj = Path::new(path);
165        if !path_obj.exists() {
166            return Err(AgentError::ToolError(format!("Path not found: {}", path)));
167        }
168
169        debug!("ast_grep: searching in path={}", path);
170
171        let context_after = args
172            .get("context_after")
173            .and_then(|v| v.as_u64())
174            .unwrap_or(0);
175        let context_before = args
176            .get("context_before")
177            .and_then(|v| v.as_u64())
178            .unwrap_or(0);
179
180        let globs: Option<Vec<String>> = args.get("globs").and_then(|v| {
181            v.as_array().map(|arr| {
182                arr.iter()
183                    .filter_map(|val| val.as_str().map(String::from))
184                    .collect()
185            })
186        });
187
188        let mut all_matches = Vec::new();
189
190        let search_pattern = Pattern::try_new(&pattern, lang)
191            .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
192
193        if path_obj.is_file() {
194            let content = fs::read_to_string(path_obj)
195                .map_err(|e| AgentError::ToolError(format!("Failed to read file: {}", e)))?;
196
197            let grep = lang.ast_grep(&content);
198
199            for match_ in grep.root().find_all(&search_pattern) {
200                let line = match_.start_pos().line();
201                let text = match_.text();
202
203                let mut match_obj = serde_json::json!({
204                    "file": path,
205                    "line": line,
206                    "text": text,
207                    "language": language
208                });
209
210                if context_after > 0 || context_before > 0 {
211                    let lines: Vec<&str> = content.lines().collect();
212                    let start_line = line.saturating_sub(context_before as usize);
213                    let end_line = (line + context_after as usize + 1).min(lines.len());
214
215                    let context_lines: Vec<String> = lines[start_line..end_line]
216                        .iter()
217                        .map(|s: &&str| s.to_string())
218                        .collect();
219
220                    match_obj["context_lines"] = serde_json::json!(context_lines);
221                }
222
223                all_matches.push(match_obj);
224            }
225        } else {
226            let mut builder = WalkBuilder::new(path);
227
228            if let Some(ref glob_patterns) = globs {
229                let mut override_builder = ignore::overrides::OverrideBuilder::new(path);
230                for glob in glob_patterns {
231                    if let Err(e) = override_builder.add(glob) {
232                        return Err(AgentError::ToolError(format!(
233                            "Invalid glob pattern '{}': {}",
234                            glob, e
235                        )));
236                    }
237                }
238                if let Ok(overrides) = override_builder.build() {
239                    builder.overrides(overrides);
240                }
241            }
242
243            let mut files_walked = 0usize;
244            let mut files_matched_lang = 0usize;
245            let mut files_rejected_lang = 0usize;
246            let mut files_no_extension = 0usize;
247            let mut files_read_errors = 0usize;
248
249            for entry in builder.build().filter_map(|e| e.ok()) {
250                if entry.file_type().is_some_and(|ft| ft.is_file()) {
251                    files_walked += 1;
252                    let file_path = entry.path();
253
254                    if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
255                        let ext_lower = ext.to_lowercase();
256                        let lang_str = lang.to_string().to_lowercase();
257
258                        let matches_lang = match lang_str.as_str() {
259                            "rust" => ext_lower == "rs",
260                            "python" => ext_lower == "py",
261                            "javascript" => ext_lower == "js",
262                            "typescript" => ext_lower == "ts",
263                            "tsx" => ext_lower == "tsx",
264                            "go" => ext_lower == "go",
265                            "java" => ext_lower == "java",
266                            "c" => ext_lower == "c",
267                            "cpp" => ext_lower == "cpp" || ext_lower == "cc" || ext_lower == "cxx",
268                            "csharp" => ext_lower == "cs",
269                            "ruby" => ext_lower == "rb",
270                            "php" => ext_lower == "php",
271                            "swift" => ext_lower == "swift",
272                            "kotlin" => ext_lower == "kt",
273                            "scala" => ext_lower == "scala",
274                            "haskell" => ext_lower == "hs",
275                            "lua" => ext_lower == "lua",
276                            "elixir" => ext_lower == "ex",
277                            "nix" => ext_lower == "nix",
278                            "solidity" => ext_lower == "sol",
279                            "bash" => ext_lower == "sh" || ext_lower == "bash",
280                            "yaml" => ext_lower == "yaml" || ext_lower == "yml",
281                            "json" => ext_lower == "json",
282                            "html" => ext_lower == "html" || ext_lower == "htm",
283                            "css" => ext_lower == "css",
284                            _ => false,
285                        };
286
287                        if files_rejected_lang < 3 && ext_lower == "rs" {
288                            debug!(
289                                "ast_grep: file={}, ext={}, lang={}, matches_lang={}",
290                                file_path.display(),
291                                ext_lower,
292                                lang_str,
293                                matches_lang
294                            );
295                        }
296
297                        if !matches_lang {
298                            files_rejected_lang += 1;
299                            continue;
300                        }
301                    } else {
302                        files_no_extension += 1;
303                        continue;
304                    }
305
306                    files_matched_lang += 1;
307                    let content = match fs::read_to_string(file_path) {
308                        Ok(c) => c,
309                        Err(e) => {
310                            files_read_errors += 1;
311                            debug!("ast_grep: failed to read {}: {}", file_path.display(), e);
312                            continue;
313                        }
314                    };
315
316                    let grep = lang.ast_grep(&content);
317
318                    for match_ in grep.root().find_all(&search_pattern) {
319                        let line = match_.start_pos().line();
320                        let text = match_.text();
321                        let display_path = file_path.display().to_string();
322
323                        let mut match_obj = serde_json::json!({
324                            "file": display_path,
325                            "line": line,
326                            "text": text,
327                            "language": language
328                        });
329
330                        if context_after > 0 || context_before > 0 {
331                            let lines: Vec<&str> = content.lines().collect();
332                            let start_line = line.saturating_sub(context_before as usize);
333                            let end_line = (line + context_after as usize + 1).min(lines.len());
334
335                            let context_lines: Vec<String> = lines[start_line..end_line]
336                                .iter()
337                                .map(|s: &&str| s.to_string())
338                                .collect();
339
340                            match_obj["context_lines"] = serde_json::json!(context_lines);
341                        }
342
343                        all_matches.push(match_obj);
344                    }
345                }
346            }
347            debug!(
348                "ast_grep search stats: files_walked={}, files_matched_lang={}, files_rejected_lang={}, files_no_extension={}, files_read_errors={}, matches_found={}",
349                files_walked, files_matched_lang, files_rejected_lang, files_no_extension, files_read_errors, all_matches.len()
350            );
351        }
352
353        Ok(serde_json::json!({
354            "matches": all_matches,
355            "count": all_matches.len(),
356            "pattern": pattern,
357            "language": language,
358            "command": "search"
359        }))
360    }
361
362    async fn execute_replace(&self, args: Value) -> Result<Value, AgentError> {
363        let pattern: String = serde_json::from_value(args["pattern"].clone())
364            .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
365
366        if pattern.trim().is_empty() {
367            return Err(AgentError::ToolError(
368                "pattern argument cannot be empty".to_string(),
369            ));
370        }
371
372        let language: String = serde_json::from_value(args["language"].clone())
373            .map_err(|e| AgentError::ToolError(format!("Invalid language argument: {}", e)))?;
374
375        let rewrite: String = serde_json::from_value(args["rewrite"].clone())
376            .map_err(|e| AgentError::ToolError(format!("Invalid rewrite argument: {}", e)))?;
377
378        if rewrite.trim().is_empty() {
379            return Err(AgentError::ToolError(
380                "rewrite argument cannot be empty".to_string(),
381            ));
382        }
383
384        let lang = Self::get_language_support(&language)?;
385
386        let default_path = std::env::current_dir()
387            .unwrap_or_else(|_| PathBuf::from("."))
388            .to_string_lossy()
389            .to_string();
390        let path = args
391            .get("path")
392            .and_then(|v| v.as_str())
393            .unwrap_or(&default_path);
394
395        let dry_run = args
396            .get("dry_run")
397            .and_then(|v| v.as_bool())
398            .unwrap_or(false);
399
400        let path_obj = Path::new(path);
401        if !path_obj.exists() {
402            return Err(AgentError::ToolError(format!("Path not found: {}", path)));
403        }
404
405        let globs: Option<Vec<String>> = args.get("globs").and_then(|v| {
406            v.as_array().map(|arr| {
407                arr.iter()
408                    .filter_map(|val| val.as_str().map(String::from))
409                    .collect()
410            })
411        });
412
413        let mut all_matches = Vec::new();
414
415        if path_obj.is_file() {
416            let content = fs::read_to_string(path_obj)
417                .map_err(|e| AgentError::ToolError(format!("Failed to read file: {}", e)))?;
418
419            let search_pattern = Pattern::try_new(&pattern, lang)
420                .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
421
422            let grep = lang.ast_grep(&content);
423
424            for match_ in grep.root().find_all(&search_pattern) {
425                let text = match_.text();
426                all_matches.push(serde_json::json!({
427                    "file": path,
428                    "text": text
429                }));
430            }
431
432            if !all_matches.is_empty() && !dry_run {
433                let mut content = content;
434                loop {
435                    let mut grep = lang.ast_grep(&content);
436                    let replaced =
437                        grep.replace(pattern.as_str(), rewrite.as_str())
438                            .map_err(|e| {
439                                AgentError::ToolError(format!("Failed to apply pattern: {}", e))
440                            })?;
441                    if !replaced {
442                        break;
443                    }
444                    content = grep.generate();
445                }
446                fs::write(path_obj, content)
447                    .map_err(|e| AgentError::ToolError(format!("Failed to write file: {}", e)))?;
448            }
449        } else {
450            let mut builder = WalkBuilder::new(path);
451
452            if let Some(ref glob_patterns) = globs {
453                let mut override_builder = ignore::overrides::OverrideBuilder::new(path);
454                for glob in glob_patterns {
455                    if let Err(e) = override_builder.add(glob) {
456                        return Err(AgentError::ToolError(format!(
457                            "Invalid glob pattern '{}': {}",
458                            glob, e
459                        )));
460                    }
461                }
462                if let Ok(overrides) = override_builder.build() {
463                    builder.overrides(overrides);
464                }
465            }
466
467            for entry in builder.build().filter_map(|e| e.ok()) {
468                if entry.file_type().is_some_and(|ft| ft.is_file()) {
469                    let file_path = entry.path();
470
471                    if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
472                        let ext_lower = ext.to_lowercase();
473                        let lang_str = lang.to_string().to_lowercase();
474
475                        let matches_lang = match lang_str.as_str() {
476                            "rust" => ext_lower == "rs",
477                            "python" => ext_lower == "py",
478                            "javascript" => ext_lower == "js",
479                            "typescript" => ext_lower == "ts",
480                            "tsx" => ext_lower == "tsx",
481                            "go" => ext_lower == "go",
482                            "java" => ext_lower == "java",
483                            "c" => ext_lower == "c",
484                            "cpp" => ext_lower == "cpp" || ext_lower == "cc" || ext_lower == "cxx",
485                            "csharp" => ext_lower == "cs",
486                            "ruby" => ext_lower == "rb",
487                            "php" => ext_lower == "php",
488                            "swift" => ext_lower == "swift",
489                            "kotlin" => ext_lower == "kt",
490                            "scala" => ext_lower == "scala",
491                            "haskell" => ext_lower == "hs",
492                            "lua" => ext_lower == "lua",
493                            "elixir" => ext_lower == "ex",
494                            "nix" => ext_lower == "nix",
495                            "solidity" => ext_lower == "sol",
496                            "bash" => ext_lower == "sh" || ext_lower == "bash",
497                            "yaml" => ext_lower == "yaml" || ext_lower == "yml",
498                            "json" => ext_lower == "json",
499                            "html" => ext_lower == "html" || ext_lower == "htm",
500                            "css" => ext_lower == "css",
501                            _ => false,
502                        };
503
504                        if !matches_lang {
505                            continue;
506                        }
507                    }
508
509                    let display_path = file_path.display().to_string();
510                    let content = match fs::read_to_string(file_path) {
511                        Ok(c) => c,
512                        Err(_) => continue,
513                    };
514
515                    let search_pattern = Pattern::try_new(&pattern, lang)
516                        .map_err(|e| AgentError::ToolError(format!("Invalid pattern: {}", e)))?;
517
518                    let grep = lang.ast_grep(&content);
519
520                    let file_matches: Vec<serde_json::Value> = grep
521                        .root()
522                        .find_all(&search_pattern)
523                        .map(|match_| {
524                            let text = match_.text();
525                            serde_json::json!({
526                                "file": display_path,
527                                "text": text
528                            })
529                        })
530                        .collect();
531
532                    if !file_matches.is_empty() && !dry_run {
533                        let mut file_content = content;
534                        loop {
535                            let mut grep = lang.ast_grep(&file_content);
536                            let replaced = grep
537                                .replace(pattern.as_str(), rewrite.as_str())
538                                .map_err(|e| {
539                                    AgentError::ToolError(format!("Failed to apply pattern: {}", e))
540                                })?;
541                            if !replaced {
542                                break;
543                            }
544                            file_content = grep.generate();
545                        }
546                        if let Err(e) = fs::write(file_path, file_content) {
547                            return Err(AgentError::ToolError(format!(
548                                "Failed to write file {}: {}",
549                                display_path, e
550                            )));
551                        }
552                    }
553
554                    all_matches.extend(file_matches);
555                }
556            }
557        }
558
559        Ok(serde_json::json!({
560            "matches": all_matches,
561            "count": all_matches.len(),
562            "pattern": pattern,
563            "language": language,
564            "rewrite": rewrite,
565            "dry_run": dry_run,
566            "command": "replace"
567        }))
568    }
569
570    async fn execute_scan(&self, _args: Value) -> Result<Value, AgentError> {
571        Err(AgentError::ToolError(
572            "scan command is not yet supported via ast-grep crates. Please use the search or replace commands.".to_string(),
573        ))
574    }
575}
576
577impl Default for AstGrepTool {
578    fn default() -> Self {
579        Self::new()
580    }
581}
582
583#[async_trait]
584impl Tool for AstGrepTool {
585    fn name(&self) -> &str {
586        "ast_grep"
587    }
588
589    async fn execute(&self, args: Value) -> Result<Value, AgentError> {
590        let command = args
591            .get("command")
592            .and_then(|v| v.as_str())
593            .unwrap_or("search");
594
595        debug!(
596            "ast_grep invoked: command={}, pattern={:?}, language={:?}, path={:?}",
597            command,
598            args.get("pattern").and_then(|v| v.as_str()),
599            args.get("language").and_then(|v| v.as_str()),
600            args.get("path").and_then(|v| v.as_str())
601        );
602
603        let result = match command {
604            "search" => self.execute_search(args).await,
605            "replace" => self.execute_replace(args).await,
606            "scan" => self.execute_scan(args).await,
607            _ => Err(AgentError::ToolError(format!(
608                "Unsupported command: {}. Supported: search, replace, scan",
609                command
610            ))),
611        };
612
613        match &result {
614            Ok(value) => {
615                if let Some(obj) = value.as_object() {
616                    let count = obj.get("count").and_then(|v| v.as_u64()).unwrap_or(0);
617                    info!("ast_grep result: {} matches", count);
618                } else {
619                    info!("ast_grep result: {:?}", value);
620                }
621            }
622            Err(e) => debug!("ast_grep error: {}", e),
623        }
624
625        result
626    }
627}
628
629pub struct LspTool;
630
631impl LspTool {
632    pub fn new() -> Self {
633        LspTool
634    }
635
636    fn get_lsp_server(file_path: &Path) -> Result<String, AgentError> {
637        let extension = file_path
638            .extension()
639            .and_then(|ext| ext.to_str())
640            .unwrap_or("");
641
642        match extension {
643            "rs" => Ok("rust-analyzer".to_string()),
644            "ts" | "tsx" | "js" | "jsx" => Ok("typescript-language-server".to_string()),
645            "py" => Ok("pylsp".to_string()),
646            _ => Err(AgentError::ToolError(format!(
647                "Unsupported file extension: {}. Supported: rs, ts, tsx, js, jsx, py",
648                extension
649            ))),
650        }
651    }
652
653    fn check_lsp_server_available(server_name: &str) -> Result<(), AgentError> {
654        let result = Command::new(server_name).arg("--version").output();
655
656        match result {
657            Ok(output) if output.status.success() => Ok(()),
658            Ok(_) => Err(AgentError::ToolError(format!(
659                "LSP server {} failed to execute",
660                server_name
661            ))),
662            Err(_) => Err(AgentError::ToolError(format!(
663                "LSP server {} not found in PATH. Please install it to use LSP features.",
664                server_name
665            ))),
666        }
667    }
668}
669
670impl Default for LspTool {
671    fn default() -> Self {
672        Self::new()
673    }
674}
675
676#[async_trait]
677impl Tool for LspTool {
678    fn name(&self) -> &str {
679        "lsp"
680    }
681
682    async fn execute(&self, args: Value) -> Result<Value, AgentError> {
683        let command: String = serde_json::from_value(args["command"].clone())
684            .map_err(|e| AgentError::ToolError(format!("Invalid command argument: {}", e)))?;
685
686        // Validate command first
687        match command.as_str() {
688            "goto_definition" | "find_references" => {}
689            _ => {
690                return Err(AgentError::ToolError(format!(
691                    "Unsupported LSP command: {}. Supported: goto_definition, find_references",
692                    command
693                )));
694            }
695        }
696
697        let file_path: String = serde_json::from_value(args["file_path"].clone())
698            .map_err(|e| AgentError::ToolError(format!("Invalid file_path argument: {}", e)))?;
699
700        if !Path::new(&file_path).exists() {
701            return Err(AgentError::ToolError(format!(
702                "File not found: {}",
703                file_path
704            )));
705        }
706
707        let position: Position = serde_json::from_value(args["position"].clone())
708            .map_err(|e| AgentError::ToolError(format!("Invalid position argument: {}", e)))?;
709        let lsp_server = Self::get_lsp_server(Path::new(&file_path))?;
710        Self::check_lsp_server_available(&lsp_server)?;
711
712        // For now, return a mock response
713        // Full LSP protocol implementation would require a proper LSP client
714        //
715        // For rust-analyzer: use rust-analyzer proc-macro
716        // For typescript: use tsserver
717        // For python: use pylsp
718        match command.as_str() {
719            "goto_definition" => Ok(serde_json::json!({
720                "command": command,
721                "file_path": file_path,
722                "position": position,
723                "result": "LSP goto_definition requires full LSP client implementation",
724                "note": "This is a placeholder. Implement full LSP client for production use."
725            })),
726            "find_references" => Ok(serde_json::json!({
727                "command": command,
728                "file_path": file_path,
729                "position": position,
730                "result": "LSP find_references requires full LSP client implementation",
731                "note": "This is a placeholder. Implement full LSP client for production use."
732            })),
733            _ => unreachable!(),
734        }
735    }
736}
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use std::io::Write;
741    use tempfile::NamedTempFile;
742
743    #[tokio::test]
744    async fn test_grep_tool_name() {
745        let tool = GrepTool::new();
746        assert_eq!(tool.name(), "grep");
747    }
748
749    #[tokio::test]
750    async fn test_grep_tool_default() {
751        let tool = GrepTool;
752        assert_eq!(tool.name(), "grep");
753    }
754
755    #[tokio::test]
756    async fn test_grep_tool_empty_pattern() {
757        let tool = GrepTool::new();
758        let args = serde_json::json!({
759            "pattern": ""
760        });
761
762        let result = tool.execute(args).await;
763        assert!(result.is_err());
764        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
765    }
766
767    #[tokio::test]
768    async fn test_lsp_tool_unsupported_extension() {
769        let tool = LspTool::new();
770
771        let mut temp_file = NamedTempFile::new().unwrap();
772        writeln!(temp_file, "test").unwrap();
773
774        let args = serde_json::json!({
775            "command": "goto_definition",
776            "file_path": temp_file.path(),
777            "position": {"line": 1, "character": 0}
778        });
779
780        let result = tool.execute(args).await;
781        assert!(result.is_err());
782        assert!(result
783            .unwrap_err()
784            .to_string()
785            .contains("Unsupported file extension"));
786    }
787
788    #[tokio::test]
789    async fn test_lsp_tool_missing_server() {
790        let tool = LspTool::new();
791
792        // Create a Rust file
793        let temp_dir = tempfile::tempdir().unwrap();
794        let rust_file = temp_dir.path().join("test.rs");
795        std::fs::write(&rust_file, "fn main() {}").unwrap();
796
797        let args = serde_json::json!({
798            "command": "goto_definition",
799            "file_path": rust_file,
800            "position": {"line": 0, "character": 0}
801        });
802        // This will likely fail because rust-analyzer is not installed in test environment
803        // But we test the parsing logic
804        let result = tool.execute(args).await;
805
806        // Expect either success (if rust-analyzer is installed) or error about missing server
807        match result {
808            Ok(value) => {
809                // LSP server is available
810                assert!(value["command"] == "goto_definition");
811            }
812            Err(e) => {
813                let error_msg = e.to_string();
814                assert!(
815                    error_msg.contains("not found in PATH")
816                        || error_msg.contains("failed to execute"),
817                    "Unexpected error: {}",
818                    error_msg
819                );
820            }
821        }
822    }
823
824    #[tokio::test]
825    async fn test_ast_grep_search_single_file() {
826        let tool = AstGrepTool::new();
827
828        let mut temp_file = NamedTempFile::new().unwrap();
829        writeln!(temp_file, "fn foo() {{}}").unwrap();
830        writeln!(temp_file, "fn bar() {{}}").unwrap();
831        temp_file.flush().unwrap();
832
833        let args = serde_json::json!({
834            "pattern": "fn $NAME() {}",
835            "language": "rust",
836            "path": temp_file.path()
837        });
838
839        let result = tool.execute(args).await;
840        assert!(result.is_ok());
841        let value = result.unwrap();
842        assert_eq!(value["count"], 2);
843        assert_eq!(value["matches"].as_array().unwrap().len(), 2);
844        assert_eq!(value["command"], "search");
845    }
846
847    #[tokio::test]
848    async fn test_ast_grep_search_multi_file() {
849        let tool = AstGrepTool::new();
850
851        let mut temp_file1 = NamedTempFile::new().unwrap();
852        writeln!(temp_file1, "fn foo() {{}}").unwrap();
853        writeln!(temp_file1, "fn bar() {{}}").unwrap();
854        temp_file1.flush().unwrap();
855
856        let mut temp_file2 = NamedTempFile::new().unwrap();
857        writeln!(temp_file2, "fn baz() {{}}").unwrap();
858        temp_file2.flush().unwrap();
859
860        let args1 = serde_json::json!({
861            "pattern": "fn $NAME() {}",
862            "language": "rust",
863            "path": temp_file1.path()
864        });
865
866        let args2 = serde_json::json!({
867            "pattern": "fn $NAME() {}",
868            "language": "rust",
869            "path": temp_file2.path()
870        });
871
872        let result1 = tool.execute(args1).await;
873        let result2 = tool.execute(args2).await;
874
875        assert!(result1.is_ok());
876        assert!(result2.is_ok());
877
878        let value1 = result1.unwrap();
879        let value2 = result2.unwrap();
880
881        let count1 = value1["count"].as_u64().unwrap_or(0);
882        let count2 = value2["count"].as_u64().unwrap_or(0);
883
884        assert_eq!(count1, 2);
885        assert_eq!(count2, 1);
886    }
887
888    #[tokio::test]
889    async fn test_ast_grep_search_with_globs() {
890        let tool = AstGrepTool::new();
891
892        let mut temp_file = NamedTempFile::new().unwrap();
893        writeln!(temp_file, "fn foo() {{}}").unwrap();
894        writeln!(temp_file, "fn bar() {{}}").unwrap();
895        temp_file.flush().unwrap();
896
897        let args = serde_json::json!({
898            "pattern": "fn $NAME() {}",
899            "language": "rust",
900            "path": temp_file.path(),
901            "globs": ["*.rs"]
902        });
903
904        let result = tool.execute(args).await;
905        assert!(result.is_ok());
906        let value = result.unwrap();
907        assert_eq!(value["count"], 2);
908    }
909
910    #[tokio::test]
911    async fn test_ast_grep_search_no_match() {
912        let tool = AstGrepTool::new();
913
914        let mut temp_file = NamedTempFile::new().unwrap();
915        writeln!(temp_file, "fn foo() {{}}").unwrap();
916        temp_file.flush().unwrap();
917
918        let args = serde_json::json!({
919            "pattern": "fn bar() {}",
920            "language": "rust",
921            "path": temp_file.path()
922        });
923
924        let result = tool.execute(args).await;
925        assert!(result.is_ok());
926        let value = result.unwrap();
927        assert_eq!(value["count"], 0);
928        assert_eq!(value["matches"].as_array().unwrap().len(), 0);
929    }
930
931    #[tokio::test]
932    async fn test_ast_grep_replace_dry_run() {
933        let tool = AstGrepTool::new();
934
935        let mut temp_file = NamedTempFile::new().unwrap();
936        writeln!(temp_file, "var x = 1;").unwrap();
937        writeln!(temp_file, "var y = 2;").unwrap();
938        temp_file.flush().unwrap();
939        let path = temp_file.path().to_path_buf();
940
941        let args = serde_json::json!({
942            "command": "replace",
943            "pattern": "var $A = $B;",
944            "rewrite": "let $A = $B;",
945            "language": "javascript",
946            "path": &path,
947            "dry_run": true
948        });
949
950        let result = tool.execute(args).await;
951        assert!(result.is_ok());
952        let value = result.unwrap();
953        assert_eq!(value["count"], 2);
954        assert_eq!(value["dry_run"], true);
955
956        let content = fs::read_to_string(&path).unwrap();
957        assert!(content.contains("var x = 1;"));
958        assert!(content.contains("var y = 2;"));
959    }
960
961    #[tokio::test]
962    async fn test_ast_grep_replace_writes_file() {
963        let tool = AstGrepTool::new();
964
965        let mut temp_file = NamedTempFile::new().unwrap();
966        writeln!(temp_file, "var x = 1;").unwrap();
967        writeln!(temp_file, "var y = 2;").unwrap();
968        temp_file.flush().unwrap();
969        let path = temp_file.path().to_path_buf();
970
971        let args = serde_json::json!({
972            "command": "replace",
973            "pattern": "var $A = $B;",
974            "rewrite": "let $A = $B;",
975            "language": "javascript",
976            "path": &path,
977            "dry_run": false
978        });
979
980        let result = tool.execute(args).await;
981        assert!(result.is_ok());
982        let value = result.unwrap();
983        assert_eq!(value["count"], 2);
984        assert_eq!(value["dry_run"], false);
985
986        let content = fs::read_to_string(&path).unwrap();
987        assert!(content.contains("let x = 1;"));
988        assert!(content.contains("let y = 2;"));
989        assert!(!content.contains("var x = 1;"));
990        assert!(!content.contains("var y = 2;"));
991    }
992
993    #[tokio::test]
994    async fn test_ast_grep_language_case_insensitive() {
995        let tool = AstGrepTool::new();
996
997        let mut temp_file = NamedTempFile::new().unwrap();
998        writeln!(temp_file, "fn foo() {{}}").unwrap();
999        temp_file.flush().unwrap();
1000
1001        for lang in ["RUST", "Rust", "rust"] {
1002            let args = serde_json::json!({
1003                "pattern": "fn $NAME() {}",
1004                "language": lang,
1005                "path": temp_file.path()
1006            });
1007
1008            let result = tool.execute(args).await;
1009            assert!(result.is_ok(), "Failed for language: {}", lang);
1010            let value = result.unwrap();
1011            assert_eq!(value["count"], 1);
1012        }
1013    }
1014
1015    #[tokio::test]
1016    async fn test_all_tools_implement_default() {
1017        let _grep = GrepTool;
1018        let _ast_grep = AstGrepTool;
1019        let _lsp = LspTool;
1020    }
1021
1022    #[tokio::test]
1023    async fn test_position_deserialize() {
1024        let json = serde_json::json!({"line": 10, "character": 5});
1025        let pos: Position = serde_json::from_value(json).unwrap();
1026        assert_eq!(pos.line, 10);
1027        assert_eq!(pos.character, 5);
1028    }
1029
1030    #[tokio::test]
1031    async fn test_ast_grep_tool_new_language_go() {
1032        let tool = AstGrepTool::new();
1033
1034        let mut temp_file = NamedTempFile::with_suffix(".go").unwrap();
1035        writeln!(temp_file, "func foo() {{}}").unwrap();
1036        writeln!(temp_file, "func bar() {{}}").unwrap();
1037        temp_file.flush().unwrap();
1038
1039        let args = serde_json::json!({
1040            "pattern": "func $NAME($$$) { }",
1041            "language": "go",
1042            "path": temp_file.path()
1043        });
1044
1045        let result = tool.execute(args).await;
1046        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1047        let value = result.unwrap();
1048        assert_eq!(value["count"], 2);
1049        assert_eq!(value["command"], "search");
1050    }
1051
1052    #[tokio::test]
1053    async fn test_ast_grep_tool_language_alias_js() {
1054        let tool = AstGrepTool::new();
1055
1056        let mut temp_file = NamedTempFile::with_suffix(".js").unwrap();
1057        writeln!(temp_file, "console.log('hello');").unwrap();
1058        writeln!(temp_file, "console.log('world');").unwrap();
1059        temp_file.flush().unwrap();
1060
1061        let args = serde_json::json!({
1062            "pattern": "console.log($X)",
1063            "language": "js",
1064            "path": temp_file.path()
1065        });
1066
1067        let result = tool.execute(args).await;
1068        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1069        let value = result.unwrap();
1070        assert_eq!(value["count"], 2);
1071        assert_eq!(value["command"], "search");
1072    }
1073
1074    #[tokio::test]
1075    async fn test_ast_grep_tool_language_alias_py() {
1076        let tool = AstGrepTool::new();
1077
1078        let mut temp_file = NamedTempFile::with_suffix(".py").unwrap();
1079        writeln!(temp_file, "def foo():").unwrap();
1080        writeln!(temp_file, "def bar():").unwrap();
1081        temp_file.flush().unwrap();
1082
1083        let args = serde_json::json!({
1084            "pattern": "def $FUNC():",
1085            "language": "py",
1086            "path": temp_file.path()
1087        });
1088
1089        let result = tool.execute(args).await;
1090        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1091        let value = result.unwrap();
1092        assert_eq!(value["count"], 2);
1093        assert_eq!(value["command"], "search");
1094    }
1095
1096    #[tokio::test]
1097    async fn test_ast_grep_tool_language_alias_rs() {
1098        let tool = AstGrepTool::new();
1099
1100        let mut temp_file = NamedTempFile::with_suffix(".rs").unwrap();
1101        writeln!(temp_file, "fn foo() {{}}").unwrap();
1102        writeln!(temp_file, "fn bar() {{}}").unwrap();
1103        temp_file.flush().unwrap();
1104
1105        let args = serde_json::json!({
1106            "pattern": "fn $NAME() {}",
1107            "language": "rs",
1108            "path": temp_file.path()
1109        });
1110
1111        let result = tool.execute(args).await;
1112        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1113        let value = result.unwrap();
1114        assert_eq!(value["count"], 2);
1115        assert_eq!(value["command"], "search");
1116    }
1117
1118    #[tokio::test]
1119    async fn test_ast_grep_tool_unsupported_command() {
1120        let tool = AstGrepTool::new();
1121        let args = serde_json::json!({
1122            "command": "test",
1123            "pattern": "fn main()",
1124            "language": "rust"
1125        });
1126
1127        let result = tool.execute(args).await;
1128        assert!(result.is_err());
1129        assert!(result
1130            .unwrap_err()
1131            .to_string()
1132            .contains("Unsupported command"));
1133    }
1134
1135    #[tokio::test]
1136    async fn test_ast_grep_tool_replace_missing_rewrite() {
1137        let tool = AstGrepTool::new();
1138        let args = serde_json::json!({
1139            "command": "replace",
1140            "pattern": "console.log($X)",
1141            "language": "javascript"
1142        });
1143
1144        let result = tool.execute(args).await;
1145        assert!(result.is_err());
1146    }
1147
1148    #[tokio::test]
1149    async fn test_ast_grep_tool_scan_path_not_found() {
1150        let tool = AstGrepTool::new();
1151        let args = serde_json::json!({
1152            "command": "scan",
1153            "path": "/nonexistent/path"
1154        });
1155
1156        let result = tool.execute(args).await;
1157        assert!(result.is_err());
1158        assert!(result
1159            .unwrap_err()
1160            .to_string()
1161            .contains("not yet supported"));
1162    }
1163
1164    #[tokio::test]
1165    async fn test_ast_grep_tool_backward_compat_no_command() {
1166        let tool = AstGrepTool::new();
1167
1168        let mut temp_file = NamedTempFile::new().unwrap();
1169        writeln!(temp_file, "fn foo() {{}}").unwrap();
1170        writeln!(temp_file, "fn bar() {{}}").unwrap();
1171        temp_file.flush().unwrap();
1172
1173        let args = serde_json::json!({
1174            "pattern": "fn $NAME() {}",
1175            "language": "rust",
1176            "path": temp_file.path()
1177        });
1178
1179        let result = tool.execute(args).await;
1180        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1181        let value = result.unwrap();
1182        assert_eq!(value["command"], "search");
1183        assert_eq!(value["count"], 2);
1184    }
1185
1186    #[tokio::test]
1187    async fn test_ast_grep_search_directory() {
1188        // Regression test: SupportLang::Rust.to_string() returns "Rust" (capitalized),
1189        // but extension matching compared against "rust" (lowercase).
1190        // This test ensures directory searches work correctly.
1191        let tool = AstGrepTool::new();
1192
1193        let temp_dir = tempfile::tempdir().unwrap();
1194        let file1 = temp_dir.path().join("test1.rs");
1195        let file2 = temp_dir.path().join("test2.rs");
1196
1197        fs::write(&file1, "fn foo() {}\nfn bar() {}").unwrap();
1198        fs::write(&file2, "fn baz() {}").unwrap();
1199
1200        let args = serde_json::json!({
1201            "pattern": "fn $NAME() {}",
1202            "language": "rust",
1203            "path": temp_dir.path()
1204        });
1205
1206        let result = tool.execute(args).await;
1207        assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err());
1208        let value = result.unwrap();
1209        assert_eq!(value["count"], 3, "Should find 3 functions in directory");
1210    }
1211}