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,757 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ruby Language Plugin
4
+
5
+ Provides Ruby-specific parsing and element extraction functionality.
6
+ Supports extraction of classes, modules, methods, constants, variables, and require statements.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, Any, Optional
10
+
11
+ if TYPE_CHECKING:
12
+ import tree_sitter
13
+
14
+ from ..core.analysis_engine import AnalysisRequest
15
+ from ..models import AnalysisResult
16
+
17
+ try:
18
+ import tree_sitter
19
+
20
+ TREE_SITTER_AVAILABLE = True
21
+ except ImportError:
22
+ TREE_SITTER_AVAILABLE = False
23
+
24
+ from ..models import Class, Function, Import, Variable
25
+ from ..plugins.base import ElementExtractor, LanguagePlugin
26
+ from ..utils import log_error
27
+
28
+
29
+ class RubyElementExtractor(ElementExtractor):
30
+ """
31
+ Ruby-specific element extractor.
32
+
33
+ This extractor parses Ruby AST and extracts code elements, mapping them
34
+ to the unified element model:
35
+ - Classes, Modules → Class elements
36
+ - Methods (instance and class methods) → Function elements
37
+ - Constants, Instance variables, Class variables → Variable elements
38
+ - Require statements → Import elements
39
+
40
+ The extractor handles modern Ruby syntax including:
41
+ - Ruby 3+ features
42
+ - Blocks, procs, and lambdas
43
+ - attr_accessor, attr_reader, attr_writer
44
+ - Symbols and string interpolation
45
+ """
46
+
47
+ def __init__(self) -> None:
48
+ """
49
+ Initialize the Ruby element extractor.
50
+
51
+ Sets up internal state for source code processing and performance
52
+ optimization caches for node text extraction.
53
+ """
54
+ super().__init__()
55
+ self.source_code: str = ""
56
+ self.content_lines: list[str] = []
57
+ self.current_module: str = ""
58
+
59
+ # Performance optimization caches
60
+ self._node_text_cache: dict[int, str] = {}
61
+ self._processed_nodes: set[int] = set()
62
+ self._element_cache: dict[tuple[int, str], Any] = {}
63
+ self._file_encoding: str | None = None
64
+
65
+ def _reset_caches(self) -> None:
66
+ """Reset all internal caches for a new file analysis."""
67
+ self._node_text_cache.clear()
68
+ self._processed_nodes.clear()
69
+ self._element_cache.clear()
70
+ self.current_module = ""
71
+
72
+ def _get_node_text_optimized(self, node: "tree_sitter.Node") -> str:
73
+ """
74
+ Get text content of a node with caching for performance.
75
+
76
+ Args:
77
+ node: Tree-sitter node to extract text from
78
+
79
+ Returns:
80
+ Text content of the node as string
81
+ """
82
+ # Use node position as cache key instead of object id for deterministic behavior
83
+ cache_key = (node.start_byte, node.end_byte)
84
+ if cache_key in self._node_text_cache:
85
+ return self._node_text_cache[cache_key]
86
+
87
+ # Extract text directly from source code string
88
+ text = self.source_code[node.start_byte : node.end_byte]
89
+ self._node_text_cache[cache_key] = text
90
+ return text
91
+
92
+ def _determine_visibility(self, node: "tree_sitter.Node") -> str:
93
+ """
94
+ Determine visibility of a method.
95
+
96
+ In Ruby, visibility is typically set using private, protected, public keywords.
97
+ Default is public for methods.
98
+
99
+ Args:
100
+ node: Method node
101
+
102
+ Returns:
103
+ Visibility string ("public", "private", "protected")
104
+ """
105
+ # TODO: Implement visibility detection by looking for visibility modifiers
106
+ # For now, default to public
107
+ return "public"
108
+
109
+ def extract_classes(
110
+ self, tree: "tree_sitter.Tree", source_code: str
111
+ ) -> list[Class]:
112
+ """
113
+ Extract Ruby classes and modules.
114
+
115
+ Args:
116
+ tree: Parsed tree-sitter tree
117
+ source_code: Source code string
118
+
119
+ Returns:
120
+ List of Class elements
121
+ """
122
+ self.source_code = source_code
123
+ self.content_lines = source_code.splitlines()
124
+ self._reset_caches()
125
+
126
+ classes: list[Class] = []
127
+
128
+ # Iterative traversal to avoid stack overflow
129
+ stack: list[tree_sitter.Node] = [tree.root_node]
130
+
131
+ while stack:
132
+ node = stack.pop()
133
+
134
+ if node.type in ("class", "module"):
135
+ class_elem = self._extract_class_element(node)
136
+ if class_elem:
137
+ classes.append(class_elem)
138
+
139
+ # Add children to stack for traversal
140
+ for child in reversed(node.children):
141
+ stack.append(child)
142
+
143
+ return classes
144
+
145
+ def _extract_class_element(self, node: "tree_sitter.Node") -> Class | None:
146
+ """
147
+ Extract a single class or module element.
148
+
149
+ Args:
150
+ node: Class/module node
151
+
152
+ Returns:
153
+ Class element or None if extraction fails
154
+ """
155
+ try:
156
+ # Extract name
157
+ name_node = None
158
+ for child in node.children:
159
+ if child.type in ("constant", "scope_resolution"):
160
+ name_node = child
161
+ break
162
+
163
+ if not name_node:
164
+ return None
165
+
166
+ name = self._get_node_text_optimized(name_node)
167
+ is_module = node.type == "module"
168
+
169
+ # Extract superclass for classes
170
+ base_classes: list[str] = []
171
+ for child in node.children:
172
+ if child.type == "superclass":
173
+ superclass_node = child.children[0] if child.children else None
174
+ if superclass_node:
175
+ base_classes.append(
176
+ self._get_node_text_optimized(superclass_node)
177
+ )
178
+
179
+ return Class(
180
+ name=name,
181
+ start_line=node.start_point[0] + 1,
182
+ end_line=node.end_point[0] + 1,
183
+ visibility="public",
184
+ is_abstract=False,
185
+ full_qualified_name=name,
186
+ superclass=base_classes[0] if base_classes else None,
187
+ interfaces=[],
188
+ modifiers=[],
189
+ annotations=[],
190
+ class_type="module" if is_module else "class",
191
+ )
192
+ except Exception as e:
193
+ log_error(f"Error extracting class element: {e}")
194
+ return None
195
+
196
+ def extract_functions(
197
+ self, tree: "tree_sitter.Tree", source_code: str
198
+ ) -> list[Function]:
199
+ """
200
+ Extract Ruby methods.
201
+
202
+ Args:
203
+ tree: Parsed tree-sitter tree
204
+ source_code: Source code string
205
+
206
+ Returns:
207
+ List of Function elements
208
+ """
209
+ self.source_code = source_code
210
+ self.content_lines = source_code.splitlines()
211
+
212
+ functions: list[Function] = []
213
+
214
+ # Iterative traversal
215
+ stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
216
+
217
+ while stack:
218
+ node, parent_class = stack.pop()
219
+
220
+ if node.type == "method":
221
+ func_elem = self._extract_method_element(node, parent_class)
222
+ if func_elem:
223
+ functions.append(func_elem)
224
+ elif node.type == "singleton_method":
225
+ func_elem = self._extract_singleton_method_element(node, parent_class)
226
+ if func_elem:
227
+ functions.append(func_elem)
228
+ elif node.type == "call":
229
+ # Check for attr_accessor, attr_reader, attr_writer
230
+ func_elems = self._extract_attr_methods(node, parent_class)
231
+ functions.extend(func_elems)
232
+
233
+ # Track parent class for methods
234
+ new_parent = parent_class
235
+ if node.type in ("class", "module"):
236
+ for child in node.children:
237
+ if child.type in ("constant", "scope_resolution"):
238
+ new_parent = self._get_node_text_optimized(child)
239
+ break
240
+
241
+ # Add children to stack
242
+ for child in reversed(node.children):
243
+ stack.append((child, new_parent))
244
+
245
+ return functions
246
+
247
+ def _extract_method_element(
248
+ self, node: "tree_sitter.Node", parent_class: str
249
+ ) -> Function | None:
250
+ """
251
+ Extract a method element.
252
+
253
+ Args:
254
+ node: Method node
255
+ parent_class: Name of the parent class
256
+
257
+ Returns:
258
+ Function element or None if extraction fails
259
+ """
260
+ try:
261
+ # Extract method name
262
+ name_node = node.child_by_field_name("name")
263
+ if not name_node:
264
+ return None
265
+
266
+ name = self._get_node_text_optimized(name_node)
267
+ visibility = self._determine_visibility(node)
268
+
269
+ # Extract parameters
270
+ parameters: list[str] = []
271
+ params_node = node.child_by_field_name("parameters")
272
+ if params_node:
273
+ for param in params_node.children:
274
+ if param.type in (
275
+ "identifier",
276
+ "optional_parameter",
277
+ "splat_parameter",
278
+ "hash_splat_parameter",
279
+ "block_parameter",
280
+ ):
281
+ param_text = self._get_node_text_optimized(param)
282
+ parameters.append(param_text)
283
+
284
+ return Function(
285
+ name=f"{parent_class}#{name}" if parent_class else name,
286
+ start_line=node.start_point[0] + 1,
287
+ end_line=node.end_point[0] + 1,
288
+ visibility=visibility,
289
+ is_static=False,
290
+ is_async=False,
291
+ is_abstract=False,
292
+ parameters=parameters,
293
+ return_type="",
294
+ modifiers=[],
295
+ annotations=[],
296
+ )
297
+ except Exception as e:
298
+ log_error(f"Error extracting method element: {e}")
299
+ return None
300
+
301
+ def _extract_singleton_method_element(
302
+ self, node: "tree_sitter.Node", parent_class: str
303
+ ) -> Function | None:
304
+ """
305
+ Extract a singleton (class) method element.
306
+
307
+ Args:
308
+ node: Singleton method node
309
+ parent_class: Name of the parent class
310
+
311
+ Returns:
312
+ Function element or None if extraction fails
313
+ """
314
+ try:
315
+ # Extract method name
316
+ name_node = node.child_by_field_name("name")
317
+ if not name_node:
318
+ return None
319
+
320
+ name = self._get_node_text_optimized(name_node)
321
+ visibility = self._determine_visibility(node)
322
+
323
+ # Extract parameters
324
+ parameters: list[str] = []
325
+ params_node = node.child_by_field_name("parameters")
326
+ if params_node:
327
+ for param in params_node.children:
328
+ if param.type in (
329
+ "identifier",
330
+ "optional_parameter",
331
+ "splat_parameter",
332
+ "hash_splat_parameter",
333
+ "block_parameter",
334
+ ):
335
+ param_text = self._get_node_text_optimized(param)
336
+ parameters.append(param_text)
337
+
338
+ return Function(
339
+ name=f"{parent_class}.{name}" if parent_class else name,
340
+ start_line=node.start_point[0] + 1,
341
+ end_line=node.end_point[0] + 1,
342
+ visibility=visibility,
343
+ is_static=True, # Singleton methods are class methods
344
+ is_async=False,
345
+ is_abstract=False,
346
+ parameters=parameters,
347
+ return_type="",
348
+ modifiers=[],
349
+ annotations=[],
350
+ )
351
+ except Exception as e:
352
+ log_error(f"Error extracting singleton method element: {e}")
353
+ return None
354
+
355
+ def _extract_attr_methods(
356
+ self, node: "tree_sitter.Node", parent_class: str
357
+ ) -> list[Function]:
358
+ """
359
+ Extract attr_accessor, attr_reader, attr_writer methods.
360
+
361
+ Args:
362
+ node: Call node
363
+ parent_class: Name of the parent class
364
+
365
+ Returns:
366
+ List of Function elements
367
+ """
368
+ functions: list[Function] = []
369
+
370
+ try:
371
+ # Check if this is an attr_* call
372
+ method_node = node.child_by_field_name("method")
373
+ if not method_node:
374
+ return functions
375
+
376
+ method_name = self._get_node_text_optimized(method_node)
377
+ if method_name not in ("attr_accessor", "attr_reader", "attr_writer"):
378
+ return functions
379
+
380
+ # Extract attribute names
381
+ args_node = node.child_by_field_name("arguments")
382
+ if not args_node:
383
+ return functions
384
+
385
+ for arg in args_node.children:
386
+ if arg.type == "simple_symbol":
387
+ attr_name = self._get_node_text_optimized(arg).lstrip(":")
388
+
389
+ # Determine read/write permissions
390
+ is_reader = method_name in ("attr_accessor", "attr_reader")
391
+ is_writer = method_name in ("attr_accessor", "attr_writer")
392
+
393
+ # metadata = { # Reserved for future use
394
+ # "parent_class": parent_class,
395
+ # "attr_type": method_name,
396
+ # "is_reader": is_reader,
397
+ # "is_writer": is_writer,
398
+ # }
399
+ _ = (is_reader, is_writer) # Mark as used
400
+
401
+ functions.append(
402
+ Function(
403
+ name=(
404
+ f"{parent_class}#{attr_name}"
405
+ if parent_class
406
+ else attr_name
407
+ ),
408
+ start_line=node.start_point[0] + 1,
409
+ end_line=node.end_point[0] + 1,
410
+ visibility="public",
411
+ is_static=False,
412
+ is_async=False,
413
+ is_abstract=False,
414
+ parameters=[],
415
+ return_type="",
416
+ modifiers=[],
417
+ annotations=[],
418
+ is_property=True,
419
+ )
420
+ )
421
+ except Exception as e:
422
+ log_error(f"Error extracting attr methods: {e}")
423
+
424
+ return functions
425
+
426
+ def extract_variables(
427
+ self, tree: "tree_sitter.Tree", source_code: str
428
+ ) -> list[Variable]:
429
+ """
430
+ Extract Ruby constants and variables.
431
+
432
+ Args:
433
+ tree: Parsed tree-sitter tree
434
+ source_code: Source code string
435
+
436
+ Returns:
437
+ List of Variable elements
438
+ """
439
+ self.source_code = source_code
440
+ self.content_lines = source_code.splitlines()
441
+
442
+ variables: list[Variable] = []
443
+
444
+ # Iterative traversal
445
+ stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
446
+
447
+ while stack:
448
+ node, parent_class = stack.pop()
449
+
450
+ if node.type == "assignment":
451
+ var_elem = self._extract_assignment_variable(node, parent_class)
452
+ if var_elem:
453
+ variables.append(var_elem)
454
+
455
+ # Track parent class
456
+ new_parent = parent_class
457
+ if node.type in ("class", "module"):
458
+ for child in node.children:
459
+ if child.type in ("constant", "scope_resolution"):
460
+ new_parent = self._get_node_text_optimized(child)
461
+ break
462
+
463
+ # Add children to stack
464
+ for child in reversed(node.children):
465
+ stack.append((child, new_parent))
466
+
467
+ return variables
468
+
469
+ def _extract_assignment_variable(
470
+ self, node: "tree_sitter.Node", parent_class: str
471
+ ) -> Variable | None:
472
+ """
473
+ Extract variable from assignment.
474
+
475
+ Args:
476
+ node: Assignment node
477
+ parent_class: Name of the parent class
478
+
479
+ Returns:
480
+ Variable element or None if extraction fails
481
+ """
482
+ try:
483
+ # Extract left side (variable name)
484
+ left_node = node.child_by_field_name("left")
485
+ if not left_node:
486
+ return None
487
+
488
+ var_text = self._get_node_text_optimized(left_node)
489
+
490
+ # Determine variable type
491
+ is_constant = left_node.type == "constant"
492
+ # is_instance_var = var_text.startswith("@") and not var_text.startswith("@@") # Reserved for future use
493
+ is_class_var = var_text.startswith("@@")
494
+ # is_global = var_text.startswith("$") # Reserved for future use
495
+
496
+ # Clean variable name
497
+ name = var_text.lstrip("@$")
498
+ full_name = (
499
+ f"{parent_class}::{name}" if parent_class and is_constant else name
500
+ )
501
+
502
+ return Variable(
503
+ name=full_name,
504
+ start_line=node.start_point[0] + 1,
505
+ end_line=node.end_point[0] + 1,
506
+ visibility="public" if is_constant else "private",
507
+ is_static=is_class_var or is_constant,
508
+ is_constant=is_constant,
509
+ is_final=is_constant,
510
+ variable_type="",
511
+ modifiers=[],
512
+ )
513
+ except Exception as e:
514
+ log_error(f"Error extracting variable: {e}")
515
+ return None
516
+
517
+ def extract_imports(
518
+ self, tree: "tree_sitter.Tree", source_code: str
519
+ ) -> list[Import]:
520
+ """
521
+ Extract Ruby require statements.
522
+
523
+ Args:
524
+ tree: Parsed tree-sitter tree
525
+ source_code: Source code string
526
+
527
+ Returns:
528
+ List of Import elements
529
+ """
530
+ self.source_code = source_code
531
+ self.content_lines = source_code.splitlines()
532
+
533
+ imports: list[Import] = []
534
+
535
+ # Iterative traversal
536
+ stack: list[tree_sitter.Node] = [tree.root_node]
537
+
538
+ while stack:
539
+ node = stack.pop()
540
+
541
+ if node.type == "call":
542
+ import_elem = self._extract_require_statement(node)
543
+ if import_elem:
544
+ imports.append(import_elem)
545
+
546
+ # Add children to stack
547
+ for child in reversed(node.children):
548
+ stack.append(child)
549
+
550
+ return imports
551
+
552
+ def _extract_require_statement(self, node: "tree_sitter.Node") -> Import | None:
553
+ """
554
+ Extract require statement.
555
+
556
+ Args:
557
+ node: Call node
558
+
559
+ Returns:
560
+ Import element or None if not a require statement
561
+ """
562
+ try:
563
+ # Check if this is a require call
564
+ method_node = node.child_by_field_name("method")
565
+ if not method_node:
566
+ return None
567
+
568
+ method_name = self._get_node_text_optimized(method_node)
569
+ if method_name not in ("require", "require_relative", "load"):
570
+ return None
571
+
572
+ # Extract required module name
573
+ args_node = node.child_by_field_name("arguments")
574
+ if not args_node or not args_node.children:
575
+ return None
576
+
577
+ # Get first argument (the module name)
578
+ first_arg = args_node.children[0]
579
+ if first_arg.type == "string":
580
+ # Extract string content
581
+ import_name = self._get_node_text_optimized(first_arg).strip("\"'")
582
+
583
+ return Import(
584
+ name=import_name,
585
+ start_line=node.start_point[0] + 1,
586
+ end_line=node.end_point[0] + 1,
587
+ alias=None,
588
+ is_wildcard=False,
589
+ )
590
+ except Exception as e:
591
+ log_error(f"Error extracting require statement: {e}")
592
+
593
+ return None
594
+
595
+
596
+ class RubyPlugin(LanguagePlugin):
597
+ """
598
+ Ruby language plugin.
599
+
600
+ Provides Ruby-specific parsing and element extraction using tree-sitter-ruby.
601
+ Supports modern Ruby features including Ruby 3+ syntax.
602
+ """
603
+
604
+ _language_instance: Optional["tree_sitter.Language"] = None
605
+
606
+ def get_language_name(self) -> str:
607
+ """
608
+ Get the name of the language.
609
+
610
+ Returns:
611
+ Language name string
612
+ """
613
+ return "ruby"
614
+
615
+ def get_file_extensions(self) -> list[str]:
616
+ """
617
+ Get supported file extensions.
618
+
619
+ Returns:
620
+ List of file extensions
621
+ """
622
+ return [".rb"]
623
+
624
+ def get_tree_sitter_language(self) -> "tree_sitter.Language":
625
+ """
626
+ Get the tree-sitter language instance for Ruby.
627
+
628
+ Returns:
629
+ tree-sitter Language instance
630
+
631
+ Raises:
632
+ ImportError: If tree-sitter-ruby is not installed
633
+ """
634
+ if not TREE_SITTER_AVAILABLE:
635
+ raise ImportError(
636
+ "tree-sitter is not installed. Install it with: pip install tree-sitter"
637
+ )
638
+
639
+ if RubyPlugin._language_instance is None:
640
+ try:
641
+ import tree_sitter_ruby
642
+
643
+ RubyPlugin._language_instance = tree_sitter.Language(
644
+ tree_sitter_ruby.language()
645
+ )
646
+ except ImportError as e:
647
+ raise ImportError(
648
+ "tree-sitter-ruby is not installed. Install it with: pip install tree-sitter-ruby"
649
+ ) from e
650
+
651
+ return RubyPlugin._language_instance
652
+
653
+ def create_extractor(self) -> ElementExtractor:
654
+ """
655
+ Create a Ruby element extractor.
656
+
657
+ Returns:
658
+ RubyElementExtractor instance
659
+ """
660
+ return RubyElementExtractor()
661
+
662
+ async def analyze_file(
663
+ self, file_path: str, request: "AnalysisRequest"
664
+ ) -> "AnalysisResult":
665
+ """
666
+ Analyze a Ruby file.
667
+
668
+ Args:
669
+ file_path: Path to the Ruby file
670
+ request: Analysis request configuration
671
+
672
+ Returns:
673
+ AnalysisResult containing extracted elements
674
+ """
675
+ from ..models import AnalysisResult
676
+
677
+ try:
678
+ # Load file content
679
+ content = await self._load_file_safe(file_path)
680
+
681
+ # Parse with tree-sitter
682
+ language = self.get_tree_sitter_language()
683
+ parser = tree_sitter.Parser(language)
684
+ tree = parser.parse(content.encode("utf-8"))
685
+
686
+ # Extract elements
687
+ extractor = self.create_extractor()
688
+ classes = extractor.extract_classes(tree, content)
689
+ functions = extractor.extract_functions(tree, content)
690
+ variables = extractor.extract_variables(tree, content)
691
+ imports = extractor.extract_imports(tree, content)
692
+
693
+ # Combine all elements
694
+ all_elements = classes + functions + variables + imports
695
+
696
+ return AnalysisResult(
697
+ language=self.get_language_name(),
698
+ file_path=file_path,
699
+ success=True,
700
+ elements=all_elements,
701
+ node_count=self._count_nodes(tree.root_node),
702
+ )
703
+ except Exception as e:
704
+ log_error(f"Error analyzing Ruby file {file_path}: {e}")
705
+ return AnalysisResult(
706
+ language=self.get_language_name(),
707
+ file_path=file_path,
708
+ success=False,
709
+ error_message=str(e),
710
+ elements=[],
711
+ node_count=0,
712
+ )
713
+
714
+ def _count_nodes(self, node: "tree_sitter.Node") -> int:
715
+ """
716
+ Count total nodes in the AST.
717
+
718
+ Args:
719
+ node: Root node to count from
720
+
721
+ Returns:
722
+ Total node count
723
+ """
724
+ count = 1
725
+ for child in node.children:
726
+ count += self._count_nodes(child)
727
+ return count
728
+
729
+ async def _load_file_safe(self, file_path: str) -> str:
730
+ """
731
+ Load file content with encoding detection.
732
+
733
+ Args:
734
+ file_path: Path to the file
735
+
736
+ Returns:
737
+ File content as string
738
+
739
+ Raises:
740
+ IOError: If file cannot be read
741
+ """
742
+ import chardet
743
+
744
+ try:
745
+ # Read file in binary mode
746
+ with open(file_path, "rb") as f:
747
+ raw_content = f.read()
748
+
749
+ # Detect encoding
750
+ detected = chardet.detect(raw_content)
751
+ encoding = detected.get("encoding", "utf-8")
752
+
753
+ # Decode with detected encoding
754
+ return raw_content.decode(encoding or "utf-8")
755
+ except Exception as e:
756
+ log_error(f"Error loading file {file_path}: {e}")
757
+ raise OSError(f"Failed to load file {file_path}: {e}") from e