Skip to content

Commit f5bf8ae

Browse files
yoffCopilot
andcommitted
Python: fix augstore for the new CFG and add store/load test
In the legacy CFG the same Python 'Name' that is the target of an augmented assignment has two distinct CFG nodes — a load node (context 3) earlier in the basic block and a store node (context 5) later. 'augstore(load, store)' relates the pair via dominance. The new (shared) CFG canonicalises each AST expression to a single CFG node, so 'load' and 'store' collapse to one. The dominance-based 'augstore' from the legacy implementation no longer holds (it would require 'load.strictlyDominates(load)'), so 'isAugLoad' / 'isAugStore' never fired and 'isStore' missed the AugAssign target entirely. Redefines 'augstore' as reflexive on the AugAssign target's canonical CFG node. With this change: * isAugLoad / isAugStore both fire on the single canonical node. * isStore fires (via 'or augstore(_, this)') — matching the legacy classification that an augmented-assignment target is a store. * isLoad does not fire (excluded by 'not augstore(_, this)'). Adds 'python/ql/test/library-tests/ControlFlow/store-load/' covering plain load/store/delete, parameters, augmented assignment, tuple unpacking, attribute and subscript stores. The test asserts the classification directly on the new-CFG facade. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 79db96c commit f5bf8ae

4 files changed

Lines changed: 115 additions & 7 deletions

File tree

python/ql/lib/semmle/python/controlflow/internal/Cfg.qll

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,22 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode {
218218
}
219219

220220
/**
221-
* Holds if `n` is an augmented assignment load and `store` is the
222-
* corresponding store node.
221+
* Holds if `load` is the load half of an augmented-assignment target,
222+
* and `store` is the corresponding store half.
223+
*
224+
* In the legacy CFG (`Flow.qll`) the same Python `Name` had two
225+
* distinct CFG nodes — a load node (context 3) earlier in the BB, and
226+
* a store node (context 5) later. The legacy `augstore` related the
227+
* pair via dominance.
228+
*
229+
* In the new (shared) CFG, the canonical node for an AST expression is
230+
* unique, so `load` and `store` collapse onto the same CFG node. The
231+
* predicate is therefore reflexive on the augmented-assignment
232+
* target's canonical node.
223233
*/
224234
private predicate augstore(ControlFlowNode load, ControlFlowNode store) {
225-
exists(Py::Expr load_store | exists(Py::AugAssign aa | aa.getTarget() = load_store) |
226-
toAst(load) = load_store and
227-
toAst(store) = load_store and
228-
load.strictlyDominates(store)
229-
)
235+
exists(Py::AugAssign aa | aa.getTarget() = toAst(load)) and
236+
load = store
230237
}
231238

232239
/**

python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.expected

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Inline-expectations test for the store/load/delete/parameter
3+
* classification predicates on the new-CFG facade.
4+
*
5+
* Each tag fires when the corresponding predicate (`isLoad`,
6+
* `isStore`, `isDelete`, `isParameter`, `isAugLoad`, `isAugStore`)
7+
* holds on the canonical CFG node wrapping a `Py::Name` with the
8+
* given identifier.
9+
*
10+
* For subscript / attribute stores the tag fires on the Subscript /
11+
* Attribute node itself, with `value` set to the rightmost identifier
12+
* (the attribute name for `Attribute`, the index expression's textual
13+
* form for `Subscript`).
14+
*/
15+
16+
import python
17+
import semmle.python.controlflow.internal.Cfg as Cfg
18+
import utils.test.InlineExpectationsTest
19+
20+
module StoreLoadTest implements TestSig {
21+
string getARelevantTag() { result = ["load", "store", "delete", "param", "augload", "augstore"] }
22+
23+
predicate hasActualResult(Location location, string element, string tag, string value) {
24+
exists(Cfg::NameNode n |
25+
location = n.getLocation() and
26+
element = n.toString() and
27+
value = n.getId() and
28+
(
29+
n.isLoad() and tag = "load"
30+
or
31+
n.isStore() and not n.isAugStore() and tag = "store"
32+
or
33+
n.isDelete() and tag = "delete"
34+
or
35+
n.isParameter() and tag = "param"
36+
or
37+
n.isAugLoad() and tag = "augload"
38+
or
39+
n.isAugStore() and tag = "augstore"
40+
)
41+
)
42+
}
43+
}
44+
45+
import MakeTest<StoreLoadTest>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Store/load/delete/parameter classification on the new-CFG facade.
2+
#
3+
# Each annotated location carries the (sorted, deduplicated) set of
4+
# kinds the CFG facade reports there. Comparing against the legacy
5+
# 'semmle.python.Flow' classification is done by the comparison query
6+
# 'StoreLoadParity.ql' — annotations here are only the positive
7+
# assertions for the new facade.
8+
#
9+
# Tags:
10+
# load=<id> -- isLoad() fires on the Name
11+
# store=<id> -- isStore() fires
12+
# delete=<id> -- isDelete() fires
13+
# param=<id> -- isParameter() fires
14+
# augload=<id> -- isAugLoad() fires (the LHS of x += ... when read)
15+
# augstore=<id> -- isAugStore() fires (the LHS of x += ... when written)
16+
17+
18+
# --- plain load / store / delete ---
19+
20+
x = 1 # $ store=x
21+
y = x + 1 # $ store=y load=x
22+
print(y) # $ load=print load=y
23+
del x # $ delete=x
24+
25+
26+
# --- function definitions (parameters) ---
27+
28+
def f(a, b=2, *args, c, **kwargs): # $ store=f param=a param=b param=args param=c param=kwargs
29+
return a + b + c # $ load=a load=b load=c
30+
31+
32+
# --- augmented assignment splits one Name into load + store halves ---
33+
34+
def aug(): # $ store=aug
35+
n = 0 # $ store=n
36+
n += 1 # $ augload=n augstore=n
37+
return n # $ load=n
38+
39+
40+
# --- subscript / attribute stores ---
41+
42+
class C: # $ store=C
43+
pass
44+
45+
46+
def stores(obj, container, idx): # $ store=stores param=obj param=container param=idx
47+
obj.attr = 1 # $ load=obj
48+
container[idx] = 2 # $ load=container load=idx
49+
return obj # $ load=obj
50+
51+
52+
# --- tuple unpacking ---
53+
54+
def unpack(pair): # $ store=unpack param=pair
55+
a, b = pair # $ store=a store=b load=pair
56+
return a + b # $ load=a load=b

0 commit comments

Comments
 (0)