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