python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. delphi_lsp/__init__.py +184 -0
  2. delphi_lsp/_version.py +1 -0
  3. delphi_lsp/agent_cache.py +1574 -0
  4. delphi_lsp/agent_cli.py +902 -0
  5. delphi_lsp/agent_context.py +3907 -0
  6. delphi_lsp/agent_cpg.py +349 -0
  7. delphi_lsp/agent_cpg_builder.py +666 -0
  8. delphi_lsp/agent_layers.py +1410 -0
  9. delphi_lsp/agent_metrics.py +526 -0
  10. delphi_lsp/agent_protocol.py +445 -0
  11. delphi_lsp/agent_relations.py +1881 -0
  12. delphi_lsp/agent_templates.py +614 -0
  13. delphi_lsp/agent_wiki.py +2853 -0
  14. delphi_lsp/agent_workspace.py +860 -0
  15. delphi_lsp/ast_serialize.py +126 -0
  16. delphi_lsp/binary.py +250 -0
  17. delphi_lsp/comment_builder.py +28 -0
  18. delphi_lsp/consts.py +345 -0
  19. delphi_lsp/delphi_lsp_native.pyd +0 -0
  20. delphi_lsp/delphiast_lexer.py +399 -0
  21. delphi_lsp/delphiast_parser.py +2189 -0
  22. delphi_lsp/delphiast_tokens.py +41 -0
  23. delphi_lsp/grammar.py +459 -0
  24. delphi_lsp/incremental.py +1139 -0
  25. delphi_lsp/lark_builder.py +2684 -0
  26. delphi_lsp/lark_tokens.py +236 -0
  27. delphi_lsp/lexical_scanner.py +30 -0
  28. delphi_lsp/lsp_server.py +6698 -0
  29. delphi_lsp/metrics.py +1452 -0
  30. delphi_lsp/native_bridge.py +300 -0
  31. delphi_lsp/navigation_cache.py +189 -0
  32. delphi_lsp/nodes.py +462 -0
  33. delphi_lsp/parallel_outline.py +662 -0
  34. delphi_lsp/parse_artifacts.py +969 -0
  35. delphi_lsp/parser.py +342 -0
  36. delphi_lsp/parser_backend.py +139 -0
  37. delphi_lsp/preprocessor.py +1122 -0
  38. delphi_lsp/progress.py +26 -0
  39. delphi_lsp/project_config.py +285 -0
  40. delphi_lsp/project_discovery.py +932 -0
  41. delphi_lsp/project_indexer.py +470 -0
  42. delphi_lsp/py.typed +1 -0
  43. delphi_lsp/response_cache.py +465 -0
  44. delphi_lsp/semantic.py +394 -0
  45. delphi_lsp/semantic_builder.py +2302 -0
  46. delphi_lsp/source_reader.py +17 -0
  47. delphi_lsp/wiki_external_sort.py +392 -0
  48. delphi_lsp/wiki_memory.py +548 -0
  49. delphi_lsp/workspace.py +83 -0
  50. delphi_lsp/writer.py +73 -0
  51. python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
  52. python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
  53. python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
  54. python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
  55. python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
  56. python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
