tree-sitter-analyzer 1.9.17.1__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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree-sitter Analyzer Custom Exceptions
|
|
4
|
+
|
|
5
|
+
Unified exception handling system for consistent error management
|
|
6
|
+
across the entire framework.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TreeSitterAnalyzerError(Exception):
|
|
14
|
+
"""Base exception for all tree-sitter analyzer errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
error_code: str | None = None,
|
|
20
|
+
context: dict[str, Any] | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.error_code = error_code or self.__class__.__name__
|
|
25
|
+
self.context = context or {}
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
"""Convert exception to dictionary format."""
|
|
29
|
+
return {
|
|
30
|
+
"error_type": self.__class__.__name__,
|
|
31
|
+
"error_code": self.error_code,
|
|
32
|
+
"message": self.message,
|
|
33
|
+
"context": self.context,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AnalysisError(TreeSitterAnalyzerError):
|
|
38
|
+
"""Raised when file analysis fails."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
file_path: str | Path | None = None,
|
|
44
|
+
language: str | None = None,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
context = kwargs.get("context", {})
|
|
48
|
+
if file_path:
|
|
49
|
+
context["file_path"] = str(file_path)
|
|
50
|
+
if language:
|
|
51
|
+
context["language"] = language
|
|
52
|
+
super().__init__(message, context=context, **kwargs)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ParseError(TreeSitterAnalyzerError):
|
|
56
|
+
"""Raised when parsing fails."""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
message: str,
|
|
61
|
+
language: str | None = None,
|
|
62
|
+
source_info: dict[str, Any] | None = None,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> None:
|
|
65
|
+
context = kwargs.get("context", {})
|
|
66
|
+
if language:
|
|
67
|
+
context["language"] = language
|
|
68
|
+
if source_info:
|
|
69
|
+
context.update(source_info)
|
|
70
|
+
super().__init__(message, context=context, **kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LanguageNotSupportedError(TreeSitterAnalyzerError):
|
|
74
|
+
"""Raised when a language is not supported."""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self, language: str, supported_languages: list[str] | None = None, **kwargs: Any
|
|
78
|
+
) -> None:
|
|
79
|
+
message = f"Language '{language}' is not supported"
|
|
80
|
+
context = kwargs.get("context", {})
|
|
81
|
+
context["language"] = language
|
|
82
|
+
if supported_languages:
|
|
83
|
+
context["supported_languages"] = supported_languages
|
|
84
|
+
message += f". Supported languages: {', '.join(supported_languages)}"
|
|
85
|
+
super().__init__(message, context=context, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PluginError(TreeSitterAnalyzerError):
|
|
89
|
+
"""Raised when plugin operations fail."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
message: str,
|
|
94
|
+
plugin_name: str | None = None,
|
|
95
|
+
operation: str | None = None,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> None:
|
|
98
|
+
context = kwargs.get("context", {})
|
|
99
|
+
if plugin_name:
|
|
100
|
+
context["plugin_name"] = plugin_name
|
|
101
|
+
if operation:
|
|
102
|
+
context["operation"] = operation
|
|
103
|
+
super().__init__(message, context=context, **kwargs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class QueryError(TreeSitterAnalyzerError):
|
|
107
|
+
"""Raised when query execution fails."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
message: str,
|
|
112
|
+
query_name: str | None = None,
|
|
113
|
+
query_string: str | None = None,
|
|
114
|
+
language: str | None = None,
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
) -> None:
|
|
117
|
+
context = kwargs.get("context", {})
|
|
118
|
+
if query_name:
|
|
119
|
+
context["query_name"] = query_name
|
|
120
|
+
if query_string:
|
|
121
|
+
context["query_string"] = query_string
|
|
122
|
+
if language:
|
|
123
|
+
context["language"] = language
|
|
124
|
+
super().__init__(message, context=context, **kwargs)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class FileHandlingError(TreeSitterAnalyzerError):
|
|
128
|
+
"""Raised when file operations fail."""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
message: str,
|
|
133
|
+
file_path: str | Path | None = None,
|
|
134
|
+
operation: str | None = None,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> None:
|
|
137
|
+
context = kwargs.get("context", {})
|
|
138
|
+
if file_path:
|
|
139
|
+
context["file_path"] = str(file_path)
|
|
140
|
+
if operation:
|
|
141
|
+
context["operation"] = operation
|
|
142
|
+
super().__init__(message, context=context, **kwargs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ConfigurationError(TreeSitterAnalyzerError):
|
|
146
|
+
"""Raised when configuration is invalid."""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
message: str,
|
|
151
|
+
config_key: str | None = None,
|
|
152
|
+
config_value: Any | None = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> None:
|
|
155
|
+
context = kwargs.get("context", {})
|
|
156
|
+
if config_key:
|
|
157
|
+
context["config_key"] = config_key
|
|
158
|
+
if config_value is not None:
|
|
159
|
+
context["config_value"] = config_value
|
|
160
|
+
super().__init__(message, context=context, **kwargs)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ValidationError(TreeSitterAnalyzerError):
|
|
164
|
+
"""Raised when validation fails."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
message: str,
|
|
169
|
+
validation_type: str | None = None,
|
|
170
|
+
invalid_value: Any | None = None,
|
|
171
|
+
**kwargs: Any,
|
|
172
|
+
) -> None:
|
|
173
|
+
context = kwargs.pop("context", {})
|
|
174
|
+
if validation_type:
|
|
175
|
+
context["validation_type"] = validation_type
|
|
176
|
+
if invalid_value is not None:
|
|
177
|
+
context["invalid_value"] = invalid_value
|
|
178
|
+
super().__init__(message, context=context, **kwargs)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class MCPError(TreeSitterAnalyzerError):
|
|
182
|
+
"""Raised when MCP operations fail."""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
message: str,
|
|
187
|
+
tool_name: str | None = None,
|
|
188
|
+
resource_uri: str | None = None,
|
|
189
|
+
**kwargs: Any,
|
|
190
|
+
) -> None:
|
|
191
|
+
context = kwargs.pop("context", {})
|
|
192
|
+
if tool_name:
|
|
193
|
+
context["tool_name"] = tool_name
|
|
194
|
+
if resource_uri:
|
|
195
|
+
context["resource_uri"] = resource_uri
|
|
196
|
+
super().__init__(message, context=context, **kwargs)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Exception handling utilities
|
|
200
|
+
def handle_exception(
|
|
201
|
+
exception: Exception,
|
|
202
|
+
context: dict[str, Any] | None = None,
|
|
203
|
+
reraise_as: type[Exception] | None = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Handle exceptions with optional context and re-raising.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
exception: The original exception
|
|
210
|
+
context: Additional context information
|
|
211
|
+
reraise_as: Exception class to re-raise as
|
|
212
|
+
"""
|
|
213
|
+
from .utils import log_error
|
|
214
|
+
|
|
215
|
+
# Log the original exception
|
|
216
|
+
error_context = context or {}
|
|
217
|
+
if hasattr(exception, "context"):
|
|
218
|
+
error_context.update(exception.context)
|
|
219
|
+
|
|
220
|
+
log_error(f"Exception handled: {exception}", extra=error_context)
|
|
221
|
+
|
|
222
|
+
# Re-raise as different exception type if requested
|
|
223
|
+
if reraise_as and not isinstance(exception, reraise_as):
|
|
224
|
+
if issubclass(reraise_as, TreeSitterAnalyzerError):
|
|
225
|
+
raise reraise_as(str(exception), context=error_context)
|
|
226
|
+
else:
|
|
227
|
+
raise reraise_as(str(exception))
|
|
228
|
+
|
|
229
|
+
# Re-raise original exception
|
|
230
|
+
raise exception
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def safe_execute(
|
|
234
|
+
func: Any,
|
|
235
|
+
*args: Any,
|
|
236
|
+
default_return: Any = None,
|
|
237
|
+
exception_types: tuple[type[Exception], ...] = (Exception,),
|
|
238
|
+
log_errors: bool = True,
|
|
239
|
+
**kwargs: Any,
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""
|
|
242
|
+
Safely execute a function with exception handling.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
func: Function to execute
|
|
246
|
+
*args: Function arguments
|
|
247
|
+
default_return: Value to return on exception
|
|
248
|
+
exception_types: Exception types to catch
|
|
249
|
+
log_errors: Whether to log errors
|
|
250
|
+
**kwargs: Function keyword arguments
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Function result or default_return on exception
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
return func(*args, **kwargs)
|
|
257
|
+
except exception_types as e:
|
|
258
|
+
if log_errors:
|
|
259
|
+
from .utils import log_error
|
|
260
|
+
|
|
261
|
+
log_error(f"Safe execution failed for {func.__name__}: {e}")
|
|
262
|
+
return default_return
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def create_error_response(
|
|
266
|
+
exception: Exception, include_traceback: bool = False
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Create standardized error response dictionary.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
exception: The exception to convert
|
|
273
|
+
include_traceback: Whether to include traceback
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Error response dictionary
|
|
277
|
+
"""
|
|
278
|
+
import traceback
|
|
279
|
+
|
|
280
|
+
response: dict[str, Any] = {
|
|
281
|
+
"success": False,
|
|
282
|
+
"error": {"type": exception.__class__.__name__, "message": str(exception)},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Add context if available
|
|
286
|
+
if hasattr(exception, "context"):
|
|
287
|
+
response["error"]["context"] = exception.context
|
|
288
|
+
|
|
289
|
+
# Add error code if available
|
|
290
|
+
if hasattr(exception, "error_code"):
|
|
291
|
+
response["error"]["code"] = exception.error_code
|
|
292
|
+
|
|
293
|
+
# Add traceback if requested
|
|
294
|
+
if include_traceback:
|
|
295
|
+
response["error"]["traceback"] = traceback.format_exc()
|
|
296
|
+
|
|
297
|
+
return response
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Decorator for exception handling
|
|
301
|
+
def handle_exceptions(
|
|
302
|
+
default_return: Any = None,
|
|
303
|
+
exception_types: tuple[type[Exception], ...] = (Exception,),
|
|
304
|
+
reraise_as: type[Exception] | None = None,
|
|
305
|
+
log_errors: bool = True,
|
|
306
|
+
) -> Any:
|
|
307
|
+
"""
|
|
308
|
+
Decorator for automatic exception handling.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
default_return: Value to return on exception
|
|
312
|
+
exception_types: Exception types to catch
|
|
313
|
+
reraise_as: Exception class to re-raise as
|
|
314
|
+
log_errors: Whether to log errors
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
def decorator(func: Any) -> Any:
|
|
318
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
319
|
+
try:
|
|
320
|
+
return func(*args, **kwargs)
|
|
321
|
+
except exception_types as e:
|
|
322
|
+
if log_errors:
|
|
323
|
+
from .utils import log_error
|
|
324
|
+
|
|
325
|
+
log_error(f"Exception in {func.__name__}: {e}")
|
|
326
|
+
|
|
327
|
+
if reraise_as:
|
|
328
|
+
if issubclass(reraise_as, TreeSitterAnalyzerError):
|
|
329
|
+
raise reraise_as(str(e)) from e
|
|
330
|
+
else:
|
|
331
|
+
raise reraise_as(str(e)) from e
|
|
332
|
+
|
|
333
|
+
return default_return
|
|
334
|
+
|
|
335
|
+
return wrapper
|
|
336
|
+
|
|
337
|
+
return decorator
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class SecurityError(TreeSitterAnalyzerError):
|
|
341
|
+
"""Raised when security validation fails."""
|
|
342
|
+
|
|
343
|
+
def __init__(
|
|
344
|
+
self,
|
|
345
|
+
message: str,
|
|
346
|
+
security_type: str | None = None,
|
|
347
|
+
file_path: str | Path | None = None,
|
|
348
|
+
**kwargs: Any,
|
|
349
|
+
) -> None:
|
|
350
|
+
context = kwargs.pop("context", {})
|
|
351
|
+
if security_type:
|
|
352
|
+
context["security_type"] = security_type
|
|
353
|
+
if file_path:
|
|
354
|
+
context["file_path"] = str(file_path)
|
|
355
|
+
|
|
356
|
+
super().__init__(message, context=context, **kwargs)
|
|
357
|
+
self.security_type = security_type
|
|
358
|
+
self.file_path = str(file_path) if file_path else None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class PathTraversalError(SecurityError):
|
|
362
|
+
"""Raised when path traversal attack is detected."""
|
|
363
|
+
|
|
364
|
+
def __init__(
|
|
365
|
+
self,
|
|
366
|
+
message: str,
|
|
367
|
+
attempted_path: str | None = None,
|
|
368
|
+
**kwargs: Any,
|
|
369
|
+
) -> None:
|
|
370
|
+
context = kwargs.pop("context", {})
|
|
371
|
+
if attempted_path:
|
|
372
|
+
context["attempted_path"] = attempted_path
|
|
373
|
+
|
|
374
|
+
super().__init__(
|
|
375
|
+
message, security_type="path_traversal", context=context, **kwargs
|
|
376
|
+
)
|
|
377
|
+
self.attempted_path = attempted_path
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class RegexSecurityError(SecurityError):
|
|
381
|
+
"""Raised when unsafe regex pattern is detected."""
|
|
382
|
+
|
|
383
|
+
def __init__(
|
|
384
|
+
self,
|
|
385
|
+
message: str,
|
|
386
|
+
pattern: str | None = None,
|
|
387
|
+
dangerous_construct: str | None = None,
|
|
388
|
+
**kwargs: Any,
|
|
389
|
+
) -> None:
|
|
390
|
+
context = kwargs.pop("context", {})
|
|
391
|
+
if pattern:
|
|
392
|
+
context["pattern"] = pattern
|
|
393
|
+
if dangerous_construct:
|
|
394
|
+
context["dangerous_construct"] = dangerous_construct
|
|
395
|
+
|
|
396
|
+
super().__init__(
|
|
397
|
+
message, security_type="regex_security", context=context, **kwargs
|
|
398
|
+
)
|
|
399
|
+
self.pattern = pattern
|
|
400
|
+
self.dangerous_construct = dangerous_construct
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# MCP-specific exceptions for enhanced error handling
|
|
404
|
+
class MCPToolError(MCPError):
|
|
405
|
+
"""Raised when MCP tool execution fails."""
|
|
406
|
+
|
|
407
|
+
def __init__(
|
|
408
|
+
self,
|
|
409
|
+
message: str,
|
|
410
|
+
tool_name: str | None = None,
|
|
411
|
+
input_params: dict[str, Any] | None = None,
|
|
412
|
+
execution_stage: str | None = None,
|
|
413
|
+
**kwargs: Any,
|
|
414
|
+
) -> None:
|
|
415
|
+
context = kwargs.pop("context", {})
|
|
416
|
+
if input_params:
|
|
417
|
+
# Sanitize sensitive information from input params
|
|
418
|
+
sanitized_params = self._sanitize_params(input_params)
|
|
419
|
+
context["input_params"] = sanitized_params
|
|
420
|
+
if execution_stage:
|
|
421
|
+
context["execution_stage"] = execution_stage
|
|
422
|
+
|
|
423
|
+
super().__init__(message, tool_name=tool_name, context=context, **kwargs)
|
|
424
|
+
self.tool_name = tool_name
|
|
425
|
+
self.input_params = input_params
|
|
426
|
+
self.execution_stage = execution_stage
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _sanitize_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
430
|
+
"""Sanitize sensitive information from parameters."""
|
|
431
|
+
sanitized = {}
|
|
432
|
+
sensitive_keys = {"password", "token", "key", "secret", "auth", "credential"}
|
|
433
|
+
|
|
434
|
+
for key, value in params.items():
|
|
435
|
+
if any(sensitive in key.lower() for sensitive in sensitive_keys):
|
|
436
|
+
sanitized[key] = "***REDACTED***"
|
|
437
|
+
elif isinstance(value, str) and len(value) > 100:
|
|
438
|
+
sanitized[key] = value[:100] + "...[TRUNCATED]"
|
|
439
|
+
else:
|
|
440
|
+
sanitized[key] = value
|
|
441
|
+
|
|
442
|
+
return sanitized
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class MCPResourceError(MCPError):
|
|
446
|
+
"""Raised when MCP resource access fails."""
|
|
447
|
+
|
|
448
|
+
def __init__(
|
|
449
|
+
self,
|
|
450
|
+
message: str,
|
|
451
|
+
resource_uri: str | None = None,
|
|
452
|
+
resource_type: str | None = None,
|
|
453
|
+
access_mode: str | None = None,
|
|
454
|
+
**kwargs: Any,
|
|
455
|
+
) -> None:
|
|
456
|
+
context = kwargs.pop("context", {})
|
|
457
|
+
if resource_type:
|
|
458
|
+
context["resource_type"] = resource_type
|
|
459
|
+
if access_mode:
|
|
460
|
+
context["access_mode"] = access_mode
|
|
461
|
+
|
|
462
|
+
super().__init__(message, resource_uri=resource_uri, context=context, **kwargs)
|
|
463
|
+
self.resource_uri = resource_uri
|
|
464
|
+
self.resource_type = resource_type
|
|
465
|
+
self.access_mode = access_mode
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class MCPTimeoutError(MCPError):
|
|
469
|
+
"""Raised when MCP operation times out."""
|
|
470
|
+
|
|
471
|
+
def __init__(
|
|
472
|
+
self,
|
|
473
|
+
message: str,
|
|
474
|
+
timeout_seconds: float | None = None,
|
|
475
|
+
operation_type: str | None = None,
|
|
476
|
+
**kwargs: Any,
|
|
477
|
+
) -> None:
|
|
478
|
+
context = kwargs.pop("context", {})
|
|
479
|
+
if timeout_seconds:
|
|
480
|
+
context["timeout_seconds"] = timeout_seconds
|
|
481
|
+
if operation_type:
|
|
482
|
+
context["operation_type"] = operation_type
|
|
483
|
+
|
|
484
|
+
super().__init__(message, context=context, **kwargs)
|
|
485
|
+
self.timeout_seconds = timeout_seconds
|
|
486
|
+
self.operation_type = operation_type
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class MCPValidationError(ValidationError):
|
|
490
|
+
"""Raised when MCP input validation fails."""
|
|
491
|
+
|
|
492
|
+
def __init__(
|
|
493
|
+
self,
|
|
494
|
+
message: str,
|
|
495
|
+
tool_name: str | None = None,
|
|
496
|
+
parameter_name: str | None = None,
|
|
497
|
+
parameter_value: Any | None = None,
|
|
498
|
+
validation_rule: str | None = None,
|
|
499
|
+
**kwargs: Any,
|
|
500
|
+
) -> None:
|
|
501
|
+
context = kwargs.pop("context", {})
|
|
502
|
+
if tool_name:
|
|
503
|
+
context["tool_name"] = tool_name
|
|
504
|
+
if parameter_name:
|
|
505
|
+
context["parameter_name"] = parameter_name
|
|
506
|
+
if validation_rule:
|
|
507
|
+
context["validation_rule"] = validation_rule
|
|
508
|
+
|
|
509
|
+
# Sanitize parameter value for logging
|
|
510
|
+
if parameter_value is not None:
|
|
511
|
+
if isinstance(parameter_value, str) and len(parameter_value) > 200:
|
|
512
|
+
context["parameter_value"] = parameter_value[:200] + "...[TRUNCATED]"
|
|
513
|
+
else:
|
|
514
|
+
context["parameter_value"] = parameter_value
|
|
515
|
+
|
|
516
|
+
super().__init__(
|
|
517
|
+
message, validation_type="mcp_parameter", context=context, **kwargs
|
|
518
|
+
)
|
|
519
|
+
self.tool_name = tool_name
|
|
520
|
+
self.parameter_name = parameter_name
|
|
521
|
+
self.validation_rule = validation_rule
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class FileRestrictionError(SecurityError):
|
|
525
|
+
"""Raised when file access is restricted by mode or security policy."""
|
|
526
|
+
|
|
527
|
+
def __init__(
|
|
528
|
+
self,
|
|
529
|
+
message: str,
|
|
530
|
+
file_path: str | Path | None = None,
|
|
531
|
+
current_mode: str | None = None,
|
|
532
|
+
allowed_patterns: list[str] | None = None,
|
|
533
|
+
**kwargs: Any,
|
|
534
|
+
) -> None:
|
|
535
|
+
context = kwargs.pop("context", {})
|
|
536
|
+
if current_mode:
|
|
537
|
+
context["current_mode"] = current_mode
|
|
538
|
+
if allowed_patterns:
|
|
539
|
+
context["allowed_patterns"] = allowed_patterns
|
|
540
|
+
|
|
541
|
+
super().__init__(
|
|
542
|
+
message,
|
|
543
|
+
security_type="file_restriction",
|
|
544
|
+
file_path=file_path,
|
|
545
|
+
context=context,
|
|
546
|
+
**kwargs,
|
|
547
|
+
)
|
|
548
|
+
self.current_mode = current_mode
|
|
549
|
+
self.allowed_patterns = allowed_patterns
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# Enhanced error response utilities for MCP
|
|
553
|
+
def create_mcp_error_response(
|
|
554
|
+
exception: Exception,
|
|
555
|
+
tool_name: str | None = None,
|
|
556
|
+
include_debug_info: bool = False,
|
|
557
|
+
sanitize_sensitive: bool = True,
|
|
558
|
+
) -> dict[str, Any]:
|
|
559
|
+
"""
|
|
560
|
+
Create standardized MCP error response dictionary.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
exception: The exception to convert
|
|
564
|
+
tool_name: Name of the MCP tool that failed
|
|
565
|
+
include_debug_info: Whether to include debug information
|
|
566
|
+
sanitize_sensitive: Whether to sanitize sensitive information
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
MCP-compliant error response dictionary
|
|
570
|
+
"""
|
|
571
|
+
import traceback
|
|
572
|
+
|
|
573
|
+
response: dict[str, Any] = {
|
|
574
|
+
"success": False,
|
|
575
|
+
"error": {
|
|
576
|
+
"type": exception.__class__.__name__,
|
|
577
|
+
"message": str(exception),
|
|
578
|
+
"timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
|
579
|
+
},
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# Add tool name if provided
|
|
583
|
+
if tool_name:
|
|
584
|
+
response["error"]["tool"] = tool_name
|
|
585
|
+
|
|
586
|
+
# Add context if available
|
|
587
|
+
if hasattr(exception, "context") and exception.context:
|
|
588
|
+
context = exception.context.copy()
|
|
589
|
+
|
|
590
|
+
# Sanitize sensitive information if requested
|
|
591
|
+
if sanitize_sensitive:
|
|
592
|
+
context = _sanitize_error_context(context)
|
|
593
|
+
|
|
594
|
+
response["error"]["context"] = context
|
|
595
|
+
|
|
596
|
+
# Add error code if available
|
|
597
|
+
if hasattr(exception, "error_code"):
|
|
598
|
+
response["error"]["code"] = exception.error_code
|
|
599
|
+
|
|
600
|
+
# Add debug information if requested
|
|
601
|
+
if include_debug_info:
|
|
602
|
+
response["error"]["debug"] = {
|
|
603
|
+
"traceback": traceback.format_exc(),
|
|
604
|
+
"exception_args": list(exception.args) if exception.args else [],
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# Add specific error details for known exception types
|
|
608
|
+
if isinstance(exception, MCPToolError):
|
|
609
|
+
response["error"]["execution_stage"] = exception.execution_stage
|
|
610
|
+
elif isinstance(exception, MCPTimeoutError):
|
|
611
|
+
response["error"]["timeout_seconds"] = exception.timeout_seconds
|
|
612
|
+
elif isinstance(exception, FileRestrictionError):
|
|
613
|
+
response["error"]["current_mode"] = exception.current_mode
|
|
614
|
+
response["error"]["allowed_patterns"] = exception.allowed_patterns
|
|
615
|
+
|
|
616
|
+
return response
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _sanitize_error_context(context: dict[str, Any]) -> dict[str, Any]:
|
|
620
|
+
"""Sanitize sensitive information from error context."""
|
|
621
|
+
sanitized: dict[str, Any] = {}
|
|
622
|
+
sensitive_keys = {
|
|
623
|
+
"password",
|
|
624
|
+
"token",
|
|
625
|
+
"key",
|
|
626
|
+
"secret",
|
|
627
|
+
"auth",
|
|
628
|
+
"credential",
|
|
629
|
+
"api_key",
|
|
630
|
+
"access_token",
|
|
631
|
+
"private_key",
|
|
632
|
+
"session_id",
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for key, value in context.items():
|
|
636
|
+
if any(sensitive in key.lower() for sensitive in sensitive_keys):
|
|
637
|
+
sanitized[key] = "***REDACTED***"
|
|
638
|
+
elif isinstance(value, str) and len(value) > 500:
|
|
639
|
+
sanitized[key] = value[:500] + "...[TRUNCATED]"
|
|
640
|
+
elif isinstance(value, list | tuple) and len(value) > 10:
|
|
641
|
+
sanitized[key] = list(value[:10]) + ["...[TRUNCATED]"]
|
|
642
|
+
elif isinstance(value, dict) and len(value) > 20:
|
|
643
|
+
# Recursively sanitize nested dictionaries
|
|
644
|
+
truncated_dict = dict(list(value.items())[:20])
|
|
645
|
+
sanitized[key] = _sanitize_error_context(truncated_dict)
|
|
646
|
+
sanitized[key]["__truncated__"] = True
|
|
647
|
+
else:
|
|
648
|
+
sanitized[key] = value
|
|
649
|
+
|
|
650
|
+
return sanitized
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# Async exception handling utilities for MCP tools
|
|
654
|
+
async def safe_execute_async(
|
|
655
|
+
coro: Any,
|
|
656
|
+
default_return: Any = None,
|
|
657
|
+
exception_types: tuple[type[Exception], ...] = (Exception,),
|
|
658
|
+
log_errors: bool = True,
|
|
659
|
+
tool_name: str | None = None,
|
|
660
|
+
) -> Any:
|
|
661
|
+
"""
|
|
662
|
+
Safely execute an async function with exception handling.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
coro: Coroutine to execute
|
|
666
|
+
default_return: Value to return on exception
|
|
667
|
+
exception_types: Exception types to catch
|
|
668
|
+
log_errors: Whether to log errors
|
|
669
|
+
tool_name: Name of the tool for error context
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Coroutine result or default_return on exception
|
|
673
|
+
"""
|
|
674
|
+
try:
|
|
675
|
+
return await coro
|
|
676
|
+
except exception_types as e:
|
|
677
|
+
if log_errors:
|
|
678
|
+
from .utils import log_error
|
|
679
|
+
|
|
680
|
+
error_context = {"tool_name": tool_name} if tool_name else {}
|
|
681
|
+
log_error(f"Async execution failed: {e}", extra=error_context)
|
|
682
|
+
|
|
683
|
+
return default_return
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def mcp_exception_handler(
|
|
687
|
+
tool_name: str,
|
|
688
|
+
include_debug: bool = False,
|
|
689
|
+
sanitize_sensitive: bool = True,
|
|
690
|
+
) -> Any:
|
|
691
|
+
"""
|
|
692
|
+
Decorator for MCP tool exception handling.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
tool_name: Name of the MCP tool
|
|
696
|
+
include_debug: Whether to include debug information
|
|
697
|
+
sanitize_sensitive: Whether to sanitize sensitive information
|
|
698
|
+
"""
|
|
699
|
+
|
|
700
|
+
def decorator(func: Any) -> Any:
|
|
701
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
702
|
+
try:
|
|
703
|
+
return await func(*args, **kwargs)
|
|
704
|
+
except Exception as e:
|
|
705
|
+
from .utils import log_error
|
|
706
|
+
|
|
707
|
+
# Log the error with tool context
|
|
708
|
+
log_error(
|
|
709
|
+
f"MCP tool '{tool_name}' failed: {e}",
|
|
710
|
+
extra={"tool_name": tool_name, "exception_type": type(e).__name__},
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Return standardized error response
|
|
714
|
+
return create_mcp_error_response(
|
|
715
|
+
e,
|
|
716
|
+
tool_name=tool_name,
|
|
717
|
+
include_debug_info=include_debug,
|
|
718
|
+
sanitize_sensitive=sanitize_sensitive,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
722
|
+
try:
|
|
723
|
+
return func(*args, **kwargs)
|
|
724
|
+
except Exception as e:
|
|
725
|
+
from .utils import log_error
|
|
726
|
+
|
|
727
|
+
# Log the error with tool context
|
|
728
|
+
log_error(
|
|
729
|
+
f"MCP tool '{tool_name}' failed: {e}",
|
|
730
|
+
extra={"tool_name": tool_name, "exception_type": type(e).__name__},
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# Return standardized error response
|
|
734
|
+
return create_mcp_error_response(
|
|
735
|
+
e,
|
|
736
|
+
tool_name=tool_name,
|
|
737
|
+
include_debug_info=include_debug,
|
|
738
|
+
sanitize_sensitive=sanitize_sensitive,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# Return appropriate wrapper based on function type
|
|
742
|
+
if __import__("asyncio").iscoroutinefunction(func):
|
|
743
|
+
return async_wrapper
|
|
744
|
+
else:
|
|
745
|
+
return sync_wrapper
|
|
746
|
+
|
|
747
|
+
return decorator
|