codeanalyzer-python 0.3.1__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 +51 -4
- codeanalyzer/core.py +153 -82
- 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/bolt.py +19 -4
- codeanalyzer/neo4j/cypher.py +9 -3
- codeanalyzer/neo4j/emit.py +8 -3
- codeanalyzer/neo4j/project.py +241 -60
- codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer/neo4j/schema.py +43 -7
- codeanalyzer/options/options.py +4 -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 +141 -30
- 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/symbol_table_builder.py +29 -10
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +230 -43
- codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
- codeanalyzer_python-0.3.1.dist-info/RECORD +0 -39
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
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 3b of the level-3 dataflow ladder: reaching definitions → DDG edges.
|
|
18
|
+
|
|
19
|
+
Classic forward may-analysis with a worklist over the statement-level CFG.
|
|
20
|
+
SSA is an implementation shortcut some ecosystems get for free; the contract
|
|
21
|
+
is the def-use edges, and Python hand-builds them.
|
|
22
|
+
|
|
23
|
+
Kill discipline (sound-leaning):
|
|
24
|
+
|
|
25
|
+
- A def of a bare local/param path strong-kills earlier defs of the exact
|
|
26
|
+
same path.
|
|
27
|
+
- Defs of attribute paths strong-kill only the identical path string (a write
|
|
28
|
+
through one name never kills a potentially-aliased other name).
|
|
29
|
+
- Subscript (``[*]``) and k-truncated (``.*``) paths are weak updates — they
|
|
30
|
+
kill nothing.
|
|
31
|
+
|
|
32
|
+
A use matches a reaching def when the paths interfere textually (exact /
|
|
33
|
+
prefix / wildcard — :func:`access_paths.interferes`) or when the may-alias
|
|
34
|
+
oracle says two suffixed paths can denote one location.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from typing import Dict, List, Set, Tuple
|
|
41
|
+
|
|
42
|
+
from codeanalyzer.dataflow.access_paths import StatementFacts, interferes, suffix_of
|
|
43
|
+
from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
|
|
44
|
+
from codeanalyzer.dataflow.cfg import ControlFlowGraph
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class DDGEdge:
|
|
49
|
+
source: int # the def node
|
|
50
|
+
target: int # the use node
|
|
51
|
+
var: str # the access path being read
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _strong_kill(path: str) -> bool:
|
|
55
|
+
return not path.endswith("*")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reaching_definitions(
|
|
59
|
+
cfg: ControlFlowGraph, facts: Dict[int, StatementFacts]
|
|
60
|
+
) -> Dict[int, Set[Tuple[str, int]]]:
|
|
61
|
+
"""IN sets: ``{node: {(path, def_node), ...}}`` via worklist iteration."""
|
|
62
|
+
preds = cfg.predecessors()
|
|
63
|
+
succs = cfg.successors()
|
|
64
|
+
node_ids = [n.id for n in cfg.nodes]
|
|
65
|
+
|
|
66
|
+
gen: Dict[int, Set[Tuple[str, int]]] = {}
|
|
67
|
+
for nid in node_ids:
|
|
68
|
+
gen[nid] = {(d, nid) for d in facts[nid].defs}
|
|
69
|
+
|
|
70
|
+
in_sets: Dict[int, Set[Tuple[str, int]]] = {nid: set() for nid in node_ids}
|
|
71
|
+
out_sets: Dict[int, Set[Tuple[str, int]]] = {nid: set() for nid in node_ids}
|
|
72
|
+
|
|
73
|
+
worklist = list(node_ids)
|
|
74
|
+
while worklist:
|
|
75
|
+
nid = worklist.pop(0)
|
|
76
|
+
new_in: Set[Tuple[str, int]] = set()
|
|
77
|
+
for p, _ in preds[nid]:
|
|
78
|
+
new_in |= out_sets[p]
|
|
79
|
+
strong = {d for d in facts[nid].defs if _strong_kill(d)}
|
|
80
|
+
new_out = {(p, m) for (p, m) in new_in if p not in strong} | gen[nid]
|
|
81
|
+
if new_in != in_sets[nid] or new_out != out_sets[nid]:
|
|
82
|
+
in_sets[nid] = new_in
|
|
83
|
+
out_sets[nid] = new_out
|
|
84
|
+
for s, _ in succs[nid]:
|
|
85
|
+
if s not in worklist:
|
|
86
|
+
worklist.append(s)
|
|
87
|
+
return in_sets
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ddg_edges(
|
|
91
|
+
cfg: ControlFlowGraph,
|
|
92
|
+
facts: Dict[int, StatementFacts],
|
|
93
|
+
oracle: TypeBasedAliasOracle,
|
|
94
|
+
) -> List[DDGEdge]:
|
|
95
|
+
"""Def-use edges: for every use at node n, an edge from each reaching def
|
|
96
|
+
whose path interferes (textually or through may-alias)."""
|
|
97
|
+
in_sets = reaching_definitions(cfg, facts)
|
|
98
|
+
edges: Set[DDGEdge] = set()
|
|
99
|
+
for node in cfg.nodes:
|
|
100
|
+
uses = facts[node.id].uses
|
|
101
|
+
if not uses:
|
|
102
|
+
continue
|
|
103
|
+
reaching = in_sets[node.id]
|
|
104
|
+
# A (path, n) pair reaches n itself only through a real cycle, so a
|
|
105
|
+
# self-edge here is precisely the loop-carried dependency.
|
|
106
|
+
for use in uses:
|
|
107
|
+
for def_path, def_node in reaching:
|
|
108
|
+
if interferes(use, def_path) or (
|
|
109
|
+
(suffix_of(use) or suffix_of(def_path))
|
|
110
|
+
and oracle.may_alias(use, def_path)
|
|
111
|
+
):
|
|
112
|
+
edges.add(DDGEdge(source=def_node, target=node.id, var=use))
|
|
113
|
+
return sorted(edges, key=lambda e: (e.source, e.target, e.var))
|
|
@@ -0,0 +1,140 @@
|
|
|
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 2 of the level-3 dataflow ladder: dominance and control dependence.
|
|
18
|
+
|
|
19
|
+
Post-dominators are computed with the Cooper–Harper–Kennedy iterative
|
|
20
|
+
algorithm over the reverse CFG. Infinite loops are already normalized by the
|
|
21
|
+
CFG builder (synthetic escape edge to EXIT), so the post-dominator tree always
|
|
22
|
+
has the unique root EXIT.
|
|
23
|
+
|
|
24
|
+
Control dependence follows Ferrante–Ottenstein–Warren: for each CFG edge
|
|
25
|
+
``(a, b)`` where ``b`` does not post-dominate ``a``, every node on the
|
|
26
|
+
post-dominator-tree path from ``b`` up to (but not including) ``a``'s
|
|
27
|
+
immediate post-dominator is control-dependent on ``a``.
|
|
28
|
+
|
|
29
|
+
Nodes with no branch-node control dependence are control-dependent on ENTRY —
|
|
30
|
+
the conventional region root, which keeps every statement anchored in the PDG
|
|
31
|
+
and gives interprocedural traversals a path from a callee's ENTRY to its
|
|
32
|
+
unconditional statements.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Dict, List, Set, Tuple
|
|
38
|
+
|
|
39
|
+
from codeanalyzer.dataflow.cfg import ControlFlowGraph
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _postorder(adj: Dict[int, List[int]], root: int) -> List[int]:
|
|
43
|
+
"""Iterative DFS postorder over ``adj`` from ``root``."""
|
|
44
|
+
order: List[int] = []
|
|
45
|
+
visited: Set[int] = set()
|
|
46
|
+
stack: List[Tuple[int, int]] = [(root, 0)]
|
|
47
|
+
visited.add(root)
|
|
48
|
+
while stack:
|
|
49
|
+
node, i = stack.pop()
|
|
50
|
+
children = adj.get(node, [])
|
|
51
|
+
if i < len(children):
|
|
52
|
+
stack.append((node, i + 1))
|
|
53
|
+
child = children[i]
|
|
54
|
+
if child not in visited:
|
|
55
|
+
visited.add(child)
|
|
56
|
+
stack.append((child, 0))
|
|
57
|
+
else:
|
|
58
|
+
order.append(node)
|
|
59
|
+
return order
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def post_dominators(cfg: ControlFlowGraph) -> Dict[int, int]:
|
|
63
|
+
"""Immediate post-dominator of every node, as ``{node: ipdom}``.
|
|
64
|
+
|
|
65
|
+
EXIT is its own post-dominator (the tree root). Cooper–Harper–Kennedy
|
|
66
|
+
("A Simple, Fast Dominance Algorithm") run on the reverse CFG.
|
|
67
|
+
"""
|
|
68
|
+
# Reverse CFG: successors of n are the CFG predecessors of n.
|
|
69
|
+
radj: Dict[int, List[int]] = {n.id: [] for n in cfg.nodes}
|
|
70
|
+
rpred: Dict[int, List[int]] = {n.id: [] for n in cfg.nodes}
|
|
71
|
+
for e in cfg.edges:
|
|
72
|
+
if e.source == e.target:
|
|
73
|
+
continue # self-loops carry no dominance information
|
|
74
|
+
radj[e.target].append(e.source)
|
|
75
|
+
rpred[e.source].append(e.target)
|
|
76
|
+
|
|
77
|
+
root = cfg.exit_id
|
|
78
|
+
post = _postorder(radj, root)
|
|
79
|
+
number = {n: i for i, n in enumerate(post)} # postorder number
|
|
80
|
+
rpo = list(reversed(post)) # reverse postorder: root first
|
|
81
|
+
|
|
82
|
+
ipdom: Dict[int, int] = {root: root}
|
|
83
|
+
|
|
84
|
+
def intersect(a: int, b: int) -> int:
|
|
85
|
+
while a != b:
|
|
86
|
+
while number[a] < number[b]:
|
|
87
|
+
a = ipdom[a]
|
|
88
|
+
while number[b] < number[a]:
|
|
89
|
+
b = ipdom[b]
|
|
90
|
+
return a
|
|
91
|
+
|
|
92
|
+
changed = True
|
|
93
|
+
while changed:
|
|
94
|
+
changed = False
|
|
95
|
+
for node in rpo:
|
|
96
|
+
if node == root:
|
|
97
|
+
continue
|
|
98
|
+
preds = [p for p in rpred[node] if p in ipdom]
|
|
99
|
+
if not preds:
|
|
100
|
+
continue
|
|
101
|
+
new = preds[0]
|
|
102
|
+
for p in preds[1:]:
|
|
103
|
+
new = intersect(new, p)
|
|
104
|
+
if ipdom.get(node) != new:
|
|
105
|
+
ipdom[node] = new
|
|
106
|
+
changed = True
|
|
107
|
+
|
|
108
|
+
return ipdom
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def control_dependence(cfg: ControlFlowGraph) -> List[Tuple[int, int]]:
|
|
112
|
+
"""CDG edges ``(branch_node, dependent_node)`` per Ferrante–Ottenstein–
|
|
113
|
+
Warren, plus ENTRY-region edges for nodes with no other controller."""
|
|
114
|
+
ipdom = post_dominators(cfg)
|
|
115
|
+
|
|
116
|
+
deps: Set[Tuple[int, int]] = set()
|
|
117
|
+
for e in cfg.edges:
|
|
118
|
+
a, b = e.source, e.target
|
|
119
|
+
if a == b:
|
|
120
|
+
continue
|
|
121
|
+
# b post-dominates a iff b is an ancestor of a in the pdom tree.
|
|
122
|
+
runner = b
|
|
123
|
+
stop = ipdom.get(a)
|
|
124
|
+
# Walk from b up the post-dominator tree to (not including) ipdom(a).
|
|
125
|
+
while runner != stop and runner != a:
|
|
126
|
+
deps.add((a, runner))
|
|
127
|
+
nxt = ipdom.get(runner)
|
|
128
|
+
if nxt is None or nxt == runner:
|
|
129
|
+
break
|
|
130
|
+
runner = nxt
|
|
131
|
+
|
|
132
|
+
# ENTRY as the region root for otherwise-uncontrolled nodes.
|
|
133
|
+
controlled = {t for (_, t) in deps}
|
|
134
|
+
for n in cfg.nodes:
|
|
135
|
+
if n.id in (cfg.entry_id, cfg.exit_id):
|
|
136
|
+
continue
|
|
137
|
+
if n.id not in controlled:
|
|
138
|
+
deps.add((cfg.entry_id, n.id))
|
|
139
|
+
|
|
140
|
+
return sorted(deps)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Bijection between internal IR node ids (ints, per function) and their
|
|
2
|
+
canonical ids.
|
|
3
|
+
|
|
4
|
+
Two forms per node:
|
|
5
|
+
|
|
6
|
+
* **local** — the intra-callable id used as the ``body`` map key and as every
|
|
7
|
+
``cfg``/``cdg``/``ddg`` edge endpoint: ``"@entry"``/``"@exit"`` for the
|
|
8
|
+
synthetic CFG bookends, ``"line:col"`` for real statements. This matches the
|
|
9
|
+
key format L1 already uses for ``call`` nodes (see ``schema/l1_body.py``), so
|
|
10
|
+
an L1 body node and its coinciding CFG node land on the same key and L1 ⊆ L3
|
|
11
|
+
holds.
|
|
12
|
+
* **global** — ``"<callable can:// id>@<local>"``, the fully addressable id for
|
|
13
|
+
cross-callable references and the Neo4j PyCFGNode keys (a later task).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from typing import Dict, Iterable, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IdentityMap:
|
|
21
|
+
def __init__(self, callable_id: str, id_to_local: Dict[int, str]):
|
|
22
|
+
self._callable_id = callable_id
|
|
23
|
+
self._map = id_to_local
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def for_function(cls, callable_id: str, pdg, param_nodes=None) -> "IdentityMap":
|
|
27
|
+
cfg = pdg.cfg
|
|
28
|
+
m: Dict[int, str] = {}
|
|
29
|
+
for n in cfg.nodes:
|
|
30
|
+
if n.id == cfg.entry_id:
|
|
31
|
+
m[n.id] = "@entry"
|
|
32
|
+
elif n.id == cfg.exit_id:
|
|
33
|
+
m[n.id] = "@exit"
|
|
34
|
+
else:
|
|
35
|
+
m[n.id] = f"{n.start_line}:{n.start_column}"
|
|
36
|
+
im = cls(callable_id, m)
|
|
37
|
+
if param_nodes:
|
|
38
|
+
im._assign_param_locals(param_nodes)
|
|
39
|
+
return im
|
|
40
|
+
|
|
41
|
+
def _assign_param_locals(self, param_nodes) -> None:
|
|
42
|
+
"""Fold the L4 synthetic param vertices into ``_map`` so ``local`` /
|
|
43
|
+
``global_id`` resolve them uniformly with CFG nodes.
|
|
44
|
+
|
|
45
|
+
Canonical locals, per node ``kind`` (idx = position within the node's
|
|
46
|
+
``(kind, call_node)`` group, in ``param_nodes`` list order):
|
|
47
|
+
|
|
48
|
+
* ``formal_in`` → ``"@formal_in:<idx>"`` (always indexed);
|
|
49
|
+
* ``formal_out`` → ``"@formal_out"`` when the function has exactly one,
|
|
50
|
+
else ``"@formal_out:<idx>"``;
|
|
51
|
+
* ``actual_in`` → ``"<callsite-local>/actual_in:<idx>"`` (always
|
|
52
|
+
indexed), ``<callsite-local> = self.local(pn.call_node)``;
|
|
53
|
+
* ``actual_out`` → ``"<callsite-local>/actual_out"`` when the callsite
|
|
54
|
+
has exactly one, else ``"<callsite-local>/actual_out:<idx>"``.
|
|
55
|
+
"""
|
|
56
|
+
counts: Dict[Tuple[str, Optional[int]], int] = defaultdict(int)
|
|
57
|
+
for pn in param_nodes:
|
|
58
|
+
counts[(pn.kind, pn.call_node)] += 1
|
|
59
|
+
|
|
60
|
+
seen: Dict[Tuple[str, Optional[int]], int] = defaultdict(int)
|
|
61
|
+
for pn in param_nodes:
|
|
62
|
+
key = (pn.kind, pn.call_node)
|
|
63
|
+
idx = seen[key]
|
|
64
|
+
seen[key] += 1
|
|
65
|
+
n = counts[key]
|
|
66
|
+
if pn.kind == "formal_in":
|
|
67
|
+
local = f"@formal_in:{idx}"
|
|
68
|
+
elif pn.kind == "formal_out":
|
|
69
|
+
local = "@formal_out" if n == 1 else f"@formal_out:{idx}"
|
|
70
|
+
elif pn.kind == "actual_in":
|
|
71
|
+
cs = self.local(pn.call_node)
|
|
72
|
+
local = f"{cs}/actual_in:{idx}"
|
|
73
|
+
elif pn.kind == "actual_out":
|
|
74
|
+
cs = self.local(pn.call_node)
|
|
75
|
+
local = f"{cs}/actual_out" if n == 1 else f"{cs}/actual_out:{idx}"
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(f"unknown param node kind: {pn.kind!r}")
|
|
78
|
+
self._map[pn.id] = local
|
|
79
|
+
|
|
80
|
+
def local(self, node_id: int) -> str:
|
|
81
|
+
"""Intra-callable id: ``"@entry"``/``"@exit"`` or ``"line:col"``."""
|
|
82
|
+
return self._map[node_id]
|
|
83
|
+
|
|
84
|
+
def global_id(self, node_id: int) -> str:
|
|
85
|
+
"""Fully addressable id: ``"<callable-id>@<local>"``."""
|
|
86
|
+
loc = self._map[node_id]
|
|
87
|
+
# local statements are "line:col"; bookends already carry the leading "@"
|
|
88
|
+
return f"{self._callable_id}{loc}" if loc.startswith("@") else f"{self._callable_id}@{loc}"
|
|
89
|
+
|
|
90
|
+
def node_ids(self) -> Iterable[int]:
|
|
91
|
+
return self._map.keys()
|
|
@@ -0,0 +1,100 @@
|
|
|
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 4 of the level-3 dataflow ladder: PDG assembly.
|
|
18
|
+
|
|
19
|
+
Per callable, the PDG is the union of the stage-2 control-dependence edges
|
|
20
|
+
(``CDG``) and the stage-3 def-use edges (``DDG``), over the same
|
|
21
|
+
``(signature, node_id)`` nodes. Nothing new is computed here — this module is
|
|
22
|
+
bookkeeping plus the intraprocedural backward slice that gates it: reverse
|
|
23
|
+
reachability over CDG ∪ DDG from a criterion node, expected to match a
|
|
24
|
+
hand-computed node set exactly on the fixture.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import ast
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Dict, List, Optional, Set
|
|
32
|
+
|
|
33
|
+
from codeanalyzer.dataflow.access_paths import (
|
|
34
|
+
FunctionScope,
|
|
35
|
+
StatementFacts,
|
|
36
|
+
build_scope,
|
|
37
|
+
statement_facts,
|
|
38
|
+
)
|
|
39
|
+
from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
|
|
40
|
+
from codeanalyzer.dataflow.cfg import ControlFlowGraph, build_cfg
|
|
41
|
+
from codeanalyzer.dataflow.defuse import ddg_edges
|
|
42
|
+
from codeanalyzer.dataflow.dominance import control_dependence
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class PDGEdge:
|
|
47
|
+
source: int
|
|
48
|
+
target: int
|
|
49
|
+
type: str # "CDG" | "DDG"
|
|
50
|
+
var: Optional[str] = None # access path on DDG edges
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FunctionPDG:
|
|
55
|
+
"""One callable's intraprocedural graphs, keyed externally by signature."""
|
|
56
|
+
|
|
57
|
+
cfg: ControlFlowGraph
|
|
58
|
+
edges: List[PDGEdge]
|
|
59
|
+
scope: FunctionScope
|
|
60
|
+
facts: Dict[int, StatementFacts] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_pdg(
|
|
64
|
+
func: ast.AST,
|
|
65
|
+
enclosing_locals: Set[str],
|
|
66
|
+
oracle: TypeBasedAliasOracle,
|
|
67
|
+
k: int = 3,
|
|
68
|
+
global_qualifier: Optional[str] = None,
|
|
69
|
+
) -> FunctionPDG:
|
|
70
|
+
"""CFG → dominance → def-use → PDG for one callable."""
|
|
71
|
+
cfg = build_cfg(func)
|
|
72
|
+
scope = build_scope(func, enclosing_locals)
|
|
73
|
+
facts = statement_facts(cfg, func, scope, k, global_qualifier)
|
|
74
|
+
|
|
75
|
+
edges: List[PDGEdge] = [
|
|
76
|
+
PDGEdge(source=a, target=b, type="CDG") for a, b in control_dependence(cfg)
|
|
77
|
+
]
|
|
78
|
+
edges.extend(
|
|
79
|
+
PDGEdge(source=e.source, target=e.target, type="DDG", var=e.var)
|
|
80
|
+
for e in ddg_edges(cfg, facts, oracle)
|
|
81
|
+
)
|
|
82
|
+
edges.sort(key=lambda e: (e.source, e.target, e.type, e.var or ""))
|
|
83
|
+
return FunctionPDG(cfg=cfg, edges=edges, scope=scope, facts=facts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def intraprocedural_backward_slice(pdg: FunctionPDG, criterion: int) -> Set[int]:
|
|
87
|
+
"""Reverse reachability over CDG ∪ DDG from the criterion node (the
|
|
88
|
+
criterion itself is in the slice). The stage-4 gate."""
|
|
89
|
+
reverse: Dict[int, List[int]] = {}
|
|
90
|
+
for e in pdg.edges:
|
|
91
|
+
reverse.setdefault(e.target, []).append(e.source)
|
|
92
|
+
seen: Set[int] = set()
|
|
93
|
+
stack = [criterion]
|
|
94
|
+
while stack:
|
|
95
|
+
n = stack.pop()
|
|
96
|
+
if n in seen:
|
|
97
|
+
continue
|
|
98
|
+
seen.add(n)
|
|
99
|
+
stack.extend(reverse.get(n, []))
|
|
100
|
+
return seen
|