tree-sitter-analyzer 1.5.0__py3-none-any.whl → 1.6.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.

Potentially problematic release.


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

@@ -419,14 +419,28 @@ class TreeSitterAnalyzerMCPServer:
419
419
  ),
420
420
  Tool(
421
421
  name="analyze_code_structure",
422
- description="Analyze code structure and generate tables with line positions",
422
+ description="Analyze code structure and generate tables with line positions, optionally save to file",
423
423
  inputSchema={
424
424
  "type": "object",
425
425
  "properties": {
426
426
  "file_path": {
427
427
  "type": "string",
428
428
  "description": "Path to the code file (relative to project root)",
429
- }
429
+ },
430
+ "format_type": {
431
+ "type": "string",
432
+ "description": "Table format type",
433
+ "enum": ["full", "compact", "csv", "json"],
434
+ "default": "full",
435
+ },
436
+ "language": {
437
+ "type": "string",
438
+ "description": "Programming language (optional, auto-detected if not specified)",
439
+ },
440
+ "output_file": {
441
+ "type": "string",
442
+ "description": "Optional filename to save output to file (extension auto-detected based on content)",
443
+ },
430
444
  },
431
445
  "required": ["file_path"],
432
446
  "additionalProperties": False,
@@ -522,6 +536,7 @@ class TreeSitterAnalyzerMCPServer:
522
536
  "file_path": arguments["file_path"],
523
537
  "format_type": arguments.get("format_type", "full"),
524
538
  "language": arguments.get("language"),
539
+ "output_file": arguments.get("output_file"),
525
540
  }
526
541
  result = await self.table_format_tool.execute(full_args)
527
542
 
@@ -22,6 +22,7 @@ from ...language_detector import detect_language_from_file
22
22
  from ...table_formatter import TableFormatter
23
23
  from ...utils import setup_logger
24
24
  from ..utils import get_performance_monitor
25
+ from ..utils.file_output_manager import FileOutputManager
25
26
  from .base_tool import BaseMCPTool
26
27
 
27
28
  # Set up logging
@@ -40,6 +41,7 @@ class TableFormatTool(BaseMCPTool):
40
41
  """Initialize the table format tool."""
41
42
  super().__init__(project_root)
42
43
  self.analysis_engine = get_analysis_engine(project_root)
44
+ self.file_output_manager = FileOutputManager(project_root)
43
45
  self.logger = logger
44
46
 
45
47
  def set_project_path(self, project_path: str) -> None:
