Skip to content

Commit 3e4d416

Browse files
committed
feat(dataflow): syntactic oracle + factor build_function_pdgs (intraprocedural)
1 parent 5361418 commit 3e4d416

3 files changed

Lines changed: 86 additions & 8 deletions

File tree

codeanalyzer/dataflow/builder.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
import ast
3838
from pathlib import Path
39-
from typing import Dict, List, Optional, Set, Tuple
39+
from typing import Callable, Dict, List, Optional, Set, Tuple
4040

4141
from codeanalyzer.dataflow.access_paths import _PathExtractor, _calls_in
4242
from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
@@ -127,14 +127,24 @@ def _match_args(
127127
return tuple(pairs)
128128

129129

130-
def build_program_graphs(
130+
def build_function_pdgs(
131131
app: PyApplication,
132132
k: int = DEFAULT_K_LIMIT,
133-
) -> ProgramGraphsIR:
134-
"""Build CFG/PDG per callable and the whole-program SDG."""
135-
class_idx = _class_index(app)
136-
callable_idx = _callable_index(app)
137-
133+
*,
134+
oracle_factory: Callable[[PyCallable], object],
135+
) -> Tuple[Dict[str, FunctionInfo], Dict[str, ast.AST]]:
136+
"""Intraprocedural phase only: one ``FunctionInfo`` (CFG → PDG) per
137+
callable, keyed by signature, with no SDG/summary/callsite work.
138+
139+
``oracle_factory(pycallable)`` supplies the may-alias oracle per callable —
140+
``TypeBasedAliasOracle`` for the L4 path, ``SyntacticOracle`` for L3.
141+
142+
Returns ``(infos, func_asts)`` rather than bare PDGs so that the L4
143+
orchestrator (:func:`build_program_graphs`) still has both the
144+
``FunctionInfo`` records its callsite/summary/SDG phases mutate and the
145+
matched def nodes its Phase 2 reads. L3 callers just read ``info.pdg`` per
146+
signature and ignore ``func_asts``.
147+
"""
138148
infos: Dict[str, FunctionInfo] = {}
139149
func_asts: Dict[str, ast.AST] = {}
140150

@@ -166,7 +176,7 @@ def build_program_graphs(
166176
if enclosing_ast is not None:
167177
enclosing_locals |= _locals_of(enclosing_ast)
168178

169-
oracle = TypeBasedAliasOracle(_base_types(pycallable))
179+
oracle = oracle_factory(pycallable)
170180
pdg = build_pdg(
171181
func,
172182
enclosing_locals=enclosing_locals,
@@ -179,6 +189,21 @@ def build_program_graphs(
179189
)
180190
func_asts[pycallable.signature] = func
181191

192+
return infos, func_asts
193+
194+
195+
def build_program_graphs(
196+
app: PyApplication,
197+
k: int = DEFAULT_K_LIMIT,
198+
) -> ProgramGraphsIR:
199+
"""Build CFG/PDG per callable and the whole-program SDG."""
200+
class_idx = _class_index(app)
201+
callable_idx = _callable_index(app)
202+
203+
infos, func_asts = build_function_pdgs(
204+
app, k, oracle_factory=lambda c: TypeBasedAliasOracle(_base_types(c))
205+
)
206+
182207
# Callsites and nested defs, now that every signature is known.
183208
for sig, info in infos.items():
184209
pycallable = callable_idx[sig]

codeanalyzer/dataflow/syntactic.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
"""The L3 (syntactic) alias oracle: two access paths alias iff they are the
18+
identical path. Bypasses the type-based may-alias so def-use yields only
19+
name-equality (textual) edges — the alias-derived edges are the L4 delta."""
20+
21+
from __future__ import annotations
22+
23+
24+
class SyntacticOracle:
25+
def may_alias(self, path_a: str, path_b: str) -> bool:
26+
return path_a == path_b

test/test_v2_l3.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
import textwrap
2+
from pathlib import Path
3+
4+
from codeanalyzer.dataflow.builder import build_function_pdgs
15
from codeanalyzer.dataflow.identity import IdentityMap
6+
from codeanalyzer.dataflow.syntactic import SyntacticOracle
7+
from codeanalyzer.schema.py_schema import PyApplication
8+
from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
29

310
class _Node:
411
def __init__(self, id, start_line, start_column, kind):
@@ -21,3 +28,23 @@ def test_ordinal_ids_for_entry_exit_and_statements():
2128
assert im.ordinal(1) == "can://python/app/m.py/f()@2:4"
2229
assert im.ordinal(2) == "can://python/app/m.py/f()@exit"
2330
assert set(im.node_ids()) == {0, 1, 2}
31+
32+
33+
def test_syntactic_oracle_only_identity_aliases():
34+
o = SyntacticOracle()
35+
assert o.may_alias("x.f", "x.f") is True
36+
assert o.may_alias("x.f", "y.f") is False
37+
assert o.may_alias("a", "b") is False
38+
39+
40+
def test_build_function_pdgs_returns_pdg_per_callable(tmp_path: Path):
41+
f = tmp_path / "m.py"
42+
f.write_text(textwrap.dedent("def f(a):\n b = a\n return b\n"), encoding="utf-8")
43+
mod = SymbolTableBuilder(tmp_path, None).build_pymodule_from_file(f)
44+
app = PyApplication(symbol_table={"m.py": mod})
45+
infos, func_asts = build_function_pdgs(
46+
app, k=3, oracle_factory=lambda c: SyntacticOracle()
47+
)
48+
sig = next(iter(mod.functions.values())).signature
49+
assert sig in infos
50+
assert infos[sig].pdg.cfg.entry_id is not None

0 commit comments

Comments
 (0)