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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Table Format Tool for MCP
4
+
5
+ This tool provides code structure analysis and table formatting through the
6
+ MCP protocol, converting analysis results into structured table formats.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from ...constants import (
13
+ ELEMENT_TYPE_CLASS,
14
+ ELEMENT_TYPE_FUNCTION,
15
+ ELEMENT_TYPE_IMPORT,
16
+ ELEMENT_TYPE_PACKAGE,
17
+ ELEMENT_TYPE_VARIABLE,
18
+ is_element_of_type,
19
+ )
20
+ from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
21
+ from ...formatters.formatter_registry import FormatterRegistry
22
+ from ...formatters.language_formatter_factory import create_language_formatter
23
+ from ...language_detector import detect_language_from_file
24
+ from ...utils import setup_logger
25
+ from ..utils import get_performance_monitor
26
+ from ..utils.file_output_manager import FileOutputManager
27
+ from .base_tool import BaseMCPTool
28
+
29
+ # Set up logging
30
+ logger = setup_logger(__name__)
31
+
32
+
33
+ class TableFormatTool(BaseMCPTool):
34
+ """
35
+ MCP Tool for code structure analysis and table formatting.
36
+
37
+ This tool integrates with existing analyzer components to provide
38
+ structured table output through the MCP protocol.
39
+ """
40
+
41
+ def __init__(self, project_root: str | None = None) -> None:
42
+ """Initialize the table format tool."""
43
+ super().__init__(project_root)
44
+ self.analysis_engine = get_analysis_engine(project_root)
45
+ self.file_output_manager = FileOutputManager.get_managed_instance(project_root)
46
+ self.logger = logger
47
+
48
+ def set_project_path(self, project_path: str) -> None:
49
+ """
50
+ Update the project path for all components.
51
+
52
+ Args:
53
+ project_path: New project root directory
54
+ """
55
+ super().set_project_path(project_path)
56
+ self.analysis_engine = get_analysis_engine(project_path)
57
+ self.file_output_manager = FileOutputManager.get_managed_instance(project_path)
58
+ logger.info(f"TableFormatTool project path updated to: {project_path}")
59
+
60
+ def get_tool_schema(self) -> dict[str, Any]:
61
+ """
62
+ Get the MCP tool schema for analyze_code_structure.
63
+
64
+ Returns:
65
+ Dictionary containing the tool schema
66
+ """
67
+ return {
68
+ "type": "object",
69
+ "properties": {
70
+ "file_path": {
71
+ "type": "string",
72
+ "description": "Path to the code file to analyze and format",
73
+ },
74
+ "format_type": {
75
+ "type": "string",
76
+ "description": "Table format type",
77
+ "enum": ["full", "compact", "csv"],
78
+ "default": "full",
79
+ },
80
+ "language": {
81
+ "type": "string",
82
+ "description": "Programming language (optional, auto-detected if not specified)",
83
+ },
84
+ "output_file": {
85
+ "type": "string",
86
+ "description": "Optional filename to save output to file "
87
+ "(extension auto-detected based on content)",
88
+ },
89
+ "suppress_output": {
90
+ "type": "boolean",
91
+ "description": "When true and output_file is specified, "
92
+ "suppress table_output in response to save tokens",
93
+ "default": False,
94
+ },
95
+ },
96
+ "required": ["file_path"],
97
+ "additionalProperties": False,
98
+ }
99
+
100
+ def validate_arguments(self, arguments: dict[str, Any]) -> bool:
101
+ """
102
+ Validate tool arguments.
103
+
104
+ Args:
105
+ arguments: Dictionary of arguments to validate
106
+
107
+ Returns:
108
+ True if arguments are valid
109
+
110
+ Raises:
111
+ ValueError: If arguments are invalid
112
+ """
113
+ # Check required fields
114
+ if "file_path" not in arguments:
115
+ raise ValueError("Required field 'file_path' is missing")
116
+
117
+ # Validate file_path
118
+ file_path = arguments["file_path"]
119
+ if not isinstance(file_path, str):
120
+ raise ValueError("file_path must be a string")
121
+ if not file_path.strip():
122
+ raise ValueError("file_path cannot be empty")
123
+
124
+ # Validate format_type if provided
125
+ if "format_type" in arguments:
126
+ format_type = arguments["format_type"]
127
+ if not isinstance(format_type, str):
128
+ raise ValueError("format_type must be a string")
129
+
130
+ # Only support v1.6.1.4 specification formats (no HTML formats)
131
+ allowed_formats = ["full", "compact", "csv"]
132
+ if format_type not in allowed_formats:
133
+ raise ValueError(
134
+ f"format_type must be one of: {', '.join(sorted(allowed_formats))}"
135
+ )
136
+
137
+ # Validate language if provided
138
+ if "language" in arguments:
139
+ language = arguments["language"]
140
+ if not isinstance(language, str):
141
+ raise ValueError("language must be a string")
142
+
143
+ # Validate output_file if provided
144
+ if "output_file" in arguments:
145
+ output_file = arguments["output_file"]
146
+ if not isinstance(output_file, str):
147
+ raise ValueError("output_file must be a string")
148
+ if not output_file.strip():
149
+ raise ValueError("output_file cannot be empty")
150
+
151
+ # Validate suppress_output if provided
152
+ if "suppress_output" in arguments:
153
+ suppress_output = arguments["suppress_output"]
154
+ if not isinstance(suppress_output, bool):
155
+ raise ValueError("suppress_output must be a boolean")
156
+
157
+ return True
158
+
159
+ def _convert_parameters(self, parameters: Any) -> list[dict[str, str]]:
160
+ """Convert parameters to expected format"""
161
+ result = []
162
+ for param in parameters:
163
+ if isinstance(param, dict):
164
+ result.append(
165
+ {
166
+ "name": param.get("name", "param"),
167
+ "type": param.get("type", "Object"),
168
+ }
169
+ )
170
+ else:
171
+ result.append(
172
+ {
173
+ "name": getattr(param, "name", "param"),
174
+ "type": getattr(param, "param_type", "Object"),
175
+ }
176
+ )
177
+ return result
178
+
179
+ def _get_method_modifiers(self, method: Any) -> list[str]:
180
+ """Extract method modifiers as a list"""
181
+ modifiers = []
182
+ if getattr(method, "is_static", False):
183
+ modifiers.append("static")
184
+ if getattr(method, "is_final", False):
185
+ modifiers.append("final")
186
+ if getattr(method, "is_abstract", False):
187
+ modifiers.append("abstract")
188
+ return modifiers
189
+
190
+ def _get_method_parameters(self, method: Any) -> list[dict[str, str]]:
191
+ """Get method parameters in correct format for TableFormatter"""
192
+ parameters = getattr(method, "parameters", [])
193
+
194
+ # If parameters is already a list of strings, convert to dict format
195
+ if parameters and isinstance(parameters[0], str):
196
+ result = []
197
+ for param_str in parameters:
198
+ parts = param_str.strip().split()
199
+ if len(parts) >= 2:
200
+ param_type = " ".join(
201
+ parts[:-1]
202
+ ) # Everything except last part is type
203
+ param_name = parts[-1] # Last part is name
204
+ result.append({"name": param_name, "type": param_type})
205
+ elif len(parts) == 1:
206
+ # Only type, no name
207
+ result.append({"name": "param", "type": parts[0]})
208
+ return result
209
+
210
+ # Fallback to original conversion method
211
+ return self._convert_parameters(parameters)
212
+
213
+ def _get_field_modifiers(self, field: Any) -> list[str]:
214
+ """Extract field modifiers as a list"""
215
+ modifiers = []
216
+
217
+ # Add visibility to modifiers for CLI compatibility
218
+ visibility = getattr(field, "visibility", "private")
219
+ if visibility and visibility != "package":
220
+ modifiers.append(visibility)
221
+
222
+ if getattr(field, "is_static", False):
223
+ modifiers.append("static")
224
+ if getattr(field, "is_final", False):
225
+ modifiers.append("final")
226
+ return modifiers
227
+
228
+ def _convert_analysis_result_to_dict(self, result: Any) -> dict[str, Any]:
229
+ """Convert AnalysisResult to dictionary format expected by TableFormatter"""
230
+ # Extract elements by type
231
+ classes = [
232
+ e for e in result.elements if is_element_of_type(e, ELEMENT_TYPE_CLASS)
233
+ ]
234
+ methods = [
235
+ e for e in result.elements if is_element_of_type(e, ELEMENT_TYPE_FUNCTION)
236
+ ]
237
+ fields = [
238
+ e for e in result.elements if is_element_of_type(e, ELEMENT_TYPE_VARIABLE)
239
+ ]
240
+ imports = [
241
+ e for e in result.elements if is_element_of_type(e, ELEMENT_TYPE_IMPORT)
242
+ ]
243
+ packages = [
244
+ e for e in result.elements if is_element_of_type(e, ELEMENT_TYPE_PACKAGE)
245
+ ]
246
+
247
+ # Convert package to expected format
248
+ package_info = None
249
+ if packages:
250
+ package_info = {"name": packages[0].name}
251
+
252
+ return {
253
+ "success": True,
254
+ "file_path": result.file_path,
255
+ "language": result.language,
256
+ "package": package_info,
257
+ "classes": [
258
+ {
259
+ "name": getattr(cls, "name", "unknown"),
260
+ "line_range": {
261
+ "start": getattr(cls, "start_line", 0),
262
+ "end": getattr(cls, "end_line", 0),
263
+ },
264
+ "type": getattr(cls, "class_type", "class"),
265
+ "visibility": "public", # Force all classes to public
266
+ "extends": getattr(cls, "extends_class", None),
267
+ "implements": getattr(cls, "implements_interfaces", []),
268
+ "annotations": [],
269
+ }
270
+ for cls in classes
271
+ ],
272
+ "methods": [
273
+ {
274
+ "name": getattr(method, "name", "unknown"),
275
+ "line_range": {
276
+ "start": getattr(method, "start_line", 0),
277
+ "end": getattr(method, "end_line", 0),
278
+ },
279
+ "return_type": getattr(method, "return_type", "void"),
280
+ "parameters": self._get_method_parameters(method),
281
+ "visibility": getattr(method, "visibility", "public"),
282
+ "is_static": getattr(method, "is_static", False),
283
+ "is_constructor": getattr(method, "is_constructor", False),
284
+ "complexity_score": getattr(method, "complexity_score", 0),
285
+ "modifiers": self._get_method_modifiers(method),
286
+ "annotations": [],
287
+ }
288
+ for method in methods
289
+ ],
290
+ "fields": [
291
+ {
292
+ "name": getattr(field, "name", "unknown"),
293
+ "type": getattr(field, "field_type", "Object"),
294
+ "line_range": {
295
+ "start": getattr(field, "start_line", 0),
296
+ "end": getattr(field, "end_line", 0),
297
+ },
298
+ "visibility": getattr(field, "visibility", "private"),
299
+ "modifiers": self._get_field_modifiers(field),
300
+ "annotations": [],
301
+ }
302
+ for field in fields
303
+ ],
304
+ "imports": [
305
+ {
306
+ "name": getattr(imp, "name", "unknown"),
307
+ "statement": getattr(
308
+ imp, "import_statement", getattr(imp, "name", "")
309
+ ), # Use import_statement if available, fallback to name
310
+ "is_static": getattr(imp, "is_static", False),
311
+ "is_wildcard": getattr(imp, "is_wildcard", False),
312
+ }
313
+ for imp in imports
314
+ ],
315
+ "statistics": {
316
+ "class_count": len(classes),
317
+ "method_count": len(methods),
318
+ "field_count": len(fields),
319
+ "import_count": len(imports),
320
+ "total_lines": result.line_count,
321
+ },
322
+ }
323
+
324
+ def _write_output_file(
325
+ self, output_file: str, content: str, format_type: str
326
+ ) -> str:
327
+ """
328
+ Write output content to file with automatic extension detection.
329
+
330
+ Args:
331
+ output_file: Base filename for output
332
+ content: Content to write
333
+ format_type: Format type (full, compact, csv, json)
334
+
335
+ Returns:
336
+ Full path of the written file
337
+ """
338
+ from pathlib import Path
339
+
340
+ # Determine file extension based on format type
341
+ extension_map = {
342
+ "full": ".md",
343
+ "compact": ".md",
344
+ "csv": ".csv",
345
+ "json": ".json",
346
+ }
347
+
348
+ # Get the appropriate extension
349
+ extension = extension_map.get(format_type, ".txt")
350
+
351
+ # Add extension if not already present
352
+ if not output_file.endswith(extension):
353
+ output_file = output_file + extension
354
+
355
+ # Resolve output path relative to project root
356
+ output_path = self.path_resolver.resolve(output_file)
357
+
358
+ # Security validation for output path
359
+ is_valid, error_msg = self.security_validator.validate_file_path(output_path)
360
+ if not is_valid:
361
+ raise ValueError(f"Invalid output file path: {error_msg}")
362
+
363
+ # Ensure output directory exists
364
+ output_dir = Path(output_path).parent
365
+ output_dir.mkdir(parents=True, exist_ok=True)
366
+
367
+ # Write content to file
368
+ try:
369
+ from ...encoding_utils import write_file_safe
370
+
371
+ write_file_safe(output_path, content)
372
+ self.logger.info(f"Output written to file: {output_path}")
373
+ return output_path
374
+ except Exception as e:
375
+ self.logger.error(f"Failed to write output file {output_path}: {e}")
376
+ raise RuntimeError(f"Failed to write output file: {e}") from e
377
+
378
+ async def execute(self, args: dict[str, Any]) -> dict[str, Any]:
379
+ """Execute code structure analysis tool."""
380
+ try:
381
+ # Validate arguments first
382
+ if "file_path" not in args:
383
+ raise ValueError("file_path is required")
384
+
385
+ file_path = args["file_path"]
386
+ format_type = args.get("format_type", "full")
387
+ language = args.get("language")
388
+ output_file = args.get("output_file")
389
+ suppress_output = args.get("suppress_output", False)
390
+
391
+ # Security validation BEFORE path resolution to catch symlinks
392
+ is_valid, error_msg = self.security_validator.validate_file_path(file_path)
393
+ if not is_valid:
394
+ self.logger.warning(
395
+ f"Security validation failed for file path: {file_path} - "
396
+ f"{error_msg}"
397
+ )
398
+ raise ValueError(f"Invalid file path: {error_msg}")
399
+
400
+ # Resolve file path using common path resolver
401
+ resolved_path = self.path_resolver.resolve(file_path)
402
+
403
+ # Additional security validation on resolved path
404
+ is_valid, error_msg = self.security_validator.validate_file_path(
405
+ resolved_path
406
+ )
407
+ if not is_valid:
408
+ self.logger.warning(
409
+ f"Security validation failed for resolved path: "
410
+ f"{resolved_path} - {error_msg}"
411
+ )
412
+ raise ValueError(f"Invalid resolved path: {error_msg}")
413
+
414
+ # Sanitize format_type input
415
+ if format_type:
416
+ format_type = self.security_validator.sanitize_input(
417
+ format_type, max_length=50
418
+ )
419
+
420
+ # Sanitize language input
421
+ if language:
422
+ language = self.security_validator.sanitize_input(
423
+ language, max_length=50
424
+ )
425
+
426
+ # Sanitize output_file input
427
+ if output_file:
428
+ output_file = self.security_validator.sanitize_input(
429
+ output_file, max_length=255
430
+ )
431
+
432
+ # Sanitize suppress_output input (boolean, validate type)
433
+ if suppress_output is not None and not isinstance(suppress_output, bool):
434
+ raise ValueError("suppress_output must be a boolean")
435
+
436
+ # Validate file exists
437
+ if not Path(resolved_path).exists():
438
+ # Tests expect FileNotFoundError here
439
+ raise FileNotFoundError(f"File not found: {file_path}")
440
+
441
+ # Detect language if not provided
442
+ if not language:
443
+ language = detect_language_from_file(resolved_path)
444
+
445
+ # Use performance monitoring
446
+ monitor = get_performance_monitor()
447
+ with monitor.measure_operation("code_structure_analysis"):
448
+ # Analyze structure using the unified analysis engine
449
+ request = AnalysisRequest(
450
+ file_path=resolved_path,
451
+ language=language,
452
+ include_complexity=True,
453
+ include_details=True,
454
+ )
455
+ structure_result = await self.analysis_engine.analyze(request)
456
+
457
+ if structure_result is None:
458
+ raise RuntimeError(
459
+ f"Failed to analyze structure for file: {file_path}"
460
+ )
461
+
462
+ # Always convert analysis result to dict for metadata extraction
463
+ structure_dict = self._convert_analysis_result_to_dict(structure_result)
464
+
465
+ # Check if we have a language-specific formatter (e.g., SQL)
466
+ language_formatter = create_language_formatter(language)
467
+ if language_formatter:
468
+ # Use language-specific formatter
469
+ table_output = language_formatter.format_table(
470
+ structure_dict, format_type
471
+ )
472
+ # Use legacy formatter directly for core formats (v1.6.1.4 compatibility)
473
+ elif format_type in ["full", "compact", "csv"]:
474
+ from ...legacy_table_formatter import LegacyTableFormatter
475
+
476
+ legacy_formatter = LegacyTableFormatter(
477
+ format_type=format_type,
478
+ language=language,
479
+ include_javadoc=False,
480
+ )
481
+ table_output = legacy_formatter.format_structure(structure_dict)
482
+ # Use FormatterRegistry for extended formats
483
+ elif FormatterRegistry.is_format_supported(format_type):
484
+ registry_formatter = FormatterRegistry.get_formatter(format_type)
485
+ table_output = registry_formatter.format(structure_result.elements)
486
+ else:
487
+ # Unsupported format
488
+ raise ValueError(f"Unsupported format type: {format_type}")
489
+
490
+ # Ensure output format matches CLI exactly
491
+ # Fix line ending differences: normalize to Unix-style LF (\n)
492
+ table_output = table_output.replace("\r\n", "\n").replace("\r", "\n")
493
+
494
+ # CLI uses sys.stdout.buffer.write() which doesn't add trailing newline
495
+ # Ensure MCP output matches this behavior exactly
496
+ # Remove any trailing whitespace and newlines to match CLI output
497
+ table_output = table_output.rstrip()
498
+
499
+ # Extract metadata from structure dict
500
+ metadata = {}
501
+ if "statistics" in structure_dict:
502
+ stats = structure_dict["statistics"]
503
+ metadata = {
504
+ "classes_count": stats.get("class_count", 0),
505
+ "methods_count": stats.get("method_count", 0),
506
+ "fields_count": stats.get("field_count", 0),
507
+ "total_lines": stats.get("total_lines", 0),
508
+ }
509
+
510
+ # Build result - include table_output based on suppress_output
511
+ result = {
512
+ "success": True,
513
+ "format_type": format_type,
514
+ "file_path": file_path,
515
+ "language": language,
516
+ "metadata": metadata,
517
+ }
518
+
519
+ # Include table_output if not suppressed or no output file
520
+ if not suppress_output or not output_file:
521
+ result["table_output"] = table_output
522
+
523
+ # Handle file output if requested
524
+ if output_file:
525
+ try:
526
+ # Generate base name from original file path if not provided
527
+ if not output_file or output_file.strip() == "":
528
+ base_name = Path(file_path).stem + "_analysis"
529
+ else:
530
+ base_name = output_file
531
+
532
+ # Save to file with automatic extension detection
533
+ saved_file_path = self.file_output_manager.save_to_file(
534
+ content=table_output, base_name=base_name
535
+ )
536
+
537
+ result["output_file_path"] = saved_file_path
538
+ result["file_saved"] = True
539
+
540
+ self.logger.info(f"Analysis output saved to: {saved_file_path}")
541
+
542
+ except Exception as e:
543
+ self.logger.error(f"Failed to save output to file: {e}")
544
+ result["file_save_error"] = str(e)
545
+ result["file_saved"] = False
546
+
547
+ return result
548
+
549
+ except Exception as e:
550
+ self.logger.error(f"Error in code structure analysis tool: {e}")
551
+ raise
552
+
553
+ def get_tool_definition(self) -> dict[str, Any]:
554
+ """
555
+ Get the MCP tool definition for analyze_code_structure.
556
+
557
+ Returns:
558
+ Tool definition dictionary compatible with MCP server
559
+ """
560
+ return {
561
+ "name": "analyze_code_structure",
562
+ "description": (
563
+ "Analyze code structure and generate detailed overview tables "
564
+ "(classes, methods, fields) with line positions for large files, "
565
+ "optionally save to file"
566
+ ),
567
+ "inputSchema": self.get_tool_schema(),
568
+ }
569
+
570
+
571
+ # Tool instance for easy access
572
+ table_format_tool = TableFormatTool()