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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- 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"]
|