@@ -51,6 +53,7 @@ class TableFormatTool(BaseMCPTool):
51
53
  """
52
54
  super().set_project_path(project_path)
53
55
  self.analysis_engine = get_analysis_engine(project_path)
56
+ self.file_output_manager.set_project_root(project_path)
54
57
  logger.info(f"TableFormatTool project path updated to: {project_path}")
55
58
 
56
59
  def get_tool_schema(self) -> dict[str, Any]:
@@ -70,13 +73,17 @@ class TableFormatTool(BaseMCPTool):
70
73
  "format_type": {
71
74
  "type": "string",
72
75
  "description": "Table format type",
73
- "enum": ["full", "compact", "csv"],
76
+ "enum": ["full", "compact", "csv", "json"],
74
77
  "default": "full",
75
78
  },
76
79
  "language": {
77
80
  "type": "string",
78
81
  "description": "Programming language (optional, auto-detected if not specified)",
79
82
  },
83
+ "output_file": {
84
+ "type": "string",
85
+ "description": "Optional filename to save output to file (extension auto-detected based on content)",
86
+ },
80
87
  },
81
88
  "required": ["file_path"],
82
89
  "additionalProperties": False,
@@ -111,8 +118,8 @@ class TableFormatTool(BaseMCPTool):
111
118
  format_type = arguments["format_type"]
112
119
  if not isinstance(format_type, str):
113
120
  raise ValueError("format_type must be a string")
114
- if format_type not in ["full", "compact", "csv"]:
115
- raise ValueError("format_type must be one of: full, compact, csv")
121
+ if format_type not in ["full", "compact", "csv", "json"]:
122
+ raise ValueError("format_type must be one of: full, compact, csv, json")
116
123
 
117
124
  # Validate language if provided
118
125
  if "language" in arguments:
@@ -120,6 +127,14 @@ class TableFormatTool(BaseMCPTool):
120
127
  if not isinstance(language, str):
121
128
  raise ValueError("language must be a string")
122
129
 
130
+ # Validate output_file if provided
131
+ if "output_file" in arguments:
132
+ output_file = arguments["output_file"]
133
+ if not isinstance(output_file, str):
134
+ raise ValueError("output_file must be a string")
135
+ if not output_file.strip():
136
+ raise ValueError("output_file cannot be empty")
137
+
123
138
  return True
124
139
 
125
140
  def _convert_parameters(self, parameters: Any) -> list[dict[str, str]]:
@@ -286,6 +301,59 @@ class TableFormatTool(BaseMCPTool):
286
301
  },
287
302
  }
288
303
 
304
+ def _write_output_file(
305
+ self, output_file: str, content: str, format_type: str
306
+ ) -> str:
307
+ """
308
+ Write output content to file with automatic extension detection.
309
+
310
+ Args:
311
+ output_file: Base filename for output
312
+ content: Content to write
313
+ format_type: Format type (full, compact, csv, json)
314
+
315
+ Returns:
316
+ Full path of the written file
317
+ """
318
+ from pathlib import Path
319
+
320
+ # Determine file extension based on format type
321
+ extension_map = {
322
+ "full": ".md",
323
+ "compact": ".md",
324
+ "csv": ".csv",
325
+ "json": ".json",
326
+ }
327
+
328
+ # Get the appropriate extension
329
+ extension = extension_map.get(format_type, ".txt")
330
+
331
+ # Add extension if not already present
332
+ if not output_file.endswith(extension):
333
+ output_file = output_file + extension
334
+
335
+ # Resolve output path relative to project root
336
+ output_path = self.path_resolver.resolve(output_file)
337
+
338
+ # Security validation for output path
339
+ is_valid, error_msg = self.security_validator.validate_file_path(output_path)
340
+ if not is_valid:
341
+ raise ValueError(f"Invalid output file path: {error_msg}")
342
+
343
+ # Ensure output directory exists
344
+ output_dir = Path(output_path).parent
345
+ output_dir.mkdir(parents=True, exist_ok=True)
346
+
347
+ # Write content to file
348
+ try:
349
+ with open(output_path, "w", encoding="utf-8") as f:
350
+ f.write(content)
351
+ self.logger.info(f"Output written to file: {output_path}")
352
+ return output_path
353
+ except Exception as e:
354
+ self.logger.error(f"Failed to write output file {output_path}: {e}")
355
+ raise RuntimeError(f"Failed to write output file: {e}") from e
356
+
289
357
  async def execute(self, args: dict[str, Any]) -> dict[str, Any]:
290
358
  """Execute code structure analysis tool."""
291
359
  try:
@@ -296,6 +364,7 @@ class TableFormatTool(BaseMCPTool):
296
364
  file_path = args["file_path"]
297
365
  format_type = args.get("format_type", "full")
298
366
  language = args.get("language")
367
+ output_file = args.get("output_file")
299
368
 
300
369
  # Resolve file path using common path resolver
301
370
  resolved_path = self.path_resolver.resolve(file_path)
@@ -322,6 +391,12 @@ class TableFormatTool(BaseMCPTool):
322
391
  language, max_length=50
323
392
  )
324
393
 
394
+ # Sanitize output_file input
395
+ if output_file:
396
+ output_file = self.security_validator.sanitize_input(
397
+ output_file, max_length=255
398
+ )
399
+
325
400
  # Validate file exists
326
401
  if not Path(resolved_path).exists():
327
402
  # Tests expect FileNotFoundError here
@@ -377,7 +452,7 @@ class TableFormatTool(BaseMCPTool):
377
452
  "total_lines": stats.get("total_lines", 0),
378
453
  }
379
454
 
380
- return {
455
+ result = {
381
456
  "table_output": table_output,
382
457
  "format_type": format_type,
383
458
  "file_path": file_path,
@@ -385,6 +460,33 @@ class TableFormatTool(BaseMCPTool):
385
460
  "metadata": metadata,
386
461
  }
387
462
 
463
+ # Handle file output if requested
464
+ if output_file:
465
+ try:
466
+ # Generate base name from original file path if not provided
467
+ if not output_file or output_file.strip() == "":
468
+ base_name = Path(file_path).stem + "_analysis"
469
+ else:
470
+ base_name = output_file
471
+
472
+ # Save to file with automatic extension detection
473
+ saved_file_path = self.file_output_manager.save_to_file(
474
+ content=table_output,
475
+ base_name=base_name
476
+ )
477
+
478
+ result["output_file_path"] = saved_file_path
479
+ result["file_saved"] = True
480
+
481
+ self.logger.info(f"Analysis output saved to: {saved_file_path}")
482
+
483
+ except Exception as e:
484
+ self.logger.error(f"Failed to save output to file: {e}")
485
+ result["file_save_error"] = str(e)
486
+ result["file_saved"] = False
487
+
488
+ return result
489
+
388
490
  except Exception as e:
389
491
  self.logger.error(f"Error in code structure analysis tool: {e}")
390
492
  raise
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ File Output Manager for MCP Tools
4
+
5
+ This module provides functionality to save analysis results to files with
6
+ appropriate extensions based on content type, with security validation.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ...utils import setup_logger
15
+
16
+ # Set up logging
17
+ logger = setup_logger(__name__)
18
+
19
+
20
+ class FileOutputManager:
21
+ """
22
+ Manages file output for analysis results with automatic extension detection
23
+ and security validation.
24
+ """
25
+
26
+ def __init__(self, project_root: str | None = None):
27
+ """
28
+ Initialize the file output manager.
29
+
30
+ Args:
31
+ project_root: Optional project root directory for fallback output path
32
+ """
33
+ self.project_root = project_root
34
+ self._output_path = None
35
+ self._initialize_output_path()
36
+
37
+ def _initialize_output_path(self) -> None:
38
+ """Initialize the output path from environment variables or project root."""
39
+ # Priority 1: Environment variable TREE_SITTER_OUTPUT_PATH
40
+ env_output_path = os.environ.get("TREE_SITTER_OUTPUT_PATH")
41
+ if env_output_path and Path(env_output_path).exists():
42
+ self._output_path = env_output_path
43
+ logger.info(f"Using output path from environment: {self._output_path}")
44
+ return
45
+
46
+ # Priority 2: Project root if available
47
+ if self.project_root and Path(self.project_root).exists():
48
+ self._output_path = self.project_root
49
+ logger.info(f"Using project root as output path: {self._output_path}")
50
+ return
51
+
52
+ # Priority 3: Current working directory as fallback
53
+ self._output_path = str(Path.cwd())
54
+ logger.warning(f"Using current directory as output path: {self._output_path}")
55
+
56
+ def get_output_path(self) -> str:
57
+ """
58
+ Get the current output path.
59
+
60
+ Returns:
61
+ Current output path
62
+ """
63
+ return self._output_path or str(Path.cwd())
64
+
65
+ def set_output_path(self, output_path: str) -> None:
66
+ """
67
+ Set a custom output path.
68
+
69
+ Args:
70
+ output_path: New output path
71
+
72
+ Raises:
73
+ ValueError: If the path doesn't exist or is not a directory
74
+ """
75
+ path_obj = Path(output_path)
76
+ if not path_obj.exists():
77
+ raise ValueError(f"Output path does not exist: {output_path}")
78
+ if not path_obj.is_dir():
79
+ raise ValueError(f"Output path is not a directory: {output_path}")
80
+
81
+ self._output_path = str(path_obj.resolve())
82
+ logger.info(f"Output path updated to: {self._output_path}")
83
+
84
+ def detect_content_type(self, content: str) -> str:
85
+ """
86
+ Detect content type based on content structure.
87
+
88
+ Args:
89
+ content: Content to analyze
90
+
91
+ Returns:
92
+ Detected content type ('json', 'csv', 'markdown', or 'text')
93
+ """
94
+ content_stripped = content.strip()
95
+
96
+ # Check for JSON
97
+ if content_stripped.startswith(("{", "[")):
98
+ try:
99
+ json.loads(content_stripped)
100
+ return "json"
101
+ except (json.JSONDecodeError, ValueError):
102
+ pass
103
+
104
+ # Check for CSV (simple heuristic)
105
+ lines = content_stripped.split("\n")
106
+ if len(lines) >= 2:
107
+ # Check if first few lines have consistent comma separation
108
+ first_line_commas = lines[0].count(",")
109
+ if first_line_commas > 0:
110
+ # Check if at least 2 more lines have similar comma counts
111
+ similar_comma_lines = sum(
112
+ 1 for line in lines[1:4] if abs(line.count(",") - first_line_commas) <= 1
113
+ )
114
+ if similar_comma_lines >= 1:
115
+ return "csv"
116
+
117
+ # Check for Markdown (simple heuristic)
118
+ markdown_indicators = ["#", "##", "###", "|", "```", "*", "-", "+"]
119
+ if any(content_stripped.startswith(indicator) for indicator in markdown_indicators):
120
+ return "markdown"
121
+
122
+ # Check for table format (pipe-separated)
123
+ if "|" in content and "\n" in content:
124
+ lines = content_stripped.split("\n")
125
+ pipe_lines = sum(1 for line in lines if "|" in line)
126
+ if pipe_lines >= 2: # At least header and one data row
127
+ return "markdown"
128
+
129
+ # Default to text
130
+ return "text"
131
+
132
+ def get_file_extension(self, content_type: str) -> str:
133
+ """
134
+ Get file extension for content type.
135
+
136
+ Args:
137
+ content_type: Content type ('json', 'csv', 'markdown', 'text')
138
+
139
+ Returns:
140
+ File extension including the dot
141
+ """
142
+ extension_map = {
143
+ "json": ".json",
144
+ "csv": ".csv",
145
+ "markdown": ".md",
146
+ "text": ".txt"
147
+ }
148
+ return extension_map.get(content_type, ".txt")
149
+
150
+ def generate_output_filename(self, base_name: str, content: str) -> str:
151
+ """
152
+ Generate output filename with appropriate extension.
153
+
154
+ Args:
155
+ base_name: Base filename (without extension)
156
+ content: Content to analyze for type detection
157
+
158
+ Returns:
159
+ Complete filename with extension
160
+ """
161
+ content_type = self.detect_content_type(content)
162
+ extension = self.get_file_extension(content_type)
163
+
164
+ # Remove existing extension if present
165
+ base_name_clean = Path(base_name).stem
166
+
167
+ return f"{base_name_clean}{extension}"
168
+
169
+ def save_to_file(self, content: str, filename: str | None = None, base_name: str | None = None) -> str:
170
+ """
171
+ Save content to file with automatic extension detection.
172
+
173
+ Args:
174
+ content: Content to save
175
+ filename: Optional specific filename (overrides base_name)
176
+ base_name: Optional base name for auto-generated filename
177
+
178
+ Returns:
179
+ Path to the saved file
180
+
181
+ Raises:
182
+ ValueError: If neither filename nor base_name is provided
183
+ OSError: If file cannot be written
184
+ """
185
+ if not filename and not base_name:
186
+ raise ValueError("Either filename or base_name must be provided")
187
+
188
+ output_path = Path(self.get_output_path())
189
+
190
+ if filename:
191
+ # Use provided filename as-is
192
+ output_file = output_path / filename
193
+ else:
194
+ # Generate filename with appropriate extension
195
+ generated_filename = self.generate_output_filename(base_name, content)
196
+ output_file = output_path / generated_filename
197
+
198
+ # Ensure output directory exists
199
+ output_file.parent.mkdir(parents=True, exist_ok=True)
200
+
201
+ # Write content to file
202
+ try:
203
+ with open(output_file, "w", encoding="utf-8") as f:
204
+ f.write(content)
205
+
206
+ logger.info(f"Content saved to file: {output_file}")
207
+ return str(output_file)
208
+
209
+ except OSError as e:
210
+ logger.error(f"Failed to save content to file {output_file}: {e}")
211
+ raise
212
+
213
+ def validate_output_path(self, path: str) -> tuple[bool, str | None]:
214
+ """
215
+ Validate if a path is safe for output.
216
+
217
+ Args:
218
+ path: Path to validate
219
+
220
+ Returns:
221
+ Tuple of (is_valid, error_message)
222
+ """
223
+ try:
224
+ path_obj = Path(path).resolve()
225
+
226
+ # Check if parent directory exists or can be created
227
+ parent_dir = path_obj.parent
228
+ if not parent_dir.exists():
229
+ try:
230
+ parent_dir.mkdir(parents=True, exist_ok=True)
231
+ except OSError as e:
232
+ return False, f"Cannot create parent directory: {e}"
233
+
234
+ # Check if we can write to the directory
235
+ if not os.access(parent_dir, os.W_OK):
236
+ return False, f"No write permission for directory: {parent_dir}"
237
+
238
+ # Check if file already exists and is writable
239
+ if path_obj.exists() and not os.access(path_obj, os.W_OK):
240
+ return False, f"No write permission for existing file: {path_obj}"
241
+
242
+ return True, None
243
+
244
+ except Exception as e:
245
+ return False, f"Path validation error: {str(e)}"
246
+
247
+ def set_project_root(self, project_root: str) -> None:
248
+ """
249
+ Update the project root and reinitialize output path if needed.
250
+
251
+ Args:
252
+ project_root: New project root directory
253
+ """
254
+ self.project_root = project_root
255
+ # Only reinitialize if we don't have an explicit output path from environment
256
+ if not os.environ.get("TREE_SITTER_OUTPUT_PATH"):
257
+ self._initialize_output_path()
@@ -72,6 +72,10 @@ class Function(CodeElement):
72
72
  is_arrow: bool = False
73
73
  is_method: bool = False
74
74
  framework_type: str | None = None
75
+ # Python-specific fields
76
+ is_property: bool = False
77
+ is_classmethod: bool = False
78
+ is_staticmethod: bool = False
75
79
 
76
80
 
77
81
  @dataclass(frozen=False)
@@ -99,6 +103,10 @@ class Class(CodeElement):
99
103
  is_react_component: bool = False
100
104
  framework_type: str | None = None
101
105
  is_exported: bool = False
106
+ # Python-specific fields
107
+ is_dataclass: bool = False
108
+ is_abstract: bool = False
109
+ is_exception: bool = False
102
110
 
103
111
 
104
112
  @dataclass(frozen=False)