mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310, S306, S110, TID251, UP042, F401) and run black on 526 files. Co-authored-by: Hanzo Dev <[email protected]>
295 lines
8.1 KiB
Python
295 lines
8.1 KiB
Python
"""AST-based code structure search using tree-sitter.
|
|
|
|
This module provides the ASTTool for searching, indexing, and querying code symbols
|
|
using tree-sitter AST parsing. It can find function definitions, class declarations,
|
|
and other code structures with full context.
|
|
"""
|
|
|
|
import os
|
|
from typing import Unpack, Annotated, TypedDict, final, override
|
|
from pathlib import Path
|
|
|
|
from pydantic import Field
|
|
from mcp.server import FastMCP
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_tools.core import BaseTool
|
|
|
|
# Lazy import for grep_ast
|
|
_tree_context_cls = None
|
|
|
|
|
|
def _get_tree_context():
|
|
"""Lazy load TreeContext to avoid import-time overhead."""
|
|
global _tree_context_cls
|
|
if _tree_context_cls is None:
|
|
from grep_ast.grep_ast import TreeContext
|
|
|
|
_tree_context_cls = TreeContext
|
|
return _tree_context_cls
|
|
|
|
|
|
# Type annotations for parameters
|
|
Pattern = Annotated[
|
|
str,
|
|
Field(
|
|
description="The regex pattern to search for in source code files",
|
|
min_length=1,
|
|
),
|
|
]
|
|
|
|
SearchPath = Annotated[
|
|
str,
|
|
Field(
|
|
description="The path to search in (file or directory)",
|
|
min_length=1,
|
|
),
|
|
]
|
|
|
|
IgnoreCase = Annotated[
|
|
bool,
|
|
Field(
|
|
description="Whether to ignore case when matching",
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
LineNumber = Annotated[
|
|
bool,
|
|
Field(
|
|
description="Whether to display line numbers",
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
|
|
class ASTToolParams(TypedDict, total=False):
|
|
"""Parameters for the ASTTool.
|
|
|
|
Attributes:
|
|
pattern: The regex pattern to search for in source code files
|
|
path: The path to search in (file or directory)
|
|
ignore_case: Whether to ignore case when matching
|
|
line_number: Whether to display line numbers
|
|
"""
|
|
|
|
pattern: Pattern
|
|
path: SearchPath
|
|
ignore_case: IgnoreCase
|
|
line_number: LineNumber
|
|
|
|
|
|
# Extensions supported by tree-sitter (common programming languages)
|
|
SUPPORTED_EXTENSIONS = {
|
|
".py",
|
|
".pyw", # Python
|
|
".js",
|
|
".jsx",
|
|
".mjs",
|
|
".cjs", # JavaScript
|
|
".ts",
|
|
".tsx",
|
|
".mts",
|
|
".cts", # TypeScript
|
|
".go", # Go
|
|
".rs", # Rust
|
|
".c",
|
|
".h", # C
|
|
".cpp",
|
|
".cc",
|
|
".cxx",
|
|
".hpp",
|
|
".hh",
|
|
".hxx", # C++
|
|
".java", # Java
|
|
".rb", # Ruby
|
|
".php", # PHP
|
|
".cs", # C#
|
|
".swift", # Swift
|
|
".kt",
|
|
".kts", # Kotlin
|
|
".scala", # Scala
|
|
".lua", # Lua
|
|
".r",
|
|
".R", # R
|
|
".jl", # Julia
|
|
".ex",
|
|
".exs", # Elixir
|
|
".erl",
|
|
".hrl", # Erlang
|
|
".ml",
|
|
".mli", # OCaml
|
|
".hs", # Haskell
|
|
".elm", # Elm
|
|
".vue", # Vue
|
|
".svelte", # Svelte
|
|
}
|
|
|
|
|
|
@final
|
|
class ASTTool(BaseTool):
|
|
"""Tool for searching and querying code structures using tree-sitter AST parsing."""
|
|
|
|
name = "ast"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
"""Get the tool description.
|
|
|
|
Returns:
|
|
Tool description
|
|
"""
|
|
return """AST-based code structure search using tree-sitter. Find functions, classes, methods with full context.
|
|
|
|
Usage:
|
|
ast "function_name" ./src
|
|
ast "class.*Service" ./src
|
|
ast "def test_" ./tests
|
|
|
|
Searches code structure intelligently, understanding syntax and providing semantic context."""
|
|
|
|
def _is_supported_file(self, path: str) -> bool:
|
|
"""Check if file has a supported extension for tree-sitter parsing."""
|
|
return Path(path).suffix.lower() in SUPPORTED_EXTENSIONS
|
|
|
|
@override
|
|
async def call(
|
|
self,
|
|
ctx: MCPContext,
|
|
**params: Unpack[ASTToolParams],
|
|
) -> str:
|
|
"""Execute the tool with the given parameters.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
**params: Tool parameters
|
|
|
|
Returns:
|
|
Tool result
|
|
"""
|
|
# Extract parameters
|
|
pattern: str = params["pattern"]
|
|
path: str = params["path"]
|
|
ignore_case = params.get("ignore_case", False)
|
|
line_number = params.get("line_number", False)
|
|
|
|
# Expand ~ in path
|
|
path = os.path.expanduser(path)
|
|
|
|
# Check if path exists
|
|
path_obj = Path(path)
|
|
if not path_obj.exists():
|
|
return f"Error: Path does not exist: {path}"
|
|
|
|
# Get the files to process
|
|
files_to_process = []
|
|
|
|
if path_obj.is_file():
|
|
if self._is_supported_file(str(path_obj)):
|
|
files_to_process.append(str(path_obj))
|
|
else:
|
|
return (
|
|
f"Error: File type not supported for AST parsing: {path_obj.suffix}"
|
|
)
|
|
elif path_obj.is_dir():
|
|
for root, _, files in os.walk(path_obj):
|
|
# Skip hidden directories and common non-code directories
|
|
root_path = Path(root)
|
|
if any(part.startswith(".") for part in root_path.parts):
|
|
continue
|
|
if any(
|
|
part
|
|
in ("node_modules", "__pycache__", "venv", ".venv", "dist", "build")
|
|
for part in root_path.parts
|
|
):
|
|
continue
|
|
|
|
for file in files:
|
|
file_path = Path(root) / file
|
|
if self._is_supported_file(str(file_path)):
|
|
files_to_process.append(str(file_path))
|
|
|
|
if not files_to_process:
|
|
return f"No source code files found in {path}"
|
|
|
|
# Get TreeContext class
|
|
TreeContext = _get_tree_context()
|
|
|
|
# Process each file
|
|
results = []
|
|
errors = []
|
|
|
|
for file_path in files_to_process:
|
|
try:
|
|
# Read the file
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
code = f.read()
|
|
|
|
# Process the file with grep-ast
|
|
try:
|
|
tc = TreeContext(
|
|
file_path,
|
|
code,
|
|
color=False,
|
|
verbose=False,
|
|
line_number=line_number,
|
|
)
|
|
|
|
# Find matches
|
|
loi = tc.grep(pattern, ignore_case)
|
|
|
|
if loi:
|
|
tc.add_lines_of_interest(loi)
|
|
tc.add_context()
|
|
output = tc.format()
|
|
|
|
# Add the result to our list
|
|
results.append(f"\n{file_path}:\n{output}\n")
|
|
except Exception as e:
|
|
# Skip files that can't be parsed by tree-sitter
|
|
errors.append(f"Could not parse {file_path}: {str(e)}")
|
|
except UnicodeDecodeError:
|
|
errors.append(f"Could not read {file_path} as text")
|
|
except Exception as e:
|
|
errors.append(f"Error processing {file_path}: {str(e)}")
|
|
|
|
if not results:
|
|
error_info = ""
|
|
if errors:
|
|
error_info = f"\n\nErrors encountered:\n" + "\n".join(errors[:5])
|
|
if len(errors) > 5:
|
|
error_info += f"\n... and {len(errors) - 5} more errors"
|
|
return f"No matches found for '{pattern}' in {path}{error_info}"
|
|
|
|
summary = f"Found matches in {len(results)} file(s) (searched {len(files_to_process)} files)"
|
|
return summary + "\n" + "".join(results)
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register this tool with the MCP server.
|
|
|
|
Creates a wrapper function with explicitly defined parameters that match
|
|
the tool's parameter schema and registers it with the MCP server.
|
|
|
|
Args:
|
|
mcp_server: The FastMCP server instance
|
|
"""
|
|
tool_self = self # Create a reference to self for use in the closure
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def ast(
|
|
ctx: MCPContext,
|
|
pattern: Pattern,
|
|
path: SearchPath,
|
|
ignore_case: IgnoreCase = False,
|
|
line_number: LineNumber = False,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
pattern=pattern,
|
|
path=path,
|
|
ignore_case=ignore_case,
|
|
line_number=line_number,
|
|
)
|