tree-sitter-analyzer 1.9.17.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
@@ -0,0 +1,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
+ )