|
| 1 | +"""Regression tests for #115: the L4 SDG port layer must be *connected* to the |
| 2 | +statement-level ddg, and call vertices must be anchored to their statement. |
| 3 | +
|
| 4 | +Before the fix, the interprocedural port lattice (``actual_in → formal_in``, |
| 5 | +``formal_out → actual_out`` via param_in/param_out/summary) was an island: no |
| 6 | +ddg edge touched any port, so an end-to-end ``flows_to(def, callee_formal)`` |
| 7 | +walk was inexpressible. The wiring existed in the IR (``fg.extra_edges``, |
| 8 | +emitted by the old v1 ``program_graphs`` projection) but the v2 emission |
| 9 | +dropped it. The restored edges carry ``prov=["reaching-defs"]`` — the same |
| 10 | +label codeanalyzer-typescript ships for its port-routing ddg edges. |
| 11 | +""" |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +from codeanalyzer.dataflow.builder import ( |
| 15 | + _base_types, |
| 16 | + build_function_pdgs, |
| 17 | + build_program_graphs, |
| 18 | + emit_ddg_pointsto_delta, |
| 19 | + emit_l3_body, |
| 20 | + emit_l4, |
| 21 | +) |
| 22 | +from codeanalyzer.dataflow.scalpel_oracle import make_alias_oracle |
| 23 | +from codeanalyzer.dataflow.syntactic import SyntacticOracle |
| 24 | +from codeanalyzer.schema import PyApplication |
| 25 | +from codeanalyzer.schema.assign_ids import assign_ids |
| 26 | +from codeanalyzer.schema.l1_body import populate_l1_body |
| 27 | +from codeanalyzer.schema.py_schema import PyCallEdge |
| 28 | +from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder |
| 29 | + |
| 30 | +_SOURCE = """\ |
| 31 | +def build(flag): |
| 32 | + result = flag + 1 |
| 33 | + return result |
| 34 | +
|
| 35 | +
|
| 36 | +def main(): |
| 37 | + x = 5 |
| 38 | + y = build(x) |
| 39 | + z = y * 2 |
| 40 | + return z |
| 41 | +""" |
| 42 | + |
| 43 | + |
| 44 | +def _build_l4_app(tmp_path: Path): |
| 45 | + f = tmp_path / "app.py" |
| 46 | + f.write_text(_SOURCE, encoding="utf-8") |
| 47 | + mod = SymbolTableBuilder(tmp_path, None).build_pymodule_from_file(f) |
| 48 | + app = PyApplication(symbol_table={"app.py": mod}) |
| 49 | + sig_to_id = assign_ids(app, "portfix") |
| 50 | + app.call_graph = [ |
| 51 | + PyCallEdge(src="app.main", dst="app.build", prov=["jedi"], weight=1) |
| 52 | + ] |
| 53 | + populate_l1_body(app) |
| 54 | + syn_infos, _ = build_function_pdgs( |
| 55 | + app, k=3, oracle_factory=lambda c, fast: SyntacticOracle() |
| 56 | + ) |
| 57 | + emit_l3_body(app, syn_infos, sig_to_id, graphs={"cfg", "dfg", "pdg"}) |
| 58 | + ir = build_program_graphs( |
| 59 | + app, k=3, |
| 60 | + oracle_factory=lambda c, fast: make_alias_oracle(c, fast, _base_types(c)), |
| 61 | + ) |
| 62 | + emit_l4(app, ir, sig_to_id) |
| 63 | + emit_ddg_pointsto_delta(app, syn_infos, ir, sig_to_id) |
| 64 | + mod = app.symbol_table["app.py"] |
| 65 | + return app, mod.functions["build"], mod.functions["main"] |
| 66 | + |
| 67 | + |
| 68 | +def _edges(c, prov=None): |
| 69 | + out = set() |
| 70 | + for e in c.ddg or []: |
| 71 | + if prov is None or e.prov == prov: |
| 72 | + out.add((e.src, e.dst, e.var)) |
| 73 | + return out |
| 74 | + |
| 75 | + |
| 76 | +def test_def_stmt_flows_into_actual_in(tmp_path): |
| 77 | + """`x = 5` (7:4) must feed the argument port of the `build(x)` callsite.""" |
| 78 | + _, _, main = _build_l4_app(tmp_path) |
| 79 | + rd = _edges(main, prov=["reaching-defs"]) |
| 80 | + assert any( |
| 81 | + src == "7:4" and dst.endswith("/actual_in:0") for src, dst, _ in rd |
| 82 | + ), f"missing def→actual_in binding edge; reaching-defs edges: {sorted(rd)}" |
| 83 | + |
| 84 | + |
| 85 | +def test_actual_out_flows_back_to_callsite(tmp_path): |
| 86 | + """The return-value port must flow back into the callsite statement.""" |
| 87 | + _, _, main = _build_l4_app(tmp_path) |
| 88 | + rd = _edges(main, prov=["reaching-defs"]) |
| 89 | + assert any( |
| 90 | + src.endswith("/actual_out") and dst == "8:4" for src, dst, _ in rd |
| 91 | + ), f"missing actual_out→use binding edge; reaching-defs edges: {sorted(rd)}" |
| 92 | + |
| 93 | + |
| 94 | +def test_formal_in_flows_to_first_use(tmp_path): |
| 95 | + """Inside the callee, the parameter port must reach its first-use stmt.""" |
| 96 | + _, build, _ = _build_l4_app(tmp_path) |
| 97 | + rd = _edges(build, prov=["reaching-defs"]) |
| 98 | + assert any( |
| 99 | + src == "@formal_in:0" and dst == "2:4" for src, dst, _ in rd |
| 100 | + ), f"missing formal_in→use edge; reaching-defs edges: {sorted(rd)}" |
| 101 | + |
| 102 | + |
| 103 | +def test_return_stmt_flows_into_formal_out(tmp_path): |
| 104 | + """`return result` (3:4) must feed the callee's formal_out port.""" |
| 105 | + _, build, _ = _build_l4_app(tmp_path) |
| 106 | + rd = _edges(build, prov=["reaching-defs"]) |
| 107 | + assert any( |
| 108 | + src == "3:4" and dst == "@formal_out" for src, dst, _ in rd |
| 109 | + ), f"missing return→formal_out edge; reaching-defs edges: {sorted(rd)}" |
| 110 | + |
| 111 | + |
| 112 | +def test_port_edges_never_replace_or_retag_l3_edges(tmp_path): |
| 113 | + """The wiring is additive: every ssa edge survives, and no reaching-defs |
| 114 | + edge duplicates an (src, dst, var) triple that ssa already carries.""" |
| 115 | + _, build, main = _build_l4_app(tmp_path) |
| 116 | + for c in (build, main): |
| 117 | + ssa = _edges(c, prov=["ssa"]) |
| 118 | + rd = _edges(c, prov=["reaching-defs"]) |
| 119 | + assert ssa, "L3 ssa edges must still be present" |
| 120 | + assert not (ssa & rd), "reaching-defs must not duplicate ssa triples" |
| 121 | + |
| 122 | + |
| 123 | +def test_port_edge_endpoints_exist_in_body(tmp_path): |
| 124 | + _, build, main = _build_l4_app(tmp_path) |
| 125 | + for c in (build, main): |
| 126 | + for e in c.ddg or []: |
| 127 | + assert e.src in c.body, f"dangling ddg src {e.src}" |
| 128 | + assert e.dst in c.body, f"dangling ddg dst {e.dst}" |
| 129 | + |
| 130 | + |
| 131 | +def test_emission_is_idempotent_under_reemit(tmp_path): |
| 132 | + """Re-running emit_l4 + the delta against the same live tree (cache-reuse |
| 133 | + shape) must not duplicate the reaching-defs edges.""" |
| 134 | + f = tmp_path / "app.py" |
| 135 | + f.write_text(_SOURCE, encoding="utf-8") |
| 136 | + mod = SymbolTableBuilder(tmp_path, None).build_pymodule_from_file(f) |
| 137 | + app = PyApplication(symbol_table={"app.py": mod}) |
| 138 | + sig_to_id = assign_ids(app, "portfix") |
| 139 | + app.call_graph = [ |
| 140 | + PyCallEdge(src="app.main", dst="app.build", prov=["jedi"], weight=1) |
| 141 | + ] |
| 142 | + populate_l1_body(app) |
| 143 | + syn_infos, _ = build_function_pdgs( |
| 144 | + app, k=3, oracle_factory=lambda c, fast: SyntacticOracle() |
| 145 | + ) |
| 146 | + emit_l3_body(app, syn_infos, sig_to_id, graphs={"cfg", "dfg", "pdg"}) |
| 147 | + ir = build_program_graphs( |
| 148 | + app, k=3, |
| 149 | + oracle_factory=lambda c, fast: make_alias_oracle(c, fast, _base_types(c)), |
| 150 | + ) |
| 151 | + emit_l4(app, ir, sig_to_id) |
| 152 | + emit_ddg_pointsto_delta(app, syn_infos, ir, sig_to_id) |
| 153 | + main = app.symbol_table["app.py"].functions["main"] |
| 154 | + first = sorted((e.src, e.dst, e.var, tuple(e.prov)) for e in main.ddg) |
| 155 | + emit_l4(app, ir, sig_to_id) |
| 156 | + emit_ddg_pointsto_delta(app, syn_infos, ir, sig_to_id) |
| 157 | + main = app.symbol_table["app.py"].functions["main"] |
| 158 | + second = sorted((e.src, e.dst, e.var, tuple(e.prov)) for e in main.ddg) |
| 159 | + assert first == second, "re-emission must be idempotent" |
| 160 | + |
| 161 | + |
| 162 | +# ---------------------------------------------------------------------------------------------- |
| 163 | +# #115 part 2: call vertices anchored to their statement via `parent`. |
| 164 | +# ---------------------------------------------------------------------------------------------- |
| 165 | + |
| 166 | + |
| 167 | +def test_nested_call_vertex_is_parented_to_its_statement(tmp_path): |
| 168 | + """`y = build(x)`: the call vertex (8:8) floats off the CFG spine; from L3 |
| 169 | + it must carry `parent` = its enclosing statement's local (8:4).""" |
| 170 | + _, _, main = _build_l4_app(tmp_path) |
| 171 | + call_nodes = {k: n for k, n in main.body.items() if n.kind == "call"} |
| 172 | + assert call_nodes, "fixture must materialize a call vertex" |
| 173 | + for key, node in call_nodes.items(): |
| 174 | + assert node.parent == "8:4", ( |
| 175 | + f"call vertex {key} must be parented to its statement, " |
| 176 | + f"got parent={node.parent!r}" |
| 177 | + ) |
| 178 | + |
| 179 | + |
| 180 | +def test_bare_call_statement_needs_no_parent(tmp_path): |
| 181 | + """A bare call (`g(b)`) shares its key with the statement node — no |
| 182 | + self-parent is emitted.""" |
| 183 | + f = tmp_path / "m.py" |
| 184 | + f.write_text( |
| 185 | + "def g(x):\n return x\n\n\ndef f(a):\n g(a)\n return a\n", |
| 186 | + encoding="utf-8", |
| 187 | + ) |
| 188 | + mod = SymbolTableBuilder(tmp_path, None).build_pymodule_from_file(f) |
| 189 | + app = PyApplication(symbol_table={"m.py": mod}) |
| 190 | + sig_to_id = assign_ids(app, "barefix") |
| 191 | + populate_l1_body(app) |
| 192 | + syn_infos, _ = build_function_pdgs( |
| 193 | + app, k=3, oracle_factory=lambda c, fast: SyntacticOracle() |
| 194 | + ) |
| 195 | + emit_l3_body(app, syn_infos, sig_to_id, graphs={"cfg", "dfg", "pdg"}) |
| 196 | + fcallable = app.symbol_table["m.py"].functions["f"] |
| 197 | + call_nodes = {k: n for k, n in fcallable.body.items() if n.kind == "call"} |
| 198 | + assert call_nodes, "fixture must materialize the bare call vertex" |
| 199 | + for key, node in call_nodes.items(): |
| 200 | + assert node.parent != key, "a call must never parent to itself" |
0 commit comments