tree-sitter-analyzer 0.1.0__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.
- tree_sitter_analyzer/__init__.py +121 -0
- tree_sitter_analyzer/__main__.py +12 -0
- tree_sitter_analyzer/api.py +539 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +13 -0
- tree_sitter_analyzer/cli/commands/__init__.py +27 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +88 -0
- tree_sitter_analyzer/cli/commands/base_command.py +155 -0
- tree_sitter_analyzer/cli/commands/default_command.py +19 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +133 -0
- tree_sitter_analyzer/cli/commands/query_command.py +82 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +121 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +93 -0
- tree_sitter_analyzer/cli/commands/table_command.py +233 -0
- tree_sitter_analyzer/cli/info_commands.py +121 -0
- tree_sitter_analyzer/cli_main.py +276 -0
- tree_sitter_analyzer/core/__init__.py +20 -0
- tree_sitter_analyzer/core/analysis_engine.py +574 -0
- tree_sitter_analyzer/core/cache_service.py +330 -0
- tree_sitter_analyzer/core/engine.py +560 -0
- tree_sitter_analyzer/core/parser.py +288 -0
- tree_sitter_analyzer/core/query.py +502 -0
- tree_sitter_analyzer/encoding_utils.py +460 -0
- tree_sitter_analyzer/exceptions.py +340 -0
- tree_sitter_analyzer/file_handler.py +222 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +168 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +74 -0
- tree_sitter_analyzer/formatters/java_formatter.py +270 -0
- tree_sitter_analyzer/formatters/python_formatter.py +235 -0
- tree_sitter_analyzer/interfaces/__init__.py +10 -0
- tree_sitter_analyzer/interfaces/cli.py +557 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +319 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +170 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +416 -0
- tree_sitter_analyzer/java_analyzer.py +219 -0
- tree_sitter_analyzer/language_detector.py +400 -0
- tree_sitter_analyzer/language_loader.py +228 -0
- tree_sitter_analyzer/languages/__init__.py +11 -0
- tree_sitter_analyzer/languages/java_plugin.py +1113 -0
- tree_sitter_analyzer/languages/python_plugin.py +712 -0
- tree_sitter_analyzer/mcp/__init__.py +32 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +47 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +213 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +550 -0
- tree_sitter_analyzer/mcp/server.py +319 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +36 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +558 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +245 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +55 -0
- tree_sitter_analyzer/mcp/tools/get_positions_tool.py +448 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +302 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +359 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +476 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +106 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +549 -0
- tree_sitter_analyzer/models.py +481 -0
- tree_sitter_analyzer/output_manager.py +264 -0
- tree_sitter_analyzer/plugins/__init__.py +334 -0
- tree_sitter_analyzer/plugins/base.py +446 -0
- tree_sitter_analyzer/plugins/java_plugin.py +625 -0
- tree_sitter_analyzer/plugins/javascript_plugin.py +439 -0
- tree_sitter_analyzer/plugins/manager.py +355 -0
- tree_sitter_analyzer/plugins/plugin_loader.py +83 -0
- tree_sitter_analyzer/plugins/python_plugin.py +598 -0
- tree_sitter_analyzer/plugins/registry.py +366 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/java.py +394 -0
- tree_sitter_analyzer/queries/javascript.py +149 -0
- tree_sitter_analyzer/queries/python.py +286 -0
- tree_sitter_analyzer/queries/typescript.py +230 -0
- tree_sitter_analyzer/query_loader.py +260 -0
- tree_sitter_analyzer/table_formatter.py +448 -0
- tree_sitter_analyzer/utils.py +201 -0
- tree_sitter_analyzer-0.1.0.dist-info/METADATA +581 -0
- tree_sitter_analyzer-0.1.0.dist-info/RECORD +78 -0
- tree_sitter_analyzer-0.1.0.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-0.1.0.dist-info/entry_points.txt +8 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Tree-sitter Analyzer Custom Exceptions
|
|
5
|
+
|
|
6
|
+
Unified exception handling system for consistent error management
|
|
7
|
+
across the entire framework.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional, Union
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TreeSitterAnalyzerError(Exception):
|
|
15
|
+
"""Base exception for all tree-sitter analyzer errors."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
error_code: Optional[str] = None,
|
|
21
|
+
context: Optional[Dict[str, Any]] = None
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.message = message
|
|
25
|
+
self.error_code = error_code or self.__class__.__name__
|
|
26
|
+
self.context = context or {}
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
29
|
+
"""Convert exception to dictionary format."""
|
|
30
|
+
return {
|
|
31
|
+
"error_type": self.__class__.__name__,
|
|
32
|
+
"error_code": self.error_code,
|
|
33
|
+
"message": self.message,
|
|
34
|
+
"context": self.context
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AnalysisError(TreeSitterAnalyzerError):
|
|
39
|
+
"""Raised when file analysis fails."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
message: str,
|
|
44
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
45
|
+
language: Optional[str] = None,
|
|
46
|
+
**kwargs
|
|
47
|
+
) -> None:
|
|
48
|
+
context = kwargs.get('context', {})
|
|
49
|
+
if file_path:
|
|
50
|
+
context['file_path'] = str(file_path)
|
|
51
|
+
if language:
|
|
52
|
+
context['language'] = language
|
|
53
|
+
super().__init__(message, context=context, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ParseError(TreeSitterAnalyzerError):
|
|
57
|
+
"""Raised when parsing fails."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
message: str,
|
|
62
|
+
language: Optional[str] = None,
|
|
63
|
+
source_info: Optional[Dict[str, Any]] = None,
|
|
64
|
+
**kwargs
|
|
65
|
+
) -> None:
|
|
66
|
+
context = kwargs.get('context', {})
|
|
67
|
+
if language:
|
|
68
|
+
context['language'] = language
|
|
69
|
+
if source_info:
|
|
70
|
+
context.update(source_info)
|
|
71
|
+
super().__init__(message, context=context, **kwargs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LanguageNotSupportedError(TreeSitterAnalyzerError):
|
|
75
|
+
"""Raised when a language is not supported."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
language: str,
|
|
80
|
+
supported_languages: Optional[list] = None,
|
|
81
|
+
**kwargs
|
|
82
|
+
) -> None:
|
|
83
|
+
message = f"Language '{language}' is not supported"
|
|
84
|
+
context = kwargs.get('context', {})
|
|
85
|
+
context['language'] = language
|
|
86
|
+
if supported_languages:
|
|
87
|
+
context['supported_languages'] = supported_languages
|
|
88
|
+
message += f". Supported languages: {', '.join(supported_languages)}"
|
|
89
|
+
super().__init__(message, context=context, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PluginError(TreeSitterAnalyzerError):
|
|
93
|
+
"""Raised when plugin operations fail."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
message: str,
|
|
98
|
+
plugin_name: Optional[str] = None,
|
|
99
|
+
operation: Optional[str] = None,
|
|
100
|
+
**kwargs
|
|
101
|
+
) -> None:
|
|
102
|
+
context = kwargs.get('context', {})
|
|
103
|
+
if plugin_name:
|
|
104
|
+
context['plugin_name'] = plugin_name
|
|
105
|
+
if operation:
|
|
106
|
+
context['operation'] = operation
|
|
107
|
+
super().__init__(message, context=context, **kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class QueryError(TreeSitterAnalyzerError):
|
|
111
|
+
"""Raised when query execution fails."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
message: str,
|
|
116
|
+
query_name: Optional[str] = None,
|
|
117
|
+
query_string: Optional[str] = None,
|
|
118
|
+
language: Optional[str] = None,
|
|
119
|
+
**kwargs
|
|
120
|
+
) -> None:
|
|
121
|
+
context = kwargs.get('context', {})
|
|
122
|
+
if query_name:
|
|
123
|
+
context['query_name'] = query_name
|
|
124
|
+
if query_string:
|
|
125
|
+
context['query_string'] = query_string
|
|
126
|
+
if language:
|
|
127
|
+
context['language'] = language
|
|
128
|
+
super().__init__(message, context=context, **kwargs)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class FileHandlingError(TreeSitterAnalyzerError):
|
|
132
|
+
"""Raised when file operations fail."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
message: str,
|
|
137
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
138
|
+
operation: Optional[str] = None,
|
|
139
|
+
**kwargs
|
|
140
|
+
) -> None:
|
|
141
|
+
context = kwargs.get('context', {})
|
|
142
|
+
if file_path:
|
|
143
|
+
context['file_path'] = str(file_path)
|
|
144
|
+
if operation:
|
|
145
|
+
context['operation'] = operation
|
|
146
|
+
super().__init__(message, context=context, **kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ConfigurationError(TreeSitterAnalyzerError):
|
|
150
|
+
"""Raised when configuration is invalid."""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
message: str,
|
|
155
|
+
config_key: Optional[str] = None,
|
|
156
|
+
config_value: Optional[Any] = None,
|
|
157
|
+
**kwargs
|
|
158
|
+
) -> None:
|
|
159
|
+
context = kwargs.get('context', {})
|
|
160
|
+
if config_key:
|
|
161
|
+
context['config_key'] = config_key
|
|
162
|
+
if config_value is not None:
|
|
163
|
+
context['config_value'] = config_value
|
|
164
|
+
super().__init__(message, context=context, **kwargs)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ValidationError(TreeSitterAnalyzerError):
|
|
168
|
+
"""Raised when validation fails."""
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
message: str,
|
|
173
|
+
validation_type: Optional[str] = None,
|
|
174
|
+
invalid_value: Optional[Any] = None,
|
|
175
|
+
**kwargs
|
|
176
|
+
) -> None:
|
|
177
|
+
context = kwargs.get('context', {})
|
|
178
|
+
if validation_type:
|
|
179
|
+
context['validation_type'] = validation_type
|
|
180
|
+
if invalid_value is not None:
|
|
181
|
+
context['invalid_value'] = invalid_value
|
|
182
|
+
super().__init__(message, context=context, **kwargs)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MCPError(TreeSitterAnalyzerError):
|
|
186
|
+
"""Raised when MCP operations fail."""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
message: str,
|
|
191
|
+
tool_name: Optional[str] = None,
|
|
192
|
+
resource_uri: Optional[str] = None,
|
|
193
|
+
**kwargs
|
|
194
|
+
) -> None:
|
|
195
|
+
context = kwargs.get('context', {})
|
|
196
|
+
if tool_name:
|
|
197
|
+
context['tool_name'] = tool_name
|
|
198
|
+
if resource_uri:
|
|
199
|
+
context['resource_uri'] = resource_uri
|
|
200
|
+
super().__init__(message, context=context, **kwargs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Exception handling utilities
|
|
204
|
+
def handle_exception(
|
|
205
|
+
exception: Exception,
|
|
206
|
+
context: Optional[Dict[str, Any]] = None,
|
|
207
|
+
reraise_as: Optional[type] = None
|
|
208
|
+
) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Handle exceptions with optional context and re-raising.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
exception: The original exception
|
|
214
|
+
context: Additional context information
|
|
215
|
+
reraise_as: Exception class to re-raise as
|
|
216
|
+
"""
|
|
217
|
+
from .utils import log_error
|
|
218
|
+
|
|
219
|
+
# Log the original exception
|
|
220
|
+
error_context = context or {}
|
|
221
|
+
if hasattr(exception, 'context'):
|
|
222
|
+
error_context.update(exception.context)
|
|
223
|
+
|
|
224
|
+
log_error(f"Exception handled: {exception}", extra=error_context)
|
|
225
|
+
|
|
226
|
+
# Re-raise as different exception type if requested
|
|
227
|
+
if reraise_as and not isinstance(exception, reraise_as):
|
|
228
|
+
if issubclass(reraise_as, TreeSitterAnalyzerError):
|
|
229
|
+
raise reraise_as(str(exception), context=error_context)
|
|
230
|
+
else:
|
|
231
|
+
raise reraise_as(str(exception))
|
|
232
|
+
|
|
233
|
+
# Re-raise original exception
|
|
234
|
+
raise exception
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def safe_execute(
|
|
238
|
+
func,
|
|
239
|
+
*args,
|
|
240
|
+
default_return=None,
|
|
241
|
+
exception_types: tuple = (Exception,),
|
|
242
|
+
log_errors: bool = True,
|
|
243
|
+
**kwargs
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Safely execute a function with exception handling.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
func: Function to execute
|
|
250
|
+
*args: Function arguments
|
|
251
|
+
default_return: Value to return on exception
|
|
252
|
+
exception_types: Exception types to catch
|
|
253
|
+
log_errors: Whether to log errors
|
|
254
|
+
**kwargs: Function keyword arguments
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Function result or default_return on exception
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
return func(*args, **kwargs)
|
|
261
|
+
except exception_types as e:
|
|
262
|
+
if log_errors:
|
|
263
|
+
from .utils import log_error
|
|
264
|
+
log_error(f"Safe execution failed for {func.__name__}: {e}")
|
|
265
|
+
return default_return
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def create_error_response(
|
|
269
|
+
exception: Exception,
|
|
270
|
+
include_traceback: bool = False
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
"""
|
|
273
|
+
Create standardized error response dictionary.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
exception: The exception to convert
|
|
277
|
+
include_traceback: Whether to include traceback
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Error response dictionary
|
|
281
|
+
"""
|
|
282
|
+
import traceback
|
|
283
|
+
|
|
284
|
+
response = {
|
|
285
|
+
"success": False,
|
|
286
|
+
"error": {
|
|
287
|
+
"type": exception.__class__.__name__,
|
|
288
|
+
"message": str(exception)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Add context if available
|
|
293
|
+
if hasattr(exception, 'context'):
|
|
294
|
+
response["error"]["context"] = exception.context
|
|
295
|
+
|
|
296
|
+
# Add error code if available
|
|
297
|
+
if hasattr(exception, 'error_code'):
|
|
298
|
+
response["error"]["code"] = exception.error_code
|
|
299
|
+
|
|
300
|
+
# Add traceback if requested
|
|
301
|
+
if include_traceback:
|
|
302
|
+
response["error"]["traceback"] = traceback.format_exc()
|
|
303
|
+
|
|
304
|
+
return response
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Decorator for exception handling
|
|
308
|
+
def handle_exceptions(
|
|
309
|
+
default_return=None,
|
|
310
|
+
exception_types: tuple = (Exception,),
|
|
311
|
+
reraise_as: Optional[type] = None,
|
|
312
|
+
log_errors: bool = True
|
|
313
|
+
):
|
|
314
|
+
"""
|
|
315
|
+
Decorator for automatic exception handling.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
default_return: Value to return on exception
|
|
319
|
+
exception_types: Exception types to catch
|
|
320
|
+
reraise_as: Exception class to re-raise as
|
|
321
|
+
log_errors: Whether to log errors
|
|
322
|
+
"""
|
|
323
|
+
def decorator(func):
|
|
324
|
+
def wrapper(*args, **kwargs):
|
|
325
|
+
try:
|
|
326
|
+
return func(*args, **kwargs)
|
|
327
|
+
except exception_types as e:
|
|
328
|
+
if log_errors:
|
|
329
|
+
from .utils import log_error
|
|
330
|
+
log_error(f"Exception in {func.__name__}: {e}")
|
|
331
|
+
|
|
332
|
+
if reraise_as:
|
|
333
|
+
if issubclass(reraise_as, TreeSitterAnalyzerError):
|
|
334
|
+
raise reraise_as(str(e))
|
|
335
|
+
else:
|
|
336
|
+
raise reraise_as(str(e))
|
|
337
|
+
|
|
338
|
+
return default_return
|
|
339
|
+
return wrapper
|
|
340
|
+
return decorator
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
File Handler Module
|
|
5
|
+
|
|
6
|
+
This module provides file reading functionality with encoding detection and fallback.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .encoding_utils import detect_encoding, read_file_safe, safe_decode
|
|
15
|
+
from .utils import log_error, log_info, log_warning
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_language_from_extension(file_path: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
ファイル拡張子から言語を判定
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file_path: ファイルパス
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
言語名または'unknown'
|
|
27
|
+
"""
|
|
28
|
+
extension = os.path.splitext(file_path)[1].lower()
|
|
29
|
+
|
|
30
|
+
extension_map = {
|
|
31
|
+
".java": "java",
|
|
32
|
+
".py": "python",
|
|
33
|
+
".js": "javascript",
|
|
34
|
+
".ts": "typescript",
|
|
35
|
+
".cpp": "cpp",
|
|
36
|
+
".c": "c",
|
|
37
|
+
".h": "c",
|
|
38
|
+
".hpp": "cpp",
|
|
39
|
+
".cs": "csharp",
|
|
40
|
+
".go": "go",
|
|
41
|
+
".rs": "rust",
|
|
42
|
+
".rb": "ruby",
|
|
43
|
+
".php": "php",
|
|
44
|
+
".kt": "kotlin",
|
|
45
|
+
".scala": "scala",
|
|
46
|
+
".swift": "swift",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return extension_map.get(extension, "unknown")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_file_with_fallback(file_path: str) -> Optional[bytes]:
|
|
53
|
+
"""
|
|
54
|
+
Read file with encoding fallback using unified encoding utilities
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file_path: Path to the file to read
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
File content as bytes, or None if file doesn't exist
|
|
61
|
+
"""
|
|
62
|
+
# まずファイルの存在を確認
|
|
63
|
+
if not os.path.exists(file_path):
|
|
64
|
+
log_error(f"File does not exist: {file_path}")
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
content, detected_encoding = read_file_safe(file_path)
|
|
69
|
+
if content is not None: # 空文字列も有効なコンテンツ
|
|
70
|
+
log_info(
|
|
71
|
+
f"Successfully read file {file_path} with encoding: {detected_encoding}"
|
|
72
|
+
)
|
|
73
|
+
return content.encode("utf-8")
|
|
74
|
+
else:
|
|
75
|
+
log_warning(f"File {file_path} is empty or could not be read")
|
|
76
|
+
return b""
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
log_error(f"Failed to read file {file_path}: {e}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def read_file_partial(
|
|
84
|
+
file_path: str,
|
|
85
|
+
start_line: int,
|
|
86
|
+
end_line: Optional[int] = None,
|
|
87
|
+
start_column: Optional[int] = None,
|
|
88
|
+
end_column: Optional[int] = None,
|
|
89
|
+
) -> Optional[str]:
|
|
90
|
+
"""
|
|
91
|
+
指定した行番号・列番号範囲でファイルの一部を読み込み
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
file_path: 読み込むファイルのパス
|
|
95
|
+
start_line: 開始行番号(1ベース)
|
|
96
|
+
end_line: 終了行番号(Noneの場合はファイル末尾まで、1ベース)
|
|
97
|
+
start_column: 開始列番号(0ベース、省略可)
|
|
98
|
+
end_column: 終了列番号(0ベース、省略可)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
指定範囲のファイル内容(文字列)、エラーの場合はNone
|
|
102
|
+
"""
|
|
103
|
+
# ファイルの存在確認
|
|
104
|
+
if not os.path.exists(file_path):
|
|
105
|
+
log_error(f"File does not exist: {file_path}")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
# パラメータ検証
|
|
109
|
+
if start_line < 1:
|
|
110
|
+
log_error(f"Invalid start_line: {start_line}. Line numbers start from 1.")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
if end_line is not None and end_line < start_line:
|
|
114
|
+
log_error(f"Invalid range: end_line ({end_line}) < start_line ({start_line})")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# ファイル全体を安全に読み込み
|
|
119
|
+
content, detected_encoding = read_file_safe(file_path)
|
|
120
|
+
if content is None:
|
|
121
|
+
log_error(f"Failed to read file: {file_path}")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# 行に分割
|
|
125
|
+
lines = content.splitlines(keepends=True)
|
|
126
|
+
total_lines = len(lines)
|
|
127
|
+
|
|
128
|
+
# 行範囲の調整
|
|
129
|
+
start_idx = start_line - 1 # 0ベースに変換
|
|
130
|
+
end_idx = min(
|
|
131
|
+
end_line - 1 if end_line is not None else total_lines - 1, total_lines - 1
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# 範囲チェック
|
|
135
|
+
if start_idx >= total_lines:
|
|
136
|
+
log_warning(
|
|
137
|
+
f"start_line ({start_line}) exceeds file length ({total_lines})"
|
|
138
|
+
)
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
# 指定範囲の行を取得
|
|
142
|
+
selected_lines = lines[start_idx : end_idx + 1]
|
|
143
|
+
|
|
144
|
+
# 列範囲の処理
|
|
145
|
+
if start_column is not None or end_column is not None:
|
|
146
|
+
processed_lines = []
|
|
147
|
+
for i, line in enumerate(selected_lines):
|
|
148
|
+
# 改行文字を除去して処理
|
|
149
|
+
line_content = line.rstrip("\r\n")
|
|
150
|
+
|
|
151
|
+
if i == 0 and start_column is not None:
|
|
152
|
+
# 最初の行:開始列から
|
|
153
|
+
line_content = (
|
|
154
|
+
line_content[start_column:]
|
|
155
|
+
if start_column < len(line_content)
|
|
156
|
+
else ""
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if i == len(selected_lines) - 1 and end_column is not None:
|
|
160
|
+
# 最後の行:終了列まで
|
|
161
|
+
if i == 0 and start_column is not None:
|
|
162
|
+
# 単一行の場合:開始列と終了列の両方を適用
|
|
163
|
+
col_start = 0 # 既に開始列でカット済み
|
|
164
|
+
col_end = (
|
|
165
|
+
end_column - start_column
|
|
166
|
+
if end_column >= start_column
|
|
167
|
+
else 0
|
|
168
|
+
)
|
|
169
|
+
line_content = line_content[:col_end] if col_end > 0 else ""
|
|
170
|
+
else:
|
|
171
|
+
line_content = (
|
|
172
|
+
line_content[:end_column]
|
|
173
|
+
if end_column < len(line_content)
|
|
174
|
+
else line_content
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# 元の改行文字を保持(最後の行以外)
|
|
178
|
+
if i < len(selected_lines) - 1:
|
|
179
|
+
# 元の行の改行文字を取得
|
|
180
|
+
original_line = lines[start_idx + i]
|
|
181
|
+
if original_line.endswith("\r\n"):
|
|
182
|
+
line_content += "\r\n"
|
|
183
|
+
elif original_line.endswith("\n"):
|
|
184
|
+
line_content += "\n"
|
|
185
|
+
elif original_line.endswith("\r"):
|
|
186
|
+
line_content += "\r"
|
|
187
|
+
|
|
188
|
+
processed_lines.append(line_content)
|
|
189
|
+
|
|
190
|
+
result = "".join(processed_lines)
|
|
191
|
+
else:
|
|
192
|
+
# 列指定なしの場合は行をそのまま結合
|
|
193
|
+
result = "".join(selected_lines)
|
|
194
|
+
|
|
195
|
+
log_info(
|
|
196
|
+
f"Successfully read partial file {file_path}: "
|
|
197
|
+
f"lines {start_line}-{end_line or total_lines}"
|
|
198
|
+
f"{f', columns {start_column}-{end_column}' if start_column is not None or end_column is not None else ''}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
log_error(f"Failed to read partial file {file_path}: {e}")
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def read_file_lines_range(
|
|
209
|
+
file_path: str, start_line: int, end_line: Optional[int] = None
|
|
210
|
+
) -> Optional[str]:
|
|
211
|
+
"""
|
|
212
|
+
指定した行番号範囲でファイルの一部を読み込み(列指定なし)
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
file_path: 読み込むファイルのパス
|
|
216
|
+
start_line: 開始行番号(1ベース)
|
|
217
|
+
end_line: 終了行番号(Noneの場合はファイル末尾まで、1ベース)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
指定範囲のファイル内容(文字列)、エラーの場合はNone
|
|
221
|
+
"""
|
|
222
|
+
return read_file_partial(file_path, start_line, end_line)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Language-specific formatters
|