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.
- tree_sitter_analyzer/__init__.py +1 -1
- tree_sitter_analyzer/cli/commands/query_command.py +50 -35
- tree_sitter_analyzer/cli_main.py +21 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +162 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +1 -2
- tree_sitter_analyzer/mcp/server.py +7 -1
- tree_sitter_analyzer/mcp/tools/query_tool.py +242 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -569
- tree_sitter_analyzer/queries/java.py +5 -0
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/METADATA +83 -13
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/RECORD +14 -11
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/entry_points.txt +0 -0
tree_sitter_analyzer/__init__.py
CHANGED
|
@@ -5,14 +5,50 @@ Query Command
|
|
|
5
5
|
Handles query execution functionality.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from ...core.query_service import QueryService
|
|
8
9
|
from ...output_manager import output_data, output_error, output_info, output_json
|
|
9
|
-
from ...query_loader import query_loader
|
|
10
10
|
from .base_command import BaseCommand
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class QueryCommand(BaseCommand):
|
|
14
14
|
"""Command for executing queries."""
|
|
15
15
|
|
|
16
|
+
def __init__(self, args):
|
|
17
|
+
"""Initialize the query command with QueryService."""
|
|
18
|
+
super().__init__(args)
|
|
19
|
+
self.query_service = QueryService()
|
|
20
|
+
|
|
21
|
+
async def execute_query(
|
|
22
|
+
self, language: str, query: str, query_name: str = "custom"
|
|
23
|
+
) -> list[dict] | None:
|
|
24
|
+
"""Execute a specific tree-sitter query using QueryService."""
|
|
25
|
+
try:
|
|
26
|
+
# Get filter expression if provided
|
|
27
|
+
filter_expression = getattr(self.args, "filter", None)
|
|
28
|
+
|
|
29
|
+
if query_name != "custom":
|
|
30
|
+
# Use predefined query key
|
|
31
|
+
results = await self.query_service.execute_query(
|
|
32
|
+
self.args.file_path,
|
|
33
|
+
language,
|
|
34
|
+
query_key=query_name,
|
|
35
|
+
filter_expression=filter_expression,
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
# Use custom query string
|
|
39
|
+
results = await self.query_service.execute_query(
|
|
40
|
+
self.args.file_path,
|
|
41
|
+
language,
|
|
42
|
+
query_string=query,
|
|
43
|
+
filter_expression=filter_expression,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return results
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
output_error(f"Query execution failed: {e}")
|
|
50
|
+
return None
|
|
51
|
+
|
|
16
52
|
async def execute_async(self, language: str) -> int:
|
|
17
53
|
# Get the query to execute
|
|
18
54
|
query_to_execute = None
|
|
@@ -22,16 +58,16 @@ class QueryCommand(BaseCommand):
|
|
|
22
58
|
sanitized_query_key = self.security_validator.sanitize_input(
|
|
23
59
|
self.args.query_key, max_length=100
|
|
24
60
|
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return 1
|
|
32
|
-
except ValueError as e:
|
|
33
|
-
output_error(f"{e}")
|
|
61
|
+
# Check if query exists
|
|
62
|
+
available_queries = self.query_service.get_available_queries(language)
|
|
63
|
+
if sanitized_query_key not in available_queries:
|
|
64
|
+
output_error(
|
|
65
|
+
f"Query '{sanitized_query_key}' not found for language '{language}'"
|
|
66
|
+
)
|
|
34
67
|
return 1
|
|
68
|
+
# Store query name - QueryService will resolve the query string
|
|
69
|
+
query_to_execute = sanitized_query_key # This is actually the query key now
|
|
70
|
+
query_name = sanitized_query_key
|
|
35
71
|
elif hasattr(self.args, "query_string") and self.args.query_string:
|
|
36
72
|
# Security check for query string (potential regex patterns)
|
|
37
73
|
is_safe, error_msg = self.security_validator.regex_checker.validate_pattern(
|
|
@@ -41,38 +77,17 @@ class QueryCommand(BaseCommand):
|
|
|
41
77
|
output_error(f"Unsafe query pattern: {error_msg}")
|
|
42
78
|
return 1
|
|
43
79
|
query_to_execute = self.args.query_string
|
|
80
|
+
query_name = "custom"
|
|
44
81
|
|
|
45
82
|
if not query_to_execute:
|
|
46
83
|
output_error("No query specified.")
|
|
47
84
|
return 1
|
|
48
85
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
if
|
|
86
|
+
# Execute specific query
|
|
87
|
+
results = await self.execute_query(language, query_to_execute, query_name)
|
|
88
|
+
if results is None:
|
|
52
89
|
return 1
|
|
53
90
|
|
|
54
|
-
# Process query results
|
|
55
|
-
results = []
|
|
56
|
-
if hasattr(analysis_result, "query_results") and analysis_result.query_results:
|
|
57
|
-
results = analysis_result.query_results.get("captures", [])
|
|
58
|
-
else:
|
|
59
|
-
# Create basic results from elements
|
|
60
|
-
if hasattr(analysis_result, "elements") and analysis_result.elements:
|
|
61
|
-
for element in analysis_result.elements:
|
|
62
|
-
results.append(
|
|
63
|
-
{
|
|
64
|
-
"capture_name": getattr(
|
|
65
|
-
element, "__class__", type(element)
|
|
66
|
-
).__name__.lower(),
|
|
67
|
-
"node_type": getattr(
|
|
68
|
-
element, "__class__", type(element)
|
|
69
|
-
).__name__,
|
|
70
|
-
"start_line": getattr(element, "start_line", 0),
|
|
71
|
-
"end_line": getattr(element, "end_line", 0),
|
|
72
|
-
"content": getattr(element, "name", str(element)),
|
|
73
|
-
}
|
|
74
|
-
)
|
|
75
|
-
|
|
76
91
|
# Output results
|
|
77
92
|
if results:
|
|
78
93
|
if self.args.output_format == "json":
|
tree_sitter_analyzer/cli_main.py
CHANGED
|
@@ -46,6 +46,13 @@ class CLICommandFactory:
|
|
|
46
46
|
if args.show_supported_extensions:
|
|
47
47
|
return ShowExtensionsCommand(args)
|
|
48
48
|
|
|
49
|
+
if args.filter_help:
|
|
50
|
+
from tree_sitter_analyzer.core.query_filter import QueryFilter
|
|
51
|
+
|
|
52
|
+
filter_service = QueryFilter()
|
|
53
|
+
output_info(filter_service.get_filter_help())
|
|
54
|
+
return None # This will exit with code 0
|
|
55
|
+
|
|
49
56
|
# File analysis commands (require file path)
|
|
50
57
|
if not args.file_path:
|
|
51
58
|
return None
|
|
@@ -95,12 +102,23 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|
|
95
102
|
"--query-string", help="Directly specify Tree-sitter query to execute"
|
|
96
103
|
)
|
|
97
104
|
|
|
105
|
+
# Query filter options
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--filter",
|
|
108
|
+
help="Filter query results (e.g., 'name=main', 'name=~get*,public=true')",
|
|
109
|
+
)
|
|
110
|
+
|
|
98
111
|
# Information options
|
|
99
112
|
parser.add_argument(
|
|
100
113
|
"--list-queries",
|
|
101
114
|
action="store_true",
|
|
102
115
|
help="Display list of available query keys",
|
|
103
116
|
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--filter-help",
|
|
119
|
+
action="store_true",
|
|
120
|
+
help="Display help for query filter syntax",
|
|
121
|
+
)
|
|
104
122
|
parser.add_argument(
|
|
105
123
|
"--describe-query", help="Display description of specified query key"
|
|
106
124
|
)
|
|
@@ -287,6 +305,9 @@ def main() -> None:
|
|
|
287
305
|
if command:
|
|
288
306
|
exit_code = command.execute()
|
|
289
307
|
sys.exit(exit_code)
|
|
308
|
+
elif command is None and hasattr(args, "filter_help") and args.filter_help:
|
|
309
|
+
# filter_help was processed successfully
|
|
310
|
+
sys.exit(0)
|
|
290
311
|
else:
|
|
291
312
|
if not args.file_path:
|
|
292
313
|
output_error("File path not specified.")
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Query Filter Service
|
|
4
|
+
|
|
5
|
+
Provides post-processing filtering for query results, supporting filtering by name, parameters, and other conditions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QueryFilter:
|
|
13
|
+
"""Query result filter"""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def filter_results(
|
|
19
|
+
self, results: list[dict[str, Any]], filter_expression: str
|
|
20
|
+
) -> list[dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Filter query results based on filter expression
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
results: Original query results
|
|
26
|
+
filter_expression: Filter expression supporting multiple formats:
|
|
27
|
+
- "name=main" - Exact name match
|
|
28
|
+
- "name~auth*" - Pattern name match
|
|
29
|
+
- "params=0" - Filter by parameter count
|
|
30
|
+
- "static=true" - Filter by modifier
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Filtered results list
|
|
34
|
+
"""
|
|
35
|
+
if not filter_expression:
|
|
36
|
+
return results
|
|
37
|
+
|
|
38
|
+
# Parse filter expression
|
|
39
|
+
filters = self._parse_filter_expression(filter_expression)
|
|
40
|
+
|
|
41
|
+
filtered_results = []
|
|
42
|
+
for result in results:
|
|
43
|
+
if self._matches_filters(result, filters):
|
|
44
|
+
filtered_results.append(result)
|
|
45
|
+
|
|
46
|
+
return filtered_results
|
|
47
|
+
|
|
48
|
+
def _parse_filter_expression(self, expression: str) -> dict[str, Any]:
|
|
49
|
+
"""Parse filter expression"""
|
|
50
|
+
filters = {}
|
|
51
|
+
|
|
52
|
+
# Support multiple conditions separated by commas
|
|
53
|
+
conditions = expression.split(",")
|
|
54
|
+
|
|
55
|
+
for condition in conditions:
|
|
56
|
+
condition = condition.strip()
|
|
57
|
+
|
|
58
|
+
if "=" in condition:
|
|
59
|
+
key, value = condition.split("=", 1)
|
|
60
|
+
key = key.strip()
|
|
61
|
+
value = value.strip()
|
|
62
|
+
|
|
63
|
+
# Handle pattern matching
|
|
64
|
+
if value.startswith("~"):
|
|
65
|
+
filters[key] = {"type": "pattern", "value": value[1:]}
|
|
66
|
+
else:
|
|
67
|
+
filters[key] = {"type": "exact", "value": value}
|
|
68
|
+
|
|
69
|
+
return filters
|
|
70
|
+
|
|
71
|
+
def _matches_filters(self, result: dict[str, Any], filters: dict[str, Any]) -> bool:
|
|
72
|
+
"""Check if result matches all filter conditions"""
|
|
73
|
+
for filter_key, filter_config in filters.items():
|
|
74
|
+
if not self._matches_single_filter(result, filter_key, filter_config):
|
|
75
|
+
return False
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def _matches_single_filter(
|
|
79
|
+
self, result: dict[str, Any], filter_key: str, filter_config: dict[str, Any]
|
|
80
|
+
) -> bool:
|
|
81
|
+
"""Check single filter condition"""
|
|
82
|
+
filter_type = filter_config["type"]
|
|
83
|
+
filter_value = filter_config["value"]
|
|
84
|
+
|
|
85
|
+
if filter_key == "name":
|
|
86
|
+
return self._match_name(result, filter_type, filter_value)
|
|
87
|
+
elif filter_key == "params":
|
|
88
|
+
return self._match_params(result, filter_type, filter_value)
|
|
89
|
+
elif filter_key == "static":
|
|
90
|
+
return self._match_modifier(result, "static", filter_value)
|
|
91
|
+
elif filter_key == "public":
|
|
92
|
+
return self._match_modifier(result, "public", filter_value)
|
|
93
|
+
elif filter_key == "private":
|
|
94
|
+
return self._match_modifier(result, "private", filter_value)
|
|
95
|
+
elif filter_key == "protected":
|
|
96
|
+
return self._match_modifier(result, "protected", filter_value)
|
|
97
|
+
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def _match_name(self, result: dict[str, Any], match_type: str, value: str) -> bool:
|
|
101
|
+
"""Match method name"""
|
|
102
|
+
content = result.get("content", "")
|
|
103
|
+
|
|
104
|
+
# Extract method name
|
|
105
|
+
method_name = self._extract_method_name(content)
|
|
106
|
+
|
|
107
|
+
if match_type == "exact":
|
|
108
|
+
return method_name == value
|
|
109
|
+
elif match_type == "pattern":
|
|
110
|
+
# Support wildcard patterns
|
|
111
|
+
pattern = value.replace("*", ".*")
|
|
112
|
+
return re.match(pattern, method_name, re.IGNORECASE) is not None
|
|
113
|
+
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _match_params(
|
|
117
|
+
self, result: dict[str, Any], match_type: str, value: str
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""Match parameter count"""
|
|
120
|
+
content = result.get("content", "")
|
|
121
|
+
param_count = self._count_parameters(content)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
target_count = int(value)
|
|
125
|
+
return param_count == target_count
|
|
126
|
+
except ValueError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _match_modifier(
|
|
130
|
+
self, result: dict[str, Any], modifier: str, value: str
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Match modifier"""
|
|
133
|
+
content = result.get("content", "")
|
|
134
|
+
has_modifier = modifier in content
|
|
135
|
+
|
|
136
|
+
return (value.lower() == "true") == has_modifier
|
|
137
|
+
|
|
138
|
+
def _extract_method_name(self, content: str) -> str:
|
|
139
|
+
"""Extract method name from content"""
|
|
140
|
+
# Match method declaration patterns
|
|
141
|
+
patterns = [
|
|
142
|
+
r"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", # Java method
|
|
143
|
+
r"def\s+(\w+)\s*\(", # Python method
|
|
144
|
+
r"function\s+(\w+)\s*\(", # JavaScript function
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
for pattern in patterns:
|
|
148
|
+
match = re.search(pattern, content)
|
|
149
|
+
if match:
|
|
150
|
+
return match.group(1)
|
|
151
|
+
|
|
152
|
+
return "unknown"
|
|
153
|
+
|
|
154
|
+
def _count_parameters(self, content: str) -> int:
|
|
155
|
+
"""Count method parameters"""
|
|
156
|
+
# Find parameter list
|
|
157
|
+
match = re.search(r"\(([^)]*)\)", content)
|
|
158
|
+
if not match:
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
params_str = match.group(1).strip()
|
|
162
|
+
if not params_str:
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
# Simple parameter counting (by comma separation)
|
|
166
|
+
# Note: This is a simple implementation, doesn't handle generics etc.
|
|
167
|
+
params = [p.strip() for p in params_str.split(",") if p.strip()]
|
|
168
|
+
return len(params)
|
|
169
|
+
|
|
170
|
+
def get_filter_help(self) -> str:
|
|
171
|
+
"""Get filter help information"""
|
|
172
|
+
return """
|
|
173
|
+
Filter Syntax Help:
|
|
174
|
+
|
|
175
|
+
Basic Syntax:
|
|
176
|
+
--filter "key=value" # Exact match
|
|
177
|
+
--filter "key=~pattern" # Pattern match (supports wildcard *)
|
|
178
|
+
--filter "key1=value1,key2=value2" # Multiple conditions (AND logic)
|
|
179
|
+
|
|
180
|
+
Supported filter keys:
|
|
181
|
+
name - Method/function name
|
|
182
|
+
e.g.: name=main, name=~auth*, name=~get*
|
|
183
|
+
|
|
184
|
+
params - Number of parameters
|
|
185
|
+
e.g.: params=0, params=2
|
|
186
|
+
|
|
187
|
+
static - Whether it is a static method
|
|
188
|
+
e.g.: static=true, static=false
|
|
189
|
+
|
|
190
|
+
public - Whether it is a public method
|
|
191
|
+
e.g.: public=true, public=false
|
|
192
|
+
|
|
193
|
+
private - Whether it is a private method
|
|
194
|
+
e.g.: private=true, private=false
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
--query-key methods --filter "name=main"
|
|
198
|
+
--query-key methods --filter "name=~get*,public=true"
|
|
199
|
+
--query-key methods --filter "params=0,static=true"
|
|
200
|
+
"""
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Query Service
|
|
4
|
+
|
|
5
|
+
Unified query service for both CLI and MCP interfaces to avoid code duplication.
|
|
6
|
+
Provides core tree-sitter query functionality including predefined and custom queries.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..encoding_utils import read_file_safe
|
|
13
|
+
from ..query_loader import query_loader
|
|
14
|
+
from .parser import Parser
|
|
15
|
+
from .query_filter import QueryFilter
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QueryService:
|
|
21
|
+
"""Unified query service providing tree-sitter query functionality"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, project_root: str | None = None) -> None:
|
|
24
|
+
"""Initialize the query service"""
|
|
25
|
+
self.project_root = project_root
|
|
26
|
+
self.parser = Parser()
|
|
27
|
+
self.filter = QueryFilter()
|
|
28
|
+
|
|
29
|
+
async def execute_query(
|
|
30
|
+
self,
|
|
31
|
+
file_path: str,
|
|
32
|
+
language: str,
|
|
33
|
+
query_key: str | None = None,
|
|
34
|
+
query_string: str | None = None,
|
|
35
|
+
filter_expression: str | None = None,
|
|
36
|
+
) -> list[dict[str, Any]] | None:
|
|
37
|
+
"""
|
|
38
|
+
Execute a query
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
file_path: Path to the file to analyze
|
|
42
|
+
language: Programming language
|
|
43
|
+
query_key: Predefined query key (e.g., 'methods', 'class')
|
|
44
|
+
query_string: Custom query string (e.g., '(method_declaration) @method')
|
|
45
|
+
filter_expression: Filter expression (e.g., 'name=main', 'name=~get*,public=true')
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of query results, each containing capture_name, node_type, start_line, end_line, content
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If neither query_key nor query_string is provided
|
|
52
|
+
FileNotFoundError: If file doesn't exist
|
|
53
|
+
Exception: If query execution fails
|
|
54
|
+
"""
|
|
55
|
+
if not query_key and not query_string:
|
|
56
|
+
raise ValueError("Must provide either query_key or query_string")
|
|
57
|
+
|
|
58
|
+
if query_key and query_string:
|
|
59
|
+
raise ValueError("Cannot provide both query_key and query_string")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Read file content
|
|
63
|
+
content, encoding = read_file_safe(file_path)
|
|
64
|
+
|
|
65
|
+
# Parse file
|
|
66
|
+
parse_result = self.parser.parse_code(content, language, file_path)
|
|
67
|
+
if not parse_result or not parse_result.tree:
|
|
68
|
+
raise Exception("Failed to parse file")
|
|
69
|
+
|
|
70
|
+
tree = parse_result.tree
|
|
71
|
+
language_obj = tree.language if hasattr(tree, "language") else None
|
|
72
|
+
if not language_obj:
|
|
73
|
+
raise Exception(f"Language object not available for {language}")
|
|
74
|
+
|
|
75
|
+
# Get query string
|
|
76
|
+
if query_key:
|
|
77
|
+
query_string = query_loader.get_query(language, query_key)
|
|
78
|
+
if not query_string:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Query '{query_key}' not found for language '{language}'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Execute tree-sitter query
|
|
84
|
+
ts_query = language_obj.query(query_string)
|
|
85
|
+
captures = ts_query.captures(tree.root_node)
|
|
86
|
+
|
|
87
|
+
# Process capture results
|
|
88
|
+
results = []
|
|
89
|
+
if isinstance(captures, dict):
|
|
90
|
+
# New tree-sitter API returns dictionary
|
|
91
|
+
for capture_name, nodes in captures.items():
|
|
92
|
+
for node in nodes:
|
|
93
|
+
results.append(self._create_result_dict(node, capture_name))
|
|
94
|
+
else:
|
|
95
|
+
# Old tree-sitter API returns list of tuples
|
|
96
|
+
for capture in captures:
|
|
97
|
+
if isinstance(capture, tuple) and len(capture) == 2:
|
|
98
|
+
node, name = capture
|
|
99
|
+
results.append(self._create_result_dict(node, name))
|
|
100
|
+
|
|
101
|
+
# Apply filters
|
|
102
|
+
if filter_expression and results:
|
|
103
|
+
results = self.filter.filter_results(results, filter_expression)
|
|
104
|
+
|
|
105
|
+
return results
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Query execution failed: {e}")
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
def _create_result_dict(self, node: Any, capture_name: str) -> dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
Create result dictionary from tree-sitter node
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
node: tree-sitter node
|
|
117
|
+
capture_name: capture name
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Result dictionary
|
|
121
|
+
"""
|
|
122
|
+
return {
|
|
123
|
+
"capture_name": capture_name,
|
|
124
|
+
"node_type": node.type if hasattr(node, "type") else "unknown",
|
|
125
|
+
"start_line": (
|
|
126
|
+
node.start_point[0] + 1 if hasattr(node, "start_point") else 0
|
|
127
|
+
),
|
|
128
|
+
"end_line": node.end_point[0] + 1 if hasattr(node, "end_point") else 0,
|
|
129
|
+
"content": (
|
|
130
|
+
node.text.decode("utf-8", errors="replace")
|
|
131
|
+
if hasattr(node, "text") and node.text
|
|
132
|
+
else ""
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def get_available_queries(self, language: str) -> list[str]:
|
|
137
|
+
"""
|
|
138
|
+
Get available query keys for specified language
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
language: Programming language
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of available query keys
|
|
145
|
+
"""
|
|
146
|
+
return query_loader.list_queries(language)
|
|
147
|
+
|
|
148
|
+
def get_query_description(self, language: str, query_key: str) -> str | None:
|
|
149
|
+
"""
|
|
150
|
+
Get description for query key
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
language: Programming language
|
|
154
|
+
query_key: Query key
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Query description, or None if not found
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
return query_loader.get_query_description(language, query_key)
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
@@ -135,9 +135,8 @@ class CodeFileResource:
|
|
|
135
135
|
raise ValueError("File path contains null bytes")
|
|
136
136
|
|
|
137
137
|
# Check for potentially dangerous path traversal
|
|
138
|
-
# normalized_path = Path(file_path).resolve() # Not used currently
|
|
139
138
|
if ".." in file_path:
|
|
140
|
-
|
|
139
|
+
raise ValueError(f"Path traversal not allowed: {file_path}")
|
|
141
140
|
|
|
142
141
|
# Additional security checks could be added here
|
|
143
142
|
# For example, restricting to certain directories
|
|
@@ -51,6 +51,7 @@ from ..utils import setup_logger
|
|
|
51
51
|
from . import MCP_INFO
|
|
52
52
|
from .resources import CodeFileResource, ProjectStatsResource
|
|
53
53
|
from .tools.base_tool import MCPTool
|
|
54
|
+
from .tools.query_tool import QueryTool
|
|
54
55
|
from .tools.read_partial_tool import ReadPartialTool
|
|
55
56
|
from .tools.table_format_tool import TableFormatTool
|
|
56
57
|
|
|
@@ -77,7 +78,8 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
77
78
|
self.security_validator = SecurityValidator(project_root)
|
|
78
79
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
79
80
|
|
|
80
|
-
# Initialize MCP tools with security validation (
|
|
81
|
+
# Initialize MCP tools with security validation (four core tools)
|
|
82
|
+
self.query_tool = QueryTool(project_root) # query_code
|
|
81
83
|
self.read_partial_tool: MCPTool = ReadPartialTool(
|
|
82
84
|
project_root
|
|
83
85
|
) # extract_code_section
|
|
@@ -324,6 +326,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
324
326
|
"additionalProperties": False,
|
|
325
327
|
},
|
|
326
328
|
),
|
|
329
|
+
Tool(**self.query_tool.get_tool_definition()),
|
|
327
330
|
]
|
|
328
331
|
|
|
329
332
|
logger.info(f"Returning {len(tools)} tools: {[t.name for t in tools]}")
|
|
@@ -406,6 +409,9 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
406
409
|
self.set_project_path(project_path)
|
|
407
410
|
result = {"status": "success", "project_root": project_path}
|
|
408
411
|
|
|
412
|
+
elif name == "query_code":
|
|
413
|
+
result = await self.query_tool.execute(arguments)
|
|
414
|
+
|
|
409
415
|
else:
|
|
410
416
|
raise ValueError(f"Unknown tool: {name}")
|
|
411
417
|
|