coretrace-python-analyzer 0.1.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.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""Build a control-flow graph from a PyHIR function."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import fields, is_dataclass, replace
|
|
7
|
+
from typing import Any, ClassVar
|
|
8
|
+
|
|
9
|
+
from coretrace_python.analysis import AnalysisContext, FunctionAnalysis
|
|
10
|
+
from coretrace_python.cfg.model import (
|
|
11
|
+
CFG,
|
|
12
|
+
BasicBlock,
|
|
13
|
+
BlockId,
|
|
14
|
+
Branch,
|
|
15
|
+
CFGError,
|
|
16
|
+
ForEach,
|
|
17
|
+
Jump,
|
|
18
|
+
Raise,
|
|
19
|
+
Return,
|
|
20
|
+
Terminator,
|
|
21
|
+
)
|
|
22
|
+
from coretrace_python.hir import nodes
|
|
23
|
+
from coretrace_python.source import SourceSpan
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Open:
|
|
27
|
+
"""A block that is still collecting statements and has no terminator yet."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, block_id: BlockId) -> None:
|
|
30
|
+
self.id = block_id
|
|
31
|
+
self.statements: list[nodes.Statement] = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _Builder:
|
|
35
|
+
def __init__(
|
|
36
|
+
self, function: nodes.Function, match_args: Mapping[str, tuple[str, ...]] | None = None
|
|
37
|
+
) -> None:
|
|
38
|
+
self.match_args = dict(match_args or {})
|
|
39
|
+
self.function = function
|
|
40
|
+
self.blocks: dict[BlockId, BasicBlock] = {}
|
|
41
|
+
self.counters: dict[str, int] = {}
|
|
42
|
+
self.loops: list[tuple[BlockId, BlockId]] = []
|
|
43
|
+
self.handlers: list[tuple[BlockId, ...]] = []
|
|
44
|
+
self.synthetic: set[str] = set()
|
|
45
|
+
|
|
46
|
+
def build(self) -> CFG:
|
|
47
|
+
entry = BlockId("entry")
|
|
48
|
+
end = self.sequence(self.function.body, _Open(entry), None, self.function.span)
|
|
49
|
+
if end is not None:
|
|
50
|
+
self.finish(end, Return(None, self.function.span))
|
|
51
|
+
return CFG(entry, self.blocks, frozenset(self.synthetic))
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------ expression-level control flow
|
|
54
|
+
|
|
55
|
+
def hidden(self, kind: str, span: SourceSpan) -> nodes.Name:
|
|
56
|
+
name = f"_coretrace_{kind}_{span.start_line}_{span.start_column}_{len(self.synthetic)}"
|
|
57
|
+
self.synthetic.add(name)
|
|
58
|
+
return nodes.Name(name, span)
|
|
59
|
+
|
|
60
|
+
def desugared(self, node: nodes.Statement, block: _Open) -> tuple[nodes.Statement, _Open]:
|
|
61
|
+
"""``node`` with its conditional expressions and comprehensions replaced by reads
|
|
62
|
+
of synthetic locals, after laying out the statements that compute them."""
|
|
63
|
+
|
|
64
|
+
pending: list[nodes.Statement] = []
|
|
65
|
+
if isinstance(node, nodes.While):
|
|
66
|
+
if not _has_control_flow(node.condition):
|
|
67
|
+
return node, block
|
|
68
|
+
# ``while <control flow>:`` recomputes its condition every iteration: it
|
|
69
|
+
# becomes ``while True`` with the condition laid out at the top of the body
|
|
70
|
+
# and a ``break``; the ``else`` clause runs before that ``break``.
|
|
71
|
+
inner: list[nodes.Statement] = []
|
|
72
|
+
condition = self.hoist(node.condition, inner)
|
|
73
|
+
stop = nodes.If(
|
|
74
|
+
nodes.UnaryOp("not", condition, node.span),
|
|
75
|
+
(*node.orelse, nodes.Break(node.span)),
|
|
76
|
+
(),
|
|
77
|
+
node.span,
|
|
78
|
+
)
|
|
79
|
+
return (
|
|
80
|
+
replace(
|
|
81
|
+
node,
|
|
82
|
+
condition=nodes.Constant(True, node.span),
|
|
83
|
+
body=(*inner, stop, *node.body),
|
|
84
|
+
orelse=(),
|
|
85
|
+
),
|
|
86
|
+
block,
|
|
87
|
+
)
|
|
88
|
+
if isinstance(node, nodes.Assign | nodes.AugAssign):
|
|
89
|
+
node = replace(node, target=self.hoist(node.target, pending), value=self.hoist(node.value, pending))
|
|
90
|
+
elif isinstance(node, nodes.ExpressionStatement):
|
|
91
|
+
node = replace(node, expression=self.hoist(node.expression, pending))
|
|
92
|
+
elif isinstance(node, nodes.Return | nodes.Raise) and node.__class__ is nodes.Return:
|
|
93
|
+
if node.value is not None:
|
|
94
|
+
node = replace(node, value=self.hoist(node.value, pending))
|
|
95
|
+
elif isinstance(node, nodes.Raise):
|
|
96
|
+
exception = self.hoist(node.exception, pending) if node.exception is not None else None
|
|
97
|
+
cause = self.hoist(node.cause, pending) if node.cause is not None else None
|
|
98
|
+
node = replace(node, exception=exception, cause=cause)
|
|
99
|
+
elif isinstance(node, nodes.Assert):
|
|
100
|
+
message = self.hoist(node.message, pending) if node.message is not None else None
|
|
101
|
+
node = replace(node, test=self.hoist(node.test, pending), message=message)
|
|
102
|
+
elif isinstance(node, nodes.If):
|
|
103
|
+
node = replace(node, condition=self.hoist(node.condition, pending))
|
|
104
|
+
elif isinstance(node, nodes.For):
|
|
105
|
+
node = replace(node, iterable=self.hoist(node.iterable, pending))
|
|
106
|
+
elif isinstance(node, nodes.With):
|
|
107
|
+
items = tuple(replace(item, context=self.hoist(item.context, pending)) for item in node.items)
|
|
108
|
+
node = replace(node, items=items)
|
|
109
|
+
if not pending:
|
|
110
|
+
return node, block
|
|
111
|
+
laid_out = self.sequence(tuple(pending), block, None, node.span)
|
|
112
|
+
assert laid_out is not None, "hoisted statements always fall through"
|
|
113
|
+
return node, laid_out
|
|
114
|
+
|
|
115
|
+
def hoist(self, node: Any, pending: list[nodes.Statement]) -> Any:
|
|
116
|
+
"""Rewrite one expression tree, appending the statements it needs to ``pending``."""
|
|
117
|
+
|
|
118
|
+
if isinstance(node, nodes.Conditional):
|
|
119
|
+
test = self.hoist(node.test, pending)
|
|
120
|
+
result = self.hidden("cond", node.span)
|
|
121
|
+
pending.append(
|
|
122
|
+
nodes.If(
|
|
123
|
+
test,
|
|
124
|
+
(nodes.Assign(result, node.body, node.span),),
|
|
125
|
+
(nodes.Assign(result, node.orelse, node.span),),
|
|
126
|
+
node.span,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return result
|
|
130
|
+
if isinstance(node, nodes.Comprehension):
|
|
131
|
+
return self.comprehension(node, pending)
|
|
132
|
+
if isinstance(node, nodes.Lambda):
|
|
133
|
+
defaults = tuple(
|
|
134
|
+
replace(p, default=self.hoist(p.default, pending)) if p.default is not None else p
|
|
135
|
+
for p in node.parameters
|
|
136
|
+
)
|
|
137
|
+
return replace(node, parameters=defaults)
|
|
138
|
+
if isinstance(node, tuple):
|
|
139
|
+
return tuple(self.hoist(item, pending) for item in node)
|
|
140
|
+
if is_dataclass(node) and not isinstance(node, type):
|
|
141
|
+
changes = {
|
|
142
|
+
f.name: self.hoist(getattr(node, f.name), pending)
|
|
143
|
+
for f in fields(node)
|
|
144
|
+
if f.name != "span" and _may_hold_expressions(getattr(node, f.name))
|
|
145
|
+
}
|
|
146
|
+
return replace(node, **changes) if changes else node
|
|
147
|
+
return node
|
|
148
|
+
|
|
149
|
+
def comprehension(self, node: nodes.Comprehension, pending: list[nodes.Statement]) -> nodes.Name:
|
|
150
|
+
"""Lay a comprehension out as loops filling a synthetic collection."""
|
|
151
|
+
|
|
152
|
+
span = node.span
|
|
153
|
+
result = self.hidden("comp", span)
|
|
154
|
+
renames: dict[str, str] = {}
|
|
155
|
+
for generator in node.generators:
|
|
156
|
+
for name in _bound_names(generator.target):
|
|
157
|
+
renames[name] = self.hidden(f"var_{name}", generator.target.span).identifier
|
|
158
|
+
if node.kind == "set":
|
|
159
|
+
initial: nodes.Expression = nodes.Call(nodes.Name("set", span), (), (), span)
|
|
160
|
+
elif node.kind == "dict":
|
|
161
|
+
initial = nodes.Dict((), span)
|
|
162
|
+
else:
|
|
163
|
+
initial = nodes.List((), span)
|
|
164
|
+
pending.append(nodes.Assign(result, initial, span))
|
|
165
|
+
|
|
166
|
+
element = _rename(node.element, renames)
|
|
167
|
+
if node.kind == "dict":
|
|
168
|
+
assert node.key is not None
|
|
169
|
+
innermost: nodes.Statement = nodes.Assign(
|
|
170
|
+
nodes.Subscript(result, _rename(node.key, renames), span), element, span
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
method = "add" if node.kind == "set" else "append"
|
|
174
|
+
call = nodes.Call(nodes.Attribute(result, method, span), (element,), (), span)
|
|
175
|
+
innermost = nodes.ExpressionStatement(call, span)
|
|
176
|
+
|
|
177
|
+
body: tuple[nodes.Statement, ...] = (innermost,)
|
|
178
|
+
for index in range(len(node.generators) - 1, -1, -1):
|
|
179
|
+
generator = node.generators[index]
|
|
180
|
+
for condition in reversed(generator.conditions):
|
|
181
|
+
body = (nodes.If(_rename(condition, renames), body, (), condition.span),)
|
|
182
|
+
iterable = _rename(generator.iterable, renames) if index else self.hoist(generator.iterable, pending)
|
|
183
|
+
if isinstance(generator.target, nodes.Name):
|
|
184
|
+
target = nodes.Name(renames[generator.target.identifier], generator.target.span)
|
|
185
|
+
else:
|
|
186
|
+
target = self.hidden("item", generator.target.span)
|
|
187
|
+
body = (nodes.Assign(_rename_target(generator.target, renames), target, generator.target.span), *body)
|
|
188
|
+
body = (nodes.For(target, iterable, body, False, generator.span),)
|
|
189
|
+
pending.extend(body)
|
|
190
|
+
return result
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------ blocks
|
|
193
|
+
|
|
194
|
+
def new_id(self, kind: str) -> BlockId:
|
|
195
|
+
self.counters[kind] = self.counters.get(kind, 0) + 1
|
|
196
|
+
return BlockId(f"{kind}_{self.counters[kind]}")
|
|
197
|
+
|
|
198
|
+
def finish(self, block: _Open, terminator: Terminator) -> None:
|
|
199
|
+
self.blocks[block.id] = BasicBlock(
|
|
200
|
+
block.id, tuple(block.statements), terminator, self.exception_targets()
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def header(self, block_id: BlockId, terminator: Terminator) -> None:
|
|
204
|
+
self.blocks[block_id] = BasicBlock(block_id, (), terminator, self.exception_targets())
|
|
205
|
+
|
|
206
|
+
def exception_targets(self) -> tuple[BlockId, ...]:
|
|
207
|
+
return self.handlers[-1] if self.handlers else ()
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------ statements
|
|
210
|
+
|
|
211
|
+
def sequence(
|
|
212
|
+
self,
|
|
213
|
+
statements: tuple[nodes.Statement, ...],
|
|
214
|
+
block: _Open,
|
|
215
|
+
continuation: BlockId | None,
|
|
216
|
+
join_span: SourceSpan,
|
|
217
|
+
) -> _Open | None:
|
|
218
|
+
"""Lay ``statements`` out from ``block``.
|
|
219
|
+
|
|
220
|
+
Returns the block left open at the end, or ``None`` when control left the
|
|
221
|
+
sequence. With a ``continuation``, an open end jumps there instead.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
current: _Open | None = block
|
|
225
|
+
last_index = len(statements) - 1
|
|
226
|
+
for index, statement in enumerate(statements):
|
|
227
|
+
assert current is not None
|
|
228
|
+
is_last = index == last_index
|
|
229
|
+
current = self.statement(statement, current, continuation if is_last else None)
|
|
230
|
+
if current is None and not is_last:
|
|
231
|
+
current = _Open(self.new_id("dead"))
|
|
232
|
+
if current is not None and continuation is not None:
|
|
233
|
+
self.finish(current, Jump(continuation, join_span))
|
|
234
|
+
return None
|
|
235
|
+
return current
|
|
236
|
+
|
|
237
|
+
def statement(
|
|
238
|
+
self, node: nodes.Statement, block: _Open, continuation: BlockId | None
|
|
239
|
+
) -> _Open | None:
|
|
240
|
+
node, block = self.desugared(node, block)
|
|
241
|
+
if isinstance(node, nodes.Return):
|
|
242
|
+
self.finish(block, Return(node.value, node.span))
|
|
243
|
+
return None
|
|
244
|
+
if isinstance(node, nodes.Raise):
|
|
245
|
+
self.finish(block, Raise(node.exception, node.span, node.cause))
|
|
246
|
+
return None
|
|
247
|
+
if isinstance(node, nodes.Break):
|
|
248
|
+
self.finish(block, Jump(self.loop("break", node.span)[1], node.span))
|
|
249
|
+
return None
|
|
250
|
+
if isinstance(node, nodes.Continue):
|
|
251
|
+
self.finish(block, Jump(self.loop("continue", node.span)[0], node.span))
|
|
252
|
+
return None
|
|
253
|
+
if isinstance(node, nodes.If):
|
|
254
|
+
return self.conditional(node, block, continuation)
|
|
255
|
+
if isinstance(node, nodes.While | nodes.For):
|
|
256
|
+
return self.loop_statement(node, block, continuation)
|
|
257
|
+
if isinstance(node, nodes.Match):
|
|
258
|
+
return self.match_statement(node, block, continuation)
|
|
259
|
+
if isinstance(node, nodes.With):
|
|
260
|
+
return self.with_statement(node, block)
|
|
261
|
+
if isinstance(node, nodes.Try):
|
|
262
|
+
return self.try_statement(node, block, continuation)
|
|
263
|
+
block.statements.append(node)
|
|
264
|
+
return block
|
|
265
|
+
|
|
266
|
+
def try_statement(
|
|
267
|
+
self, node: nodes.Try, block: _Open, continuation: BlockId | None
|
|
268
|
+
) -> _Open | None:
|
|
269
|
+
"""Body blocks carry exception edges to every handler; handlers, ``else`` and
|
|
270
|
+
``finally`` join on the normal path (exceptional exits are approximated)."""
|
|
271
|
+
|
|
272
|
+
body_id = self.new_id("try")
|
|
273
|
+
handler_ids = tuple(self.new_id("handler") for _ in node.handlers)
|
|
274
|
+
else_id = self.new_id("else") if node.orelse else None
|
|
275
|
+
final_id = self.new_id("finally") if node.finalbody else None
|
|
276
|
+
after_id = continuation if continuation is not None and final_id is None else None
|
|
277
|
+
if after_id is None:
|
|
278
|
+
after_id = self.new_id("after") if final_id is None or continuation is None else None
|
|
279
|
+
join = final_id if final_id is not None else after_id
|
|
280
|
+
assert join is not None
|
|
281
|
+
|
|
282
|
+
self.finish(block, Jump(body_id, node.span))
|
|
283
|
+
self.handlers.append(handler_ids)
|
|
284
|
+
try:
|
|
285
|
+
# The whole body, including the block that leaves it, may raise into a handler.
|
|
286
|
+
end = self.sequence(node.body, _Open(body_id), None, node.span)
|
|
287
|
+
if end is not None:
|
|
288
|
+
self.finish(end, Jump(else_id if else_id is not None else join, node.span))
|
|
289
|
+
finally:
|
|
290
|
+
self.handlers.pop()
|
|
291
|
+
if end is not None and else_id is not None:
|
|
292
|
+
self.sequence(node.orelse, _Open(else_id), join, node.span)
|
|
293
|
+
for handler, handler_id in zip(node.handlers, handler_ids, strict=True):
|
|
294
|
+
opened = _Open(handler_id)
|
|
295
|
+
opened.statements.append(nodes.EnterHandler(handler, handler.span))
|
|
296
|
+
self.sequence(handler.body, opened, join, node.span)
|
|
297
|
+
if final_id is not None:
|
|
298
|
+
target = continuation if continuation is not None else after_id
|
|
299
|
+
assert target is not None
|
|
300
|
+
self.sequence(node.finalbody, _Open(final_id), target, node.span)
|
|
301
|
+
return None if continuation is not None else _Open(target)
|
|
302
|
+
return None if continuation is not None else _Open(after_id) # type: ignore[arg-type]
|
|
303
|
+
|
|
304
|
+
def with_statement(self, node: nodes.With, block: _Open) -> _Open | None:
|
|
305
|
+
"""Lay the body out inline; an early exit skips the ``ExitWith`` statements."""
|
|
306
|
+
|
|
307
|
+
for item in node.items:
|
|
308
|
+
block.statements.append(nodes.EnterWith(item, node.span))
|
|
309
|
+
current = self.sequence(node.body, block, None, node.span)
|
|
310
|
+
if current is None:
|
|
311
|
+
return None
|
|
312
|
+
for item in reversed(node.items):
|
|
313
|
+
current.statements.append(nodes.ExitWith(item, node.span))
|
|
314
|
+
return current
|
|
315
|
+
|
|
316
|
+
def conditional(
|
|
317
|
+
self, node: nodes.If, block: _Open, continuation: BlockId | None
|
|
318
|
+
) -> _Open | None:
|
|
319
|
+
merge = continuation if continuation is not None else self.new_id("merge")
|
|
320
|
+
then_id = self.new_id("then")
|
|
321
|
+
else_id = self.new_id("else") if node.orelse else merge
|
|
322
|
+
self.finish(block, Branch(node.condition, then_id, else_id, node.span))
|
|
323
|
+
self.sequence(node.body, _Open(then_id), merge, node.span)
|
|
324
|
+
if node.orelse:
|
|
325
|
+
self.sequence(node.orelse, _Open(else_id), merge, node.span)
|
|
326
|
+
return None if continuation is not None else _Open(merge)
|
|
327
|
+
|
|
328
|
+
def loop_statement(
|
|
329
|
+
self, node: nodes.While | nodes.For, block: _Open, continuation: BlockId | None
|
|
330
|
+
) -> _Open | None:
|
|
331
|
+
header_id = self.new_id("loop")
|
|
332
|
+
body_id = self.new_id("body")
|
|
333
|
+
after_id = continuation if continuation is not None else self.new_id("exit")
|
|
334
|
+
# An ``else`` clause runs when the loop is exhausted; ``break`` skips it.
|
|
335
|
+
exit_id = self.new_id("else") if node.orelse else after_id
|
|
336
|
+
self.finish(block, Jump(header_id, node.span))
|
|
337
|
+
if isinstance(node, nodes.While):
|
|
338
|
+
self.header(header_id, Branch(node.condition, body_id, exit_id, node.span))
|
|
339
|
+
else:
|
|
340
|
+
self.header(header_id, ForEach(node.target, node.iterable, body_id, exit_id, node.span))
|
|
341
|
+
self.loops.append((header_id, after_id))
|
|
342
|
+
try:
|
|
343
|
+
self.sequence(node.body, _Open(body_id), header_id, node.span)
|
|
344
|
+
finally:
|
|
345
|
+
self.loops.pop()
|
|
346
|
+
if node.orelse:
|
|
347
|
+
self.sequence(node.orelse, _Open(exit_id), after_id, node.span)
|
|
348
|
+
return None if continuation is not None else _Open(after_id)
|
|
349
|
+
|
|
350
|
+
def match_statement(
|
|
351
|
+
self, node: nodes.Match, block: _Open, continuation: BlockId | None
|
|
352
|
+
) -> _Open | None:
|
|
353
|
+
"""``match`` as an ``if`` chain over a hidden subject: literal, singleton, capture,
|
|
354
|
+
wildcard and or-patterns, with guards; other patterns are reported."""
|
|
355
|
+
|
|
356
|
+
subject = self.hidden("match", node.span)
|
|
357
|
+
statements: tuple[nodes.Statement, ...] = ()
|
|
358
|
+
for case in reversed(node.cases):
|
|
359
|
+
condition, bindings = self.pattern(case.pattern, subject)
|
|
360
|
+
if case.guard is not None:
|
|
361
|
+
condition = nodes.BoolOp("and", (condition, case.guard), case.span)
|
|
362
|
+
statements = (*bindings, nodes.If(condition, case.body, statements, case.span))
|
|
363
|
+
chain = (nodes.Assign(subject, node.subject, node.span), *statements)
|
|
364
|
+
return self.sequence(chain, block, continuation, node.span)
|
|
365
|
+
|
|
366
|
+
def pattern(
|
|
367
|
+
self, node: nodes.Pattern, subject: nodes.Expression
|
|
368
|
+
) -> tuple[nodes.Expression, tuple[nodes.Statement, ...]]:
|
|
369
|
+
"""The condition a pattern tests on ``subject`` and the names it binds."""
|
|
370
|
+
|
|
371
|
+
if isinstance(node, nodes.ValuePattern):
|
|
372
|
+
return nodes.Compare("eq", subject, node.value, node.span), ()
|
|
373
|
+
if isinstance(node, nodes.SingletonPattern):
|
|
374
|
+
return nodes.Compare("is", subject, nodes.Constant(node.value, node.span), node.span), ()
|
|
375
|
+
if isinstance(node, nodes.WildcardPattern):
|
|
376
|
+
return nodes.Constant(True, node.span), ()
|
|
377
|
+
if isinstance(node, nodes.CapturePattern):
|
|
378
|
+
binding = nodes.Assign(nodes.Name(node.name, node.span), subject, node.span)
|
|
379
|
+
if node.pattern is None:
|
|
380
|
+
return nodes.Constant(True, node.span), (binding,)
|
|
381
|
+
condition, inner = self.pattern(node.pattern, subject)
|
|
382
|
+
return condition, (*inner, binding)
|
|
383
|
+
if isinstance(node, nodes.OrPattern):
|
|
384
|
+
conditions: list[nodes.Expression] = []
|
|
385
|
+
bindings: list[nodes.Statement] = []
|
|
386
|
+
for alternative in node.alternatives:
|
|
387
|
+
condition, inner = self.pattern(alternative, subject)
|
|
388
|
+
conditions.append(condition)
|
|
389
|
+
bindings.extend(inner)
|
|
390
|
+
return nodes.BoolOp("or", tuple(conditions), node.span), tuple(bindings)
|
|
391
|
+
if isinstance(node, nodes.SequencePattern):
|
|
392
|
+
return self.sequence_pattern(node, subject)
|
|
393
|
+
if isinstance(node, nodes.MappingPattern):
|
|
394
|
+
return self.mapping_pattern(node, subject)
|
|
395
|
+
if isinstance(node, nodes.ClassPattern):
|
|
396
|
+
return self.class_pattern(node, subject)
|
|
397
|
+
if isinstance(node, nodes.StarPattern):
|
|
398
|
+
raise CFGError(f"{node.span.display()}: a star pattern only belongs in a sequence pattern")
|
|
399
|
+
raise CFGError(f"{node.span.display()}: match pattern {node.kind} is not supported yet")
|
|
400
|
+
|
|
401
|
+
def sequence_pattern(
|
|
402
|
+
self, node: nodes.SequencePattern, subject: nodes.Expression
|
|
403
|
+
) -> tuple[nodes.Expression, tuple[nodes.Statement, ...]]:
|
|
404
|
+
"""``[a, 0, *rest, b]``: the length fits, each item matches its sub-pattern and
|
|
405
|
+
the star captures the middle slice."""
|
|
406
|
+
|
|
407
|
+
span = node.span
|
|
408
|
+
count = len(node.patterns)
|
|
409
|
+
star = next((i for i, p in enumerate(node.patterns) if isinstance(p, nodes.StarPattern)), None)
|
|
410
|
+
length = nodes.Call(nodes.Name("len", span), (subject,), (), span)
|
|
411
|
+
if star is None:
|
|
412
|
+
conditions: list[nodes.Expression] = [nodes.Compare("eq", length, nodes.Constant(count, span), span)]
|
|
413
|
+
else:
|
|
414
|
+
conditions = [nodes.Compare("gt_eq", length, nodes.Constant(count - 1, span), span)]
|
|
415
|
+
bindings: list[nodes.Statement] = []
|
|
416
|
+
for index, sub in enumerate(node.patterns):
|
|
417
|
+
if isinstance(sub, nodes.StarPattern):
|
|
418
|
+
if sub.name is not None:
|
|
419
|
+
upper = None if index == count - 1 else nodes.Constant(index - count + 1, span)
|
|
420
|
+
piece = nodes.Subscript(subject, nodes.Slice(nodes.Constant(index, span), upper, None, span), span)
|
|
421
|
+
bindings.append(nodes.Assign(nodes.Name(sub.name, sub.span), piece, sub.span))
|
|
422
|
+
continue
|
|
423
|
+
position = index if star is None or index < star else index - count
|
|
424
|
+
item = nodes.Subscript(subject, nodes.Constant(position, span), span)
|
|
425
|
+
condition, inner = self.pattern(sub, item)
|
|
426
|
+
conditions.append(condition)
|
|
427
|
+
bindings.extend(inner)
|
|
428
|
+
return _conjunction(conditions, span), tuple(bindings)
|
|
429
|
+
|
|
430
|
+
def mapping_pattern(
|
|
431
|
+
self, node: nodes.MappingPattern, subject: nodes.Expression
|
|
432
|
+
) -> tuple[nodes.Expression, tuple[nodes.Statement, ...]]:
|
|
433
|
+
"""``{key: pattern, **rest}``: every key is present and its value matches."""
|
|
434
|
+
|
|
435
|
+
span = node.span
|
|
436
|
+
conditions: list[nodes.Expression] = []
|
|
437
|
+
bindings: list[nodes.Statement] = []
|
|
438
|
+
for key, sub in zip(node.keys, node.patterns, strict=True):
|
|
439
|
+
conditions.append(nodes.Compare("in", key, subject, span))
|
|
440
|
+
condition, inner = self.pattern(sub, nodes.Subscript(subject, key, span))
|
|
441
|
+
conditions.append(condition)
|
|
442
|
+
bindings.extend(inner)
|
|
443
|
+
if node.rest is not None:
|
|
444
|
+
bindings.append(nodes.Assign(nodes.Name(node.rest, span), subject, span))
|
|
445
|
+
return _conjunction(conditions, span), tuple(bindings)
|
|
446
|
+
|
|
447
|
+
def class_pattern(
|
|
448
|
+
self, node: nodes.ClassPattern, subject: nodes.Expression
|
|
449
|
+
) -> tuple[nodes.Expression, tuple[nodes.Statement, ...]]:
|
|
450
|
+
"""``Cls(name=pattern)``: an instance of the class whose attributes match."""
|
|
451
|
+
|
|
452
|
+
span = node.span
|
|
453
|
+
conditions: list[nodes.Expression] = [
|
|
454
|
+
nodes.Call(nodes.Name("isinstance", span), (subject, node.cls), (), span)
|
|
455
|
+
]
|
|
456
|
+
bindings: list[nodes.Statement] = []
|
|
457
|
+
# Positional sub-patterns match the attributes ``__match_args__`` names, known
|
|
458
|
+
# for the module's classes; an unknown class gets a conservative position.
|
|
459
|
+
known = self.match_args.get(node.cls.identifier, ()) if isinstance(node.cls, nodes.Name) else ()
|
|
460
|
+
positional = [
|
|
461
|
+
(known[i] if i < len(known) else f"_match_arg_{i}", sub) for i, sub in enumerate(node.patterns)
|
|
462
|
+
]
|
|
463
|
+
for name, sub in (*positional, *zip(node.keyword_names, node.keyword_patterns, strict=True)):
|
|
464
|
+
condition, inner = self.pattern(sub, nodes.Attribute(subject, name, span))
|
|
465
|
+
conditions.append(condition)
|
|
466
|
+
bindings.extend(inner)
|
|
467
|
+
return _conjunction(conditions, span), tuple(bindings)
|
|
468
|
+
|
|
469
|
+
def loop(self, keyword: str, span: SourceSpan) -> tuple[BlockId, BlockId]:
|
|
470
|
+
if not self.loops:
|
|
471
|
+
raise CFGError(f"{span.display()}: '{keyword}' outside loop")
|
|
472
|
+
return self.loops[-1]
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _conjunction(conditions: list[nodes.Expression], span: SourceSpan) -> nodes.Expression:
|
|
476
|
+
return conditions[0] if len(conditions) == 1 else nodes.BoolOp("and", tuple(conditions), span)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _may_hold_expressions(value: object) -> bool:
|
|
480
|
+
return isinstance(value, tuple) or (is_dataclass(value) and not isinstance(value, type))
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _has_control_flow(node: object) -> bool:
|
|
484
|
+
if isinstance(node, nodes.Conditional | nodes.Comprehension):
|
|
485
|
+
return True
|
|
486
|
+
if isinstance(node, nodes.Lambda):
|
|
487
|
+
return False
|
|
488
|
+
if isinstance(node, tuple):
|
|
489
|
+
return any(_has_control_flow(item) for item in node)
|
|
490
|
+
if is_dataclass(node) and not isinstance(node, type):
|
|
491
|
+
return any(_has_control_flow(getattr(node, f.name)) for f in fields(node) if f.name != "span")
|
|
492
|
+
return False
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _bound_names(target: nodes.Target) -> list[str]:
|
|
496
|
+
if isinstance(target, nodes.Name):
|
|
497
|
+
return [target.identifier]
|
|
498
|
+
if isinstance(target, nodes.Tuple):
|
|
499
|
+
return [name for element in target.elements for name in _bound_names(element)] # type: ignore[arg-type]
|
|
500
|
+
return []
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _rename(node: Any, renames: dict[str, str]) -> Any:
|
|
504
|
+
"""``node`` with comprehension variables replaced by their synthetic locals, without
|
|
505
|
+
entering scopes that rebind them."""
|
|
506
|
+
|
|
507
|
+
if not renames:
|
|
508
|
+
return node
|
|
509
|
+
if isinstance(node, nodes.Name):
|
|
510
|
+
return nodes.Name(renames[node.identifier], node.span) if node.identifier in renames else node
|
|
511
|
+
if isinstance(node, nodes.Lambda):
|
|
512
|
+
inner = {k: v for k, v in renames.items() if k not in {p.name for p in node.parameters}}
|
|
513
|
+
return replace(node, body=_rename(node.body, inner))
|
|
514
|
+
if isinstance(node, nodes.Comprehension):
|
|
515
|
+
shadowed = {name for g in node.generators for name in _bound_names(g.target)}
|
|
516
|
+
inner = {k: v for k, v in renames.items() if k not in shadowed}
|
|
517
|
+
generators = tuple(
|
|
518
|
+
replace(
|
|
519
|
+
g,
|
|
520
|
+
iterable=_rename(g.iterable, renames if index == 0 else inner),
|
|
521
|
+
conditions=_rename(g.conditions, inner),
|
|
522
|
+
)
|
|
523
|
+
for index, g in enumerate(node.generators)
|
|
524
|
+
)
|
|
525
|
+
key = _rename(node.key, inner) if node.key is not None else None
|
|
526
|
+
return replace(node, element=_rename(node.element, inner), generators=generators, key=key)
|
|
527
|
+
if isinstance(node, tuple):
|
|
528
|
+
return tuple(_rename(item, renames) for item in node)
|
|
529
|
+
if is_dataclass(node) and not isinstance(node, type):
|
|
530
|
+
changes = {
|
|
531
|
+
f.name: _rename(getattr(node, f.name), renames)
|
|
532
|
+
for f in fields(node)
|
|
533
|
+
if f.name != "span" and _may_hold_expressions(getattr(node, f.name))
|
|
534
|
+
}
|
|
535
|
+
return replace(node, **changes) if changes else node
|
|
536
|
+
return node
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _rename_target(target: nodes.Target, renames: dict[str, str]) -> nodes.Target:
|
|
540
|
+
renamed = _rename(target, renames)
|
|
541
|
+
assert isinstance(renamed, nodes.Name | nodes.Tuple | nodes.Attribute | nodes.Subscript)
|
|
542
|
+
return renamed
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def build_cfg(function: nodes.Function, match_args: Mapping[str, tuple[str, ...]] | None = None) -> CFG:
|
|
546
|
+
return _Builder(function, match_args).build()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def match_args_of(module: nodes.Module) -> dict[str, tuple[str, ...]]:
|
|
550
|
+
"""``__match_args__`` of the module's classes: explicit, or the field order of a
|
|
551
|
+
dataclass (bare annotated declarations and assignments, in order)."""
|
|
552
|
+
|
|
553
|
+
found: dict[str, tuple[str, ...]] = {}
|
|
554
|
+
for statement in module.body:
|
|
555
|
+
if not isinstance(statement, nodes.Class):
|
|
556
|
+
continue
|
|
557
|
+
explicit = None
|
|
558
|
+
fields: list[str] = []
|
|
559
|
+
for member in statement.body:
|
|
560
|
+
if isinstance(member, nodes.Assign) and isinstance(member.target, nodes.Name):
|
|
561
|
+
if member.target.identifier == "__match_args__" and isinstance(member.value, nodes.Tuple | nodes.List):
|
|
562
|
+
explicit = tuple(
|
|
563
|
+
e.value for e in member.value.elements if isinstance(e, nodes.Constant) and isinstance(e.value, str)
|
|
564
|
+
)
|
|
565
|
+
elif not member.target.identifier.startswith("_"):
|
|
566
|
+
fields.append(member.target.identifier)
|
|
567
|
+
elif isinstance(member, nodes.Declaration):
|
|
568
|
+
fields.append(member.name)
|
|
569
|
+
if explicit is not None:
|
|
570
|
+
found[statement.name] = explicit
|
|
571
|
+
elif any(_is_dataclass(d) for d in statement.decorators):
|
|
572
|
+
found[statement.name] = tuple(fields)
|
|
573
|
+
return found
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _is_dataclass(decorator: nodes.Expression) -> bool:
|
|
577
|
+
if isinstance(decorator, nodes.Call):
|
|
578
|
+
decorator = decorator.callee
|
|
579
|
+
if isinstance(decorator, nodes.Name):
|
|
580
|
+
return decorator.identifier == "dataclass"
|
|
581
|
+
return isinstance(decorator, nodes.Attribute) and decorator.name == "dataclass"
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class CFGAnalysis(FunctionAnalysis[CFG]):
|
|
585
|
+
name: ClassVar[str] = "cfg.function"
|
|
586
|
+
|
|
587
|
+
@classmethod
|
|
588
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> CFG:
|
|
589
|
+
return build_cfg(function, match_args_of(ctx.module))
|