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,424 @@
|
|
|
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 7 of the level-3 dataflow ladder: SDG assembly (Horwitz–Reps–Binkley).
|
|
18
|
+
|
|
19
|
+
Parameter-passing structure per function and callsite:
|
|
20
|
+
|
|
21
|
+
- **formal_in** nodes: one per parameter (var = the parameter name), one per
|
|
22
|
+
captured variable (``<capture>:name``), one per transitively-read global
|
|
23
|
+
(``<global>:module::name``);
|
|
24
|
+
- **formal_out** nodes: the return value (``<return>``), each caller-visibly
|
|
25
|
+
mutated parameter, each written global;
|
|
26
|
+
- **actual_in / actual_out** nodes at each callsite, mirroring the callee's
|
|
27
|
+
formals that the callsite binds (positional/keyword-matched arguments, the
|
|
28
|
+
receiver as ``self``, globals from the callee's summary footprint);
|
|
29
|
+
- closure captures bind at the nested function's *definition* statement: an
|
|
30
|
+
``actual_in`` at the def node, ``PARAM_IN`` to the nested callable's
|
|
31
|
+
``<capture>`` formal.
|
|
32
|
+
|
|
33
|
+
Parameter nodes share the owning function's node-id space, allocated after
|
|
34
|
+
EXIT (the CFG keeps its ``ENTRY = 0 … EXIT = last CFG id`` contract; parameter
|
|
35
|
+
nodes are PDG/SDG-level, deterministically ordered). Intra-function wiring
|
|
36
|
+
(defs → formal_out, formal_in → uses, defs → actual_in, actual_out → callsite)
|
|
37
|
+
is emitted as ordinary DDG/CDG edges of the function's PDG; cross-function
|
|
38
|
+
``CALL`` / ``PARAM_IN`` / ``PARAM_OUT`` edges and same-signature ``SUMMARY``
|
|
39
|
+
edges (actual_in → actual_out, encoding the callee's transitive flow) form the
|
|
40
|
+
``sdg_edges`` section.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
47
|
+
|
|
48
|
+
from codeanalyzer.dataflow.access_paths import RETURN_PATH, base_of, interferes, suffix_of
|
|
49
|
+
from codeanalyzer.dataflow.defuse import DDGEdge
|
|
50
|
+
from codeanalyzer.dataflow.pdg import FunctionPDG, PDGEdge
|
|
51
|
+
from codeanalyzer.dataflow.summaries import (
|
|
52
|
+
CallSite,
|
|
53
|
+
FunctionInfo,
|
|
54
|
+
FunctionSummary,
|
|
55
|
+
solve_function,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
CAPTURE_PREFIX = "<capture>:"
|
|
59
|
+
GLOBAL_PREFIX = "<global>:"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ParamNode:
|
|
64
|
+
id: int
|
|
65
|
+
kind: str # formal_in | formal_out | actual_in | actual_out
|
|
66
|
+
var: str
|
|
67
|
+
call_node: Optional[int] = None # owning callsite statement (actuals)
|
|
68
|
+
start_line: int = -1
|
|
69
|
+
end_line: int = -1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class SDGEdge:
|
|
74
|
+
source_sig: str
|
|
75
|
+
source_node: int
|
|
76
|
+
target_sig: str
|
|
77
|
+
target_node: int
|
|
78
|
+
type: str # CALL | PARAM_IN | PARAM_OUT | SUMMARY
|
|
79
|
+
var: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class FunctionGraphs:
|
|
84
|
+
"""One callable's complete level-3 graphs, ready for emission."""
|
|
85
|
+
|
|
86
|
+
pdg: FunctionPDG
|
|
87
|
+
ddg: List[DDGEdge] = field(default_factory=list) # augmented, final
|
|
88
|
+
param_nodes: List[ParamNode] = field(default_factory=list)
|
|
89
|
+
extra_edges: List[PDGEdge] = field(default_factory=list) # param wiring
|
|
90
|
+
summary: Optional[FunctionSummary] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class ProgramGraphsIR:
|
|
95
|
+
functions: Dict[str, FunctionGraphs] = field(default_factory=dict)
|
|
96
|
+
sdg_edges: List[SDGEdge] = field(default_factory=list)
|
|
97
|
+
k_limit: int = 3
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _formal_key_to_var(key: str) -> str:
|
|
101
|
+
kind, _, name = key.partition(":")
|
|
102
|
+
if kind == "param":
|
|
103
|
+
return name
|
|
104
|
+
if kind == "capture":
|
|
105
|
+
return CAPTURE_PREFIX + name
|
|
106
|
+
return GLOBAL_PREFIX + name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _FunctionAssembler:
|
|
110
|
+
"""Allocates parameter nodes and wiring edges for one function."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, info: FunctionInfo, summary: FunctionSummary, facts, ddg):
|
|
113
|
+
self.info = info
|
|
114
|
+
self.summary = summary
|
|
115
|
+
self.facts = facts
|
|
116
|
+
self.ddg = ddg
|
|
117
|
+
self.cfg = info.pdg.cfg
|
|
118
|
+
self.scope = info.pdg.scope
|
|
119
|
+
self.next_id = len(self.cfg.nodes)
|
|
120
|
+
self.param_nodes: List[ParamNode] = []
|
|
121
|
+
self.extra: List[PDGEdge] = []
|
|
122
|
+
self.formal_in: Dict[str, int] = {} # var -> node id
|
|
123
|
+
self.formal_out: Dict[str, int] = {}
|
|
124
|
+
# (call_node, var) -> node id
|
|
125
|
+
self.actual_in: Dict[Tuple[int, str], int] = {}
|
|
126
|
+
self.actual_out: Dict[Tuple[int, str], int] = {}
|
|
127
|
+
entry = self.cfg.node_by_id(self.cfg.entry_id)
|
|
128
|
+
exit_ = self.cfg.node_by_id(self.cfg.exit_id)
|
|
129
|
+
self._entry_span = (entry.start_line, entry.end_line)
|
|
130
|
+
self._exit_span = (exit_.start_line, exit_.end_line)
|
|
131
|
+
|
|
132
|
+
def _alloc(self, kind: str, var: str, span, call_node=None) -> int:
|
|
133
|
+
nid = self.next_id
|
|
134
|
+
self.next_id += 1
|
|
135
|
+
self.param_nodes.append(
|
|
136
|
+
ParamNode(
|
|
137
|
+
id=nid, kind=kind, var=var, call_node=call_node,
|
|
138
|
+
start_line=span[0], end_line=span[1],
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return nid
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------- formals
|
|
144
|
+
|
|
145
|
+
def build_formals(self) -> None:
|
|
146
|
+
scope, summary = self.scope, self.summary
|
|
147
|
+
params = list(scope.params)
|
|
148
|
+
for p in params:
|
|
149
|
+
self.formal_in[p] = self._alloc("formal_in", p, self._entry_span)
|
|
150
|
+
for c in sorted(scope.captures):
|
|
151
|
+
var = CAPTURE_PREFIX + c
|
|
152
|
+
self.formal_in[var] = self._alloc("formal_in", var, self._entry_span)
|
|
153
|
+
for g in sorted(summary.global_reads):
|
|
154
|
+
var = GLOBAL_PREFIX + g
|
|
155
|
+
self.formal_in[var] = self._alloc("formal_in", var, self._entry_span)
|
|
156
|
+
|
|
157
|
+
self.formal_out[RETURN_PATH] = self._alloc(
|
|
158
|
+
"formal_out", RETURN_PATH, self._exit_span
|
|
159
|
+
)
|
|
160
|
+
for p in sorted(summary.mutated_params):
|
|
161
|
+
self.formal_out[p] = self._alloc("formal_out", p, self._exit_span)
|
|
162
|
+
for g in sorted(summary.global_writes):
|
|
163
|
+
var = GLOBAL_PREFIX + g
|
|
164
|
+
self.formal_out[var] = self._alloc("formal_out", var, self._exit_span)
|
|
165
|
+
|
|
166
|
+
# Wiring: formal_in → first uses (mirror the ENTRY-def DDG edges).
|
|
167
|
+
entry = self.cfg.entry_id
|
|
168
|
+
for e in self.ddg:
|
|
169
|
+
if e.source != entry:
|
|
170
|
+
continue
|
|
171
|
+
b = base_of(e.var)
|
|
172
|
+
if b in self.formal_in:
|
|
173
|
+
fid = self.formal_in[b]
|
|
174
|
+
elif CAPTURE_PREFIX + b in self.formal_in:
|
|
175
|
+
fid = self.formal_in[CAPTURE_PREFIX + b]
|
|
176
|
+
elif "::" in b and GLOBAL_PREFIX + b in self.formal_in:
|
|
177
|
+
fid = self.formal_in[GLOBAL_PREFIX + b]
|
|
178
|
+
else:
|
|
179
|
+
continue
|
|
180
|
+
self.extra.append(PDGEdge(source=fid, target=e.target, type="DDG", var=e.var))
|
|
181
|
+
|
|
182
|
+
# Wiring: defining nodes → formal_out.
|
|
183
|
+
param_names = set(scope.params)
|
|
184
|
+
if scope.self_name:
|
|
185
|
+
param_names.add(scope.self_name)
|
|
186
|
+
for nid, f in self.facts.items():
|
|
187
|
+
if nid == entry:
|
|
188
|
+
continue
|
|
189
|
+
if RETURN_PATH in f.defs:
|
|
190
|
+
self.extra.append(
|
|
191
|
+
PDGEdge(
|
|
192
|
+
source=nid,
|
|
193
|
+
target=self.formal_out[RETURN_PATH],
|
|
194
|
+
type="DDG",
|
|
195
|
+
var=RETURN_PATH,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
for d in f.defs:
|
|
199
|
+
b = base_of(d)
|
|
200
|
+
if "::" in b and GLOBAL_PREFIX + b in self.formal_out:
|
|
201
|
+
self.extra.append(
|
|
202
|
+
PDGEdge(
|
|
203
|
+
source=nid,
|
|
204
|
+
target=self.formal_out[GLOBAL_PREFIX + b],
|
|
205
|
+
type="DDG",
|
|
206
|
+
var=d,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
elif b in param_names and suffix_of(d) and b in self.formal_out:
|
|
210
|
+
self.extra.append(
|
|
211
|
+
PDGEdge(source=nid, target=self.formal_out[b], type="DDG", var=d)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------- actuals
|
|
215
|
+
|
|
216
|
+
def _defs_reaching_call_matching(self, call_node: int, path: Optional[str]):
|
|
217
|
+
"""Sources of DDG in-edges of the call node whose var matches the
|
|
218
|
+
actual's access path (all of them when the actual is an expression)."""
|
|
219
|
+
sources = []
|
|
220
|
+
for e in self.ddg:
|
|
221
|
+
if e.target != call_node:
|
|
222
|
+
continue
|
|
223
|
+
if path is None or interferes(e.var, path) or interferes(path, e.var):
|
|
224
|
+
sources.append((e.source, e.var))
|
|
225
|
+
return sources
|
|
226
|
+
|
|
227
|
+
def build_actuals(
|
|
228
|
+
self,
|
|
229
|
+
summaries: Dict[str, FunctionSummary],
|
|
230
|
+
formal_ids: Dict[str, Dict[str, int]],
|
|
231
|
+
sdg_edges: List[SDGEdge],
|
|
232
|
+
) -> None:
|
|
233
|
+
sig = self.info.signature
|
|
234
|
+
node_span = {
|
|
235
|
+
n.id: (n.start_line, n.end_line) for n in self.cfg.nodes
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for cs in sorted(self.info.call_sites, key=lambda c: (c.node_id, c.targets)):
|
|
239
|
+
span = node_span.get(cs.node_id, (-1, -1))
|
|
240
|
+
for target in cs.targets:
|
|
241
|
+
callee_summary = summaries.get(target)
|
|
242
|
+
callee_formals = formal_ids.get(target)
|
|
243
|
+
if callee_summary is None or callee_formals is None:
|
|
244
|
+
continue # external — conservative pass-through already applies
|
|
245
|
+
|
|
246
|
+
# CALL: callsite statement → callee ENTRY.
|
|
247
|
+
sdg_edges.append(
|
|
248
|
+
SDGEdge(
|
|
249
|
+
source_sig=sig, source_node=cs.node_id,
|
|
250
|
+
target_sig=target, target_node=0, type="CALL",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
bound_in: Dict[str, int] = {} # formal key -> actual_in id
|
|
255
|
+
bound_out: Dict[str, int] = {} # formal key -> actual_out id
|
|
256
|
+
|
|
257
|
+
# Argument actual_ins for the callee formals this site binds.
|
|
258
|
+
for param, path in cs.arg_paths:
|
|
259
|
+
if param not in callee_formals:
|
|
260
|
+
continue
|
|
261
|
+
key = (cs.node_id, f"{target}::{param}")
|
|
262
|
+
if key not in self.actual_in:
|
|
263
|
+
aid = self._alloc("actual_in", param, span, cs.node_id)
|
|
264
|
+
self.actual_in[key] = aid
|
|
265
|
+
self.extra.append(
|
|
266
|
+
PDGEdge(source=cs.node_id, target=aid, type="CDG")
|
|
267
|
+
)
|
|
268
|
+
for src, var in self._defs_reaching_call_matching(
|
|
269
|
+
cs.node_id, path
|
|
270
|
+
):
|
|
271
|
+
self.extra.append(
|
|
272
|
+
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
273
|
+
)
|
|
274
|
+
bound_in[f"param:{param}"] = self.actual_in[key]
|
|
275
|
+
sdg_edges.append(
|
|
276
|
+
SDGEdge(
|
|
277
|
+
source_sig=sig, source_node=self.actual_in[key],
|
|
278
|
+
target_sig=target,
|
|
279
|
+
target_node=callee_formals[param],
|
|
280
|
+
type="PARAM_IN", var=param,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Global actual_ins from the callee's read footprint.
|
|
285
|
+
for g in sorted(callee_summary.global_reads):
|
|
286
|
+
fvar = GLOBAL_PREFIX + g
|
|
287
|
+
if fvar not in callee_formals:
|
|
288
|
+
continue
|
|
289
|
+
key = (cs.node_id, f"{target}::{fvar}")
|
|
290
|
+
if key not in self.actual_in:
|
|
291
|
+
aid = self._alloc("actual_in", fvar, span, cs.node_id)
|
|
292
|
+
self.actual_in[key] = aid
|
|
293
|
+
self.extra.append(
|
|
294
|
+
PDGEdge(source=cs.node_id, target=aid, type="CDG")
|
|
295
|
+
)
|
|
296
|
+
for src, var in self._defs_reaching_call_matching(
|
|
297
|
+
cs.node_id, g
|
|
298
|
+
):
|
|
299
|
+
self.extra.append(
|
|
300
|
+
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
301
|
+
)
|
|
302
|
+
bound_in[f"global:{g}"] = self.actual_in[key]
|
|
303
|
+
sdg_edges.append(
|
|
304
|
+
SDGEdge(
|
|
305
|
+
source_sig=sig, source_node=self.actual_in[key],
|
|
306
|
+
target_sig=target, target_node=callee_formals[fvar],
|
|
307
|
+
type="PARAM_IN", var=fvar,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# actual_outs: return, mutated bound params, written globals.
|
|
312
|
+
out_specs: List[Tuple[str, str]] = [("return", RETURN_PATH)]
|
|
313
|
+
for p in sorted(callee_summary.mutated_params):
|
|
314
|
+
if cs.arg_path_of(p) is not None:
|
|
315
|
+
out_specs.append((f"param:{p}", p))
|
|
316
|
+
for g in sorted(callee_summary.global_writes):
|
|
317
|
+
out_specs.append((f"global:{g}", GLOBAL_PREFIX + g))
|
|
318
|
+
|
|
319
|
+
callee_formal_outs = formal_ids.get(f"{target}<out>", {})
|
|
320
|
+
for key_name, fvar in out_specs:
|
|
321
|
+
if fvar not in callee_formal_outs:
|
|
322
|
+
continue
|
|
323
|
+
key = (cs.node_id, f"{target}::out::{fvar}")
|
|
324
|
+
if key not in self.actual_out:
|
|
325
|
+
oid = self._alloc("actual_out", fvar, span, cs.node_id)
|
|
326
|
+
self.actual_out[key] = oid
|
|
327
|
+
self.extra.append(
|
|
328
|
+
PDGEdge(source=cs.node_id, target=oid, type="CDG")
|
|
329
|
+
)
|
|
330
|
+
self.extra.append(
|
|
331
|
+
PDGEdge(source=oid, target=cs.node_id, type="DDG", var=fvar)
|
|
332
|
+
)
|
|
333
|
+
bound_out[key_name] = self.actual_out[key]
|
|
334
|
+
sdg_edges.append(
|
|
335
|
+
SDGEdge(
|
|
336
|
+
source_sig=target,
|
|
337
|
+
source_node=callee_formal_outs[fvar],
|
|
338
|
+
target_sig=sig, target_node=self.actual_out[key],
|
|
339
|
+
type="PARAM_OUT", var=fvar,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# SUMMARY: actual_in → actual_out per callee transitive flow.
|
|
344
|
+
for in_key, out_key in sorted(callee_summary.flows):
|
|
345
|
+
a_in = bound_in.get(in_key)
|
|
346
|
+
a_out = bound_out.get(out_key)
|
|
347
|
+
if a_in is not None and a_out is not None:
|
|
348
|
+
sdg_edges.append(
|
|
349
|
+
SDGEdge(
|
|
350
|
+
source_sig=sig, source_node=a_in,
|
|
351
|
+
target_sig=sig, target_node=a_out,
|
|
352
|
+
type="SUMMARY", var=None,
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Closure captures: bind at the nested callable's def statement.
|
|
357
|
+
for def_node, nested_sig in sorted(self.info.nested_defs):
|
|
358
|
+
nested_formals = formal_ids.get(nested_sig)
|
|
359
|
+
if not nested_formals:
|
|
360
|
+
continue
|
|
361
|
+
span = node_span.get(def_node, (-1, -1))
|
|
362
|
+
for fvar, fid in sorted(nested_formals.items()):
|
|
363
|
+
if not fvar.startswith(CAPTURE_PREFIX):
|
|
364
|
+
continue
|
|
365
|
+
name = fvar[len(CAPTURE_PREFIX):]
|
|
366
|
+
key = (def_node, f"{nested_sig}::{fvar}")
|
|
367
|
+
if key not in self.actual_in:
|
|
368
|
+
aid = self._alloc("actual_in", fvar, span, def_node)
|
|
369
|
+
self.actual_in[key] = aid
|
|
370
|
+
self.extra.append(PDGEdge(source=def_node, target=aid, type="CDG"))
|
|
371
|
+
for src, var in self._defs_reaching_call_matching(def_node, name):
|
|
372
|
+
self.extra.append(
|
|
373
|
+
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
374
|
+
)
|
|
375
|
+
sdg_edges.append(
|
|
376
|
+
SDGEdge(
|
|
377
|
+
source_sig=self.info.signature,
|
|
378
|
+
source_node=self.actual_in[key],
|
|
379
|
+
target_sig=nested_sig, target_node=fid,
|
|
380
|
+
type="PARAM_IN", var=fvar,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def assemble_sdg(
|
|
386
|
+
infos: Dict[str, FunctionInfo],
|
|
387
|
+
summaries: Dict[str, FunctionSummary],
|
|
388
|
+
k: int,
|
|
389
|
+
) -> ProgramGraphsIR:
|
|
390
|
+
"""Stitch every function's PDG into the whole-program SDG."""
|
|
391
|
+
ir = ProgramGraphsIR(k_limit=k)
|
|
392
|
+
|
|
393
|
+
# Pass 1: solve each function against the final summaries and lay out its
|
|
394
|
+
# formal nodes (their ids must exist before callsites reference them).
|
|
395
|
+
assemblers: Dict[str, _FunctionAssembler] = {}
|
|
396
|
+
formal_ids: Dict[str, Dict[str, int]] = {}
|
|
397
|
+
for sig in sorted(infos):
|
|
398
|
+
info = infos[sig]
|
|
399
|
+
summary, facts, ddg = solve_function(info, summaries)
|
|
400
|
+
asm = _FunctionAssembler(info, summary, facts, ddg)
|
|
401
|
+
asm.build_formals()
|
|
402
|
+
assemblers[sig] = asm
|
|
403
|
+
formal_ids[sig] = dict(asm.formal_in)
|
|
404
|
+
formal_ids[f"{sig}<out>"] = dict(asm.formal_out)
|
|
405
|
+
|
|
406
|
+
# Pass 2: callsite actuals and cross-function edges.
|
|
407
|
+
sdg_edges: List[SDGEdge] = []
|
|
408
|
+
for sig in sorted(assemblers):
|
|
409
|
+
assemblers[sig].build_actuals(summaries, formal_ids, sdg_edges)
|
|
410
|
+
|
|
411
|
+
for sig, asm in assemblers.items():
|
|
412
|
+
ir.functions[sig] = FunctionGraphs(
|
|
413
|
+
pdg=asm.info.pdg,
|
|
414
|
+
ddg=asm.ddg,
|
|
415
|
+
param_nodes=asm.param_nodes,
|
|
416
|
+
extra_edges=asm.extra,
|
|
417
|
+
summary=asm.summary,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
ir.sdg_edges = sorted(
|
|
421
|
+
set(sdg_edges),
|
|
422
|
+
key=lambda e: (e.source_sig, e.source_node, e.target_sig, e.target_node, e.type, e.var or ""),
|
|
423
|
+
)
|
|
424
|
+
return ir
|
|
@@ -0,0 +1,93 @@
|
|
|
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 8 of the level-3 dataflow ladder: backward slicing as an SDG query.
|
|
18
|
+
|
|
19
|
+
The classic Horwitz–Reps–Binkley two-phase traversal, which is what makes the
|
|
20
|
+
slice *context-sensitive* without re-descending into callees:
|
|
21
|
+
|
|
22
|
+
- **Phase 1** walks backward over every dependence edge **except PARAM_OUT**:
|
|
23
|
+
it ascends from the criterion to callers (PARAM_IN/CALL reversed) and steps
|
|
24
|
+
*across* callsites through SUMMARY edges, but never descends into a callee.
|
|
25
|
+
- **Phase 2** starts from everything phase 1 reached and walks backward over
|
|
26
|
+
every edge **except PARAM_IN and CALL**: it descends into callees
|
|
27
|
+
(PARAM_OUT reversed) but never re-ascends — which is exactly what prevents
|
|
28
|
+
infeasible call–return mismatches.
|
|
29
|
+
|
|
30
|
+
Slicing consumes the assembled :class:`~codeanalyzer.dataflow.sdg.
|
|
31
|
+
ProgramGraphsIR`; taint is the same labeled traversal with a model pack and
|
|
32
|
+
is deliberately left to the CLDK SDK (language-independent once the SDG is
|
|
33
|
+
emitted — see #67).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import Dict, List, Set, Tuple
|
|
39
|
+
|
|
40
|
+
from codeanalyzer.dataflow.sdg import ProgramGraphsIR
|
|
41
|
+
|
|
42
|
+
Node = Tuple[str, int] # (signature, node_id)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _reverse_adjacency(ir: ProgramGraphsIR) -> Dict[Node, List[Tuple[Node, str]]]:
|
|
46
|
+
"""target → [(source, edge_type)] over intra- and inter-procedural edges."""
|
|
47
|
+
radj: Dict[Node, List[Tuple[Node, str]]] = {}
|
|
48
|
+
|
|
49
|
+
def add(src: Node, tgt: Node, kind: str) -> None:
|
|
50
|
+
radj.setdefault(tgt, []).append((src, kind))
|
|
51
|
+
|
|
52
|
+
for sig, fg in ir.functions.items():
|
|
53
|
+
for e in fg.pdg.edges:
|
|
54
|
+
if e.type == "CDG":
|
|
55
|
+
add((sig, e.source), (sig, e.target), "CDG")
|
|
56
|
+
for e in fg.ddg:
|
|
57
|
+
add((sig, e.source), (sig, e.target), "DDG")
|
|
58
|
+
for e in fg.extra_edges:
|
|
59
|
+
add((sig, e.source), (sig, e.target), e.type)
|
|
60
|
+
for e in ir.sdg_edges:
|
|
61
|
+
add(
|
|
62
|
+
(e.source_sig, e.source_node),
|
|
63
|
+
(e.target_sig, e.target_node),
|
|
64
|
+
e.type,
|
|
65
|
+
)
|
|
66
|
+
return radj
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def backward_slice(ir: ProgramGraphsIR, signature: str, node_id: int) -> Set[Node]:
|
|
70
|
+
"""Context-sensitive backward slice of ``(signature, node_id)``."""
|
|
71
|
+
if signature not in ir.functions:
|
|
72
|
+
raise KeyError(f"unknown signature: {signature}")
|
|
73
|
+
radj = _reverse_adjacency(ir)
|
|
74
|
+
criterion: Node = (signature, node_id)
|
|
75
|
+
|
|
76
|
+
def sweep(seeds: Set[Node], skip: Set[str]) -> Set[Node]:
|
|
77
|
+
seen: Set[Node] = set()
|
|
78
|
+
stack = list(seeds)
|
|
79
|
+
while stack:
|
|
80
|
+
node = stack.pop()
|
|
81
|
+
if node in seen:
|
|
82
|
+
continue
|
|
83
|
+
seen.add(node)
|
|
84
|
+
for src, kind in radj.get(node, ()):
|
|
85
|
+
if kind in skip:
|
|
86
|
+
continue
|
|
87
|
+
if src not in seen:
|
|
88
|
+
stack.append(src)
|
|
89
|
+
return seen
|
|
90
|
+
|
|
91
|
+
phase1 = sweep({criterion}, skip={"PARAM_OUT"})
|
|
92
|
+
phase2 = sweep(phase1, skip={"PARAM_IN", "CALL"})
|
|
93
|
+
return phase1 | phase2
|