codeanalyzer-python 1.0.3__py3-none-any.whl → 1.1.1__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.
@@ -283,6 +283,23 @@ def emit_l3_body(
283
283
  continue
284
284
  pycallable.body[local] = BodyNode(kind=node.kind, span=span)
285
285
 
286
+ # #115: anchor nested call vertices to their statement. A bare-call
287
+ # statement shares its key with its CFG node (handled above); a call
288
+ # nested inside a larger statement (`y = f(x)`) has its own key and
289
+ # no cfg contact, so it carries `parent` = the enclosing statement's
290
+ # local id — the same anchoring actual_in/actual_out vertices use.
291
+ for node in pdg.cfg.nodes:
292
+ if node.ast_node is None:
293
+ continue
294
+ stmt_local = im.local(node.id)
295
+ for call in _calls_in(node.ast_node):
296
+ call_key = f"{call.lineno}:{call.col_offset}"
297
+ child = pycallable.body.get(call_key)
298
+ if child is None or call_key == stmt_local:
299
+ continue
300
+ if child.kind == "call":
301
+ child.parent = stmt_local
302
+
286
303
  if want_cfg:
287
304
  pycallable.cfg = [
288
305
  CfgEdge(
@@ -437,7 +454,12 @@ def emit_l4(
437
454
  points-to provenance and taint are *not* emitted here (later tasks).
438
455
  """
439
456
  from codeanalyzer.dataflow.identity import IdentityMap
440
- from codeanalyzer.schema.py_schema import BodyNode, ParamEdge, SummaryEdge
457
+ from codeanalyzer.schema.py_schema import (
458
+ BodyNode,
459
+ DdgEdge,
460
+ ParamEdge,
461
+ SummaryEdge,
462
+ )
441
463
 
442
464
  # L4 emission is additive (it *appends* summary/param edges), so it must
443
465
  # first clear any L4 state a reused cache left on these live objects —
@@ -517,6 +539,41 @@ def emit_l4(
517
539
  )
518
540
  (app.param_in if e.type == "PARAM_IN" else app.param_out).append(edge)
519
541
 
542
+ # (e) statement ↔ port ddg wiring (#115). The IR's ``extra_edges`` — the
543
+ # def→actual_in, actual_out→callsite, formal_in→use and def→formal_out
544
+ # bindings ``assemble_sdg`` wires — are what connect the port lattice to
545
+ # the statement-level ddg; without them the SDG is two disconnected
546
+ # graphs and no end-to-end flows_to walk can cross a call. Emitted with
547
+ # ``prov=["reaching-defs"]`` (the label codeanalyzer-typescript ships for
548
+ # its port-routing ddg edges, so the vocabulary stays keystone-shared).
549
+ # CDG-typed extras (callsite → actual_in containment) are skipped — the
550
+ # actual vertices already carry that anchoring in ``parent``.
551
+ for sig, fg in ir.functions.items():
552
+ pycallable = sig_to_callable.get(sig)
553
+ im = ims.get(sig)
554
+ if pycallable is None or im is None:
555
+ continue
556
+ # Idempotency under cache reuse, mirroring the points-to delta: strip
557
+ # any reaching-defs edges a prior run appended before re-emitting.
558
+ pycallable.ddg = [e for e in pycallable.ddg if e.prov != ["reaching-defs"]]
559
+ seen: set = set()
560
+ rows = []
561
+ for e in fg.extra_edges:
562
+ if e.type != "DDG":
563
+ continue
564
+ src, dst = im.local(e.source), im.local(e.target)
565
+ if src not in pycallable.body or dst not in pycallable.body:
566
+ continue
567
+ key = (src, dst, e.var)
568
+ if key in seen:
569
+ continue
570
+ seen.add(key)
571
+ rows.append(
572
+ DdgEdge(src=src, dst=dst, var=e.var, prov=["reaching-defs"])
573
+ )
574
+ rows.sort(key=lambda r: (r.src, r.dst, r.var or ""))
575
+ pycallable.ddg.extend(rows)
576
+
520
577
 
521
578
  def _ddg_local_set(im, pdg) -> Set[Tuple[str, str, Optional[str]]]:
522
579
  """The DDG edges of ``pdg`` as a set of ``(local_src, local_dst, var)``.
@@ -0,0 +1,34 @@
1
+ # Vendored Scalpel (typed_ast-free slice)
2
+
3
+ Vendored from **[SMAT-Lab/Scalpel](https://github.com/SMAT-Lab/Scalpel)**,
4
+ package `python-scalpel==1.0b0`, licensed **Apache-2.0** (see `LICENSE`).
5
+
6
+ ## Why vendored
7
+
8
+ `python-scalpel` hard-depends on `typed_ast`, whose last release (1.5.5) has no
9
+ wheel for Python 3.12+ and fails to build from source on modern compilers — so
10
+ `pip install python-scalpel` fails on 3.12/3.13/3.14. `typed_ast` is imported by
11
+ exactly one scalpel module, `typeinfer/analysers.py`, which codeanalyzer does not
12
+ use. Vendoring the small slice the L4 may-alias oracle needs removes the
13
+ `typed_ast` dependency and makes scalpel the default oracle on every supported
14
+ Python.
15
+
16
+ ## What is vendored
17
+
18
+ Exactly the 9-module closure that `scalpel.SSA.const` + `scalpel.cfg` load
19
+ (verified via `sys.modules`; provably free of `typeinfer`/`typed_ast`):
20
+
21
+ __init__.py
22
+ SSA/__init__.py, SSA/const.py
23
+ cfg/__init__.py, cfg/builder.py, cfg/model.py
24
+ core/__init__.py, core/func_call_visitor.py, core/vars_visitor.py
25
+
26
+ Copied verbatim except **one patch**: `cfg/model.py`'s top-level
27
+ `import graphviz as gv` is guarded (`try/except ImportError: gv = None`) so the
28
+ module imports without the `graphviz` package — the graphviz-using
29
+ `build_visual()` methods are unused here.
30
+
31
+ Runtime deps of this slice: `astor`, `networkx` (both core dependencies of
32
+ codeanalyzer). `typed_ast` and `graphviz` are NOT required.
33
+
34
+ To refresh: re-run the vendoring in `docs/superpowers/plans/2026-07-22-vendored-scalpel-default-oracle.md` Task 1.
@@ -0,0 +1,8 @@
1
+ """
2
+ Static Single Assignment (SSA) is a technique of IR in the compiling thoery, it also shows great benefits to static anaysis tasks such as constant propagation, dead code elimination and etc.
3
+ Constant propagation is also a matured technique in static anaysis.
4
+ It is the process of evaluating or recognizing the actual constant values or expressions at a particular program point. This is realized by utilizing control flow and data flow information. Determining the possible values for variables before runtime gives great benefits to software anaysis.
5
+ For instance, with constant value propagation, we can detect and remove dead code or perfrom type checking.
6
+ In scalpel, we implement constant propagation along with the SSA for execution efficiency.
7
+ """
8
+ __slots__ = ["const"]
@@ -0,0 +1,398 @@
1
+ """
2
+ In this module, the single static assignment forms are implemented to allow
3
+ further analysis. The module contain a single class named SSA.
4
+ """
5
+ import ast
6
+ import astor
7
+ from functools import reduce
8
+ from collections import OrderedDict
9
+ import networkx as nx
10
+ from ..core.vars_visitor import get_vars
11
+
12
+ def parse_val(node):
13
+ # does not return anything
14
+ if isinstance(node, ast.Constant):
15
+ return node.value
16
+ if isinstance(node, ast.Str):
17
+ if hasattr(node, "value"):
18
+ return node.value
19
+ else:
20
+ return node.s
21
+ return "other"
22
+
23
+
24
+ class SSA:
25
+ """
26
+ Build SSA graph from a given AST node based on the CFG.
27
+ """
28
+ def __init__ (self):
29
+ """
30
+ Args:
31
+ src: the source code as input.
32
+ """
33
+ # the class SSA takes a module as the input
34
+ self.numbering = {} # numbering variables
35
+ self.var_values = {} # numbering variables
36
+ self.global_live_idents = []
37
+ self.ssa_blocks = []
38
+ self.error_paths = {}
39
+ self.dom = {}
40
+
41
+ self.block_ident_gen = {}
42
+ self.block_ident_use = {}
43
+ self.reachable_table = {}
44
+ id2block = {}
45
+ self.unreachable_names = {}
46
+ self.undefined_names_from = {}
47
+ self.global_names = []
48
+
49
+ def get_attribute_stmts(self, stmts):
50
+ call_stmts = []
51
+ for stmt in stmts:
52
+ if isinstance(stmt,ast.Call) and isinstance(stmt.func, ast.Attribute):
53
+ call_stmts += [stmt]
54
+
55
+ def get_identifiers(self, ast_node):
56
+ """
57
+ Extract all identifiers from the given AST node.
58
+ Args:
59
+ ast_node: AST node.
60
+ """
61
+ if ast_node is None:
62
+ return []
63
+ res = get_vars(ast_node)
64
+ idents = [r['name'] for r in res if r['name'] is not None and "." not in r['name']]
65
+ return idents
66
+
67
+ def compute_SSA(self, cfg):
68
+ """
69
+ Compute single static assignment form representations for a given CFG.
70
+ During the computing, constant value and alias pairs are generated. The following steps are used to compute SSA representations:
71
+ step 1a: compute the dominance frontier
72
+ step 1b: use dominance frontier to place phi node
73
+ if node X contains assignment to a, put phi node for an in dominance frontier of X
74
+ adding phi function may require introducing additional phi function
75
+ start from the entry node
76
+ step2: rename variables so only one definition per name
77
+
78
+ Args:
79
+ cfg: a control flow graph.
80
+ """
81
+ # to count how many times a var is defined
82
+ ident_name_counter = {}
83
+ # constant assignment dict
84
+ ident_const_dict = {}
85
+ # step 1a: compute the dominance frontier
86
+ all_blocks = cfg.get_all_blocks()
87
+ id2blocks = {block.id:block for block in all_blocks}
88
+
89
+ block_loaded_idents = {block.id:[] for block in all_blocks}
90
+ block_stored_idents = {block.id:[] for block in all_blocks}
91
+
92
+ block_const_dict = {block.id:[] for block in all_blocks}
93
+
94
+ block_renamed_stored = {block.id:[] for block in all_blocks}
95
+ block_renamed_loaded = {block.id:[] for block in all_blocks}
96
+
97
+ DF = self.compute_DF(all_blocks)
98
+
99
+ for block in all_blocks:
100
+ df_nodes = DF[block.id]
101
+ tmp_const_dict = {}
102
+
103
+
104
+ for idx, stmt in enumerate(block.statements):
105
+ stmt_const_dict = {}
106
+ stored_idents, loaded_idents, func_names = self.get_stmt_idents_ctx(stmt, const_dict=stmt_const_dict)
107
+ tmp_const_dict[idx] = stmt_const_dict
108
+ block_loaded_idents[block.id] += [loaded_idents]
109
+ block_stored_idents[block.id] += [stored_idents]
110
+ block_renamed_loaded[block.id] += [{ident:set() for ident in loaded_idents}]
111
+
112
+ block_const_dict[block.id] = tmp_const_dict
113
+
114
+ for block in all_blocks:
115
+ stored_idents = block_stored_idents[block.id]
116
+ loaded_idents = block_loaded_idents[block.id]
117
+ n_stmts = len(stored_idents)
118
+ assert (n_stmts == len(loaded_idents))
119
+ affected_idents = []
120
+ tmp_const_dict = block_const_dict[block.id]
121
+ for i in range(n_stmts):
122
+ stmt_stored_idents = stored_idents[i]
123
+ stmt_loaded_idents = loaded_idents[i]
124
+ stmt_renamed_stored = {}
125
+
126
+ for ident in stmt_stored_idents:
127
+ affected_idents.append(ident)
128
+ if ident in ident_name_counter:
129
+ ident_name_counter[ident] += 1
130
+ else:
131
+ ident_name_counter[ident] = 0
132
+ # rename the var name as the number of assignments
133
+ stmt_const_dict = tmp_const_dict[i]
134
+ if ident in stmt_const_dict:
135
+ ident_const_dict[(ident, ident_name_counter[ident])] = stmt_const_dict[ident]
136
+
137
+ stmt_renamed_stored[ident] = ident_name_counter[ident]
138
+ block_renamed_stored[block.id] += [stmt_renamed_stored]
139
+
140
+
141
+ #same block, number used identifiers
142
+ for ident in stmt_loaded_idents:
143
+ # a list of dictions for each of idents used in this statement
144
+ phi_loaded_idents = block_renamed_loaded[block.id][i]
145
+ if ident in ident_name_counter:
146
+ phi_loaded_idents[ident].add(ident_name_counter[ident])
147
+
148
+ df_block_ids = DF[block.id]
149
+ for df_block_id in df_block_ids:
150
+ df_block = id2blocks[df_block_id]
151
+ block_ident_gen_produced = []
152
+ df_block_stored_idents = block_stored_idents[df_block_id]
153
+ for af_ident in affected_idents:
154
+ # this for-loop process every statement in the block
155
+ for idx, phi_loaded_idents in enumerate(block_renamed_loaded[df_block_id]):
156
+ block_ident_gen_produced.extend(df_block_stored_idents[idx])
157
+ if af_ident in block_ident_gen_produced:
158
+ continue
159
+ # place phi function here this var used
160
+ # if af_ident has been assigned in this block beforclee this statement, then discard it
161
+ # so theck af_ident has been generated in this block
162
+ if af_ident in phi_loaded_idents:
163
+ phi_loaded_idents[af_ident].add(ident_name_counter[af_ident])
164
+
165
+ return block_renamed_loaded, ident_const_dict
166
+
167
+ def get_stmt_idents_ctx(self, stmt, del_set=[], const_dict = {}):
168
+ """
169
+ Extract the contextual information of each of identifiers.
170
+ For assignment statements, the assigned values for each of variables will be stored.
171
+ In addition, the del_set will store all deleted variables.
172
+ Args:
173
+ stmt: statement from AST trees.
174
+ del_set: deleted identifiers
175
+ const_dict: a mapping relationship between variables and their assigned values in this statement
176
+ """
177
+ # if this is a definition of class/function, ignore
178
+ stored_idents = []
179
+ loaded_idents = []
180
+ func_names = []
181
+ # assignment with only one target
182
+
183
+ if isinstance(stmt, ast.Assign):
184
+ targets = stmt.targets
185
+ value = stmt.value
186
+ if len(targets) == 1:
187
+ if hasattr(targets[0], "id"):
188
+ left_name = stmt.targets[0].id
189
+ const_dict[left_name] = stmt.value
190
+ elif isinstance(targets[0], ast.Attribute):
191
+ left_name = astor.to_source(stmt.targets[0]).strip()
192
+ const_dict[left_name] = value
193
+ # multiple targets are represented as tuple
194
+ elif isinstance(targets[0], ast.Tuple):
195
+ # value is also represented as tuple
196
+ if isinstance(value, ast.Tuple):
197
+ for elt, val in zip(targets[0].elts, value.elts):
198
+ if hasattr(elt, "id"):
199
+ left_name = elt.id
200
+ const_dict[left_name] = val
201
+ elif isinstance(targets[0], ast.Attribute):
202
+ #TODO: resolve attributes
203
+ pass
204
+ # value is represented as call
205
+ if isinstance(value, ast.Call):
206
+ for elt in targets[0].elts:
207
+ if hasattr(elt, "id"):
208
+ left_name = elt.id
209
+ const_dict[left_name] = value
210
+ elif isinstance(targets[0], ast.Attribute):
211
+ #TODO: resolve attributes
212
+ pass
213
+ else:
214
+ # Note in some python versions, there are more than one target for an assignment
215
+ # while in some other python versions, multiple targets are deemed as ast.Tuple type in assignment statement
216
+ for target in stmt.targets:
217
+ # this is an assignment to tuple such as a,b = fun()
218
+ # then no valid constant value can be recorded for this statement
219
+ if hasattr(target, "id"):
220
+ left_name = target.id
221
+ const_dict[left_name] = None # TODO: design a type for these kind of values
222
+ elif isinstance(stmt.targets[0], ast.Attribute):
223
+ #TODO: resolve attributes
224
+ pass
225
+
226
+
227
+
228
+ # one target assignment with type annotations
229
+ if isinstance(stmt, ast.AnnAssign):
230
+ if hasattr(stmt.target, "id"):
231
+ left_name = stmt.target.id
232
+ const_dict[left_name] = stmt.value
233
+ elif isinstance(stmt.target, ast.Attribute):
234
+ #TODO: resolve attributes
235
+ pass
236
+ if isinstance(stmt, ast.AugAssign):
237
+ # note here , we need to rewrite this value to its extended form
238
+ # if the statement is "a += 1", then the assigned value should be a+1
239
+ if hasattr(stmt.target, "id"):
240
+ left_name = stmt.target.id
241
+ extended_right = ast.BinOp(ast.Name(left_name, ast.Load()), stmt.op, stmt.value)
242
+ const_dict[left_name] = extended_right
243
+ elif isinstance(stmt.target, ast.Attribute):
244
+ #TODO: resolve attributes
245
+ pass
246
+ if isinstance(stmt, ast.For):
247
+ # there is a variation of assignment in for loop
248
+ # in the case of : for i in [1,2,3]
249
+ # the element of stmt.iter is the value of this assignment
250
+ if hasattr(stmt.target, "id"):
251
+ left_name = stmt.target.id
252
+ iter_value = stmt.iter
253
+ # make a iter call
254
+ #iter_node = ast.Call(ast.Name("iter", ast.Load()), [stmt.iter], [])
255
+ # make a next call
256
+ #next_call_node = ast.Call(ast.Name("next", ast.Load()), [iter_node], [])
257
+ const_dict[left_name] = iter_value
258
+
259
+ elif isinstance(stmt.target, ast.Tuple):
260
+ # to handle for-loop uch as:
261
+ # for x, y in fun():
262
+ for elt in stmt.target.elts:
263
+ if hasattr(elt, "id"):
264
+ const_dict[elt.id] = stmt.iter
265
+ elif isinstance(stmt.target, ast.Attribute):
266
+ #TODO: resolve attributes
267
+ pass
268
+
269
+
270
+
271
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
272
+ stored_idents.append(stmt.name)
273
+ const_dict[stmt.name] = stmt
274
+ func_names.append(stmt.name)
275
+ new_stmt = stmt
276
+ new_stmt.body = []
277
+ ident_info = get_vars(new_stmt)
278
+ for r in ident_info:
279
+ if r['name'] is None:
280
+ continue
281
+ if r['usage'] == "load":
282
+ loaded_idents.append(r['name'])
283
+ return stored_idents, loaded_idents, func_names
284
+
285
+ if isinstance(stmt, ast.ClassDef):
286
+ stored_idents.append(stmt.name)
287
+ const_dict[stmt.name] = None
288
+ func_names.append(stmt.name)
289
+ return stored_idents, loaded_idents, func_names
290
+
291
+ # if this is control flow statements, we should not visit its body to avoid duplicates
292
+ # as they are already in the next blocks
293
+ if isinstance(stmt, (ast.Import, ast.ImportFrom)):
294
+ for alias in stmt.names:
295
+ if alias.asname is None:
296
+ stored_idents += [alias.name.split('.')[0]]
297
+ else:
298
+ stored_idents += [alias.asname.split('.')[0]]
299
+ return stored_idents, loaded_idents, []
300
+
301
+ if isinstance(stmt, (ast.Try)):
302
+ for handler in stmt.handlers:
303
+ if handler.name is not None:
304
+ stored_idents.append(handler.name)
305
+
306
+ if isinstance(handler.type, ast.Name):
307
+ loaded_idents.append(handler.type.id)
308
+ elif isinstance(handler.type, ast.Attribute) and isinstance(handler.type.value, ast.Name):
309
+ loaded_idents.append(handler.type.value.id)
310
+ return stored_idents, loaded_idents, []
311
+ if isinstance(stmt, ast.Global):
312
+ for name in stmt.names:
313
+ self.global_names.append(name)
314
+ return stored_idents, loaded_idents, []
315
+
316
+ visit_node = stmt
317
+
318
+ if isinstance(visit_node,(ast.If, ast.IfExp)):
319
+ # visit_node.body = []
320
+ # visit_node.orlse=[]
321
+ visit_node = stmt.test
322
+
323
+ elif isinstance(visit_node, (ast.With)):
324
+ visit_node.body = []
325
+ visit_node.orlse=[]
326
+
327
+ elif isinstance(visit_node, (ast.While)):
328
+ visit_node.body = []
329
+
330
+ elif isinstance(visit_node, (ast.For)):
331
+ visit_node.body = []
332
+
333
+ elif isinstance(visit_node, ast.Return):
334
+ # imaginary variable
335
+ stored_idents.append("<ret>")
336
+ const_dict["<ret>"] = visit_node.value
337
+ elif isinstance(visit_node, ast.Yield):
338
+ # imaginary variable
339
+ stored_idents.append("<ret>")
340
+ const_dict["<ret>"] = visit_node.value
341
+
342
+ ident_info = get_vars(visit_node)
343
+ for r in ident_info:
344
+ if r['name'] is None or "_hidden_" in r['name']:
345
+ continue
346
+ if r['usage'] == 'store':
347
+ stored_idents.append(r['name'])
348
+ else:
349
+ loaded_idents.append(r['name'])
350
+ if r['usage'] == 'del':
351
+ del_set.append(r['name'])
352
+ return stored_idents, loaded_idents, []
353
+
354
+ def to_json(self):
355
+ pass
356
+
357
+ def print_block(self, block):
358
+ return block.get_source()
359
+
360
+ # compute the dominators
361
+ def compute_idom(self, ssa_blocks):
362
+ """
363
+ Compute immediate dominators for each of blocks
364
+ Args:
365
+ ssa_blocks: blocks from a control flow graph.
366
+ """
367
+ # construct the Graph
368
+ entry_block = ssa_blocks[0]
369
+ G = nx.DiGraph()
370
+ for block in ssa_blocks:
371
+ G.add_node(block.id)
372
+ exits = block.exits
373
+ preds = block.predecessors
374
+ for link in preds+exits:
375
+ G.add_edge(link.source.id, link.target.id)
376
+ # DF = nx.dominance_frontiers(G, entry_block.id)
377
+ idom = nx.immediate_dominators(G, entry_block.id)
378
+ return idom
379
+
380
+ # compute dominance frontiers
381
+ def compute_DF(self, ssa_blocks):
382
+ """
383
+ Compute dominating frontiers for each of blocks
384
+ Args:
385
+ ssa_blocks: blocks from a control flow graph.
386
+ """
387
+ # construct the Graph
388
+ entry_block = ssa_blocks[0]
389
+ G = nx.DiGraph()
390
+ for block in ssa_blocks:
391
+ G.add_node(block.id)
392
+ exits = block.exits
393
+ preds = block.predecessors
394
+ for link in preds+exits:
395
+ G.add_edge(link.source.id, link.target.id)
396
+ DF = nx.dominance_frontiers(G, entry_block.id)
397
+ #idom = nx.immediate_dominators(G, entry_block.id)
398
+ return DF
@@ -0,0 +1,11 @@
1
+ """
2
+ Static Anaysis for Python Programs
3
+ ==================================
4
+ Scalpel is a Python library integrating classical program anaysis algorithms
5
+ with tailored features for Python language. It aims to provide simple and
6
+ efficient solutions to software engineering researchers that are accessible to
7
+ everybody and reusable in various contexts.
8
+ """
9
+
10
+ __all__ = ["cfg", "call_graph", "SSA", "core", "typeinfer", "import_graph", "rewriter"]
11
+ __version__ = '1.0dev'
@@ -0,0 +1,10 @@
1
+ """
2
+ The control-flow graph(CFG) is an essential component in static flow analysis with applications such as program
3
+ optimization and taint analysis.
4
+ scalpel.cfg module is used to construct the control flow graph for given python programs. The basic unit in the CFG,
5
+ Block, contains a list of sequential statements that can be executed in a program without any control jumps. The Blocks
6
+ are linked by Link objects, which represent control flow jumps between two blocks and contain the jump conditions in
7
+ the form of an expression. Please see the example diagram a control flow graph ![Fibonacci CFG](https://raw.githubusercontent.com/SMAT-Lab/Scalpel/main/docs/_static/resources/cfg_example.png)
8
+ """
9
+ from .builder import CFGBuilder
10
+ from .model import Block, Link, CFG