sentinel-codegraph 0.3.0__py3-none-any.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.
- codegraph/__init__.py +5 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +475 -0
- codegraph/config.py +55 -0
- codegraph/graph_store.py +455 -0
- codegraph/models.py +87 -0
- codegraph/parser/__init__.py +27 -0
- codegraph/parser/base.py +114 -0
- codegraph/parser/lang_go.py +524 -0
- codegraph/parser/lang_python.py +458 -0
- codegraph/parser/lang_typescript.py +554 -0
- codegraph/parser/links.py +314 -0
- codegraph/parser/queries.py +114 -0
- codegraph/parser/raw_core.py +66 -0
- codegraph/parser/rows.py +191 -0
- codegraph/pipeline.py +370 -0
- codegraph/query.py +125 -0
- codegraph/tree.py +230 -0
- codegraph/walk.py +114 -0
- sentinel_codegraph-0.3.0.dist-info/METADATA +251 -0
- sentinel_codegraph-0.3.0.dist-info/RECORD +23 -0
- sentinel_codegraph-0.3.0.dist-info/WHEEL +4 -0
- sentinel_codegraph-0.3.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""TypeScript / JavaScript structural extraction from a raw tree.
|
|
2
|
+
|
|
3
|
+
Collect phase (:func:`collect_ts_file`): walks once, emits defs
|
|
4
|
+
(classes, functions, methods, interfaces, type aliases, arrow-bound
|
|
5
|
+
consts) + imports with alias originals, and buffers bare-name call
|
|
6
|
+
sites unresolved. Call resolution happens later in the link phase
|
|
7
|
+
(:mod:`codegraph.parser.links`). Covers both the ``typescript`` and
|
|
8
|
+
``javascript`` grammars, which share structure shapes (interfaces and
|
|
9
|
+
type aliases only occur in TypeScript).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from dataclasses import field as dc_field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from tree_sitter import Node
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from codegraph.parser.rows import FileRows
|
|
25
|
+
|
|
26
|
+
from codegraph.models import Edge, EdgeKind
|
|
27
|
+
from codegraph.models import Node as GraphNode
|
|
28
|
+
from codegraph.models import NodeKind
|
|
29
|
+
from codegraph.parser.base import ParsedCall, ParsedDefinition, ParsedImport
|
|
30
|
+
from codegraph.parser.raw_core import field_text, node_text, parse, span, walk
|
|
31
|
+
|
|
32
|
+
TYPESCRIPT: str = "typescript"
|
|
33
|
+
JAVASCRIPT: str = "javascript"
|
|
34
|
+
LANGUAGE: str = TYPESCRIPT
|
|
35
|
+
|
|
36
|
+
_MODULE_RE: re.Pattern[str] = re.compile(r"""["']([^"']+)["']""")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _clause_symbols(clause: str) -> list[tuple[str, str]]:
|
|
40
|
+
"""Extract ``(bound, original)`` pairs from the import clause.
|
|
41
|
+
|
|
42
|
+
``{a as b}`` binds ``b`` for the defining-module name ``a``;
|
|
43
|
+
default imports bind with no original (the link phase joins them
|
|
44
|
+
on the bound name); ``* as ns`` binds a namespace (attribute
|
|
45
|
+
calls only, so it never resolves a bare site).
|
|
46
|
+
"""
|
|
47
|
+
symbols: list[tuple[str, str]] = []
|
|
48
|
+
text: str = clause.strip()
|
|
49
|
+
if text.startswith("type "):
|
|
50
|
+
text = text[5:].strip()
|
|
51
|
+
if not text:
|
|
52
|
+
return symbols
|
|
53
|
+
if text.startswith("{"):
|
|
54
|
+
inner: str = text[1:]
|
|
55
|
+
if "}" in inner:
|
|
56
|
+
inner = inner[: inner.index("}")]
|
|
57
|
+
for raw in inner.split(","):
|
|
58
|
+
symbol: str = raw.strip()
|
|
59
|
+
if not symbol or symbol.startswith("//"):
|
|
60
|
+
continue
|
|
61
|
+
if symbol.startswith("type "):
|
|
62
|
+
symbol = symbol[5:].strip()
|
|
63
|
+
original, sep, alias = symbol.partition(" as ")
|
|
64
|
+
original = original.strip()
|
|
65
|
+
alias = alias.strip()
|
|
66
|
+
if not original:
|
|
67
|
+
continue
|
|
68
|
+
bound: str = alias if sep and alias else original
|
|
69
|
+
if bound:
|
|
70
|
+
symbols.append((bound, original))
|
|
71
|
+
elif text.startswith("*"):
|
|
72
|
+
_, sep, alias = text.partition(" as ")
|
|
73
|
+
alias = alias.strip()
|
|
74
|
+
symbols.append((alias if sep and alias else "*", ""))
|
|
75
|
+
else:
|
|
76
|
+
head, sep, tail = text.partition(",")
|
|
77
|
+
default: str = head.strip()
|
|
78
|
+
if default and default not in ("*", "{"):
|
|
79
|
+
symbols.append((default, ""))
|
|
80
|
+
if sep and tail.strip():
|
|
81
|
+
symbols.extend(_clause_symbols(tail.strip()))
|
|
82
|
+
return symbols
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _imports_from_statement(
|
|
86
|
+
source: str, start_line: int, end_line: int
|
|
87
|
+
) -> list[ParsedImport]:
|
|
88
|
+
"""Parse one TS/JS import statement into per-name import rows."""
|
|
89
|
+
match: re.Match[str] | None = _MODULE_RE.search(source)
|
|
90
|
+
if match is None:
|
|
91
|
+
return []
|
|
92
|
+
module: str = match.group(1).strip()
|
|
93
|
+
if not module:
|
|
94
|
+
return []
|
|
95
|
+
head: str = source[: match.start()].strip()
|
|
96
|
+
if head.startswith("import"):
|
|
97
|
+
head = head[6:].strip()
|
|
98
|
+
else:
|
|
99
|
+
return []
|
|
100
|
+
head = re.sub(r"\bfrom\s*$", "", head).strip()
|
|
101
|
+
symbols: list[tuple[str, str]] = _clause_symbols(head) if head else [("*", "")]
|
|
102
|
+
if not symbols:
|
|
103
|
+
symbols = [("*", "")]
|
|
104
|
+
return [
|
|
105
|
+
ParsedImport(
|
|
106
|
+
module=module,
|
|
107
|
+
name=bound,
|
|
108
|
+
start_line=start_line,
|
|
109
|
+
end_line=end_line,
|
|
110
|
+
original="" if bound == original else original,
|
|
111
|
+
)
|
|
112
|
+
for bound, original in symbols
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def extract_imports(root: Node) -> list[ParsedImport]:
|
|
117
|
+
"""Collect imports from ``import_statement`` nodes."""
|
|
118
|
+
found: list[ParsedImport] = []
|
|
119
|
+
for node in walk(root):
|
|
120
|
+
if node.type != "import_statement":
|
|
121
|
+
continue
|
|
122
|
+
text: str = node_text(node)
|
|
123
|
+
if not text.strip().startswith("import"):
|
|
124
|
+
continue
|
|
125
|
+
start, end = span(node)
|
|
126
|
+
found.extend(_imports_from_statement(text, start, end))
|
|
127
|
+
return found
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _bound_variable_name(node: Node) -> str:
|
|
131
|
+
"""Return the variable name binding an arrow/function expression."""
|
|
132
|
+
parent: Node | None = node.parent
|
|
133
|
+
if parent is not None and parent.type == "variable_declarator":
|
|
134
|
+
return field_text(parent, "name")
|
|
135
|
+
return ""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_definitions(root: Node) -> list[ParsedDefinition]:
|
|
139
|
+
"""Collect classes, functions, methods, and arrow-bound consts (preorder)."""
|
|
140
|
+
found: list[ParsedDefinition] = []
|
|
141
|
+
scope: list[tuple[str, str]] = [] # enclosing (kind, name)
|
|
142
|
+
stack: list[tuple[Node, bool]] = [(root, False)] # True = exit, pops scope
|
|
143
|
+
while stack:
|
|
144
|
+
node, exiting = stack.pop()
|
|
145
|
+
if exiting:
|
|
146
|
+
scope.pop()
|
|
147
|
+
continue
|
|
148
|
+
if node.type == "class_declaration":
|
|
149
|
+
kind: str = "class"
|
|
150
|
+
name: str = field_text(node, "name")
|
|
151
|
+
elif node.type == "function_declaration":
|
|
152
|
+
kind = "method" if scope and scope[-1][0] == "class" else "function"
|
|
153
|
+
name = field_text(node, "name")
|
|
154
|
+
elif node.type == "method_definition":
|
|
155
|
+
kind = "method"
|
|
156
|
+
name = field_text(node, "name")
|
|
157
|
+
elif node.type in ("arrow_function", "function_expression"):
|
|
158
|
+
kind = "function"
|
|
159
|
+
name = _bound_variable_name(node)
|
|
160
|
+
else:
|
|
161
|
+
for child in reversed(node.named_children):
|
|
162
|
+
stack.append((child, False))
|
|
163
|
+
continue
|
|
164
|
+
if not name:
|
|
165
|
+
for child in reversed(node.named_children):
|
|
166
|
+
stack.append((child, False))
|
|
167
|
+
continue
|
|
168
|
+
start, end = span(node)
|
|
169
|
+
found.append(
|
|
170
|
+
ParsedDefinition(
|
|
171
|
+
kind=kind,
|
|
172
|
+
name=name,
|
|
173
|
+
start_line=start,
|
|
174
|
+
end_line=end,
|
|
175
|
+
parent=scope[-1][1] if scope else None,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
scope.append(("class" if kind == "class" else "function", name))
|
|
179
|
+
stack.append((node, True))
|
|
180
|
+
for child in reversed(node.named_children):
|
|
181
|
+
stack.append((child, False))
|
|
182
|
+
return found
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(slots=True)
|
|
186
|
+
class _ActiveDefinition:
|
|
187
|
+
"""One live entry on the definition nesting stack."""
|
|
188
|
+
|
|
189
|
+
def_kind: str # "class" | "function"
|
|
190
|
+
def_name: str
|
|
191
|
+
node_id: str
|
|
192
|
+
end_byte: int
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(slots=True)
|
|
196
|
+
class ExtractedFileGraph:
|
|
197
|
+
"""Per-file collect output: rows plus unresolved call sites.
|
|
198
|
+
|
|
199
|
+
``definitions`` maps def name -> node id (first registration wins);
|
|
200
|
+
``imports`` / ``calls`` are the raw IR the link phase resolves.
|
|
201
|
+
No ``calls`` edges are emitted here.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
rel_path: str
|
|
205
|
+
language: str
|
|
206
|
+
total_lines: int
|
|
207
|
+
nodes: dict[str, GraphNode] = dc_field(
|
|
208
|
+
default_factory=lambda: dict[str, GraphNode]()
|
|
209
|
+
)
|
|
210
|
+
edges: list[Edge] = dc_field(default_factory=lambda: list[Edge]())
|
|
211
|
+
definitions: dict[str, str] = dc_field(
|
|
212
|
+
default_factory=lambda: dict[str, str]()
|
|
213
|
+
)
|
|
214
|
+
imports: list[ParsedImport] = dc_field(
|
|
215
|
+
default_factory=lambda: list[ParsedImport]()
|
|
216
|
+
)
|
|
217
|
+
calls: list[ParsedCall] = dc_field(default_factory=lambda: list[ParsedCall]())
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_defined_node_id(
|
|
221
|
+
rel_path: str, def_name: str, start_line: int, end_line: int
|
|
222
|
+
) -> str:
|
|
223
|
+
"""Return the symbolic def id ``base:name:start:end``."""
|
|
224
|
+
return f"{rel_path}:{def_name}:{start_line}:{end_line}"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _pop_finished_definitions(
|
|
228
|
+
active_definition_stack: list[_ActiveDefinition], byte_offset: int
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Pop stack tops whose def ended before byte offset ``byte_offset``."""
|
|
231
|
+
while active_definition_stack and byte_offset > active_definition_stack[-1].end_byte:
|
|
232
|
+
active_definition_stack.pop()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _register_node_once(
|
|
236
|
+
nodes_by_id: dict[str, GraphNode],
|
|
237
|
+
*,
|
|
238
|
+
node_id: str,
|
|
239
|
+
graph_root: str,
|
|
240
|
+
rel_path: str,
|
|
241
|
+
language: str,
|
|
242
|
+
kind: NodeKind,
|
|
243
|
+
name: str,
|
|
244
|
+
start_line: int,
|
|
245
|
+
end_line: int,
|
|
246
|
+
parent_id: str | None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Insert a node row (first registration wins)."""
|
|
249
|
+
if node_id in nodes_by_id:
|
|
250
|
+
return
|
|
251
|
+
nodes_by_id[node_id] = GraphNode(
|
|
252
|
+
id=node_id,
|
|
253
|
+
root=graph_root,
|
|
254
|
+
file_path=rel_path,
|
|
255
|
+
kind=kind,
|
|
256
|
+
name=name,
|
|
257
|
+
language=language,
|
|
258
|
+
start_line=start_line,
|
|
259
|
+
end_line=end_line,
|
|
260
|
+
parent_id=parent_id,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _register_edge_once(
|
|
265
|
+
emitted_edges: list[Edge],
|
|
266
|
+
emitted_edge_keys: set[tuple[str, str, str]],
|
|
267
|
+
*,
|
|
268
|
+
graph_root: str,
|
|
269
|
+
src_id: str,
|
|
270
|
+
dst_id: str,
|
|
271
|
+
kind: EdgeKind,
|
|
272
|
+
target_module: str | None = None,
|
|
273
|
+
site_line: int | None = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Append an edge row, deduped by ``(src, dst, kind)``."""
|
|
276
|
+
key: tuple[str, str, str] = (src_id, dst_id, kind.value)
|
|
277
|
+
if key in emitted_edge_keys:
|
|
278
|
+
return
|
|
279
|
+
emitted_edge_keys.add(key)
|
|
280
|
+
emitted_edges.append(
|
|
281
|
+
Edge(
|
|
282
|
+
id=f"{src_id}::{kind.value}::{dst_id}",
|
|
283
|
+
root=graph_root,
|
|
284
|
+
src_id=src_id,
|
|
285
|
+
dst_id=dst_id,
|
|
286
|
+
kind=kind,
|
|
287
|
+
target_module=target_module,
|
|
288
|
+
site_line=site_line,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _bare_callee_name(syntax_node: Node) -> str | None:
|
|
294
|
+
"""Return the callee name for a bare-name call or construction.
|
|
295
|
+
|
|
296
|
+
Covers ``f()`` (``call_expression``) and ``new C()``
|
|
297
|
+
(``new_expression`` — construction is a call). Member calls
|
|
298
|
+
(``obj.m()``) never match by construction.
|
|
299
|
+
"""
|
|
300
|
+
if syntax_node.type not in ("call_expression", "new_expression"):
|
|
301
|
+
return None
|
|
302
|
+
function_node: Node | None = syntax_node.child_by_field_name("function")
|
|
303
|
+
if function_node is None:
|
|
304
|
+
function_node = syntax_node.child_by_field_name("constructor")
|
|
305
|
+
if function_node is None or function_node.type != "identifier":
|
|
306
|
+
return None
|
|
307
|
+
return node_text(function_node) or None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def collect_ts_file(
|
|
311
|
+
rel_path: str,
|
|
312
|
+
syntax_root: Node,
|
|
313
|
+
graph_root: str,
|
|
314
|
+
total_lines: int,
|
|
315
|
+
) -> ExtractedFileGraph:
|
|
316
|
+
"""Walk one TS/JS file once, collecting defs + imports + call sites.
|
|
317
|
+
|
|
318
|
+
- defs (classes, functions, methods, interfaces, type aliases,
|
|
319
|
+
arrow-bound consts; ``export`` wrappers are transparent since the
|
|
320
|
+
walk visits nested nodes) → ``Node`` + ``Edge(CONTAINS)`` with
|
|
321
|
+
``base:name:start:end`` ids.
|
|
322
|
+
- imports → ``Node(IMPORT)`` + ``Edge(IMPORTS)`` (existing
|
|
323
|
+
statement rules, now keeping alias originals).
|
|
324
|
+
- bare calls / constructions with a live enclosing def → buffered
|
|
325
|
+
as ``ParsedCall(caller=name, callee, site_line)`` for the link
|
|
326
|
+
phase. Module-level sites are dropped here.
|
|
327
|
+
|
|
328
|
+
Pure given the parsed tree: no module resolution, no ``calls``
|
|
329
|
+
edges.
|
|
330
|
+
"""
|
|
331
|
+
suffix: str = Path(rel_path).suffix.lower()
|
|
332
|
+
language: str = JAVASCRIPT if suffix in (".js", ".jsx") else TYPESCRIPT
|
|
333
|
+
extracted_file = ExtractedFileGraph(
|
|
334
|
+
rel_path=rel_path, language=language, total_lines=total_lines
|
|
335
|
+
)
|
|
336
|
+
_register_node_once(
|
|
337
|
+
extracted_file.nodes,
|
|
338
|
+
node_id=rel_path,
|
|
339
|
+
graph_root=graph_root,
|
|
340
|
+
rel_path=rel_path,
|
|
341
|
+
language=language,
|
|
342
|
+
kind=NodeKind.FILE,
|
|
343
|
+
name=Path(rel_path).name,
|
|
344
|
+
start_line=1,
|
|
345
|
+
end_line=total_lines,
|
|
346
|
+
parent_id=None,
|
|
347
|
+
)
|
|
348
|
+
active_definition_stack: list[_ActiveDefinition] = []
|
|
349
|
+
emitted_edge_keys: set[tuple[str, str, str]] = set()
|
|
350
|
+
first_def_node_id_by_name: dict[str, str] = {}
|
|
351
|
+
|
|
352
|
+
for syntax_node in walk(syntax_root):
|
|
353
|
+
if syntax_node.type in (
|
|
354
|
+
"class_declaration",
|
|
355
|
+
"function_declaration",
|
|
356
|
+
"method_definition",
|
|
357
|
+
"method_signature",
|
|
358
|
+
"interface_declaration",
|
|
359
|
+
"type_alias_declaration",
|
|
360
|
+
"arrow_function",
|
|
361
|
+
"function_expression",
|
|
362
|
+
):
|
|
363
|
+
_pop_finished_definitions(
|
|
364
|
+
active_definition_stack, syntax_node.start_byte
|
|
365
|
+
)
|
|
366
|
+
if syntax_node.type == "class_declaration":
|
|
367
|
+
def_name: str = field_text(syntax_node, "name")
|
|
368
|
+
graph_node_kind: NodeKind = NodeKind.CLASS
|
|
369
|
+
stack_kind: str = "class"
|
|
370
|
+
elif syntax_node.type == "interface_declaration":
|
|
371
|
+
def_name = field_text(syntax_node, "name")
|
|
372
|
+
graph_node_kind = NodeKind.INTERFACE
|
|
373
|
+
stack_kind = "class"
|
|
374
|
+
elif syntax_node.type == "type_alias_declaration":
|
|
375
|
+
def_name = field_text(syntax_node, "name")
|
|
376
|
+
graph_node_kind = NodeKind.TYPE
|
|
377
|
+
stack_kind = "function"
|
|
378
|
+
elif syntax_node.type == "method_signature":
|
|
379
|
+
def_name = field_text(syntax_node, "name")
|
|
380
|
+
graph_node_kind = NodeKind.METHOD
|
|
381
|
+
stack_kind = "function"
|
|
382
|
+
elif syntax_node.type == "function_declaration":
|
|
383
|
+
def_name = field_text(syntax_node, "name")
|
|
384
|
+
is_method: bool = bool(
|
|
385
|
+
active_definition_stack
|
|
386
|
+
and active_definition_stack[-1].def_kind == "class"
|
|
387
|
+
)
|
|
388
|
+
graph_node_kind = NodeKind.METHOD if is_method else NodeKind.FUNCTION
|
|
389
|
+
stack_kind = "function"
|
|
390
|
+
elif syntax_node.type == "method_definition":
|
|
391
|
+
def_name = field_text(syntax_node, "name")
|
|
392
|
+
graph_node_kind = NodeKind.METHOD
|
|
393
|
+
stack_kind = "function"
|
|
394
|
+
else:
|
|
395
|
+
def_name = _bound_variable_name(syntax_node)
|
|
396
|
+
graph_node_kind = NodeKind.FUNCTION
|
|
397
|
+
stack_kind = "function"
|
|
398
|
+
if not def_name:
|
|
399
|
+
continue
|
|
400
|
+
start_line, end_line = span(syntax_node)
|
|
401
|
+
definition_node_id: str = build_defined_node_id(
|
|
402
|
+
rel_path, def_name, start_line, end_line
|
|
403
|
+
)
|
|
404
|
+
parent_node_id: str = (
|
|
405
|
+
active_definition_stack[-1].node_id
|
|
406
|
+
if active_definition_stack
|
|
407
|
+
else rel_path
|
|
408
|
+
)
|
|
409
|
+
_register_node_once(
|
|
410
|
+
extracted_file.nodes,
|
|
411
|
+
node_id=definition_node_id,
|
|
412
|
+
graph_root=graph_root,
|
|
413
|
+
rel_path=rel_path,
|
|
414
|
+
language=language,
|
|
415
|
+
kind=graph_node_kind,
|
|
416
|
+
name=def_name,
|
|
417
|
+
start_line=start_line,
|
|
418
|
+
end_line=end_line,
|
|
419
|
+
parent_id=parent_node_id,
|
|
420
|
+
)
|
|
421
|
+
_register_edge_once(
|
|
422
|
+
extracted_file.edges,
|
|
423
|
+
emitted_edge_keys,
|
|
424
|
+
graph_root=graph_root,
|
|
425
|
+
src_id=parent_node_id,
|
|
426
|
+
dst_id=definition_node_id,
|
|
427
|
+
kind=EdgeKind.CONTAINS,
|
|
428
|
+
)
|
|
429
|
+
first_def_node_id_by_name.setdefault(def_name, definition_node_id)
|
|
430
|
+
active_definition_stack.append(
|
|
431
|
+
_ActiveDefinition(
|
|
432
|
+
def_kind=stack_kind,
|
|
433
|
+
def_name=def_name,
|
|
434
|
+
node_id=definition_node_id,
|
|
435
|
+
end_byte=syntax_node.end_byte,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
elif syntax_node.type == "import_statement":
|
|
439
|
+
import_statement_text: str = node_text(syntax_node)
|
|
440
|
+
if not import_statement_text.strip().startswith("import"):
|
|
441
|
+
continue
|
|
442
|
+
start_line, end_line = span(syntax_node)
|
|
443
|
+
for parsed_import in _imports_from_statement(
|
|
444
|
+
import_statement_text, start_line, end_line
|
|
445
|
+
):
|
|
446
|
+
import_node_id: str = (
|
|
447
|
+
f"{rel_path}:import:{parsed_import.name}:{start_line}"
|
|
448
|
+
)
|
|
449
|
+
_register_node_once(
|
|
450
|
+
extracted_file.nodes,
|
|
451
|
+
node_id=import_node_id,
|
|
452
|
+
graph_root=graph_root,
|
|
453
|
+
rel_path=rel_path,
|
|
454
|
+
language=language,
|
|
455
|
+
kind=NodeKind.IMPORT,
|
|
456
|
+
name=parsed_import.name,
|
|
457
|
+
start_line=start_line,
|
|
458
|
+
end_line=end_line,
|
|
459
|
+
parent_id=rel_path,
|
|
460
|
+
)
|
|
461
|
+
_register_edge_once(
|
|
462
|
+
extracted_file.edges,
|
|
463
|
+
emitted_edge_keys,
|
|
464
|
+
graph_root=graph_root,
|
|
465
|
+
src_id=rel_path,
|
|
466
|
+
dst_id=import_node_id,
|
|
467
|
+
kind=EdgeKind.IMPORTS,
|
|
468
|
+
target_module=parsed_import.module,
|
|
469
|
+
)
|
|
470
|
+
extracted_file.imports.append(parsed_import)
|
|
471
|
+
else:
|
|
472
|
+
callee_name: str | None = _bare_callee_name(syntax_node)
|
|
473
|
+
if callee_name is None:
|
|
474
|
+
continue
|
|
475
|
+
_pop_finished_definitions(
|
|
476
|
+
active_definition_stack, syntax_node.start_byte
|
|
477
|
+
)
|
|
478
|
+
if not active_definition_stack:
|
|
479
|
+
continue # module-level call: drop
|
|
480
|
+
call_site_line: int = span(syntax_node)[0]
|
|
481
|
+
extracted_file.calls.append(
|
|
482
|
+
ParsedCall(
|
|
483
|
+
caller=active_definition_stack[-1].def_name,
|
|
484
|
+
callee=callee_name,
|
|
485
|
+
site_line=call_site_line,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
extracted_file.definitions.update(first_def_node_id_by_name)
|
|
490
|
+
return extracted_file
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def build_ts_file_rows(
|
|
494
|
+
root: str,
|
|
495
|
+
rel_path: str,
|
|
496
|
+
source_text: str,
|
|
497
|
+
_import_index: Mapping[str, str],
|
|
498
|
+
) -> FileRows:
|
|
499
|
+
"""Build one TS/JS file's rows via the collect phase.
|
|
500
|
+
|
|
501
|
+
Nodes + ``contains`` / ``imports`` edges come out of
|
|
502
|
+
:func:`collect_ts_file` directly; ``calls`` edges are never
|
|
503
|
+
emitted here — ``rows.calls`` carries the buffered call sites for
|
|
504
|
+
:func:`codegraph.parser.links.resolve_call_edges`.
|
|
505
|
+
``_import_index`` is accepted for builder-signature uniformity and
|
|
506
|
+
ignored: the link phase resolves modules from the full file set.
|
|
507
|
+
"""
|
|
508
|
+
from codegraph.parser.rows import FileRows
|
|
509
|
+
|
|
510
|
+
suffix: str = Path(rel_path).suffix.lower()
|
|
511
|
+
language: str = JAVASCRIPT if suffix in (".js", ".jsx") else TYPESCRIPT
|
|
512
|
+
total_lines: int = max(source_text.count("\n") + 1, 1)
|
|
513
|
+
rows = FileRows(rel_path=rel_path, language=language)
|
|
514
|
+
if not source_text.strip():
|
|
515
|
+
rows.nodes.append(
|
|
516
|
+
GraphNode(
|
|
517
|
+
id=rel_path,
|
|
518
|
+
root=root,
|
|
519
|
+
file_path=rel_path,
|
|
520
|
+
kind=NodeKind.FILE,
|
|
521
|
+
name=Path(rel_path).name,
|
|
522
|
+
language=language,
|
|
523
|
+
start_line=1,
|
|
524
|
+
end_line=total_lines,
|
|
525
|
+
parent_id=None,
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
return rows
|
|
529
|
+
grammar: str = language
|
|
530
|
+
tree = parse(grammar, source_text.encode("utf-8"))
|
|
531
|
+
out: ExtractedFileGraph = collect_ts_file(
|
|
532
|
+
rel_path, tree.root_node, root, total_lines
|
|
533
|
+
)
|
|
534
|
+
rows.nodes.extend(out.nodes.values())
|
|
535
|
+
rows.edges.extend(out.edges)
|
|
536
|
+
rows.definitions.update(out.definitions)
|
|
537
|
+
for parsed_import in out.imports:
|
|
538
|
+
rows.imports.setdefault(parsed_import.name, parsed_import.module)
|
|
539
|
+
rows.import_details.extend(out.imports)
|
|
540
|
+
rows.calls.extend(out.calls)
|
|
541
|
+
return rows
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
__all__ = [
|
|
545
|
+
"ExtractedFileGraph",
|
|
546
|
+
"JAVASCRIPT",
|
|
547
|
+
"LANGUAGE",
|
|
548
|
+
"TYPESCRIPT",
|
|
549
|
+
"build_defined_node_id",
|
|
550
|
+
"build_ts_file_rows",
|
|
551
|
+
"collect_ts_file",
|
|
552
|
+
"extract_definitions",
|
|
553
|
+
"extract_imports",
|
|
554
|
+
]
|