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