Skip to main content

opendev_tools_impl/file_search/
ast_grep_tool.rs

1//! AstGrepTool — structural code search using ast-grep.
2
3use std::collections::HashMap;
4
5use opendev_tools_core::{BaseTool, ToolContext, ToolResult};
6
7use super::types::AstGrepArgs;
8use crate::dir_hints::list_available_dirs;
9use crate::path_utils::{resolve_dir_path, validate_path_access};
10
11/// Tool for structural code search using ast-grep.
12#[derive(Debug)]
13pub struct AstGrepTool;
14
15#[async_trait::async_trait]
16impl BaseTool for AstGrepTool {
17    fn name(&self) -> &str {
18        "ast_grep"
19    }
20
21    fn description(&self) -> &str {
22        "Search code structurally using AST patterns via ast-grep. \
23         Use $VAR wildcards for structural matching (e.g., \"$A && $A()\"). \
24         $$$VAR matches multiple nodes (e.g., \"fn $NAME() { $$$BODY }\")."
25    }
26
27    fn parameter_schema(&self) -> serde_json::Value {
28        serde_json::json!({
29            "type": "object",
30            "properties": {
31                "pattern": {
32                    "type": "string",
33                    "description": "AST pattern with $VAR wildcards for structural matching"
34                },
35                "path": {
36                    "type": "string",
37                    "description": "File or directory to search in (defaults to working directory)"
38                },
39                "lang": {
40                    "type": "string",
41                    "description": "Language hint (e.g., 'rust', 'javascript', 'python'). Auto-detected from file extension if not specified."
42                },
43                "head_limit": {
44                    "type": "number",
45                    "description": "Limit output to first N matches"
46                }
47            },
48            "required": ["pattern"]
49        })
50    }
51
52    async fn execute(
53        &self,
54        args: HashMap<String, serde_json::Value>,
55        ctx: &ToolContext,
56    ) -> ToolResult {
57        let ast_args = match AstGrepArgs::from_map(&args) {
58            Ok(a) => a,
59            Err(e) => return ToolResult::fail(e),
60        };
61
62        let search_path = ast_args
63            .path
64            .as_deref()
65            .map(|p| resolve_dir_path(p, &ctx.working_dir))
66            .unwrap_or_else(|| ctx.working_dir.clone());
67
68        if let Err(msg) = validate_path_access(&search_path, &ctx.working_dir) {
69            return ToolResult::fail(msg);
70        }
71
72        if !search_path.exists() {
73            let available = list_available_dirs(&ctx.working_dir);
74            return ToolResult::fail(format!(
75                "Path not found: {}\n\nAvailable directories in working dir ({}):\n{}",
76                search_path.display(),
77                ctx.working_dir.display(),
78                available
79            ));
80        }
81
82        self.run_ast_grep(&ast_args, &search_path).await
83    }
84}