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,384 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Plugin Manager
4
+
5
+ Dynamic plugin discovery and management system.
6
+ Handles loading plugins from entry points and local directories.
7
+ """
8
+
9
+ import importlib
10
+ import importlib.metadata
11
+ import logging
12
+ import pkgutil
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from ..utils import log_debug, log_error, log_info, log_warning
17
+ from .base import LanguagePlugin
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class PluginManager:
23
+ """
24
+ Manages dynamic discovery and loading of language plugins.
25
+
26
+ This class handles:
27
+ - Discovery of plugins via entry points
28
+ - Loading plugins from local directories
29
+ - Plugin lifecycle management
30
+ - Error handling and fallback mechanisms
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ """Initialize the plugin manager."""
35
+ self._loaded_plugins: dict[str, LanguagePlugin] = {}
36
+ self._plugin_classes: dict[str, type[LanguagePlugin]] = {}
37
+ self._entry_point_group = "tree_sitter_analyzer.plugins"
38
+
39
+ def load_plugins(self) -> list[LanguagePlugin]:
40
+ """
41
+ Load all available plugins from various sources.
42
+
43
+ Returns:
44
+ List of successfully loaded plugin instances
45
+ """
46
+ loaded_plugins = []
47
+
48
+ # Load plugins from entry points (installed packages)
49
+ entry_point_plugins = self._load_from_entry_points()
50
+ loaded_plugins.extend(entry_point_plugins)
51
+
52
+ # Load plugins from local languages directory
53
+ local_plugins = self._load_from_local_directory()
54
+ loaded_plugins.extend(local_plugins)
55
+
56
+ # Store loaded plugins and deduplicate by language
57
+ unique_plugins = {}
58
+ for plugin in loaded_plugins:
59
+ language = plugin.get_language_name()
60
+ if language not in unique_plugins:
61
+ unique_plugins[language] = plugin
62
+ self._loaded_plugins[language] = plugin
63
+ else:
64
+ log_debug(f"Skipping duplicate plugin for language: {language}")
65
+
66
+ final_plugins = list(unique_plugins.values())
67
+ # Only log if not in CLI mode (check if we're in quiet mode)
68
+ import os
69
+
70
+ log_level = os.environ.get("LOG_LEVEL", "WARNING")
71
+ if log_level != "ERROR":
72
+ log_info(f"Successfully loaded {len(final_plugins)} plugins")
73
+ return final_plugins
74
+
75
+ def _load_from_entry_points(self) -> list[LanguagePlugin]:
76
+ """
77
+ Load plugins from setuptools entry points.
78
+
79
+ Returns:
80
+ List of plugin instances loaded from entry points
81
+ """
82
+ plugins = []
83
+
84
+ try:
85
+ # Get entry points for our plugin group
86
+ entry_points = importlib.metadata.entry_points()
87
+
88
+ # Handle both old and new entry_points API
89
+ plugin_entries: Any = []
90
+ if hasattr(entry_points, "select"):
91
+ # New API (Python 3.10+)
92
+ plugin_entries = entry_points.select(group=self._entry_point_group)
93
+ else:
94
+ # Old API - handle different return types
95
+ try:
96
+ # Try to get entry points, handling different API versions
97
+ if hasattr(entry_points, "get"):
98
+ result = entry_points.get(self._entry_point_group)
99
+ plugin_entries = list(result) if result else []
100
+ else:
101
+ plugin_entries = []
102
+ except (TypeError, AttributeError):
103
+ # Fallback for incompatible entry_points types
104
+ plugin_entries = []
105
+
106
+ for entry_point in plugin_entries:
107
+ try:
108
+ # Load the plugin class
109
+ plugin_class = entry_point.load()
110
+
111
+ # Validate it's a LanguagePlugin
112
+ if not issubclass(plugin_class, LanguagePlugin):
113
+ log_warning(
114
+ f"Entry point {entry_point.name} is not a LanguagePlugin"
115
+ )
116
+ continue
117
+
118
+ # Create instance
119
+ plugin_instance = plugin_class()
120
+ plugins.append(plugin_instance)
121
+
122
+ log_debug(f"Loaded plugin from entry point: {entry_point.name}")
123
+
124
+ except Exception as e:
125
+ log_error(
126
+ f"Failed to load plugin from entry point {entry_point.name}: {e}"
127
+ )
128
+
129
+ except Exception as e:
130
+ log_warning(f"Failed to load plugins from entry points: {e}")
131
+
132
+ return plugins
133
+
134
+ def _load_from_local_directory(self) -> list[LanguagePlugin]:
135
+ """
136
+ Load plugins from the local languages directory.
137
+
138
+ Returns:
139
+ List of plugin instances loaded from local directory
140
+ """
141
+ plugins: list[LanguagePlugin] = []
142
+
143
+ try:
144
+ # Get the languages directory path
145
+ current_dir = Path(__file__).parent.parent
146
+ languages_dir = current_dir / "languages"
147
+
148
+ if not languages_dir.exists():
149
+ log_debug("Languages directory does not exist, creating it")
150
+ languages_dir.mkdir(exist_ok=True)
151
+ # Create __init__.py
152
+ (languages_dir / "__init__.py").touch()
153
+ return plugins
154
+
155
+ # Import the languages package
156
+ languages_package = "tree_sitter_analyzer.languages"
157
+
158
+ try:
159
+ languages_module = importlib.import_module(languages_package)
160
+ except ImportError as e:
161
+ log_warning(f"Could not import languages package: {e}")
162
+ return plugins
163
+
164
+ # Discover plugin modules in the languages directory
165
+ for _finder, name, ispkg in pkgutil.iter_modules(
166
+ languages_module.__path__, languages_module.__name__ + "."
167
+ ):
168
+ if ispkg:
169
+ continue
170
+
171
+ try:
172
+ # Import the module
173
+ module = importlib.import_module(name)
174
+
175
+ # Look for LanguagePlugin classes
176
+ plugin_classes = self._find_plugin_classes(module)
177
+
178
+ for plugin_class in plugin_classes:
179
+ try:
180
+ plugin_instance = plugin_class()
181
+ plugins.append(plugin_instance)
182
+ log_debug(f"Loaded local plugin: {plugin_class.__name__}")
183
+ except Exception as e:
184
+ log_error(
185
+ f"Failed to instantiate plugin {plugin_class.__name__}: {e}"
186
+ )
187
+
188
+ except Exception as e:
189
+ log_error(f"Failed to load plugin module {name}: {e}")
190
+
191
+ except Exception as e:
192
+ log_warning(f"Failed to load plugins from local directory: {e}")
193
+
194
+ return plugins
195
+
196
+ def _find_plugin_classes(self, module: Any) -> list[type[LanguagePlugin]]:
197
+ """
198
+ Find LanguagePlugin classes in a module.
199
+
200
+ Args:
201
+ module: Python module to search
202
+
203
+ Returns:
204
+ List of LanguagePlugin classes found in the module
205
+ """
206
+ plugin_classes: list[type[LanguagePlugin]] = []
207
+
208
+ for attr_name in dir(module):
209
+ attr = getattr(module, attr_name)
210
+
211
+ # Check if it's a class and subclass of LanguagePlugin
212
+ if (
213
+ isinstance(attr, type)
214
+ and issubclass(attr, LanguagePlugin)
215
+ and attr is not LanguagePlugin
216
+ ):
217
+ plugin_classes.append(attr)
218
+
219
+ return plugin_classes
220
+
221
+ def get_plugin(self, language: str) -> LanguagePlugin | None:
222
+ """
223
+ Get a plugin for a specific language.
224
+
225
+ Args:
226
+ language: Programming language name
227
+
228
+ Returns:
229
+ Plugin instance or None if not found
230
+ """
231
+ return self._loaded_plugins.get(language)
232
+
233
+ def get_all_plugins(self) -> dict[str, LanguagePlugin]:
234
+ """
235
+ Get all loaded plugins.
236
+
237
+ Returns:
238
+ Dictionary mapping language names to plugin instances
239
+ """
240
+ return self._loaded_plugins.copy()
241
+
242
+ def get_supported_languages(self) -> list[str]:
243
+ """
244
+ Get list of all supported languages.
245
+
246
+ Returns:
247
+ List of supported language names
248
+ """
249
+ return list(self._loaded_plugins.keys())
250
+
251
+ def reload_plugins(self) -> list[LanguagePlugin]:
252
+ """
253
+ Reload all plugins (useful for development).
254
+
255
+ Returns:
256
+ List of reloaded plugin instances
257
+ """
258
+ log_info("Reloading all plugins")
259
+
260
+ # Clear existing plugins
261
+ self._loaded_plugins.clear()
262
+ self._plugin_classes.clear()
263
+
264
+ # Reload
265
+ return self.load_plugins()
266
+
267
+ def register_plugin(self, plugin: LanguagePlugin) -> bool:
268
+ """
269
+ Manually register a plugin instance.
270
+
271
+ Args:
272
+ plugin: Plugin instance to register
273
+
274
+ Returns:
275
+ True if registration was successful
276
+ """
277
+ try:
278
+ language = plugin.get_language_name()
279
+
280
+ if language in self._loaded_plugins:
281
+ log_warning(
282
+ f"Plugin for language '{language}' already exists, replacing"
283
+ )
284
+
285
+ self._loaded_plugins[language] = plugin
286
+ log_debug(f"Manually registered plugin for language: {language}")
287
+ return True
288
+
289
+ except Exception as e:
290
+ log_error(f"Failed to register plugin: {e}")
291
+ return False
292
+
293
+ def unregister_plugin(self, language: str) -> bool:
294
+ """
295
+ Unregister a plugin for a specific language.
296
+
297
+ Args:
298
+ language: Programming language name
299
+
300
+ Returns:
301
+ True if unregistration was successful
302
+ """
303
+ if language in self._loaded_plugins:
304
+ del self._loaded_plugins[language]
305
+ log_debug(f"Unregistered plugin for language: {language}")
306
+ return True
307
+
308
+ return False
309
+
310
+ def get_plugin_info(self, language: str) -> dict[str, Any] | None:
311
+ """
312
+ Get information about a specific plugin.
313
+
314
+ Args:
315
+ language: Programming language name
316
+
317
+ Returns:
318
+ Plugin information dictionary or None
319
+ """
320
+ plugin = self.get_plugin(language)
321
+ if not plugin:
322
+ return None
323
+
324
+ try:
325
+ return {
326
+ "language": plugin.get_language_name(),
327
+ "extensions": plugin.get_file_extensions(),
328
+ "class_name": plugin.__class__.__name__,
329
+ "module": plugin.__class__.__module__,
330
+ "has_extractor": hasattr(plugin, "create_extractor"),
331
+ }
332
+ except Exception as e:
333
+ log_error(f"Failed to get plugin info for {language}: {e}")
334
+ return None
335
+
336
+ def validate_plugin(self, plugin: LanguagePlugin) -> bool:
337
+ """
338
+ Validate that a plugin implements the required interface correctly.
339
+
340
+ Args:
341
+ plugin: Plugin instance to validate
342
+
343
+ Returns:
344
+ True if the plugin is valid
345
+ """
346
+ try:
347
+ # Check required methods
348
+ required_methods = [
349
+ "get_language_name",
350
+ "get_file_extensions",
351
+ "create_extractor",
352
+ ]
353
+
354
+ for method_name in required_methods:
355
+ if not hasattr(plugin, method_name):
356
+ log_error(f"Plugin missing required method: {method_name}")
357
+ return False
358
+
359
+ method = getattr(plugin, method_name)
360
+ if not callable(method):
361
+ log_error(f"Plugin method {method_name} is not callable")
362
+ return False
363
+
364
+ # Test basic functionality
365
+ language = plugin.get_language_name()
366
+ if not language or not isinstance(language, str):
367
+ log_error("Plugin get_language_name() must return a non-empty string")
368
+ return False
369
+
370
+ extensions = plugin.get_file_extensions()
371
+ if not isinstance(extensions, list):
372
+ log_error("Plugin get_file_extensions() must return a list") # type: ignore[unreachable]
373
+ return False
374
+
375
+ extractor = plugin.create_extractor()
376
+ if not extractor:
377
+ log_error("Plugin create_extractor() must return an extractor instance")
378
+ return False
379
+
380
+ return True
381
+
382
+ except Exception as e:
383
+ log_error(f"Plugin validation failed: {e}")
384
+ return False
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Root Detection
4
+
5
+ Intelligent detection of project root directories based on common project markers.
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Common project root indicators (in priority order)
14
+ PROJECT_MARKERS = [
15
+ # Version control
16
+ ".git",
17
+ ".hg",
18
+ ".svn",
19
+ # Python projects
20
+ "pyproject.toml",
21
+ "setup.py",
22
+ "setup.cfg",
23
+ "requirements.txt",
24
+ "Pipfile",
25
+ "poetry.lock",
26
+ "conda.yaml",
27
+ "environment.yml",
28
+ # JavaScript/Node.js projects
29
+ "package.json",
30
+ "package-lock.json",
31
+ "yarn.lock",
32
+ "node_modules",
33
+ # Java projects
34
+ "pom.xml",
35
+ "build.gradle",
36
+ "build.gradle.kts",
37
+ "gradlew",
38
+ "mvnw",
39
+ # C/C++ projects
40
+ "CMakeLists.txt",
41
+ "Makefile",
42
+ "configure.ac",
43
+ "configure.in",
44
+ # Rust projects
45
+ "Cargo.toml",
46
+ "Cargo.lock",
47
+ # Go projects
48
+ "go.mod",
49
+ "go.sum",
50
+ # .NET projects
51
+ "*.sln",
52
+ "*.csproj",
53
+ "*.vbproj",
54
+ "*.fsproj",
55
+ # Other common markers
56
+ "README.md",
57
+ "README.rst",
58
+ "README.txt",
59
+ "LICENSE",
60
+ "CHANGELOG.md",
61
+ ".dockerignore",
62
+ "Dockerfile",
63
+ "docker-compose.yml",
64
+ ".editorconfig",
65
+ ]
66
+
67
+
68
+ class ProjectRootDetector:
69
+ """Intelligent project root directory detection."""
70
+
71
+ def __init__(self, max_depth: int = 10):
72
+ """
73
+ Initialize project root detector.
74
+
75
+ Args:
76
+ max_depth: Maximum directory levels to traverse upward
77
+ """
78
+ self.max_depth = max_depth
79
+
80
+ def detect_from_file(self, file_path: str) -> str | None:
81
+ """
82
+ Detect project root from a file path.
83
+
84
+ Args:
85
+ file_path: Path to a file within the project
86
+
87
+ Returns:
88
+ Project root directory path, or None if not detected
89
+ """
90
+ if not file_path:
91
+ return None
92
+
93
+ try:
94
+ # Convert to absolute path and get directory
95
+ abs_path = Path(file_path).resolve()
96
+ if abs_path.is_file():
97
+ start_dir = abs_path.parent
98
+ else:
99
+ start_dir = abs_path
100
+
101
+ return self._traverse_upward(str(start_dir))
102
+
103
+ except Exception as e:
104
+ logger.warning(f"Error detecting project root from {file_path}: {e}")
105
+ return None
106
+
107
+ def detect_from_cwd(self) -> str | None:
108
+ """
109
+ Detect project root from current working directory.
110
+
111
+ Returns:
112
+ Project root directory path, or None if not detected
113
+ """
114
+ try:
115
+ return self._traverse_upward(str(Path.cwd()))
116
+ except Exception as e:
117
+ logger.warning(f"Error detecting project root from cwd: {e}")
118
+ return None
119
+
120
+ def _traverse_upward(self, start_dir: str) -> str | None:
121
+ """
122
+ Traverse upward from start directory looking for project markers.
123
+
124
+ Args:
125
+ start_dir: Directory to start traversal from
126
+
127
+ Returns:
128
+ Project root directory path, or None if not found
129
+ """
130
+ current_dir = str(Path(start_dir).resolve())
131
+ candidates = []
132
+
133
+ for _depth in range(self.max_depth):
134
+ # Check for project markers in current directory
135
+ markers_found = self._find_markers_in_dir(current_dir)
136
+
137
+ if markers_found:
138
+ # Calculate score based on marker priority and count
139
+ score = self._calculate_score(markers_found)
140
+ candidates.append((current_dir, score, markers_found))
141
+
142
+ # If we find high-priority markers, we can stop early
143
+ if any(
144
+ marker
145
+ in [
146
+ ".git",
147
+ "pyproject.toml",
148
+ "package.json",
149
+ "pom.xml",
150
+ "Cargo.toml",
151
+ "go.mod",
152
+ ]
153
+ for marker in markers_found
154
+ ):
155
+ logger.debug(
156
+ f"Found high-priority project root: {current_dir} (markers: {markers_found})"
157
+ )
158
+ return current_dir
159
+
160
+ # Move up one directory
161
+ current_path = Path(current_dir)
162
+ parent_path = current_path.parent
163
+ if parent_path == current_path: # Reached filesystem root
164
+ break
165
+ current_dir = str(parent_path)
166
+
167
+ # Return the best candidate if any found
168
+ if candidates:
169
+ # Sort by score (descending) and return the best
170
+ candidates.sort(key=lambda x: x[1], reverse=True)
171
+ best_candidate = candidates[0]
172
+ logger.debug(
173
+ f"Selected project root: {best_candidate[0]} (score: {best_candidate[1]}, markers: {best_candidate[2]})"
174
+ )
175
+ return best_candidate[0]
176
+
177
+ logger.debug(f"No project root detected from {start_dir}")
178
+ return None
179
+
180
+ def _find_markers_in_dir(self, directory: str) -> list[str]:
181
+ """
182
+ Find project markers in a directory.
183
+
184
+ Args:
185
+ directory: Directory to search in
186
+
187
+ Returns:
188
+ List of found marker names
189
+ """
190
+ found_markers = []
191
+
192
+ try:
193
+ dir_path = Path(directory)
194
+
195
+ for marker in PROJECT_MARKERS:
196
+ if "*" in marker:
197
+ # Handle glob patterns using pathlib
198
+ if list(dir_path.glob(marker)):
199
+ found_markers.append(marker)
200
+ else:
201
+ # Handle exact matches
202
+ if (dir_path / marker).exists():
203
+ found_markers.append(marker)
204
+
205
+ except (OSError, PermissionError) as e:
206
+ logger.debug(f"Cannot access directory {directory}: {e}")
207
+
208
+ return found_markers
209
+
210
+ def _calculate_score(self, markers: list[str]) -> int:
211
+ """
212
+ Calculate a score for project root candidates based on markers found.
213
+
214
+ Args:
215
+ markers: List of found markers
216
+
217
+ Returns:
218
+ Score (higher is better)
219
+ """
220
+ score = 0
221
+
222
+ # High-priority markers
223
+ high_priority = [
224
+ ".git",
225
+ "pyproject.toml",
226
+ "package.json",
227
+ "pom.xml",
228
+ "Cargo.toml",
229
+ "go.mod",
230
+ ]
231
+ medium_priority = ["setup.py", "requirements.txt", "CMakeLists.txt", "Makefile"]
232
+
233
+ for marker in markers:
234
+ if marker in high_priority:
235
+ score += 100
236
+ elif marker in medium_priority:
237
+ score += 50
238
+ else:
239
+ score += 10
240
+
241
+ # Bonus for multiple markers
242
+ if len(markers) > 1:
243
+ score += len(markers) * 5
244
+
245
+ return score
246
+
247
+ def get_fallback_root(self, file_path: str) -> str:
248
+ """
249
+ Get fallback project root when detection fails.
250
+
251
+ Args:
252
+ file_path: Original file path
253
+
254
+ Returns:
255
+ Fallback directory (file's directory or cwd)
256
+ """
257
+ try:
258
+ if file_path:
259
+ path = Path(file_path)
260
+ if path.exists():
261
+ if path.is_file():
262
+ return str(path.resolve().parent)
263
+ else:
264
+ return str(path.resolve())
265
+ return str(Path.cwd())
266
+ except Exception:
267
+ return str(Path.cwd())
268
+
269
+
270
+ def detect_project_root(
271
+ file_path: str | None = None, explicit_root: str | None = None
272
+ ) -> str | None:
273
+ """
274
+ Unified project root detection with priority handling.
275
+
276
+ Priority order:
277
+ 1. explicit_root parameter (highest priority)
278
+ 2. Auto-detection from file_path
279
+ 3. Auto-detection from current working directory
280
+ 4. Return None if no markers found
281
+
282
+ Args:
283
+ file_path: Path to a file within the project
284
+ explicit_root: Explicitly specified project root
285
+
286
+ Returns:
287
+ Project root directory path, or None if no markers found
288
+ """
289
+ detector = ProjectRootDetector()
290
+
291
+ # Priority 1: Explicit root
292
+ if explicit_root:
293
+ explicit_path = Path(explicit_root)
294
+ if explicit_path.exists() and explicit_path.is_dir():
295
+ logger.debug(f"Using explicit project root: {explicit_root}")
296
+ return str(explicit_path.resolve())
297
+ else:
298
+ logger.warning(f"Explicit project root does not exist: {explicit_root}")
299
+
300
+ # Priority 2: Auto-detection from file path
301
+ if file_path:
302
+ detected_root = detector.detect_from_file(file_path)
303
+ if detected_root:
304
+ logger.debug(f"Auto-detected project root from file: {detected_root}")
305
+ return detected_root
306
+
307
+ # Priority 3: Auto-detection from cwd
308
+ detected_root = detector.detect_from_cwd()
309
+ if detected_root:
310
+ logger.debug(f"Auto-detected project root from cwd: {detected_root}")
311
+ return detected_root
312
+
313
+ # Priority 4: Return None if no markers found
314
+ logger.debug("No project markers found, returning None")
315
+ return None
316
+
317
+
318
+ if __name__ == "__main__":
319
+ # Test the detector
320
+ import sys
321
+
322
+ if len(sys.argv) > 1:
323
+ test_path = sys.argv[1]
324
+ result = detect_project_root(test_path)
325
+ print(f"Project root for '{test_path}': {result}")
326
+ else:
327
+ result = detect_project_root()
328
+ print(f"Project root from cwd: {result}")