codeanalyzer-python 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. codeanalyzer/__main__.py +77 -4
  2. codeanalyzer/core.py +174 -72
  3. codeanalyzer/dataflow/__init__.py +35 -0
  4. codeanalyzer/dataflow/access_paths.py +563 -0
  5. codeanalyzer/dataflow/alias.py +93 -0
  6. codeanalyzer/dataflow/builder.py +688 -0
  7. codeanalyzer/dataflow/cfg.py +605 -0
  8. codeanalyzer/dataflow/defuse.py +113 -0
  9. codeanalyzer/dataflow/dominance.py +140 -0
  10. codeanalyzer/dataflow/identity.py +91 -0
  11. codeanalyzer/dataflow/pdg.py +100 -0
  12. codeanalyzer/dataflow/scalpel_oracle.py +269 -0
  13. codeanalyzer/dataflow/scc.py +91 -0
  14. codeanalyzer/dataflow/sdg.py +424 -0
  15. codeanalyzer/dataflow/slicing.py +93 -0
  16. codeanalyzer/dataflow/summaries.py +217 -0
  17. codeanalyzer/dataflow/syntactic.py +26 -0
  18. codeanalyzer/neo4j/__init__.py +1 -1
  19. codeanalyzer/neo4j/bolt.py +19 -4
  20. codeanalyzer/neo4j/cypher.py +9 -3
  21. codeanalyzer/neo4j/emit.py +10 -5
  22. codeanalyzer/neo4j/project.py +307 -60
  23. codeanalyzer/neo4j/rows.py +18 -15
  24. codeanalyzer/neo4j/schema.py +297 -15
  25. codeanalyzer/options/options.py +4 -0
  26. codeanalyzer/provenance.py +61 -0
  27. codeanalyzer/schema/__init__.py +19 -0
  28. codeanalyzer/schema/assign_ids.py +37 -0
  29. codeanalyzer/schema/call_graph_ids.py +12 -0
  30. codeanalyzer/schema/ids.py +23 -0
  31. codeanalyzer/schema/l1_body.py +29 -0
  32. codeanalyzer/schema/l2_callees.py +36 -0
  33. codeanalyzer/schema/py_schema.py +175 -26
  34. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  35. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
  36. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  37. codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
  38. codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
  39. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
  40. codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
  41. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
  42. codeanalyzer/neo4j/catalog.py +0 -245
  43. codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
  44. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
  45. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {codeanalyzer_python-0.3.0.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