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,673 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Rust Language Plugin
|
|
4
|
+
|
|
5
|
+
Provides Rust-specific parsing and element extraction functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import tree_sitter
|
|
13
|
+
|
|
14
|
+
from ..core.analysis_engine import AnalysisRequest
|
|
15
|
+
from ..models import AnalysisResult
|
|
16
|
+
|
|
17
|
+
from ..encoding_utils import extract_text_slice, safe_encode
|
|
18
|
+
from ..models import Class, Function, Import, Variable
|
|
19
|
+
from ..plugins.base import ElementExtractor, LanguagePlugin
|
|
20
|
+
from ..utils import log_debug, log_error
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RustElementExtractor(ElementExtractor):
|
|
24
|
+
"""Rust-specific element extractor"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize the Rust element extractor."""
|
|
28
|
+
self.current_module: str = ""
|
|
29
|
+
self.current_file: str = ""
|
|
30
|
+
self.source_code: str = ""
|
|
31
|
+
self.content_lines: list[str] = []
|
|
32
|
+
self._node_text_cache: dict[int, str] = {}
|
|
33
|
+
self.impl_blocks: list[dict[str, Any]] = []
|
|
34
|
+
self.modules: list[dict[str, Any]] = []
|
|
35
|
+
|
|
36
|
+
def extract_functions(
|
|
37
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
38
|
+
) -> list[Function]:
|
|
39
|
+
"""Extract Rust function declarations"""
|
|
40
|
+
self.source_code = source_code
|
|
41
|
+
self.content_lines = source_code.split("\n")
|
|
42
|
+
self._reset_caches()
|
|
43
|
+
|
|
44
|
+
functions: list[Function] = []
|
|
45
|
+
|
|
46
|
+
# Use tree traversal to find function_item
|
|
47
|
+
self._traverse_and_extract(
|
|
48
|
+
tree.root_node,
|
|
49
|
+
{"function_item": self._extract_function},
|
|
50
|
+
functions,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
log_debug(f"Extracted {len(functions)} Rust functions")
|
|
54
|
+
return functions
|
|
55
|
+
|
|
56
|
+
def extract_classes(
|
|
57
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
58
|
+
) -> list[Class]:
|
|
59
|
+
"""Extract Rust struct, enum, trait, and impl definitions"""
|
|
60
|
+
self.source_code = source_code
|
|
61
|
+
self.content_lines = source_code.split("\n")
|
|
62
|
+
self._reset_caches()
|
|
63
|
+
|
|
64
|
+
# Extract modules first
|
|
65
|
+
self._extract_modules(tree.root_node)
|
|
66
|
+
|
|
67
|
+
classes: list[Class] = []
|
|
68
|
+
|
|
69
|
+
extractors = {
|
|
70
|
+
"struct_item": self._extract_struct,
|
|
71
|
+
"enum_item": self._extract_enum,
|
|
72
|
+
"trait_item": self._extract_trait,
|
|
73
|
+
"impl_item": self._extract_impl, # Impl blocks are treated as related to classes
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
self._traverse_and_extract(
|
|
77
|
+
tree.root_node,
|
|
78
|
+
extractors,
|
|
79
|
+
classes,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Process collected impl blocks and add them to classes list if they are standalone
|
|
83
|
+
# Or we might want to return them as separate metadata.
|
|
84
|
+
# For now, we'll include impl blocks as Class objects with type='impl' for visibility
|
|
85
|
+
for _impl in self.impl_blocks:
|
|
86
|
+
# Creating a Class object for impl block to represent it in the structure
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
log_debug(f"Extracted {len(classes)} Rust structs/enums/traits")
|
|
90
|
+
return classes
|
|
91
|
+
|
|
92
|
+
def extract_variables(
|
|
93
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
94
|
+
) -> list[Variable]:
|
|
95
|
+
"""Extract Rust struct fields"""
|
|
96
|
+
self.source_code = source_code
|
|
97
|
+
self.content_lines = source_code.split("\n")
|
|
98
|
+
self._reset_caches()
|
|
99
|
+
|
|
100
|
+
variables: list[Variable] = []
|
|
101
|
+
|
|
102
|
+
# We extract fields from struct definitions
|
|
103
|
+
extractors = {
|
|
104
|
+
"field_declaration": self._extract_field,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
self._traverse_and_extract(
|
|
108
|
+
tree.root_node,
|
|
109
|
+
extractors,
|
|
110
|
+
variables,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
log_debug(f"Extracted {len(variables)} Rust fields")
|
|
114
|
+
return variables
|
|
115
|
+
|
|
116
|
+
def extract_imports(
|
|
117
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
118
|
+
) -> list[Import]:
|
|
119
|
+
"""Extract Rust use declarations"""
|
|
120
|
+
self.source_code = source_code
|
|
121
|
+
self.content_lines = source_code.split("\n")
|
|
122
|
+
self._reset_caches()
|
|
123
|
+
|
|
124
|
+
imports: list[Import] = []
|
|
125
|
+
|
|
126
|
+
# We extract use declarations
|
|
127
|
+
extractors = {
|
|
128
|
+
"use_declaration": self._extract_import,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
self._traverse_and_extract(
|
|
132
|
+
tree.root_node,
|
|
133
|
+
extractors,
|
|
134
|
+
imports,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
log_debug(f"Extracted {len(imports)} Rust imports")
|
|
138
|
+
return imports
|
|
139
|
+
|
|
140
|
+
def _extract_import(self, node: "tree_sitter.Node") -> Import | None:
|
|
141
|
+
"""Extract import statement (use declaration)"""
|
|
142
|
+
try:
|
|
143
|
+
raw_text = self._get_node_text(node)
|
|
144
|
+
start_line = node.start_point[0] + 1
|
|
145
|
+
end_line = node.end_point[0] + 1
|
|
146
|
+
|
|
147
|
+
# Extract name (the path)
|
|
148
|
+
# use std::collections::HashMap;
|
|
149
|
+
# The actual path is within the node children.
|
|
150
|
+
# Typically we can just use raw text or try to parse it.
|
|
151
|
+
# For simplicity, we'll use raw text as the import statement.
|
|
152
|
+
|
|
153
|
+
return Import(
|
|
154
|
+
name=raw_text, # Use full statement as name for now, or parse better
|
|
155
|
+
start_line=start_line,
|
|
156
|
+
end_line=end_line,
|
|
157
|
+
raw_text=raw_text,
|
|
158
|
+
language="rust",
|
|
159
|
+
import_statement=raw_text,
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
log_error(f"Error extracting Rust import: {e}")
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _reset_caches(self) -> None:
|
|
166
|
+
"""Reset performance caches"""
|
|
167
|
+
self._node_text_cache.clear()
|
|
168
|
+
# Modules and impls persist across extraction calls within the same file analysis
|
|
169
|
+
# but we clear them here if we assume sequential full extraction calls.
|
|
170
|
+
# Ideally, we should call extract_modules separately or share state.
|
|
171
|
+
# For simplicity in this architecture, we might re-extract or just check if empty.
|
|
172
|
+
if not self.source_code:
|
|
173
|
+
self.modules.clear()
|
|
174
|
+
self.impl_blocks.clear()
|
|
175
|
+
|
|
176
|
+
def _traverse_and_extract(
|
|
177
|
+
self,
|
|
178
|
+
node: "tree_sitter.Node",
|
|
179
|
+
extractors: dict[str, Any],
|
|
180
|
+
results: list[Any],
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Recursive traversal to find and extract elements"""
|
|
183
|
+
if node.type in extractors:
|
|
184
|
+
element = extractors[node.type](node)
|
|
185
|
+
if element:
|
|
186
|
+
results.append(element)
|
|
187
|
+
|
|
188
|
+
for child in node.children:
|
|
189
|
+
self._traverse_and_extract(child, extractors, results)
|
|
190
|
+
|
|
191
|
+
def _extract_modules(self, node: "tree_sitter.Node") -> None:
|
|
192
|
+
"""Extract module information"""
|
|
193
|
+
if node.type == "mod_item":
|
|
194
|
+
self._extract_module(node)
|
|
195
|
+
|
|
196
|
+
for child in node.children:
|
|
197
|
+
self._extract_modules(child)
|
|
198
|
+
|
|
199
|
+
def _extract_module(self, node: "tree_sitter.Node") -> None:
|
|
200
|
+
"""Extract single module"""
|
|
201
|
+
try:
|
|
202
|
+
name_node = node.child_by_field_name("name")
|
|
203
|
+
if name_node:
|
|
204
|
+
name = self._get_node_text(name_node)
|
|
205
|
+
visibility = self._extract_visibility(node)
|
|
206
|
+
|
|
207
|
+
self.modules.append(
|
|
208
|
+
{
|
|
209
|
+
"name": name,
|
|
210
|
+
"visibility": visibility,
|
|
211
|
+
"line_range": {
|
|
212
|
+
"start": node.start_point[0] + 1,
|
|
213
|
+
"end": node.end_point[0] + 1,
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
log_error(f"Error extracting module: {e}")
|
|
219
|
+
|
|
220
|
+
def _extract_function(self, node: "tree_sitter.Node") -> Function | None:
|
|
221
|
+
"""Extract function information"""
|
|
222
|
+
try:
|
|
223
|
+
name_node = node.child_by_field_name("name")
|
|
224
|
+
if not name_node:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
name = self._get_node_text(name_node)
|
|
228
|
+
start_line = node.start_point[0] + 1
|
|
229
|
+
end_line = node.end_point[0] + 1
|
|
230
|
+
|
|
231
|
+
# Parameters
|
|
232
|
+
parameters = []
|
|
233
|
+
params_node = node.child_by_field_name("parameters")
|
|
234
|
+
if params_node:
|
|
235
|
+
for child in params_node.children:
|
|
236
|
+
if child.type == "parameter":
|
|
237
|
+
parameters.append(self._get_node_text(child))
|
|
238
|
+
elif child.type == "self_parameter":
|
|
239
|
+
parameters.append("self")
|
|
240
|
+
|
|
241
|
+
# Return type
|
|
242
|
+
return_type = "()"
|
|
243
|
+
ret_node = node.child_by_field_name("return_type")
|
|
244
|
+
if ret_node:
|
|
245
|
+
return_type = self._get_node_text(ret_node)
|
|
246
|
+
# Remove "->" prefix if captured
|
|
247
|
+
if return_type.startswith("->"):
|
|
248
|
+
return_type = return_type[2:].strip()
|
|
249
|
+
|
|
250
|
+
# Visibility
|
|
251
|
+
visibility = self._extract_visibility(node)
|
|
252
|
+
|
|
253
|
+
# Async - check function_modifiers node for async keyword
|
|
254
|
+
is_async = False
|
|
255
|
+
for child in node.children:
|
|
256
|
+
if child.type == "function_modifiers":
|
|
257
|
+
# Check if async is in the modifiers
|
|
258
|
+
for modifier in child.children:
|
|
259
|
+
if modifier.type == "async":
|
|
260
|
+
is_async = True
|
|
261
|
+
break
|
|
262
|
+
if is_async:
|
|
263
|
+
break
|
|
264
|
+
# Also check for direct async child (older tree-sitter versions)
|
|
265
|
+
elif child.type == "async":
|
|
266
|
+
is_async = True
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
# Docstring
|
|
270
|
+
docstring = self._extract_docstring(node)
|
|
271
|
+
|
|
272
|
+
# Raw text
|
|
273
|
+
raw_text = self._get_node_text(node)
|
|
274
|
+
|
|
275
|
+
# Add to Function object
|
|
276
|
+
# Note: We're using dynamic attributes for Rust-specific fields
|
|
277
|
+
func = Function(
|
|
278
|
+
name=name,
|
|
279
|
+
start_line=start_line,
|
|
280
|
+
end_line=end_line,
|
|
281
|
+
raw_text=raw_text,
|
|
282
|
+
language="rust",
|
|
283
|
+
parameters=parameters,
|
|
284
|
+
return_type=return_type,
|
|
285
|
+
visibility=visibility,
|
|
286
|
+
docstring=docstring,
|
|
287
|
+
)
|
|
288
|
+
# Attach Rust-specific attributes
|
|
289
|
+
func.is_async = is_async
|
|
290
|
+
|
|
291
|
+
return func
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
log_error(f"Error extracting Rust function: {e}")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def _extract_struct(self, node: "tree_sitter.Node") -> Class | None:
|
|
298
|
+
"""Extract struct information"""
|
|
299
|
+
return self._extract_type_def(node, "struct")
|
|
300
|
+
|
|
301
|
+
def _extract_enum(self, node: "tree_sitter.Node") -> Class | None:
|
|
302
|
+
"""Extract enum information"""
|
|
303
|
+
return self._extract_type_def(node, "enum")
|
|
304
|
+
|
|
305
|
+
def _extract_trait(self, node: "tree_sitter.Node") -> Class | None:
|
|
306
|
+
"""Extract trait information"""
|
|
307
|
+
return self._extract_type_def(node, "trait")
|
|
308
|
+
|
|
309
|
+
def _extract_type_def(
|
|
310
|
+
self, node: "tree_sitter.Node", type_name: str
|
|
311
|
+
) -> Class | None:
|
|
312
|
+
"""Generic type definition extractor"""
|
|
313
|
+
try:
|
|
314
|
+
name_node = node.child_by_field_name("name")
|
|
315
|
+
if not name_node:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
name = self._get_node_text(name_node)
|
|
319
|
+
start_line = node.start_point[0] + 1
|
|
320
|
+
end_line = node.end_point[0] + 1
|
|
321
|
+
visibility = self._extract_visibility(node)
|
|
322
|
+
|
|
323
|
+
raw_text = self._get_node_text(node)
|
|
324
|
+
|
|
325
|
+
cls = Class(
|
|
326
|
+
name=name,
|
|
327
|
+
start_line=start_line,
|
|
328
|
+
end_line=end_line,
|
|
329
|
+
raw_text=raw_text,
|
|
330
|
+
language="rust",
|
|
331
|
+
class_type=type_name,
|
|
332
|
+
visibility=visibility,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Extract implemented traits (for structs/enums) or supertraits (for traits)
|
|
336
|
+
# This is complex in Rust as impls are separate.
|
|
337
|
+
# We can scan for derive macros here.
|
|
338
|
+
derives = self._extract_derives(node)
|
|
339
|
+
if derives:
|
|
340
|
+
# Add derives to implemented interfaces list for display
|
|
341
|
+
cls.implements_interfaces = derives
|
|
342
|
+
|
|
343
|
+
return cls
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
log_error(f"Error extracting Rust {type_name}: {e}")
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def _extract_impl(self, node: "tree_sitter.Node") -> None:
|
|
350
|
+
"""Extract impl block information"""
|
|
351
|
+
try:
|
|
352
|
+
trait_node = node.child_by_field_name("trait")
|
|
353
|
+
type_node = node.child_by_field_name("type")
|
|
354
|
+
|
|
355
|
+
trait_name = self._get_node_text(trait_node) if trait_node else None
|
|
356
|
+
type_name = self._get_node_text(type_node) if type_node else None
|
|
357
|
+
|
|
358
|
+
if type_name:
|
|
359
|
+
self.impl_blocks.append(
|
|
360
|
+
{
|
|
361
|
+
"type": type_name,
|
|
362
|
+
"trait": trait_name,
|
|
363
|
+
"line_range": {
|
|
364
|
+
"start": node.start_point[0] + 1,
|
|
365
|
+
"end": node.end_point[0] + 1,
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
log_error(f"Error extracting impl block: {e}")
|
|
371
|
+
|
|
372
|
+
def _extract_field(self, node: "tree_sitter.Node") -> Variable | None:
|
|
373
|
+
"""Extract struct field"""
|
|
374
|
+
try:
|
|
375
|
+
name_node = node.child_by_field_name("name")
|
|
376
|
+
type_node = node.child_by_field_name("type")
|
|
377
|
+
|
|
378
|
+
if not name_node or not type_node:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
name = self._get_node_text(name_node)
|
|
382
|
+
field_type = self._get_node_text(type_node)
|
|
383
|
+
start_line = node.start_point[0] + 1
|
|
384
|
+
end_line = node.end_point[0] + 1
|
|
385
|
+
visibility = self._extract_visibility(node)
|
|
386
|
+
|
|
387
|
+
raw_text = self._get_node_text(node)
|
|
388
|
+
docstring = self._extract_docstring(node)
|
|
389
|
+
|
|
390
|
+
return Variable(
|
|
391
|
+
name=name,
|
|
392
|
+
start_line=start_line,
|
|
393
|
+
end_line=end_line,
|
|
394
|
+
raw_text=raw_text,
|
|
395
|
+
language="rust",
|
|
396
|
+
variable_type=field_type,
|
|
397
|
+
visibility=visibility,
|
|
398
|
+
docstring=docstring,
|
|
399
|
+
)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
log_error(f"Error extracting Rust field: {e}")
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
def _extract_visibility(self, node: "tree_sitter.Node") -> str:
|
|
405
|
+
"""Extract visibility modifier"""
|
|
406
|
+
for child in node.children:
|
|
407
|
+
if child.type == "visibility_modifier":
|
|
408
|
+
return self._get_node_text(child)
|
|
409
|
+
return "private" # Default in Rust
|
|
410
|
+
|
|
411
|
+
def _extract_docstring(self, node: "tree_sitter.Node") -> str | None:
|
|
412
|
+
"""Extract doc comments (/// or /** ... */)"""
|
|
413
|
+
# In tree-sitter-rust, doc comments are often 'line_comment' or 'block_comment'
|
|
414
|
+
# preceding the item, or attributes.
|
|
415
|
+
# But often they are just comments attached to the node if we look at previous siblings
|
|
416
|
+
# or they are part of the node as 'outer attributes' which are 'attribute_item'
|
|
417
|
+
|
|
418
|
+
docs = []
|
|
419
|
+
# Look for attribute items that are doc comments
|
|
420
|
+
# This simple implementation might need refinement based on actual tree structure
|
|
421
|
+
for child in node.children:
|
|
422
|
+
if child.type == "line_comment" and self._get_node_text(child).startswith(
|
|
423
|
+
"///"
|
|
424
|
+
):
|
|
425
|
+
docs.append(self._get_node_text(child)[3:].strip())
|
|
426
|
+
elif child.type == "block_comment" and self._get_node_text(
|
|
427
|
+
child
|
|
428
|
+
).startswith("/**"):
|
|
429
|
+
content = self._get_node_text(child)
|
|
430
|
+
# Strip /** and */
|
|
431
|
+
if content.startswith("/**") and content.endswith("*/"):
|
|
432
|
+
docs.append(content[3:-2].strip())
|
|
433
|
+
|
|
434
|
+
if docs:
|
|
435
|
+
return "\n".join(docs)
|
|
436
|
+
|
|
437
|
+
# Fallback: check source lines before start_line (similar to Java)
|
|
438
|
+
start_line = node.start_point[0]
|
|
439
|
+
if start_line > 0:
|
|
440
|
+
# Check previous lines
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
def _extract_derives(self, node: "tree_sitter.Node") -> list[str]:
|
|
446
|
+
"""Extract derived traits from attributes"""
|
|
447
|
+
derives = []
|
|
448
|
+
for child in node.children:
|
|
449
|
+
if child.type == "attribute_item":
|
|
450
|
+
text = self._get_node_text(child)
|
|
451
|
+
if "derive" in text:
|
|
452
|
+
# Naive parsing of #[derive(Debug, Clone)]
|
|
453
|
+
match = re.search(r"derive\((.*?)\)", text)
|
|
454
|
+
if match:
|
|
455
|
+
traits = match.group(1).split(",")
|
|
456
|
+
derives.extend([t.strip() for t in traits if t.strip()])
|
|
457
|
+
return derives
|
|
458
|
+
|
|
459
|
+
def _get_node_text(self, node: "tree_sitter.Node") -> str:
|
|
460
|
+
"""Get node text with caching"""
|
|
461
|
+
node_id = id(node)
|
|
462
|
+
if node_id in self._node_text_cache:
|
|
463
|
+
return self._node_text_cache[node_id]
|
|
464
|
+
|
|
465
|
+
try:
|
|
466
|
+
start_byte = node.start_byte
|
|
467
|
+
end_byte = node.end_byte
|
|
468
|
+
encoding = "utf-8" # Default
|
|
469
|
+
content_bytes = safe_encode("\n".join(self.content_lines), encoding)
|
|
470
|
+
text = extract_text_slice(content_bytes, start_byte, end_byte, encoding)
|
|
471
|
+
self._node_text_cache[node_id] = text
|
|
472
|
+
return text
|
|
473
|
+
except Exception:
|
|
474
|
+
return ""
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class RustPlugin(LanguagePlugin):
|
|
478
|
+
"""Rust language plugin implementation"""
|
|
479
|
+
|
|
480
|
+
def __init__(self) -> None:
|
|
481
|
+
"""Initialize the Rust language plugin."""
|
|
482
|
+
super().__init__()
|
|
483
|
+
self.extractor = RustElementExtractor()
|
|
484
|
+
self.language = "rust"
|
|
485
|
+
self.supported_extensions = self.get_file_extensions()
|
|
486
|
+
self._cached_language: Any | None = None
|
|
487
|
+
|
|
488
|
+
def get_language_name(self) -> str:
|
|
489
|
+
"""Get the language name."""
|
|
490
|
+
return "rust"
|
|
491
|
+
|
|
492
|
+
def get_file_extensions(self) -> list[str]:
|
|
493
|
+
"""Get supported file extensions."""
|
|
494
|
+
return [".rs"]
|
|
495
|
+
|
|
496
|
+
def create_extractor(self) -> ElementExtractor:
|
|
497
|
+
"""Create a new element extractor instance."""
|
|
498
|
+
return RustElementExtractor()
|
|
499
|
+
|
|
500
|
+
async def analyze_file(
|
|
501
|
+
self, file_path: str, request: "AnalysisRequest"
|
|
502
|
+
) -> "AnalysisResult":
|
|
503
|
+
"""Analyze Rust code and return structured results."""
|
|
504
|
+
|
|
505
|
+
from ..models import AnalysisResult
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
from ..encoding_utils import read_file_safe
|
|
509
|
+
|
|
510
|
+
file_content, detected_encoding = read_file_safe(file_path)
|
|
511
|
+
|
|
512
|
+
# Get tree-sitter language and parse
|
|
513
|
+
language = self.get_tree_sitter_language()
|
|
514
|
+
if language is None:
|
|
515
|
+
return AnalysisResult(
|
|
516
|
+
file_path=file_path,
|
|
517
|
+
language="rust",
|
|
518
|
+
line_count=len(file_content.split("\n")),
|
|
519
|
+
elements=[],
|
|
520
|
+
source_code=file_content,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
import tree_sitter
|
|
524
|
+
|
|
525
|
+
parser = tree_sitter.Parser()
|
|
526
|
+
|
|
527
|
+
# Set language
|
|
528
|
+
if hasattr(parser, "set_language"):
|
|
529
|
+
parser.set_language(language)
|
|
530
|
+
elif hasattr(parser, "language"):
|
|
531
|
+
parser.language = language
|
|
532
|
+
else:
|
|
533
|
+
parser = tree_sitter.Parser(language)
|
|
534
|
+
|
|
535
|
+
tree = parser.parse(file_content.encode("utf-8"))
|
|
536
|
+
|
|
537
|
+
# Extract elements
|
|
538
|
+
elements_dict = self.extract_elements(tree, file_content)
|
|
539
|
+
|
|
540
|
+
all_elements = []
|
|
541
|
+
all_elements.extend(elements_dict.get("functions", []))
|
|
542
|
+
all_elements.extend(elements_dict.get("classes", []))
|
|
543
|
+
all_elements.extend(elements_dict.get("variables", []))
|
|
544
|
+
all_elements.extend(elements_dict.get("imports", []))
|
|
545
|
+
|
|
546
|
+
# Count nodes
|
|
547
|
+
node_count = (
|
|
548
|
+
self._count_tree_nodes(tree.root_node) if tree and tree.root_node else 0
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Pass extra Rust metadata (impls, modules) via result object or merged into elements
|
|
552
|
+
# For now, we rely on the standard AnalysisResult fields, but the formatter needs impls/modules.
|
|
553
|
+
# We can attach them to the AnalysisResult object dynamically or put them in elements list if they are CodeElements.
|
|
554
|
+
# Currently AnalysisResult.elements is list[CodeElement].
|
|
555
|
+
# We can't easily add dicts to elements list if they are not CodeElements.
|
|
556
|
+
# But the formatter receives `analysis_result: dict[str, Any]` which is `as_dict()` of AnalysisResult.
|
|
557
|
+
# We need to ensure `as_dict()` includes our extra data if we add it as attributes.
|
|
558
|
+
|
|
559
|
+
result = AnalysisResult(
|
|
560
|
+
file_path=file_path,
|
|
561
|
+
language="rust",
|
|
562
|
+
line_count=len(file_content.split("\n")),
|
|
563
|
+
elements=all_elements,
|
|
564
|
+
node_count=node_count,
|
|
565
|
+
source_code=file_content,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Attach extra metadata for the formatter
|
|
569
|
+
# Note: This requires AnalysisResult to handle arbitrary attributes or `as_dict` to include them.
|
|
570
|
+
# Looking at models.py (not visible here but assumed), standard attributes are fixed.
|
|
571
|
+
# However, `AnalysisResult` might allow dynamic attributes.
|
|
572
|
+
# Alternatively, we include them as custom elements in `elements` list.
|
|
573
|
+
|
|
574
|
+
# Let's check models.py later. For now, we'll attach them and hope for the best or modify models.py if needed.
|
|
575
|
+
result.modules = self.extractor.modules
|
|
576
|
+
result.impls = self.extractor.impl_blocks
|
|
577
|
+
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
except Exception as e:
|
|
581
|
+
log_error(f"Error analyzing Rust file {file_path}: {e}")
|
|
582
|
+
return AnalysisResult(
|
|
583
|
+
file_path=file_path,
|
|
584
|
+
language="rust",
|
|
585
|
+
line_count=0,
|
|
586
|
+
elements=[],
|
|
587
|
+
source_code="",
|
|
588
|
+
error_message=str(e),
|
|
589
|
+
success=False,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
def _count_tree_nodes(self, node: Any) -> int:
|
|
593
|
+
"""Recursively count nodes."""
|
|
594
|
+
if node is None:
|
|
595
|
+
return 0
|
|
596
|
+
count = 1
|
|
597
|
+
if hasattr(node, "children"):
|
|
598
|
+
for child in node.children:
|
|
599
|
+
count += self._count_tree_nodes(child)
|
|
600
|
+
return count
|
|
601
|
+
|
|
602
|
+
def get_tree_sitter_language(self) -> Any | None:
|
|
603
|
+
"""Get the tree-sitter language for Rust."""
|
|
604
|
+
if self._cached_language is not None:
|
|
605
|
+
return self._cached_language
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
import tree_sitter
|
|
609
|
+
import tree_sitter_rust
|
|
610
|
+
|
|
611
|
+
caps_or_lang = tree_sitter_rust.language()
|
|
612
|
+
|
|
613
|
+
if hasattr(caps_or_lang, "__class__") and "Language" in str(
|
|
614
|
+
type(caps_or_lang)
|
|
615
|
+
):
|
|
616
|
+
self._cached_language = caps_or_lang
|
|
617
|
+
else:
|
|
618
|
+
try:
|
|
619
|
+
self._cached_language = tree_sitter.Language(caps_or_lang)
|
|
620
|
+
except Exception as e:
|
|
621
|
+
log_error(f"Failed to create Language object: {e}")
|
|
622
|
+
return None
|
|
623
|
+
|
|
624
|
+
return self._cached_language
|
|
625
|
+
except ImportError as e:
|
|
626
|
+
log_error(f"tree-sitter-rust not available: {e}")
|
|
627
|
+
return None
|
|
628
|
+
except Exception as e:
|
|
629
|
+
log_error(f"Failed to load tree-sitter language for Rust: {e}")
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
def extract_elements(self, tree: Any | None, source_code: str) -> dict[str, Any]:
|
|
633
|
+
"""Extract all elements."""
|
|
634
|
+
if tree is None:
|
|
635
|
+
return {"functions": [], "classes": [], "variables": []}
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
# Reset extractor state
|
|
639
|
+
# We need to ensure we use the same extractor instance to collect side-effects like modules/impls
|
|
640
|
+
# But create_extractor() creates a new one.
|
|
641
|
+
# Here we use self.extractor which is initialized in __init__
|
|
642
|
+
# Wait, `analyze_file` calls `extract_elements` which calls `create_extractor` in Java plugin...
|
|
643
|
+
# In JavaPlugin.extract_elements: `extractor = self.create_extractor()`
|
|
644
|
+
# This means new extractor every time.
|
|
645
|
+
# So if we want to access `modules` and `impls` after extraction, we need to get them from THAT extractor instance.
|
|
646
|
+
|
|
647
|
+
extractor = (
|
|
648
|
+
self.create_extractor()
|
|
649
|
+
) # Create new instance for thread safety / isolation
|
|
650
|
+
|
|
651
|
+
result = {
|
|
652
|
+
"functions": extractor.extract_functions(tree, source_code),
|
|
653
|
+
"classes": extractor.extract_classes(tree, source_code),
|
|
654
|
+
"variables": extractor.extract_variables(tree, source_code),
|
|
655
|
+
"imports": extractor.extract_imports(tree, source_code),
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# Capture side-effects
|
|
659
|
+
if isinstance(extractor, RustElementExtractor):
|
|
660
|
+
self.extractor.modules = extractor.modules
|
|
661
|
+
self.extractor.impl_blocks = extractor.impl_blocks
|
|
662
|
+
|
|
663
|
+
return result
|
|
664
|
+
|
|
665
|
+
except Exception as e:
|
|
666
|
+
log_error(f"Error extracting elements: {e}")
|
|
667
|
+
return {"functions": [], "classes": [], "variables": []}
|
|
668
|
+
|
|
669
|
+
def supports_file(self, file_path: str) -> bool:
|
|
670
|
+
"""Check if this plugin supports the given file."""
|
|
671
|
+
return any(
|
|
672
|
+
file_path.lower().endswith(ext) for ext in self.get_file_extensions()
|
|
673
|
+
)
|