codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.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 (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
@@ -0,0 +1,537 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict, Optional, Tuple
3
+ from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger, debug_logger
4
+ from codegraphcontext.utils.tree_sitter_manager import execute_query
5
+
6
+ RUBY_QUERIES = {
7
+ "functions": """
8
+ (method
9
+ name: (identifier) @name
10
+ ) @function_node
11
+ """,
12
+ "classes": """
13
+ (class
14
+ name: (constant) @name
15
+ ) @class
16
+ """,
17
+ "modules": """
18
+ (module
19
+ name: (constant) @name
20
+ ) @module_node
21
+ """,
22
+ "imports": """
23
+ (call
24
+ method: (identifier) @method_name
25
+ arguments: (argument_list
26
+ (string) @path
27
+ )
28
+ ) @import
29
+ """,
30
+ "calls": """
31
+ (call
32
+ receiver: (_)? @receiver
33
+ method: (identifier) @name
34
+ arguments: (argument_list)? @args
35
+ ) @call_node
36
+ """,
37
+ "variables": """
38
+ (assignment
39
+ left: (identifier) @name
40
+ right: (_) @value
41
+ )
42
+ (assignment
43
+ left: (instance_variable) @name
44
+ right: (_) @value
45
+ )
46
+ """,
47
+ "comments": """
48
+ (comment) @comment
49
+ """,
50
+ "module_includes": """
51
+ (call
52
+ method: (identifier) @method
53
+ arguments: (argument_list (constant) @module)
54
+ ) @include_call
55
+ """,
56
+ }
57
+
58
+
59
+ class RubyTreeSitterParser:
60
+ """A Ruby-specific parser using tree-sitter."""
61
+
62
+ def __init__(self, generic_parser_wrapper: Any):
63
+ self.generic_parser_wrapper = generic_parser_wrapper
64
+ self.language_name = "ruby"
65
+ self.language = generic_parser_wrapper.language
66
+ self.parser = generic_parser_wrapper.parser
67
+
68
+ def _get_node_text(self, node: Any) -> str:
69
+ return node.text.decode("utf-8")
70
+
71
+ def _enclosing_class_name(self, node: Any) -> Optional[str]:
72
+ name, typ, _ = self._get_parent_context(node, ('class',))
73
+ return name
74
+
75
+ def _find_modules(self, root_node: Any) -> list[Dict[str, Any]]:
76
+ modules = []
77
+ query_str = RUBY_QUERIES["modules"]
78
+ # name via captures
79
+ captures = list(execute_query(self.language, query_str, root_node))
80
+ for node, cap in captures:
81
+ if cap == "module_node":
82
+ name = None
83
+ for n, c in captures:
84
+ if c == "name":
85
+ if n.start_byte >= node.start_byte and n.end_byte <= node.end_byte:
86
+ name = self._get_node_text(n)
87
+ break
88
+ if name:
89
+ module_data = {
90
+ "name": name,
91
+ "line_number": node.start_point[0] + 1,
92
+ "end_line": node.end_point[0] + 1,
93
+ "lang": self.language_name,
94
+ "is_dependency": False,
95
+ }
96
+ if self.index_source:
97
+ module_data["source"] = self._get_node_text(node)
98
+
99
+ modules.append(module_data)
100
+ return modules
101
+
102
+ def _find_module_inclusions(self, root_node: Any) -> list[Dict[str, Any]]:
103
+ includes = []
104
+ query_str = RUBY_QUERIES["module_includes"]
105
+ for node, cap in execute_query(self.language, query_str, root_node):
106
+ if cap == "method":
107
+ method_name = self._get_node_text(node)
108
+ if method_name != "include":
109
+ continue
110
+ if cap == "include_call":
111
+ method = None
112
+ module = None
113
+ for n, c in execute_query(self.language, query_str, node):
114
+ if c == "method":
115
+ method = self._get_node_text(n)
116
+ elif c == "module":
117
+ module = self._get_node_text(n)
118
+ if method == "include" and module:
119
+ cls = self._enclosing_class_name(node)
120
+ if cls:
121
+ includes.append({
122
+ "class": cls,
123
+ "module": module,
124
+ "line_number": node.start_point[0] + 1,
125
+ "lang": self.language_name,
126
+ "is_dependency": False,
127
+ })
128
+ return includes
129
+
130
+
131
+ def _get_parent_context(self, node: Any, types: Tuple[str, ...] = ('class', 'module', 'method')):
132
+ """Find parent context for Ruby constructs."""
133
+ curr = node.parent
134
+ while curr:
135
+ if curr.type in types:
136
+ name_node = curr.child_by_field_name('name')
137
+ if name_node:
138
+ return self._get_node_text(name_node), curr.type, curr.start_point[0] + 1
139
+ curr = curr.parent
140
+ return None, None, None
141
+
142
+ def _calculate_complexity(self, node: Any) -> int:
143
+ """Calculate cyclomatic complexity for Ruby constructs."""
144
+ complexity_nodes = {
145
+ "if", "unless", "case", "when", "while", "until", "for", "rescue", "ensure",
146
+ "and", "or", "&&", "||", "?", "ternary"
147
+ }
148
+ count = 1
149
+
150
+ def traverse(n):
151
+ nonlocal count
152
+ if n.type in complexity_nodes:
153
+ count += 1
154
+ for child in n.children:
155
+ traverse(child)
156
+
157
+ traverse(node)
158
+ return count
159
+
160
+ def _get_docstring(self, node: Any) -> Optional[str]:
161
+ """Extract comments as docstrings for Ruby constructs."""
162
+ # Look for comments before the node
163
+ prev_sibling = node.prev_sibling
164
+ while prev_sibling and prev_sibling.type in ('comment', '\n', ' '):
165
+ if prev_sibling.type == 'comment':
166
+ comment_text = self._get_node_text(prev_sibling)
167
+ if comment_text.startswith('#') and not comment_text.startswith('#!'):
168
+ return comment_text.strip()
169
+ prev_sibling = prev_sibling.prev_sibling
170
+ return None
171
+
172
+ def _parse_method_parameters(self, method_node: Any) -> list[str]:
173
+ """Parse method parameters from a method node."""
174
+ params = []
175
+ # Look for parameters in the method node
176
+ for child in method_node.children:
177
+ if child.type == 'identifier' and child != method_node.child_by_field_name('name'):
178
+ # This is likely a parameter
179
+ params.append(self._get_node_text(child))
180
+ return params
181
+
182
+ def parse(self, path: Path, is_dependency: bool = False, index_source: bool = False) -> Dict[str, Any]:
183
+ """Parses a Ruby file and returns its structure."""
184
+ self.index_source = index_source
185
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
186
+ source_code = f.read()
187
+
188
+ tree = self.parser.parse(bytes(source_code, "utf8"))
189
+ root_node = tree.root_node
190
+
191
+ functions = self._find_functions(root_node)
192
+ classes = self._find_classes(root_node)
193
+ imports = self._find_imports(root_node)
194
+ function_calls = self._find_calls(root_node)
195
+ variables = self._find_variables(root_node)
196
+ modules = self._find_modules(root_node)
197
+ module_inclusions = self._find_module_inclusions(root_node)
198
+
199
+ return {
200
+ "path": str(path),
201
+ "functions": functions,
202
+ "classes": classes,
203
+ "variables": variables,
204
+ "imports": imports,
205
+ "function_calls": function_calls,
206
+ "is_dependency": is_dependency,
207
+ "lang": self.language_name,
208
+ "modules": modules,
209
+ "module_inclusions": module_inclusions,
210
+ }
211
+
212
+ def _find_functions(self, root_node: Any) -> list[Dict[str, Any]]:
213
+ """Find all function/method definitions."""
214
+ functions = []
215
+ query_str = RUBY_QUERIES["functions"]
216
+
217
+ # Collect all captures first
218
+ all_captures = list(execute_query(self.language, query_str, root_node))
219
+
220
+ # Group captures by function node using a different approach
221
+ captures_by_function = {}
222
+ for node, capture_name in all_captures:
223
+ if capture_name == 'function_node':
224
+ captures_by_function[id(node)] = {'node': node, 'name': None}
225
+
226
+ # Now find names for each function
227
+ for node, capture_name in all_captures:
228
+ if capture_name == 'name':
229
+ # Find which function this name belongs to
230
+ for func_id, func_data in captures_by_function.items():
231
+ func_node = func_data['node']
232
+ # Check if this name node is within the function node
233
+ if (node.start_byte >= func_node.start_byte and
234
+ node.end_byte <= func_node.end_byte):
235
+ captures_by_function[func_id]['name'] = self._get_node_text(node)
236
+ break
237
+
238
+ # Build function entries
239
+ for func_data in captures_by_function.values():
240
+ func_node = func_data['node']
241
+ name = func_data['name']
242
+
243
+ if name:
244
+ args = self._parse_method_parameters(func_node)
245
+
246
+ # Get context and docstring
247
+ context, context_type, _ = self._get_parent_context(func_node)
248
+ class_context = context if context_type in ('class', 'module') else None
249
+ docstring = self._get_docstring(func_node)
250
+
251
+ func_data = {
252
+ "name": name,
253
+ "line_number": func_node.start_point[0] + 1,
254
+ "end_line": func_node.end_point[0] + 1,
255
+ "args": args,
256
+ "lang": self.language_name,
257
+ "is_dependency": False,
258
+ }
259
+ if self.index_source:
260
+ func_data["source"] = self._get_node_text(func_node)
261
+ func_data["docstring"] = docstring
262
+
263
+ functions.append(func_data)
264
+
265
+ return functions
266
+
267
+ def _find_classes(self, root_node: Any) -> list[Dict[str, Any]]:
268
+ """Find all class and module definitions."""
269
+ classes = []
270
+ query_str = RUBY_QUERIES["classes"]
271
+
272
+ # Collect all captures first
273
+ all_captures = list(execute_query(self.language, query_str, root_node))
274
+
275
+ # Group captures by class node using a different approach
276
+ captures_by_class = {}
277
+ for node, capture_name in all_captures:
278
+ if capture_name == 'class':
279
+ captures_by_class[id(node)] = {'node': node, 'name': None}
280
+
281
+ # Now find names for each class
282
+ for node, capture_name in all_captures:
283
+ if capture_name == 'name':
284
+ # Find which class this name belongs to
285
+ for class_id, class_data in captures_by_class.items():
286
+ class_node = class_data['node']
287
+ # Check if this name node is within the class node
288
+ if (node.start_byte >= class_node.start_byte and
289
+ node.end_byte <= class_node.end_byte):
290
+ captures_by_class[class_id]['name'] = self._get_node_text(node)
291
+ break
292
+
293
+ # Build class entries
294
+ for class_data in captures_by_class.values():
295
+ class_node = class_data['node']
296
+ name = class_data['name']
297
+
298
+ if name:
299
+ # Get superclass for inheritance (simplified)
300
+ bases = []
301
+
302
+ # Get context and docstring
303
+ context, context_type, _ = self._get_parent_context(class_node)
304
+ class_context = context if context_type in ('class', 'module') else None
305
+ docstring = self._get_docstring(class_node)
306
+
307
+ class_data = {
308
+ "name": name,
309
+ "line_number": class_node.start_point[0] + 1,
310
+ "end_line": class_node.end_point[0] + 1,
311
+ "bases": bases,
312
+ "context": context,
313
+ "context_type": context_type,
314
+ "class_context": class_context,
315
+ "decorators": [],
316
+ "lang": self.language_name,
317
+ "is_dependency": False,
318
+ }
319
+ if self.index_source:
320
+ class_data["source"] = self._get_node_text(class_node)
321
+ class_data["docstring"] = docstring
322
+
323
+ classes.append(class_data)
324
+
325
+ return classes
326
+
327
+ def _find_imports(self, root_node: Any) -> list[Dict[str, Any]]:
328
+ """Find all require/load statements."""
329
+ imports = []
330
+ query_str = RUBY_QUERIES["imports"]
331
+
332
+ # Collect all captures first
333
+ all_captures = list(execute_query(self.language, query_str, root_node))
334
+
335
+ # Group captures by import node using a different approach
336
+ captures_by_import = {}
337
+ for node, capture_name in all_captures:
338
+ if capture_name == 'import':
339
+ captures_by_import[id(node)] = {'node': node, 'method_name': None, 'path': None}
340
+
341
+ # Now find method names and paths for each import
342
+ for node, capture_name in all_captures:
343
+ if capture_name == 'method_name':
344
+ # Find which import this method name belongs to
345
+ for import_id, import_data in captures_by_import.items():
346
+ import_node = import_data['node']
347
+ # Check if this method name node is within the import node
348
+ if (node.start_byte >= import_node.start_byte and
349
+ node.end_byte <= import_node.end_byte):
350
+ captures_by_import[import_id]['method_name'] = self._get_node_text(node)
351
+ break
352
+ elif capture_name == 'path':
353
+ # Find which import this path belongs to
354
+ for import_id, import_data in captures_by_import.items():
355
+ import_node = import_data['node']
356
+ # Check if this path node is within the import node
357
+ if (node.start_byte >= import_node.start_byte and
358
+ node.end_byte <= import_node.end_byte):
359
+ captures_by_import[import_id]['path'] = self._get_node_text(node)
360
+ break
361
+
362
+ # Build import entries
363
+ for import_data in captures_by_import.values():
364
+ import_node = import_data['node']
365
+ method_name = import_data['method_name']
366
+ path = import_data['path']
367
+
368
+ if method_name and path:
369
+ path = path.strip('\'"')
370
+
371
+ # Only process require/load statements
372
+ if method_name in ('require', 'require_relative', 'load'):
373
+ imports.append({
374
+ "name": path,
375
+ "full_import_name": f"{method_name} '{path}'",
376
+ "line_number": import_node.start_point[0] + 1,
377
+ "alias": None,
378
+ "lang": self.language_name,
379
+ "is_dependency": False,
380
+ })
381
+
382
+ return imports
383
+
384
+ def _find_calls(self, root_node: Any) -> list[Dict[str, Any]]:
385
+ """Find all function and method calls."""
386
+ calls = []
387
+ query_str = RUBY_QUERIES["calls"]
388
+
389
+ # Collect all captures
390
+ all_captures = list(execute_query(self.language, query_str, root_node))
391
+
392
+ # Group by call node
393
+ captures_by_call = {}
394
+ for node, capture_name in all_captures:
395
+ if capture_name == 'call_node':
396
+ captures_by_call[id(node)] = {'node': node, 'name': None, 'receiver': None, 'args': []}
397
+
398
+ for node, capture_name in all_captures:
399
+ for call_id, call_data in captures_by_call.items():
400
+ call_node = call_data['node']
401
+ if not (node.start_byte >= call_node.start_byte and node.end_byte <= call_node.end_byte):
402
+ continue
403
+
404
+ if capture_name == 'name':
405
+ # The identifier could be part of receiver or arguments too, be careful
406
+ # But tree-sitter structure ensures method name is distinct
407
+ # Check if node is child 'method' of call_node
408
+ if node == call_node.child_by_field_name('method'):
409
+ captures_by_call[call_id]['name'] = self._get_node_text(node)
410
+
411
+ elif capture_name == 'receiver':
412
+ captures_by_call[call_id]['receiver'] = self._get_node_text(node)
413
+
414
+ elif capture_name == 'args':
415
+ # Capture arguments
416
+ args_text = self._get_node_text(node)
417
+ # Simple heuristic: split by comma
418
+ captures_by_call[call_id]['args'] = [a.strip() for a in args_text.strip("()").split(',') if a.strip()]
419
+
420
+ for call_data in captures_by_call.values():
421
+ call_node = call_data['node']
422
+ name = call_data['name']
423
+
424
+ if name:
425
+ receiver = call_data['receiver']
426
+ full_name = f"{receiver}.{name}" if receiver else name
427
+
428
+ context_name, context_type, context_line = self._get_parent_context(call_node)
429
+ class_context = context_name if context_type in ('class', 'module') else None
430
+ if context_type == 'method':
431
+ # If inside a method, try to find enclosing class too
432
+ enclosing_class, _, _ = self._get_parent_context(call_node.parent, ('class', 'module'))
433
+ class_context = enclosing_class
434
+
435
+
436
+ calls.append({
437
+ "name": name,
438
+ "full_name": full_name,
439
+ "line_number": call_node.start_point[0] + 1,
440
+ "args": call_data['args'],
441
+ "inferred_obj_type": None,
442
+ "context": (context_name, context_type, context_line),
443
+ "class_context": class_context,
444
+ "lang": self.language_name,
445
+ "is_dependency": False,
446
+ })
447
+
448
+ return calls
449
+
450
+ def _find_variables(self, root_node: Any) -> list[Dict[str, Any]]:
451
+ """Find all variable assignments."""
452
+ variables = []
453
+ query_str = RUBY_QUERIES["variables"]
454
+
455
+ # Group captures by assignment node
456
+ captures_by_assignment = {}
457
+ for node, capture_name in execute_query(self.language, query_str, root_node):
458
+ if capture_name == 'name':
459
+ # Find the parent assignment node
460
+ current = node.parent
461
+ while current and current.type != 'assignment':
462
+ current = current.parent
463
+ if current:
464
+ assignment_id = id(current)
465
+ if assignment_id not in captures_by_assignment:
466
+ captures_by_assignment[assignment_id] = {'node': current, 'name': None, 'value': None}
467
+ captures_by_assignment[assignment_id]['name'] = self._get_node_text(node)
468
+ elif capture_name == 'value':
469
+ # Find the parent assignment node
470
+ current = node.parent
471
+ while current and current.type != 'assignment':
472
+ current = current.parent
473
+ if current:
474
+ assignment_id = id(current)
475
+ if assignment_id not in captures_by_assignment:
476
+ captures_by_assignment[assignment_id] = {'node': current, 'name': None, 'value': None}
477
+ captures_by_assignment[assignment_id]['value'] = self._get_node_text(node)
478
+
479
+ # Build variable entries
480
+ for var_data in captures_by_assignment.values():
481
+ name = var_data['name']
482
+ value = var_data['value']
483
+
484
+ if name:
485
+ # Determine variable type based on name prefix
486
+ var_type = "local"
487
+ if name.startswith("@"):
488
+ var_type = "instance"
489
+ elif name.startswith("@@"):
490
+ var_type = "class"
491
+ elif name.startswith("$"):
492
+ var_type = "global"
493
+
494
+ variables.append({
495
+ "name": name,
496
+ "line_number": var_data['node'].start_point[0] + 1,
497
+ "value": value,
498
+ "type": var_type,
499
+ "context": None, # Placeholder
500
+ "class_context": None, # Placeholder
501
+ "lang": self.language_name,
502
+ "is_dependency": False,
503
+ })
504
+
505
+ return variables
506
+
507
+
508
+ def pre_scan_ruby(files: list[Path], parser_wrapper) -> dict:
509
+ """Scans Ruby files to create a map of class/method names to their file paths."""
510
+ imports_map = {}
511
+ query_str = """
512
+ (class
513
+ name: (constant) @name
514
+ )
515
+ (module
516
+ name: (constant) @name
517
+ )
518
+ (method
519
+ name: (identifier) @name
520
+ )
521
+ """
522
+
523
+
524
+ for path in files:
525
+ try:
526
+ with open(path, "r", encoding="utf-8") as f:
527
+ tree = parser_wrapper.parser.parse(bytes(f.read(), "utf8"))
528
+
529
+ for capture, _ in execute_query(parser_wrapper.language, query_str, tree.root_node):
530
+ name = capture.text.decode('utf-8')
531
+ if name not in imports_map:
532
+ imports_map[name] = []
533
+ imports_map[name].append(str(path.resolve()))
534
+ except Exception as e:
535
+ warning_logger(f"Tree-sitter pre-scan failed for {path}: {e}")
536
+
537
+ return imports_map