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,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()
|