tree-sitter-analyzer 0.9.5__py3-none-any.whl → 0.9.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Query Tool for MCP
4
+
5
+ MCP tool providing tree-sitter query functionality using unified QueryService.
6
+ Supports both predefined query keys and custom query strings.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ...core.query_service import QueryService
13
+ from ...language_detector import detect_language_from_file
14
+ from ...security import SecurityValidator
15
+ from ..utils.error_handler import handle_mcp_errors
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class QueryTool:
21
+ """MCP query tool providing tree-sitter query functionality"""
22
+
23
+ def __init__(self, project_root: str | None = None) -> None:
24
+ """Initialize query tool"""
25
+ self.project_root = project_root
26
+ self.query_service = QueryService(project_root)
27
+ self.security_validator = SecurityValidator(project_root)
28
+
29
+ def get_tool_definition(self) -> dict[str, Any]:
30
+ """
31
+ Get MCP tool definition
32
+
33
+ Returns:
34
+ Tool definition dictionary
35
+ """
36
+ return {
37
+ "name": "query_code",
38
+ "description": "Execute tree-sitter queries on code files to extract specific code elements",
39
+ "inputSchema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "file_path": {
43
+ "type": "string",
44
+ "description": "Path to the code file to query (relative to project root)",
45
+ },
46
+ "language": {
47
+ "type": "string",
48
+ "description": "Programming language (optional, auto-detected if not provided)",
49
+ },
50
+ "query_key": {
51
+ "type": "string",
52
+ "description": "Predefined query key (e.g., 'methods', 'class', 'functions')",
53
+ },
54
+ "query_string": {
55
+ "type": "string",
56
+ "description": "Custom tree-sitter query string (e.g., '(method_declaration) @method')",
57
+ },
58
+ "filter": {
59
+ "type": "string",
60
+ "description": "Filter expression to refine results (e.g., 'name=main', 'name=~get*,public=true')",
61
+ },
62
+ "output_format": {
63
+ "type": "string",
64
+ "enum": ["json", "summary"],
65
+ "default": "json",
66
+ "description": "Output format",
67
+ },
68
+ },
69
+ "required": ["file_path"],
70
+ "anyOf": [
71
+ {"required": ["query_key"]},
72
+ {"required": ["query_string"]},
73
+ ],
74
+ },
75
+ }
76
+
77
+ @handle_mcp_errors("query_code")
78
+ async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
79
+ """
80
+ Execute query tool
81
+
82
+ Args:
83
+ arguments: Tool arguments
84
+
85
+ Returns:
86
+ Query results
87
+ """
88
+ # Validate input parameters
89
+ file_path = arguments.get("file_path")
90
+ if not file_path:
91
+ raise ValueError("file_path is required")
92
+
93
+ # Security validation
94
+ is_valid, error_msg = self.security_validator.validate_file_path(file_path)
95
+ if not is_valid:
96
+ raise ValueError(f"Invalid or unsafe file path: {error_msg or file_path}")
97
+ # Use the original path as validated path after checks
98
+ validated_path = file_path
99
+
100
+ # Get query parameters
101
+ query_key = arguments.get("query_key")
102
+ query_string = arguments.get("query_string")
103
+ filter_expression = arguments.get("filter")
104
+ output_format = arguments.get("output_format", "json")
105
+
106
+ if not query_key and not query_string:
107
+ raise ValueError("Either query_key or query_string must be provided")
108
+
109
+ if query_key and query_string:
110
+ raise ValueError("Cannot provide both query_key and query_string")
111
+
112
+ # Detect language
113
+ language = arguments.get("language")
114
+ if not language:
115
+ language = detect_language_from_file(validated_path)
116
+ if not language:
117
+ raise ValueError(f"Could not detect language for file: {file_path}")
118
+
119
+ try:
120
+ # Execute query
121
+ results = await self.query_service.execute_query(
122
+ validated_path, language, query_key, query_string, filter_expression
123
+ )
124
+
125
+ if not results:
126
+ return {
127
+ "success": True,
128
+ "message": "No results found matching the query",
129
+ "results": [],
130
+ "count": 0,
131
+ }
132
+
133
+ # Format output
134
+ if output_format == "summary":
135
+ return self._format_summary(results, query_key or "custom", language)
136
+ else:
137
+ return {
138
+ "success": True,
139
+ "results": results,
140
+ "count": len(results),
141
+ "file_path": file_path,
142
+ "language": language,
143
+ "query": query_key or query_string,
144
+ }
145
+
146
+ except Exception as e:
147
+ logger.error(f"Query execution failed: {e}")
148
+ return {
149
+ "success": False,
150
+ "error": str(e),
151
+ "file_path": file_path,
152
+ "language": language,
153
+ }
154
+
155
+ def _format_summary(
156
+ self, results: list[dict[str, Any]], query_type: str, language: str
157
+ ) -> dict[str, Any]:
158
+ """
159
+ Format summary output
160
+
161
+ Args:
162
+ results: Query results
163
+ query_type: Query type
164
+ language: Programming language
165
+
166
+ Returns:
167
+ Summary formatted results
168
+ """
169
+ # Group by capture name
170
+ by_capture = {}
171
+ for result in results:
172
+ capture_name = result["capture_name"]
173
+ if capture_name not in by_capture:
174
+ by_capture[capture_name] = []
175
+ by_capture[capture_name].append(result)
176
+
177
+ # Create summary
178
+ summary = {
179
+ "success": True,
180
+ "query_type": query_type,
181
+ "language": language,
182
+ "total_count": len(results),
183
+ "captures": {},
184
+ }
185
+
186
+ for capture_name, items in by_capture.items():
187
+ summary["captures"][capture_name] = {
188
+ "count": len(items),
189
+ "items": [
190
+ {
191
+ "name": self._extract_name_from_content(item["content"]),
192
+ "line_range": f"{item['start_line']}-{item['end_line']}",
193
+ "node_type": item["node_type"],
194
+ }
195
+ for item in items
196
+ ],
197
+ }
198
+
199
+ return summary
200
+
201
+ def _extract_name_from_content(self, content: str) -> str:
202
+ """
203
+ Extract name from content (simple heuristic method)
204
+
205
+ Args:
206
+ content: Code content
207
+
208
+ Returns:
209
+ Extracted name
210
+ """
211
+ # Simple name extraction logic, can be improved as needed
212
+ lines = content.strip().split("\n")
213
+ if lines:
214
+ first_line = lines[0].strip()
215
+ # Extract method names, class names, etc.
216
+ import re
217
+
218
+ # Match common declaration patterns
219
+ patterns = [
220
+ r"(?:public|private|protected)?\s*(?:static)?\s*(?:class|interface)\s+(\w+)", # class/interface
221
+ r"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", # method
222
+ r"(\w+)\s*\(", # simple function call
223
+ ]
224
+
225
+ for pattern in patterns:
226
+ match = re.search(pattern, first_line)
227
+ if match:
228
+ return match.group(1)
229
+
230
+ return "unnamed"
231
+
232
+ def get_available_queries(self, language: str) -> list[str]:
233
+ """
234
+ Get available query keys
235
+
236
+ Args:
237
+ language: Programming language
238
+
239
+ Returns:
240
+ List of available query keys
241
+ """
242
+ return self.query_service.get_available_queries(language)