tree-sitter-analyzer 0.9.4__py3-none-any.whl → 0.9.6__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.

@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "0.9.3"
14
+ __version__ = "0.9.6"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -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
- try:
26
- query_to_execute = query_loader.get_query(language, sanitized_query_key)
27
- if query_to_execute is None:
28
- output_error(
29
- f"Query '{sanitized_query_key}' not found for language '{language}'"
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
- # Perform analysis
50
- analysis_result = await self.analyze_file(language)
51
- if not analysis_result:
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":
@@ -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
@@ -29,10 +29,13 @@ def _setup_encoding_environment() -> None:
29
29
  sys.stderr.reconfigure(encoding="utf-8", errors="replace")
30
30
  except Exception as e:
31
31
  # Ignore setup errors, use defaults; log at debug when possible
32
- try:
33
- sys.stderr.write(f"[encoding_setup] non-fatal setup error: {e}\n")
34
- except Exception:
35
- pass
32
+ msg = f"[encoding_setup] non-fatal setup error: {e}\n"
33
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
34
+ try:
35
+ sys.stderr.write(msg)
36
+ except Exception:
37
+ # Swallow secondary I/O errors intentionally
38
+ ...
36
39
 
37
40
 
38
41
  # Set up environment when module is imported
@@ -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
- logger.warning(f"Potentially dangerous path traversal in: {file_path}")
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 (three core tools)
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