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,217 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ File Output Manager Factory
4
+
5
+ This module provides a Managed Singleton Factory Pattern for FileOutputManager
6
+ to prevent duplicate initialization and ensure consistent instance management
7
+ across MCP tools.
8
+ """
9
+
10
+ import threading
11
+ from pathlib import Path
12
+
13
+ from ...utils import setup_logger
14
+ from .file_output_manager import FileOutputManager
15
+
16
+ # Set up logging
17
+ logger = setup_logger(__name__)
18
+
19
+
20
+ class FileOutputManagerFactory:
21
+ """
22
+ Factory class that manages FileOutputManager instances using a Managed Singleton
23
+ pattern. Each project root gets its own singleton instance, ensuring consistency
24
+ across MCP tools while preventing duplicate initialization.
25
+ """
26
+
27
+ # Class-level lock for thread safety
28
+ _lock = threading.RLock()
29
+
30
+ # Dictionary to store instances by project root
31
+ _instances: dict[str, FileOutputManager] = {}
32
+
33
+ @classmethod
34
+ def get_instance(cls, project_root: str | None = None) -> FileOutputManager:
35
+ """
36
+ Get or create a FileOutputManager instance for the specified project root.
37
+
38
+ This method implements the Managed Singleton pattern - one instance per
39
+ project root, ensuring consistency across all MCP tools.
40
+
41
+ Args:
42
+ project_root: Project root directory. If None, uses current working directory.
43
+
44
+ Returns:
45
+ FileOutputManager instance for the specified project root
46
+ """
47
+ # Normalize project root path
48
+ normalized_root = cls._normalize_project_root(project_root)
49
+
50
+ # Double-checked locking pattern for thread safety
51
+ if normalized_root not in cls._instances:
52
+ with cls._lock:
53
+ if normalized_root not in cls._instances:
54
+ logger.info(
55
+ f"Creating new FileOutputManager instance for project root: {normalized_root}"
56
+ )
57
+ cls._instances[normalized_root] = FileOutputManager(normalized_root)
58
+ else:
59
+ logger.debug(
60
+ f"Using existing FileOutputManager instance for project root: {normalized_root}"
61
+ )
62
+ else:
63
+ logger.debug(
64
+ f"Using existing FileOutputManager instance for project root: {normalized_root}"
65
+ )
66
+
67
+ return cls._instances[normalized_root]
68
+
69
+ @classmethod
70
+ def _normalize_project_root(cls, project_root: str | None) -> str:
71
+ """
72
+ Normalize project root path for consistent key generation.
73
+
74
+ Args:
75
+ project_root: Raw project root path
76
+
77
+ Returns:
78
+ Normalized absolute path string
79
+ """
80
+ if project_root is None:
81
+ return str(Path.cwd().resolve())
82
+
83
+ try:
84
+ return str(Path(project_root).resolve())
85
+ except Exception as e:
86
+ logger.warning(f"Failed to resolve project root path '{project_root}': {e}")
87
+ return str(Path.cwd().resolve())
88
+
89
+ @classmethod
90
+ def clear_instance(cls, project_root: str | None = None) -> bool:
91
+ """
92
+ Clear a specific FileOutputManager instance from the factory.
93
+
94
+ This method is primarily for testing purposes or when you need to
95
+ force recreation of an instance.
96
+
97
+ Args:
98
+ project_root: Project root directory. If None, uses current working directory.
99
+
100
+ Returns:
101
+ True if instance was cleared, False if it didn't exist
102
+ """
103
+ normalized_root = cls._normalize_project_root(project_root)
104
+
105
+ with cls._lock:
106
+ if normalized_root in cls._instances:
107
+ logger.info(
108
+ f"Clearing FileOutputManager instance for project root: {normalized_root}"
109
+ )
110
+ del cls._instances[normalized_root]
111
+ return True
112
+ else:
113
+ logger.debug(
114
+ f"No FileOutputManager instance found for project root: {normalized_root}"
115
+ )
116
+ return False
117
+
118
+ @classmethod
119
+ def clear_all_instances(cls) -> int:
120
+ """
121
+ Clear all FileOutputManager instances from the factory.
122
+
123
+ This method is primarily for testing purposes or cleanup.
124
+
125
+ Returns:
126
+ Number of instances that were cleared
127
+ """
128
+ with cls._lock:
129
+ count = len(cls._instances)
130
+ if count > 0:
131
+ logger.info(f"Clearing all {count} FileOutputManager instances")
132
+ cls._instances.clear()
133
+ else:
134
+ logger.debug("No FileOutputManager instances to clear")
135
+ return count
136
+
137
+ @classmethod
138
+ def get_instance_count(cls) -> int:
139
+ """
140
+ Get the current number of managed instances.
141
+
142
+ Returns:
143
+ Number of currently managed FileOutputManager instances
144
+ """
145
+ with cls._lock:
146
+ return len(cls._instances)
147
+
148
+ @classmethod
149
+ def get_managed_project_roots(cls) -> list[str]:
150
+ """
151
+ Get list of all currently managed project roots.
152
+
153
+ Returns:
154
+ List of project root paths that have managed instances
155
+ """
156
+ with cls._lock:
157
+ return list(cls._instances.keys())
158
+
159
+ @classmethod
160
+ def update_project_root(cls, old_root: str | None, new_root: str) -> bool:
161
+ """
162
+ Update the project root for an existing instance.
163
+
164
+ This method moves an existing instance from one project root key to another,
165
+ and updates the instance's internal project root.
166
+
167
+ Args:
168
+ old_root: Current project root (None for current working directory)
169
+ new_root: New project root
170
+
171
+ Returns:
172
+ True if update was successful, False if old instance didn't exist
173
+ """
174
+ old_normalized = cls._normalize_project_root(old_root)
175
+ new_normalized = cls._normalize_project_root(new_root)
176
+
177
+ if old_normalized == new_normalized:
178
+ logger.debug(f"Project root update not needed: {old_normalized}")
179
+ return True
180
+
181
+ with cls._lock:
182
+ if old_normalized in cls._instances:
183
+ instance = cls._instances[old_normalized]
184
+
185
+ # Update the instance's internal project root
186
+ instance.set_project_root(new_root)
187
+
188
+ # Move to new key
189
+ cls._instances[new_normalized] = instance
190
+ del cls._instances[old_normalized]
191
+
192
+ logger.info(
193
+ f"Updated FileOutputManager project root: {old_normalized} -> {new_normalized}"
194
+ )
195
+ return True
196
+ else:
197
+ logger.warning(
198
+ f"No FileOutputManager instance found for old project root: {old_normalized}"
199
+ )
200
+ return False
201
+
202
+
203
+ # Convenience function for backward compatibility and ease of use
204
+ def get_file_output_manager(project_root: str | None = None) -> FileOutputManager:
205
+ """
206
+ Convenience function to get a FileOutputManager instance.
207
+
208
+ This function provides a simple interface to the factory while maintaining
209
+ the singleton behavior per project root.
210
+
211
+ Args:
212
+ project_root: Project root directory. If None, uses current working directory.
213
+
214
+ Returns:
215
+ FileOutputManager instance for the specified project root
216
+ """
217
+ return FileOutputManagerFactory.get_instance(project_root)
@@ -0,0 +1,322 @@
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
+ Enhanced with Managed Singleton Factory Pattern support for consistent
9
+ instance management across MCP tools.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+
16
+ from ...utils import setup_logger
17
+
18
+ # Set up logging
19
+ logger = setup_logger(__name__)
20
+
21
+
22
+ class FileOutputManager:
23
+ """
24
+ Manages file output for analysis results with automatic extension detection
25
+ and security validation.
26
+
27
+ Enhanced with factory method support for consistent instance management
28
+ across MCP tools while maintaining full backward compatibility.
29
+ """
30
+
31
+ def __init__(self, project_root: str | None = None):
32
+ """
33
+ Initialize the file output manager.
34
+
35
+ Args:
36
+ project_root: Optional project root directory for fallback output path
37
+ """
38
+ self.project_root = project_root
39
+ self._output_path: str | None = None
40
+ self._initialize_output_path()
41
+
42
+ @classmethod
43
+ def get_managed_instance(
44
+ cls, project_root: str | None = None
45
+ ) -> "FileOutputManager":
46
+ """
47
+ Get a managed FileOutputManager instance using the factory pattern.
48
+
49
+ This method provides access to the Managed Singleton Factory Pattern,
50
+ ensuring one instance per project root for optimal resource usage
51
+ and consistency across MCP tools.
52
+
53
+ Args:
54
+ project_root: Project root directory. If None, uses current working directory.
55
+
56
+ Returns:
57
+ FileOutputManager instance managed by the factory
58
+
59
+ Note:
60
+ This method requires the factory module to be available. If the factory
61
+ is not available, it falls back to creating a new instance directly.
62
+ """
63
+ try:
64
+ # Import here to avoid circular imports
65
+ from .file_output_factory import FileOutputManagerFactory
66
+
67
+ return FileOutputManagerFactory.get_instance(project_root)
68
+ except ImportError as e:
69
+ logger.warning(
70
+ f"Factory not available, creating new instance directly: {e}"
71
+ )
72
+ return cls(project_root)
73
+
74
+ @classmethod
75
+ def create_instance(cls, project_root: str | None = None) -> "FileOutputManager":
76
+ """
77
+ Create a new FileOutputManager instance directly (bypass factory).
78
+
79
+ This method creates a new instance without using the factory pattern.
80
+ Use this when you specifically need a separate instance that won't
81
+ be managed by the factory.
82
+
83
+ Args:
84
+ project_root: Project root directory. If None, uses current working directory.
85
+
86
+ Returns:
87
+ New FileOutputManager instance
88
+ """
89
+ return cls(project_root)
90
+
91
+ def _initialize_output_path(self) -> None:
92
+ """Initialize the output path from environment variables or project root."""
93
+ # Priority 1: Environment variable TREE_SITTER_OUTPUT_PATH
94
+ env_output_path = os.environ.get("TREE_SITTER_OUTPUT_PATH")
95
+ if env_output_path and Path(env_output_path).exists():
96
+ self._output_path = env_output_path
97
+ logger.info(f"Using output path from environment: {self._output_path}")
98
+ return
99
+
100
+ # Priority 2: Project root if available
101
+ if self.project_root and Path(self.project_root).exists():
102
+ self._output_path = self.project_root
103
+ logger.info(f"Using project root as output path: {self._output_path}")
104
+ return
105
+
106
+ # Priority 3: Current working directory as fallback
107
+ self._output_path = str(Path.cwd())
108
+ logger.warning(f"Using current directory as output path: {self._output_path}")
109
+
110
+ def get_output_path(self) -> str:
111
+ """
112
+ Get the current output path.
113
+
114
+ Returns:
115
+ Current output path
116
+ """
117
+ return self._output_path or str(Path.cwd())
118
+
119
+ def set_output_path(self, output_path: str) -> None:
120
+ """
121
+ Set a custom output path.
122
+
123
+ Args:
124
+ output_path: New output path
125
+
126
+ Raises:
127
+ ValueError: If the path doesn't exist or is not a directory
128
+ """
129
+ path_obj = Path(output_path)
130
+ if not path_obj.exists():
131
+ raise ValueError(f"Output path does not exist: {output_path}")
132
+ if not path_obj.is_dir():
133
+ raise ValueError(f"Output path is not a directory: {output_path}")
134
+
135
+ self._output_path = str(path_obj.resolve())
136
+ logger.info(f"Output path updated to: {self._output_path}")
137
+
138
+ def detect_content_type(self, content: str) -> str:
139
+ """
140
+ Detect content type based on content structure.
141
+
142
+ Args:
143
+ content: Content to analyze
144
+
145
+ Returns:
146
+ Detected content type ('json', 'csv', 'markdown', or 'text')
147
+ """
148
+ content_stripped = content.strip()
149
+
150
+ # Check for JSON
151
+ if content_stripped.startswith(("{", "[")):
152
+ try:
153
+ json.loads(content_stripped)
154
+ return "json"
155
+ except (json.JSONDecodeError, ValueError):
156
+ pass
157
+
158
+ # Check for CSV (simple heuristic)
159
+ lines = content_stripped.split("\n")
160
+ if len(lines) >= 2:
161
+ # Check if first few lines have consistent comma separation
162
+ first_line_commas = lines[0].count(",")
163
+ if first_line_commas > 0:
164
+ # Check if at least 2 more lines have similar comma counts
165
+ similar_comma_lines = sum(
166
+ 1
167
+ for line in lines[1:4]
168
+ if abs(line.count(",") - first_line_commas) <= 1
169
+ )
170
+ if similar_comma_lines >= 1:
171
+ return "csv"
172
+
173
+ # Check for Markdown (simple heuristic)
174
+ markdown_indicators = ["#", "##", "###", "|", "```", "*", "-", "+"]
175
+ if any(
176
+ content_stripped.startswith(indicator) for indicator in markdown_indicators
177
+ ):
178
+ return "markdown"
179
+
180
+ # Check for table format (pipe-separated)
181
+ if "|" in content and "\n" in content:
182
+ lines = content_stripped.split("\n")
183
+ pipe_lines = sum(1 for line in lines if "|" in line)
184
+ if pipe_lines >= 2: # At least header and one data row
185
+ return "markdown"
186
+
187
+ # Default to text
188
+ return "text"
189
+
190
+ def get_file_extension(self, content_type: str) -> str:
191
+ """
192
+ Get file extension for content type.
193
+
194
+ Args:
195
+ content_type: Content type ('json', 'csv', 'markdown', 'text')
196
+
197
+ Returns:
198
+ File extension including the dot
199
+ """
200
+ extension_map = {
201
+ "json": ".json",
202
+ "csv": ".csv",
203
+ "markdown": ".md",
204
+ "text": ".txt",
205
+ }
206
+ return extension_map.get(content_type, ".txt")
207
+
208
+ def generate_output_filename(self, base_name: str, content: str) -> str:
209
+ """
210
+ Generate output filename with appropriate extension.
211
+
212
+ Args:
213
+ base_name: Base filename (without extension)
214
+ content: Content to analyze for type detection
215
+
216
+ Returns:
217
+ Complete filename with extension
218
+ """
219
+ content_type = self.detect_content_type(content)
220
+ extension = self.get_file_extension(content_type)
221
+
222
+ # Remove existing extension if present
223
+ base_name_clean = Path(base_name).stem
224
+
225
+ return f"{base_name_clean}{extension}"
226
+
227
+ def save_to_file(
228
+ self, content: str, filename: str | None = None, base_name: str | None = None
229
+ ) -> str:
230
+ """
231
+ Save content to file with automatic extension detection.
232
+
233
+ Args:
234
+ content: Content to save
235
+ filename: Optional specific filename (overrides base_name)
236
+ base_name: Optional base name for auto-generated filename
237
+
238
+ Returns:
239
+ Path to the saved file
240
+
241
+ Raises:
242
+ ValueError: If neither filename nor base_name is provided
243
+ OSError: If file cannot be written
244
+ """
245
+ if not filename and not base_name:
246
+ raise ValueError("Either filename or base_name must be provided")
247
+
248
+ output_path = Path(self.get_output_path())
249
+
250
+ if filename:
251
+ # Use provided filename as-is
252
+ output_file = output_path / filename
253
+ else:
254
+ # Generate filename with appropriate extension
255
+ if base_name is None:
256
+ raise ValueError(
257
+ "base_name cannot be None when filename is not provided"
258
+ )
259
+ generated_filename = self.generate_output_filename(base_name, content)
260
+ output_file = output_path / generated_filename
261
+
262
+ # Ensure output directory exists
263
+ output_file.parent.mkdir(parents=True, exist_ok=True)
264
+
265
+ # Write content to file
266
+ try:
267
+ from ...encoding_utils import write_file_safe
268
+
269
+ write_file_safe(output_file, content)
270
+
271
+ logger.info(f"Content saved to file: {output_file}")
272
+ return str(output_file)
273
+
274
+ except OSError as e:
275
+ logger.error(f"Failed to save content to file {output_file}: {e}")
276
+ raise
277
+
278
+ def validate_output_path(self, path: str) -> tuple[bool, str | None]:
279
+ """
280
+ Validate if a path is safe for output.
281
+
282
+ Args:
283
+ path: Path to validate
284
+
285
+ Returns:
286
+ Tuple of (is_valid, error_message)
287
+ """
288
+ try:
289
+ path_obj = Path(path).resolve()
290
+
291
+ # Check if parent directory exists or can be created
292
+ parent_dir = path_obj.parent
293
+ if not parent_dir.exists():
294
+ try:
295
+ parent_dir.mkdir(parents=True, exist_ok=True)
296
+ except OSError as e:
297
+ return False, f"Cannot create parent directory: {e}"
298
+
299
+ # Check if we can write to the directory
300
+ if not os.access(parent_dir, os.W_OK):
301
+ return False, f"No write permission for directory: {parent_dir}"
302
+
303
+ # Check if file already exists and is writable
304
+ if path_obj.exists() and not os.access(path_obj, os.W_OK):
305
+ return False, f"No write permission for existing file: {path_obj}"
306
+
307
+ return True, None
308
+
309
+ except Exception as e:
310
+ return False, f"Path validation error: {str(e)}"
311
+
312
+ def set_project_root(self, project_root: str) -> None:
313
+ """
314
+ Update the project root and reinitialize output path if needed.
315
+
316
+ Args:
317
+ project_root: New project root directory
318
+ """
319
+ self.project_root = project_root
320
+ # Only reinitialize if we don't have an explicit output path from environment
321
+ if not os.environ.get("TREE_SITTER_OUTPUT_PATH"):
322
+ self._initialize_output_path()