superlocalmemory 3.3.20 → 3.3.21
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.
- package/package.json +1 -1
- package/pyproject.toml +9 -1
- package/src/superlocalmemory/cli/commands.py +138 -22
- package/src/superlocalmemory/cli/daemon.py +372 -0
- package/src/superlocalmemory/cli/main.py +8 -0
- package/src/superlocalmemory/cli/pending_store.py +158 -0
- package/src/superlocalmemory/cli/setup_wizard.py +39 -6
- package/src/superlocalmemory/code_graph/__init__.py +46 -0
- package/src/superlocalmemory/code_graph/blast_radius.py +177 -0
- package/src/superlocalmemory/code_graph/bridge/__init__.py +36 -0
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +464 -0
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +195 -0
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +159 -0
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +170 -0
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +152 -0
- package/src/superlocalmemory/code_graph/changes.py +363 -0
- package/src/superlocalmemory/code_graph/communities.py +299 -0
- package/src/superlocalmemory/code_graph/config.py +88 -0
- package/src/superlocalmemory/code_graph/database.py +482 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +78 -0
- package/src/superlocalmemory/code_graph/extractors/python.py +413 -0
- package/src/superlocalmemory/code_graph/extractors/typescript.py +556 -0
- package/src/superlocalmemory/code_graph/flows.py +350 -0
- package/src/superlocalmemory/code_graph/git_hooks.py +226 -0
- package/src/superlocalmemory/code_graph/graph_engine.py +295 -0
- package/src/superlocalmemory/code_graph/graph_store.py +158 -0
- package/src/superlocalmemory/code_graph/incremental.py +200 -0
- package/src/superlocalmemory/code_graph/models.py +130 -0
- package/src/superlocalmemory/code_graph/parser.py +507 -0
- package/src/superlocalmemory/code_graph/resolver.py +321 -0
- package/src/superlocalmemory/code_graph/search.py +460 -0
- package/src/superlocalmemory/code_graph/service.py +95 -0
- package/src/superlocalmemory/code_graph/watcher.py +207 -0
- package/src/superlocalmemory/core/embedding_worker.py +4 -2
- package/src/superlocalmemory/core/embeddings.py +8 -2
- package/src/superlocalmemory/core/engine.py +32 -0
- package/src/superlocalmemory/core/engine_wiring.py +5 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
- package/src/superlocalmemory/infra/event_bus.py +5 -0
- package/src/superlocalmemory/mcp/server.py +23 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
- package/src/superlocalmemory/retrieval/engine.py +137 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
- package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
- package/src/superlocalmemory/retrieval/strategy.py +16 -0
- package/src/superlocalmemory/server/api.py +4 -2
- package/src/superlocalmemory/server/ui.py +5 -2
- package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
- package/src/superlocalmemory/ui/index.html +1879 -0
- package/src/superlocalmemory/ui/js/agents.js +192 -0
- package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
- package/src/superlocalmemory/ui/js/behavioral.js +276 -0
- package/src/superlocalmemory/ui/js/clusters.js +206 -0
- package/src/superlocalmemory/ui/js/compliance.js +252 -0
- package/src/superlocalmemory/ui/js/core.js +246 -0
- package/src/superlocalmemory/ui/js/dashboard.js +110 -0
- package/src/superlocalmemory/ui/js/events.js +178 -0
- package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
- package/src/superlocalmemory/ui/js/feedback.js +333 -0
- package/src/superlocalmemory/ui/js/graph-core.js +447 -0
- package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
- package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
- package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
- package/src/superlocalmemory/ui/js/ide-status.js +102 -0
- package/src/superlocalmemory/ui/js/init.js +45 -0
- package/src/superlocalmemory/ui/js/learning.js +435 -0
- package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
- package/src/superlocalmemory/ui/js/math-health.js +98 -0
- package/src/superlocalmemory/ui/js/memories.js +264 -0
- package/src/superlocalmemory/ui/js/modal.js +357 -0
- package/src/superlocalmemory/ui/js/patterns.js +93 -0
- package/src/superlocalmemory/ui/js/profiles.js +236 -0
- package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
- package/src/superlocalmemory/ui/js/search.js +59 -0
- package/src/superlocalmemory/ui/js/settings.js +224 -0
- package/src/superlocalmemory/ui/js/timeline.js +32 -0
- package/src/superlocalmemory/ui/js/trust-dashboard.js +73 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4 — CodeGraph Module
|
|
4
|
+
|
|
5
|
+
"""Python-specific AST extractor using tree-sitter S-expression queries.
|
|
6
|
+
|
|
7
|
+
Extracts: function_definition, class_definition, import_statement,
|
|
8
|
+
import_from_statement, call. Detects test functions and docstrings.
|
|
9
|
+
|
|
10
|
+
tree-sitter imports are lazy (HR-07) — only imported when this module
|
|
11
|
+
is actually used at parse time, never at package import time.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from superlocalmemory.code_graph.config import CodeGraphConfig
|
|
21
|
+
from superlocalmemory.code_graph.extractors import BaseExtractor
|
|
22
|
+
from superlocalmemory.code_graph.models import (
|
|
23
|
+
EdgeKind,
|
|
24
|
+
GraphEdge,
|
|
25
|
+
GraphNode,
|
|
26
|
+
NodeKind,
|
|
27
|
+
)
|
|
28
|
+
from superlocalmemory.storage.models import _new_id
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# S-expression queries (compiled once per class, not per instance)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
FUNC_QUERY_SRC = (
|
|
37
|
+
"(function_definition"
|
|
38
|
+
" name: (identifier) @func.name"
|
|
39
|
+
" parameters: (parameters) @func.params"
|
|
40
|
+
") @func.def"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
CLASS_QUERY_SRC = (
|
|
44
|
+
"(class_definition"
|
|
45
|
+
" name: (identifier) @class.name"
|
|
46
|
+
") @class.def"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
IMPORT_QUERY_SRC = (
|
|
50
|
+
"(import_statement) @import.stmt"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
IMPORT_FROM_QUERY_SRC = (
|
|
54
|
+
"(import_from_statement) @import.from_stmt"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
CALL_QUERY_SRC = (
|
|
58
|
+
"(call"
|
|
59
|
+
" function: (identifier) @call.name"
|
|
60
|
+
") @call.expr"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
CALL_METHOD_QUERY_SRC = (
|
|
64
|
+
"(call"
|
|
65
|
+
" function: (attribute"
|
|
66
|
+
" attribute: (identifier) @call.method)"
|
|
67
|
+
") @call.method_expr"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_ts_modules() -> tuple[Any, Any, Any]:
|
|
72
|
+
"""Lazy import of tree-sitter modules."""
|
|
73
|
+
try:
|
|
74
|
+
import tree_sitter
|
|
75
|
+
from tree_sitter_language_pack import get_language
|
|
76
|
+
except ImportError as exc:
|
|
77
|
+
raise ImportError(
|
|
78
|
+
"tree-sitter and tree-sitter-language-pack are required for "
|
|
79
|
+
"code graph parsing. Install with: "
|
|
80
|
+
"pip install 'superlocalmemory[code-graph]'"
|
|
81
|
+
) from exc
|
|
82
|
+
return tree_sitter, get_language, None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _node_text(node: Any, source: bytes) -> str:
|
|
86
|
+
"""Extract text from a tree-sitter node."""
|
|
87
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _find_parent_class(node: Any) -> str | None:
|
|
91
|
+
"""Walk up the AST to find the enclosing class_definition name."""
|
|
92
|
+
current = node.parent
|
|
93
|
+
while current is not None:
|
|
94
|
+
if current.type == "class_definition":
|
|
95
|
+
name_node = current.child_by_field_name("name")
|
|
96
|
+
if name_node:
|
|
97
|
+
return name_node.text.decode("utf-8", errors="replace")
|
|
98
|
+
current = current.parent
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_docstring(func_node: Any, source: bytes) -> str | None:
|
|
103
|
+
"""Extract the docstring from a function/class body."""
|
|
104
|
+
body = func_node.child_by_field_name("body")
|
|
105
|
+
if body is None or len(body.children) == 0:
|
|
106
|
+
return None
|
|
107
|
+
first_stmt = body.children[0]
|
|
108
|
+
# tree-sitter Python may represent docstrings as:
|
|
109
|
+
# 1. expression_statement > string (older grammars)
|
|
110
|
+
# 2. string directly in block (newer grammars)
|
|
111
|
+
string_node = None
|
|
112
|
+
if first_stmt.type == "expression_statement":
|
|
113
|
+
expr = first_stmt.children[0] if first_stmt.children else None
|
|
114
|
+
if expr and expr.type == "string":
|
|
115
|
+
string_node = expr
|
|
116
|
+
elif first_stmt.type == "string":
|
|
117
|
+
string_node = first_stmt
|
|
118
|
+
|
|
119
|
+
if string_node is not None:
|
|
120
|
+
raw = _node_text(string_node, source)
|
|
121
|
+
# Strip triple-quote markers
|
|
122
|
+
for quote in ('"""', "'''", '"', "'"):
|
|
123
|
+
if raw.startswith(quote) and raw.endswith(quote):
|
|
124
|
+
raw = raw[len(quote):-len(quote)]
|
|
125
|
+
break
|
|
126
|
+
return raw.strip()
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_return_type(func_node: Any, source: bytes) -> str | None:
|
|
131
|
+
"""Extract return type annotation from function_definition."""
|
|
132
|
+
rt = func_node.child_by_field_name("return_type")
|
|
133
|
+
if rt:
|
|
134
|
+
return _node_text(rt, source)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _make_qualified_name(
|
|
139
|
+
file_path: str, name: str, parent_name: str | None
|
|
140
|
+
) -> str:
|
|
141
|
+
if parent_name:
|
|
142
|
+
return f"{file_path}::{parent_name}.{name}"
|
|
143
|
+
return f"{file_path}::{name}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _run_query(
|
|
147
|
+
query_src: str, root_node: Any, language_name: str = "python"
|
|
148
|
+
) -> list[tuple[int, dict[str, list[Any]]]]:
|
|
149
|
+
"""Run a tree-sitter query and return matches."""
|
|
150
|
+
ts, get_language, _ = _get_ts_modules()
|
|
151
|
+
lang = get_language(language_name)
|
|
152
|
+
query = ts.Query(lang, query_src)
|
|
153
|
+
cursor = ts.QueryCursor(query)
|
|
154
|
+
return cursor.matches(root_node)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class PythonExtractor(BaseExtractor):
|
|
158
|
+
"""Python-specific AST extractor using S-expression queries."""
|
|
159
|
+
|
|
160
|
+
def extract_functions(self) -> list[GraphNode]:
|
|
161
|
+
"""Extract all function/method definitions."""
|
|
162
|
+
matches = _run_query(FUNC_QUERY_SRC, self._root)
|
|
163
|
+
nodes: list[GraphNode] = []
|
|
164
|
+
|
|
165
|
+
for _pattern_idx, captures in matches:
|
|
166
|
+
func_defs = captures.get("func.def", [])
|
|
167
|
+
func_names = captures.get("func.name", [])
|
|
168
|
+
func_params = captures.get("func.params", [])
|
|
169
|
+
|
|
170
|
+
for i, func_def in enumerate(func_defs):
|
|
171
|
+
name_node = func_names[i] if i < len(func_names) else None
|
|
172
|
+
params_node = func_params[i] if i < len(func_params) else None
|
|
173
|
+
if name_node is None:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
name = _node_text(name_node, self._source)
|
|
177
|
+
parent_name = _find_parent_class(func_def)
|
|
178
|
+
kind = NodeKind.METHOD if parent_name else NodeKind.FUNCTION
|
|
179
|
+
line_start = func_def.start_point[0]
|
|
180
|
+
line_end = func_def.end_point[0]
|
|
181
|
+
|
|
182
|
+
# Build signature
|
|
183
|
+
params_text = _node_text(params_node, self._source) if params_node else "()"
|
|
184
|
+
return_type = _extract_return_type(func_def, self._source)
|
|
185
|
+
sig = f"def {name}{params_text}"
|
|
186
|
+
if return_type:
|
|
187
|
+
sig += f" -> {return_type}"
|
|
188
|
+
|
|
189
|
+
# Docstring
|
|
190
|
+
docstring = _extract_docstring(func_def, self._source)
|
|
191
|
+
|
|
192
|
+
# Decorators
|
|
193
|
+
decorators: list[str] = []
|
|
194
|
+
for child in func_def.children:
|
|
195
|
+
if child.type == "decorator":
|
|
196
|
+
decorators.append(_node_text(child, self._source))
|
|
197
|
+
|
|
198
|
+
extra: dict[str, Any] = {}
|
|
199
|
+
if decorators:
|
|
200
|
+
extra["decorators"] = decorators
|
|
201
|
+
|
|
202
|
+
node = GraphNode(
|
|
203
|
+
node_id=_new_id(),
|
|
204
|
+
kind=kind,
|
|
205
|
+
name=name,
|
|
206
|
+
qualified_name=_make_qualified_name(
|
|
207
|
+
self._file_path, name, parent_name
|
|
208
|
+
),
|
|
209
|
+
file_path=self._file_path,
|
|
210
|
+
line_start=line_start,
|
|
211
|
+
line_end=line_end,
|
|
212
|
+
language="python",
|
|
213
|
+
parent_name=parent_name,
|
|
214
|
+
signature=sig,
|
|
215
|
+
docstring=docstring,
|
|
216
|
+
extra_json=json.dumps(extra) if extra else "{}",
|
|
217
|
+
)
|
|
218
|
+
nodes.append(node)
|
|
219
|
+
|
|
220
|
+
return nodes
|
|
221
|
+
|
|
222
|
+
def extract_classes(self) -> list[GraphNode]:
|
|
223
|
+
"""Extract all class definitions, including INHERITS edges."""
|
|
224
|
+
matches = _run_query(CLASS_QUERY_SRC, self._root)
|
|
225
|
+
nodes: list[GraphNode] = []
|
|
226
|
+
self._inherits_info: list[tuple[str, str, int]] = [] # (class_qname, base_name, line)
|
|
227
|
+
|
|
228
|
+
for _pattern_idx, captures in matches:
|
|
229
|
+
class_defs = captures.get("class.def", [])
|
|
230
|
+
class_names = captures.get("class.name", [])
|
|
231
|
+
|
|
232
|
+
for i, class_def in enumerate(class_defs):
|
|
233
|
+
name_node = class_names[i] if i < len(class_names) else None
|
|
234
|
+
if name_node is None:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
name = _node_text(name_node, self._source)
|
|
238
|
+
line_start = class_def.start_point[0]
|
|
239
|
+
line_end = class_def.end_point[0]
|
|
240
|
+
|
|
241
|
+
# Check for parent class in enclosing class
|
|
242
|
+
parent_class = _find_parent_class(class_def)
|
|
243
|
+
|
|
244
|
+
qualified_name = _make_qualified_name(
|
|
245
|
+
self._file_path, name, parent_class
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Extract superclasses
|
|
249
|
+
for child in class_def.children:
|
|
250
|
+
if child.type == "argument_list":
|
|
251
|
+
for arg in child.named_children:
|
|
252
|
+
base_name = _node_text(arg, self._source)
|
|
253
|
+
self._inherits_info.append(
|
|
254
|
+
(qualified_name, base_name, line_start)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Docstring
|
|
258
|
+
docstring = _extract_docstring(class_def, self._source)
|
|
259
|
+
|
|
260
|
+
node = GraphNode(
|
|
261
|
+
node_id=_new_id(),
|
|
262
|
+
kind=NodeKind.CLASS,
|
|
263
|
+
name=name,
|
|
264
|
+
qualified_name=qualified_name,
|
|
265
|
+
file_path=self._file_path,
|
|
266
|
+
line_start=line_start,
|
|
267
|
+
line_end=line_end,
|
|
268
|
+
language="python",
|
|
269
|
+
parent_name=parent_class,
|
|
270
|
+
docstring=docstring,
|
|
271
|
+
)
|
|
272
|
+
nodes.append(node)
|
|
273
|
+
|
|
274
|
+
return nodes
|
|
275
|
+
|
|
276
|
+
def extract_imports(
|
|
277
|
+
self,
|
|
278
|
+
) -> tuple[list[GraphEdge], dict[str, tuple[str, str]]]:
|
|
279
|
+
"""Extract import statements and build import_map."""
|
|
280
|
+
edges: list[GraphEdge] = []
|
|
281
|
+
import_map: dict[str, tuple[str, str]] = {}
|
|
282
|
+
|
|
283
|
+
# Process "import X" statements
|
|
284
|
+
matches = _run_query(IMPORT_QUERY_SRC, self._root)
|
|
285
|
+
for _pat, captures in matches:
|
|
286
|
+
for stmt in captures.get("import.stmt", []):
|
|
287
|
+
for child in stmt.named_children:
|
|
288
|
+
if child.type == "dotted_name":
|
|
289
|
+
module = _node_text(child, self._source)
|
|
290
|
+
local_name = module.split(".")[-1]
|
|
291
|
+
import_map[local_name] = (module, local_name)
|
|
292
|
+
elif child.type == "aliased_import":
|
|
293
|
+
name_node = child.child_by_field_name("name")
|
|
294
|
+
alias_node = child.child_by_field_name("alias")
|
|
295
|
+
if name_node:
|
|
296
|
+
module = _node_text(name_node, self._source)
|
|
297
|
+
alias = (
|
|
298
|
+
_node_text(alias_node, self._source)
|
|
299
|
+
if alias_node
|
|
300
|
+
else module.split(".")[-1]
|
|
301
|
+
)
|
|
302
|
+
import_map[alias] = (module, module.split(".")[-1])
|
|
303
|
+
|
|
304
|
+
edges.append(GraphEdge(
|
|
305
|
+
edge_id=_new_id(),
|
|
306
|
+
kind=EdgeKind.IMPORTS,
|
|
307
|
+
source_node_id="__unresolved__",
|
|
308
|
+
target_node_id="__unresolved__",
|
|
309
|
+
file_path=self._file_path,
|
|
310
|
+
line=stmt.start_point[0],
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
# Process "from X import Y" statements
|
|
314
|
+
from_matches = _run_query(IMPORT_FROM_QUERY_SRC, self._root)
|
|
315
|
+
for _pat, captures in from_matches:
|
|
316
|
+
for stmt in captures.get("import.from_stmt", []):
|
|
317
|
+
module_name = ""
|
|
318
|
+
# Find module name (dotted_name or relative_import)
|
|
319
|
+
for child in stmt.children:
|
|
320
|
+
if child.type == "dotted_name":
|
|
321
|
+
if not module_name:
|
|
322
|
+
module_name = _node_text(child, self._source)
|
|
323
|
+
continue
|
|
324
|
+
elif child.type == "relative_import":
|
|
325
|
+
module_name = _node_text(child, self._source)
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Find imported names
|
|
329
|
+
imported_names: list[tuple[str, str]] = []
|
|
330
|
+
seen_import_keyword = False
|
|
331
|
+
for child in stmt.children:
|
|
332
|
+
if child.type == "import":
|
|
333
|
+
seen_import_keyword = True
|
|
334
|
+
continue
|
|
335
|
+
if not seen_import_keyword:
|
|
336
|
+
continue
|
|
337
|
+
if child.type == "dotted_name":
|
|
338
|
+
imp_name = _node_text(child, self._source)
|
|
339
|
+
imported_names.append((imp_name, imp_name))
|
|
340
|
+
elif child.type == "aliased_import":
|
|
341
|
+
name_n = child.child_by_field_name("name")
|
|
342
|
+
alias_n = child.child_by_field_name("alias")
|
|
343
|
+
if name_n:
|
|
344
|
+
orig = _node_text(name_n, self._source)
|
|
345
|
+
alias = (
|
|
346
|
+
_node_text(alias_n, self._source)
|
|
347
|
+
if alias_n
|
|
348
|
+
else orig
|
|
349
|
+
)
|
|
350
|
+
imported_names.append((alias, orig))
|
|
351
|
+
elif child.type == "wildcard_import":
|
|
352
|
+
imported_names.append(("*", "*"))
|
|
353
|
+
|
|
354
|
+
for local_name, orig_name in imported_names:
|
|
355
|
+
import_map[local_name] = (module_name, orig_name)
|
|
356
|
+
|
|
357
|
+
edges.append(GraphEdge(
|
|
358
|
+
edge_id=_new_id(),
|
|
359
|
+
kind=EdgeKind.IMPORTS,
|
|
360
|
+
source_node_id="__unresolved__",
|
|
361
|
+
target_node_id="__unresolved__",
|
|
362
|
+
file_path=self._file_path,
|
|
363
|
+
line=stmt.start_point[0],
|
|
364
|
+
))
|
|
365
|
+
|
|
366
|
+
return edges, import_map
|
|
367
|
+
|
|
368
|
+
def extract_calls(
|
|
369
|
+
self, import_map: dict[str, tuple[str, str]]
|
|
370
|
+
) -> list[GraphEdge]:
|
|
371
|
+
"""Extract function/method calls."""
|
|
372
|
+
edges: list[GraphEdge] = []
|
|
373
|
+
|
|
374
|
+
# Simple calls: foo()
|
|
375
|
+
matches = _run_query(CALL_QUERY_SRC, self._root)
|
|
376
|
+
for _pat, captures in matches:
|
|
377
|
+
call_names = captures.get("call.name", [])
|
|
378
|
+
call_exprs = captures.get("call.expr", [])
|
|
379
|
+
for i, name_node in enumerate(call_names):
|
|
380
|
+
call_name = _node_text(name_node, self._source)
|
|
381
|
+
line = name_node.start_point[0]
|
|
382
|
+
# Find enclosing function for source context
|
|
383
|
+
confidence = 1.0 if call_name in import_map else self._config.heuristic_confidence
|
|
384
|
+
edges.append(GraphEdge(
|
|
385
|
+
edge_id=_new_id(),
|
|
386
|
+
kind=EdgeKind.CALLS,
|
|
387
|
+
source_node_id="__unresolved__",
|
|
388
|
+
target_node_id=f"__call__{call_name}",
|
|
389
|
+
file_path=self._file_path,
|
|
390
|
+
line=line,
|
|
391
|
+
confidence=confidence,
|
|
392
|
+
extra_json=json.dumps({"call_name": call_name}),
|
|
393
|
+
))
|
|
394
|
+
|
|
395
|
+
# Method calls: obj.method()
|
|
396
|
+
method_matches = _run_query(CALL_METHOD_QUERY_SRC, self._root)
|
|
397
|
+
for _pat, captures in method_matches:
|
|
398
|
+
method_names = captures.get("call.method", [])
|
|
399
|
+
for name_node in method_names:
|
|
400
|
+
method_name = _node_text(name_node, self._source)
|
|
401
|
+
line = name_node.start_point[0]
|
|
402
|
+
edges.append(GraphEdge(
|
|
403
|
+
edge_id=_new_id(),
|
|
404
|
+
kind=EdgeKind.CALLS,
|
|
405
|
+
source_node_id="__unresolved__",
|
|
406
|
+
target_node_id=f"__call__{method_name}",
|
|
407
|
+
file_path=self._file_path,
|
|
408
|
+
line=line,
|
|
409
|
+
confidence=self._config.heuristic_confidence,
|
|
410
|
+
extra_json=json.dumps({"call_name": method_name, "is_method": True}),
|
|
411
|
+
))
|
|
412
|
+
|
|
413
|
+
return edges
|