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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- 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
|