tree-sitter-analyzer 0.9.1__py3-none-any.whl → 0.9.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tree-sitter-analyzer might be problematic. Click here for more details.
- tree_sitter_analyzer/__init__.py +132 -132
- tree_sitter_analyzer/__main__.py +11 -11
- tree_sitter_analyzer/api.py +533 -533
- tree_sitter_analyzer/cli/__init__.py +39 -39
- tree_sitter_analyzer/cli/__main__.py +12 -12
- tree_sitter_analyzer/cli/commands/__init__.py +26 -26
- tree_sitter_analyzer/cli/commands/advanced_command.py +88 -88
- tree_sitter_analyzer/cli/commands/base_command.py +181 -178
- tree_sitter_analyzer/cli/commands/structure_command.py +138 -138
- tree_sitter_analyzer/cli/commands/summary_command.py +101 -101
- tree_sitter_analyzer/cli_main.py +7 -3
- tree_sitter_analyzer/core/__init__.py +15 -15
- tree_sitter_analyzer/core/analysis_engine.py +91 -87
- tree_sitter_analyzer/core/cache_service.py +320 -320
- tree_sitter_analyzer/core/engine.py +566 -566
- tree_sitter_analyzer/core/parser.py +293 -293
- tree_sitter_analyzer/encoding_utils.py +459 -459
- tree_sitter_analyzer/file_handler.py +210 -210
- tree_sitter_analyzer/formatters/__init__.py +1 -1
- tree_sitter_analyzer/formatters/base_formatter.py +167 -167
- tree_sitter_analyzer/formatters/formatter_factory.py +78 -78
- tree_sitter_analyzer/formatters/java_formatter.py +18 -18
- tree_sitter_analyzer/formatters/python_formatter.py +19 -19
- tree_sitter_analyzer/interfaces/__init__.py +9 -9
- tree_sitter_analyzer/interfaces/cli.py +528 -528
- tree_sitter_analyzer/interfaces/cli_adapter.py +344 -343
- tree_sitter_analyzer/interfaces/mcp_adapter.py +206 -206
- tree_sitter_analyzer/language_detector.py +53 -53
- tree_sitter_analyzer/languages/__init__.py +10 -10
- tree_sitter_analyzer/languages/java_plugin.py +1 -1
- tree_sitter_analyzer/languages/javascript_plugin.py +446 -446
- tree_sitter_analyzer/languages/python_plugin.py +755 -755
- tree_sitter_analyzer/mcp/__init__.py +34 -45
- tree_sitter_analyzer/mcp/resources/__init__.py +44 -44
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +209 -209
- tree_sitter_analyzer/mcp/server.py +623 -568
- tree_sitter_analyzer/mcp/tools/__init__.py +30 -30
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +681 -673
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +247 -247
- tree_sitter_analyzer/mcp/tools/base_tool.py +54 -54
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +310 -308
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +386 -379
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +563 -559
- tree_sitter_analyzer/mcp/utils/__init__.py +107 -107
- tree_sitter_analyzer/models.py +10 -10
- tree_sitter_analyzer/output_manager.py +253 -253
- tree_sitter_analyzer/plugins/__init__.py +280 -280
- tree_sitter_analyzer/plugins/base.py +529 -529
- tree_sitter_analyzer/plugins/manager.py +379 -379
- tree_sitter_analyzer/project_detector.py +330 -317
- tree_sitter_analyzer/queries/__init__.py +26 -26
- tree_sitter_analyzer/queries/java.py +391 -391
- tree_sitter_analyzer/queries/javascript.py +148 -148
- tree_sitter_analyzer/queries/python.py +285 -285
- tree_sitter_analyzer/queries/typescript.py +229 -229
- tree_sitter_analyzer/query_loader.py +257 -257
- tree_sitter_analyzer/security/boundary_manager.py +57 -51
- tree_sitter_analyzer/security/validator.py +246 -241
- tree_sitter_analyzer/utils.py +294 -277
- {tree_sitter_analyzer-0.9.1.dist-info → tree_sitter_analyzer-0.9.3.dist-info}/METADATA +13 -13
- tree_sitter_analyzer-0.9.3.dist-info/RECORD +77 -0
- {tree_sitter_analyzer-0.9.1.dist-info → tree_sitter_analyzer-0.9.3.dist-info}/entry_points.txt +1 -0
- tree_sitter_analyzer-0.9.1.dist-info/RECORD +0 -77
- {tree_sitter_analyzer-0.9.1.dist-info → tree_sitter_analyzer-0.9.3.dist-info}/WHEEL +0 -0
|
@@ -1,379 +1,379 @@
|
|
|
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
|
-
log_info(f"Successfully loaded {len(final_plugins)} plugins")
|
|
68
|
-
return final_plugins
|
|
69
|
-
|
|
70
|
-
def _load_from_entry_points(self) -> list[LanguagePlugin]:
|
|
71
|
-
"""
|
|
72
|
-
Load plugins from setuptools entry points.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
List of plugin instances loaded from entry points
|
|
76
|
-
"""
|
|
77
|
-
plugins = []
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
# Get entry points for our plugin group
|
|
81
|
-
entry_points = importlib.metadata.entry_points()
|
|
82
|
-
|
|
83
|
-
# Handle both old and new entry_points API
|
|
84
|
-
plugin_entries: Any = []
|
|
85
|
-
if hasattr(entry_points, "select"):
|
|
86
|
-
# New API (Python 3.10+)
|
|
87
|
-
plugin_entries = entry_points.select(group=self._entry_point_group)
|
|
88
|
-
else:
|
|
89
|
-
# Old API - handle different return types
|
|
90
|
-
try:
|
|
91
|
-
# Try to get entry points, handling different API versions
|
|
92
|
-
if hasattr(entry_points, "get"):
|
|
93
|
-
result = entry_points.get(self._entry_point_group)
|
|
94
|
-
plugin_entries = list(result) if result else []
|
|
95
|
-
else:
|
|
96
|
-
plugin_entries = []
|
|
97
|
-
except (TypeError, AttributeError):
|
|
98
|
-
# Fallback for incompatible entry_points types
|
|
99
|
-
plugin_entries = []
|
|
100
|
-
|
|
101
|
-
for entry_point in plugin_entries:
|
|
102
|
-
try:
|
|
103
|
-
# Load the plugin class
|
|
104
|
-
plugin_class = entry_point.load()
|
|
105
|
-
|
|
106
|
-
# Validate it's a LanguagePlugin
|
|
107
|
-
if not issubclass(plugin_class, LanguagePlugin):
|
|
108
|
-
log_warning(
|
|
109
|
-
f"Entry point {entry_point.name} is not a LanguagePlugin"
|
|
110
|
-
)
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
# Create instance
|
|
114
|
-
plugin_instance = plugin_class()
|
|
115
|
-
plugins.append(plugin_instance)
|
|
116
|
-
|
|
117
|
-
log_debug(f"Loaded plugin from entry point: {entry_point.name}")
|
|
118
|
-
|
|
119
|
-
except Exception as e:
|
|
120
|
-
log_error(
|
|
121
|
-
f"Failed to load plugin from entry point {entry_point.name}: {e}"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
log_warning(f"Failed to load plugins from entry points: {e}")
|
|
126
|
-
|
|
127
|
-
return plugins
|
|
128
|
-
|
|
129
|
-
def _load_from_local_directory(self) -> list[LanguagePlugin]:
|
|
130
|
-
"""
|
|
131
|
-
Load plugins from the local languages directory.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
List of plugin instances loaded from local directory
|
|
135
|
-
"""
|
|
136
|
-
plugins: list[LanguagePlugin] = []
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
# Get the languages directory path
|
|
140
|
-
current_dir = Path(__file__).parent.parent
|
|
141
|
-
languages_dir = current_dir / "languages"
|
|
142
|
-
|
|
143
|
-
if not languages_dir.exists():
|
|
144
|
-
log_debug("Languages directory does not exist, creating it")
|
|
145
|
-
languages_dir.mkdir(exist_ok=True)
|
|
146
|
-
# Create __init__.py
|
|
147
|
-
(languages_dir / "__init__.py").touch()
|
|
148
|
-
return plugins
|
|
149
|
-
|
|
150
|
-
# Import the languages package
|
|
151
|
-
languages_package = "tree_sitter_analyzer.languages"
|
|
152
|
-
|
|
153
|
-
try:
|
|
154
|
-
languages_module = importlib.import_module(languages_package)
|
|
155
|
-
except ImportError as e:
|
|
156
|
-
log_warning(f"Could not import languages package: {e}")
|
|
157
|
-
return plugins
|
|
158
|
-
|
|
159
|
-
# Discover plugin modules in the languages directory
|
|
160
|
-
for _finder, name, ispkg in pkgutil.iter_modules(
|
|
161
|
-
languages_module.__path__, languages_module.__name__ + "."
|
|
162
|
-
):
|
|
163
|
-
if ispkg:
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
# Import the module
|
|
168
|
-
module = importlib.import_module(name)
|
|
169
|
-
|
|
170
|
-
# Look for LanguagePlugin classes
|
|
171
|
-
plugin_classes = self._find_plugin_classes(module)
|
|
172
|
-
|
|
173
|
-
for plugin_class in plugin_classes:
|
|
174
|
-
try:
|
|
175
|
-
plugin_instance = plugin_class()
|
|
176
|
-
plugins.append(plugin_instance)
|
|
177
|
-
log_debug(f"Loaded local plugin: {plugin_class.__name__}")
|
|
178
|
-
except Exception as e:
|
|
179
|
-
log_error(
|
|
180
|
-
f"Failed to instantiate plugin {plugin_class.__name__}: {e}"
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
except Exception as e:
|
|
184
|
-
log_error(f"Failed to load plugin module {name}: {e}")
|
|
185
|
-
|
|
186
|
-
except Exception as e:
|
|
187
|
-
log_warning(f"Failed to load plugins from local directory: {e}")
|
|
188
|
-
|
|
189
|
-
return plugins
|
|
190
|
-
|
|
191
|
-
def _find_plugin_classes(self, module: Any) -> list[type[LanguagePlugin]]:
|
|
192
|
-
"""
|
|
193
|
-
Find LanguagePlugin classes in a module.
|
|
194
|
-
|
|
195
|
-
Args:
|
|
196
|
-
module: Python module to search
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
List of LanguagePlugin classes found in the module
|
|
200
|
-
"""
|
|
201
|
-
plugin_classes: list[type[LanguagePlugin]] = []
|
|
202
|
-
|
|
203
|
-
for attr_name in dir(module):
|
|
204
|
-
attr = getattr(module, attr_name)
|
|
205
|
-
|
|
206
|
-
# Check if it's a class and subclass of LanguagePlugin
|
|
207
|
-
if (
|
|
208
|
-
isinstance(attr, type)
|
|
209
|
-
and issubclass(attr, LanguagePlugin)
|
|
210
|
-
and attr is not LanguagePlugin
|
|
211
|
-
):
|
|
212
|
-
plugin_classes.append(attr)
|
|
213
|
-
|
|
214
|
-
return plugin_classes
|
|
215
|
-
|
|
216
|
-
def get_plugin(self, language: str) -> LanguagePlugin | None:
|
|
217
|
-
"""
|
|
218
|
-
Get a plugin for a specific language.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
language: Programming language name
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
Plugin instance or None if not found
|
|
225
|
-
"""
|
|
226
|
-
return self._loaded_plugins.get(language)
|
|
227
|
-
|
|
228
|
-
def get_all_plugins(self) -> dict[str, LanguagePlugin]:
|
|
229
|
-
"""
|
|
230
|
-
Get all loaded plugins.
|
|
231
|
-
|
|
232
|
-
Returns:
|
|
233
|
-
Dictionary mapping language names to plugin instances
|
|
234
|
-
"""
|
|
235
|
-
return self._loaded_plugins.copy()
|
|
236
|
-
|
|
237
|
-
def get_supported_languages(self) -> list[str]:
|
|
238
|
-
"""
|
|
239
|
-
Get list of all supported languages.
|
|
240
|
-
|
|
241
|
-
Returns:
|
|
242
|
-
List of supported language names
|
|
243
|
-
"""
|
|
244
|
-
return list(self._loaded_plugins.keys())
|
|
245
|
-
|
|
246
|
-
def reload_plugins(self) -> list[LanguagePlugin]:
|
|
247
|
-
"""
|
|
248
|
-
Reload all plugins (useful for development).
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
List of reloaded plugin instances
|
|
252
|
-
"""
|
|
253
|
-
log_info("Reloading all plugins")
|
|
254
|
-
|
|
255
|
-
# Clear existing plugins
|
|
256
|
-
self._loaded_plugins.clear()
|
|
257
|
-
self._plugin_classes.clear()
|
|
258
|
-
|
|
259
|
-
# Reload
|
|
260
|
-
return self.load_plugins()
|
|
261
|
-
|
|
262
|
-
def register_plugin(self, plugin: LanguagePlugin) -> bool:
|
|
263
|
-
"""
|
|
264
|
-
Manually register a plugin instance.
|
|
265
|
-
|
|
266
|
-
Args:
|
|
267
|
-
plugin: Plugin instance to register
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
True if registration was successful
|
|
271
|
-
"""
|
|
272
|
-
try:
|
|
273
|
-
language = plugin.get_language_name()
|
|
274
|
-
|
|
275
|
-
if language in self._loaded_plugins:
|
|
276
|
-
log_warning(
|
|
277
|
-
f"Plugin for language '{language}' already exists, replacing"
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
self._loaded_plugins[language] = plugin
|
|
281
|
-
log_debug(f"Manually registered plugin for language: {language}")
|
|
282
|
-
return True
|
|
283
|
-
|
|
284
|
-
except Exception as e:
|
|
285
|
-
log_error(f"Failed to register plugin: {e}")
|
|
286
|
-
return False
|
|
287
|
-
|
|
288
|
-
def unregister_plugin(self, language: str) -> bool:
|
|
289
|
-
"""
|
|
290
|
-
Unregister a plugin for a specific language.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
language: Programming language name
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
True if unregistration was successful
|
|
297
|
-
"""
|
|
298
|
-
if language in self._loaded_plugins:
|
|
299
|
-
del self._loaded_plugins[language]
|
|
300
|
-
log_debug(f"Unregistered plugin for language: {language}")
|
|
301
|
-
return True
|
|
302
|
-
|
|
303
|
-
return False
|
|
304
|
-
|
|
305
|
-
def get_plugin_info(self, language: str) -> dict[str, Any] | None:
|
|
306
|
-
"""
|
|
307
|
-
Get information about a specific plugin.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
language: Programming language name
|
|
311
|
-
|
|
312
|
-
Returns:
|
|
313
|
-
Plugin information dictionary or None
|
|
314
|
-
"""
|
|
315
|
-
plugin = self.get_plugin(language)
|
|
316
|
-
if not plugin:
|
|
317
|
-
return None
|
|
318
|
-
|
|
319
|
-
try:
|
|
320
|
-
return {
|
|
321
|
-
"language": plugin.get_language_name(),
|
|
322
|
-
"extensions": plugin.get_file_extensions(),
|
|
323
|
-
"class_name": plugin.__class__.__name__,
|
|
324
|
-
"module": plugin.__class__.__module__,
|
|
325
|
-
"has_extractor": hasattr(plugin, "create_extractor"),
|
|
326
|
-
}
|
|
327
|
-
except Exception as e:
|
|
328
|
-
log_error(f"Failed to get plugin info for {language}: {e}")
|
|
329
|
-
return None
|
|
330
|
-
|
|
331
|
-
def validate_plugin(self, plugin: LanguagePlugin) -> bool:
|
|
332
|
-
"""
|
|
333
|
-
Validate that a plugin implements the required interface correctly.
|
|
334
|
-
|
|
335
|
-
Args:
|
|
336
|
-
plugin: Plugin instance to validate
|
|
337
|
-
|
|
338
|
-
Returns:
|
|
339
|
-
True if the plugin is valid
|
|
340
|
-
"""
|
|
341
|
-
try:
|
|
342
|
-
# Check required methods
|
|
343
|
-
required_methods = [
|
|
344
|
-
"get_language_name",
|
|
345
|
-
"get_file_extensions",
|
|
346
|
-
"create_extractor",
|
|
347
|
-
]
|
|
348
|
-
|
|
349
|
-
for method_name in required_methods:
|
|
350
|
-
if not hasattr(plugin, method_name):
|
|
351
|
-
log_error(f"Plugin missing required method: {method_name}")
|
|
352
|
-
return False
|
|
353
|
-
|
|
354
|
-
method = getattr(plugin, method_name)
|
|
355
|
-
if not callable(method):
|
|
356
|
-
log_error(f"Plugin method {method_name} is not callable")
|
|
357
|
-
return False
|
|
358
|
-
|
|
359
|
-
# Test basic functionality
|
|
360
|
-
language = plugin.get_language_name()
|
|
361
|
-
if not language or not isinstance(language, str):
|
|
362
|
-
log_error("Plugin get_language_name() must return a non-empty string")
|
|
363
|
-
return False
|
|
364
|
-
|
|
365
|
-
extensions = plugin.get_file_extensions()
|
|
366
|
-
if not isinstance(extensions, list):
|
|
367
|
-
log_error("Plugin get_file_extensions() must return a list") # type: ignore[unreachable]
|
|
368
|
-
return False
|
|
369
|
-
|
|
370
|
-
extractor = plugin.create_extractor()
|
|
371
|
-
if not extractor:
|
|
372
|
-
log_error("Plugin create_extractor() must return an extractor instance")
|
|
373
|
-
return False
|
|
374
|
-
|
|
375
|
-
return True
|
|
376
|
-
|
|
377
|
-
except Exception as e:
|
|
378
|
-
log_error(f"Plugin validation failed: {e}")
|
|
379
|
-
return False
|
|
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
|
+
log_info(f"Successfully loaded {len(final_plugins)} plugins")
|
|
68
|
+
return final_plugins
|
|
69
|
+
|
|
70
|
+
def _load_from_entry_points(self) -> list[LanguagePlugin]:
|
|
71
|
+
"""
|
|
72
|
+
Load plugins from setuptools entry points.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of plugin instances loaded from entry points
|
|
76
|
+
"""
|
|
77
|
+
plugins = []
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Get entry points for our plugin group
|
|
81
|
+
entry_points = importlib.metadata.entry_points()
|
|
82
|
+
|
|
83
|
+
# Handle both old and new entry_points API
|
|
84
|
+
plugin_entries: Any = []
|
|
85
|
+
if hasattr(entry_points, "select"):
|
|
86
|
+
# New API (Python 3.10+)
|
|
87
|
+
plugin_entries = entry_points.select(group=self._entry_point_group)
|
|
88
|
+
else:
|
|
89
|
+
# Old API - handle different return types
|
|
90
|
+
try:
|
|
91
|
+
# Try to get entry points, handling different API versions
|
|
92
|
+
if hasattr(entry_points, "get"):
|
|
93
|
+
result = entry_points.get(self._entry_point_group)
|
|
94
|
+
plugin_entries = list(result) if result else []
|
|
95
|
+
else:
|
|
96
|
+
plugin_entries = []
|
|
97
|
+
except (TypeError, AttributeError):
|
|
98
|
+
# Fallback for incompatible entry_points types
|
|
99
|
+
plugin_entries = []
|
|
100
|
+
|
|
101
|
+
for entry_point in plugin_entries:
|
|
102
|
+
try:
|
|
103
|
+
# Load the plugin class
|
|
104
|
+
plugin_class = entry_point.load()
|
|
105
|
+
|
|
106
|
+
# Validate it's a LanguagePlugin
|
|
107
|
+
if not issubclass(plugin_class, LanguagePlugin):
|
|
108
|
+
log_warning(
|
|
109
|
+
f"Entry point {entry_point.name} is not a LanguagePlugin"
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Create instance
|
|
114
|
+
plugin_instance = plugin_class()
|
|
115
|
+
plugins.append(plugin_instance)
|
|
116
|
+
|
|
117
|
+
log_debug(f"Loaded plugin from entry point: {entry_point.name}")
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
log_error(
|
|
121
|
+
f"Failed to load plugin from entry point {entry_point.name}: {e}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
log_warning(f"Failed to load plugins from entry points: {e}")
|
|
126
|
+
|
|
127
|
+
return plugins
|
|
128
|
+
|
|
129
|
+
def _load_from_local_directory(self) -> list[LanguagePlugin]:
|
|
130
|
+
"""
|
|
131
|
+
Load plugins from the local languages directory.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of plugin instances loaded from local directory
|
|
135
|
+
"""
|
|
136
|
+
plugins: list[LanguagePlugin] = []
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Get the languages directory path
|
|
140
|
+
current_dir = Path(__file__).parent.parent
|
|
141
|
+
languages_dir = current_dir / "languages"
|
|
142
|
+
|
|
143
|
+
if not languages_dir.exists():
|
|
144
|
+
log_debug("Languages directory does not exist, creating it")
|
|
145
|
+
languages_dir.mkdir(exist_ok=True)
|
|
146
|
+
# Create __init__.py
|
|
147
|
+
(languages_dir / "__init__.py").touch()
|
|
148
|
+
return plugins
|
|
149
|
+
|
|
150
|
+
# Import the languages package
|
|
151
|
+
languages_package = "tree_sitter_analyzer.languages"
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
languages_module = importlib.import_module(languages_package)
|
|
155
|
+
except ImportError as e:
|
|
156
|
+
log_warning(f"Could not import languages package: {e}")
|
|
157
|
+
return plugins
|
|
158
|
+
|
|
159
|
+
# Discover plugin modules in the languages directory
|
|
160
|
+
for _finder, name, ispkg in pkgutil.iter_modules(
|
|
161
|
+
languages_module.__path__, languages_module.__name__ + "."
|
|
162
|
+
):
|
|
163
|
+
if ispkg:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# Import the module
|
|
168
|
+
module = importlib.import_module(name)
|
|
169
|
+
|
|
170
|
+
# Look for LanguagePlugin classes
|
|
171
|
+
plugin_classes = self._find_plugin_classes(module)
|
|
172
|
+
|
|
173
|
+
for plugin_class in plugin_classes:
|
|
174
|
+
try:
|
|
175
|
+
plugin_instance = plugin_class()
|
|
176
|
+
plugins.append(plugin_instance)
|
|
177
|
+
log_debug(f"Loaded local plugin: {plugin_class.__name__}")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
log_error(
|
|
180
|
+
f"Failed to instantiate plugin {plugin_class.__name__}: {e}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
log_error(f"Failed to load plugin module {name}: {e}")
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
log_warning(f"Failed to load plugins from local directory: {e}")
|
|
188
|
+
|
|
189
|
+
return plugins
|
|
190
|
+
|
|
191
|
+
def _find_plugin_classes(self, module: Any) -> list[type[LanguagePlugin]]:
|
|
192
|
+
"""
|
|
193
|
+
Find LanguagePlugin classes in a module.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
module: Python module to search
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of LanguagePlugin classes found in the module
|
|
200
|
+
"""
|
|
201
|
+
plugin_classes: list[type[LanguagePlugin]] = []
|
|
202
|
+
|
|
203
|
+
for attr_name in dir(module):
|
|
204
|
+
attr = getattr(module, attr_name)
|
|
205
|
+
|
|
206
|
+
# Check if it's a class and subclass of LanguagePlugin
|
|
207
|
+
if (
|
|
208
|
+
isinstance(attr, type)
|
|
209
|
+
and issubclass(attr, LanguagePlugin)
|
|
210
|
+
and attr is not LanguagePlugin
|
|
211
|
+
):
|
|
212
|
+
plugin_classes.append(attr)
|
|
213
|
+
|
|
214
|
+
return plugin_classes
|
|
215
|
+
|
|
216
|
+
def get_plugin(self, language: str) -> LanguagePlugin | None:
|
|
217
|
+
"""
|
|
218
|
+
Get a plugin for a specific language.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
language: Programming language name
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Plugin instance or None if not found
|
|
225
|
+
"""
|
|
226
|
+
return self._loaded_plugins.get(language)
|
|
227
|
+
|
|
228
|
+
def get_all_plugins(self) -> dict[str, LanguagePlugin]:
|
|
229
|
+
"""
|
|
230
|
+
Get all loaded plugins.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary mapping language names to plugin instances
|
|
234
|
+
"""
|
|
235
|
+
return self._loaded_plugins.copy()
|
|
236
|
+
|
|
237
|
+
def get_supported_languages(self) -> list[str]:
|
|
238
|
+
"""
|
|
239
|
+
Get list of all supported languages.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of supported language names
|
|
243
|
+
"""
|
|
244
|
+
return list(self._loaded_plugins.keys())
|
|
245
|
+
|
|
246
|
+
def reload_plugins(self) -> list[LanguagePlugin]:
|
|
247
|
+
"""
|
|
248
|
+
Reload all plugins (useful for development).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of reloaded plugin instances
|
|
252
|
+
"""
|
|
253
|
+
log_info("Reloading all plugins")
|
|
254
|
+
|
|
255
|
+
# Clear existing plugins
|
|
256
|
+
self._loaded_plugins.clear()
|
|
257
|
+
self._plugin_classes.clear()
|
|
258
|
+
|
|
259
|
+
# Reload
|
|
260
|
+
return self.load_plugins()
|
|
261
|
+
|
|
262
|
+
def register_plugin(self, plugin: LanguagePlugin) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Manually register a plugin instance.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
plugin: Plugin instance to register
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if registration was successful
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
language = plugin.get_language_name()
|
|
274
|
+
|
|
275
|
+
if language in self._loaded_plugins:
|
|
276
|
+
log_warning(
|
|
277
|
+
f"Plugin for language '{language}' already exists, replacing"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self._loaded_plugins[language] = plugin
|
|
281
|
+
log_debug(f"Manually registered plugin for language: {language}")
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
log_error(f"Failed to register plugin: {e}")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
def unregister_plugin(self, language: str) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Unregister a plugin for a specific language.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
language: Programming language name
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
True if unregistration was successful
|
|
297
|
+
"""
|
|
298
|
+
if language in self._loaded_plugins:
|
|
299
|
+
del self._loaded_plugins[language]
|
|
300
|
+
log_debug(f"Unregistered plugin for language: {language}")
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def get_plugin_info(self, language: str) -> dict[str, Any] | None:
|
|
306
|
+
"""
|
|
307
|
+
Get information about a specific plugin.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
language: Programming language name
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Plugin information dictionary or None
|
|
314
|
+
"""
|
|
315
|
+
plugin = self.get_plugin(language)
|
|
316
|
+
if not plugin:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
return {
|
|
321
|
+
"language": plugin.get_language_name(),
|
|
322
|
+
"extensions": plugin.get_file_extensions(),
|
|
323
|
+
"class_name": plugin.__class__.__name__,
|
|
324
|
+
"module": plugin.__class__.__module__,
|
|
325
|
+
"has_extractor": hasattr(plugin, "create_extractor"),
|
|
326
|
+
}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
log_error(f"Failed to get plugin info for {language}: {e}")
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def validate_plugin(self, plugin: LanguagePlugin) -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Validate that a plugin implements the required interface correctly.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
plugin: Plugin instance to validate
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
True if the plugin is valid
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
# Check required methods
|
|
343
|
+
required_methods = [
|
|
344
|
+
"get_language_name",
|
|
345
|
+
"get_file_extensions",
|
|
346
|
+
"create_extractor",
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
for method_name in required_methods:
|
|
350
|
+
if not hasattr(plugin, method_name):
|
|
351
|
+
log_error(f"Plugin missing required method: {method_name}")
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
method = getattr(plugin, method_name)
|
|
355
|
+
if not callable(method):
|
|
356
|
+
log_error(f"Plugin method {method_name} is not callable")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# Test basic functionality
|
|
360
|
+
language = plugin.get_language_name()
|
|
361
|
+
if not language or not isinstance(language, str):
|
|
362
|
+
log_error("Plugin get_language_name() must return a non-empty string")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
extensions = plugin.get_file_extensions()
|
|
366
|
+
if not isinstance(extensions, list):
|
|
367
|
+
log_error("Plugin get_file_extensions() must return a list") # type: ignore[unreachable]
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
extractor = plugin.create_extractor()
|
|
371
|
+
if not extractor:
|
|
372
|
+
log_error("Plugin create_extractor() must return an extractor instance")
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
log_error(f"Plugin validation failed: {e}")
|
|
379
|
+
return False
|