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,414 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Path Resolver Utility for MCP Tools
|
|
4
|
+
|
|
5
|
+
This module provides unified path resolution functionality for all MCP tools,
|
|
6
|
+
ensuring consistent handling of relative and absolute paths across different
|
|
7
|
+
operating systems.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_path_cross_platform(path_str: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Normalize path for cross-platform compatibility.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path_str: Input path string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Normalized path string
|
|
26
|
+
"""
|
|
27
|
+
if not path_str:
|
|
28
|
+
return path_str
|
|
29
|
+
|
|
30
|
+
# Handle macOS path normalization issues
|
|
31
|
+
if os.name == "posix":
|
|
32
|
+
# Handle /System/Volumes/Data prefix on macOS
|
|
33
|
+
if path_str.startswith("/System/Volumes/Data/"):
|
|
34
|
+
# Remove the /System/Volumes/Data prefix
|
|
35
|
+
normalized = path_str[len("/System/Volumes/Data") :]
|
|
36
|
+
return normalized
|
|
37
|
+
|
|
38
|
+
# Handle /private/var vs /var symlink difference on macOS
|
|
39
|
+
if path_str.startswith("/private/var/"):
|
|
40
|
+
# Always normalize to /var form on macOS for consistency
|
|
41
|
+
var_path = path_str.replace("/private/var/", "/var/", 1)
|
|
42
|
+
return var_path
|
|
43
|
+
elif path_str.startswith("/var/"):
|
|
44
|
+
# Keep /var form as is
|
|
45
|
+
return path_str
|
|
46
|
+
|
|
47
|
+
# Handle Windows short path names (8.3 format)
|
|
48
|
+
if os.name == "nt" and path_str:
|
|
49
|
+
try:
|
|
50
|
+
# Try to get the long path name on Windows
|
|
51
|
+
import ctypes
|
|
52
|
+
from ctypes import wintypes
|
|
53
|
+
|
|
54
|
+
# GetLongPathNameW function
|
|
55
|
+
_GetLongPathNameW = ctypes.windll.kernel32.GetLongPathNameW
|
|
56
|
+
_GetLongPathNameW.argtypes = [
|
|
57
|
+
wintypes.LPCWSTR,
|
|
58
|
+
wintypes.LPWSTR,
|
|
59
|
+
wintypes.DWORD,
|
|
60
|
+
]
|
|
61
|
+
_GetLongPathNameW.restype = wintypes.DWORD
|
|
62
|
+
|
|
63
|
+
# Buffer for the long path
|
|
64
|
+
buffer_size = 1000
|
|
65
|
+
buffer = ctypes.create_unicode_buffer(buffer_size)
|
|
66
|
+
|
|
67
|
+
# Get the long path name
|
|
68
|
+
result = _GetLongPathNameW(path_str, buffer, buffer_size)
|
|
69
|
+
if result > 0 and result < buffer_size:
|
|
70
|
+
long_path = buffer.value
|
|
71
|
+
if long_path and long_path != path_str:
|
|
72
|
+
return long_path
|
|
73
|
+
except (ImportError, AttributeError, OSError):
|
|
74
|
+
# If Windows API calls fail, continue with original path
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
return path_str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_windows_absolute_path(path_str: str) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check if a path is a Windows-style absolute path.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path_str: Path string to check
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if it's a Windows absolute path (e.g., C:\\path or C:/path)
|
|
89
|
+
"""
|
|
90
|
+
if not path_str or len(path_str) < 3:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Check for drive letter pattern: X:\ or X:/
|
|
94
|
+
return path_str[1] == ":" and path_str[0].isalpha() and path_str[2] in ("\\", "/")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PathResolver:
|
|
98
|
+
"""
|
|
99
|
+
Utility class for resolving file paths in MCP tools.
|
|
100
|
+
|
|
101
|
+
Handles relative path resolution against project root and provides
|
|
102
|
+
cross-platform compatibility for Windows, macOS, and Linux.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, project_root: str | None = None):
|
|
106
|
+
"""
|
|
107
|
+
Initialize the path resolver.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
project_root: Optional project root directory for resolving relative paths
|
|
111
|
+
"""
|
|
112
|
+
self.project_root = None
|
|
113
|
+
self._cache: dict[str, str] = {} # Simple cache for resolved paths
|
|
114
|
+
self._cache_size_limit = 100 # Limit cache size to prevent memory issues
|
|
115
|
+
|
|
116
|
+
if project_root:
|
|
117
|
+
# Use pathlib for consistent path handling, but preserve relative paths for compatibility
|
|
118
|
+
path_obj = Path(project_root)
|
|
119
|
+
if path_obj.is_absolute():
|
|
120
|
+
resolved_root = str(path_obj.resolve())
|
|
121
|
+
# Apply cross-platform normalization
|
|
122
|
+
self.project_root = _normalize_path_cross_platform(resolved_root)
|
|
123
|
+
else:
|
|
124
|
+
# For relative paths, normalize but don't resolve to absolute
|
|
125
|
+
self.project_root = str(path_obj)
|
|
126
|
+
logger.debug(
|
|
127
|
+
f"PathResolver initialized with project root: {self.project_root}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def resolve(self, file_path: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Resolve a file path to an absolute path.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
file_path: Input file path (can be relative or absolute)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Resolved absolute file path
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If file_path is empty or None
|
|
142
|
+
TypeError: If file_path is not a string
|
|
143
|
+
"""
|
|
144
|
+
if not file_path:
|
|
145
|
+
raise ValueError("file_path cannot be empty or None")
|
|
146
|
+
|
|
147
|
+
if not isinstance(file_path, str):
|
|
148
|
+
raise TypeError(
|
|
149
|
+
f"file_path must be a string, got {type(file_path).__name__}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Check cache first
|
|
153
|
+
if file_path in self._cache:
|
|
154
|
+
logger.debug(f"Cache hit for path: {file_path}")
|
|
155
|
+
return self._cache[file_path]
|
|
156
|
+
|
|
157
|
+
# Normalize path separators first
|
|
158
|
+
normalized_input = file_path.replace("\\", "/")
|
|
159
|
+
|
|
160
|
+
# Special handling for Windows absolute paths on non-Windows systems
|
|
161
|
+
if _is_windows_absolute_path(file_path) and os.name != "nt":
|
|
162
|
+
# On non-Windows systems, Windows absolute paths should be treated as-is
|
|
163
|
+
# Don't try to resolve them relative to project root
|
|
164
|
+
logger.debug(f"Windows absolute path on non-Windows system: {file_path}")
|
|
165
|
+
self._add_to_cache(file_path, file_path)
|
|
166
|
+
return file_path
|
|
167
|
+
|
|
168
|
+
path_obj = Path(normalized_input)
|
|
169
|
+
|
|
170
|
+
# Handle Unix-style absolute paths on Windows (starting with /) FIRST
|
|
171
|
+
# This must come before the is_absolute() check because Unix paths aren't
|
|
172
|
+
# considered absolute on Windows by pathlib
|
|
173
|
+
if (
|
|
174
|
+
normalized_input.startswith("/") and os.name == "nt"
|
|
175
|
+
): # Check if we're on Windows
|
|
176
|
+
# On Windows, convert Unix-style absolute paths to Windows format
|
|
177
|
+
# by prepending the current drive with proper separator
|
|
178
|
+
current_drive = Path.cwd().drive
|
|
179
|
+
if current_drive:
|
|
180
|
+
# Remove leading slash and join with current drive
|
|
181
|
+
unix_path_without_slash = normalized_input[1:]
|
|
182
|
+
# Ensure proper Windows path format with backslash after drive
|
|
183
|
+
resolved_path = str(
|
|
184
|
+
Path(current_drive + "\\") / unix_path_without_slash
|
|
185
|
+
)
|
|
186
|
+
logger.debug(
|
|
187
|
+
f"Converted Unix absolute path: {file_path} -> {resolved_path}"
|
|
188
|
+
)
|
|
189
|
+
# Apply cross-platform normalization
|
|
190
|
+
resolved_path = _normalize_path_cross_platform(resolved_path)
|
|
191
|
+
self._add_to_cache(file_path, resolved_path)
|
|
192
|
+
return resolved_path
|
|
193
|
+
# If no drive available, continue with normal processing
|
|
194
|
+
|
|
195
|
+
# Check if path is absolute
|
|
196
|
+
if path_obj.is_absolute():
|
|
197
|
+
resolved_path = str(path_obj.resolve())
|
|
198
|
+
logger.debug(f"Path already absolute: {file_path} -> {resolved_path}")
|
|
199
|
+
# Apply cross-platform normalization
|
|
200
|
+
resolved_path = _normalize_path_cross_platform(resolved_path)
|
|
201
|
+
self._add_to_cache(file_path, resolved_path)
|
|
202
|
+
return resolved_path
|
|
203
|
+
|
|
204
|
+
# If we have a project root, resolve relative to it
|
|
205
|
+
if self.project_root:
|
|
206
|
+
resolved_path = str((Path(self.project_root) / normalized_input).resolve())
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"Resolved relative path '{file_path}' to '{resolved_path}' using project root"
|
|
209
|
+
)
|
|
210
|
+
# Apply cross-platform normalization
|
|
211
|
+
resolved_path = _normalize_path_cross_platform(resolved_path)
|
|
212
|
+
self._add_to_cache(file_path, resolved_path)
|
|
213
|
+
return resolved_path
|
|
214
|
+
|
|
215
|
+
# Fallback: try to resolve relative to current working directory
|
|
216
|
+
resolved_path = str(Path(normalized_input).resolve())
|
|
217
|
+
logger.debug(
|
|
218
|
+
f"Resolved relative path '{file_path}' to '{resolved_path}' using current working directory"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Apply cross-platform normalization
|
|
222
|
+
resolved_path = _normalize_path_cross_platform(resolved_path)
|
|
223
|
+
|
|
224
|
+
# Cache the result
|
|
225
|
+
self._add_to_cache(file_path, resolved_path)
|
|
226
|
+
|
|
227
|
+
return resolved_path
|
|
228
|
+
|
|
229
|
+
def _add_to_cache(self, file_path: str, resolved_path: str) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Add a resolved path to the cache.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
file_path: Original file path
|
|
235
|
+
resolved_path: Resolved absolute path
|
|
236
|
+
"""
|
|
237
|
+
# Limit cache size to prevent memory issues
|
|
238
|
+
if len(self._cache) >= self._cache_size_limit:
|
|
239
|
+
# Remove oldest entries (simple FIFO)
|
|
240
|
+
oldest_key = next(iter(self._cache))
|
|
241
|
+
del self._cache[oldest_key]
|
|
242
|
+
logger.debug(f"Cache full, removed oldest entry: {oldest_key}")
|
|
243
|
+
|
|
244
|
+
self._cache[file_path] = resolved_path
|
|
245
|
+
logger.debug(f"Cached path resolution: {file_path} -> {resolved_path}")
|
|
246
|
+
|
|
247
|
+
def clear_cache(self) -> None:
|
|
248
|
+
"""Clear the path resolution cache."""
|
|
249
|
+
cache_size = len(self._cache)
|
|
250
|
+
self._cache.clear()
|
|
251
|
+
logger.info(f"Cleared path resolution cache ({cache_size} entries)")
|
|
252
|
+
|
|
253
|
+
def get_cache_stats(self) -> dict[str, int]:
|
|
254
|
+
"""
|
|
255
|
+
Get cache statistics.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Dictionary with cache statistics
|
|
259
|
+
"""
|
|
260
|
+
return {"size": len(self._cache), "limit": self._cache_size_limit}
|
|
261
|
+
|
|
262
|
+
def is_relative(self, file_path: str) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Check if a file path is relative.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: File path to check
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if the path is relative, False if absolute
|
|
271
|
+
"""
|
|
272
|
+
return not Path(file_path).is_absolute()
|
|
273
|
+
|
|
274
|
+
def get_relative_path(self, absolute_path: str) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Get the relative path from project root to the given absolute path.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
absolute_path: Absolute file path
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Relative path from project root, or the original path if no project root
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
ValueError: If absolute_path is not actually absolute
|
|
286
|
+
"""
|
|
287
|
+
abs_path = Path(absolute_path)
|
|
288
|
+
if not abs_path.is_absolute():
|
|
289
|
+
raise ValueError(f"Path is not absolute: {absolute_path}")
|
|
290
|
+
|
|
291
|
+
if not self.project_root:
|
|
292
|
+
return absolute_path
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Get relative path from project root using pathlib
|
|
296
|
+
project_path = Path(self.project_root)
|
|
297
|
+
|
|
298
|
+
# Normalize both paths for consistent comparison
|
|
299
|
+
normalized_abs_path = _normalize_path_cross_platform(
|
|
300
|
+
str(abs_path.resolve())
|
|
301
|
+
)
|
|
302
|
+
normalized_project_root = _normalize_path_cross_platform(
|
|
303
|
+
str(project_path.resolve())
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Convert back to Path objects for relative_to calculation
|
|
307
|
+
normalized_abs_path_obj = Path(normalized_abs_path)
|
|
308
|
+
normalized_project_root_obj = Path(normalized_project_root)
|
|
309
|
+
|
|
310
|
+
relative_path = str(
|
|
311
|
+
normalized_abs_path_obj.relative_to(normalized_project_root_obj)
|
|
312
|
+
)
|
|
313
|
+
logger.debug(
|
|
314
|
+
f"Converted absolute path '{absolute_path}' to relative path '{relative_path}'"
|
|
315
|
+
)
|
|
316
|
+
return relative_path
|
|
317
|
+
except ValueError:
|
|
318
|
+
# Paths are on different drives (Windows) or other error
|
|
319
|
+
logger.warning(
|
|
320
|
+
f"Could not convert absolute path '{absolute_path}' to relative path"
|
|
321
|
+
)
|
|
322
|
+
return absolute_path
|
|
323
|
+
|
|
324
|
+
def validate_path(self, file_path: str) -> tuple[bool, str | None]:
|
|
325
|
+
"""
|
|
326
|
+
Validate if a file path is valid and safe.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
file_path: File path to validate
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Tuple of (is_valid, error_message)
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
resolved_path = self.resolve(file_path)
|
|
336
|
+
resolved_path_obj = Path(resolved_path)
|
|
337
|
+
|
|
338
|
+
if not resolved_path_obj.exists():
|
|
339
|
+
return False, f"File does not exist: {resolved_path}"
|
|
340
|
+
|
|
341
|
+
# Check if it's a file (not directory)
|
|
342
|
+
if not resolved_path_obj.is_file():
|
|
343
|
+
return False, f"Path is not a file: {resolved_path}"
|
|
344
|
+
|
|
345
|
+
# Check if it's a symlink (reject symlinks for security)
|
|
346
|
+
try:
|
|
347
|
+
if resolved_path_obj.is_symlink():
|
|
348
|
+
return False, f"Path is a symlink: {resolved_path}"
|
|
349
|
+
except (OSError, AttributeError):
|
|
350
|
+
# is_symlink() might not be available on all platforms
|
|
351
|
+
# or might fail due to permissions, skip this check
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# Check if it's within project root (if we have one)
|
|
355
|
+
if self.project_root:
|
|
356
|
+
try:
|
|
357
|
+
project_path = Path(self.project_root).resolve()
|
|
358
|
+
resolved_abs_path = resolved_path_obj.resolve()
|
|
359
|
+
# Check if the resolved path is within the project root
|
|
360
|
+
resolved_abs_path.relative_to(project_path)
|
|
361
|
+
except ValueError:
|
|
362
|
+
return False, f"File path is outside project root: {resolved_path}"
|
|
363
|
+
|
|
364
|
+
return True, None
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return False, f"Path validation error: {str(e)}"
|
|
368
|
+
|
|
369
|
+
def get_project_root(self) -> str | None:
|
|
370
|
+
"""
|
|
371
|
+
Get the current project root.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Project root path or None if not set
|
|
375
|
+
"""
|
|
376
|
+
return self.project_root
|
|
377
|
+
|
|
378
|
+
def set_project_root(self, project_root: str) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Set or update the project root.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
project_root: New project root directory
|
|
384
|
+
"""
|
|
385
|
+
if project_root:
|
|
386
|
+
# Use pathlib for consistent path handling, but preserve relative paths for compatibility
|
|
387
|
+
path_obj = Path(project_root)
|
|
388
|
+
if path_obj.is_absolute():
|
|
389
|
+
resolved_root = str(path_obj.resolve())
|
|
390
|
+
# Apply cross-platform normalization
|
|
391
|
+
self.project_root = _normalize_path_cross_platform(resolved_root)
|
|
392
|
+
else:
|
|
393
|
+
# For relative paths, normalize but don't resolve to absolute
|
|
394
|
+
self.project_root = str(path_obj)
|
|
395
|
+
logger.info(f"Project root updated to: {self.project_root}")
|
|
396
|
+
else:
|
|
397
|
+
self.project_root = None
|
|
398
|
+
logger.info("Project root cleared")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Convenience function for backward compatibility
|
|
402
|
+
def resolve_path(file_path: str, project_root: str | None = None) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Convenience function to resolve a file path.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
file_path: File path to resolve
|
|
408
|
+
project_root: Optional project root directory
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Resolved absolute file path
|
|
412
|
+
"""
|
|
413
|
+
resolver = PathResolver(project_root)
|
|
414
|
+
return resolver.resolve(file_path)
|