codeanalyzer-python 0.3.0__py3-none-any.whl → 1.0.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.
- codeanalyzer/__main__.py +77 -4
- codeanalyzer/core.py +174 -72
- codeanalyzer/dataflow/__init__.py +35 -0
- codeanalyzer/dataflow/access_paths.py +563 -0
- codeanalyzer/dataflow/alias.py +93 -0
- codeanalyzer/dataflow/builder.py +688 -0
- codeanalyzer/dataflow/cfg.py +605 -0
- codeanalyzer/dataflow/defuse.py +113 -0
- codeanalyzer/dataflow/dominance.py +140 -0
- codeanalyzer/dataflow/identity.py +91 -0
- codeanalyzer/dataflow/pdg.py +100 -0
- codeanalyzer/dataflow/scalpel_oracle.py +269 -0
- codeanalyzer/dataflow/scc.py +91 -0
- codeanalyzer/dataflow/sdg.py +424 -0
- codeanalyzer/dataflow/slicing.py +93 -0
- codeanalyzer/dataflow/summaries.py +217 -0
- codeanalyzer/dataflow/syntactic.py +26 -0
- codeanalyzer/neo4j/__init__.py +1 -1
- codeanalyzer/neo4j/bolt.py +19 -4
- codeanalyzer/neo4j/cypher.py +9 -3
- codeanalyzer/neo4j/emit.py +10 -5
- codeanalyzer/neo4j/project.py +307 -60
- codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer/neo4j/schema.py +297 -15
- codeanalyzer/options/options.py +4 -0
- codeanalyzer/provenance.py +61 -0
- codeanalyzer/schema/__init__.py +19 -0
- codeanalyzer/schema/assign_ids.py +37 -0
- codeanalyzer/schema/call_graph_ids.py +12 -0
- codeanalyzer/schema/ids.py +23 -0
- codeanalyzer/schema/l1_body.py +29 -0
- codeanalyzer/schema/l2_callees.py +36 -0
- codeanalyzer/schema/py_schema.py +175 -26
- codeanalyzer/semantic_analysis/call_graph.py +24 -27
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
- codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
- codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
- codeanalyzer/neo4j/catalog.py +0 -245
- codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Copyright IBM Corporation 2025
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
"""Stage 1 of the level-3 dataflow ladder: the exceptional, statement-level CFG.
|
|
18
|
+
|
|
19
|
+
One CFG per callable, lowered from the stdlib ``ast`` tree — the same parse the
|
|
20
|
+
symbol-table builder uses, so node spans and callable signatures line up with
|
|
21
|
+
the rest of ``analysis.json``.
|
|
22
|
+
|
|
23
|
+
Lowering rules (the Python checklist from the CLDK dataflow contract):
|
|
24
|
+
|
|
25
|
+
- One synthetic ``ENTRY`` (node id 0) and one synthetic ``EXIT`` (last CFG id).
|
|
26
|
+
Multi-exit is normalized: every ``return``/``raise``/fall-off-end gets an
|
|
27
|
+
edge to ``EXIT`` with the appropriate kind.
|
|
28
|
+
- ``if``/``while``/``for`` headers are their own nodes (kinds ``branch`` /
|
|
29
|
+
``loop``) with ``true``/``false`` out-edges; loop back edges carry
|
|
30
|
+
``loop_back``; ``break``/``continue`` carry their own kinds.
|
|
31
|
+
- ``try/except/else/finally``: the try body is lowered in sequence; each
|
|
32
|
+
statement that can raise gets an ``exception`` edge to the innermost
|
|
33
|
+
enclosing handler chain (or ``EXIT`` when there is none). ``except`` match
|
|
34
|
+
clauses are ``handler`` nodes chained by ``false`` edges; an unmatched
|
|
35
|
+
exception propagates outward. ``finally`` bodies are lowered once, on the
|
|
36
|
+
normal path; abrupt entries (return / unhandled raise / break / continue
|
|
37
|
+
observed in the protected region) add corresponding out-edges from the
|
|
38
|
+
finally's end. Exceptions raised inside nested ``finally``-protected regions
|
|
39
|
+
connect straight to the enclosing handler chain — a documented
|
|
40
|
+
over-approximation (the finally body still executes on every normal path,
|
|
41
|
+
so its definitions are never lost, only their ordering on pure-exception
|
|
42
|
+
paths).
|
|
43
|
+
- ``with``/``async with``: the header is a ``statement`` node that defines the
|
|
44
|
+
``as`` targets; the implicit ``__exit__`` try/finally is *not* materialized
|
|
45
|
+
(documented over-approximation); body statements keep their exception edges.
|
|
46
|
+
- Generators: a statement containing ``yield``/``yield from`` gets its
|
|
47
|
+
fall-through successor edge with kind ``yield`` (the resume path) plus a
|
|
48
|
+
``yield`` edge to ``EXIT`` (the generator may never be resumed).
|
|
49
|
+
``await`` marks the successor edge ``await_resume``.
|
|
50
|
+
- ``raise`` → ``exception`` edge to the handler chain / EXIT, no fall-through.
|
|
51
|
+
``assert`` gets a fall-through plus an ``exception`` edge.
|
|
52
|
+
- Expression-level short-circuit (``and``/``or``/ternary) stays atomic inside
|
|
53
|
+
its statement node — the CFG is statement-level by contract.
|
|
54
|
+
- Comprehensions are atomic expressions of their statement (their implicit
|
|
55
|
+
loop and scope are handled by the access-path model, not the CFG).
|
|
56
|
+
- Nested ``def``/``class`` statements are single ``statement`` nodes (the
|
|
57
|
+
binding); their bodies get their own CFGs keyed by their own signatures.
|
|
58
|
+
Decorators are call-site facts, not CFG nodes.
|
|
59
|
+
- Infinite loops (``while True:`` with no break) get a synthetic ``exception``
|
|
60
|
+
edge from the loop header to ``EXIT`` so post-dominance stays well-formed
|
|
61
|
+
(in Python any loop can exit via an async signal such as KeyboardInterrupt,
|
|
62
|
+
so the edge is semantically honest).
|
|
63
|
+
- Statements unreachable from ``ENTRY`` (dead code after a return/raise) are
|
|
64
|
+
pruned: they cannot carry dependence.
|
|
65
|
+
|
|
66
|
+
Statements are considered able to raise when they contain a call, attribute
|
|
67
|
+
access, subscript, explicit ``raise``/``assert``, a ``with`` header, or a
|
|
68
|
+
``for`` header (iterator protocol) — over-approximate by design.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
from __future__ import annotations
|
|
72
|
+
|
|
73
|
+
import ast
|
|
74
|
+
from dataclasses import dataclass, field
|
|
75
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
76
|
+
|
|
77
|
+
# The shared, cross-language node-kind and edge-kind vocabulary. Python adds no
|
|
78
|
+
# renamed/repurposed kinds; `yield` / `await_resume` are the contract's own.
|
|
79
|
+
NODE_KINDS = (
|
|
80
|
+
"entry",
|
|
81
|
+
"exit",
|
|
82
|
+
"statement",
|
|
83
|
+
"branch",
|
|
84
|
+
"loop",
|
|
85
|
+
"return",
|
|
86
|
+
"raise",
|
|
87
|
+
"handler",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
EDGE_KINDS = (
|
|
91
|
+
"fallthrough",
|
|
92
|
+
"true",
|
|
93
|
+
"false",
|
|
94
|
+
"switch_case",
|
|
95
|
+
"loop_back",
|
|
96
|
+
"exception",
|
|
97
|
+
"return",
|
|
98
|
+
"break",
|
|
99
|
+
"continue",
|
|
100
|
+
"yield",
|
|
101
|
+
"await_resume",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class CFGNode:
|
|
107
|
+
"""A statement-level CFG node. ``id`` is assigned in source-span order
|
|
108
|
+
after construction (ENTRY = 0, EXIT = last CFG id)."""
|
|
109
|
+
|
|
110
|
+
id: int
|
|
111
|
+
kind: str
|
|
112
|
+
start_line: int = -1
|
|
113
|
+
end_line: int = -1
|
|
114
|
+
start_column: int = -1
|
|
115
|
+
end_column: int = -1
|
|
116
|
+
# The owning AST statement/expression (None for ENTRY/EXIT). Not emitted;
|
|
117
|
+
# used by later stages to compute def/use sets.
|
|
118
|
+
ast_node: Optional[ast.AST] = field(default=None, repr=False, compare=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class CFGEdge:
|
|
123
|
+
source: int
|
|
124
|
+
target: int
|
|
125
|
+
kind: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ControlFlowGraph:
|
|
130
|
+
"""CFG of a single callable, keyed externally by the callable signature."""
|
|
131
|
+
|
|
132
|
+
nodes: List[CFGNode]
|
|
133
|
+
edges: List[CFGEdge]
|
|
134
|
+
entry_id: int
|
|
135
|
+
exit_id: int
|
|
136
|
+
|
|
137
|
+
def successors(self) -> Dict[int, List[Tuple[int, str]]]:
|
|
138
|
+
succ: Dict[int, List[Tuple[int, str]]] = {n.id: [] for n in self.nodes}
|
|
139
|
+
for e in self.edges:
|
|
140
|
+
succ[e.source].append((e.target, e.kind))
|
|
141
|
+
return succ
|
|
142
|
+
|
|
143
|
+
def predecessors(self) -> Dict[int, List[Tuple[int, str]]]:
|
|
144
|
+
pred: Dict[int, List[Tuple[int, str]]] = {n.id: [] for n in self.nodes}
|
|
145
|
+
for e in self.edges:
|
|
146
|
+
pred[e.target].append((e.source, e.kind))
|
|
147
|
+
return pred
|
|
148
|
+
|
|
149
|
+
def node_by_id(self, node_id: int) -> CFGNode:
|
|
150
|
+
return next(n for n in self.nodes if n.id == node_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _TempNode:
|
|
154
|
+
"""Mutable node used during lowering, renumbered at finalize time."""
|
|
155
|
+
|
|
156
|
+
__slots__ = ("kind", "ast_node", "span", "seq")
|
|
157
|
+
|
|
158
|
+
def __init__(self, kind: str, ast_node: Optional[ast.AST], span, seq: int):
|
|
159
|
+
self.kind = kind
|
|
160
|
+
self.ast_node = ast_node
|
|
161
|
+
self.span = span # (start_line, start_col, end_line, end_col)
|
|
162
|
+
self.seq = seq
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _span_of(node: ast.AST) -> Tuple[int, int, int, int]:
|
|
166
|
+
return (
|
|
167
|
+
getattr(node, "lineno", -1),
|
|
168
|
+
getattr(node, "col_offset", -1),
|
|
169
|
+
getattr(node, "end_lineno", getattr(node, "lineno", -1)),
|
|
170
|
+
getattr(node, "end_col_offset", -1),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _contains(node: ast.AST, types: tuple, *, into_nested_defs: bool = False) -> bool:
|
|
175
|
+
"""True if ``node`` contains an AST node of one of ``types``, without
|
|
176
|
+
descending into nested function/class definitions (their bodies belong to
|
|
177
|
+
other CFGs) unless requested."""
|
|
178
|
+
stop = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
179
|
+
for child in ast.iter_child_nodes(node):
|
|
180
|
+
if isinstance(child, types):
|
|
181
|
+
return True
|
|
182
|
+
if not into_nested_defs and isinstance(child, stop):
|
|
183
|
+
continue
|
|
184
|
+
if _contains(child, types, into_nested_defs=into_nested_defs):
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _can_raise(stmt: ast.stmt) -> bool:
|
|
190
|
+
"""Over-approximate: if we can't prove the statement doesn't throw, it
|
|
191
|
+
gets the exception edge (contract rule)."""
|
|
192
|
+
if isinstance(stmt, (ast.Raise, ast.Assert, ast.With, ast.AsyncWith, ast.For, ast.AsyncFor)):
|
|
193
|
+
return True
|
|
194
|
+
return _contains(stmt, (ast.Call, ast.Attribute, ast.Subscript, ast.Await))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _stmt_kind(stmt: ast.stmt) -> str:
|
|
198
|
+
if isinstance(stmt, ast.Return):
|
|
199
|
+
return "return"
|
|
200
|
+
if isinstance(stmt, ast.Raise):
|
|
201
|
+
return "raise"
|
|
202
|
+
if isinstance(stmt, ast.If):
|
|
203
|
+
return "branch"
|
|
204
|
+
if isinstance(stmt, (ast.While, ast.For, ast.AsyncFor)):
|
|
205
|
+
return "loop"
|
|
206
|
+
return "statement"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _resume_kind(stmt: ast.stmt) -> str:
|
|
210
|
+
"""Edge kind of the statement's normal successor edge: generators resume
|
|
211
|
+
after a yield, coroutines after an await."""
|
|
212
|
+
if _contains(stmt, (ast.Yield, ast.YieldFrom)):
|
|
213
|
+
return "yield"
|
|
214
|
+
if _contains(stmt, (ast.Await,)):
|
|
215
|
+
return "await_resume"
|
|
216
|
+
return "fallthrough"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class _LoopFrame:
|
|
220
|
+
__slots__ = ("header", "break_fringe")
|
|
221
|
+
|
|
222
|
+
def __init__(self, header: _TempNode):
|
|
223
|
+
self.header = header
|
|
224
|
+
# (node, kind) dangling edges produced by `break` — connected to the
|
|
225
|
+
# loop's successor once the loop is fully lowered.
|
|
226
|
+
self.break_fringe: List[Tuple[_TempNode, str]] = []
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _FinallyFrame:
|
|
230
|
+
"""Tracks a try/finally protected region while its body is lowered.
|
|
231
|
+
|
|
232
|
+
``entry_fringe`` collects the abrupt-exit nodes (return / raise / break /
|
|
233
|
+
continue) observed inside the protected region — they become incoming
|
|
234
|
+
edges of the finally body, which is how the finally stays reachable when
|
|
235
|
+
the try body never completes normally. ``abrupt`` records which exit kinds
|
|
236
|
+
were seen so the finally's end re-emits a matching out-edge for each."""
|
|
237
|
+
|
|
238
|
+
__slots__ = ("abrupt", "entry_fringe")
|
|
239
|
+
|
|
240
|
+
def __init__(self):
|
|
241
|
+
self.abrupt: Set[str] = set()
|
|
242
|
+
self.entry_fringe: List[Tuple[_TempNode, str]] = []
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class CFGBuilder:
|
|
246
|
+
"""Lowers one callable's AST into a :class:`ControlFlowGraph`."""
|
|
247
|
+
|
|
248
|
+
def __init__(self) -> None:
|
|
249
|
+
self._nodes: List[_TempNode] = []
|
|
250
|
+
self._edges: List[Tuple[_TempNode, _TempNode, str]] = []
|
|
251
|
+
self._seq = 0
|
|
252
|
+
self._loop_stack: List[_LoopFrame] = []
|
|
253
|
+
# Innermost-first chain of exception targets: (first handler node of a
|
|
254
|
+
# try's except chain, finally-stack depth when it was pushed). The
|
|
255
|
+
# depth lets exception edges mark only the finally frames *inside* the
|
|
256
|
+
# protected region as abruptly exited — an exception caught by this
|
|
257
|
+
# try's own handler re-enters the normal path.
|
|
258
|
+
self._handler_stack: List[Tuple[_TempNode, int]] = []
|
|
259
|
+
self._finally_stack: List[_FinallyFrame] = []
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------- helpers
|
|
262
|
+
|
|
263
|
+
def _new_node(self, kind: str, ast_node: Optional[ast.AST], span=None) -> _TempNode:
|
|
264
|
+
node = _TempNode(kind, ast_node, span or (_span_of(ast_node) if ast_node else (-1, -1, -1, -1)), self._seq)
|
|
265
|
+
self._seq += 1
|
|
266
|
+
self._nodes.append(node)
|
|
267
|
+
return node
|
|
268
|
+
|
|
269
|
+
def _connect(self, fringe: List[Tuple[_TempNode, str]], target: _TempNode) -> None:
|
|
270
|
+
for source, kind in fringe:
|
|
271
|
+
self._edges.append((source, target, kind))
|
|
272
|
+
|
|
273
|
+
def _exception_target(self) -> Optional[_TempNode]:
|
|
274
|
+
return self._handler_stack[-1][0] if self._handler_stack else None
|
|
275
|
+
|
|
276
|
+
def _mark_exception_transit(self, node: Optional[_TempNode] = None) -> None:
|
|
277
|
+
"""Mark the finally frames an in-flight exception passes through:
|
|
278
|
+
every frame inside the innermost handler's protected region, or all
|
|
279
|
+
frames when the exception escapes the function."""
|
|
280
|
+
depth = self._handler_stack[-1][1] if self._handler_stack else 0
|
|
281
|
+
transit = self._finally_stack[depth:]
|
|
282
|
+
for frame in transit:
|
|
283
|
+
frame.abrupt.add("exception")
|
|
284
|
+
if node is not None and transit:
|
|
285
|
+
transit[-1].entry_fringe.append((node, "exception"))
|
|
286
|
+
|
|
287
|
+
def _add_exception_edge(self, node: _TempNode, exit_node: _TempNode) -> None:
|
|
288
|
+
target = self._exception_target() or exit_node
|
|
289
|
+
self._edges.append((node, target, "exception"))
|
|
290
|
+
self._mark_exception_transit()
|
|
291
|
+
|
|
292
|
+
# ----------------------------------------------------------------- build
|
|
293
|
+
|
|
294
|
+
def build(self, func: ast.AST) -> ControlFlowGraph:
|
|
295
|
+
"""``func`` is a FunctionDef / AsyncFunctionDef whose body is lowered.
|
|
296
|
+
ENTRY takes the ``def`` line's span; EXIT the end of the callable."""
|
|
297
|
+
entry = self._new_node("entry", None, span=(func.lineno, func.col_offset, func.lineno, func.col_offset))
|
|
298
|
+
end_line = getattr(func, "end_lineno", func.lineno)
|
|
299
|
+
end_col = getattr(func, "end_col_offset", -1)
|
|
300
|
+
self._exit = self._new_node("exit", None, span=(end_line, end_col, end_line, end_col))
|
|
301
|
+
|
|
302
|
+
fringe = self._lower_block(func.body, [(entry, "fallthrough")])
|
|
303
|
+
# Fall-off-end is an implicit `return None`.
|
|
304
|
+
self._connect([(n, "return") for n, _ in fringe], self._exit)
|
|
305
|
+
|
|
306
|
+
return self._finalize(entry, self._exit)
|
|
307
|
+
|
|
308
|
+
# ------------------------------------------------------------- lowering
|
|
309
|
+
|
|
310
|
+
def _lower_block(
|
|
311
|
+
self, stmts: List[ast.stmt], fringe: List[Tuple[_TempNode, str]]
|
|
312
|
+
) -> List[Tuple[_TempNode, str]]:
|
|
313
|
+
for stmt in stmts:
|
|
314
|
+
if not fringe:
|
|
315
|
+
# Dead code after return/raise/break/continue: lower it anyway
|
|
316
|
+
# (nodes unreachable from ENTRY are pruned at finalize).
|
|
317
|
+
pass
|
|
318
|
+
fringe = self._lower_stmt(stmt, fringe)
|
|
319
|
+
return fringe
|
|
320
|
+
|
|
321
|
+
def _lower_stmt(
|
|
322
|
+
self, stmt: ast.stmt, fringe: List[Tuple[_TempNode, str]]
|
|
323
|
+
) -> List[Tuple[_TempNode, str]]:
|
|
324
|
+
if isinstance(stmt, ast.If):
|
|
325
|
+
return self._lower_if(stmt, fringe)
|
|
326
|
+
if isinstance(stmt, ast.While):
|
|
327
|
+
return self._lower_while(stmt, fringe)
|
|
328
|
+
if isinstance(stmt, (ast.For, ast.AsyncFor)):
|
|
329
|
+
return self._lower_for(stmt, fringe)
|
|
330
|
+
if isinstance(stmt, ast.Try):
|
|
331
|
+
return self._lower_try(stmt, fringe)
|
|
332
|
+
if isinstance(stmt, (ast.With, ast.AsyncWith)):
|
|
333
|
+
return self._lower_with(stmt, fringe)
|
|
334
|
+
if isinstance(stmt, ast.Return):
|
|
335
|
+
return self._lower_return(stmt, fringe)
|
|
336
|
+
if isinstance(stmt, ast.Raise):
|
|
337
|
+
return self._lower_raise(stmt, fringe)
|
|
338
|
+
if isinstance(stmt, ast.Break):
|
|
339
|
+
return self._lower_break(stmt, fringe)
|
|
340
|
+
if isinstance(stmt, ast.Continue):
|
|
341
|
+
return self._lower_continue(stmt, fringe)
|
|
342
|
+
# Simple statement (incl. nested def/class = the binding statement).
|
|
343
|
+
node = self._new_node(_stmt_kind(stmt), stmt)
|
|
344
|
+
self._connect(fringe, node)
|
|
345
|
+
if _can_raise(stmt):
|
|
346
|
+
self._add_exception_edge(node, self._exit)
|
|
347
|
+
resume = _resume_kind(stmt)
|
|
348
|
+
if resume == "yield":
|
|
349
|
+
# The generator may be abandoned at any yield.
|
|
350
|
+
self._edges.append((node, self._exit, "yield"))
|
|
351
|
+
return [(node, resume)]
|
|
352
|
+
|
|
353
|
+
def _lower_if(self, stmt: ast.If, fringe):
|
|
354
|
+
header = self._new_node("branch", stmt, span=_span_of(stmt.test))
|
|
355
|
+
self._connect(fringe, header)
|
|
356
|
+
if _can_raise_expr(stmt.test):
|
|
357
|
+
self._add_exception_edge(header, self._exit)
|
|
358
|
+
then_fringe = self._lower_block(stmt.body, [(header, "true")])
|
|
359
|
+
if stmt.orelse:
|
|
360
|
+
else_fringe = self._lower_block(stmt.orelse, [(header, "false")])
|
|
361
|
+
else:
|
|
362
|
+
else_fringe = [(header, "false")]
|
|
363
|
+
return then_fringe + else_fringe
|
|
364
|
+
|
|
365
|
+
def _lower_while(self, stmt: ast.While, fringe):
|
|
366
|
+
header = self._new_node("loop", stmt, span=_span_of(stmt.test))
|
|
367
|
+
self._connect(fringe, header)
|
|
368
|
+
if _can_raise_expr(stmt.test):
|
|
369
|
+
self._add_exception_edge(header, self._exit)
|
|
370
|
+
|
|
371
|
+
frame = _LoopFrame(header)
|
|
372
|
+
self._loop_stack.append(frame)
|
|
373
|
+
body_fringe = self._lower_block(stmt.body, [(header, "true")])
|
|
374
|
+
self._loop_stack.pop()
|
|
375
|
+
self._connect([(n, "loop_back") for n, _ in body_fringe], header)
|
|
376
|
+
|
|
377
|
+
# `while True:` / constant-true tests never take the false edge.
|
|
378
|
+
always_true = isinstance(stmt.test, ast.Constant) and bool(stmt.test.value)
|
|
379
|
+
out = [] if always_true else [(header, "false")]
|
|
380
|
+
if stmt.orelse:
|
|
381
|
+
out = self._lower_block(stmt.orelse, out)
|
|
382
|
+
return out + frame.break_fringe
|
|
383
|
+
|
|
384
|
+
def _lower_for(self, stmt, fringe):
|
|
385
|
+
header = self._new_node("loop", stmt, span=_span_of(stmt.iter))
|
|
386
|
+
self._connect(fringe, header)
|
|
387
|
+
# The iterator protocol can raise.
|
|
388
|
+
self._add_exception_edge(header, self._exit)
|
|
389
|
+
|
|
390
|
+
frame = _LoopFrame(header)
|
|
391
|
+
self._loop_stack.append(frame)
|
|
392
|
+
body_fringe = self._lower_block(stmt.body, [(header, "true")])
|
|
393
|
+
self._loop_stack.pop()
|
|
394
|
+
self._connect([(n, "loop_back") for n, _ in body_fringe], header)
|
|
395
|
+
|
|
396
|
+
out = [(header, "false")]
|
|
397
|
+
if stmt.orelse:
|
|
398
|
+
out = self._lower_block(stmt.orelse, out)
|
|
399
|
+
return out + frame.break_fringe
|
|
400
|
+
|
|
401
|
+
def _lower_try(self, stmt: ast.Try, fringe):
|
|
402
|
+
has_finally = bool(stmt.finalbody)
|
|
403
|
+
finally_frame = _FinallyFrame() if has_finally else None
|
|
404
|
+
|
|
405
|
+
handler_entry: Optional[_TempNode] = None
|
|
406
|
+
handler_nodes: List[_TempNode] = []
|
|
407
|
+
if stmt.handlers:
|
|
408
|
+
for handler in stmt.handlers:
|
|
409
|
+
node = self._new_node("handler", handler, span=(
|
|
410
|
+
handler.lineno,
|
|
411
|
+
handler.col_offset,
|
|
412
|
+
getattr(handler.type, "end_lineno", handler.lineno) if handler.type else handler.lineno,
|
|
413
|
+
getattr(handler.type, "end_col_offset", -1) if handler.type else -1,
|
|
414
|
+
))
|
|
415
|
+
handler_nodes.append(node)
|
|
416
|
+
handler_entry = handler_nodes[0]
|
|
417
|
+
|
|
418
|
+
if finally_frame is not None:
|
|
419
|
+
self._finally_stack.append(finally_frame)
|
|
420
|
+
|
|
421
|
+
# Protected region: body (+ else) raises reach this try's handlers.
|
|
422
|
+
if handler_entry is not None:
|
|
423
|
+
self._handler_stack.append((handler_entry, len(self._finally_stack)))
|
|
424
|
+
body_fringe = self._lower_block(stmt.body, fringe)
|
|
425
|
+
if stmt.orelse:
|
|
426
|
+
body_fringe = self._lower_block(stmt.orelse, body_fringe)
|
|
427
|
+
if handler_entry is not None:
|
|
428
|
+
self._handler_stack.pop()
|
|
429
|
+
|
|
430
|
+
# Handler chain: matched → handler body; unmatched → next handler,
|
|
431
|
+
# falling off the chain propagates outward (outer handler or EXIT).
|
|
432
|
+
handler_exit_fringes: List[Tuple[_TempNode, str]] = []
|
|
433
|
+
for i, (handler, node) in enumerate(zip(stmt.handlers, handler_nodes)):
|
|
434
|
+
hb_fringe = self._lower_block(handler.body, [(node, "true")])
|
|
435
|
+
handler_exit_fringes.extend(hb_fringe)
|
|
436
|
+
is_catch_all = handler.type is None
|
|
437
|
+
if i + 1 < len(handler_nodes):
|
|
438
|
+
self._edges.append((node, handler_nodes[i + 1], "false"))
|
|
439
|
+
elif not is_catch_all:
|
|
440
|
+
outer = self._exception_target() or self._exit
|
|
441
|
+
self._edges.append((node, outer, "exception"))
|
|
442
|
+
self._mark_exception_transit(node)
|
|
443
|
+
|
|
444
|
+
normal_fringe = body_fringe + handler_exit_fringes
|
|
445
|
+
|
|
446
|
+
if finally_frame is not None:
|
|
447
|
+
self._finally_stack.pop()
|
|
448
|
+
fin_entry = normal_fringe + finally_frame.entry_fringe
|
|
449
|
+
fin_fringe = self._lower_block(stmt.finalbody, fin_entry)
|
|
450
|
+
# Abrupt completions observed in the protected region re-emerge
|
|
451
|
+
# from the finally body's end.
|
|
452
|
+
for node, _kind in list(fin_fringe):
|
|
453
|
+
if "return" in finally_frame.abrupt:
|
|
454
|
+
self._edges.append((node, self._exit, "return"))
|
|
455
|
+
if "exception" in finally_frame.abrupt:
|
|
456
|
+
target = self._exception_target() or self._exit
|
|
457
|
+
self._edges.append((node, target, "exception"))
|
|
458
|
+
if "break" in finally_frame.abrupt and self._loop_stack:
|
|
459
|
+
self._loop_stack[-1].break_fringe.append((node, "break"))
|
|
460
|
+
if "continue" in finally_frame.abrupt and self._loop_stack:
|
|
461
|
+
self._edges.append((node, self._loop_stack[-1].header, "continue"))
|
|
462
|
+
return fin_fringe
|
|
463
|
+
|
|
464
|
+
return normal_fringe
|
|
465
|
+
|
|
466
|
+
def _lower_with(self, stmt, fringe):
|
|
467
|
+
node = self._new_node("statement", stmt, span=(
|
|
468
|
+
stmt.lineno,
|
|
469
|
+
stmt.col_offset,
|
|
470
|
+
stmt.items[-1].context_expr.end_lineno,
|
|
471
|
+
stmt.items[-1].context_expr.end_col_offset,
|
|
472
|
+
))
|
|
473
|
+
self._connect(fringe, node)
|
|
474
|
+
self._add_exception_edge(node, self._exit)
|
|
475
|
+
return self._lower_block(stmt.body, [(node, "fallthrough")])
|
|
476
|
+
|
|
477
|
+
def _lower_return(self, stmt: ast.Return, fringe):
|
|
478
|
+
node = self._new_node("return", stmt)
|
|
479
|
+
self._connect(fringe, node)
|
|
480
|
+
if stmt.value is not None and _can_raise_expr(stmt.value):
|
|
481
|
+
self._add_exception_edge(node, self._exit)
|
|
482
|
+
if self._finally_stack:
|
|
483
|
+
# Routed through the innermost finally; its end re-emits `return`.
|
|
484
|
+
for frame in self._finally_stack:
|
|
485
|
+
frame.abrupt.add("return")
|
|
486
|
+
self._finally_stack[-1].entry_fringe.append((node, "return"))
|
|
487
|
+
return []
|
|
488
|
+
self._edges.append((node, self._exit, "return"))
|
|
489
|
+
return []
|
|
490
|
+
|
|
491
|
+
def _lower_raise(self, stmt: ast.Raise, fringe):
|
|
492
|
+
node = self._new_node("raise", stmt)
|
|
493
|
+
self._connect(fringe, node)
|
|
494
|
+
target = self._exception_target() or self._exit
|
|
495
|
+
self._edges.append((node, target, "exception"))
|
|
496
|
+
self._mark_exception_transit(node)
|
|
497
|
+
return []
|
|
498
|
+
|
|
499
|
+
def _lower_break(self, stmt: ast.Break, fringe):
|
|
500
|
+
node = self._new_node("statement", stmt)
|
|
501
|
+
self._connect(fringe, node)
|
|
502
|
+
if self._loop_stack:
|
|
503
|
+
self._loop_stack[-1].break_fringe.append((node, "break"))
|
|
504
|
+
for frame in self._finally_stack:
|
|
505
|
+
frame.abrupt.add("break")
|
|
506
|
+
if self._finally_stack:
|
|
507
|
+
self._finally_stack[-1].entry_fringe.append((node, "break"))
|
|
508
|
+
return []
|
|
509
|
+
|
|
510
|
+
def _lower_continue(self, stmt: ast.Continue, fringe):
|
|
511
|
+
node = self._new_node("statement", stmt)
|
|
512
|
+
self._connect(fringe, node)
|
|
513
|
+
if self._loop_stack:
|
|
514
|
+
self._edges.append((node, self._loop_stack[-1].header, "continue"))
|
|
515
|
+
for frame in self._finally_stack:
|
|
516
|
+
frame.abrupt.add("continue")
|
|
517
|
+
if self._finally_stack:
|
|
518
|
+
self._finally_stack[-1].entry_fringe.append((node, "continue"))
|
|
519
|
+
return []
|
|
520
|
+
|
|
521
|
+
# ------------------------------------------------------------- finalize
|
|
522
|
+
|
|
523
|
+
def _finalize(self, entry: _TempNode, exit_node: _TempNode) -> ControlFlowGraph:
|
|
524
|
+
# 1. Prune nodes unreachable from ENTRY (dead code).
|
|
525
|
+
succ: Dict[_TempNode, List[Tuple[_TempNode, str]]] = {n: [] for n in self._nodes}
|
|
526
|
+
for s, t, k in self._edges:
|
|
527
|
+
succ[s].append((t, k))
|
|
528
|
+
reachable: Set[_TempNode] = set()
|
|
529
|
+
stack = [entry]
|
|
530
|
+
while stack:
|
|
531
|
+
n = stack.pop()
|
|
532
|
+
if n in reachable:
|
|
533
|
+
continue
|
|
534
|
+
reachable.add(n)
|
|
535
|
+
for t, _ in succ[n]:
|
|
536
|
+
if t not in reachable:
|
|
537
|
+
stack.append(t)
|
|
538
|
+
reachable.add(exit_node) # EXIT always exists even if nothing reaches it yet
|
|
539
|
+
|
|
540
|
+
# 2. Synthetic escape edges: any reachable node that cannot reach EXIT
|
|
541
|
+
# sits in an infinite loop; give its loop header an `exception`
|
|
542
|
+
# edge to EXIT (documented above).
|
|
543
|
+
live_edges = [(s, t, k) for s, t, k in self._edges if s in reachable and t in reachable]
|
|
544
|
+
pred: Dict[_TempNode, List[_TempNode]] = {n: [] for n in reachable}
|
|
545
|
+
for s, t, _ in live_edges:
|
|
546
|
+
pred[t].append(s)
|
|
547
|
+
reaches_exit: Set[_TempNode] = set()
|
|
548
|
+
stack = [exit_node]
|
|
549
|
+
while stack:
|
|
550
|
+
n = stack.pop()
|
|
551
|
+
if n in reaches_exit:
|
|
552
|
+
continue
|
|
553
|
+
reaches_exit.add(n)
|
|
554
|
+
for p in pred[n]:
|
|
555
|
+
if p not in reaches_exit:
|
|
556
|
+
stack.append(p)
|
|
557
|
+
stuck = [n for n in reachable if n not in reaches_exit]
|
|
558
|
+
if stuck:
|
|
559
|
+
headers = [n for n in stuck if n.kind == "loop"] or stuck
|
|
560
|
+
for header in headers:
|
|
561
|
+
live_edges.append((header, exit_node, "exception"))
|
|
562
|
+
|
|
563
|
+
# 3. Renumber in source-span order: ENTRY = 0, EXIT = last.
|
|
564
|
+
middle = sorted(
|
|
565
|
+
(n for n in reachable if n is not entry and n is not exit_node),
|
|
566
|
+
key=lambda n: (n.span, n.seq),
|
|
567
|
+
)
|
|
568
|
+
ordered = [entry] + middle + [exit_node]
|
|
569
|
+
ids = {n: i for i, n in enumerate(ordered)}
|
|
570
|
+
|
|
571
|
+
nodes = [
|
|
572
|
+
CFGNode(
|
|
573
|
+
id=ids[n],
|
|
574
|
+
kind=n.kind,
|
|
575
|
+
start_line=n.span[0],
|
|
576
|
+
start_column=n.span[1],
|
|
577
|
+
end_line=n.span[2],
|
|
578
|
+
end_column=n.span[3],
|
|
579
|
+
ast_node=n.ast_node,
|
|
580
|
+
)
|
|
581
|
+
for n in ordered
|
|
582
|
+
]
|
|
583
|
+
seen: Set[Tuple[int, int, str]] = set()
|
|
584
|
+
edges: List[CFGEdge] = []
|
|
585
|
+
for s, t, k in sorted(live_edges, key=lambda e: (ids[e[0]], ids[e[1]], e[2])):
|
|
586
|
+
key = (ids[s], ids[t], k)
|
|
587
|
+
if key in seen:
|
|
588
|
+
continue
|
|
589
|
+
seen.add(key)
|
|
590
|
+
edges.append(CFGEdge(source=ids[s], target=ids[t], kind=k))
|
|
591
|
+
|
|
592
|
+
return ControlFlowGraph(
|
|
593
|
+
nodes=nodes, edges=edges, entry_id=ids[entry], exit_id=ids[exit_node]
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _can_raise_expr(expr: ast.expr) -> bool:
|
|
598
|
+
return isinstance(expr, (ast.Call, ast.Attribute, ast.Subscript, ast.Await)) or _contains(
|
|
599
|
+
expr, (ast.Call, ast.Attribute, ast.Subscript, ast.Await)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def build_cfg(func: ast.AST) -> ControlFlowGraph:
|
|
604
|
+
"""Build the exceptional, statement-level CFG of one callable."""
|
|
605
|
+
return CFGBuilder().build(func)
|