From 558cd5b00c1bf0edd115ebfedf9f3f39f2d47f58 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 10:48:43 +0000 Subject: [PATCH] Python: test dead bindings under no-raise CFG abstraction Adds 'dead_under_no_raise.py' to the bindings test suite, capturing the three CPython patterns where bindings legitimately have no CFG node because the surrounding code is unreachable under the 'no expressions raise' abstraction: 1. Statements after a 'try: return X; except: pass' block. 2. The 'else:' clause of a try whose body always raises. 3. Cache-lookup pattern 'try: return cache[k]; except: pass' followed by computation and store. These bindings intentionally carry no 'cfgdefines=' annotations. If raise modelling is later added to the CFG, the BindingsTest will surface the new CFG nodes as unexpected results and this file will need to be revisited. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../bindings/dead_under_no_raise.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/dead_under_no_raise.py diff --git a/python/ql/test/library-tests/ControlFlow/bindings/dead_under_no_raise.py b/python/ql/test/library-tests/ControlFlow/bindings/dead_under_no_raise.py new file mode 100644 index 00000000000..dbfb857b536 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/dead_under_no_raise.py @@ -0,0 +1,52 @@ +# Dead bindings under the "no expressions raise" CFG abstraction. +# +# The new CFG does not currently model raise edges from arbitrary +# expressions. As a consequence, code that is only reachable through +# exception flow is (correctly) classified as dead and has no CFG node. +# Variable bindings in dead code do not need CFG nodes - SSA / dataflow +# over dead code is moot. +# +# These tests act as a regression guard: the bindings below intentionally +# have no `cfgdefines=` annotations. If raise modelling is later added, +# the BindingsTest infrastructure will surface the new CFG nodes as +# unexpected results, and this file will need to be revisited. + + +def f(obj): # $ cfgdefines=f cfgdefines=obj + try: + return len(obj) + except TypeError: + pass + + # The first try's body always returns; its except handler does not + # raise or otherwise transfer control, so under "no expressions + # raise" the only paths out of the try-statement are dead. Everything + # below is unreachable. + try: + hint = type(obj).__length_hint__ + except AttributeError: + return None + return hint + + +def g(): # $ cfgdefines=g + try: + raise Exception("inner") + except: + raise Exception("outer") + else: + # Unreachable: the inner try body always raises, so the `else:` + # clause never runs. + hit_inner_else = True + + +def h(cache, key): # $ cfgdefines=h cfgdefines=cache cfgdefines=key + try: + return cache[key] + except KeyError: + pass + + # Same pattern as `f`: dead under "no expressions raise". + value = compute(key) + cache[key] = value + return value