superlocalmemory 3.3.20 → 3.3.22
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,556 @@
|
|
|
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
|
+
"""TypeScript/JavaScript AST extractor using tree-sitter.
|
|
6
|
+
|
|
7
|
+
Handles .ts, .tsx, .js, .jsx via appropriate grammar selection.
|
|
8
|
+
Extracts: function_declaration, arrow_function, class_declaration,
|
|
9
|
+
interface_declaration, import_statement, call_expression, JSX elements.
|
|
10
|
+
|
|
11
|
+
tree-sitter imports are lazy (HR-07).
|
|
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
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
FUNC_DECL_QUERY_SRC = (
|
|
37
|
+
"(function_declaration"
|
|
38
|
+
" name: (identifier) @func.name"
|
|
39
|
+
") @func.def"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
METHOD_DEF_QUERY_SRC = (
|
|
43
|
+
"(method_definition"
|
|
44
|
+
" name: (property_identifier) @method.name"
|
|
45
|
+
") @method.def"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
ARROW_QUERY_SRC = (
|
|
49
|
+
"(lexical_declaration"
|
|
50
|
+
" (variable_declarator"
|
|
51
|
+
" name: (identifier) @arrow.name"
|
|
52
|
+
" value: (arrow_function) @arrow.func"
|
|
53
|
+
" )"
|
|
54
|
+
") @arrow.decl"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
CLASS_QUERY_SRC = (
|
|
58
|
+
"(class_declaration"
|
|
59
|
+
" name: (type_identifier) @class.name"
|
|
60
|
+
") @class.def"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
IFACE_QUERY_SRC = (
|
|
64
|
+
"(interface_declaration"
|
|
65
|
+
" name: (type_identifier) @iface.name"
|
|
66
|
+
") @iface.def"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
IMPORT_QUERY_SRC = (
|
|
70
|
+
"(import_statement"
|
|
71
|
+
" source: (string) @import.source"
|
|
72
|
+
") @import.stmt"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
CALL_QUERY_SRC = (
|
|
76
|
+
"(call_expression"
|
|
77
|
+
" function: (identifier) @call.name"
|
|
78
|
+
") @call.expr"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
CALL_MEMBER_QUERY_SRC = (
|
|
82
|
+
"(call_expression"
|
|
83
|
+
" function: (member_expression"
|
|
84
|
+
" property: (property_identifier) @call.method)"
|
|
85
|
+
") @call.member_expr"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
JSX_ELEMENT_QUERY_SRC = (
|
|
89
|
+
"(jsx_opening_element"
|
|
90
|
+
" name: (identifier) @jsx.name"
|
|
91
|
+
") @jsx.open"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
JSX_SELF_CLOSING_QUERY_SRC = (
|
|
95
|
+
"(jsx_self_closing_element"
|
|
96
|
+
" name: (identifier) @jsx.name"
|
|
97
|
+
") @jsx.self_close"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
NEW_EXPR_QUERY_SRC = (
|
|
101
|
+
"(new_expression"
|
|
102
|
+
" constructor: (identifier) @new.name"
|
|
103
|
+
") @new.expr"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
EXPORT_QUERY_SRC = "(export_statement) @export.stmt"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_ts_modules() -> tuple[Any, Any]:
|
|
110
|
+
"""Lazy import of tree-sitter modules."""
|
|
111
|
+
try:
|
|
112
|
+
import tree_sitter
|
|
113
|
+
from tree_sitter_language_pack import get_language
|
|
114
|
+
except ImportError as exc:
|
|
115
|
+
raise ImportError(
|
|
116
|
+
"tree-sitter and tree-sitter-language-pack required for parsing. "
|
|
117
|
+
"Install: pip install 'superlocalmemory[code-graph]'"
|
|
118
|
+
) from exc
|
|
119
|
+
return tree_sitter, get_language
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _node_text(node: Any, source: bytes) -> str:
|
|
123
|
+
"""Extract text from a tree-sitter node."""
|
|
124
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _find_parent_class(node: Any) -> str | None:
|
|
128
|
+
"""Walk up AST to find enclosing class_declaration name."""
|
|
129
|
+
current = node.parent
|
|
130
|
+
while current is not None:
|
|
131
|
+
if current.type == "class_declaration":
|
|
132
|
+
name_node = current.child_by_field_name("name")
|
|
133
|
+
if name_node:
|
|
134
|
+
return name_node.text.decode("utf-8", errors="replace")
|
|
135
|
+
current = current.parent
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _make_qualified_name(
|
|
140
|
+
file_path: str, name: str, parent_name: str | None
|
|
141
|
+
) -> str:
|
|
142
|
+
if parent_name:
|
|
143
|
+
return f"{file_path}::{parent_name}.{name}"
|
|
144
|
+
return f"{file_path}::{name}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _run_query(
|
|
148
|
+
query_src: str, root_node: Any, language_name: str
|
|
149
|
+
) -> list[tuple[int, dict[str, list[Any]]]]:
|
|
150
|
+
"""Run a tree-sitter query and return matches."""
|
|
151
|
+
ts_mod, get_language = _get_ts_modules()
|
|
152
|
+
lang = get_language(language_name)
|
|
153
|
+
query = ts_mod.Query(lang, query_src)
|
|
154
|
+
cursor = ts_mod.QueryCursor(query)
|
|
155
|
+
return cursor.matches(root_node)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _safe_query(
|
|
159
|
+
query_src: str, root_node: Any, language_name: str
|
|
160
|
+
) -> list[tuple[int, dict[str, list[Any]]]]:
|
|
161
|
+
"""Run query, returning empty list on error (some queries may not apply to all grammars)."""
|
|
162
|
+
try:
|
|
163
|
+
return _run_query(query_src, root_node, language_name)
|
|
164
|
+
except Exception:
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _detect_language(file_path: str) -> str:
|
|
169
|
+
"""Map file extension to tree-sitter grammar name."""
|
|
170
|
+
if file_path.endswith(".tsx"):
|
|
171
|
+
return "tsx"
|
|
172
|
+
if file_path.endswith(".ts"):
|
|
173
|
+
return "typescript"
|
|
174
|
+
if file_path.endswith(".jsx"):
|
|
175
|
+
return "javascript"
|
|
176
|
+
return "javascript"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TypeScriptExtractor(BaseExtractor):
|
|
180
|
+
"""TypeScript/JavaScript AST extractor."""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
root_node: Any,
|
|
185
|
+
source_bytes: bytes,
|
|
186
|
+
file_path: str,
|
|
187
|
+
config: CodeGraphConfig,
|
|
188
|
+
) -> None:
|
|
189
|
+
super().__init__(root_node, source_bytes, file_path, config)
|
|
190
|
+
self._lang_name = _detect_language(file_path)
|
|
191
|
+
self._exports: list[str] = []
|
|
192
|
+
|
|
193
|
+
def extract_functions(self) -> list[GraphNode]:
|
|
194
|
+
"""Extract function declarations, arrow functions, and methods."""
|
|
195
|
+
nodes: list[GraphNode] = []
|
|
196
|
+
|
|
197
|
+
# Function declarations
|
|
198
|
+
for _pat, captures in _run_query(FUNC_DECL_QUERY_SRC, self._root, self._lang_name):
|
|
199
|
+
func_defs = captures.get("func.def", [])
|
|
200
|
+
func_names = captures.get("func.name", [])
|
|
201
|
+
for i, func_def in enumerate(func_defs):
|
|
202
|
+
name_node = func_names[i] if i < len(func_names) else None
|
|
203
|
+
if not name_node:
|
|
204
|
+
continue
|
|
205
|
+
name = _node_text(name_node, self._source)
|
|
206
|
+
parent = _find_parent_class(func_def)
|
|
207
|
+
sig = self._build_func_signature(func_def, name)
|
|
208
|
+
nodes.append(GraphNode(
|
|
209
|
+
node_id=_new_id(),
|
|
210
|
+
kind=NodeKind.METHOD if parent else NodeKind.FUNCTION,
|
|
211
|
+
name=name,
|
|
212
|
+
qualified_name=_make_qualified_name(self._file_path, name, parent),
|
|
213
|
+
file_path=self._file_path,
|
|
214
|
+
line_start=func_def.start_point[0],
|
|
215
|
+
line_end=func_def.end_point[0],
|
|
216
|
+
language=self._lang_name,
|
|
217
|
+
parent_name=parent,
|
|
218
|
+
signature=sig,
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
# Method definitions
|
|
222
|
+
for _pat, captures in _run_query(METHOD_DEF_QUERY_SRC, self._root, self._lang_name):
|
|
223
|
+
method_defs = captures.get("method.def", [])
|
|
224
|
+
method_names = captures.get("method.name", [])
|
|
225
|
+
for i, method_def in enumerate(method_defs):
|
|
226
|
+
name_node = method_names[i] if i < len(method_names) else None
|
|
227
|
+
if not name_node:
|
|
228
|
+
continue
|
|
229
|
+
name = _node_text(name_node, self._source)
|
|
230
|
+
parent = _find_parent_class(method_def)
|
|
231
|
+
sig = self._build_method_signature(method_def, name)
|
|
232
|
+
nodes.append(GraphNode(
|
|
233
|
+
node_id=_new_id(),
|
|
234
|
+
kind=NodeKind.METHOD,
|
|
235
|
+
name=name,
|
|
236
|
+
qualified_name=_make_qualified_name(self._file_path, name, parent),
|
|
237
|
+
file_path=self._file_path,
|
|
238
|
+
line_start=method_def.start_point[0],
|
|
239
|
+
line_end=method_def.end_point[0],
|
|
240
|
+
language=self._lang_name,
|
|
241
|
+
parent_name=parent,
|
|
242
|
+
signature=sig,
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
# Arrow functions assigned to variables
|
|
246
|
+
for _pat, captures in _run_query(ARROW_QUERY_SRC, self._root, self._lang_name):
|
|
247
|
+
arrow_decls = captures.get("arrow.decl", [])
|
|
248
|
+
arrow_names = captures.get("arrow.name", [])
|
|
249
|
+
arrow_funcs = captures.get("arrow.func", [])
|
|
250
|
+
for i, decl in enumerate(arrow_decls):
|
|
251
|
+
name_node = arrow_names[i] if i < len(arrow_names) else None
|
|
252
|
+
func_node = arrow_funcs[i] if i < len(arrow_funcs) else None
|
|
253
|
+
if not name_node:
|
|
254
|
+
continue
|
|
255
|
+
name = _node_text(name_node, self._source)
|
|
256
|
+
parent = _find_parent_class(decl)
|
|
257
|
+
sig = self._build_arrow_signature(func_node, name) if func_node else f"const {name} = (...) => ..."
|
|
258
|
+
nodes.append(GraphNode(
|
|
259
|
+
node_id=_new_id(),
|
|
260
|
+
kind=NodeKind.METHOD if parent else NodeKind.FUNCTION,
|
|
261
|
+
name=name,
|
|
262
|
+
qualified_name=_make_qualified_name(self._file_path, name, parent),
|
|
263
|
+
file_path=self._file_path,
|
|
264
|
+
line_start=decl.start_point[0],
|
|
265
|
+
line_end=decl.end_point[0],
|
|
266
|
+
language=self._lang_name,
|
|
267
|
+
parent_name=parent,
|
|
268
|
+
signature=sig,
|
|
269
|
+
))
|
|
270
|
+
|
|
271
|
+
return nodes
|
|
272
|
+
|
|
273
|
+
def extract_classes(self) -> list[GraphNode]:
|
|
274
|
+
"""Extract class and interface declarations."""
|
|
275
|
+
nodes: list[GraphNode] = []
|
|
276
|
+
self._inherits_info: list[tuple[str, str, int]] = []
|
|
277
|
+
|
|
278
|
+
# Classes
|
|
279
|
+
for _pat, captures in _run_query(CLASS_QUERY_SRC, self._root, self._lang_name):
|
|
280
|
+
class_defs = captures.get("class.def", [])
|
|
281
|
+
class_names = captures.get("class.name", [])
|
|
282
|
+
for i, class_def in enumerate(class_defs):
|
|
283
|
+
name_node = class_names[i] if i < len(class_names) else None
|
|
284
|
+
if not name_node:
|
|
285
|
+
continue
|
|
286
|
+
name = _node_text(name_node, self._source)
|
|
287
|
+
qname = _make_qualified_name(self._file_path, name, None)
|
|
288
|
+
# Check for heritage clause (extends/implements)
|
|
289
|
+
self._extract_heritage(class_def, qname)
|
|
290
|
+
nodes.append(GraphNode(
|
|
291
|
+
node_id=_new_id(),
|
|
292
|
+
kind=NodeKind.CLASS,
|
|
293
|
+
name=name,
|
|
294
|
+
qualified_name=qname,
|
|
295
|
+
file_path=self._file_path,
|
|
296
|
+
line_start=class_def.start_point[0],
|
|
297
|
+
line_end=class_def.end_point[0],
|
|
298
|
+
language=self._lang_name,
|
|
299
|
+
))
|
|
300
|
+
|
|
301
|
+
# Interfaces
|
|
302
|
+
for _pat, captures in _safe_query(IFACE_QUERY_SRC, self._root, self._lang_name):
|
|
303
|
+
iface_defs = captures.get("iface.def", [])
|
|
304
|
+
iface_names = captures.get("iface.name", [])
|
|
305
|
+
for i, iface_def in enumerate(iface_defs):
|
|
306
|
+
name_node = iface_names[i] if i < len(iface_names) else None
|
|
307
|
+
if not name_node:
|
|
308
|
+
continue
|
|
309
|
+
name = _node_text(name_node, self._source)
|
|
310
|
+
nodes.append(GraphNode(
|
|
311
|
+
node_id=_new_id(),
|
|
312
|
+
kind=NodeKind.CLASS,
|
|
313
|
+
name=name,
|
|
314
|
+
qualified_name=_make_qualified_name(self._file_path, name, None),
|
|
315
|
+
file_path=self._file_path,
|
|
316
|
+
line_start=iface_def.start_point[0],
|
|
317
|
+
line_end=iface_def.end_point[0],
|
|
318
|
+
language=self._lang_name,
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
return nodes
|
|
322
|
+
|
|
323
|
+
def extract_imports(
|
|
324
|
+
self,
|
|
325
|
+
) -> tuple[list[GraphEdge], dict[str, tuple[str, str]]]:
|
|
326
|
+
"""Extract import statements and build import_map."""
|
|
327
|
+
edges: list[GraphEdge] = []
|
|
328
|
+
import_map: dict[str, tuple[str, str]] = {}
|
|
329
|
+
|
|
330
|
+
for _pat, captures in _run_query(IMPORT_QUERY_SRC, self._root, self._lang_name):
|
|
331
|
+
stmts = captures.get("import.stmt", [])
|
|
332
|
+
sources = captures.get("import.source", [])
|
|
333
|
+
for i, stmt in enumerate(stmts):
|
|
334
|
+
source_node = sources[i] if i < len(sources) else None
|
|
335
|
+
if not source_node:
|
|
336
|
+
continue
|
|
337
|
+
module_path = _node_text(source_node, self._source).strip("'\"")
|
|
338
|
+
|
|
339
|
+
# Parse import clause
|
|
340
|
+
for child in stmt.children:
|
|
341
|
+
if child.type == "import_clause":
|
|
342
|
+
self._parse_import_clause(child, module_path, import_map)
|
|
343
|
+
|
|
344
|
+
edges.append(GraphEdge(
|
|
345
|
+
edge_id=_new_id(),
|
|
346
|
+
kind=EdgeKind.IMPORTS,
|
|
347
|
+
source_node_id="__unresolved__",
|
|
348
|
+
target_node_id="__unresolved__",
|
|
349
|
+
file_path=self._file_path,
|
|
350
|
+
line=stmt.start_point[0],
|
|
351
|
+
extra_json=json.dumps({"module": module_path}),
|
|
352
|
+
))
|
|
353
|
+
|
|
354
|
+
return edges, import_map
|
|
355
|
+
|
|
356
|
+
def extract_calls(
|
|
357
|
+
self, import_map: dict[str, tuple[str, str]]
|
|
358
|
+
) -> list[GraphEdge]:
|
|
359
|
+
"""Extract function/method calls and new expressions."""
|
|
360
|
+
edges: list[GraphEdge] = []
|
|
361
|
+
|
|
362
|
+
# Simple calls: foo()
|
|
363
|
+
for _pat, captures in _run_query(CALL_QUERY_SRC, self._root, self._lang_name):
|
|
364
|
+
call_names = captures.get("call.name", [])
|
|
365
|
+
for name_node in call_names:
|
|
366
|
+
call_name = _node_text(name_node, self._source)
|
|
367
|
+
confidence = 1.0 if call_name in import_map else self._config.heuristic_confidence
|
|
368
|
+
edges.append(GraphEdge(
|
|
369
|
+
edge_id=_new_id(),
|
|
370
|
+
kind=EdgeKind.CALLS,
|
|
371
|
+
source_node_id="__unresolved__",
|
|
372
|
+
target_node_id=f"__call__{call_name}",
|
|
373
|
+
file_path=self._file_path,
|
|
374
|
+
line=name_node.start_point[0],
|
|
375
|
+
confidence=confidence,
|
|
376
|
+
extra_json=json.dumps({"call_name": call_name}),
|
|
377
|
+
))
|
|
378
|
+
|
|
379
|
+
# Member calls: obj.method()
|
|
380
|
+
for _pat, captures in _run_query(CALL_MEMBER_QUERY_SRC, self._root, self._lang_name):
|
|
381
|
+
method_names = captures.get("call.method", [])
|
|
382
|
+
for name_node in method_names:
|
|
383
|
+
method_name = _node_text(name_node, self._source)
|
|
384
|
+
edges.append(GraphEdge(
|
|
385
|
+
edge_id=_new_id(),
|
|
386
|
+
kind=EdgeKind.CALLS,
|
|
387
|
+
source_node_id="__unresolved__",
|
|
388
|
+
target_node_id=f"__call__{method_name}",
|
|
389
|
+
file_path=self._file_path,
|
|
390
|
+
line=name_node.start_point[0],
|
|
391
|
+
confidence=self._config.heuristic_confidence,
|
|
392
|
+
extra_json=json.dumps({"call_name": method_name, "is_method": True}),
|
|
393
|
+
))
|
|
394
|
+
|
|
395
|
+
# new Foo()
|
|
396
|
+
for _pat, captures in _run_query(NEW_EXPR_QUERY_SRC, self._root, self._lang_name):
|
|
397
|
+
new_names = captures.get("new.name", [])
|
|
398
|
+
for name_node in new_names:
|
|
399
|
+
call_name = _node_text(name_node, self._source)
|
|
400
|
+
confidence = 1.0 if call_name in import_map else self._config.heuristic_confidence
|
|
401
|
+
edges.append(GraphEdge(
|
|
402
|
+
edge_id=_new_id(),
|
|
403
|
+
kind=EdgeKind.CALLS,
|
|
404
|
+
source_node_id="__unresolved__",
|
|
405
|
+
target_node_id=f"__call__{call_name}",
|
|
406
|
+
file_path=self._file_path,
|
|
407
|
+
line=name_node.start_point[0],
|
|
408
|
+
confidence=confidence,
|
|
409
|
+
extra_json=json.dumps({"call_name": call_name, "is_constructor": True}),
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
# JSX components (tsx/jsx only)
|
|
413
|
+
if self._lang_name in ("tsx", "javascript"):
|
|
414
|
+
self._extract_jsx_calls(edges, import_map)
|
|
415
|
+
|
|
416
|
+
return edges
|
|
417
|
+
|
|
418
|
+
def extract_exports(self) -> list[str]:
|
|
419
|
+
"""Extract exported names."""
|
|
420
|
+
exports: list[str] = []
|
|
421
|
+
for _pat, captures in _safe_query(EXPORT_QUERY_SRC, self._root, self._lang_name):
|
|
422
|
+
for stmt in captures.get("export.stmt", []):
|
|
423
|
+
# Look for named children that contain identifiers
|
|
424
|
+
for child in stmt.named_children:
|
|
425
|
+
if child.type == "function_declaration":
|
|
426
|
+
name_n = child.child_by_field_name("name")
|
|
427
|
+
if name_n:
|
|
428
|
+
exports.append(_node_text(name_n, self._source))
|
|
429
|
+
elif child.type == "class_declaration":
|
|
430
|
+
name_n = child.child_by_field_name("name")
|
|
431
|
+
if name_n:
|
|
432
|
+
exports.append(_node_text(name_n, self._source))
|
|
433
|
+
elif child.type == "lexical_declaration":
|
|
434
|
+
for decl in child.named_children:
|
|
435
|
+
if decl.type == "variable_declarator":
|
|
436
|
+
name_n = decl.child_by_field_name("name")
|
|
437
|
+
if name_n:
|
|
438
|
+
exports.append(_node_text(name_n, self._source))
|
|
439
|
+
self._exports = exports
|
|
440
|
+
return exports
|
|
441
|
+
|
|
442
|
+
def extract(self) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
443
|
+
"""Override to also extract exports."""
|
|
444
|
+
self.extract_exports()
|
|
445
|
+
return super().extract()
|
|
446
|
+
|
|
447
|
+
# ------------------------------------------------------------------
|
|
448
|
+
# Private helpers
|
|
449
|
+
# ------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
def _parse_import_clause(
|
|
452
|
+
self, clause: Any, module_path: str, import_map: dict[str, tuple[str, str]]
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Parse an import_clause node into import_map entries."""
|
|
455
|
+
for child in clause.children:
|
|
456
|
+
if child.type == "identifier":
|
|
457
|
+
# Default import: import Foo from '...'
|
|
458
|
+
name = _node_text(child, self._source)
|
|
459
|
+
import_map[name] = (module_path, "default")
|
|
460
|
+
elif child.type == "named_imports":
|
|
461
|
+
for spec in child.named_children:
|
|
462
|
+
if spec.type == "import_specifier":
|
|
463
|
+
name_n = spec.child_by_field_name("name")
|
|
464
|
+
alias_n = spec.child_by_field_name("alias")
|
|
465
|
+
if name_n:
|
|
466
|
+
orig = _node_text(name_n, self._source)
|
|
467
|
+
alias = _node_text(alias_n, self._source) if alias_n else orig
|
|
468
|
+
import_map[alias] = (module_path, orig)
|
|
469
|
+
elif child.type == "namespace_import":
|
|
470
|
+
# import * as ns from '...'
|
|
471
|
+
for sub in child.children:
|
|
472
|
+
if sub.type == "identifier":
|
|
473
|
+
ns_name = _node_text(sub, self._source)
|
|
474
|
+
import_map[ns_name] = (module_path, "*")
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
def _extract_heritage(
|
|
478
|
+
self, class_node: Any, class_qname: str
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Extract extends/implements from class heritage clause."""
|
|
481
|
+
for child in class_node.children:
|
|
482
|
+
if child.type == "class_heritage":
|
|
483
|
+
for heritage in child.named_children:
|
|
484
|
+
if heritage.type in ("extends_clause", "implements_clause"):
|
|
485
|
+
for type_node in heritage.named_children:
|
|
486
|
+
base_name = _node_text(type_node, self._source)
|
|
487
|
+
self._inherits_info.append(
|
|
488
|
+
(class_qname, base_name, class_node.start_point[0])
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def _extract_jsx_calls(
|
|
492
|
+
self, edges: list[GraphEdge], import_map: dict[str, tuple[str, str]]
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Extract JSX component usage as CALLS edges."""
|
|
495
|
+
for query_src in (JSX_ELEMENT_QUERY_SRC, JSX_SELF_CLOSING_QUERY_SRC):
|
|
496
|
+
for _pat, captures in _safe_query(query_src, self._root, self._lang_name):
|
|
497
|
+
for name_node in captures.get("jsx.name", []):
|
|
498
|
+
comp_name = _node_text(name_node, self._source)
|
|
499
|
+
# Only uppercase = React component (lowercase = HTML tag)
|
|
500
|
+
if comp_name and comp_name[0].isupper():
|
|
501
|
+
confidence = 1.0 if comp_name in import_map else self._config.heuristic_confidence
|
|
502
|
+
edges.append(GraphEdge(
|
|
503
|
+
edge_id=_new_id(),
|
|
504
|
+
kind=EdgeKind.CALLS,
|
|
505
|
+
source_node_id="__unresolved__",
|
|
506
|
+
target_node_id=f"__call__{comp_name}",
|
|
507
|
+
file_path=self._file_path,
|
|
508
|
+
line=name_node.start_point[0],
|
|
509
|
+
confidence=confidence,
|
|
510
|
+
extra_json=json.dumps({"call_name": comp_name, "is_jsx": True}),
|
|
511
|
+
))
|
|
512
|
+
|
|
513
|
+
def _build_func_signature(self, func_def: Any, name: str) -> str:
|
|
514
|
+
"""Build signature for function_declaration."""
|
|
515
|
+
params_node = func_def.child_by_field_name("parameters")
|
|
516
|
+
params = _node_text(params_node, self._source) if params_node else "()"
|
|
517
|
+
rt = func_def.child_by_field_name("return_type")
|
|
518
|
+
rt_text = _node_text(rt, self._source) if rt else ""
|
|
519
|
+
# Check for async keyword
|
|
520
|
+
is_async = any(
|
|
521
|
+
c.type == "async" for c in func_def.children
|
|
522
|
+
if hasattr(c, "type")
|
|
523
|
+
)
|
|
524
|
+
prefix = "async function" if is_async else "function"
|
|
525
|
+
sig = f"{prefix} {name}{params}"
|
|
526
|
+
if rt_text:
|
|
527
|
+
sig += rt_text
|
|
528
|
+
return sig
|
|
529
|
+
|
|
530
|
+
def _build_method_signature(self, method_def: Any, name: str) -> str:
|
|
531
|
+
"""Build signature for method_definition."""
|
|
532
|
+
params_node = method_def.child_by_field_name("parameters")
|
|
533
|
+
params = _node_text(params_node, self._source) if params_node else "()"
|
|
534
|
+
rt = method_def.child_by_field_name("return_type")
|
|
535
|
+
rt_text = _node_text(rt, self._source) if rt else ""
|
|
536
|
+
is_async = any(
|
|
537
|
+
c.type == "async" for c in method_def.children
|
|
538
|
+
if hasattr(c, "type")
|
|
539
|
+
)
|
|
540
|
+
prefix = "async " if is_async else ""
|
|
541
|
+
sig = f"{prefix}{name}{params}"
|
|
542
|
+
if rt_text:
|
|
543
|
+
sig += rt_text
|
|
544
|
+
return sig
|
|
545
|
+
|
|
546
|
+
def _build_arrow_signature(self, arrow_func: Any, name: str) -> str:
|
|
547
|
+
"""Build signature for arrow function."""
|
|
548
|
+
params_node = arrow_func.child_by_field_name("parameters")
|
|
549
|
+
params = _node_text(params_node, self._source) if params_node else "()"
|
|
550
|
+
rt = arrow_func.child_by_field_name("return_type")
|
|
551
|
+
rt_text = _node_text(rt, self._source) if rt else ""
|
|
552
|
+
sig = f"const {name} = {params}"
|
|
553
|
+
if rt_text:
|
|
554
|
+
sig += rt_text
|
|
555
|
+
sig += " => ..."
|
|
556
|
+
return sig
|