codegraphcontext-rust 0.5.5__cp310-cp310-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.cp310-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,576 @@
1
+ import os
2
+ import tempfile
3
+ import nbformat
4
+ from nbconvert import PythonExporter
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional, Tuple
7
+ import ast
8
+ import logging
9
+ import warnings
10
+ from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger, debug_logger
11
+ from codegraphcontext.utils.tree_sitter_manager import execute_query
12
+
13
+ # Suppress verbose traitlets/nbconvert DEBUG logs
14
+ logging.getLogger('traitlets').setLevel(logging.WARNING)
15
+ logging.getLogger('nbconvert').setLevel(logging.WARNING)
16
+
17
+ # Suppress IPython UserWarning from nbconvert
18
+ warnings.filterwarnings('ignore', message='.*IPython is needed to transform IPython syntax.*')
19
+
20
+
21
+ PY_QUERIES = {
22
+ "imports": """
23
+ (import_statement name: (_) @import)
24
+ (import_from_statement) @from_import_stmt
25
+ """,
26
+ "classes": """
27
+ (class_definition
28
+ name: (identifier) @name
29
+ superclasses: (argument_list)? @superclasses
30
+ body: (block) @body)
31
+ """,
32
+ "functions": """
33
+ (function_definition
34
+ name: (identifier) @name
35
+ parameters: (parameters) @parameters
36
+ body: (block) @body
37
+ return_type: (_)? @return_type)
38
+ """,
39
+ "calls": """
40
+ (call
41
+ function: (identifier) @name)
42
+ (call
43
+ function: (attribute attribute: (identifier) @name) @full_call)
44
+ """,
45
+ "variables": """
46
+ (assignment
47
+ left: (identifier) @name)
48
+ """,
49
+ "lambda_assignments": """
50
+ (assignment
51
+ left: (identifier) @name
52
+ right: (lambda) @lambda_node)
53
+ """,
54
+ "docstrings": """
55
+ (expression_statement (string) @docstring)
56
+ """,
57
+ "dict_method_refs": """
58
+ (dictionary
59
+ (pair
60
+ key: (_) @key
61
+ value: (attribute) @method_ref))
62
+ """,
63
+ }
64
+
65
+ class PythonTreeSitterParser:
66
+ """A Python-specific parser using tree-sitter, encapsulating language-specific logic."""
67
+
68
+ def __init__(self, generic_parser_wrapper):
69
+ self.generic_parser_wrapper = generic_parser_wrapper
70
+ self.language_name = generic_parser_wrapper.language_name
71
+ self.language = generic_parser_wrapper.language
72
+ self.parser = generic_parser_wrapper.parser
73
+
74
+ def _get_node_text(self, node) -> str:
75
+ return node.text.decode('utf-8')
76
+
77
+ def _get_parent_context(self, node, types=('function_definition', 'class_definition')):
78
+ curr = node.parent
79
+ while curr:
80
+ if curr.type in types:
81
+ name_node = curr.child_by_field_name('name')
82
+ return self._get_node_text(name_node) if name_node else None, curr.type, curr.start_point[0] + 1
83
+ curr = curr.parent
84
+ return None, None, None
85
+
86
+ def _calculate_complexity(self, node):
87
+ complexity_nodes = {
88
+ "if_statement", "for_statement", "while_statement", "except_clause",
89
+ "with_statement", "boolean_operator", "list_comprehension",
90
+ "generator_expression", "case_clause"
91
+ }
92
+ count = 1
93
+
94
+ def traverse(n):
95
+ nonlocal count
96
+ if n.type in complexity_nodes:
97
+ count += 1
98
+ for child in n.children:
99
+ traverse(child)
100
+
101
+ traverse(node)
102
+ return count
103
+
104
+ def _get_docstring(self, body_node):
105
+ if body_node and body_node.child_count > 0:
106
+ first_child = body_node.children[0]
107
+ if first_child.type == 'expression_statement' and first_child.children[0].type == 'string':
108
+ try:
109
+ return ast.literal_eval(self._get_node_text(first_child.children[0]))
110
+ except (ValueError, SyntaxError):
111
+ return self._get_node_text(first_child.children[0])
112
+ return None
113
+
114
+ def parse(self, path: Path, is_dependency: bool = False, is_notebook: bool = False, index_source: bool = False) -> Dict:
115
+ """Parses a file and returns its structure in a standardized dictionary format."""
116
+ original_file_path = path
117
+ temp_py_file = None
118
+ source_code = None
119
+ self.index_source = index_source
120
+
121
+ try:
122
+ if is_notebook:
123
+ info_logger(f"Converting notebook {path} to temporary Python file.")
124
+ with open(path, 'r', encoding='utf-8') as f:
125
+ notebook_node = nbformat.read(f, as_version=4)
126
+
127
+ exporter = PythonExporter()
128
+ python_code, _ = exporter.from_notebook_node(notebook_node)
129
+
130
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', encoding='utf-8') as tf:
131
+ tf.write(python_code)
132
+ temp_py_file = Path(tf.name)
133
+
134
+ # The file to be parsed is now the temporary file
135
+ path = temp_py_file
136
+
137
+ with open(path, "r", encoding="utf-8") as f:
138
+ source_code = f.read()
139
+
140
+ tree = self.parser.parse(bytes(source_code, "utf8"))
141
+ root_node = tree.root_node
142
+
143
+ functions = self._find_functions(root_node)
144
+ functions.extend(self._find_lambda_assignments(root_node, index_source))
145
+ classes = self._find_classes(root_node)
146
+ imports = self._find_imports(root_node)
147
+ function_calls = self._find_calls(root_node)
148
+ variables = self._find_variables(root_node)
149
+
150
+ return {
151
+ "path": str(original_file_path), # Always return the original path
152
+ "functions": functions,
153
+ "classes": classes,
154
+ "variables": variables,
155
+ "imports": imports,
156
+ "function_calls": function_calls,
157
+ "is_dependency": is_dependency,
158
+ "lang": self.language_name,
159
+ }
160
+ except Exception as e:
161
+ error_logger(f"Failed to parse {original_file_path}: {e}")
162
+ return {"path": str(original_file_path), "error": str(e)}
163
+ finally:
164
+ if temp_py_file and temp_py_file.exists():
165
+ os.remove(temp_py_file)
166
+ info_logger(f"Removed temporary file: {temp_py_file}")
167
+
168
+ def _find_lambda_assignments(self, root_node, index_source: bool = False):
169
+ functions = []
170
+ query_str = PY_QUERIES.get('lambda_assignments')
171
+ if not query_str: return []
172
+
173
+ for match in execute_query(self.language, query_str, root_node):
174
+ capture_name = match[1]
175
+ node = match[0]
176
+
177
+ if capture_name == 'name':
178
+ assignment_node = node.parent
179
+ lambda_node = assignment_node.child_by_field_name('right')
180
+ name = self._get_node_text(node)
181
+ params_node = lambda_node.child_by_field_name('parameters')
182
+
183
+ context, context_type, _ = self._get_parent_context(assignment_node)
184
+ class_context, _, _ = self._get_parent_context(assignment_node, types=('class_definition',))
185
+
186
+ func_data = {
187
+ "name": name,
188
+ "line_number": node.start_point[0] + 1,
189
+ "end_line": assignment_node.end_point[0] + 1,
190
+ "args": [p for p in [self._get_node_text(p) for p in params_node.children if p.type == 'identifier'] if p] if params_node else [],
191
+ "cyclomatic_complexity": 1,
192
+ "context": context,
193
+ "context_type": context_type,
194
+ "class_context": class_context,
195
+ "decorators": [],
196
+ "lang": self.language_name,
197
+ "is_dependency": False,
198
+ }
199
+ if self.index_source:
200
+ func_data["source"] = self._get_node_text(assignment_node)
201
+ func_data["docstring"] = None
202
+
203
+ functions.append(func_data)
204
+ return functions
205
+
206
+ def _find_functions(self, root_node, index_source: bool = False):
207
+ functions = []
208
+ query_str = PY_QUERIES['functions']
209
+ for match in execute_query(self.language, query_str, root_node):
210
+ capture_name = match[1]
211
+ node = match[0]
212
+
213
+ if capture_name == 'name':
214
+ func_node = node.parent
215
+ name = self._get_node_text(node)
216
+ params_node = func_node.child_by_field_name('parameters')
217
+ body_node = func_node.child_by_field_name('body')
218
+
219
+ decorators = [self._get_node_text(child) for child in func_node.children if child.type == 'decorator']
220
+
221
+ context, context_type, _ = self._get_parent_context(func_node)
222
+ class_context, _, _ = self._get_parent_context(func_node, types=('class_definition',))
223
+
224
+ args = []
225
+ if params_node:
226
+ for p in params_node.children:
227
+ arg_text = None
228
+ if p.type == 'identifier':
229
+ # Simple parameter: def foo(x)
230
+ arg_text = self._get_node_text(p)
231
+ elif p.type == 'default_parameter':
232
+ # Parameter with default: def foo(x=5)
233
+ name_node = p.child_by_field_name('name')
234
+ if name_node:
235
+ arg_text = self._get_node_text(name_node)
236
+ elif p.type == 'typed_parameter':
237
+ # Typed parameter: def foo(x: int)
238
+ name_node = p.child_by_field_name('name')
239
+ if name_node:
240
+ arg_text = self._get_node_text(name_node)
241
+ elif p.type == 'typed_default_parameter':
242
+ # Typed parameter with default: def foo(x: int = 5) or def foo(x: str = typer.Argument(...))
243
+ name_node = p.child_by_field_name('name')
244
+ if name_node:
245
+ arg_text = self._get_node_text(name_node)
246
+ elif p.type == 'list_splat_pattern' or p.type == 'dictionary_splat_pattern':
247
+ # *args or **kwargs
248
+ arg_text = self._get_node_text(p)
249
+
250
+ if arg_text:
251
+ args.append(arg_text)
252
+
253
+ func_data = {
254
+ "name": name,
255
+ "line_number": node.start_point[0] + 1,
256
+ "end_line": func_node.end_point[0] + 1,
257
+ "args": args,
258
+ "cyclomatic_complexity": self._calculate_complexity(func_node),
259
+ "context": context,
260
+ "context_type": context_type,
261
+ "class_context": class_context,
262
+ "decorators": [d for d in decorators if d],
263
+ "lang": self.language_name,
264
+ "is_dependency": False,
265
+ }
266
+
267
+ if self.index_source:
268
+ func_data["source"] = self._get_node_text(func_node)
269
+ func_data["docstring"] = self._get_docstring(body_node)
270
+
271
+ functions.append(func_data)
272
+ return functions
273
+
274
+ def _find_classes(self, root_node, index_source: bool = False):
275
+ classes = []
276
+ query_str = PY_QUERIES['classes']
277
+ for match in execute_query(self.language, query_str, root_node):
278
+ capture_name = match[1]
279
+ node = match[0]
280
+
281
+ if capture_name == 'name':
282
+ class_node = node.parent
283
+ name = self._get_node_text(node)
284
+ body_node = class_node.child_by_field_name('body')
285
+ superclasses_node = class_node.child_by_field_name('superclasses')
286
+
287
+ bases = []
288
+ if superclasses_node:
289
+ bases = [self._get_node_text(child) for child in superclasses_node.children if child.type in ('identifier', 'attribute')]
290
+
291
+ decorators = [self._get_node_text(child) for child in class_node.children if child.type == 'decorator']
292
+
293
+ context, _, _ = self._get_parent_context(class_node)
294
+
295
+ class_data = {
296
+ "name": name,
297
+ "line_number": node.start_point[0] + 1,
298
+ "end_line": class_node.end_point[0] + 1,
299
+ "bases": [b for b in bases if b],
300
+ "context": context,
301
+ "decorators": [d for d in decorators if d],
302
+ "lang": self.language_name,
303
+ "is_dependency": False,
304
+ }
305
+ if self.index_source:
306
+ class_data["source"] = self._get_node_text(class_node)
307
+ class_data["docstring"] = self._get_docstring(body_node)
308
+
309
+ classes.append(class_data)
310
+ return classes
311
+
312
+ def _find_imports(self, root_node):
313
+ imports = []
314
+ seen_modules = set()
315
+ query_str = PY_QUERIES['imports']
316
+ for node, capture_name in execute_query(self.language, query_str, root_node):
317
+ if capture_name in ('import', 'from_import_stmt'):
318
+ # For 'import_statement'
319
+ if capture_name == 'import':
320
+ node_text = self._get_node_text(node)
321
+ alias = None
322
+ if ' as ' in node_text:
323
+ parts = node_text.split(' as ')
324
+ full_name = parts[0].strip()
325
+ alias = parts[1].strip()
326
+ else:
327
+ full_name = node_text.strip()
328
+
329
+ if full_name in seen_modules:
330
+ continue
331
+ seen_modules.add(full_name)
332
+
333
+ import_data = {
334
+ "name": full_name,
335
+ "full_import_name": full_name,
336
+ "line_number": node.start_point[0] + 1,
337
+ "alias": alias,
338
+ "context": self._get_parent_context(node)[:2],
339
+ "lang": self.language_name,
340
+ "is_dependency": False,
341
+ }
342
+ imports.append(import_data)
343
+ # For 'import_from_statement'
344
+ elif capture_name == 'from_import_stmt':
345
+ module_name_node = node.child_by_field_name('module_name')
346
+ if not module_name_node: continue
347
+
348
+ module_name = self._get_node_text(module_name_node)
349
+
350
+ # Handle 'from ... import ...'
351
+ import_list_node = node.child_by_field_name('name')
352
+ if import_list_node:
353
+ for child in import_list_node.children:
354
+ imported_name = None
355
+ alias = None
356
+ if child.type == 'aliased_import':
357
+ name_node = child.child_by_field_name('name')
358
+ alias_node = child.child_by_field_name('alias')
359
+ if name_node: imported_name = self._get_node_text(name_node)
360
+ if alias_node: alias = self._get_node_text(alias_node)
361
+ elif child.type == 'dotted_name' or child.type == 'identifier':
362
+ imported_name = self._get_node_text(child)
363
+
364
+ if imported_name:
365
+ full_import_name = f"{module_name}.{imported_name}"
366
+ if full_import_name in seen_modules:
367
+ continue
368
+ seen_modules.add(full_import_name)
369
+ imports.append({
370
+ "name": imported_name,
371
+ "full_import_name": full_import_name,
372
+ "line_number": child.start_point[0] + 1,
373
+ "alias": alias,
374
+ "context": self._get_parent_context(child)[:2],
375
+ "lang": self.language_name,
376
+ "is_dependency": False,
377
+ })
378
+
379
+ return imports
380
+
381
+ def _find_calls(self, root_node):
382
+ calls = []
383
+
384
+ # First, find all direct function calls
385
+ query_str = PY_QUERIES['calls']
386
+ for node, capture_name in execute_query(self.language, query_str, root_node):
387
+ if capture_name == 'name':
388
+ call_node = node.parent if node.parent.type == 'call' else node.parent.parent
389
+ full_call_node = call_node.child_by_field_name('function')
390
+
391
+ args = []
392
+ arguments_node = call_node.child_by_field_name('arguments')
393
+ if arguments_node:
394
+ for arg in arguments_node.children:
395
+ arg_text = self._get_node_text(arg)
396
+ if arg_text and arg_text not in ('(', ')', ','):
397
+ args.append(arg_text)
398
+
399
+ call_data = {
400
+ "name": self._get_node_text(node),
401
+ "full_name": self._get_node_text(full_call_node),
402
+ "line_number": node.start_point[0] + 1,
403
+ "args": args,
404
+ "inferred_obj_type": None,
405
+ "context": self._get_parent_context(node),
406
+ "class_context": self._get_parent_context(node, types=('class_definition',))[:2],
407
+ "lang": self.language_name,
408
+ "is_dependency": False,
409
+ }
410
+ calls.append(call_data)
411
+
412
+ # Second, find dictionary-based method references (indirect calls)
413
+ # This handles patterns like: tool_map = {"name": self.method, ...}
414
+ # followed by: handler = tool_map.get(name); handler()
415
+ dict_method_calls = self._find_dict_method_references(root_node)
416
+ calls.extend(dict_method_calls)
417
+
418
+ return calls
419
+
420
+ def _find_dict_method_references(self, root_node):
421
+ """
422
+ Detects indirect function calls through dictionary mappings.
423
+
424
+ Example pattern:
425
+ tool_map = {
426
+ "add_code": self.add_code_to_graph_tool,
427
+ "find_code": self.find_code_tool,
428
+ }
429
+ handler = tool_map.get(tool_name)
430
+ if handler:
431
+ handler(**args)
432
+
433
+ This creates CALLS relationships from the context function to all
434
+ methods referenced in the dictionary.
435
+ """
436
+ calls = []
437
+ query_str = PY_QUERIES.get('dict_method_refs')
438
+ if not query_str:
439
+ return calls
440
+
441
+ # Track dictionaries that contain method references
442
+ dict_assignments = {} # dict_var_name -> list of method references
443
+
444
+ for node, capture_name in execute_query(self.language, query_str, root_node):
445
+ if capture_name == 'method_ref':
446
+ # Found a method reference in a dictionary value
447
+ # Navigate up to find the assignment
448
+ dict_node = node.parent # pair node
449
+ while dict_node and dict_node.type != 'dictionary':
450
+ dict_node = dict_node.parent
451
+
452
+ if dict_node:
453
+ # Find the assignment node
454
+ assignment_node = dict_node.parent
455
+ if assignment_node and assignment_node.type == 'assignment':
456
+ # Get the variable name being assigned
457
+ left_node = assignment_node.child_by_field_name('left')
458
+ if left_node:
459
+ var_name = self._get_node_text(left_node)
460
+ method_ref = self._get_node_text(node)
461
+
462
+ # Extract just the method name (remove 'self.')
463
+ method_name = method_ref.split('.')[-1] if '.' in method_ref else method_ref
464
+
465
+ if var_name not in dict_assignments:
466
+ dict_assignments[var_name] = {
467
+ 'methods': [],
468
+ 'context': self._get_parent_context(assignment_node),
469
+ 'line_number': assignment_node.start_point[0] + 1
470
+ }
471
+
472
+ dict_assignments[var_name]['methods'].append({
473
+ 'name': method_name,
474
+ 'full_name': method_ref,
475
+ 'line_number': node.start_point[0] + 1
476
+ })
477
+
478
+ # Now create call relationships for each method in the dictionaries
479
+ # The context is the function where the dictionary is defined
480
+ for dict_var, data in dict_assignments.items():
481
+ context, context_type, context_line = data['context']
482
+ class_context, _, _ = (None, None, None)
483
+
484
+ for method_info in data['methods']:
485
+ call_data = {
486
+ "name": method_info['name'],
487
+ "full_name": method_info['full_name'],
488
+ "line_number": method_info['line_number'],
489
+ "args": [], # We don't know the args at this point
490
+ "inferred_obj_type": None,
491
+ "context": (context, context_type, context_line),
492
+ "class_context": (class_context, None),
493
+ "lang": self.language_name,
494
+ "is_dependency": False,
495
+ "is_indirect_call": True, # Mark as indirect for debugging
496
+ }
497
+ calls.append(call_data)
498
+
499
+ return calls
500
+
501
+ def _find_variables(self, root_node):
502
+ variables = []
503
+ query_str = PY_QUERIES['variables']
504
+ for match in execute_query(self.language, query_str, root_node):
505
+ capture_name = match[1]
506
+ node = match[0]
507
+
508
+ if capture_name == 'name':
509
+ assignment_node = node.parent
510
+
511
+ # Skip lambda assignments, they are handled by _find_lambda_assignments
512
+ right_node = assignment_node.child_by_field_name('right')
513
+ if right_node and right_node.type == 'lambda':
514
+ continue
515
+
516
+ name = self._get_node_text(node)
517
+ value = self._get_node_text(right_node) if right_node else None
518
+
519
+ type_node = assignment_node.child_by_field_name('type')
520
+ type_text = self._get_node_text(type_node) if type_node else None
521
+
522
+ context, _, _ = self._get_parent_context(node)
523
+ class_context, _, _ = self._get_parent_context(node, types=('class_definition',))
524
+
525
+ variable_data = {
526
+ "name": name,
527
+ "line_number": node.start_point[0] + 1,
528
+ "value": value,
529
+ "type": type_text,
530
+ "context": context,
531
+ "class_context": class_context,
532
+ "lang": self.language_name,
533
+ "is_dependency": False,
534
+ }
535
+ variables.append(variable_data)
536
+ return variables
537
+
538
+ def pre_scan_python(files: list[Path], parser_wrapper) -> dict:
539
+ """Scans Python files to create a map of class/function names to their file paths."""
540
+ imports_map = {}
541
+ query_str = """
542
+ (class_definition name: (identifier) @name)
543
+ (function_definition name: (identifier) @name)
544
+ """
545
+
546
+ for path in files:
547
+ temp_py_file = None
548
+ try:
549
+ source_to_parse = ""
550
+ if path.suffix == '.ipynb':
551
+ with open(path, 'r', encoding='utf-8') as f:
552
+ notebook_node = nbformat.read(f, as_version=4)
553
+ exporter = PythonExporter()
554
+ python_code, _ = exporter.from_notebook_node(notebook_node)
555
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', encoding='utf-8') as tf:
556
+ tf.write(python_code)
557
+ temp_py_file = Path(tf.name)
558
+ with open(temp_py_file, "r", encoding="utf-8") as f:
559
+ source_to_parse = f.read()
560
+ else:
561
+ with open(path, "r", encoding="utf-8") as f:
562
+ source_to_parse = f.read()
563
+
564
+ tree = parser_wrapper.parser.parse(bytes(source_to_parse, "utf8"))
565
+
566
+ for capture, _ in execute_query(parser_wrapper.language, query_str, tree.root_node):
567
+ name = capture.text.decode('utf-8')
568
+ if name not in imports_map:
569
+ imports_map[name] = []
570
+ imports_map[name].append(str(path.resolve()))
571
+ except Exception as e:
572
+ warning_logger(f"Tree-sitter pre-scan failed for {path}: {e}")
573
+ finally:
574
+ if temp_py_file and temp_py_file.exists():
575
+ os.remove(temp_py_file)
576
+ return imports_map