@@ -0,0 +1,666 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from dataclasses import dataclass
5
+
6
+ from .agent_cpg import CpgEdge, CpgNode, CpgProblem, CpgSubgraph, CpgTarget
7
+ from .consts import AttributeName, SyntaxNodeType
8
+ from .nodes import CompoundSyntaxNode, SyntaxNode
9
+
10
+ _ROUTINE_KINDS = frozenset(
11
+ {"method", "function", "procedure", "constructor", "destructor"}
12
+ )
13
+ _TYPE_KINDS = frozenset({"type", "class", "record", "interface", "enum"})
14
+ _CONTROL_TYPES = frozenset(
15
+ {
16
+ SyntaxNodeType.ntCase,
17
+ SyntaxNodeType.ntCaseElse,
18
+ SyntaxNodeType.ntExcept,
19
+ SyntaxNodeType.ntExceptionHandler,
20
+ SyntaxNodeType.ntFinally,
21
+ SyntaxNodeType.ntFor,
22
+ SyntaxNodeType.ntGoto,
23
+ SyntaxNodeType.ntIf,
24
+ SyntaxNodeType.ntRaise,
25
+ SyntaxNodeType.ntRepeat,
26
+ SyntaxNodeType.ntTry,
27
+ SyntaxNodeType.ntWhile,
28
+ SyntaxNodeType.ntWith,
29
+ }
30
+ )
31
+ _STATEMENT_TYPES = frozenset(
32
+ {
33
+ SyntaxNodeType.ntAssign,
34
+ SyntaxNodeType.ntEmptyStatement,
35
+ SyntaxNodeType.ntStatement,
36
+ SyntaxNodeType.ntStatements,
37
+ }
38
+ )
39
+ _SYMBOL_TYPES = frozenset(
40
+ {
41
+ SyntaxNodeType.ntConstant,
42
+ SyntaxNodeType.ntField,
43
+ SyntaxNodeType.ntParameter,
44
+ SyntaxNodeType.ntProperty,
45
+ SyntaxNodeType.ntTypeDecl,
46
+ SyntaxNodeType.ntVariable,
47
+ }
48
+ )
49
+
50
+
51
+ def build_cpg_subgraph(
52
+ *,
53
+ target: CpgTarget,
54
+ syntax_root: SyntaxNode,
55
+ candidates: Mapping[str, Sequence[CpgTarget]],
56
+ graph: str,
57
+ direction: str,
58
+ depth: int,
59
+ record_limit: int = 50_000,
60
+ ) -> CpgSubgraph:
61
+ selected = _select_target_syntax(target, syntax_root)
62
+ root_node = _target_node(target)
63
+ nodes = [root_node]
64
+ edges: list[CpgEdge] = []
65
+ problems: list[CpgProblem] = []
66
+ syntax_nodes: dict[int, CpgNode] = {}
67
+ unresolved = 0
68
+ if selected is None:
69
+ problems.append(
70
+ CpgProblem(
71
+ kind="target_syntax_missing",
72
+ message="Target syntax could not be located.",
73
+ path=target.path,
74
+ line=target.line,
75
+ column=target.column,
76
+ )
77
+ )
78
+ else:
79
+ syntax_nodes[id(selected)] = root_node
80
+ stack: list[tuple[SyntaxNode, tuple[int, ...], str]] = [
81
+ (child, (index,), root_node.node_id)
82
+ for index, child in reversed(tuple(enumerate(selected.child_nodes)))
83
+ ]
84
+ while stack and len(nodes) + len(edges) < record_limit:
85
+ current, syntax_path, parent_id = stack.pop()
86
+ node = _syntax_node(target, current, syntax_path)
87
+ syntax_nodes[id(current)] = node
88
+ nodes.append(node)
89
+ edges.append(
90
+ CpgEdge.create(
91
+ label="AST",
92
+ source=parent_id,
93
+ target=node.node_id,
94
+ identity=syntax_path,
95
+ properties={"order": syntax_path[-1]},
96
+ )
97
+ )
98
+ for index, child in reversed(tuple(enumerate(current.child_nodes))):
99
+ stack.append((child, (*syntax_path, index), node.node_id))
100
+ if stack:
101
+ problems.append(
102
+ CpgProblem(
103
+ kind="record_limit",
104
+ message="CPG record limit was reached.",
105
+ path=target.path,
106
+ line=target.line,
107
+ column=target.column,
108
+ )
109
+ )
110
+ if target.kind in _ROUTINE_KINDS:
111
+ cfg_edges: list[CpgEdge] = []
112
+ if graph in {"cfg", "dfg", "full"}:
113
+ cfg_nodes, cfg_edges = _build_cfg(
114
+ target,
115
+ selected,
116
+ syntax_nodes,
117
+ )
118
+ nodes.extend(cfg_nodes)
119
+ edges.extend(cfg_edges)
120
+ if graph in {"dfg", "full"}:
121
+ edges.extend(
122
+ _build_data_flow(
123
+ selected,
124
+ syntax_nodes,
125
+ cfg_edges,
126
+ root_node,
127
+ )
128
+ )
129
+ if graph in {"call", "full"}:
130
+ call_nodes, call_edges, unresolved_calls = _build_calls(
131
+ target,
132
+ selected,
133
+ syntax_nodes,
134
+ candidates,
135
+ root_node,
136
+ )
137
+ nodes.extend(call_nodes)
138
+ edges.extend(call_edges)
139
+ unresolved += unresolved_calls
140
+ complete = CpgSubgraph.create(
141
+ target_id=target.target_id,
142
+ graph="full",
143
+ root_node_id=root_node.node_id,
144
+ nodes=nodes,
145
+ edges=edges,
146
+ problems=problems,
147
+ truncated=bool(stack) if selected is not None else False,
148
+ unresolved=unresolved,
149
+ record_limit=record_limit,
150
+ )
151
+ return complete.select(
152
+ graph=graph,
153
+ direction=direction,
154
+ depth=depth,
155
+ )
156
+
157
+
158
+ def _target_node(target: CpgTarget) -> CpgNode:
159
+ label = (
160
+ "UNIT"
161
+ if target.kind == "unit"
162
+ else "ROUTINE"
163
+ if target.kind in _ROUTINE_KINDS
164
+ else "TYPE"
165
+ if target.kind in _TYPE_KINDS
166
+ else "SYMBOL"
167
+ )
168
+ properties: dict[str, object] = {
169
+ "target_id": target.target_id,
170
+ "unit_id": target.unit_id,
171
+ "name": target.name,
172
+ "qualified_name": target.qualified_name,
173
+ "kind": target.kind,
174
+ "path": target.path,
175
+ "line": target.line,
176
+ "column": target.column,
177
+ "visibility": target.visibility,
178
+ }
179
+ if target.end_line:
180
+ properties["end_line"] = target.end_line
181
+ properties["end_column"] = target.end_column
182
+ if target.type_name:
183
+ properties["type"] = target.type_name
184
+ if target.owner:
185
+ properties["owner"] = target.owner
186
+ return CpgNode.create(
187
+ label=label,
188
+ identity=("target", target.target_id),
189
+ properties=properties,
190
+ )
191
+
192
+
193
+ def _select_target_syntax(
194
+ target: CpgTarget,
195
+ root: SyntaxNode,
196
+ ) -> SyntaxNode | None:
197
+ if target.kind == "unit":
198
+ return root
199
+ candidates: list[tuple[int, int, SyntaxNode]] = []
200
+ stack = [root]
201
+ normalized_name = target.name.casefold()
202
+ normalized_qualified = target.qualified_name.casefold()
203
+ while stack:
204
+ current = stack.pop()
205
+ stack.extend(reversed(current.child_nodes))
206
+ if target.kind in _ROUTINE_KINDS and current.typ != SyntaxNodeType.ntMethod:
207
+ continue
208
+ if target.kind in _TYPE_KINDS and current.typ != SyntaxNodeType.ntTypeDecl:
209
+ continue
210
+ name = current.get_attribute(AttributeName.anName).casefold()
211
+ if not name:
212
+ continue
213
+ name_matches = (
214
+ name == normalized_name
215
+ or name == normalized_qualified
216
+ or name.rsplit(".", 1)[-1] == normalized_name
217
+ )
218
+ if not name_matches:
219
+ continue
220
+ candidates.append(
221
+ (
222
+ abs(max(1, current.line) - max(1, target.line)),
223
+ -len(current.child_nodes),
224
+ current,
225
+ )
226
+ )
227
+ if not candidates:
228
+ return None
229
+ candidates.sort(key=lambda item: (item[0], item[1], item[2].line, item[2].col))
230
+ return candidates[0][2]
231
+
232
+
233
+ def _syntax_node(
234
+ target: CpgTarget,
235
+ syntax: SyntaxNode,
236
+ syntax_path: tuple[int, ...],
237
+ ) -> CpgNode:
238
+ properties: dict[str, object] = {
239
+ "path": target.path,
240
+ "line": max(1, syntax.line),
241
+ "column": max(1, syntax.col),
242
+ "syntax_type": syntax.typ.name,
243
+ }
244
+ if isinstance(syntax, CompoundSyntaxNode) and syntax.end_line > 0:
245
+ properties["end_line"] = syntax.end_line
246
+ properties["end_column"] = syntax.end_col
247
+ name = syntax.get_attribute(AttributeName.anName)
248
+ if name:
249
+ properties["name"] = name
250
+ return CpgNode.create(
251
+ label=_syntax_label(syntax.typ),
252
+ identity=(
253
+ target.path,
254
+ target.target_id,
255
+ syntax_path,
256
+ syntax.typ.name,
257
+ syntax.line,
258
+ syntax.col,
259
+ ),
260
+ properties=properties,
261
+ )
262
+
263
+
264
+ def _syntax_label(typ: SyntaxNodeType) -> str:
265
+ if typ in _CONTROL_TYPES:
266
+ return "CONTROL"
267
+ if typ == SyntaxNodeType.ntCall:
268
+ return "CALL"
269
+ if typ in {SyntaxNodeType.ntIdentifier, SyntaxNodeType.ntName}:
270
+ return "IDENTIFIER"
271
+ if typ == SyntaxNodeType.ntLiteral:
272
+ return "LITERAL"
273
+ if typ in _STATEMENT_TYPES:
274
+ return "STATEMENT"
275
+ if typ in _SYMBOL_TYPES:
276
+ return "SYMBOL"
277
+ return "EXPRESSION"
278
+
279
+
280
+ @dataclass(frozen=True, slots=True)
281
+ class _CfgExit:
282
+ node_id: str
283
+ kind: str = "next"
284
+
285
+
286
+ @dataclass(frozen=True, slots=True)
287
+ class _CfgFragment:
288
+ entries: tuple[str, ...]
289
+ exits: tuple[_CfgExit, ...]
290
+
291
+
292
+ def _build_cfg(
293
+ target: CpgTarget,
294
+ selected: SyntaxNode,
295
+ syntax_nodes: Mapping[int, CpgNode],
296
+ ) -> tuple[list[CpgNode], list[CpgEdge]]:
297
+ entry = CpgNode.create(
298
+ label="ENTRY",
299
+ identity=(target.target_id, "entry"),
300
+ properties={
301
+ "path": target.path,
302
+ "line": target.line,
303
+ "column": target.column,
304
+ },
305
+ )
306
+ exit_node = CpgNode.create(
307
+ label="EXIT",
308
+ identity=(target.target_id, "exit"),
309
+ properties={
310
+ "path": target.path,
311
+ "line": target.end_line or target.line,
312
+ "column": target.end_column or target.column,
313
+ },
314
+ )
315
+ edges: list[CpgEdge] = []
316
+ root_node = syntax_nodes[id(selected)]
317
+ _cfg_edge(edges, root_node.node_id, entry.node_id, "entry")
318
+ statements = _first_descendant(selected, SyntaxNodeType.ntStatements)
319
+ if statements is None:
320
+ _cfg_edge(edges, entry.node_id, exit_node.node_id, "empty")
321
+ return [entry, exit_node], edges
322
+ fragment = _cfg_fragment(statements, syntax_nodes, edges)
323
+ if not fragment.entries:
324
+ _cfg_edge(edges, entry.node_id, exit_node.node_id, "empty")
325
+ else:
326
+ for fragment_entry in fragment.entries:
327
+ _cfg_edge(edges, entry.node_id, fragment_entry, "next")
328
+ for pending in fragment.exits:
329
+ _cfg_edge(edges, pending.node_id, exit_node.node_id, pending.kind)
330
+ return [entry, exit_node], edges
331
+
332
+
333
+ def _cfg_fragment(
334
+ syntax: SyntaxNode,
335
+ syntax_nodes: Mapping[int, CpgNode],
336
+ edges: list[CpgEdge],
337
+ ) -> _CfgFragment:
338
+ if syntax.typ == SyntaxNodeType.ntStatements:
339
+ fragments = [
340
+ _cfg_fragment(child, syntax_nodes, edges)
341
+ for child in syntax.child_nodes
342
+ if _is_executable(child)
343
+ ]
344
+ fragments = [fragment for fragment in fragments if fragment.entries]
345
+ if not fragments:
346
+ return _CfgFragment((), ())
347
+ for left, right in zip(fragments, fragments[1:]):
348
+ for pending in left.exits:
349
+ for entry in right.entries:
350
+ _cfg_edge(edges, pending.node_id, entry, pending.kind)
351
+ return _CfgFragment(fragments[0].entries, fragments[-1].exits)
352
+
353
+ node = syntax_nodes.get(id(syntax))
354
+ if node is None:
355
+ return _CfgFragment((), ())
356
+
357
+ if syntax.typ == SyntaxNodeType.ntIf:
358
+ branches = _branch_nodes(syntax)
359
+ if not branches:
360
+ return _CfgFragment((node.node_id,), (_CfgExit(node.node_id, "false"),))
361
+ then_fragment = _cfg_fragment(branches[0], syntax_nodes, edges)
362
+ if then_fragment.entries:
363
+ for entry in then_fragment.entries:
364
+ _cfg_edge(edges, node.node_id, entry, "true")
365
+ exits = list(then_fragment.exits)
366
+ if len(branches) > 1:
367
+ else_fragment = _cfg_fragment(branches[1], syntax_nodes, edges)
368
+ for entry in else_fragment.entries:
369
+ _cfg_edge(edges, node.node_id, entry, "false")
370
+ exits.extend(else_fragment.exits)
371
+ else:
372
+ exits.append(_CfgExit(node.node_id, "false"))
373
+ return _CfgFragment((node.node_id,), tuple(exits))
374
+
375
+ if syntax.typ in {SyntaxNodeType.ntWhile, SyntaxNodeType.ntFor}:
376
+ branches = _branch_nodes(syntax)
377
+ body = branches[-1] if branches else None
378
+ if body is not None:
379
+ body_fragment = _cfg_fragment(body, syntax_nodes, edges)
380
+ for entry in body_fragment.entries:
381
+ _cfg_edge(edges, node.node_id, entry, "true")
382
+ for pending in body_fragment.exits:
383
+ _cfg_edge(edges, pending.node_id, node.node_id, "loop_back")
384
+ return _CfgFragment(
385
+ (node.node_id,),
386
+ (_CfgExit(node.node_id, "false"),),
387
+ )
388
+
389
+ if syntax.typ == SyntaxNodeType.ntRepeat:
390
+ branches = _branch_nodes(syntax)
391
+ if not branches:
392
+ return _CfgFragment((node.node_id,), (_CfgExit(node.node_id, "true"),))
393
+ body_fragments = [
394
+ _cfg_fragment(branch, syntax_nodes, edges)
395
+ for branch in branches
396
+ ]
397
+ body_fragments = [fragment for fragment in body_fragments if fragment.entries]
398
+ if not body_fragments:
399
+ return _CfgFragment((node.node_id,), (_CfgExit(node.node_id, "true"),))
400
+ for left, right in zip(body_fragments, body_fragments[1:]):
401
+ for pending in left.exits:
402
+ for entry in right.entries:
403
+ _cfg_edge(edges, pending.node_id, entry, pending.kind)
404
+ for pending in body_fragments[-1].exits:
405
+ _cfg_edge(edges, pending.node_id, node.node_id, "loop_test")
406
+ for entry in body_fragments[0].entries:
407
+ _cfg_edge(edges, node.node_id, entry, "loop_back")
408
+ return _CfgFragment(
409
+ body_fragments[0].entries,
410
+ (_CfgExit(node.node_id, "true"),),
411
+ )
412
+
413
+ if syntax.typ in {
414
+ SyntaxNodeType.ntCase,
415
+ SyntaxNodeType.ntTry,
416
+ SyntaxNodeType.ntExcept,
417
+ SyntaxNodeType.ntFinally,
418
+ }:
419
+ branches = _branch_nodes(syntax)
420
+ exits: list[_CfgExit] = []
421
+ for branch in branches:
422
+ fragment = _cfg_fragment(branch, syntax_nodes, edges)
423
+ for entry in fragment.entries:
424
+ _cfg_edge(edges, node.node_id, entry, "branch")
425
+ exits.extend(fragment.exits)
426
+ if not exits:
427
+ exits.append(_CfgExit(node.node_id))
428
+ return _CfgFragment((node.node_id,), tuple(exits))
429
+
430
+ if syntax.typ in {SyntaxNodeType.ntRaise, SyntaxNodeType.ntGoto}:
431
+ return _CfgFragment((node.node_id,), (_CfgExit(node.node_id, "terminal"),))
432
+
433
+ return _CfgFragment((node.node_id,), (_CfgExit(node.node_id),))
434
+
435
+
436
+ def _branch_nodes(syntax: SyntaxNode) -> list[SyntaxNode]:
437
+ result: list[SyntaxNode] = []
438
+ for child in syntax.child_nodes:
439
+ if child.typ == SyntaxNodeType.ntElse:
440
+ result.extend(candidate for candidate in child.child_nodes if _is_executable(candidate))
441
+ elif _is_executable(child):
442
+ result.append(child)
443
+ return result
444
+
445
+
446
+ def _is_executable(syntax: SyntaxNode) -> bool:
447
+ return (
448
+ syntax.typ in _STATEMENT_TYPES
449
+ or syntax.typ in _CONTROL_TYPES
450
+ or syntax.typ == SyntaxNodeType.ntCall
451
+ )
452
+
453
+
454
+ def _first_descendant(
455
+ root: SyntaxNode,
456
+ typ: SyntaxNodeType,
457
+ ) -> SyntaxNode | None:
458
+ stack = list(reversed(root.child_nodes))
459
+ while stack:
460
+ current = stack.pop()
461
+ if current.typ == typ:
462
+ return current
463
+ stack.extend(reversed(current.child_nodes))
464
+ return None
465
+
466
+
467
+ def _cfg_edge(
468
+ edges: list[CpgEdge],
469
+ source: str,
470
+ target: str,
471
+ kind: str,
472
+ ) -> None:
473
+ edges.append(
474
+ CpgEdge.create(
475
+ label="CFG",
476
+ source=source,
477
+ target=target,
478
+ properties={"kind": kind},
479
+ )
480
+ )
481
+
482
+
483
+ def _build_data_flow(
484
+ selected: SyntaxNode,
485
+ syntax_nodes: Mapping[int, CpgNode],
486
+ cfg_edges: Sequence[CpgEdge],
487
+ root_node: CpgNode,
488
+ ) -> list[CpgEdge]:
489
+ definitions: dict[str, list[tuple[str, str]]] = {}
490
+ uses: dict[str, list[tuple[str, str]]] = {}
491
+ stack = [selected]
492
+ while stack:
493
+ current = stack.pop()
494
+ stack.extend(reversed(current.child_nodes))
495
+ owner = syntax_nodes.get(id(current))
496
+ if owner is None or not _is_executable(current):
497
+ continue
498
+ if current.typ == SyntaxNodeType.ntAssign:
499
+ lhs = current.find_node(SyntaxNodeType.ntLHS)
500
+ rhs = current.find_node(SyntaxNodeType.ntRHS)
501
+ definitions[owner.node_id] = _named_identifiers(lhs, syntax_nodes)
502
+ uses[owner.node_id] = _named_identifiers(rhs, syntax_nodes)
503
+ elif current.typ == SyntaxNodeType.ntCall:
504
+ identifiers = _named_identifiers(current, syntax_nodes)
505
+ uses[owner.node_id] = identifiers[1:]
506
+ elif current.typ in _CONTROL_TYPES:
507
+ identifiers: list[tuple[str, str]] = []
508
+ for child in current.child_nodes:
509
+ if not _is_executable(child):
510
+ identifiers.extend(_named_identifiers(child, syntax_nodes))
511
+ uses[owner.node_id] = identifiers
512
+
513
+ result: list[CpgEdge] = []
514
+ for owner_id, records in definitions.items():
515
+ for name, identifier_id in records:
516
+ properties = {"name": name, "owner_node_id": owner_id}
517
+ result.append(
518
+ CpgEdge.create(
519
+ label="DEF",
520
+ source=root_node.node_id,
521
+ target=identifier_id,
522
+ properties=properties,
523
+ )
524
+ )
525
+ for owner_id, records in uses.items():
526
+ for name, identifier_id in records:
527
+ properties = {"name": name, "owner_node_id": owner_id}
528
+ result.append(
529
+ CpgEdge.create(
530
+ label="USE",
531
+ source=root_node.node_id,
532
+ target=identifier_id,
533
+ properties=properties,
534
+ )
535
+ )
536
+
537
+ predecessors: dict[str, set[str]] = {}
538
+ flow_nodes: set[str] = set(definitions) | set(uses)
539
+ for edge in cfg_edges:
540
+ predecessors.setdefault(edge.target, set()).add(edge.source)
541
+ flow_nodes.add(edge.source)
542
+ flow_nodes.add(edge.target)
543
+ incoming: dict[str, dict[str, frozenset[str]]] = {
544
+ node_id: {} for node_id in flow_nodes
545
+ }
546
+ outgoing: dict[str, dict[str, frozenset[str]]] = {
547
+ node_id: {} for node_id in flow_nodes
548
+ }
549
+ ordered_nodes = sorted(flow_nodes)
550
+ changed = True
551
+ remaining = max(1, len(ordered_nodes) * 4)
552
+ while changed and remaining > 0:
553
+ changed = False
554
+ remaining -= 1
555
+ for node_id in ordered_nodes:
556
+ merged: dict[str, set[str]] = {}
557
+ for predecessor in predecessors.get(node_id, ()):
558
+ for name, identifiers in outgoing.get(predecessor, {}).items():
559
+ merged.setdefault(name, set()).update(identifiers)
560
+ next_incoming = {
561
+ name: frozenset(identifiers)
562
+ for name, identifiers in merged.items()
563
+ }
564
+ next_outgoing = dict(next_incoming)
565
+ for name, identifier_id in definitions.get(node_id, ()):
566
+ next_outgoing[name] = frozenset({identifier_id})
567
+ if incoming[node_id] != next_incoming or outgoing[node_id] != next_outgoing:
568
+ incoming[node_id] = next_incoming
569
+ outgoing[node_id] = next_outgoing
570
+ changed = True
571
+
572
+ for owner_id, records in uses.items():
573
+ available = incoming.get(owner_id, {})
574
+ for name, use_id in records:
575
+ for definition_id in sorted(available.get(name, ())):
576
+ result.append(
577
+ CpgEdge.create(
578
+ label="REACHING_DEF",
579
+ source=definition_id,
580
+ target=use_id,
581
+ properties={"name": name, "owner_node_id": owner_id},
582
+ )
583
+ )
584
+ return result
585
+
586
+
587
+ def _build_calls(
588
+ target: CpgTarget,
589
+ selected: SyntaxNode,
590
+ syntax_nodes: Mapping[int, CpgNode],
591
+ candidates: Mapping[str, Sequence[CpgTarget]],
592
+ root_node: CpgNode,
593
+ ) -> tuple[list[CpgNode], list[CpgEdge], int]:
594
+ nodes: list[CpgNode] = []
595
+ edges: list[CpgEdge] = []
596
+ unresolved = 0
597
+ calls: list[tuple[SyntaxNode, CpgNode, str]] = []
598
+ stack = [selected]
599
+ while stack:
600
+ current = stack.pop()
601
+ stack.extend(reversed(current.child_nodes))
602
+ if current.typ != SyntaxNodeType.ntCall:
603
+ continue
604
+ call_node = syntax_nodes.get(id(current))
605
+ identifiers = _named_identifiers(current, syntax_nodes)
606
+ if call_node is None or not identifiers:
607
+ unresolved += 1
608
+ continue
609
+ name = identifiers[0][0]
610
+ calls.append((current, call_node, name.casefold()))
611
+
612
+ names = {name for _, _, name in calls}
613
+ batch_resolver = getattr(candidates, "resolve_many", None)
614
+ if callable(batch_resolver):
615
+ resolved = batch_resolver(names)
616
+ else:
617
+ resolved = {
618
+ name: tuple(candidates.get(name, ()))
619
+ for name in names
620
+ }
621
+ for current, call_node, name in calls:
622
+ matches = tuple(resolved.get(name, ()))
623
+ if len(matches) != 1:
624
+ unresolved += 1
625
+ continue
626
+ callee = matches[0]
627
+ callee_node = _target_node(callee)
628
+ nodes.append(callee_node)
629
+ edges.append(
630
+ CpgEdge.create(
631
+ label="CALL",
632
+ source=root_node.node_id,
633
+ target=callee_node.node_id,
634
+ properties={
635
+ "call_node_id": call_node.node_id,
636
+ "name": name,
637
+ "path": target.path,
638
+ "line": current.line,
639
+ "column": current.col,
640
+ },
641
+ )
642
+ )
643
+ return nodes, edges, unresolved
644
+
645
+
646
+ def _named_identifiers(
647
+ root: SyntaxNode | None,
648
+ syntax_nodes: Mapping[int, CpgNode],
649
+ ) -> list[tuple[str, str]]:
650
+ if root is None:
651
+ return []
652
+ result: list[tuple[str, str]] = []
653
+ stack = [root]
654
+ while stack:
655
+ current = stack.pop()
656
+ stack.extend(reversed(current.child_nodes))
657
+ if current.typ not in {SyntaxNodeType.ntIdentifier, SyntaxNodeType.ntName}:
658
+ continue
659
+ name = current.get_attribute(AttributeName.anName)
660
+ node = syntax_nodes.get(id(current))
661
+ if name and node is not None:
662
+ result.append((name.casefold(), node.node_id))
663
+ return result
664
+
665
+
666
+ __all__ = ["build_cpg_subgraph"]