From 6e77a45fb3cd159fe59b41f9bbd333cc4ec235ba Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 14:38:34 +0000 Subject: [PATCH 01/72] Python: Add self-validating CFG tests These tests consist of various Python constructions (hopefully a somewhat comprehensive set) with specific timestamp annotations scattered throughout. When the tests are run using the Python 3 interpreter, these annotations are checked and compared to the "current timestamp" to see that they are in agreement. This is what makes the tests "self-validating". There are a few different kinds of annotations: the basic `t[4]` style (meaning this is executed at timestamp 4), the `t.dead[4]` variant (meaning this _would_ happen at timestamp 4, but it is in a dead branch), and `t.never` (meaning this is never executed at all). In addition to this, there is a query, MissingAnnotations, which checks whether we have applied these annotations maximally. Many expression nodes are not actually annotatable, so there is a sizeable list of excluded nodes for that query. --- .../MissingAnnotations.expected | 0 .../evaluation-order/MissingAnnotations.ql | 15 + .../evaluation-order/TimerUtils.qll | 297 ++++++++++++++++++ .../evaluation-order/test_assert_raise.py | 56 ++++ .../evaluation-order/test_async.py | 97 ++++++ .../evaluation-order/test_augassign.py | 53 ++++ .../evaluation-order/test_basic.py | 223 +++++++++++++ .../evaluation-order/test_boolean.py | 76 +++++ .../evaluation-order/test_classes.py | 74 +++++ .../evaluation-order/test_comprehensions.py | 46 +++ .../evaluation-order/test_conditional.py | 44 +++ .../evaluation-order/test_fstring.py | 34 ++ .../evaluation-order/test_functions.py | 85 +++++ .../ControlFlow/evaluation-order/test_if.py | 108 +++++++ .../evaluation-order/test_lambda.py | 46 +++ .../evaluation-order/test_loops.py | 146 +++++++++ .../evaluation-order/test_match.py | 173 ++++++++++ .../ControlFlow/evaluation-order/test_try.py | 182 +++++++++++ .../evaluation-order/test_unpacking.py | 48 +++ .../ControlFlow/evaluation-order/test_with.py | 58 ++++ .../evaluation-order/test_yield.py | 105 +++++++ .../ControlFlow/evaluation-order/timer.py | 185 +++++++++++ 22 files changed, 2151 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_async.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_augassign.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_classes.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_comprehensions.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_fstring.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_functions.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_lambda.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_unpacking.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_with.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/test_yield.py create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.ql new file mode 100644 index 000000000000..51f324e9399c --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/MissingAnnotations.ql @@ -0,0 +1,15 @@ +/** + * Finds expressions in test functions that lack a timer annotation + * and are not part of the timer mechanism or otherwise excluded. + * An empty result means every annotatable expression is covered. + */ + +import python +import TimerUtils + +from TestFunction f, Expr e +where + e.getScope().getEnclosingScope*() = f and + not isTimerMechanism(e, f) and + not isUnannotatable(e) +select e, "Missing annotation in $@", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll new file mode 100644 index 000000000000..6ad4ef1ef19e --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll @@ -0,0 +1,297 @@ +/** + * Utility library for identifying timer annotations in evaluation-order tests. + * + * Identifies `expr @ t[n]` (matmul), `t(expr, n)` (call), and + * `expr @ t.dead[n]` (dead-code) patterns, extracts timestamp values, + * and provides predicates for traversing consecutive annotated CFG nodes. + */ + +import python + +/** + * A function decorated with `@test` from the timer module. + * The first parameter is the timer object. + */ +class TestFunction extends Function { + TestFunction() { + this.getADecorator().(Name).getId() = "test" and + this.getPositionalParameterCount() >= 1 + } + + /** Gets the name of the timer parameter (first parameter). */ + string getTimerParamName() { result = this.getArgName(0) } +} + +/** Gets an IntegerLiteral from a timestamp expression (single int or tuple of ints). */ +private IntegerLiteral timestampLiteral(Expr timestamps) { + result = timestamps + or + result = timestamps.(Tuple).getAnElt() +} + +/** A timer annotation in the AST. */ +private newtype TTimerAnnotation = + /** `expr @ t[n]` or `expr @ t[n, m, ...]` */ + TMatmulAnnotation(TestFunction func, Expr annotated, Expr timestamps) { + exists(BinaryExpr be | + be.getOp() instanceof MatMult and + be.getRight().(Subscript).getObject().(Name).getId() = func.getTimerParamName() and + be.getScope().getEnclosingScope*() = func and + annotated = be.getLeft() and + timestamps = be.getRight().(Subscript).getIndex() + ) + } or + /** `t(expr, n)` */ + TCallAnnotation(TestFunction func, Expr annotated, Expr timestamps) { + exists(Call call | + call.getFunc().(Name).getId() = func.getTimerParamName() and + call.getScope().getEnclosingScope*() = func and + annotated = call.getArg(0) and + timestamps = call.getArg(1) + ) + } or + /** `expr @ t.dead[n]` — dead-code annotation */ + TDeadAnnotation(TestFunction func, Expr annotated, Expr timestamps) { + exists(BinaryExpr be | + be.getOp() instanceof MatMult and + be.getRight().(Subscript).getObject().(Attribute).getObject("dead").(Name).getId() = + func.getTimerParamName() and + be.getScope().getEnclosingScope*() = func and + annotated = be.getLeft() and + timestamps = be.getRight().(Subscript).getIndex() + ) + } or + /** `expr @ t.never` — annotation for code that should never be evaluated */ + TNeverAnnotation(TestFunction func, Expr annotated) { + exists(BinaryExpr be | + be.getOp() instanceof MatMult and + be.getRight().(Attribute).getObject("never").(Name).getId() = func.getTimerParamName() and + be.getScope().getEnclosingScope*() = func and + annotated = be.getLeft() + ) + } + +/** A timer annotation (wrapping the newtype for a clean API). */ +class TimerAnnotation extends TTimerAnnotation { + /** Gets a timestamp value from this annotation. */ + int getATimestamp() { exists(this.getTimestampExpr(result)) } + + /** Gets the source expression for timestamp value `ts`. */ + IntegerLiteral getTimestampExpr(int ts) { + result = timestampLiteral(this.getTimestampsExpr()) and + result.getValue() = ts + } + + /** Gets the raw timestamp expression (single int or tuple). */ + abstract Expr getTimestampsExpr(); + + /** Gets the test function this annotation belongs to. */ + abstract TestFunction getTestFunction(); + + /** Gets the annotated expression (the LHS of `@` or the first arg of `t(...)`). */ + abstract Expr getAnnotatedExpr(); + + /** Gets the enclosing annotation expression (the `BinaryExpr` or `Call`). */ + abstract Expr getExpr(); + + /** Holds if this is a dead-code annotation (`t.dead[n]`). */ + predicate isDead() { this instanceof DeadTimerAnnotation } + + /** Holds if this is a never-evaluated annotation (`t.never`). */ + predicate isNever() { this instanceof NeverTimerAnnotation } + + string toString() { result = this.getExpr().toString() } + + Location getLocation() { result = this.getExpr().getLocation() } +} + +/** A matmul-based timer annotation: `expr @ t[n]`. */ +class MatmulTimerAnnotation extends TMatmulAnnotation, TimerAnnotation { + TestFunction func; + Expr annotated; + Expr timestamps; + + MatmulTimerAnnotation() { this = TMatmulAnnotation(func, annotated, timestamps) } + + override Expr getTimestampsExpr() { result = timestamps } + + override TestFunction getTestFunction() { result = func } + + override Expr getAnnotatedExpr() { result = annotated } + + override BinaryExpr getExpr() { result.getLeft() = annotated } +} + +/** A call-based timer annotation: `t(expr, n)`. */ +class CallTimerAnnotation extends TCallAnnotation, TimerAnnotation { + TestFunction func; + Expr annotated; + Expr timestamps; + + CallTimerAnnotation() { this = TCallAnnotation(func, annotated, timestamps) } + + override Expr getTimestampsExpr() { result = timestamps } + + override TestFunction getTestFunction() { result = func } + + override Expr getAnnotatedExpr() { result = annotated } + + override Call getExpr() { result.getArg(0) = annotated } +} + +/** A dead-code timer annotation: `expr @ t.dead[n]`. */ +class DeadTimerAnnotation extends TDeadAnnotation, TimerAnnotation { + TestFunction func; + Expr annotated; + Expr timestamps; + + DeadTimerAnnotation() { this = TDeadAnnotation(func, annotated, timestamps) } + + override Expr getTimestampsExpr() { result = timestamps } + + override TestFunction getTestFunction() { result = func } + + override Expr getAnnotatedExpr() { result = annotated } + + override BinaryExpr getExpr() { result.getLeft() = annotated } +} + +/** A never-evaluated annotation: `expr @ t.never`. */ +class NeverTimerAnnotation extends TNeverAnnotation, TimerAnnotation { + TestFunction func; + Expr annotated; + + NeverTimerAnnotation() { this = TNeverAnnotation(func, annotated) } + + override Expr getTimestampsExpr() { none() } + + override TestFunction getTestFunction() { result = func } + + override Expr getAnnotatedExpr() { result = annotated } + + override BinaryExpr getExpr() { result.getLeft() = annotated } +} + +/** + * A CFG node corresponding to a timer annotation. + */ +class TimerCfgNode extends ControlFlowNode { + private TimerAnnotation annot; + + TimerCfgNode() { annot.getExpr() = this.getNode() } + + /** Gets a timestamp value from this annotation. */ + int getATimestamp() { result = annot.getATimestamp() } + + /** Gets the source expression for timestamp value `ts`. */ + IntegerLiteral getTimestampExpr(int ts) { result = annot.getTimestampExpr(ts) } + + /** Gets the test function this annotation belongs to. */ + TestFunction getTestFunction() { result = annot.getTestFunction() } + + /** Holds if this is a dead-code annotation. */ + predicate isDead() { annot.isDead() } + + /** Holds if this is a never-evaluated annotation. */ + predicate isNever() { annot.isNever() } +} + +/** + * Holds if `next` is the next timer annotation reachable from `n` via + * CFG successors (both normal and exceptional), skipping non-annotated + * intermediaries within the same scope. + */ +predicate nextTimerAnnotation(ControlFlowNode n, TimerCfgNode next) { + next = n.getASuccessor() and + next.getScope() = n.getScope() + or + exists(ControlFlowNode mid | + mid = n.getASuccessor() and + not mid instanceof TimerCfgNode and + mid.getScope() = n.getScope() and + nextTimerAnnotation(mid, next) + ) +} + +/** + * Holds if `e` is part of the timer mechanism: a top-level timer + * expression or a (transitive) sub-expression of one. + */ +predicate isTimerMechanism(Expr e, TestFunction f) { + exists(TimerAnnotation a | + a.getTestFunction() = f and + e = a.getExpr().getASubExpression*() + ) +} + +/** + * Holds if expression `e` cannot be annotated due to Python syntax + * limitations (e.g., it is a definition target, a pattern, or part + * of a decorator application). + */ +predicate isUnannotatable(Expr e) { + // Function/class definitions + e instanceof FunctionExpr + or + e instanceof ClassExpr + or + // Docstrings are string literals used as expression statements + e instanceof StringLiteral and e.getParent() instanceof ExprStmt + or + // Function parameters are bound by the call, not evaluated in the body + e instanceof Parameter + or + // Name nodes that are definitions or deletions (assignment targets, def/class + // name bindings, augmented assignment targets, for-loop targets, del targets) + e.(Name).isDefinition() + or + e.(Name).isDeletion() + or + // Tuple/List/Starred nodes in assignment or for-loop targets are + // structural unpack patterns, not evaluations + (e instanceof Tuple or e instanceof List or e instanceof Starred) and + e = any(AssignStmt a).getATarget().getASubExpression*() + or + (e instanceof Tuple or e instanceof List or e instanceof Starred) and + e = any(For f).getTarget().getASubExpression*() + or + // The decorator call node wrapping a function/class definition, + // and its sub-expressions (the decorator name itself) + e = any(FunctionExpr func).getADecoratorCall().getASubExpression*() + or + e = any(ClassExpr cls).getADecoratorCall().getASubExpression*() + or + // Augmented assignment (x += e): the implicit BinaryExpr for the operation + e = any(AugAssign aug).getOperation() + or + // with-statement `as` variables are bindings + (e instanceof Name or e instanceof Tuple or e instanceof List) and + e = any(With w).getOptionalVars().getASubExpression*() + or + // except-clause exception type and `as` variable are part of except syntax + exists(ExceptStmt ex | e = ex.getType() or e = ex.getName()) + or + // match/case pattern expressions are part of pattern syntax + e.getParent+() instanceof Pattern + or + // Subscript/Attribute nodes on the LHS of an assignment are store + // operations, not value expressions (including nested ones like d["a"][1]) + (e instanceof Subscript or e instanceof Attribute) and + e = any(AssignStmt a).getATarget().getASubExpression*() + or + // Match/case guard nodes are part of case syntax + e instanceof Guard + or + // Yield/YieldFrom in statement position — the return value is + // discarded and cannot be meaningfully annotated + (e instanceof Yield or e instanceof YieldFrom) and + e.getParent() instanceof ExprStmt + or + // Synthetic nodes inside desugared comprehensions + e.getScope() = any(Comp c).getFunction() and + ( + e.(Name).getId() = ".0" + or + e instanceof Tuple and e.getParent() instanceof Yield + ) +} diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py new file mode 100644 index 000000000000..9958d922ec8f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py @@ -0,0 +1,56 @@ +"""Assert and raise statement evaluation order.""" + +from timer import test + + +@test +def test_assert_true(t): + x = True @ t[0] + assert x @ t[1] + y = 1 @ t[2] + + +@test +def test_assert_true_with_message(t): + x = True @ t[0] + assert x @ t[1], "msg" @ t.dead[2] + y = 1 @ t[2] + + +@test +def test_assert_false_caught(t): + try: + x = False @ t[0] + assert x @ t[1], "fail" @ t[2] + except AssertionError: + y = 1 @ t[3] + + +@test +def test_raise_caught(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])("test" @ t[2]) @ t[3]) + except ValueError: + y = 2 @ t[4] + + +@test +def test_raise_from_caught(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])("test" @ t[2]) @ t[3]) from ((RuntimeError @ t[4])("cause" @ t[5]) @ t[6]) + except ValueError: + y = 2 @ t[7] + + +@test +def test_bare_reraise(t): + try: + try: + raise ((ValueError @ t[0])("test" @ t[1]) @ t[2]) + except ValueError: + x = 1 @ t[3] + raise + except ValueError: + y = 2 @ t[4] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_async.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_async.py new file mode 100644 index 000000000000..0c9b08e3e9eb --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_async.py @@ -0,0 +1,97 @@ +"""Async/await evaluation order tests. + +Coroutine bodies are lazy — like generators, the body runs only when +awaited (or driven by the event loop). asyncio.run() drives the +coroutine to completion synchronously from the caller's perspective. +""" + +import asyncio +from contextlib import asynccontextmanager +from timer import test + + +@test +def test_simple_async(t): + """Simple async function: body runs inside asyncio.run().""" + async def coro(): + x = 1 @ t[4] + return x @ t[5] + + result = ((asyncio @ t[0]).run @ t[1])((coro @ t[2])() @ t[3]) @ t[6] + + +@test +def test_await_expression(t): + """await suspends the caller until the inner coroutine completes.""" + async def helper(): + return 1 @ t[4] + + async def main(): + x = await helper() @ t[5] + return x @ t[6] + + result = ((asyncio @ t[0]).run @ t[1])((main @ t[2])() @ t[3]) @ t[7] + + +@test +def test_async_for(t): + """async for iterates an async generator.""" + async def agen(): + yield 1 @ t[5] + yield 2 @ t[7] + + async def main(): + async for val in agen() @ t[4]: + val @ t[6, 8] + + ((asyncio @ t[0]).run @ t[1])((main @ t[2])() @ t[3]) @ t[9] + + +@test +def test_async_with(t): + """async with enters/exits an async context manager.""" + @asynccontextmanager + async def ctx(): + yield 1 @ t[5] + + async def main(): + async with ctx() @ t[4] as val: + val @ t[6] + + ((asyncio @ t[0]).run @ t[1])((main @ t[2])() @ t[3]) @ t[7] + + +@test +def test_multiple_awaits(t): + """Sequential awaits in one coroutine.""" + async def task_a(): + return 10 @ t[4] + + async def task_b(): + return 20 @ t[6] + + async def main(): + a = await task_a() @ t[5] + b = await task_b() @ t[7] + return (a @ t[8] + b @ t[9]) @ t[10] + + result = ((asyncio @ t[0]).run @ t[1])((main @ t[2])() @ t[3]) @ t[11] + + +@test +def test_gather(t): + """asyncio.gather schedules coroutines as concurrent tasks.""" + async def task_a(): + return 1 @ t[6] + + async def task_b(): + return 2 @ t[7] + + async def main(): + results = await asyncio.gather( + task_a() @ t[4], + task_b() @ t[5], + ) @ t[8] + return results @ t[9] + + result = ((asyncio @ t[0]).run @ t[1])((main @ t[2])() @ t[3]) @ t[10] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_augassign.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_augassign.py new file mode 100644 index 000000000000..2f1d5eb5c3e6 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_augassign.py @@ -0,0 +1,53 @@ +"""Augmented assignment evaluation order.""" + +from timer import test + + +@test +def test_plus_equals(t): + x = 1 @ t[0] + x += 2 @ t[1] + y = x @ t[2] + + +@test +def test_sub_mul_div(t): + x = 20 @ t[0] + x -= 5 @ t[1] + x *= 2 @ t[2] + x /= 6 @ t[3] + x = 17 @ t[4] + x //= 3 @ t[5] + x %= 3 @ t[6] + y = x @ t[7] + + +@test +def test_power_equals(t): + x = 2 @ t[0] + x **= 3 @ t[1] + y = x @ t[2] + + +@test +def test_bitwise_equals(t): + x = 0b1111 @ t[0] + x &= 0b1010 @ t[1] + x |= 0b0101 @ t[2] + x ^= 0b0011 @ t[3] + y = x @ t[4] + + +@test +def test_shift_equals(t): + x = 1 @ t[0] + x <<= 4 @ t[1] + x >>= 2 @ t[2] + y = x @ t[3] + + +@test +def test_list_extend(t): + x = [1 @ t[0], 2 @ t[1]] @ t[2] + x += [3 @ t[3], 4 @ t[4]] @ t[5] + y = x @ t[6] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py new file mode 100644 index 000000000000..f2ece3a0820d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py @@ -0,0 +1,223 @@ +"""Basic expression evaluation order. + +These tests verify that sub-expressions within a single expression +are evaluated in the expected order (typically left to right for +operands of binary operators, elements of collection literals, etc.) + +Every evaluated expression has a timestamp annotation, except the +timer mechanism itself (t[n], t.dead[n]). +""" + +from timer import test + + +@test +def test_sequential_statements(t): + """Statements execute top to bottom.""" + x = 1 @ t[0] + y = 2 @ t[1] + z = 3 @ t[2] + + +@test +def test_binary_add(t): + """In a + b, left operand evaluates before right.""" + x = (1 @ t[0] + 2 @ t[1]) @ t[2] + + +@test +def test_binary_subtract(t): + """In a - b, left operand evaluates before right.""" + x = (10 @ t[0] - 3 @ t[1]) @ t[2] + + +@test +def test_binary_multiply(t): + """In a * b, left operand evaluates before right.""" + x = ((3 @ t[0]) * (4 @ t[1])) @ t[2] + + +@test +def test_nested_binary(t): + """Sub-expressions evaluate before their containing expression.""" + x = ((1 @ t[0] + 2 @ t[1]) @ t[2] + (3 @ t[3] + 4 @ t[4]) @ t[5]) @ t[6] + + +@test +def test_chained_add(t): + """a + b + c is (a + b) + c: left to right.""" + x = ((1 @ t[0] + 2 @ t[1]) @ t[2] + 3 @ t[3]) @ t[4] + + +@test +def test_mixed_precedence(t): + """In a + b * c, all operands still evaluate left to right.""" + x = (1 @ t[0] + ((2 @ t[1]) * (3 @ t[2])) @ t[3]) @ t[4] + + +@test +def test_string_concat(t): + """String concatenation operands: left to right.""" + x = (("hello" @ t[0] + " " @ t[1]) @ t[2] + "world" @ t[3]) @ t[4] + + +@test +def test_comparison(t): + """In a < b, left operand evaluates before right.""" + x = (1 @ t[0] < 2 @ t[1]) @ t[2] + + +@test +def test_chained_comparison(t): + """Chained a < b < c: all evaluated left to right (b only once).""" + x = (1 @ t[0] < 2 @ t[1] < 3 @ t[2]) @ t[3] + + +@test +def test_list_elements(t): + """List elements evaluate left to right.""" + x = [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3] + + +@test +def test_dict_entries(t): + """Dict: key before value, entries left to right.""" + d = {1 @ t[0]: "a" @ t[1], 2 @ t[2]: "b" @ t[3]} @ t[4] + + +@test +def test_tuple_elements(t): + """Tuple elements evaluate left to right.""" + x = (1 @ t[0], 2 @ t[1], 3 @ t[2]) @ t[3] + + +@test +def test_set_elements(t): + """Set elements evaluate left to right.""" + x = {1 @ t[0], 2 @ t[1], 3 @ t[2]} @ t[3] + + +@test +def test_subscript(t): + """In obj[idx], object evaluates before index.""" + x = ([10 @ t[0], 20 @ t[1], 30 @ t[2]] @ t[3])[1 @ t[4]] @ t[5] + + +@test +def test_slice(t): + """Slice parameters: object, then start, then stop.""" + x = ([1 @ t[0], 2 @ t[1], 3 @ t[2], 4 @ t[3], 5 @ t[4]] @ t[5])[1 @ t[6]:3 @ t[7]] @ t[8] + + +@test +def test_method_call(t): + """Object evaluated, then attribute lookup, then arguments left to right, then call.""" + x = (("hello world" @ t[0]).replace @ t[1])("world" @ t[2], "there" @ t[3]) @ t[4] + + +@test +def test_method_chaining(t): + """Chained method calls: left to right.""" + x = ((((" hello " @ t[0]).strip @ t[1])() @ t[2]).upper @ t[3])() @ t[4] + + +@test +def test_unary_not(t): + """Unary not: operand evaluated first.""" + x = (not True @ t[0]) @ t[1] + + +@test +def test_unary_neg(t): + """Unary negation: operand evaluated first.""" + x = (-(3 @ t[0])) @ t[1] + + +@test +def test_multiple_assignment(t): + """RHS evaluated once in x = y = expr.""" + x = y = (1 @ t[0] + 2 @ t[1]) @ t[2] + + +@test +def test_callable_syntax(t): + """t(value, n) is equivalent to value @ t[n].""" + x = (1 @ t[0] + 2 @ t[1]) @ t[2] + y = (x @ t[3] * 3 @ t[4]) @ t[5] + + +@test +def test_subscript_assign(t): + """In obj[idx] = val, value is evaluated before target sub-expressions.""" + lst = [0 @ t[0], 0 @ t[1], 0 @ t[2]] @ t[3] + (lst @ t[5])[1 @ t[6]] = 42 @ t[4] + x = lst @ t[7] + + +@test +def test_attribute_assign(t): + """In obj.attr = val, value is evaluated before the object.""" + class Obj: + pass + o = (Obj @ t[0])() @ t[1] + (o @ t[3]).x = 42 @ t[2] + y = (o @ t[4]).x @ t[5] + + +@test +def test_nested_subscript_assign(t): + """Nested subscript assignment: val, then outer obj, then keys.""" + d = {"a" @ t[0]: [0 @ t[1], 0 @ t[2]] @ t[3]} @ t[4] + (d @ t[6])["a" @ t[7]][1 @ t[8]] = 99 @ t[5] + x = d @ t[9] + + +@test +def test_unreachable_after_return(t): + """Code after return has no CFG node.""" + def f(): + x = 1 @ t[1] + return x @ t[2] + y = 2 @ t.never + result = (f @ t[0])() @ t[3] + + +@test +def test_none_literal(t): + """None is a name constant.""" + x = None @ t[0] + y = (x @ t[1] is None @ t[2]) @ t[3] + + +@test +def test_delete(t): + """del statement removes a variable binding.""" + x = 1 @ t[0] + del x + y = 2 @ t[1] + + +@test +def test_global(t): + """global statement allows writing to module-level variable.""" + global _test_global_var + _test_global_var = 1 @ t[0] + x = _test_global_var @ t[1] + + +@test +def test_nonlocal(t): + """nonlocal statement allows inner function to rebind outer variable.""" + x = 0 @ t[0] + def inner(): + nonlocal x + x = 1 @ t[2] + (inner @ t[1])() @ t[3] + y = x @ t[4] + + +@test +def test_walrus(t): + """Walrus operator := evaluates the RHS and binds it.""" + if (y := 1 @ t[0]) @ t[1]: + z = y @ t[2] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py new file mode 100644 index 000000000000..d8183cb64842 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py @@ -0,0 +1,76 @@ +"""Short-circuit boolean operators and evaluation order.""" + +from timer import test + + +@test +def test_and_both_sides(t): + # True and X — both operands evaluated, result is X + x = (True @ t[0] and 42 @ t[1]) @ t[2] + + +@test +def test_and_short_circuit(t): + # False and ... — right side never evaluated + x = (False @ t[0] and True @ t.dead[1]) @ t[1] + + +@test +def test_or_short_circuit(t): + # True or ... — right side never evaluated + x = (True @ t[0] or False @ t.dead[1]) @ t[1] + + +@test +def test_or_both_sides(t): + # False or X — both operands evaluated, result is X + x = (False @ t[0] or 42 @ t[1]) @ t[2] + + +@test +def test_not(t): + # not evaluates its operand, then negates + x = (not True @ t[0]) @ t[1] + y = (not False @ t[2]) @ t[3] + + +@test +def test_chained_and(t): + # 1 and 2 and 3 — all truthy, all evaluated left-to-right + x = (1 @ t[0] and 2 @ t[1] and 3 @ t[2]) @ t[3] + + +@test +def test_chained_or(t): + # 0 or "" or 42 — first two falsy, all evaluated until truthy found + x = (0 @ t[0] or "" @ t[1] or 42 @ t[2]) @ t[3] + + +@test +def test_mixed_and_or(t): + # True and False or 42 => (True and False) or 42 => False or 42 => 42 + x = ((True @ t[0] and False @ t[1]) @ t[2] or 42 @ t[3]) @ t[4] + + +@test +def test_and_side_effects(t): + # Both functions called when left side is truthy + def f(): + return 10 @ t[1] + + def g(): + return 20 @ t[4] + + x = ((f @ t[0])() @ t[2] and (g @ t[3])() @ t[5]) @ t[6] + + +@test +def test_or_side_effects(t): + # Both functions called when left side is falsy + def f(): + return 0 @ t[1] + + def g(): + return 20 @ t[4] + + x = ((f @ t[0])() @ t[2] or (g @ t[3])() @ t[5]) @ t[6] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_classes.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_classes.py new file mode 100644 index 000000000000..92313b5073c3 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_classes.py @@ -0,0 +1,74 @@ +"""Class definitions — evaluation order.""" + +from timer import test + + +@test +def test_simple_class(t): + """Simple class definition and instantiation.""" + class Foo: + pass + obj = (Foo @ t[0])() @ t[1] + + +@test +def test_class_with_bases(t): + """Base class expressions evaluated at class definition time.""" + class Base: + pass + class Derived(Base @ t[0]): + pass + obj = (Derived @ t[1])() @ t[2] + + +@test +def test_class_with_methods(t): + """Object evaluated before method is called.""" + class Foo: + def greet(self, name): + return ("hello " @ t[5] + name @ t[6]) @ t[7] + obj = (Foo @ t[0])() @ t[1] + msg = ((obj @ t[2]).greet @ t[3])("world" @ t[4]) @ t[8] + + +@test +def test_class_instantiation(t): + """Arguments to __init__ evaluate before instantiation completes.""" + class Foo: + def __init__(self, x): + (self @ t[3]).x = x @ t[2] + obj = (Foo @ t[0])(42 @ t[1]) @ t[4] + val = (obj @ t[5]).x @ t[6] + + +@test +def test_method_call(t): + """Method arguments evaluate left-to-right before the call.""" + class Calculator: + def __init__(self, value): + (self @ t[3]).value = value @ t[2] + def add(self, x): + return ((self @ t[8]).value @ t[9] + x @ t[10]) @ t[11] + calc = (Calculator @ t[0])(10 @ t[1]) @ t[4] + result = ((calc @ t[5]).add @ t[6])(5 @ t[7]) @ t[12] + + +@test +def test_class_level_attribute(t): + """Multiple attribute accesses in a single expression.""" + class Config: + debug = True @ t[0] + version = 1 @ t[1] + x = ((Config @ t[2]).debug @ t[3], (Config @ t[4]).version @ t[5]) @ t[6] + + +@test +def test_class_decorator(t): + """Decorator expression evaluated, class defined, then decorator called.""" + def add_marker(cls): + (cls @ t[2]).marked = True @ t[1] + return cls @ t[3] + @(add_marker @ t[0]) + class Foo: + pass + result = (Foo @ t[4]).marked @ t[5] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_comprehensions.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_comprehensions.py new file mode 100644 index 000000000000..8ce8ca6e4c46 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_comprehensions.py @@ -0,0 +1,46 @@ +"""Evaluation order tests for comprehensions and generator expressions.""" + +from timer import test + + +@test +def test_list_comprehension(t): + items = [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3] + result = [x @ t[5, 6, 7] for x in items @ t[4]] @ t[8] + + +@test +def test_filtered_comprehension(t): + items = [1 @ t[0], 2 @ t[1], 3 @ t[2], 4 @ t[3]] @ t[4] + result = [x @ t[14, 23] for x in items @ t[5] if (x @ t[6, 10, 15, 19] % 2 @ t[7, 11, 16, 20] == 0 @ t[8, 12, 17, 21]) @ t[9, 13, 18, 22]] @ t[24] + + +@test +def test_dict_comprehension(t): + items = [("a" @ t[0], 1 @ t[1]) @ t[2], ("b" @ t[3], 2 @ t[4]) @ t[5]] @ t[6] + result = {k @ t[8, 10]: v @ t[9, 11] for k, v in items @ t[7]} @ t[12] + + +@test +def test_set_comprehension(t): + items = [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3] + result = {x @ t[5, 6, 7] for x in items @ t[4]} @ t[8] + + +@test +def test_generator_expression(t): + items = [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3] + gen = (x @ t[8, 9, 10] for x in items @ t[4]) @ t[5] + result = (list @ t[6])(gen @ t[7]) @ t[11] + + +@test +def test_nested_comprehension(t): + matrix = [[1 @ t[0], 2 @ t[1]] @ t[2], [3 @ t[3], 4 @ t[4]] @ t[5]] @ t[6] + result = [x @ t[9, 10, 12, 13] for row in matrix @ t[7] for x in row @ t[8, 11]] @ t[14] + + +@test +def test_comprehension_with_call(t): + items = [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3] + result = [(str @ t[5, 8, 11])(x @ t[6, 9, 12]) @ t[7, 10, 13] for x in items @ t[4]] @ t[14] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py new file mode 100644 index 000000000000..2c543e913e4d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py @@ -0,0 +1,44 @@ +"""Ternary conditional expressions and evaluation order.""" + +from timer import test + + +@test +def test_ternary_true(t): + # Condition is True — consequent evaluated, alternative skipped + x = (1 @ t[1] if True @ t[0] else 2 @ t.dead[1]) @ t[2] + + +@test +def test_ternary_false(t): + # Condition is False — alternative evaluated, consequent skipped + x = (1 @ t.dead[1] if False @ t[0] else 2 @ t[1]) @ t[2] + + +@test +def test_ternary_nested(t): + # Nested: outer condition True, inner condition True + # ((10 if C1 else 20) if C2 else 30) — C2 first, then C1, then 10 + x = ((10 @ t[2] if True @ t[1] else 20 @ t.dead[2]) @ t[3] if True @ t[0] else 30 @ t.dead[1]) @ t[4] + + +@test +def test_ternary_assignment(t): + # Ternary result assigned, then used in later expression + value = (100 @ t[1] if True @ t[0] else 200 @ t.dead[1]) @ t[2] + result = (value @ t[3] + 1 @ t[4]) @ t[5] + + +@test +def test_ternary_complex_expressions(t): + # Complex sub-expressions in condition and consequent + x = ((1 @ t[3] + 2 @ t[4]) @ t[5] if (3 @ t[0] > 2 @ t[1]) @ t[2] else (4 @ t.dead[3] + 5 @ t.dead[4]) @ t.dead[5]) @ t[6] + + +@test +def test_ternary_as_argument(t): + # Ternary used as a function argument + def f(a): + return a @ t[4] + + result = (f @ t[0])((1 @ t[2] if True @ t[1] else 2 @ t.dead[2]) @ t[3]) @ t[5] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_fstring.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_fstring.py new file mode 100644 index 000000000000..2dd36f6ef36a --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_fstring.py @@ -0,0 +1,34 @@ +"""F-string evaluation order.""" + +from timer import test + + +@test +def test_simple_fstring(t): + name = "world" @ t[0] + s = f"hello {name @ t[1]}" @ t[2] + + +@test +def test_multi_expr_fstring(t): + a = "hello" @ t[0] + b = "world" @ t[1] + s = f"{a @ t[2]} {b @ t[3]}" @ t[4] + + +@test +def test_nested_fstring(t): + inner = "world" @ t[0] + s = f"hello {f'dear {inner @ t[1]}' @ t[2]}" @ t[3] + + +@test +def test_format_spec(t): + x = 3.14159 @ t[0] + s = f"{x @ t[1]:.2f}" @ t[2] + + +@test +def test_method_in_fstring(t): + name = "world" @ t[0] + s = f"hello {((name @ t[1]).upper @ t[2])() @ t[3]}" @ t[4] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_functions.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_functions.py new file mode 100644 index 000000000000..e19b944c4cef --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_functions.py @@ -0,0 +1,85 @@ +"""Function calls and definitions — evaluation order.""" + +from timer import test + + +@test +def test_argument_order(t): + """Arguments evaluate left-to-right before the call.""" + def add(a, b): + return (a @ t[3] + b @ t[4]) @ t[5] + result = (add @ t[0])(1 @ t[1], 2 @ t[2]) @ t[6] + + +@test +def test_multiple_arguments(t): + """All arguments left-to-right, then the call.""" + def f(a, b, c): + return ((a @ t[4] + b @ t[5]) @ t[6] + c @ t[7]) @ t[8] + result = (f @ t[0])(1 @ t[1], 2 @ t[2], 3 @ t[3]) @ t[9] + + +@test +def test_default_arguments(t): + """Default expressions are evaluated at definition time.""" + val = 5 @ t[0] + def f(a, b=val @ t[1]): + return (a @ t[4] + b @ t[5]) @ t[6] + result = (f @ t[2])(10 @ t[3]) @ t[7] + + +@test +def test_args_kwargs(t): + """*args and **kwargs — expressions evaluated before the call.""" + def f(*args, **kwargs): + return ((sum @ t[9])(args @ t[10]) @ t[11] + (sum @ t[12])(((kwargs @ t[13]).values @ t[14])() @ t[15]) @ t[16]) @ t[17] + args = [1 @ t[0], 2 @ t[1]] @ t[2] + kwargs = {"c" @ t[3]: 3 @ t[4]} @ t[5] + result = (f @ t[6])(*args @ t[7], **kwargs @ t[8]) @ t[18] + + +@test +def test_nested_calls(t): + """Inner call completes before becoming an argument to outer call.""" + def f(x): + return (x @ t[7] + 1 @ t[8]) @ t[9] + def g(x): + return (x @ t[3] * 2 @ t[4]) @ t[5] + result = (f @ t[0])((g @ t[1])(1 @ t[2]) @ t[6]) @ t[10] + + +@test +def test_function_as_argument(t): + """Function object is just another argument, evaluated left-to-right.""" + def apply(fn, x): + return (fn @ t[3])(x @ t[4]) @ t[8] + def double(x): + return (x @ t[5] * 2 @ t[6]) @ t[7] + result = (apply @ t[0])(double @ t[1], 5 @ t[2]) @ t[9] + + +@test +def test_decorator(t): + """Decorator: expression evaluated, function defined, decorator called.""" + def my_decorator(fn): + return fn @ t[1] + @(my_decorator @ t[0]) + def f(): + return 42 @ t[3] + result = (f @ t[2])() @ t[4] + + +@test +def test_keyword_arguments(t): + """Keyword argument values evaluate left-to-right.""" + def f(a, b): + return (a @ t[3] + b @ t[4]) @ t[5] + result = (f @ t[0])(a=1 @ t[1], b=2 @ t[2]) @ t[6] + + +@test +def test_return_value(t): + """The return value is just the result of the call expression.""" + def f(x): + return (x @ t[2] * 2 @ t[3]) @ t[4] + result = (f @ t[0])(3 @ t[1]) @ t[5] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py new file mode 100644 index 000000000000..3190e94c6eba --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py @@ -0,0 +1,108 @@ +"""If/elif/else control flow evaluation order.""" + +from timer import test + + +@test +def test_if_true(t): + x = True @ t[0] + if x @ t[1]: + y = 1 @ t[2] + z = 0 @ t[3] + + +@test +def test_if_false(t): + x = False @ t[0] + if x @ t[1]: + y = 1 @ t.dead[2] + z = 0 @ t[2] + + +@test +def test_if_else_true(t): + x = True @ t[0] + if x @ t[1]: + y = 1 @ t[2] + else: + y = 2 @ t.dead[2] + z = 0 @ t[3] + + +@test +def test_if_else_false(t): + x = False @ t[0] + if x @ t[1]: + y = 1 @ t.dead[2] + else: + y = 2 @ t[2] + z = 0 @ t[3] + + +@test +def test_if_elif_else_first(t): + x = 1 @ t[0] + if (x @ t[1] == 1 @ t[2]) @ t[3]: + y = "first" @ t[4] + elif (x @ t.dead[4] == 2 @ t.dead[5]) @ t.dead[6]: + y = "second" @ t.dead[4] + else: + y = "third" @ t.dead[4] + z = 0 @ t[5] + + +@test +def test_if_elif_else_second(t): + x = 2 @ t[0] + if (x @ t[1] == 1 @ t[2]) @ t[3]: + y = "first" @ t.dead[7] + elif (x @ t[4] == 2 @ t[5]) @ t[6]: + y = "second" @ t[7] + else: + y = "third" @ t.dead[7] + z = 0 @ t[8] + + +@test +def test_if_elif_else_third(t): + x = 3 @ t[0] + if (x @ t[1] == 1 @ t[2]) @ t[3]: + y = "first" @ t.dead[7] + elif (x @ t[4] == 2 @ t[5]) @ t[6]: + y = "second" @ t.dead[7] + else: + y = "third" @ t[7] + z = 0 @ t[8] + + +@test +def test_nested_if_else(t): + x = True @ t[0] + y = True @ t[1] + if x @ t[2]: + if y @ t[3]: + z = 1 @ t[4] + else: + z = 2 @ t.dead[4] + else: + z = 3 @ t.dead[4] + w = 0 @ t[5] + + +@test +def test_if_compound_condition(t): + x = True @ t[0] + y = False @ t[1] + if (x @ t[2] and y @ t[3]) @ t[4]: + z = 1 @ t.dead[5] + else: + z = 2 @ t[5] + w = 0 @ t[6] + + +@test +def test_if_pass(t): + x = True @ t[0] + if x @ t[1]: + pass + z = 0 @ t[2] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_lambda.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_lambda.py new file mode 100644 index 000000000000..c60cbb5b3172 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_lambda.py @@ -0,0 +1,46 @@ +"""Lambda expressions — evaluation order.""" + +from timer import test + + +@test +def test_simple_lambda(t): + """Lambda creates a function object in one step.""" + f = (lambda x: (x @ t[3] + 1 @ t[4]) @ t[5]) @ t[0] + result = (f @ t[1])(10 @ t[2]) @ t[6] + + +@test +def test_lambda_multiple_args(t): + """Lambda call: arguments evaluate left to right.""" + f = (lambda a, b, c: ((a @ t[5] + b @ t[6]) @ t[7] + c @ t[8]) @ t[9]) @ t[0] + result = (f @ t[1])(1 @ t[2], 2 @ t[3], 3 @ t[4]) @ t[10] + + +@test +def test_lambda_default(t): + """Default argument evaluated at lambda creation time.""" + val = 5 @ t[0] + f = (lambda x, y=val @ t[1]: (x @ t[5] + y @ t[6]) @ t[7]) @ t[2] + result = (f @ t[3])(10 @ t[4]) @ t[8] + + +@test +def test_lambda_map(t): + """Lambda body runs once per element when consumed by list(map(...)).""" + f = (lambda x: (x @ t[9, 12, 15] * 2 @ t[10, 13, 16]) @ t[11, 14, 17]) @ t[0] + result = (list @ t[1])((map @ t[2])(f @ t[3], [1 @ t[4], 2 @ t[5], 3 @ t[6]] @ t[7]) @ t[8]) @ t[18] + + +@test +def test_immediately_invoked(t): + """Arguments evaluated, then immediately-invoked lambda called.""" + result = ((lambda x: (x @ t[2] + 1 @ t[3]) @ t[4]) @ t[0])(10 @ t[1]) @ t[5] + + +@test +def test_lambda_closure(t): + """Lambda captures enclosing scope; body runs at call time.""" + x = 10 @ t[0] + f = (lambda: x @ t[3]) @ t[1] + result = (f @ t[2])() @ t[4] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py new file mode 100644 index 000000000000..e81c31acde5c --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py @@ -0,0 +1,146 @@ +"""Loop control flow evaluation order tests.""" + +from timer import test + + +# 1. Simple while loop (fixed iterations) +@test +def test_while_loop(t): + i = 0 @ t[0] + while (i @ t[1, 7, 13, 19] < 3 @ t[2, 8, 14, 20]) @ t[3, 9, 15, 21]: # 4 checks: 3 true + 1 false + i = (i @ t[4, 10, 16] + 1 @ t[5, 11, 17]) @ t[6, 12, 18] + done = True @ t[22] + + +# 2. While loop with break +@test +def test_while_break(t): + i = 0 @ t[0] + while (i @ t[1, 10, 19] < 5 @ t[2, 11, 20]) @ t[3, 12, 21]: + if (i @ t[4, 13, 22] == 2 @ t[5, 14, 23]) @ t[6, 15, 24]: + break + i = (i @ t[7, 16] + 1 @ t[8, 17]) @ t[9, 18] + done = True @ t[25] + + +# 3. While loop with continue +@test +def test_while_continue(t): + i = 0 @ t[0] + total = 0 @ t[1] + while (i @ t[2, 14, 23, 35] < 3 @ t[3, 15, 24, 36]) @ t[4, 16, 25, 37]: + i = (i @ t[5, 17, 26] + 1 @ t[6, 18, 27]) @ t[7, 19, 28] + if (i @ t[8, 20, 29] == 2 @ t[9, 21, 30]) @ t[10, 22, 31]: + continue + total = (total @ t[11, 32] + i @ t[12, 33]) @ t[13, 34] + done = True @ t[38] + + +# 4. While/else (no break — else executes) +@test +def test_while_else(t): + i = 0 @ t[0] + while (i @ t[1, 7, 13] < 2 @ t[2, 8, 14]) @ t[3, 9, 15]: + i = (i @ t[4, 10] + 1 @ t[5, 11]) @ t[6, 12] + else: + done = True @ t[16] + + +# 5. While/else (with break — else skipped) +@test +def test_while_else_break(t): + i = 0 @ t[0] + while (i @ t[1, 10] < 5 @ t[2, 11]) @ t[3, 12]: + if (i @ t[4, 13] == 1 @ t[5, 14]) @ t[6, 15]: + break + i = (i @ t[7] + 1 @ t[8]) @ t[9] + else: + never = True @ t.dead[16] + after = True @ t[16] + + +# 6. Simple for loop over a list +@test +def test_for_list(t): + for x in [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3]: + x @ t[4, 5, 6] + done = True @ t[7] + + +# 7. For loop with range +@test +def test_for_range(t): + for i in (range @ t[0])(3 @ t[1]) @ t[2]: + i @ t[3, 4, 5] + done = True @ t[6] + + +# 8. For loop with break +@test +def test_for_break(t): + for x in [1 @ t[0], 2 @ t[1], 3 @ t[2], 4 @ t[3]] @ t[4]: + if (x @ t[5, 9, 13] == 3 @ t[6, 10, 14]) @ t[7, 11, 15]: + break + x @ t[8, 12] + done = True @ t[16] + + +# 9. For loop with continue +@test +def test_for_continue(t): + total = 0 @ t[0] + for x in [1 @ t[1], 2 @ t[2], 3 @ t[3]] @ t[4]: + if (x @ t[5, 11, 14] == 2 @ t[6, 12, 15]) @ t[7, 13, 16]: + continue + total = (total @ t[8, 17] + x @ t[9, 18]) @ t[10, 19] + done = True @ t[20] + + +# 10. For/else (no break — else executes) +@test +def test_for_else(t): + for x in [1 @ t[0], 2 @ t[1]] @ t[2]: + x @ t[3, 4] + else: + done = True @ t[5] + + +# 11. For/else (with break — else skipped) +@test +def test_for_else_break(t): + for x in [1 @ t[0], 2 @ t[1], 3 @ t[2]] @ t[3]: + if (x @ t[4, 8] == 2 @ t[5, 9]) @ t[6, 10]: + break + x @ t[7] + else: + never = True @ t.dead[11] + after = True @ t[11] + + +# 12. Nested loops +@test +def test_nested_loops(t): + for i in [1 @ t[0], 2 @ t[1]] @ t[2]: + for j in [10 @ t[3, 12], 20 @ t[4, 13]] @ t[5, 14]: + (i @ t[6, 9, 15, 18] + j @ t[7, 10, 16, 19]) @ t[8, 11, 17, 20] + done = True @ t[21] + + +# 13. While True with conditional break +@test +def test_while_true_break(t): + i = 0 @ t[0] + while True @ t[1, 8, 15]: + i = (i @ t[2, 9, 16] + 1 @ t[3, 10, 17]) @ t[4, 11, 18] + if (i @ t[5, 12, 19] == 3 @ t[6, 13, 20]) @ t[7, 14, 21]: + break + done = True @ t[22] + + +# 14. For with enumerate +@test +def test_for_enumerate(t): + for idx, val in (enumerate @ t[0])(["a" @ t[1], "b" @ t[2], "c" @ t[3]] @ t[4]) @ t[5]: + idx @ t[6, 8, 10] + val @ t[7, 9, 11] + done = True @ t[12] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py new file mode 100644 index 000000000000..1dac5b0985c9 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py @@ -0,0 +1,173 @@ +"""Evaluation order for match/case (structural pattern matching, Python 3.10+).""" + +import sys +if sys.version_info < (3, 10): + print("Skipping match/case tests (requires Python 3.10+)") + print("---") + print("0/0 tests passed") + sys.exit(0) + +from timer import test + + +@test +def test_match_literal(t): + x = 1 @ t[0] + match x @ t[1]: + case 1: + y = "one" @ t[2] + case 2: + y = "two" @ t.dead[2] + z = y @ t[3] + + +@test +def test_match_literal_fallthrough(t): + x = 3 @ t[0] + match x @ t[1]: + case 1: + y = "one" @ t.dead[2] + case 2: + y = "two" @ t.dead[2] + case 3: + y = "three" @ t[2] + z = y @ t[3] + + +@test +def test_match_wildcard(t): + x = 42 @ t[0] + match x @ t[1]: + case 1: + y = "one" @ t.dead[2] + case _: + y = "other" @ t[2] + z = y @ t[3] + + +@test +def test_match_capture(t): + x = 42 @ t[0] + match x @ t[1]: + case n: + y = n @ t[2] + z = y @ t[3] + + +@test +def test_match_or_pattern(t): + x = 2 @ t[0] + match x @ t[1]: + case 1 | 2: + y = "low" @ t[2] + case _: + y = "other" @ t.dead[2] + z = y @ t[3] + + +@test +def test_match_guard(t): + x = 5 @ t[0] + match x @ t[1]: + case n if (n @ t[2] > 3 @ t[3]) @ t[4]: + y = n @ t[5] + case _: + y = 0 @ t.dead[5] + z = y @ t[6] + + +@test +def test_match_class_pattern(t): + x = 42 @ t[0] + match x @ t[1]: + case int(): + y = "integer" @ t[2] + case str(): + y = "string" @ t.dead[2] + z = y @ t[3] + + +@test +def test_match_sequence(t): + x = [1 @ t[0], 2 @ t[1]] @ t[2] + match x @ t[3]: + case [a, b]: + y = (a @ t[4] + b @ t[5]) @ t[6] + case _: + y = 0 @ t.dead[6] + z = y @ t[7] + + +@test +def test_match_mapping(t): + x = {"key" @ t[0]: 42 @ t[1]} @ t[2] + match x @ t[3]: + case {"key": value}: + y = value @ t[4] + case _: + y = 0 @ t.dead[4] + z = y @ t[5] + + +@test +def test_match_nested(t): + x = {"users" @ t[0]: [{"name" @ t[1]: "Alice" @ t[2]} @ t[3]] @ t[4]} @ t[5] + match x @ t[6]: + case {"users": [{"name": name}]}: + y = name @ t[7] + case _: + y = "unknown" @ t.dead[7] + z = y @ t[8] + + +@test +def test_match_or_pattern_with_as(t): + """OR pattern with `as` binding and method call on the result.""" + clause = "foo@bar" @ t[0] + match clause @ t[1]: + case (str() as uses) | {"uses": uses}: + result = ((uses @ t[2]).partition @ t[3])("@" @ t[4]) @ t[5] + x = (result @ t[6])[0 @ t[7]] @ t[8] + case _: + raise ((ValueError @ t.dead[2])(clause @ t.dead[3]) @ t.dead[4]) + y = x @ t[9] + + +@test +def test_match_wildcard_raise(t): + """Wildcard case that raises, with OR pattern on the other branch.""" + clause = 42 @ t[0] + try: + match clause @ t[1]: + case (str() as uses) | {"uses": uses}: + result = uses @ t.dead[2] + case _: + raise ((ValueError @ t[2])(f"Invalid: {clause @ t[3]}" @ t[4]) @ t[5]) + except ValueError: + y = 0 @ t[6] + + +@test +def test_match_exhaustive_return_first(t): + """Every case returns; code after match is unreachable (first case taken).""" + def f(x): + match x @ t[2]: + case 1: + return "one" @ t[3] + case _: + return "other" @ t.dead[3] + y = 0 @ t.never + result = (f @ t[0])(1 @ t[1]) @ t[4] + + +@test +def test_match_exhaustive_return_wildcard(t): + """Every case returns; code after match is unreachable (wildcard taken).""" + def f(x): + match x @ t[2]: + case 1: + return "one" @ t.dead[3] + case _: + return "other" @ t[3] + y = 0 @ t.never + result = (f @ t[0])(99 @ t[1]) @ t[4] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py new file mode 100644 index 000000000000..d54730478b11 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py @@ -0,0 +1,182 @@ +"""Exception handling control flow: try/except/else/finally evaluation order.""" + +from timer import test + + +# 1. try/except — no exception raised (except block skipped) +@test +def test_try_no_exception(t): + try: + x = 1 @ t[0] + y = 2 @ t[1] + except ValueError: + z = 3 @ t.dead[2] + after = 0 @ t[2] + + +# 2. try/except — exception raised and caught +@test +def test_try_with_exception(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + y = 2 @ t.never + except ValueError: + z = 3 @ t[3] + after = 0 @ t[4] + + +# 3. try/except/else — no exception (else runs) +@test +def test_try_except_else_no_exception(t): + try: + x = 1 @ t[0] + except ValueError: + y = 2 @ t.dead[1] + else: + z = 3 @ t[1] + after = 0 @ t[2] + + +# 4. try/except/else — exception raised (else skipped) +@test +def test_try_except_else_with_exception(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + except ValueError: + y = 2 @ t[3] + else: + z = 3 @ t.dead[3] + after = 0 @ t[4] + + +# 5. try/finally — no exception +@test +def test_try_finally_no_exception(t): + try: + x = 1 @ t[0] + y = 2 @ t[1] + finally: + z = 3 @ t[2] + after = 0 @ t[3] + + +# 6. try/finally — exception raised (finally runs, then exception propagates) +@test +def test_try_finally_exception(t): + try: + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + finally: + y = 2 @ t[3] + except ValueError: + z = 3 @ t[4] + + +# 7. try/except/finally — no exception +@test +def test_try_except_finally_no_exception(t): + try: + x = 1 @ t[0] + except ValueError: + y = 2 @ t.dead[1] + finally: + z = 3 @ t[1] + after = 0 @ t[2] + + +# 8. try/except/finally — exception caught +@test +def test_try_except_finally_exception(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + except ValueError: + y = 2 @ t[3] + finally: + z = 3 @ t[4] + after = 0 @ t[5] + + +# 9. Multiple except clauses — first matching +@test +def test_multiple_except_first(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + except ValueError: + y = 2 @ t[3] + except TypeError: + z = 3 @ t.dead[3] + after = 0 @ t[4] + + +# 10. Multiple except clauses — second matching +@test +def test_multiple_except_second(t): + try: + x = 1 @ t[0] + raise ((TypeError @ t[1])() @ t[2]) + except ValueError: + y = 2 @ t.dead[3] + except TypeError: + z = 3 @ t[3] + after = 0 @ t[4] + + +# 11. except with `as` binding +@test +def test_except_as_binding(t): + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])("msg" @ t[2]) @ t[3]) + except ValueError as e: + y = (str @ t[4])(e @ t[5]) @ t[6] + after = 0 @ t[7] + + +# 12. Nested try/except +@test +def test_nested_try_except(t): + try: + x = 1 @ t[0] + try: + y = 2 @ t[1] + raise ((ValueError @ t[2])() @ t[3]) + except ValueError: + z = 3 @ t[4] + w = 4 @ t[5] + except TypeError: + v = 5 @ t.dead[6] + after = 0 @ t[6] + + +# 13. try/except in a loop +@test +def test_try_in_loop(t): + total = 0 @ t[0] + for i in (range @ t[1])(3 @ t[2]) @ t[3]: + try: + if (i @ t[4, 11, 20] == 1 @ t[5, 12, 21]) @ t[6, 13, 22]: + raise ((ValueError @ t[14])() @ t[15]) + total = (total @ t[7, 23] + 1 @ t[8, 24]) @ t[9, 25] + except ValueError: + total = (total @ t[16] + 10 @ t[17]) @ t[18] + r = 0 @ t[10, 19, 26] + + +# 14. Re-raise with bare `raise` +@test +def test_reraise(t): + try: + try: + x = 1 @ t[0] + raise ((ValueError @ t[1])() @ t[2]) + except ValueError: + y = 2 @ t[3] + raise + except ValueError: + z = 3 @ t[4] + after = 0 @ t[5] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_unpacking.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_unpacking.py new file mode 100644 index 000000000000..45f292cb0b7d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_unpacking.py @@ -0,0 +1,48 @@ +"""Unpacking and star expressions evaluation order.""" + +from timer import test + + +@test +def test_tuple_unpack(t): + """RHS expression evaluates, then unpacking assigns targets.""" + a, b = (1 @ t[0], 2 @ t[1]) @ t[2] + x = (a @ t[3] + b @ t[4]) @ t[5] + + +@test +def test_list_unpack(t): + """List unpacking: RHS elements left to right, then unpack.""" + [a, b] = [1 @ t[0], 2 @ t[1]] @ t[2] + x = (a @ t[3] + b @ t[4]) @ t[5] + + +@test +def test_star_unpack(t): + """Star unpacking: RHS evaluates first.""" + a, *b = [1 @ t[0], 2 @ t[1], 3 @ t[2], 4 @ t[3]] @ t[4] + x = (a @ t[5], b @ t[6]) @ t[7] + + +@test +def test_nested_unpack(t): + """Nested unpacking: RHS evaluates first.""" + (a, b), c = ((1 @ t[0], 2 @ t[1]) @ t[2], 3 @ t[3]) @ t[4] + x = ((a @ t[5] + b @ t[6]) @ t[7] + c @ t[8]) @ t[9] + + +@test +def test_swap(t): + a = 1 @ t[0] + b = 2 @ t[1] + a, b = (b @ t[2], a @ t[3]) @ t[4] + x = a @ t[5] + y = b @ t[6] + + +@test +def test_unpack_for(t): + pairs = [(1 @ t[0], 2 @ t[1]) @ t[2], (3 @ t[3], 4 @ t[4]) @ t[5]] @ t[6] + for a, b in pairs @ t[7]: + x = a @ t[8, 10] + y = b @ t[9, 11] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_with.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_with.py new file mode 100644 index 000000000000..1dcc7169092b --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_with.py @@ -0,0 +1,58 @@ +"""Evaluation order tests for with statements.""" + +from contextlib import contextmanager +from timer import test + + +@contextmanager +def ctx(value=None): + yield value + + +@test +def test_simple_with(t): + x = 1 @ t[0] + with (ctx @ t[1])() @ t[2]: + y = 2 @ t[3] + z = 3 @ t[4] + + +@test +def test_with_as(t): + with (ctx @ t[0])(42 @ t[1]) @ t[2] as v: + x = v @ t[3] + y = 0 @ t[4] + + +@test +def test_nested_with(t): + with (ctx @ t[0])() @ t[1]: + with (ctx @ t[2])() @ t[3]: + x = 1 @ t[4] + y = 2 @ t[5] + + +@test +def test_multiple_context_managers(t): + with (ctx @ t[0])(1 @ t[1]) @ t[2] as a, (ctx @ t[3])(2 @ t[4]) @ t[5] as b: + x = (a @ t[6], b @ t[7]) @ t[8] + y = 0 @ t[9] + + +@test +def test_with_exception_handling(t): + try: + with (ctx @ t[0])() @ t[1]: + x = 1 @ t[2] + raise ((ValueError @ t[3])() @ t[4]) + except ValueError: + y = 2 @ t[5] + z = 3 @ t[6] + + +@test +def test_with_in_loop(t): + for i in [1 @ t[0], 2 @ t[1]] @ t[2]: + with (ctx @ t[3, 6])() @ t[4, 7]: + x = i @ t[5, 8] + y = 0 @ t[9] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_yield.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_yield.py new file mode 100644 index 000000000000..b2a28d793bc6 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_yield.py @@ -0,0 +1,105 @@ +"""Generator and yield evaluation order tests. + +Generator bodies are lazy — code runs only when iterated. The timer +annotations inside generator bodies fire interleaved with the caller's +annotations, reflecting the suspend/resume semantics of yield. +""" + +from timer import test + + +@test +def test_simple_generator(t): + """Basic generator: body runs on next(), not on gen().""" + def gen(): + yield 1 @ t[4] + yield 2 @ t[8] + + g = (gen @ t[0])() @ t[1] + x = (next @ t[2])(g @ t[3]) @ t[5] + y = (next @ t[6])(g @ t[7]) @ t[9] + + +@test +def test_multiple_yields(t): + """Three yields interleave with three next() calls.""" + def gen(): + yield 1 @ t[4] + yield 2 @ t[8] + yield 3 @ t[12] + + g = (gen @ t[0])() @ t[1] + a = (next @ t[2])(g @ t[3]) @ t[5] + b = (next @ t[6])(g @ t[7]) @ t[9] + c = (next @ t[10])(g @ t[11]) @ t[13] + + +@test +def test_generator_for_loop(t): + """for-loop consumes generator, interleaving body and loop.""" + def gen(): + yield 1 @ t[2] + yield 2 @ t[4] + + for val in (gen @ t[0])() @ t[1]: + val @ t[3, 5] + + +@test +def test_generator_list(t): + """list() consumes the entire generator without interleaving.""" + def gen(): + yield 10 @ t[3] + yield 20 @ t[4] + yield 30 @ t[5] + + result = (list @ t[0])((gen @ t[1])() @ t[2]) @ t[6] + + +@test +def test_yield_from(t): + """yield from delegates to an inner generator transparently.""" + def inner(): + yield 1 @ t[6] + yield 2 @ t[10] + + def outer(): + yield from (inner @ t[4])() @ t[5] + + g = (outer @ t[0])() @ t[1] + x = (next @ t[2])(g @ t[3]) @ t[7] + y = (next @ t[8])(g @ t[9]) @ t[11] + + +@test +def test_generator_return(t): + """Generator return value accessed via yield from.""" + def gen(): + yield 1 @ t[6] + return 42 @ t[10] + + def wrapper(): + result = (yield from (gen @ t[4])() @ t[5]) @ t[11] + yield result @ t[12] + + g = (wrapper @ t[0])() @ t[1] + x = (next @ t[2])(g @ t[3]) @ t[7] + y = (next @ t[8])(g @ t[9]) @ t[13] + + +@test +def test_generator_send(t): + """send() passes a value into the generator at the yield point.""" + def gen(): + x = (yield 1 @ t[4]) @ t[9] + yield (x @ t[10] + 10 @ t[11]) @ t[12] + + g = (gen @ t[0])() @ t[1] + first = (next @ t[2])(g @ t[3]) @ t[5] + second = ((g @ t[6]).send @ t[7])(42 @ t[8]) @ t[13] + + +@test +def test_generator_expression(t): + """Inline generator expression consumed by list().""" + result = (list @ t[0])(x @ t[5, 6, 7] for x in [10 @ t[1], 20 @ t[2], 30 @ t[3]] @ t[4]) @ t[8] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py new file mode 100644 index 000000000000..6cec3fd50cba --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py @@ -0,0 +1,185 @@ +"""Abstract timer for self-validating CFG evaluation-order tests. + +Provides a Timer context manager and a @test decorator for writing tests +that verify the order in which Python evaluates expressions. + +Usage with @test decorator (preferred): + + from timer import test + + @test + def test_sequential(t): + x = 1 @ t[0] + y = 2 @ t[1] + z = (x + y) @ t[2] + +Usage with context manager (manual): + + from timer import Timer + + with Timer("my_test") as t: + x = 1 @ t[0] + +Timer API: + t[n] - assert current timestamp is n, return marker + t[n, m, ...] - assert current timestamp is one of {n, m, ...} + t["label"] - record current timestamp under label (development aid) + t(value, n) - equivalent to: value @ t[n] + +Run a test file directly to self-validate: python test_file.py +""" + +import atexit +import sys + +_results = [] + + +class _Check: + """Marker returned by t[n] — asserts the current timestamp.""" + + __slots__ = ("_timer", "_expected") + + def __init__(self, timer, expected): + self._timer = timer + self._expected = expected + + def __rmatmul__(self, value): + ts = self._timer._tick() + if ts not in self._expected: + self._timer._error( + f"expected {sorted(self._expected)}, got {ts}" + ) + return value + + +class _Label: + """Marker returned by t["name"] — records the timestamp under a label.""" + + __slots__ = ("_timer", "_name") + + def __init__(self, timer, name): + self._timer = timer + self._name = name + + def __rmatmul__(self, value): + ts = self._timer._tick() + self._timer._labels.setdefault(self._name, []).append(ts) + return value + + +class _NeverCheck: + """Marker returned by t.never — fails if the expression is ever evaluated.""" + + def __init__(self, timer): + self._timer = timer + + def __rmatmul__(self, value): + self._timer._error("expression annotated with t.never was evaluated") + return value + + +class _DeadCheck: + """Marker returned by t.dead[n] — fails if the expression is ever evaluated.""" + + def __init__(self, timer): + self._timer = timer + + def __rmatmul__(self, value): + self._timer._error("expression annotated with t.dead was evaluated") + return value + + +class _DeadSubscript: + """Subscriptable returned by t.dead — produces _DeadCheck markers.""" + + def __init__(self, timer): + self._timer = timer + + def __getitem__(self, key): + return _DeadCheck(self._timer) + + +class Timer: + """Context manager tracking abstract evaluation timestamps. + + Each Timer instance maintains a counter starting at 0. Every time an + annotation (@ t[n] or t(value, n)) is encountered, the counter is + compared against the expected value and then incremented. + """ + + def __init__(self, name=""): + self._name = name + self._counter = 0 + self._errors = [] + self._labels = {} + self.dead = _DeadSubscript(self) + self.never = _NeverCheck(self) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._labels: + for name, timestamps in sorted(self._labels.items()): + print(f" {name}: {', '.join(map(str, timestamps))}") + _results.append((self._name, list(self._errors))) + if self._errors: + print(f"{self._name}: FAIL") + for err in self._errors: + print(f" {err}") + else: + print(f"{self._name}: ok") + return False + + def _tick(self): + ts = self._counter + self._counter += 1 + return ts + + def _error(self, msg): + self._errors.append(msg) + + def __getitem__(self, key): + if isinstance(key, str): + return _Label(self, key) + elif isinstance(key, tuple): + return _Check(self, list(key)) + else: + return _Check(self, [key]) + + def __call__(self, value, key): + """Alternative to @ operator: t(value, 4) or t(value, [1, 2, 3]).""" + if isinstance(key, list): + key = tuple(key) + marker = self[key] + return marker.__rmatmul__(value) + + +def test(fn): + """Decorator that creates a Timer and runs the test function immediately. + + The function receives a fresh Timer as its sole argument. Errors are + collected (not raised) and reported after the function completes. + """ + with Timer(fn.__name__) as t: + try: + fn(t) + except Exception as e: + t._error(f"exception: {type(e).__name__}: {e}") + return fn + + +def _report(): + """Print summary at interpreter exit.""" + if not _results: + return + total = len(_results) + passed = sum(1 for _, errors in _results if not errors) + print("---") + print(f"{passed}/{total} tests passed") + if passed < total: + sys.exit(1) + + +atexit.register(_report) From 29ce07c20488ee54502e974de88b80979d220e92 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 15:59:21 +0000 Subject: [PATCH 02/72] Python: Add some CFG-validation queries These use the annotated, self-verifying test files to check various consistency requirements. Some of these may be expressing the same thing in different ways, but it's fairly cheap to keep them around, so I have not attempted to produce a minimal set of queries for this. --- .../AllLiveReachable.expected | 0 .../evaluation-order/AllLiveReachable.ql | 17 ++++++++++++ .../BasicBlockAnnotationGap.expected | 0 .../BasicBlockAnnotationGap.ql | 26 +++++++++++++++++++ .../ContiguousTimestamps.expected | 0 .../evaluation-order/ContiguousTimestamps.ql | 18 +++++++++++++ .../evaluation-order/NoBackwardFlow.expected | 0 .../evaluation-order/NoBackwardFlow.ql | 19 ++++++++++++++ .../NoSharedReachable.expected | 0 .../evaluation-order/NoSharedReachable.ql | 23 ++++++++++++++++ .../evaluation-order/StrictForward.expected | 0 .../evaluation-order/StrictForward.ql | 25 ++++++++++++++++++ 12 files changed, 128 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql new file mode 100644 index 000000000000..946930f29d19 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql @@ -0,0 +1,17 @@ +/** + * Checks that every live (non-dead) annotation in the test function's + * own scope is reachable from the function entry in the CFG. + * Annotations in nested scopes (generators, async, lambdas, comprehensions) + * have separate CFGs and are excluded from this check. + */ + +import python +import TimerUtils + +from TimerCfgNode a, TestFunction f +where + not a.isDead() and + f = a.getTestFunction() and + a.getScope() = f and + not f.getEntryNode().getBasicBlock().reaches(a.getBasicBlock()) +select a, "Unreachable live annotation; entry of $@ does not reach this node", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql new file mode 100644 index 000000000000..8f84e2062181 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql @@ -0,0 +1,26 @@ +/** + * Checks that within a basic block, if a node is annotated then its + * successor is also annotated (or excluded). A gap in annotations + * within a basic block indicates a missing annotation, since there + * are no branches to justify the gap. + * + * Nodes with exceptional successors are excluded, as the exception + * edge leaves the basic block and the normal successor may be dead. + */ + +import python +import TimerUtils + +from TimerCfgNode a, ControlFlowNode succ +where + exists(BasicBlock bb, int i | + a = bb.getNode(i) and + succ = bb.getNode(i + 1) + ) and + not succ instanceof TimerCfgNode and + not isUnannotatable(succ.getNode()) and + not isTimerMechanism(succ.getNode(), a.getTestFunction()) and + not exists(a.getAnExceptionalSuccessor()) and + succ.getNode() instanceof Expr +select a, "Annotated node followed by unannotated $@ in the same basic block", succ, + succ.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.ql new file mode 100644 index 000000000000..456ebf447dad --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/ContiguousTimestamps.ql @@ -0,0 +1,18 @@ +/** + * Checks that timestamps form a contiguous sequence {0, 1, ..., max} + * within each test function. Every integer in the range must appear + * in at least one annotation (live or dead). + */ + +import python +import TimerUtils + +from TestFunction f, int missing, int maxTs, TimerAnnotation maxAnn +where + maxTs = max(TimerAnnotation a | a.getTestFunction() = f | a.getATimestamp()) and + maxAnn.getTestFunction() = f and + maxAnn.getATimestamp() = maxTs and + missing = [0 .. maxTs] and + not exists(TimerAnnotation a | a.getTestFunction() = f and a.getATimestamp() = missing) +select f, "Missing timestamp " + missing + " (max is $@)", maxAnn.getTimestampExpr(maxTs), + maxTs.toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql new file mode 100644 index 000000000000..64f0c3ba1862 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql @@ -0,0 +1,19 @@ +/** + * Checks that time never flows backward between consecutive timer annotations + * in the CFG. For each pair of consecutive annotated nodes (A -> B), there must + * exist timestamps a in A and b in B with a < b. + */ + +import python +import TimerUtils + +from TimerCfgNode a, TimerCfgNode b, int minA, int maxB +where + nextTimerAnnotation(a, b) and + not a.isDead() and + not b.isDead() and + minA = min(a.getATimestamp()) and + maxB = max(b.getATimestamp()) and + minA >= maxB +select a, "Backward flow: $@ flows to $@ (max timestamp $@)", a.getTimestampExpr(minA), + minA.toString(), b, b.getNode().toString(), b.getTimestampExpr(maxB), maxB.toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql new file mode 100644 index 000000000000..59b680206387 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql @@ -0,0 +1,23 @@ +/** + * Checks that two annotations sharing a timestamp value are on + * mutually exclusive CFG paths (neither can reach the other). + */ + +import python +import TimerUtils + +from TimerCfgNode a, TimerCfgNode b, int ts +where + a != b and + not a.isDead() and + not b.isDead() and + a.getTestFunction() = b.getTestFunction() and + ts = a.getATimestamp() and + ts = b.getATimestamp() and + ( + a.getBasicBlock().strictlyReaches(b.getBasicBlock()) + or + exists(BasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) + ) +select a, "Shared timestamp $@ but this node reaches $@", a.getTimestampExpr(ts), ts.toString(), b, + b.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql new file mode 100644 index 000000000000..8147062664fc --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql @@ -0,0 +1,25 @@ +/** + * Stronger version of NoBackwardFlow: for consecutive annotated nodes + * A -> B that both have a single timestamp (non-loop code) and B does + * NOT dominate A (forward edge), requires max(A) < min(B). + */ + +import python +import TimerUtils + +from TimerCfgNode a, TimerCfgNode b, int maxA, int minB +where + nextTimerAnnotation(a, b) and + not a.isDead() and + not b.isDead() and + // Only apply to non-loop code (single timestamps on both sides) + strictcount(a.getATimestamp()) = 1 and + strictcount(b.getATimestamp()) = 1 and + // Forward edge: B does not strictly dominate A (excludes loop back-edges + // but still checks same-basic-block pairs) + not b.getBasicBlock().strictlyDominates(a.getBasicBlock()) and + maxA = max(a.getATimestamp()) and + minB = min(b.getATimestamp()) and + maxA >= minB +select a, "Strict forward violation: $@ flows to $@", a.getTimestampExpr(maxA), "timestamp " + maxA, + b.getTimestampExpr(minB), "timestamp " + minB From 500dec3f67ebad6f849eb444e3e37f3bffcedb54 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 16:10:03 +0000 Subject: [PATCH 03/72] Python: Add BasicBlockOrdering test This one demonstrates a bug in the current CFG. In a dictionary comprehension `{k: v for k, v in d.items()}`, we evaluate the value before the key, which is incorrect. (A fix for this bug has been implemented in a separate PR.) --- .../evaluation-order/BasicBlockOrdering.expected | 1 + .../evaluation-order/BasicBlockOrdering.ql | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected new file mode 100644 index 000000000000..573094ddf734 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected @@ -0,0 +1 @@ +| test_comprehensions.py:21:29:21:40 | ControlFlowNode for BinaryExpr | Basic block ordering: $@ appears before $@ | test_comprehensions.py:21:35:21:35 | IntegerLiteral | timestamp 9 | test_comprehensions.py:21:21:21:21 | IntegerLiteral | timestamp 8 | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql new file mode 100644 index 000000000000..772781e367eb --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql @@ -0,0 +1,16 @@ +/** + * Checks that within a single basic block, annotations appear in + * increasing minimum-timestamp order. + */ + +import python +import TimerUtils + +from TimerCfgNode a, TimerCfgNode b, int minA, int minB +where + exists(BasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) and + minA = min(a.getATimestamp()) and + minB = min(b.getATimestamp()) and + minA >= minB +select a, "Basic block ordering: $@ appears before $@", a.getTimestampExpr(minA), + "timestamp " + minA, b.getTimestampExpr(minB), "timestamp " + minB From e21b6b9b2eb940f398a672939bb75d752621752d Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 16:12:31 +0000 Subject: [PATCH 04/72] Python: Add NeverReachable test This looks for nodes annotated with `t.never` in the test that are reachable in the CFG. This should not happen (it messes with various queries, e.g. the "mixed returns" query), but the test shows that in a few particular cases (involving the `match` statement where all cases contain `return`s), we _do_ have reachable nodes that shouldn't be. --- .../evaluation-order/NeverReachable.expected | 2 ++ .../evaluation-order/NeverReachable.ql | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected new file mode 100644 index 000000000000..200ebdbc6a74 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected @@ -0,0 +1,2 @@ +| test_match.py:159:13:159:23 | BinaryExpr | Node annotated with t.never is reachable in $@ | test_match.py:151:1:151:42 | Function test_match_exhaustive_return_first | test_match_exhaustive_return_first | +| test_match.py:172:13:172:23 | BinaryExpr | Node annotated with t.never is reachable in $@ | test_match.py:164:1:164:45 | Function test_match_exhaustive_return_wildcard | test_match_exhaustive_return_wildcard | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql new file mode 100644 index 000000000000..adc347527539 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql @@ -0,0 +1,26 @@ +/** + * Checks that expressions annotated with `t.never` either have no CFG + * node, or if they do, that the node is not reachable from its scope's + * entry (including within the same basic block). + */ + +import python +import TimerUtils + +from NeverTimerAnnotation ann +where + exists(ControlFlowNode n, Scope s | + n.getNode() = ann.getExpr() and + s = n.getScope() and + ( + // Reachable via inter-block path (includes same block) + s.getEntryNode().getBasicBlock().reaches(n.getBasicBlock()) + or + // In same block as entry but at a later index + exists(BasicBlock bb, int i, int j | + bb.getNode(i) = s.getEntryNode() and bb.getNode(j) = n and i < j + ) + ) + ) +select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(), + ann.getTestFunction().getName() From 66bdd22a1421236281b2c591a35b38f9a1e797e6 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 16:14:56 +0000 Subject: [PATCH 05/72] Python: Add ConsecutiveTimestamps test This one is potentially a bit iffy -- it checks for a very powerful propetry (that implies many of the other queries), but as the test results show, it can produce false positives when there is in fact no problem. We may want to get rid of it entirely, if it becomes too noisy. --- .../ConsecutiveTimestamps.expected | 1 + .../evaluation-order/ConsecutiveTimestamps.ql | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected new file mode 100644 index 000000000000..e20e20c464d4 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected @@ -0,0 +1 @@ +| test_if.py:51:9:51:16 | BinaryExpr | $@ in $@ has no consecutive successor (expected 6) | test_if.py:51:15:51:15 | IntegerLiteral | Timestamp 5 | test_if.py:43:1:43:31 | Function test_if_elif_else_first | test_if_elif_else_first | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql new file mode 100644 index 000000000000..8c7a49b74fbf --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql @@ -0,0 +1,46 @@ +/** + * Checks that consecutive annotated nodes have consecutive timestamps: + * for each annotation with timestamp `a`, some CFG node for that annotation + * must have a next annotation containing `a + 1`. + * + * Handles CFG splitting (e.g., finally blocks duplicated for normal/exceptional + * flow) by checking that at least one split has the required successor. + * + * Only applies to functions where all annotations are in the function's + * own scope (excludes tests with generators, async, comprehensions, or + * lambdas that have annotations in nested scopes). + */ + +import python +import TimerUtils + +/** + * Holds if function `f` has an annotation in a nested scope + * (generator, async function, comprehension, lambda). + */ +private predicate hasNestedScopeAnnotation(TestFunction f) { + exists(TimerAnnotation a | + a.getTestFunction() = f and + a.getExpr().getScope() != f + ) +} + +from TimerAnnotation ann, int a +where + not hasNestedScopeAnnotation(ann.getTestFunction()) and + not ann.isDead() and + a = ann.getATimestamp() and + not exists(TimerCfgNode x, TimerCfgNode y | + ann.getExpr() = x.getNode() and + nextTimerAnnotation(x, y) and + (a + 1) = y.getATimestamp() + ) and + // Exclude the maximum timestamp in the function (it has no successor) + not a = + max(TimerAnnotation other | + other.getTestFunction() = ann.getTestFunction() + | + other.getATimestamp() + ) +select ann, "$@ in $@ has no consecutive successor (expected " + (a + 1) + ")", + ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName() From 166b3226ac6232c4ba5b0962d80ec2a011f585d4 Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 20 Apr 2026 16:12:25 +0000 Subject: [PATCH 06/72] Python: Make CFG tests parameterised Currently we only instantiate them with the old CFG library, but in the future we'll want to do this with the new library as well. Co-authored-by: yoff --- .../evaluation-order/AllLiveReachable.ql | 12 +- .../BasicBlockAnnotationGap.ql | 19 +- .../evaluation-order/BasicBlockOrdering.ql | 12 +- .../evaluation-order/ConsecutiveTimestamps.ql | 32 +-- .../evaluation-order/NeverReachable.ql | 20 +- .../evaluation-order/NoBackwardFlow.ql | 14 +- .../evaluation-order/NoSharedReachable.ql | 19 +- .../evaluation-order/OldCfgImpl.qll | 16 ++ .../evaluation-order/StrictForward.ql | 20 +- .../evaluation-order/TimerUtils.qll | 261 ++++++++++++++++-- 10 files changed, 305 insertions(+), 120 deletions(-) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/OldCfgImpl.qll diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql index 946930f29d19..de44daa3e2c2 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/AllLiveReachable.ql @@ -7,11 +7,13 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerCfgNode a, TestFunction f -where - not a.isDead() and - f = a.getTestFunction() and - a.getScope() = f and - not f.getEntryNode().getBasicBlock().reaches(a.getBasicBlock()) +where allLiveReachable(a, f) select a, "Unreachable live annotation; entry of $@ does not reach this node", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql index 8f84e2062181..0a2b08ff3fdd 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockAnnotationGap.ql @@ -10,17 +10,14 @@ import python import TimerUtils +import OldCfgImpl -from TimerCfgNode a, ControlFlowNode succ -where - exists(BasicBlock bb, int i | - a = bb.getNode(i) and - succ = bb.getNode(i + 1) - ) and - not succ instanceof TimerCfgNode and - not isUnannotatable(succ.getNode()) and - not isTimerMechanism(succ.getNode(), a.getTestFunction()) and - not exists(a.getAnExceptionalSuccessor()) and - succ.getNode() instanceof Expr +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, CfgNode succ +where basicBlockAnnotationGap(a, succ) select a, "Annotated node followed by unannotated $@ in the same basic block", succ, succ.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql index 772781e367eb..30697f1403e2 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.ql @@ -5,12 +5,14 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerCfgNode a, TimerCfgNode b, int minA, int minB -where - exists(BasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) and - minA = min(a.getATimestamp()) and - minB = min(b.getATimestamp()) and - minA >= minB +where basicBlockOrdering(a, b, minA, minB) select a, "Basic block ordering: $@ appears before $@", a.getTimestampExpr(minA), "timestamp " + minA, b.getTimestampExpr(minB), "timestamp " + minB diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql index 8c7a49b74fbf..709fd5665ea4 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.ql @@ -13,34 +13,14 @@ import python import TimerUtils +import OldCfgImpl -/** - * Holds if function `f` has an annotation in a nested scope - * (generator, async function, comprehension, lambda). - */ -private predicate hasNestedScopeAnnotation(TestFunction f) { - exists(TimerAnnotation a | - a.getTestFunction() = f and - a.getExpr().getScope() != f - ) -} +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerAnnotation ann, int a -where - not hasNestedScopeAnnotation(ann.getTestFunction()) and - not ann.isDead() and - a = ann.getATimestamp() and - not exists(TimerCfgNode x, TimerCfgNode y | - ann.getExpr() = x.getNode() and - nextTimerAnnotation(x, y) and - (a + 1) = y.getATimestamp() - ) and - // Exclude the maximum timestamp in the function (it has no successor) - not a = - max(TimerAnnotation other | - other.getTestFunction() = ann.getTestFunction() - | - other.getATimestamp() - ) +where consecutiveTimestamps(ann, a) select ann, "$@ in $@ has no consecutive successor (expected " + (a + 1) + ")", ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql index adc347527539..db55c1d92e4b 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql @@ -6,21 +6,13 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils::CfgTests from NeverTimerAnnotation ann -where - exists(ControlFlowNode n, Scope s | - n.getNode() = ann.getExpr() and - s = n.getScope() and - ( - // Reachable via inter-block path (includes same block) - s.getEntryNode().getBasicBlock().reaches(n.getBasicBlock()) - or - // In same block as entry but at a later index - exists(BasicBlock bb, int i, int j | - bb.getNode(i) = s.getEntryNode() and bb.getNode(j) = n and i < j - ) - ) - ) +where neverReachable(ann) select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql index 64f0c3ba1862..4acf45db3cda 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.ql @@ -6,14 +6,14 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerCfgNode a, TimerCfgNode b, int minA, int maxB -where - nextTimerAnnotation(a, b) and - not a.isDead() and - not b.isDead() and - minA = min(a.getATimestamp()) and - maxB = max(b.getATimestamp()) and - minA >= maxB +where noBackwardFlow(a, b, minA, maxB) select a, "Backward flow: $@ flows to $@ (max timestamp $@)", a.getTimestampExpr(minA), minA.toString(), b, b.getNode().toString(), b.getTimestampExpr(maxB), maxB.toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql index 59b680206387..1fcceb2aca98 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoSharedReachable.ql @@ -5,19 +5,14 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerCfgNode a, TimerCfgNode b, int ts -where - a != b and - not a.isDead() and - not b.isDead() and - a.getTestFunction() = b.getTestFunction() and - ts = a.getATimestamp() and - ts = b.getATimestamp() and - ( - a.getBasicBlock().strictlyReaches(b.getBasicBlock()) - or - exists(BasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) - ) +where noSharedReachable(a, b, ts) select a, "Shared timestamp $@ but this node reaches $@", a.getTimestampExpr(ts), ts.toString(), b, b.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/OldCfgImpl.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/OldCfgImpl.qll new file mode 100644 index 000000000000..6ddfe672de75 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/OldCfgImpl.qll @@ -0,0 +1,16 @@ +/** + * Implementation of the evaluation-order CFG signature using the existing + * Python control flow graph. + */ + +private import python as Py +import TimerUtils + +/** Existing Python CFG implementation of the evaluation-order signature. */ +module OldCfg implements EvalOrderCfgSig { + class CfgNode = Py::ControlFlowNode; + + class BasicBlock = Py::BasicBlock; + + CfgNode scopeGetEntryNode(Scope s) { result = s.getEntryNode() } +} diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql index 8147062664fc..9e64770bab4d 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.ql @@ -6,20 +6,14 @@ import python import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests from TimerCfgNode a, TimerCfgNode b, int maxA, int minB -where - nextTimerAnnotation(a, b) and - not a.isDead() and - not b.isDead() and - // Only apply to non-loop code (single timestamps on both sides) - strictcount(a.getATimestamp()) = 1 and - strictcount(b.getATimestamp()) = 1 and - // Forward edge: B does not strictly dominate A (excludes loop back-edges - // but still checks same-basic-block pairs) - not b.getBasicBlock().strictlyDominates(a.getBasicBlock()) and - maxA = max(a.getATimestamp()) and - minB = min(b.getATimestamp()) and - maxA >= minB +where strictForward(a, b, maxA, minB) select a, "Strict forward violation: $@ flows to $@", a.getTimestampExpr(maxA), "timestamp " + maxA, b.getTimestampExpr(minB), "timestamp " + minB diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll index 6ad4ef1ef19e..7d9329155b5f 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll @@ -173,44 +173,251 @@ class NeverTimerAnnotation extends TNeverAnnotation, TimerAnnotation { } /** - * A CFG node corresponding to a timer annotation. + * Signature module defining the CFG interface needed by evaluation-order tests. + * This allows the test utilities to be instantiated with different CFG implementations. */ -class TimerCfgNode extends ControlFlowNode { - private TimerAnnotation annot; +signature module EvalOrderCfgSig { + /** A control flow node. */ + class CfgNode { + /** Gets a textual representation of this node. */ + string toString(); - TimerCfgNode() { annot.getExpr() = this.getNode() } + /** Gets the location of this node. */ + Location getLocation(); - /** Gets a timestamp value from this annotation. */ - int getATimestamp() { result = annot.getATimestamp() } + /** Gets the AST node corresponding to this CFG node, if any. */ + AstNode getNode(); - /** Gets the source expression for timestamp value `ts`. */ - IntegerLiteral getTimestampExpr(int ts) { result = annot.getTimestampExpr(ts) } + /** Gets a successor of this CFG node (including exceptional). */ + CfgNode getASuccessor(); - /** Gets the test function this annotation belongs to. */ - TestFunction getTestFunction() { result = annot.getTestFunction() } + /** Gets an exceptional successor of this CFG node. */ + CfgNode getAnExceptionalSuccessor(); + + /** Gets the scope containing this CFG node. */ + Scope getScope(); + + /** Gets the basic block containing this CFG node. */ + BasicBlock getBasicBlock(); + } + + /** A basic block in the control flow graph. */ + class BasicBlock { + /** Gets the CFG node at position `n` in this basic block. */ + CfgNode getNode(int n); - /** Holds if this is a dead-code annotation. */ - predicate isDead() { annot.isDead() } + /** Holds if this basic block reaches `bb` (reflexive). */ + predicate reaches(BasicBlock bb); - /** Holds if this is a never-evaluated annotation. */ - predicate isNever() { annot.isNever() } + /** Holds if this basic block strictly reaches `bb` (non-reflexive). */ + predicate strictlyReaches(BasicBlock bb); + + /** Holds if this basic block strictly dominates `bb`. */ + predicate strictlyDominates(BasicBlock bb); + } + + /** Gets the entry CFG node for scope `s`. */ + CfgNode scopeGetEntryNode(Scope s); } /** - * Holds if `next` is the next timer annotation reachable from `n` via - * CFG successors (both normal and exceptional), skipping non-annotated - * intermediaries within the same scope. + * Parameterised module providing CFG-dependent utilities for evaluation-order tests. + * Instantiate with a specific CFG implementation to get `TimerCfgNode` and related predicates. */ -predicate nextTimerAnnotation(ControlFlowNode n, TimerCfgNode next) { - next = n.getASuccessor() and - next.getScope() = n.getScope() - or - exists(ControlFlowNode mid | - mid = n.getASuccessor() and - not mid instanceof TimerCfgNode and - mid.getScope() = n.getScope() and - nextTimerAnnotation(mid, next) - ) +module EvalOrderCfgUtils { + /** The CFG node type from the underlying implementation. */ + final class CfgNode = Input::CfgNode; + + /** The basic block type from the underlying implementation (named to avoid clash with `python::BasicBlock`). */ + final class CfgBasicBlock = Input::BasicBlock; + + /** Gets the entry CFG node for scope `s`. */ + CfgNode scopeGetEntryNode(Scope s) { result = Input::scopeGetEntryNode(s) } + + /** + * A CFG node corresponding to a timer annotation. + */ + class TimerCfgNode extends CfgNode { + private TimerAnnotation annot; + + TimerCfgNode() { annot.getExpr() = this.getNode() } + + /** Gets a timestamp value from this annotation. */ + int getATimestamp() { result = annot.getATimestamp() } + + /** Gets the source expression for timestamp value `ts`. */ + IntegerLiteral getTimestampExpr(int ts) { result = annot.getTimestampExpr(ts) } + + /** Gets the test function this annotation belongs to. */ + TestFunction getTestFunction() { result = annot.getTestFunction() } + + /** Holds if this is a dead-code annotation. */ + predicate isDead() { annot.isDead() } + + /** Holds if this is a never-evaluated annotation. */ + predicate isNever() { annot.isNever() } + } + + /** + * Holds if `next` is the next timer annotation reachable from `n` via + * CFG successors (both normal and exceptional), skipping non-annotated + * intermediaries within the same scope. + */ + predicate nextTimerAnnotation(CfgNode n, TimerCfgNode next) { + next = n.getASuccessor() and + next.getScope() = n.getScope() + or + exists(CfgNode mid | + mid = n.getASuccessor() and + not mid instanceof TimerCfgNode and + mid.getScope() = n.getScope() and + nextTimerAnnotation(mid, next) + ) + } + + /** CFG-dependent test predicates, one per evaluation-order query. */ + module CfgTests { + /** + * Holds if live annotation `a` in function `f` is unreachable from + * the function entry in the CFG. + */ + predicate allLiveReachable(TimerCfgNode a, TestFunction f) { + not a.isDead() and + f = a.getTestFunction() and + a.getScope() = f and + not scopeGetEntryNode(f).getBasicBlock().reaches(a.getBasicBlock()) + } + + /** + * Holds if annotated node `a` is followed by unannotated `succ` in the + * same basic block. + */ + predicate basicBlockAnnotationGap(TimerCfgNode a, CfgNode succ) { + exists(CfgBasicBlock bb, int i | + a = bb.getNode(i) and + succ = bb.getNode(i + 1) + ) and + not succ instanceof TimerCfgNode and + not isUnannotatable(succ.getNode()) and + not isTimerMechanism(succ.getNode(), a.getTestFunction()) and + not exists(a.getAnExceptionalSuccessor()) and + succ.getNode() instanceof Expr + } + + /** + * Holds if annotations `a` and `b` appear in the same basic block with + * `a` before `b`, but `a`'s minimum timestamp is not less than `b`'s. + */ + predicate basicBlockOrdering(TimerCfgNode a, TimerCfgNode b, int minA, int minB) { + exists(CfgBasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) and + minA = min(a.getATimestamp()) and + minB = min(b.getATimestamp()) and + minA >= minB + } + + /** + * Holds if function `f` has an annotation in a nested scope + * (generator, async function, comprehension, lambda). + */ + private predicate hasNestedScopeAnnotation(TestFunction f) { + exists(TimerAnnotation a | + a.getTestFunction() = f and + a.getExpr().getScope() != f + ) + } + + /** + * Holds if annotation `ann` with timestamp `a` has no consecutive + * successor (expected `a + 1`) in the CFG. + */ + predicate consecutiveTimestamps(TimerAnnotation ann, int a) { + not hasNestedScopeAnnotation(ann.getTestFunction()) and + not ann.isDead() and + a = ann.getATimestamp() and + not exists(TimerCfgNode x, TimerCfgNode y | + ann.getExpr() = x.getNode() and + nextTimerAnnotation(x, y) and + (a + 1) = y.getATimestamp() + ) and + // Exclude the maximum timestamp in the function (it has no successor) + not a = + max(TimerAnnotation other | + other.getTestFunction() = ann.getTestFunction() + | + other.getATimestamp() + ) + } + + /** + * Holds if the expression annotated with `t.never` is reachable from + * its scope's entry. + */ + predicate neverReachable(NeverTimerAnnotation ann) { + exists(CfgNode n, Scope s | + n.getNode() = ann.getExpr() and + s = n.getScope() and + ( + // Reachable via inter-block path (includes same block) + scopeGetEntryNode(s).getBasicBlock().reaches(n.getBasicBlock()) + or + // In same block as entry but at a later index + exists(CfgBasicBlock bb, int i, int j | + bb.getNode(i) = scopeGetEntryNode(s) and bb.getNode(j) = n and i < j + ) + ) + ) + } + + /** + * Holds if consecutive annotated nodes `a` -> `b` have backward time + * flow (`minA >= maxB`). + */ + predicate noBackwardFlow(TimerCfgNode a, TimerCfgNode b, int minA, int maxB) { + nextTimerAnnotation(a, b) and + not a.isDead() and + not b.isDead() and + minA = min(a.getATimestamp()) and + maxB = max(b.getATimestamp()) and + minA >= maxB + } + + /** + * Holds if annotations `a` and `b` share timestamp `ts` but `a` + * can reach `b` in the CFG. + */ + predicate noSharedReachable(TimerCfgNode a, TimerCfgNode b, int ts) { + a != b and + not a.isDead() and + not b.isDead() and + a.getTestFunction() = b.getTestFunction() and + ts = a.getATimestamp() and + ts = b.getATimestamp() and + ( + a.getBasicBlock().strictlyReaches(b.getBasicBlock()) + or + exists(CfgBasicBlock bb, int i, int j | a = bb.getNode(i) and b = bb.getNode(j) and i < j) + ) + } + + /** + * Holds if consecutive single-timestamp annotations `a` -> `b` on a + * forward edge have `maxA >= minB`. + */ + predicate strictForward(TimerCfgNode a, TimerCfgNode b, int maxA, int minB) { + nextTimerAnnotation(a, b) and + not a.isDead() and + not b.isDead() and + // Only apply to non-loop code (single timestamps on both sides) + strictcount(a.getATimestamp()) = 1 and + strictcount(b.getATimestamp()) = 1 and + // Forward edge: B does not strictly dominate A (excludes loop back-edges + // but still checks same-basic-block pairs) + not b.getBasicBlock().strictlyDominates(a.getBasicBlock()) and + maxA = max(a.getATimestamp()) and + minB = min(b.getATimestamp()) and + maxA >= minB + } + } } /** From 53f34376c0d54eb11fe3e56f7e1b491cc5aa5dc0 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 16 Apr 2026 14:31:51 +0000 Subject: [PATCH 07/72] Python: First stab at shared control-flow --- python/ql/lib/qlpack.yml | 1 + .../controlflow/internal/AstNodeImpl.qll | 376 ++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll diff --git a/python/ql/lib/qlpack.yml b/python/ql/lib/qlpack.yml index 981ab78ff33e..c8b592b95f01 100644 --- a/python/ql/lib/qlpack.yml +++ b/python/ql/lib/qlpack.yml @@ -7,6 +7,7 @@ library: true upgrades: upgrades dependencies: codeql/concepts: ${workspace} + codeql/controlflow: ${workspace} codeql/dataflow: ${workspace} codeql/mad: ${workspace} codeql/regex: ${workspace} diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll new file mode 100644 index 000000000000..fd99d05c0edc --- /dev/null +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -0,0 +1,376 @@ +/** + * Provides a newtype-based interface layer that mediates between the existing + * Python AST classes and the shared control-flow library's `AstSig` signature. + * + * The newtype unifies Python's `Stmt`, `Expr`, `Scope`, and `StmtList` into a + * single `AstNode` type. Notably, `StmtList` (which is not an `AstNode` in the + * existing Python AST) is wrapped as a `BlockStmt` (a subtype of `Stmt`), + * since the shared CFG library expects statement blocks to be statements. + */ + +private import python as Py +private import codeql.controlflow.ControlFlowGraph + +private module Ast { + /** The newtype representing AST nodes for the shared CFG library. */ + private newtype TAstNode = + TStmtNode(Py::Stmt s) or + TExprNode(Py::Expr e) or + TScopeNode(Py::Scope sc) or + TStmtListNode(Py::StmtList sl) + + /** + * An AST node for the shared CFG. Each branch of the newtype gets a + * subclass that overrides `toString` and `getLocation`. + */ + class Node extends TAstNode { + string toString() { none() } + + Py::Location getLocation() { none() } + + /** Gets the enclosing scope of this node, if any. */ + ScopeNode getEnclosingScope() { none() } + } + + class StmtNode extends Node, TStmtNode { + /** Gets the underlying Python statement. */ + Py::Stmt asStmt() { this = TStmtNode(result) } + + override string toString() { result = this.asStmt().toString() } + + override Py::Location getLocation() { result = this.asStmt().getLocation() } + + /** Gets the enclosing scope of this statement. */ + override ScopeNode getEnclosingScope() { result.asScope() = this.asStmt().getScope() } + } + + class ExprNode extends Node, TExprNode { + /** Gets the underlying Python expression. */ + Py::Expr asExpr() { this = TExprNode(result) } + + override string toString() { result = this.asExpr().toString() } + + override Py::Location getLocation() { result = this.asExpr().getLocation() } + + /** Gets the enclosing scope of this expression. */ + override ScopeNode getEnclosingScope() { result.asScope() = this.asExpr().getScope() } + } + + class ScopeNode extends Node, TScopeNode { + /** Gets the underlying Python scope. */ + Py::Scope asScope() { this = TScopeNode(result) } + + override string toString() { result = this.asScope().toString() } + + override Py::Location getLocation() { result = this.asScope().getLocation() } + + /** Gets the body of this scope. */ + StmtListNode getBody() { result.asStmtList() = this.asScope().getBody() } + + /** Gets the enclosing scope of this scope, if any. */ + override ScopeNode getEnclosingScope() { result.asScope() = this.asScope().getEnclosingScope() } + } + + class StmtListNode extends Node, TStmtListNode { + /** Gets the underlying Python statement list. */ + Py::StmtList asStmtList() { this = TStmtListNode(result) } + + override string toString() { result = this.asStmtList().toString() } + + // StmtList has no native location; approximate with first item's location. + override Py::Location getLocation() { result = this.asStmtList().getItem(0).getLocation() } + + /** Gets the `n`th (zero-based) statement in this block. */ + StmtNode getItem(int n) { result.asStmt() = this.asStmtList().getItem(n) } + + /** Gets the last statement in this block. */ + StmtNode getLastItem() { result.asStmt() = this.asStmtList().getLastItem() } + + /** Gets the enclosing scope of this statement list. */ + override ScopeNode getEnclosingScope() { + result.asScope() = this.asStmtList().getParent().(Py::Scope) + or + result.asScope() = this.asStmtList().getParent().(Py::Stmt).getScope() + } + } + + /** An `if` statement. */ + class IfNode extends StmtNode { + private Py::If ifStmt; + + IfNode() { ifStmt = this.asStmt() } + + /** Gets the condition of this `if` statement. */ + ExprNode getTest() { result.asExpr() = ifStmt.getTest() } + + /** Gets the if-true branch. */ + StmtListNode getBody() { result.asStmtList() = ifStmt.getBody() } + + /** Gets the if-false branch, if any. */ + StmtListNode getOrelse() { result.asStmtList() = ifStmt.getOrelse() } + } + + /** An expression statement. */ + class ExprStmtNode extends StmtNode { + private Py::ExprStmt exprStmt; + + ExprStmtNode() { exprStmt = this.asStmt() } + + /** Gets the expression in this statement. */ + ExprNode getValue() { result.asExpr() = exprStmt.getValue() } + } +} + +/** Provides an implementation of the AST signature for Python. */ +module AstSigImpl implements AstSig { + class AstNode = Ast::Node; + + /** Gets the child of `n` at the specified (zero-based) index. */ + AstNode getChild(AstNode n, int index) { + exists(Ast::IfNode ifNode | ifNode = n | + index = 0 and result = ifNode.getTest() + or + index = 1 and result = ifNode.getBody() + or + index = 2 and result = ifNode.getOrelse() + ) + or + result = n.(Ast::StmtListNode).getItem(index) + or + index = 0 and result = n.(Ast::ExprStmtNode).getValue() + } + + Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingScope() } + + /** + * A callable: a function, class, or module scope. + * + * In Python, all three are executable scopes with statement bodies. + */ + class Callable extends Ast::ScopeNode { } + + /** Gets the body of callable `c`. */ + AstNode callableGetBody(Callable c) { result = c.getBody() } + + /** A statement. Includes both wrapped `Stmt` nodes and `StmtList` blocks. */ + class Stmt extends AstNode { + Stmt() { this instanceof Ast::StmtNode or this instanceof Ast::StmtListNode } + } + + /** An expression. */ + class Expr extends Ast::ExprNode { } + + /** A block of statements, wrapping Python's `StmtList`. */ + class BlockStmt extends Stmt, Ast::StmtListNode { + /** Gets the `n`th (zero-based) statement in this block. */ + Stmt getStmt(int n) { result = Ast::StmtListNode.super.getItem(n) } + + /** Gets the last statement in this block. */ + Stmt getLastStmt() { result = Ast::StmtListNode.super.getLastItem() } + } + + /** An expression statement. */ + class ExprStmt extends Stmt, Ast::ExprStmtNode { + /** Gets the expression in this expression statement. */ + Expr getExpr() { result = this.getValue() } + } + + /** + * An `if` statement. + * + * Python's `elif` chains are represented as nested `If` nodes in the + * else branch's `StmtList`. The shared CFG library handles this naturally: + * `getElse()` returns the `BlockStmt` wrapping the else branch, and if that + * block contains a single `If`, the result is a chained conditional. + */ + class IfStmt extends Stmt, Ast::IfNode { + /** Gets the condition of this `if` statement. */ + Expr getCondition() { result = this.getTest() } + + /** Gets the `then` (true) branch of this `if` statement. */ + Stmt getThen() { result = Ast::IfNode.super.getBody() } + + /** Gets the `else` (false) branch of this `if` statement, if any. */ + Stmt getElse() { result = this.getOrelse() } + } + + // ===== Stub types for constructs not yet implemented ===== + /** A loop statement. Not yet implemented for Python. */ + class LoopStmt extends Stmt { + LoopStmt() { none() } + + /** Gets the body of this loop statement. */ + Stmt getBody() { none() } + } + + /** A `while` loop statement. Not yet implemented for Python. */ + class WhileStmt extends LoopStmt { + /** Gets the boolean condition of this `while` loop. */ + Expr getCondition() { none() } + } + + /** A `do-while` loop statement. Python has no do-while construct. */ + class DoStmt extends LoopStmt { + /** Gets the boolean condition of this `do-while` loop. */ + Expr getCondition() { none() } + } + + /** A C-style `for` loop. Python has no C-style for loop. */ + class ForStmt extends LoopStmt { + /** Gets the initializer expression at the specified position. */ + Expr getInit(int index) { none() } + + /** Gets the boolean condition of this `for` loop. */ + Expr getCondition() { none() } + + /** Gets the update expression at the specified position. */ + Expr getUpdate(int index) { none() } + } + + /** A for-each loop. Not yet implemented for Python. */ + class ForeachStmt extends LoopStmt { + /** Gets the loop variable. */ + Expr getVariable() { none() } + + /** Gets the collection being iterated. */ + Expr getCollection() { none() } + } + + /** A `break` statement. Not yet implemented for Python. */ + class BreakStmt extends Stmt { + BreakStmt() { none() } + } + + /** A `continue` statement. Not yet implemented for Python. */ + class ContinueStmt extends Stmt { + ContinueStmt() { none() } + } + + /** A `return` statement. Not yet implemented for Python. */ + class ReturnStmt extends Stmt { + ReturnStmt() { none() } + + /** Gets the expression being returned, if any. */ + Expr getExpr() { none() } + } + + /** A `throw`/`raise` statement. Not yet implemented for Python. */ + class ThrowStmt extends Stmt { + ThrowStmt() { none() } + + /** Gets the expression being thrown. */ + Expr getExpr() { none() } + } + + /** A `try` statement. Not yet implemented for Python. */ + class TryStmt extends Stmt { + TryStmt() { none() } + + /** Gets the body of this `try` statement. */ + Stmt getBody() { none() } + + /** Gets the `catch` clause at the specified position. */ + CatchClause getCatch(int index) { none() } + + /** Gets the `finally` block of this `try` statement, if any. */ + Stmt getFinally() { none() } + } + + /** A catch clause. Not yet implemented for Python. */ + class CatchClause extends AstNode { + CatchClause() { none() } + + /** Gets the variable declared by this catch clause. */ + AstNode getVariable() { none() } + + /** Gets the guard condition, if any. */ + Expr getCondition() { none() } + + /** Gets the body of this catch clause. */ + Stmt getBody() { none() } + } + + /** A switch/match statement. Not yet implemented for Python. */ + class Switch extends AstNode { + Switch() { none() } + + /** Gets the expression being switched on. */ + Expr getExpr() { none() } + + /** Gets the case at the specified position. */ + Case getCase(int index) { none() } + + /** Gets the statement at the specified position. */ + Stmt getStmt(int index) { none() } + } + + /** A case in a switch/match. Not yet implemented for Python. */ + class Case extends AstNode { + Case() { none() } + + /** Gets a pattern being matched. */ + AstNode getAPattern() { none() } + + /** Gets the guard expression, if any. */ + Expr getGuard() { none() } + + /** Gets the body of this case. */ + AstNode getBody() { none() } + } + + /** A default case. Not yet implemented for Python. */ + class DefaultCase extends Case { } + + /** A ternary conditional expression. Not yet implemented for Python. */ + class ConditionalExpr extends Expr { + ConditionalExpr() { none() } + + /** Gets the condition of this expression. */ + Expr getCondition() { none() } + + /** Gets the true branch of this expression. */ + Expr getThen() { none() } + + /** Gets the false branch of this expression. */ + Expr getElse() { none() } + } + + /** A binary expression. Not yet implemented for Python. */ + class BinaryExpr extends Expr { + BinaryExpr() { none() } + + /** Gets the left operand. */ + Expr getLeftOperand() { none() } + + /** Gets the right operand. */ + Expr getRightOperand() { none() } + } + + /** A short-circuiting logical AND expression. Not yet implemented for Python. */ + class LogicalAndExpr extends BinaryExpr { } + + /** A short-circuiting logical OR expression. Not yet implemented for Python. */ + class LogicalOrExpr extends BinaryExpr { } + + /** A null-coalescing expression. Python has no null-coalescing operator. */ + class NullCoalescingExpr extends BinaryExpr { } + + /** A unary expression. Not yet implemented for Python. */ + class UnaryExpr extends Expr { + UnaryExpr() { none() } + + /** Gets the operand. */ + Expr getOperand() { none() } + } + + /** A logical NOT expression. Not yet implemented for Python. */ + class LogicalNotExpr extends UnaryExpr { } + + /** A boolean literal expression. Not yet implemented for Python. */ + class BooleanLiteral extends Expr { + BooleanLiteral() { none() } + + /** Gets the boolean value of this literal. */ + boolean getValue() { none() } + } +} From 5519570157c28961823b54bdff9f29c4438c31ec Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 20 Apr 2026 11:34:05 +0000 Subject: [PATCH 08/72] Python: Use fields everywhere in new AST classes Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index fd99d05c0edc..6fb36ffc115e 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -33,64 +33,80 @@ private module Ast { } class StmtNode extends Node, TStmtNode { + private Py::Stmt stmt; + + StmtNode() { this = TStmtNode(stmt) } + /** Gets the underlying Python statement. */ - Py::Stmt asStmt() { this = TStmtNode(result) } + Py::Stmt asStmt() { result = stmt } - override string toString() { result = this.asStmt().toString() } + override string toString() { result = stmt.toString() } - override Py::Location getLocation() { result = this.asStmt().getLocation() } + override Py::Location getLocation() { result = stmt.getLocation() } /** Gets the enclosing scope of this statement. */ - override ScopeNode getEnclosingScope() { result.asScope() = this.asStmt().getScope() } + override ScopeNode getEnclosingScope() { result.asScope() = stmt.getScope() } } class ExprNode extends Node, TExprNode { + private Py::Expr expr; + + ExprNode() { this = TExprNode(expr) } + /** Gets the underlying Python expression. */ - Py::Expr asExpr() { this = TExprNode(result) } + Py::Expr asExpr() { result = expr } - override string toString() { result = this.asExpr().toString() } + override string toString() { result = expr.toString() } - override Py::Location getLocation() { result = this.asExpr().getLocation() } + override Py::Location getLocation() { result = expr.getLocation() } /** Gets the enclosing scope of this expression. */ - override ScopeNode getEnclosingScope() { result.asScope() = this.asExpr().getScope() } + override ScopeNode getEnclosingScope() { result.asScope() = expr.getScope() } } class ScopeNode extends Node, TScopeNode { + private Py::Scope scope; + + ScopeNode() { this = TScopeNode(scope) } + /** Gets the underlying Python scope. */ - Py::Scope asScope() { this = TScopeNode(result) } + Py::Scope asScope() { result = scope } - override string toString() { result = this.asScope().toString() } + override string toString() { result = scope.toString() } - override Py::Location getLocation() { result = this.asScope().getLocation() } + override Py::Location getLocation() { result = scope.getLocation() } /** Gets the body of this scope. */ - StmtListNode getBody() { result.asStmtList() = this.asScope().getBody() } + StmtListNode getBody() { result.asStmtList() = scope.getBody() } /** Gets the enclosing scope of this scope, if any. */ - override ScopeNode getEnclosingScope() { result.asScope() = this.asScope().getEnclosingScope() } + override ScopeNode getEnclosingScope() { result.asScope() = scope.getEnclosingScope() } } class StmtListNode extends Node, TStmtListNode { + private Py::StmtList stmtList; + + StmtListNode() { this = TStmtListNode(stmtList) } + /** Gets the underlying Python statement list. */ - Py::StmtList asStmtList() { this = TStmtListNode(result) } + Py::StmtList asStmtList() { result = stmtList } - override string toString() { result = this.asStmtList().toString() } + override string toString() { result = stmtList.toString() } // StmtList has no native location; approximate with first item's location. - override Py::Location getLocation() { result = this.asStmtList().getItem(0).getLocation() } + override Py::Location getLocation() { result = stmtList.getItem(0).getLocation() } /** Gets the `n`th (zero-based) statement in this block. */ - StmtNode getItem(int n) { result.asStmt() = this.asStmtList().getItem(n) } + StmtNode getItem(int n) { result.asStmt() = stmtList.getItem(n) } /** Gets the last statement in this block. */ - StmtNode getLastItem() { result.asStmt() = this.asStmtList().getLastItem() } + StmtNode getLastItem() { result.asStmt() = stmtList.getLastItem() } /** Gets the enclosing scope of this statement list. */ override ScopeNode getEnclosingScope() { - result.asScope() = this.asStmtList().getParent().(Py::Scope) + result.asScope() = stmtList.getParent().(Py::Scope) or - result.asScope() = this.asStmtList().getParent().(Py::Stmt).getScope() + result.asScope() = stmtList.getParent().(Py::Stmt).getScope() } } From 28ebe213379405011bec59dca2a2a77ffc859240 Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 20 Apr 2026 11:50:51 +0000 Subject: [PATCH 09/72] Python: Instantiate CFG module fully Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 6fb36ffc115e..526733a340a9 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -390,3 +390,40 @@ module AstSigImpl implements AstSig { boolean getValue() { none() } } } + +private module Cfg0 = Make0; + +private import Cfg0 + +private module Cfg1 = Make1; + +private import Cfg1 + +private module Cfg2 = Make2; + +private import Cfg2 + +private module Input implements InputSig1, InputSig2 { + predicate cfgCachedStageRef() { CfgCachedStage::ref() } + + private newtype TLabel = TNone() + + class Label extends TLabel { + string toString() { result = "label" } + } + + predicate beginAbruptCompletion( + AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c, boolean always + ) { + none() + } + + predicate endAbruptCompletion(AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c) { + none() + } + + predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { none() } +} + +import CfgCachedStage +import Public From 49c38dddb7624907c33d9ce06cbb002508c7ec76 Mon Sep 17 00:00:00 2001 From: Taus Date: Mon, 20 Apr 2026 16:13:44 +0000 Subject: [PATCH 10/72] Python: Instantiate CFG tests with new CFG library Co-authored-by: yoff --- .../NewCfgAllLiveReachable.expected | 0 .../NewCfgAllLiveReachable.ql | 14 +++++ .../NewCfgBasicBlockAnnotationGap.expected | 0 .../NewCfgBasicBlockAnnotationGap.ql | 26 ++++++++++ .../NewCfgBasicBlockOrdering.expected | 0 .../NewCfgBasicBlockOrdering.ql | 21 ++++++++ .../NewCfgConsecutiveTimestamps.expected | 0 .../NewCfgConsecutiveTimestamps.ql | 29 +++++++++++ .../evaluation-order/NewCfgImpl.qll | 52 +++++++++++++++++++ .../NewCfgNeverReachable.expected | 0 .../evaluation-order/NewCfgNeverReachable.ql | 21 ++++++++ .../NewCfgNoBackwardFlow.expected | 0 .../evaluation-order/NewCfgNoBackwardFlow.ql | 22 ++++++++ .../NewCfgNoSharedReachable.expected | 0 .../NewCfgNoSharedReachable.ql | 21 ++++++++ .../NewCfgStrictForward.expected | 0 .../evaluation-order/NewCfgStrictForward.ql | 22 ++++++++ 17 files changed, 228 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.ql new file mode 100644 index 000000000000..75f02d14a9cb --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAllLiveReachable.ql @@ -0,0 +1,14 @@ +/** New-CFG version of AllLiveReachable. */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, TestFunction f +where allLiveReachable(a, f) +select a, "Unreachable live annotation; entry of $@ does not reach this node", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.ql new file mode 100644 index 000000000000..80dd759a3651 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockAnnotationGap.ql @@ -0,0 +1,26 @@ +/** + * New-CFG version of BasicBlockAnnotationGap. + * + * Original: + * Checks that within a basic block, if a node is annotated then its + * successor is also annotated (or excluded). A gap in annotations + * within a basic block indicates a missing annotation, since there + * are no branches to justify the gap. + * + * Nodes with exceptional successors are excluded, as the exception + * edge leaves the basic block and the normal successor may be dead. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, CfgNode succ +where basicBlockAnnotationGap(a, succ) +select a, "Annotated node followed by unannotated $@ in the same basic block", succ, + succ.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.ql new file mode 100644 index 000000000000..f06d08d937e3 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBasicBlockOrdering.ql @@ -0,0 +1,21 @@ +/** + * New-CFG version of BasicBlockOrdering. + * + * Original: + * Checks that within a single basic block, annotations appear in + * increasing minimum-timestamp order. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, TimerCfgNode b, int minA, int minB +where basicBlockOrdering(a, b, minA, minB) +select a, "Basic block ordering: $@ appears before $@", a.getTimestampExpr(minA), + "timestamp " + minA, b.getTimestampExpr(minB), "timestamp " + minB diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.ql new file mode 100644 index 000000000000..8e52663d6eaf --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.ql @@ -0,0 +1,29 @@ +/** + * New-CFG version of ConsecutiveTimestamps. + * + * Original: + * Checks that consecutive annotated nodes have consecutive timestamps: + * for each annotation with timestamp `a`, some CFG node for that annotation + * must have a next annotation containing `a + 1`. + * + * Handles CFG splitting (e.g., finally blocks duplicated for normal/exceptional + * flow) by checking that at least one split has the required successor. + * + * Only applies to functions where all annotations are in the function's + * own scope (excludes tests with generators, async, comprehensions, or + * lambdas that have annotations in nested scopes). + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerAnnotation ann, int a +where consecutiveTimestamps(ann, a) +select ann, "$@ in $@ has no consecutive successor (expected " + (a + 1) + ")", + ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll new file mode 100644 index 000000000000..8549ca1b2060 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll @@ -0,0 +1,52 @@ +/** + * Implementation of the evaluation-order CFG signature using the new + * shared control flow graph from AstNodeImpl. + */ + +private import python as Py +import TimerUtils +private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl + +private class NewControlFlowNode = CfgImpl::ControlFlowNode; + +private class NewBasicBlock = CfgImpl::BasicBlock; + +/** New (shared) CFG implementation of the evaluation-order signature. */ +module NewCfg implements EvalOrderCfgSig { + class CfgNode instanceof NewControlFlowNode { + string toString() { result = NewControlFlowNode.super.toString() } + + Py::Location getLocation() { result = NewControlFlowNode.super.getLocation() } + + Py::AstNode getNode() { + result = CfgImpl::astNodeToPyNode(NewControlFlowNode.super.getAstNode()) + } + + CfgNode getASuccessor() { result = NewControlFlowNode.super.getASuccessor() } + + CfgNode getAnExceptionalSuccessor() { + result = NewControlFlowNode.super.getAnExceptionSuccessor() + } + + Py::Scope getScope() { result = NewControlFlowNode.super.getEnclosingCallable().asScope() } + + BasicBlock getBasicBlock() { result = NewControlFlowNode.super.getBasicBlock() } + } + + class BasicBlock instanceof NewBasicBlock { + string toString() { result = NewBasicBlock.super.toString() } + + CfgNode getNode(int n) { result = NewBasicBlock.super.getNode(n) } + + predicate reaches(BasicBlock bb) { this = bb or this.strictlyReaches(bb) } + + predicate strictlyReaches(BasicBlock bb) { NewBasicBlock.super.getASuccessor+() = bb } + + predicate strictlyDominates(BasicBlock bb) { NewBasicBlock.super.strictlyDominates(bb) } + } + + CfgNode scopeGetEntryNode(Py::Scope s) { + result instanceof CfgImpl::ControlFlow::EntryNode and + result.getScope() = s + } +} diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql new file mode 100644 index 000000000000..3430d49b57ef --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql @@ -0,0 +1,21 @@ +/** + * New-CFG version of NeverReachable. + * + * Original: + * Checks that expressions annotated with `t.never` either have no CFG + * node, or if they do, that the node is not reachable from its scope's + * entry (including within the same basic block). + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils::CfgTests + +from NeverTimerAnnotation ann +where neverReachable(ann) +select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(), + ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.ql new file mode 100644 index 000000000000..442ca5f5456c --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBackwardFlow.ql @@ -0,0 +1,22 @@ +/** + * New-CFG version of NoBackwardFlow. + * + * Original: + * Checks that time never flows backward between consecutive timer annotations + * in the CFG. For each pair of consecutive annotated nodes (A -> B), there must + * exist timestamps a in A and b in B with a < b. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, TimerCfgNode b, int minA, int maxB +where noBackwardFlow(a, b, minA, maxB) +select a, "Backward flow: $@ flows to $@ (max timestamp $@)", a.getTimestampExpr(minA), + minA.toString(), b, b.getNode().toString(), b.getTimestampExpr(maxB), maxB.toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.ql new file mode 100644 index 000000000000..5a1a1aba2a7a --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoSharedReachable.ql @@ -0,0 +1,21 @@ +/** + * New-CFG version of NoSharedReachable. + * + * Original: + * Checks that two annotations sharing a timestamp value are on + * mutually exclusive CFG paths (neither can reach the other). + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, TimerCfgNode b, int ts +where noSharedReachable(a, b, ts) +select a, "Shared timestamp $@ but this node reaches $@", a.getTimestampExpr(ts), ts.toString(), b, + b.getNode().toString() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.ql new file mode 100644 index 000000000000..ebbc60346db0 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgStrictForward.ql @@ -0,0 +1,22 @@ +/** + * New-CFG version of StrictForward. + * + * Original: + * Stronger version of NoBackwardFlow: for consecutive annotated nodes + * A -> B that both have a single timestamp (non-loop code) and B does + * NOT dominate A (forward edge), requires max(A) < min(B). + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode a, TimerCfgNode b, int maxA, int minB +where strictForward(a, b, maxA, minB) +select a, "Strict forward violation: $@ flows to $@", a.getTimestampExpr(maxA), "timestamp " + maxA, + b.getTimestampExpr(minB), "timestamp " + minB From 2f2c071920e2e15a9720ae1d23ce0adc3d733c5e Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 13:54:26 +0000 Subject: [PATCH 11/72] Python: More AstNodeImpl improvements Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 521 +++++++++++++++--- 1 file changed, 439 insertions(+), 82 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 526733a340a9..47df5c0f619a 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -17,7 +17,17 @@ private module Ast { TStmtNode(Py::Stmt s) or TExprNode(Py::Expr e) or TScopeNode(Py::Scope sc) or - TStmtListNode(Py::StmtList sl) + TStmtListNode(Py::StmtList sl) or + /** + * A synthetic node representing an intermediate pair in a multi-operand + * `and`/`or` expression. For `a and b and c` (values 0,1,2), we + * synthesize a right-nested tree: the pair at index 1 represents + * `b and c`, which becomes the right operand of the outermost pair. + * + * Only created for inner pairs (index >= 1); the outermost pair (index 0) + * is represented by the original `BoolExpr` node via `TExprNode`. + */ + TBoolExprPair(Py::BoolExpr be, int index) { index = [1 .. count(be.getAValue()) - 2] } /** * An AST node for the shared CFG. Each branch of the newtype gets a @@ -135,6 +145,226 @@ private module Ast { /** Gets the expression in this statement. */ ExprNode getValue() { result.asExpr() = exprStmt.getValue() } } + + /** A `while` statement. */ + class WhileNode extends StmtNode { + private Py::While whileStmt; + + WhileNode() { whileStmt = this.asStmt() } + + ExprNode getTest() { result.asExpr() = whileStmt.getTest() } + + StmtListNode getBody() { result.asStmtList() = whileStmt.getBody() } + + StmtListNode getOrelse() { result.asStmtList() = whileStmt.getOrelse() } + } + + /** A `for` statement. */ + class ForNode extends StmtNode { + private Py::For forStmt; + + ForNode() { forStmt = this.asStmt() } + + ExprNode getTarget() { result.asExpr() = forStmt.getTarget() } + + ExprNode getIter() { result.asExpr() = forStmt.getIter() } + + StmtListNode getBody() { result.asStmtList() = forStmt.getBody() } + + StmtListNode getOrelse() { result.asStmtList() = forStmt.getOrelse() } + } + + /** A `return` statement. */ + class ReturnNode extends StmtNode { + private Py::Return ret; + + ReturnNode() { ret = this.asStmt() } + + ExprNode getValue() { result.asExpr() = ret.getValue() } + } + + /** A `raise` statement. */ + class RaiseNode extends StmtNode { + private Py::Raise raise; + + RaiseNode() { raise = this.asStmt() } + + ExprNode getException() { result.asExpr() = raise.getException() } + + ExprNode getCause() { result.asExpr() = raise.getCause() } + } + + /** A `break` statement. */ + class BreakNode extends StmtNode { + BreakNode() { this.asStmt() instanceof Py::Break } + } + + /** A `continue` statement. */ + class ContinueNode extends StmtNode { + ContinueNode() { this.asStmt() instanceof Py::Continue } + } + + /** A `try` statement. */ + class TryNode extends StmtNode { + private Py::Try tryStmt; + + TryNode() { tryStmt = this.asStmt() } + + StmtListNode getBody() { result.asStmtList() = tryStmt.getBody() } + + StmtListNode getOrelse() { result.asStmtList() = tryStmt.getOrelse() } + + StmtListNode getFinalbody() { result.asStmtList() = tryStmt.getFinalbody() } + + ExceptionHandlerNode getHandler(int i) { result.asStmt() = tryStmt.getHandler(i) } + } + + /** An exception handler (`except` or `except*`). */ + class ExceptionHandlerNode extends StmtNode { + private Py::ExceptionHandler handler; + + ExceptionHandlerNode() { handler = this.asStmt() } + + ExprNode getType() { result.asExpr() = handler.getType() } + + ExprNode getName() { result.asExpr() = handler.getName() } + + StmtListNode getBody() { + result.asStmtList() = handler.(Py::ExceptStmt).getBody() or + result.asStmtList() = handler.(Py::ExceptGroupStmt).getBody() + } + } + + /** A conditional expression (`x if cond else y`). */ + class IfExpNode extends ExprNode { + private Py::IfExp ifExp; + + IfExpNode() { ifExp = this.asExpr() } + + ExprNode getTest() { result.asExpr() = ifExp.getTest() } + + ExprNode getBody() { result.asExpr() = ifExp.getBody() } + + ExprNode getOrelse() { result.asExpr() = ifExp.getOrelse() } + } + + /** A Python binary expression (arithmetic, bitwise, matmul, etc.). */ + class BinaryExprNode extends ExprNode { + private Py::BinaryExpr binExpr; + + BinaryExprNode() { binExpr = this.asExpr() } + + ExprNode getLeft() { result.asExpr() = binExpr.getLeft() } + + ExprNode getRight() { result.asExpr() = binExpr.getRight() } + } + + /** A subscript expression (`obj[index]`). */ + class SubscriptNode extends ExprNode { + private Py::Subscript sub; + + SubscriptNode() { sub = this.asExpr() } + + ExprNode getObject() { result.asExpr() = sub.getObject() } + + ExprNode getIndex() { result.asExpr() = sub.getIndex() } + } + + /** + * A `not` expression. This is a `UnaryExpr` whose operator is `Not`. + */ + class NotExprNode extends ExprNode { + private Py::UnaryExpr notExpr; + + NotExprNode() { notExpr = this.asExpr() and notExpr.getOp() instanceof Py::Not } + + ExprNode getOperand() { result.asExpr() = notExpr.getOperand() } + } + + /** + * A boolean expression (`and`/`or`) with exactly 2 operands. + * For 2-operand BoolExprs, the `TExprNode` itself serves as the + * logical and/or expression. + */ + class BoolExpr2Node extends ExprNode { + private Py::BoolExpr boolExpr; + + BoolExpr2Node() { boolExpr = this.asExpr() and count(boolExpr.getAValue()) = 2 } + + predicate isAnd() { boolExpr.getOp() instanceof Py::And } + + predicate isOr() { boolExpr.getOp() instanceof Py::Or } + + ExprNode getLeftOperand() { result.asExpr() = boolExpr.getValue(0) } + + ExprNode getRightOperand() { result.asExpr() = boolExpr.getValue(1) } + } + + /** + * The outermost pair of a multi-operand (3+) boolean expression. + * Represented by the original `BoolExpr` node (`TExprNode`). + * Left operand is `getValue(0)`, right operand is `TBoolExprPair(be, 1)`. + */ + class BoolExprOuterNode extends ExprNode { + private Py::BoolExpr boolExpr; + + BoolExprOuterNode() { boolExpr = this.asExpr() and count(boolExpr.getAValue()) > 2 } + + predicate isAnd() { boolExpr.getOp() instanceof Py::And } + + predicate isOr() { boolExpr.getOp() instanceof Py::Or } + + Node getLeftOperand() { result = TExprNode(boolExpr.getValue(0)) } + + Node getRightOperand() { result = TBoolExprPair(boolExpr, 1) } + } + + /** + * A synthetic intermediate node in a multi-operand boolean expression. + * Pair at index `i` has left=`getValue(i)` and right=pair at `i+1` + * (or `getValue(n-1)` for the last pair). + */ + class BoolExprPairNode extends Node, TBoolExprPair { + private Py::BoolExpr boolExpr; + private int index; + + BoolExprPairNode() { this = TBoolExprPair(boolExpr, index) } + + override string toString() { result = boolExpr.getOperator() } + + override Py::Location getLocation() { result = boolExpr.getValue(index).getLocation() } + + override ScopeNode getEnclosingScope() { + result.asScope() = boolExpr.getValue(index).getScope() + } + + predicate isAnd() { boolExpr.getOp() instanceof Py::And } + + predicate isOr() { boolExpr.getOp() instanceof Py::Or } + + Node getLeftOperand() { result = TExprNode(boolExpr.getValue(index)) } + + Node getRightOperand() { + // Last pair: right operand is the final value + index = count(boolExpr.getAValue()) - 2 and + result = TExprNode(boolExpr.getValue(index + 1)) + or + // Not last pair: right operand is the next synthetic pair + index < count(boolExpr.getAValue()) - 2 and + result = TBoolExprPair(boolExpr, index + 1) + } + } + + /** A `True` or `False` literal. */ + class BoolLiteralNode extends ExprNode { + BoolLiteralNode() { this.asExpr() instanceof Py::True or this.asExpr() instanceof Py::False } + + boolean getBoolValue() { + this.asExpr() instanceof Py::True and result = true + or + this.asExpr() instanceof Py::False and result = false + } + } } /** Provides an implementation of the AST signature for Python. */ @@ -143,6 +373,7 @@ module AstSigImpl implements AstSig { /** Gets the child of `n` at the specified (zero-based) index. */ AstNode getChild(AstNode n, int index) { + // IfStmt: condition (0), then branch (1), else branch (2) exists(Ast::IfNode ifNode | ifNode = n | index = 0 and result = ifNode.getTest() or @@ -151,9 +382,101 @@ module AstSigImpl implements AstSig { index = 2 and result = ifNode.getOrelse() ) or + // BlockStmt (StmtList): indexed statements result = n.(Ast::StmtListNode).getItem(index) or + // ExprStmt: the expression (0) index = 0 and result = n.(Ast::ExprStmtNode).getValue() + or + // WhileStmt: condition (0), body (1) + // Note: Python while/else is not directly supported by the shared library. + exists(Ast::WhileNode w | w = n | + index = 0 and result = w.getTest() + or + index = 1 and result = w.getBody() + ) + or + // ForStmt (mapped as ForeachStmt): collection (0), variable (1), body (2) + exists(Ast::ForNode f | f = n | + index = 0 and result = f.getIter() + or + index = 1 and result = f.getTarget() + or + index = 2 and result = f.getBody() + ) + or + // ReturnStmt: the value (0) + index = 0 and result = n.(Ast::ReturnNode).getValue() + or + // ThrowStmt (raise): the exception (0), the cause (1) + exists(Ast::RaiseNode r | r = n | + index = 0 and result = r.getException() + or + index = 1 and result = r.getCause() + ) + or + // TryStmt: body (0), handlers (1..n), finally (-1) + exists(Ast::TryNode t | t = n | + index = 0 and result = t.getBody() + or + result = t.getHandler(index - 1) and index >= 1 + ) + or + // CatchClause (except handler): type (0), name (1), body (2) + exists(Ast::ExceptionHandlerNode h | h = n | + index = 0 and result = h.getType() + or + index = 1 and result = h.getName() + or + index = 2 and result = h.getBody() + ) + or + // ConditionalExpr (IfExp): condition (0), then (1), else (2) + exists(Ast::IfExpNode ie | ie = n | + index = 0 and result = ie.getTest() + or + index = 1 and result = ie.getBody() + or + index = 2 and result = ie.getOrelse() + ) + or + // Python BinaryExpr (arithmetic, bitwise, matmul, etc.): left (0), right (1) + exists(Ast::BinaryExprNode be | be = n | + index = 0 and result = be.getLeft() + or + index = 1 and result = be.getRight() + ) + or + // Subscript (obj[index]): object (0), index (1) + exists(Ast::SubscriptNode sub | sub = n | + index = 0 and result = sub.getObject() + or + index = 1 and result = sub.getIndex() + ) + or + // LogicalNotExpr: operand (0) + index = 0 and result = n.(Ast::NotExprNode).getOperand() + or + // 2-operand BoolExpr: left (0), right (1) + exists(Ast::BoolExpr2Node be | be = n | + index = 0 and result = be.getLeftOperand() + or + index = 1 and result = be.getRightOperand() + ) + or + // Multi-operand BoolExpr (outermost): left (0), right (1) + exists(Ast::BoolExprOuterNode be | be = n | + index = 0 and result = be.getLeftOperand() + or + index = 1 and result = be.getRightOperand() + ) + or + // Synthetic BoolExpr pair: left (0), right (1) + exists(Ast::BoolExprPairNode bp | bp = n | + index = 0 and result = bp.getLeftOperand() + or + index = 1 and result = bp.getRightOperand() + ) } Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingScope() } @@ -173,8 +496,10 @@ module AstSigImpl implements AstSig { Stmt() { this instanceof Ast::StmtNode or this instanceof Ast::StmtListNode } } - /** An expression. */ - class Expr extends Ast::ExprNode { } + /** An expression. Includes `TExprNode` and synthetic `TBoolExprPair` nodes. */ + class Expr extends AstNode { + Expr() { this instanceof Ast::ExprNode or this instanceof Ast::BoolExprPairNode } + } /** A block of statements, wrapping Python's `StmtList`. */ class BlockStmt extends Stmt, Ast::StmtListNode { @@ -210,113 +535,107 @@ module AstSigImpl implements AstSig { Stmt getElse() { result = this.getOrelse() } } - // ===== Stub types for constructs not yet implemented ===== - /** A loop statement. Not yet implemented for Python. */ + // ===== Loop statements ===== + /** A loop statement. */ class LoopStmt extends Stmt { - LoopStmt() { none() } + LoopStmt() { this instanceof Ast::WhileNode or this instanceof Ast::ForNode } /** Gets the body of this loop statement. */ Stmt getBody() { none() } } - /** A `while` loop statement. Not yet implemented for Python. */ - class WhileStmt extends LoopStmt { + /** A `while` loop statement. */ + class WhileStmt extends LoopStmt instanceof Ast::WhileNode { /** Gets the boolean condition of this `while` loop. */ - Expr getCondition() { none() } + Expr getCondition() { result = this.(Ast::WhileNode).getTest() } + + override Stmt getBody() { result = this.(Ast::WhileNode).getBody() } } /** A `do-while` loop statement. Python has no do-while construct. */ class DoStmt extends LoopStmt { - /** Gets the boolean condition of this `do-while` loop. */ + DoStmt() { none() } + Expr getCondition() { none() } } /** A C-style `for` loop. Python has no C-style for loop. */ class ForStmt extends LoopStmt { - /** Gets the initializer expression at the specified position. */ + ForStmt() { none() } + Expr getInit(int index) { none() } - /** Gets the boolean condition of this `for` loop. */ Expr getCondition() { none() } - /** Gets the update expression at the specified position. */ Expr getUpdate(int index) { none() } } - /** A for-each loop. Not yet implemented for Python. */ + /** A for-each loop (`for x in iterable:`). */ class ForeachStmt extends LoopStmt { + ForeachStmt() { this instanceof Ast::ForNode } + /** Gets the loop variable. */ - Expr getVariable() { none() } + Expr getVariable() { result = this.(Ast::ForNode).getTarget() } /** Gets the collection being iterated. */ - Expr getCollection() { none() } - } + Expr getCollection() { result = this.(Ast::ForNode).getIter() } - /** A `break` statement. Not yet implemented for Python. */ - class BreakStmt extends Stmt { - BreakStmt() { none() } + override Stmt getBody() { result = this.(Ast::ForNode).getBody() } } - /** A `continue` statement. Not yet implemented for Python. */ - class ContinueStmt extends Stmt { - ContinueStmt() { none() } - } + // ===== Abrupt completion statements ===== + /** A `break` statement. */ + class BreakStmt extends Stmt, Ast::BreakNode { } - /** A `return` statement. Not yet implemented for Python. */ - class ReturnStmt extends Stmt { - ReturnStmt() { none() } + /** A `continue` statement. */ + class ContinueStmt extends Stmt, Ast::ContinueNode { } + /** A `return` statement. */ + class ReturnStmt extends Stmt, Ast::ReturnNode { /** Gets the expression being returned, if any. */ - Expr getExpr() { none() } + Expr getExpr() { result = this.getValue() } } - /** A `throw`/`raise` statement. Not yet implemented for Python. */ - class ThrowStmt extends Stmt { - ThrowStmt() { none() } - - /** Gets the expression being thrown. */ - Expr getExpr() { none() } + /** A `raise` statement (mapped to `ThrowStmt`). */ + class ThrowStmt extends Stmt, Ast::RaiseNode { + /** Gets the expression being raised. */ + Expr getExpr() { result = this.getException() } } - /** A `try` statement. Not yet implemented for Python. */ + // ===== Try/except ===== + /** A `try` statement. */ class TryStmt extends Stmt { - TryStmt() { none() } + TryStmt() { this instanceof Ast::TryNode } - /** Gets the body of this `try` statement. */ - Stmt getBody() { none() } + Stmt getBody() { result = this.(Ast::TryNode).getBody() } - /** Gets the `catch` clause at the specified position. */ - CatchClause getCatch(int index) { none() } + CatchClause getCatch(int index) { result = this.(Ast::TryNode).getHandler(index) } - /** Gets the `finally` block of this `try` statement, if any. */ - Stmt getFinally() { none() } + Stmt getFinally() { result = this.(Ast::TryNode).getFinalbody() } } - /** A catch clause. Not yet implemented for Python. */ - class CatchClause extends AstNode { - CatchClause() { none() } + AstNode getTryElse(TryStmt try) { result = try.(Ast::TryNode).getOrelse() } - /** Gets the variable declared by this catch clause. */ - AstNode getVariable() { none() } + /** An except clause in a try statement. */ + class CatchClause extends Stmt { + CatchClause() { this instanceof Ast::ExceptionHandlerNode } + + AstNode getVariable() { result = this.(Ast::ExceptionHandlerNode).getName() } - /** Gets the guard condition, if any. */ Expr getCondition() { none() } - /** Gets the body of this catch clause. */ - Stmt getBody() { none() } + Stmt getBody() { result = this.(Ast::ExceptionHandlerNode).getBody() } } + // ===== Switch/match — stubs for now ===== /** A switch/match statement. Not yet implemented for Python. */ class Switch extends AstNode { Switch() { none() } - /** Gets the expression being switched on. */ Expr getExpr() { none() } - /** Gets the case at the specified position. */ Case getCase(int index) { none() } - /** Gets the statement at the specified position. */ Stmt getStmt(int index) { none() } } @@ -324,70 +643,96 @@ module AstSigImpl implements AstSig { class Case extends AstNode { Case() { none() } - /** Gets a pattern being matched. */ AstNode getAPattern() { none() } - /** Gets the guard expression, if any. */ Expr getGuard() { none() } - /** Gets the body of this case. */ AstNode getBody() { none() } } /** A default case. Not yet implemented for Python. */ class DefaultCase extends Case { } - /** A ternary conditional expression. Not yet implemented for Python. */ - class ConditionalExpr extends Expr { - ConditionalExpr() { none() } - + // ===== Expression types ===== + /** A conditional expression (`x if cond else y`). */ + class ConditionalExpr extends Expr, Ast::IfExpNode { /** Gets the condition of this expression. */ - Expr getCondition() { none() } + Expr getCondition() { result = this.getTest() } /** Gets the true branch of this expression. */ - Expr getThen() { none() } + Expr getThen() { result = Ast::IfExpNode.super.getBody() } /** Gets the false branch of this expression. */ - Expr getElse() { none() } + Expr getElse() { result = this.getOrelse() } } - /** A binary expression. Not yet implemented for Python. */ + /** + * A binary expression for the shared CFG. In Python, this covers + * `and`/`or` expressions (both real 2-operand and synthetic pairs). + */ class BinaryExpr extends Expr { - BinaryExpr() { none() } + BinaryExpr() { + this instanceof Ast::BoolExpr2Node or + this instanceof Ast::BoolExprOuterNode or + this instanceof Ast::BoolExprPairNode + } /** Gets the left operand. */ - Expr getLeftOperand() { none() } + Expr getLeftOperand() { + result = this.(Ast::BoolExpr2Node).getLeftOperand() + or + result = this.(Ast::BoolExprOuterNode).getLeftOperand() + or + result = this.(Ast::BoolExprPairNode).getLeftOperand() + } /** Gets the right operand. */ - Expr getRightOperand() { none() } + Expr getRightOperand() { + result = this.(Ast::BoolExpr2Node).getRightOperand() + or + result = this.(Ast::BoolExprOuterNode).getRightOperand() + or + result = this.(Ast::BoolExprPairNode).getRightOperand() + } } - /** A short-circuiting logical AND expression. Not yet implemented for Python. */ - class LogicalAndExpr extends BinaryExpr { } + /** A short-circuiting logical `and` expression. */ + class LogicalAndExpr extends BinaryExpr { + LogicalAndExpr() { + this.(Ast::BoolExpr2Node).isAnd() or + this.(Ast::BoolExprOuterNode).isAnd() or + this.(Ast::BoolExprPairNode).isAnd() + } + } - /** A short-circuiting logical OR expression. Not yet implemented for Python. */ - class LogicalOrExpr extends BinaryExpr { } + /** A short-circuiting logical `or` expression. */ + class LogicalOrExpr extends BinaryExpr { + LogicalOrExpr() { + this.(Ast::BoolExpr2Node).isOr() or + this.(Ast::BoolExprOuterNode).isOr() or + this.(Ast::BoolExprPairNode).isOr() + } + } /** A null-coalescing expression. Python has no null-coalescing operator. */ - class NullCoalescingExpr extends BinaryExpr { } + class NullCoalescingExpr extends BinaryExpr { + NullCoalescingExpr() { none() } + } - /** A unary expression. Not yet implemented for Python. */ + /** A unary expression. Exists for the `not` subclass. */ class UnaryExpr extends Expr { - UnaryExpr() { none() } + UnaryExpr() { this instanceof Ast::NotExprNode } - /** Gets the operand. */ - Expr getOperand() { none() } + Expr getOperand() { result = this.(Ast::NotExprNode).getOperand() } } - /** A logical NOT expression. Not yet implemented for Python. */ + /** A logical `not` expression. */ class LogicalNotExpr extends UnaryExpr { } - /** A boolean literal expression. Not yet implemented for Python. */ - class BooleanLiteral extends Expr { - BooleanLiteral() { none() } - + /** A boolean literal expression (`True` or `False`). */ + class BooleanLiteral extends Expr, Ast::BoolLiteralNode { /** Gets the boolean value of this literal. */ - boolean getValue() { none() } + boolean getValue() { result = this.getBoolValue() } } } @@ -427,3 +772,15 @@ private module Input implements InputSig1, InputSig2 { import CfgCachedStage import Public + +/** + * Maps a new-CFG AST wrapper node to the corresponding Python AST node, if any. + * Entry, exit, and synthetic nodes have no corresponding Python AST node. + */ +Py::AstNode astNodeToPyNode(AstSigImpl::AstNode n) { + result = n.(Ast::ExprNode).asExpr() + or + result = n.(Ast::StmtNode).asStmt() + or + result = n.(Ast::ScopeNode).asScope() +} From 75a3168c097a3d1d743c6f2e01661155122c0a90 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 13:55:57 +0000 Subject: [PATCH 12/72] Python: Ignore synthetic CFG nodes We can only annotate the ones that correspond directly to AST nodes anyway. Co-authored-by: yoff --- .../evaluation-order/NewCfgImpl.qll | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll index 8549ca1b2060..cb968c6fb603 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll @@ -14,6 +14,10 @@ private class NewBasicBlock = CfgImpl::BasicBlock; /** New (shared) CFG implementation of the evaluation-order signature. */ module NewCfg implements EvalOrderCfgSig { class CfgNode instanceof NewControlFlowNode { + // Only include the unique representative node for each AST node, + // filtering out synthetic before/after/entry/exit/additional nodes. + CfgNode() { NewControlFlowNode.super.injects(_) } + string toString() { result = NewControlFlowNode.super.toString() } Py::Location getLocation() { result = NewControlFlowNode.super.getLocation() } @@ -22,17 +26,42 @@ module NewCfg implements EvalOrderCfgSig { result = CfgImpl::astNodeToPyNode(NewControlFlowNode.super.getAstNode()) } - CfgNode getASuccessor() { result = NewControlFlowNode.super.getASuccessor() } + CfgNode getASuccessor() { nextCfgNode(this, result) } CfgNode getAnExceptionalSuccessor() { - result = NewControlFlowNode.super.getAnExceptionSuccessor() + exists(NewControlFlowNode mid | + mid = NewControlFlowNode.super.getAnExceptionSuccessor() and + nextCfgNodeFrom(mid, result) + ) } Py::Scope getScope() { result = NewControlFlowNode.super.getEnclosingCallable().asScope() } - BasicBlock getBasicBlock() { result = NewControlFlowNode.super.getBasicBlock() } + BasicBlock getBasicBlock() { + exists(NewBasicBlock bb, int i | bb.getNode(i) = this and result = bb) + } } + /** + * Holds if `next` is the nearest CfgNode reachable from `n` via + * one or more raw CFG successor edges, skipping non-CfgNode intermediaries. + */ + private predicate nextCfgNodeFrom(NewControlFlowNode n, CfgNode next) { + next = n.getASuccessor() + or + exists(NewControlFlowNode mid | + mid = n.getASuccessor() and + not mid instanceof CfgNode and + nextCfgNodeFrom(mid, next) + ) + } + + /** + * Holds if `next` is the nearest CfgNode successor of `n`, + * skipping synthetic intermediate nodes. + */ + private predicate nextCfgNode(CfgNode n, CfgNode next) { nextCfgNodeFrom(n, next) } + class BasicBlock instanceof NewBasicBlock { string toString() { result = NewBasicBlock.super.toString() } @@ -46,7 +75,9 @@ module NewCfg implements EvalOrderCfgSig { } CfgNode scopeGetEntryNode(Py::Scope s) { - result instanceof CfgImpl::ControlFlow::EntryNode and - result.getScope() = s + exists(CfgImpl::ControlFlow::EntryNode entry | + entry.getEnclosingCallable().asScope() = s and + nextCfgNodeFrom(entry, result) + ) } } From 2b3df57eea655982914b12c914938dea300e37a3 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 14:28:04 +0000 Subject: [PATCH 13/72] Python: Support various literals Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 47df5c0f619a..2d68b2cb69b3 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -270,6 +270,59 @@ private module Ast { ExprNode getIndex() { result.asExpr() = sub.getIndex() } } + /** A tuple literal. */ + class TupleNode extends ExprNode { + private Py::Tuple tuple; + + TupleNode() { tuple = this.asExpr() } + + ExprNode getElt(int n) { result.asExpr() = tuple.getElt(n) } + } + + /** A list literal. */ + class ListNode extends ExprNode { + private Py::List list; + + ListNode() { list = this.asExpr() } + + ExprNode getElt(int n) { result.asExpr() = list.getElt(n) } + } + + /** A set literal. */ + class SetNode extends ExprNode { + private Py::Set set; + + SetNode() { set = this.asExpr() } + + ExprNode getElt(int n) { result.asExpr() = set.getElt(n) } + } + + /** A dict literal. */ + class DictNode extends ExprNode { + private Py::Dict dict; + + DictNode() { dict = this.asExpr() } + + /** + * Gets the key of the `n`th item (at child index `2*n`), and the + * value at child index `2*n + 1`. + */ + ExprNode getKey(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getKey() } + + ExprNode getValue(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getValue() } + + int getNumberOfItems() { result = count(dict.getAnItem()) } + } + + /** A unary expression other than `not` (e.g., `-x`, `+x`, `~x`). */ + class ArithmeticUnaryNode extends ExprNode { + private Py::UnaryExpr unaryExpr; + + ArithmeticUnaryNode() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } + + ExprNode getOperand() { result.asExpr() = unaryExpr.getOperand() } + } + /** * A `not` expression. This is a `UnaryExpr` whose operator is `Not`. */ @@ -454,6 +507,23 @@ module AstSigImpl implements AstSig { index = 1 and result = sub.getIndex() ) or + // Tuple, List, Set: elements left to right + result = n.(Ast::TupleNode).getElt(index) + or + result = n.(Ast::ListNode).getElt(index) + or + result = n.(Ast::SetNode).getElt(index) + or + // Dict: key(0), value(0), key(1), value(1), ... + exists(Ast::DictNode d, int item | d = n | + index = 2 * item and result = d.getKey(item) + or + index = 2 * item + 1 and result = d.getValue(item) + ) + or + // Arithmetic unary (-x, +x, ~x): operand (0) + index = 0 and result = n.(Ast::ArithmeticUnaryNode).getOperand() + or // LogicalNotExpr: operand (0) index = 0 and result = n.(Ast::NotExprNode).getOperand() or From 56804771797fca06f81b24b3aa11626b16c18bf8 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 14:40:24 +0000 Subject: [PATCH 14/72] Python: Assert statements Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 2d68b2cb69b3..a68a01f50a92 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -10,6 +10,7 @@ private import python as Py private import codeql.controlflow.ControlFlowGraph +private import codeql.controlflow.SuccessorType private module Ast { /** The newtype representing AST nodes for the shared CFG library. */ @@ -204,6 +205,17 @@ private module Ast { ContinueNode() { this.asStmt() instanceof Py::Continue } } + /** An `assert` statement. */ + class AssertNode extends StmtNode { + private Py::Assert assertStmt; + + AssertNode() { assertStmt = this.asStmt() } + + ExprNode getTest() { result.asExpr() = assertStmt.getTest() } + + ExprNode getMsg() { result.asExpr() = assertStmt.getMsg() } + } + /** A `try` statement. */ class TryNode extends StmtNode { private Py::Try tryStmt; @@ -461,6 +473,13 @@ module AstSigImpl implements AstSig { // ReturnStmt: the value (0) index = 0 and result = n.(Ast::ReturnNode).getValue() or + // Assert: test (0), message (1) + exists(Ast::AssertNode a | a = n | + index = 0 and result = a.getTest() + or + index = 1 and result = a.getMsg() + ) + or // ThrowStmt (raise): the exception (0), the cause (1) exists(Ast::RaiseNode r | r = n | index = 0 and result = r.getException() @@ -827,17 +846,50 @@ private module Input implements InputSig1, InputSig2 { string toString() { result = "label" } } + predicate inConditionalContext(AstSigImpl::AstNode n, ConditionKind kind) { + kind.isBoolean() and + n = any(Ast::AssertNode a).getTest() + } + + private string assertThrowTag() { result = "[assert-throw]" } + + predicate additionalNode(AstSigImpl::AstNode n, string tag, NormalSuccessor t) { + n instanceof Ast::AssertNode and tag = assertThrowTag() and t instanceof DirectSuccessor + } + predicate beginAbruptCompletion( AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c, boolean always ) { - none() + ast instanceof Ast::AssertNode and + n.isAdditional(ast, assertThrowTag()) and + c.asSimpleAbruptCompletion() instanceof ExceptionSuccessor and + always = true } predicate endAbruptCompletion(AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c) { none() } - predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { none() } + predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { + exists(Ast::AssertNode assertStmt | + n1.isBefore(assertStmt) and + n2.isBefore(assertStmt.getTest()) + or + n1.isAfterTrue(assertStmt.getTest()) and + n2.isAfter(assertStmt) + or + n1.isAfterFalse(assertStmt.getTest()) and + ( + n2.isBefore(assertStmt.getMsg()) + or + not exists(assertStmt.getMsg()) and + n2.isAdditional(assertStmt, assertThrowTag()) + ) + or + n1.isAfter(assertStmt.getMsg()) and + n2.isAdditional(assertStmt, assertThrowTag()) + ) + } } import CfgCachedStage From da663da87b0d085830021219ac3e3744bd5f726e Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 14:45:21 +0000 Subject: [PATCH 15/72] Python: Function calls Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index a68a01f50a92..09f815297243 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -271,6 +271,25 @@ private module Ast { ExprNode getRight() { result.asExpr() = binExpr.getRight() } } + /** A call expression (`func(args...)`). */ + class CallNode extends ExprNode { + private Py::Call call; + + CallNode() { call = this.asExpr() } + + ExprNode getFunc() { result.asExpr() = call.getFunc() } + + ExprNode getPositionalArg(int n) { result.asExpr() = call.getPositionalArg(n) } + + int getNumberOfPositionalArgs() { result = count(call.getAPositionalArg()) } + + ExprNode getKeywordValue(int n) { + result.asExpr() = call.getNamedArg(n).(Py::Keyword).getValue() + } + + int getNumberOfNamedArgs() { result = count(call.getANamedArg()) } + } + /** A subscript expression (`obj[index]`). */ class SubscriptNode extends ExprNode { private Py::Subscript sub; @@ -512,6 +531,16 @@ module AstSigImpl implements AstSig { index = 2 and result = ie.getOrelse() ) or + // Call: func (0), positional args (1..n), keyword values (n+1..n+k) + exists(Ast::CallNode call | call = n | + index = 0 and result = call.getFunc() + or + result = call.getPositionalArg(index - 1) and index >= 1 + or + result = call.getKeywordValue(index - 1 - call.getNumberOfPositionalArgs()) and + index >= 1 + call.getNumberOfPositionalArgs() + ) + or // Python BinaryExpr (arithmetic, bitwise, matmul, etc.): left (0), right (1) exists(Ast::BinaryExprNode be | be = n | index = 0 and result = be.getLeft() From 319e49b95538f18b79730e4e282c65b0cd4259e4 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 14:49:13 +0000 Subject: [PATCH 16/72] Python: Attributes Co-authored-by: yoff --- .../python/controlflow/internal/AstNodeImpl.qll | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 09f815297243..8eab5bbb5057 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -301,6 +301,15 @@ private module Ast { ExprNode getIndex() { result.asExpr() = sub.getIndex() } } + /** An attribute access (`obj.name`). */ + class AttributeNode extends ExprNode { + private Py::Attribute attr; + + AttributeNode() { attr = this.asExpr() } + + ExprNode getObject() { result.asExpr() = attr.getObject() } + } + /** A tuple literal. */ class TupleNode extends ExprNode { private Py::Tuple tuple; @@ -555,6 +564,9 @@ module AstSigImpl implements AstSig { index = 1 and result = sub.getIndex() ) or + // Attribute (obj.name): object (0) + index = 0 and result = n.(Ast::AttributeNode).getObject() + or // Tuple, List, Set: elements left to right result = n.(Ast::TupleNode).getElt(index) or From fc3940fb5dfb9045e9c20088171d800e47c20021 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 14:53:26 +0000 Subject: [PATCH 17/72] Python: assignments Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 8eab5bbb5057..9671d6c31bbd 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -147,6 +147,39 @@ private module Ast { ExprNode getValue() { result.asExpr() = exprStmt.getValue() } } + /** An assignment statement (`x = y = expr`). */ + class AssignNode extends StmtNode { + private Py::Assign assign; + + AssignNode() { assign = this.asStmt() } + + ExprNode getValue() { result.asExpr() = assign.getValue() } + + ExprNode getTarget(int n) { result.asExpr() = assign.getTarget(n) } + + int getNumberOfTargets() { result = count(assign.getATarget()) } + } + + /** An augmented assignment statement (`x += expr`). */ + class AugAssignNode extends StmtNode { + private Py::AugAssign augAssign; + + AugAssignNode() { augAssign = this.asStmt() } + + ExprNode getOperation() { result.asExpr() = augAssign.getOperation() } + } + + /** An assignment expression / walrus operator (`x := expr`). */ + class AssignExprNode extends ExprNode { + private Py::AssignExpr assignExpr; + + AssignExprNode() { assignExpr = this.asExpr() } + + ExprNode getValue() { result.asExpr() = assignExpr.getValue() } + + ExprNode getTarget() { result.asExpr() = assignExpr.getTarget() } + } + /** A `while` statement. */ class WhileNode extends StmtNode { private Py::While whileStmt; @@ -481,6 +514,23 @@ module AstSigImpl implements AstSig { // ExprStmt: the expression (0) index = 0 and result = n.(Ast::ExprStmtNode).getValue() or + // Assign: value (0), targets (1..n) + exists(Ast::AssignNode a | a = n | + index = 0 and result = a.getValue() + or + result = a.getTarget(index - 1) and index >= 1 + ) + or + // AugAssign: the operation (0) + index = 0 and result = n.(Ast::AugAssignNode).getOperation() + or + // AssignExpr (walrus :=): value (0), target (1) + exists(Ast::AssignExprNode ae | ae = n | + index = 0 and result = ae.getValue() + or + index = 1 and result = ae.getTarget() + ) + or // WhileStmt: condition (0), body (1) // Note: Python while/else is not directly supported by the shared library. exists(Ast::WhileNode w | w = n | From 6573eed42b2f5427f8749ad08df667185bec5485 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:02:11 +0000 Subject: [PATCH 18/72] Python: More simple statements Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 9671d6c31bbd..26f778130ccb 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -249,6 +249,15 @@ private module Ast { ExprNode getMsg() { result.asExpr() = assertStmt.getMsg() } } + /** A `delete` statement. */ + class DeleteNode extends StmtNode { + private Py::Delete del; + + DeleteNode() { del = this.asStmt() } + + ExprNode getTarget(int n) { result.asExpr() = del.getTarget(n) } + } + /** A `try` statement. */ class TryNode extends StmtNode { private Py::Try tryStmt; @@ -396,6 +405,86 @@ private module Ast { ExprNode getOperand() { result.asExpr() = unaryExpr.getOperand() } } + /** A comparison expression (`a < b`, `a < b < c`, etc.). */ + class CompareNode extends ExprNode { + private Py::Compare cmp; + + CompareNode() { cmp = this.asExpr() } + + ExprNode getLeft() { result.asExpr() = cmp.getLeft() } + + ExprNode getComparator(int n) { result.asExpr() = cmp.getComparator(n) } + } + + /** A slice expression (`start:stop:step`). */ + class SliceNode extends ExprNode { + private Py::Slice slice; + + SliceNode() { slice = this.asExpr() } + + ExprNode getStart() { result.asExpr() = slice.getStart() } + + ExprNode getStop() { result.asExpr() = slice.getStop() } + + ExprNode getStep() { result.asExpr() = slice.getStep() } + } + + /** A starred expression (`*x`). */ + class StarredNode extends ExprNode { + private Py::Starred starred; + + StarredNode() { starred = this.asExpr() } + + ExprNode getValue() { result.asExpr() = starred.getValue() } + } + + /** A formatted string literal (`f"...{expr}..."`). */ + class FstringNode extends ExprNode { + private Py::Fstring fstring; + + FstringNode() { fstring = this.asExpr() } + + ExprNode getValue(int n) { result.asExpr() = fstring.getValue(n) } + } + + /** A formatted value inside an f-string (`{expr}` or `{expr:spec}`). */ + class FormattedValueNode extends ExprNode { + private Py::FormattedValue fv; + + FormattedValueNode() { fv = this.asExpr() } + + ExprNode getValue() { result.asExpr() = fv.getValue() } + + ExprNode getFormatSpec() { result.asExpr() = fv.getFormatSpec() } + } + + /** A `yield` expression. */ + class YieldNode extends ExprNode { + private Py::Yield yield; + + YieldNode() { yield = this.asExpr() } + + ExprNode getValue() { result.asExpr() = yield.getValue() } + } + + /** A `yield from` expression. */ + class YieldFromNode extends ExprNode { + private Py::YieldFrom yieldFrom; + + YieldFromNode() { yieldFrom = this.asExpr() } + + ExprNode getValue() { result.asExpr() = yieldFrom.getValue() } + } + + /** An `await` expression. */ + class AwaitNode extends ExprNode { + private Py::Await await; + + AwaitNode() { await = this.asExpr() } + + ExprNode getValue() { result.asExpr() = await.getValue() } + } + /** * A `not` expression. This is a `UnaryExpr` whose operator is `Not`. */ @@ -558,6 +647,9 @@ module AstSigImpl implements AstSig { index = 1 and result = a.getMsg() ) or + // Delete: targets left to right + result = n.(Ast::DeleteNode).getTarget(index) + or // ThrowStmt (raise): the exception (0), the cause (1) exists(Ast::RaiseNode r | r = n | index = 0 and result = r.getException() @@ -634,6 +726,44 @@ module AstSigImpl implements AstSig { // Arithmetic unary (-x, +x, ~x): operand (0) index = 0 and result = n.(Ast::ArithmeticUnaryNode).getOperand() or + // Compare (a < b < c): left (0), comparators (1..n) + exists(Ast::CompareNode cmp | cmp = n | + index = 0 and result = cmp.getLeft() + or + result = cmp.getComparator(index - 1) and index >= 1 + ) + or + // Slice (start:stop:step): start (0), stop (1), step (2) + exists(Ast::SliceNode sl | sl = n | + index = 0 and result = sl.getStart() + or + index = 1 and result = sl.getStop() + or + index = 2 and result = sl.getStep() + ) + or + // Starred (*x): value (0) + index = 0 and result = n.(Ast::StarredNode).getValue() + or + // Fstring: values left to right + result = n.(Ast::FstringNode).getValue(index) + or + // FormattedValue ({expr} or {expr:spec}): value (0), format spec (1) + exists(Ast::FormattedValueNode fv | fv = n | + index = 0 and result = fv.getValue() + or + index = 1 and result = fv.getFormatSpec() + ) + or + // Yield: value (0) + index = 0 and result = n.(Ast::YieldNode).getValue() + or + // YieldFrom: value (0) + index = 0 and result = n.(Ast::YieldFromNode).getValue() + or + // Await: value (0) + index = 0 and result = n.(Ast::AwaitNode).getValue() + or // LogicalNotExpr: operand (0) index = 0 and result = n.(Ast::NotExprNode).getOperand() or From abd7c2989dafd4589edb6559db229dfe473b1548 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:05:48 +0000 Subject: [PATCH 19/72] Python: Add `with` Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 26f778130ccb..18bdeca4e317 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -228,6 +228,19 @@ private module Ast { ExprNode getCause() { result.asExpr() = raise.getCause() } } + /** A `with` statement. */ + class WithNode extends StmtNode { + private Py::With withStmt; + + WithNode() { withStmt = this.asStmt() } + + ExprNode getContextExpr() { result.asExpr() = withStmt.getContextExpr() } + + ExprNode getOptionalVars() { result.asExpr() = withStmt.getOptionalVars() } + + StmtListNode getBody() { result.asStmtList() = withStmt.getBody() } + } + /** A `break` statement. */ class BreakNode extends StmtNode { BreakNode() { this.asStmt() instanceof Py::Break } @@ -650,6 +663,15 @@ module AstSigImpl implements AstSig { // Delete: targets left to right result = n.(Ast::DeleteNode).getTarget(index) or + // With: context expr (0), optional vars (1), body (2) + exists(Ast::WithNode w | w = n | + index = 0 and result = w.getContextExpr() + or + index = 1 and result = w.getOptionalVars() + or + index = 2 and result = w.getBody() + ) + or // ThrowStmt (raise): the exception (0), the cause (1) exists(Ast::RaiseNode r | r = n | index = 0 and result = r.getException() From 98637bcdc727e2884cd931c7d097c8e623ea3f2d Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:09:40 +0000 Subject: [PATCH 20/72] Python: Comprehensions Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 18bdeca4e317..cd41a0e1c3c4 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -418,6 +418,27 @@ private module Ast { ExprNode getOperand() { result.asExpr() = unaryExpr.getOperand() } } + /** + * A comprehension or generator expression. + * The iterable is evaluated in the enclosing scope; the body runs in a + * nested synthetic function scope handled by its own CFG. + */ + class ComprehensionNode extends ExprNode { + private Py::Expr iterable; + + ComprehensionNode() { + iterable = this.asExpr().(Py::ListComp).getIterable() + or + iterable = this.asExpr().(Py::SetComp).getIterable() + or + iterable = this.asExpr().(Py::DictComp).getIterable() + or + iterable = this.asExpr().(Py::GeneratorExp).getIterable() + } + + ExprNode getIterable() { result.asExpr() = iterable } + } + /** A comparison expression (`a < b`, `a < b < c`, etc.). */ class CompareNode extends ExprNode { private Py::Compare cmp; @@ -731,6 +752,9 @@ module AstSigImpl implements AstSig { // Attribute (obj.name): object (0) index = 0 and result = n.(Ast::AttributeNode).getObject() or + // Comprehension/generator: iterable (0) + index = 0 and result = n.(Ast::ComprehensionNode).getIterable() + or // Tuple, List, Set: elements left to right result = n.(Ast::TupleNode).getElt(index) or From 024702e019f0d2325a9e5841b13ce5542ed881df Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:18:27 +0000 Subject: [PATCH 21/72] Python: More nodes Not entirely sure about the `else:` blocks. Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index cd41a0e1c3c4..7bf4f1514b0d 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -519,6 +519,37 @@ private module Ast { ExprNode getValue() { result.asExpr() = await.getValue() } } + /** A class definition expression (has base classes evaluated at definition time). */ + class ClassExprNode extends ExprNode { + private Py::ClassExpr classExpr; + + ClassExprNode() { classExpr = this.asExpr() } + + ExprNode getBase(int n) { result.asExpr() = classExpr.getBase(n) } + } + + /** A function definition expression (has default args evaluated at definition time). */ + class FunctionExprNode extends ExprNode { + private Py::FunctionExpr funcExpr; + + FunctionExprNode() { funcExpr = this.asExpr() } + + ExprNode getDefault(int n) { result.asExpr() = funcExpr.getArgs().getDefault(n) } + + ExprNode getKwDefault(int n) { result.asExpr() = funcExpr.getArgs().getKwDefault(n) } + } + + /** A lambda expression (has default args evaluated at definition time). */ + class LambdaNode extends ExprNode { + private Py::Lambda lambda; + + LambdaNode() { lambda = this.asExpr() } + + ExprNode getDefault(int n) { result.asExpr() = lambda.getArgs().getDefault(n) } + + ExprNode getKwDefault(int n) { result.asExpr() = lambda.getArgs().getKwDefault(n) } + } + /** * A `not` expression. This is a `UnaryExpr` whose operator is `Not`. */ @@ -810,6 +841,27 @@ module AstSigImpl implements AstSig { // Await: value (0) index = 0 and result = n.(Ast::AwaitNode).getValue() or + // ClassExpr: base classes left to right + result = n.(Ast::ClassExprNode).getBase(index) + or + // FunctionExpr: defaults left to right, then kw defaults + exists(Ast::FunctionExprNode fe | fe = n | + result = fe.getDefault(index) + or + result = + fe.getKwDefault(index - + count(Py::Expr d | d = fe.asExpr().(Py::FunctionExpr).getArgs().getADefault())) + ) + or + // Lambda: defaults left to right, then kw defaults + exists(Ast::LambdaNode lam | lam = n | + result = lam.getDefault(index) + or + result = + lam.getKwDefault(index - + count(Py::Expr d | d = lam.asExpr().(Py::Lambda).getArgs().getADefault())) + ) + or // LogicalNotExpr: operand (0) index = 0 and result = n.(Ast::NotExprNode).getOperand() or @@ -1156,6 +1208,29 @@ private module Input implements InputSig1, InputSig2 { n1.isAfter(assertStmt.getMsg()) and n2.isAdditional(assertStmt, assertThrowTag()) ) + or + // While/else: when the condition is false, flow to the else block + // (if present) before the after-while node. + exists(Ast::WhileNode w, Ast::StmtListNode orelse | orelse = w.getOrelse() | + n1.isAfterFalse(w.getTest()) and + n2.isBefore(orelse) + or + n1.isAfter(orelse) and + n2.isAfter(w) + ) + or + // For/else: when the collection is empty or the loop completes normally, + // flow through the else block before the after-for node. + exists(Ast::ForNode f, Ast::StmtListNode orelse | orelse = f.getOrelse() | + n1.isAfterValue(f.getIter(), any(EmptinessSuccessor t | t.getValue() = true)) and + n2.isBefore(orelse) + or + n1.isAfter(f.getBody()) and + n2.isBefore(orelse) + or + n1.isAfter(orelse) and + n2.isAfter(f) + ) } } From 356907990ab31850437f3926d92119edc5382ce2 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:28:11 +0000 Subject: [PATCH 22/72] Python: Support `match` Co-authored-by: yoff --- .../controlflow/internal/AstNodeImpl.qll | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 7bf4f1514b0d..ae2cd1d5a69e 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -271,6 +271,30 @@ private module Ast { ExprNode getTarget(int n) { result.asExpr() = del.getTarget(n) } } + /** A `match` statement. */ + class MatchStmtNode extends StmtNode { + private Py::MatchStmt matchStmt; + + MatchStmtNode() { matchStmt = this.asStmt() } + + ExprNode getSubject() { result.asExpr() = matchStmt.getSubject() } + + CaseNode getCase(int n) { result.asStmt() = matchStmt.getCase(n) } + } + + /** A `case` clause in a match statement. */ + class CaseNode extends StmtNode { + private Py::Case caseStmt; + + CaseNode() { caseStmt = this.asStmt() } + + ExprNode getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } + + StmtListNode getBody() { result.asStmtList() = caseStmt.getBody() } + + predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } + } + /** A `try` statement. */ class TryNode extends StmtNode { private Py::Try tryStmt; @@ -1035,31 +1059,33 @@ module AstSigImpl implements AstSig { Stmt getBody() { result = this.(Ast::ExceptionHandlerNode).getBody() } } - // ===== Switch/match — stubs for now ===== - /** A switch/match statement. Not yet implemented for Python. */ - class Switch extends AstNode { - Switch() { none() } + // ===== Switch/match ===== + /** A `match` statement, mapped to the shared CFG's `Switch`. */ + class Switch extends Stmt { + Switch() { this instanceof Ast::MatchStmtNode } - Expr getExpr() { none() } + Expr getExpr() { result = this.(Ast::MatchStmtNode).getSubject() } - Case getCase(int index) { none() } + Case getCase(int index) { result = this.(Ast::MatchStmtNode).getCase(index) } Stmt getStmt(int index) { none() } } - /** A case in a switch/match. Not yet implemented for Python. */ - class Case extends AstNode { - Case() { none() } + /** A `case` clause in a match statement. */ + class Case extends Stmt { + Case() { this instanceof Ast::CaseNode } AstNode getAPattern() { none() } - Expr getGuard() { none() } + Expr getGuard() { result = this.(Ast::CaseNode).getGuard() } - AstNode getBody() { none() } + AstNode getBody() { result = this.(Ast::CaseNode).getBody() } } - /** A default case. Not yet implemented for Python. */ - class DefaultCase extends Case { } + /** A wildcard case (`case _:`). */ + class DefaultCase extends Case { + DefaultCase() { this.(Ast::CaseNode).isWildcard() } + } // ===== Expression types ===== /** A conditional expression (`x if cond else y`). */ From 852aba880dfcf0d543c3d60018ad3d3e5661b528 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 15:36:54 +0000 Subject: [PATCH 23/72] Python: Fix match Co-authored-by: yoff --- .../python/controlflow/internal/AstNodeImpl.qll | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index ae2cd1d5a69e..d027e5184755 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -762,6 +762,20 @@ module AstSigImpl implements AstSig { result = t.getHandler(index - 1) and index >= 1 ) or + // MatchStmt: subject (0), cases (1..n) + exists(Ast::MatchStmtNode m | m = n | + index = 0 and result = m.getSubject() + or + result = m.getCase(index - 1) and index >= 1 + ) + or + // Case: guard (0), body (1) + exists(Ast::CaseNode c | c = n | + index = 0 and result = c.getGuard() + or + index = 1 and result = c.getBody() + ) + or // CatchClause (except handler): type (0), name (1), body (2) exists(Ast::ExceptionHandlerNode h | h = n | index = 0 and result = h.getType() From bac48b4914f2c47218a520dd8365b259aac242ba Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 16:00:46 +0000 Subject: [PATCH 24/72] Python: Fix exception issue Co-authored-by: yoff --- .../ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d027e5184755..c57cd6973732 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -760,6 +760,8 @@ module AstSigImpl implements AstSig { index = 0 and result = t.getBody() or result = t.getHandler(index - 1) and index >= 1 + or + index = -1 and result = t.getFinalbody() ) or // MatchStmt: subject (0), cases (1..n) From 71a547b0d32a734658b438cef591714874e40aac Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 16:03:38 +0000 Subject: [PATCH 25/72] Python: Handle dict unpacking in calls Co-authored-by: yoff --- .../ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index c57cd6973732..c5e2d010688c 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -364,6 +364,8 @@ private module Ast { ExprNode getKeywordValue(int n) { result.asExpr() = call.getNamedArg(n).(Py::Keyword).getValue() + or + result.asExpr() = call.getNamedArg(n).(Py::DictUnpacking).getValue() } int getNumberOfNamedArgs() { result = count(call.getANamedArg()) } From f5629a55835e6230346276568da27d5d474d8cfa Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 21 Apr 2026 16:19:00 +0000 Subject: [PATCH 26/72] WIP --- .../AnnotationHasCfgNode.expected | 1 + .../evaluation-order/AnnotationHasCfgNode.ql | 16 +++++++ .../NewCfgAnnotationHasCfgNode.expected | 1 + .../NewCfgAnnotationHasCfgNode.ql | 18 ++++++++ .../NewCfgConsecutiveTimestamps.expected | 1 + .../evaluation-order/NewCfgImpl.qll | 9 ++-- .../NewCfgNoBasicBlock.expected | 1 + .../evaluation-order/NewCfgNoBasicBlock.ql | 18 ++++++++ .../evaluation-order/NoBasicBlock.expected | 1 + .../evaluation-order/NoBasicBlock.ql | 16 +++++++ .../evaluation-order/TimerUtils.qll | 45 ++++++++++++++----- 11 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.expected @@ -0,0 +1 @@ + diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.ql new file mode 100644 index 000000000000..5311d118576b --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/AnnotationHasCfgNode.ql @@ -0,0 +1,16 @@ +/** + * Checks that every timer annotation has a corresponding CFG node. + */ + +import python +import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils::CfgTests + +from TimerAnnotation ann +where annotationWithoutCfgNode(ann) +select ann, "Annotation in $@ has no CFG node", ann.getTestFunction(), + ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.expected @@ -0,0 +1 @@ + diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.ql new file mode 100644 index 000000000000..4b1d82e27e67 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgAnnotationHasCfgNode.ql @@ -0,0 +1,18 @@ +/** + * New-CFG version of AnnotationHasCfgNode. + * + * Checks that every timer annotation has a corresponding CFG node. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils::CfgTests + +from TimerAnnotation ann +where annotationWithoutCfgNode(ann) +select ann, "Annotation in $@ has no CFG node", ann.getTestFunction(), + ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected index e69de29bb2d1..bce948bb58a5 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected @@ -0,0 +1 @@ +| test_if.py:51:9:51:9 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 6) | test_if.py:51:15:51:15 | IntegerLiteral | Timestamp 5 | test_if.py:43:1:43:31 | Function test_if_elif_else_first | test_if_elif_else_first | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll index cb968c6fb603..97c6a9c043fa 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll @@ -14,9 +14,12 @@ private class NewBasicBlock = CfgImpl::BasicBlock; /** New (shared) CFG implementation of the evaluation-order signature. */ module NewCfg implements EvalOrderCfgSig { class CfgNode instanceof NewControlFlowNode { - // Only include the unique representative node for each AST node, - // filtering out synthetic before/after/entry/exit/additional nodes. - CfgNode() { NewControlFlowNode.super.injects(_) } + // Use the post-order representative for each AST node: the "after" node. + // For simple leaf nodes this is the merged before/after node. For + // post-order expressions this is the TAstNode. For pre-order expressions + // (and/or/not/ternary) this uses an AfterValueNode, which places the + // expression after its operands — matching the timer test expectations. + CfgNode() { NewControlFlowNode.super.isAfter(_) } string toString() { result = NewControlFlowNode.super.toString() } diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.expected @@ -0,0 +1 @@ + diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.ql new file mode 100644 index 000000000000..e07890f72502 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNoBasicBlock.ql @@ -0,0 +1,18 @@ +/** + * New-CFG version of NoBasicBlock. + * + * Checks that every annotated CFG node belongs to a basic block. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from CfgNode n, TestFunction f +where noBasicBlock(n, f) +select n, "CFG node in $@ does not belong to any basic block", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.expected @@ -0,0 +1 @@ + diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.ql new file mode 100644 index 000000000000..5568bd2a9a4a --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBasicBlock.ql @@ -0,0 +1,16 @@ +/** + * Checks that every annotated CFG node belongs to a basic block. + */ + +import python +import TimerUtils +import OldCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from CfgNode n, TestFunction f +where noBasicBlock(n, f) +select n, "CFG node in $@ does not belong to any basic block", f, f.getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll index 7d9329155b5f..dc46f00f6f56 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll @@ -92,7 +92,7 @@ class TimerAnnotation extends TTimerAnnotation { abstract Expr getAnnotatedExpr(); /** Gets the enclosing annotation expression (the `BinaryExpr` or `Call`). */ - abstract Expr getExpr(); + abstract Expr getTimerExpr(); /** Holds if this is a dead-code annotation (`t.dead[n]`). */ predicate isDead() { this instanceof DeadTimerAnnotation } @@ -100,9 +100,9 @@ class TimerAnnotation extends TTimerAnnotation { /** Holds if this is a never-evaluated annotation (`t.never`). */ predicate isNever() { this instanceof NeverTimerAnnotation } - string toString() { result = this.getExpr().toString() } + string toString() { result = this.getAnnotatedExpr().toString() } - Location getLocation() { result = this.getExpr().getLocation() } + Location getLocation() { result = this.getAnnotatedExpr().getLocation() } } /** A matmul-based timer annotation: `expr @ t[n]`. */ @@ -119,7 +119,7 @@ class MatmulTimerAnnotation extends TMatmulAnnotation, TimerAnnotation { override Expr getAnnotatedExpr() { result = annotated } - override BinaryExpr getExpr() { result.getLeft() = annotated } + override BinaryExpr getTimerExpr() { result.getLeft() = annotated } } /** A call-based timer annotation: `t(expr, n)`. */ @@ -136,7 +136,7 @@ class CallTimerAnnotation extends TCallAnnotation, TimerAnnotation { override Expr getAnnotatedExpr() { result = annotated } - override Call getExpr() { result.getArg(0) = annotated } + override Call getTimerExpr() { result.getArg(0) = annotated } } /** A dead-code timer annotation: `expr @ t.dead[n]`. */ @@ -153,7 +153,7 @@ class DeadTimerAnnotation extends TDeadAnnotation, TimerAnnotation { override Expr getAnnotatedExpr() { result = annotated } - override BinaryExpr getExpr() { result.getLeft() = annotated } + override BinaryExpr getTimerExpr() { result.getLeft() = annotated } } /** A never-evaluated annotation: `expr @ t.never`. */ @@ -169,7 +169,7 @@ class NeverTimerAnnotation extends TNeverAnnotation, TimerAnnotation { override Expr getAnnotatedExpr() { result = annotated } - override BinaryExpr getExpr() { result.getLeft() = annotated } + override BinaryExpr getTimerExpr() { result.getLeft() = annotated } } /** @@ -240,7 +240,7 @@ module EvalOrderCfgUtils { class TimerCfgNode extends CfgNode { private TimerAnnotation annot; - TimerCfgNode() { annot.getExpr() = this.getNode() } + TimerCfgNode() { annot.getAnnotatedExpr() = this.getNode() } /** Gets a timestamp value from this annotation. */ int getATimestamp() { result = annot.getATimestamp() } @@ -322,7 +322,7 @@ module EvalOrderCfgUtils { private predicate hasNestedScopeAnnotation(TestFunction f) { exists(TimerAnnotation a | a.getTestFunction() = f and - a.getExpr().getScope() != f + a.getAnnotatedExpr().getScope() != f ) } @@ -335,7 +335,7 @@ module EvalOrderCfgUtils { not ann.isDead() and a = ann.getATimestamp() and not exists(TimerCfgNode x, TimerCfgNode y | - ann.getExpr() = x.getNode() and + ann.getAnnotatedExpr() = x.getNode() and nextTimerAnnotation(x, y) and (a + 1) = y.getATimestamp() ) and @@ -354,7 +354,7 @@ module EvalOrderCfgUtils { */ predicate neverReachable(NeverTimerAnnotation ann) { exists(CfgNode n, Scope s | - n.getNode() = ann.getExpr() and + n.getNode() = ann.getAnnotatedExpr() and s = n.getScope() and ( // Reachable via inter-block path (includes same block) @@ -417,6 +417,27 @@ module EvalOrderCfgUtils { minB = min(b.getATimestamp()) and maxA >= minB } + + /** + * Holds if CFG node `n` in test function `f` does not belong to any basic block. + */ + predicate noBasicBlock(CfgNode n, TestFunction f) { + n.getScope() = f and + not exists(n.getBasicBlock()) + } + + /** + * Holds if non-dead annotation `ann` has no corresponding CFG node. + */ + predicate annotationWithoutCfgNode(TimerAnnotation ann) { + not ann.isDead() and + not ann.isNever() and + not exists(CfgNode n | n.getNode() = ann.getAnnotatedExpr()) + } + + predicate annotationWithCfgNode(TimerAnnotation ann) { + exists(CfgNode n | n.getNode() = ann.getAnnotatedExpr()) + } } } @@ -427,7 +448,7 @@ module EvalOrderCfgUtils { predicate isTimerMechanism(Expr e, TestFunction f) { exists(TimerAnnotation a | a.getTestFunction() = f and - e = a.getExpr().getASubExpression*() + e = a.getTimerExpr().getASubExpression*() ) } From 28567870acfd671465157cfb3422c1c1c7731454 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 28 Apr 2026 14:12:13 +0000 Subject: [PATCH 27/72] WIP2 --- .../BasicBlockOrdering.expected | 14 +- .../ConsecutiveTimestamps.expected | 10 +- .../evaluation-order/NeverReachable.expected | 4 +- .../evaluation-order/NeverReachable.ql | 2 +- .../NewCfgBranchTimestamps.expected | 282 ++++++++++++++++++ .../NewCfgBranchTimestamps.ql | 23 ++ ...gConsecutivePredecessorTimestamps.expected | 1 + .../NewCfgConsecutivePredecessorTimestamps.ql | 22 ++ .../NewCfgConsecutiveTimestamps.expected | 1 - .../evaluation-order/NewCfgImpl.qll | 15 + .../evaluation-order/NewCfgNeverReachable.ql | 2 +- .../evaluation-order/NoBackwardFlow.expected | 10 + .../evaluation-order/StrictForward.expected | 10 + .../evaluation-order/TimerUtils.qll | 225 ++++++++++---- .../evaluation-order/test_assert_raise.py | 4 +- .../evaluation-order/test_basic.py | 4 +- .../evaluation-order/test_boolean.py | 16 +- .../evaluation-order/test_conditional.py | 14 +- .../ControlFlow/evaluation-order/test_if.py | 34 ++- .../evaluation-order/test_loops.py | 10 +- .../evaluation-order/test_match.py | 34 +-- .../ControlFlow/evaluation-order/test_try.py | 18 +- .../ControlFlow/evaluation-order/timer.py | 94 +++--- 23 files changed, 668 insertions(+), 181 deletions(-) create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.ql create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.expected create mode 100644 python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.ql diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected index 573094ddf734..80fa3350282f 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/BasicBlockOrdering.expected @@ -1 +1,13 @@ -| test_comprehensions.py:21:29:21:40 | ControlFlowNode for BinaryExpr | Basic block ordering: $@ appears before $@ | test_comprehensions.py:21:35:21:35 | IntegerLiteral | timestamp 9 | test_comprehensions.py:21:21:21:21 | IntegerLiteral | timestamp 8 | +| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:9:59:9:59 | IntegerLiteral | timestamp 2 | test_boolean.py:9:19:9:19 | IntegerLiteral | timestamp 0 | +| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:15:50:15:50 | IntegerLiteral | timestamp 1 | test_boolean.py:15:20:15:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:21:49:21:49 | IntegerLiteral | timestamp 1 | test_boolean.py:21:19:21:19 | IntegerLiteral | timestamp 0 | +| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:27:59:27:59 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:40:86:40:86 | IntegerLiteral | timestamp 3 | test_boolean.py:40:16:40:16 | IntegerLiteral | timestamp 0 | +| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:46:86:46:86 | IntegerLiteral | timestamp 3 | test_boolean.py:46:16:46:16 | IntegerLiteral | timestamp 0 | +| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:52:120:52:120 | IntegerLiteral | timestamp 4 | test_boolean.py:52:20:52:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:52:120:52:120 | IntegerLiteral | timestamp 4 | test_boolean.py:52:63:52:63 | IntegerLiteral | timestamp 2 | +| test_boolean.py:52:11:52:47 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:52:63:52:63 | IntegerLiteral | timestamp 2 | test_boolean.py:52:20:52:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:64:10:64:52 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:64:59:64:59 | IntegerLiteral | timestamp 6 | test_boolean.py:64:17:64:17 | IntegerLiteral | timestamp 0 | +| test_boolean.py:64:10:64:52 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:64:59:64:59 | IntegerLiteral | timestamp 6 | test_boolean.py:64:27:64:27 | IntegerLiteral | timestamp 2 | +| test_boolean.py:76:10:76:51 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:76:58:76:58 | IntegerLiteral | timestamp 6 | test_boolean.py:76:17:76:17 | IntegerLiteral | timestamp 0 | +| test_boolean.py:76:10:76:51 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:76:58:76:58 | IntegerLiteral | timestamp 6 | test_boolean.py:76:27:76:27 | IntegerLiteral | timestamp 2 | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected index e20e20c464d4..e8071c044213 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/ConsecutiveTimestamps.expected @@ -1 +1,9 @@ -| test_if.py:51:9:51:16 | BinaryExpr | $@ in $@ has no consecutive successor (expected 6) | test_if.py:51:15:51:15 | IntegerLiteral | Timestamp 5 | test_if.py:43:1:43:31 | Function test_if_elif_else_first | test_if_elif_else_first | +| test_boolean.py:9:26:9:27 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 2) | test_boolean.py:9:33:9:33 | IntegerLiteral | Timestamp 1 | test_boolean.py:7:1:7:27 | Function test_and_both_sides | test_and_both_sides | +| test_boolean.py:15:10:15:14 | False | $@ in $@ has no consecutive successor (expected 1) | test_boolean.py:15:20:15:20 | IntegerLiteral | Timestamp 0 | test_boolean.py:13:1:13:30 | Function test_and_short_circuit | test_and_short_circuit | +| test_boolean.py:21:10:21:13 | True | $@ in $@ has no consecutive successor (expected 1) | test_boolean.py:21:19:21:19 | IntegerLiteral | Timestamp 0 | test_boolean.py:19:1:19:29 | Function test_or_short_circuit | test_or_short_circuit | +| test_boolean.py:27:26:27:27 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 2) | test_boolean.py:27:33:27:33 | IntegerLiteral | Timestamp 1 | test_boolean.py:25:1:25:26 | Function test_or_both_sides | test_or_both_sides | +| test_boolean.py:40:45:40:45 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 3) | test_boolean.py:40:51:40:51 | IntegerLiteral | Timestamp 2 | test_boolean.py:38:1:38:24 | Function test_chained_and | test_chained_and | +| test_boolean.py:46:44:46:45 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 3) | test_boolean.py:46:51:46:51 | IntegerLiteral | Timestamp 2 | test_boolean.py:44:1:44:23 | Function test_chained_or | test_chained_or | +| test_boolean.py:52:11:52:47 | BoolExpr | $@ in $@ has no consecutive successor (expected 3) | test_boolean.py:52:63:52:63 | IntegerLiteral | Timestamp 2 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or | +| test_boolean.py:52:27:52:31 | False | $@ in $@ has no consecutive successor (expected 2) | test_boolean.py:52:37:52:37 | IntegerLiteral | Timestamp 1 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or | +| test_boolean.py:52:78:52:79 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 4) | test_boolean.py:52:85:52:85 | IntegerLiteral | Timestamp 3 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected index 200ebdbc6a74..874a7dfb0960 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.expected @@ -1,2 +1,2 @@ -| test_match.py:159:13:159:23 | BinaryExpr | Node annotated with t.never is reachable in $@ | test_match.py:151:1:151:42 | Function test_match_exhaustive_return_first | test_match_exhaustive_return_first | -| test_match.py:172:13:172:23 | BinaryExpr | Node annotated with t.never is reachable in $@ | test_match.py:164:1:164:45 | Function test_match_exhaustive_return_wildcard | test_match_exhaustive_return_wildcard | +| test_match.py:159:13:159:13 | IntegerLiteral | Node annotated with t.never is reachable in $@ | test_match.py:151:1:151:42 | Function test_match_exhaustive_return_first | test_match_exhaustive_return_first | +| test_match.py:172:13:172:13 | IntegerLiteral | Node annotated with t.never is reachable in $@ | test_match.py:164:1:164:45 | Function test_match_exhaustive_return_wildcard | test_match_exhaustive_return_wildcard | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql index db55c1d92e4b..b09a936a0a40 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NeverReachable.ql @@ -12,7 +12,7 @@ private module Utils = EvalOrderCfgUtils; private import Utils::CfgTests -from NeverTimerAnnotation ann +from TimerAnnotation ann where neverReachable(ann) select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected new file mode 100644 index 000000000000..fcc9a17aa746 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected @@ -0,0 +1,282 @@ +| test_assert_raise.py:51:20:51:53 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_assert_raise.py:48:1:48:25 | Function test_bare_reraise | test_bare_reraise | +| test_assert_raise.py:51:20:51:53 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_assert_raise.py:48:1:48:25 | Function test_bare_reraise | test_bare_reraise | +| test_assert_raise.py:51:20:51:53 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_assert_raise.py:48:1:48:25 | Function test_bare_reraise | test_bare_reraise | +| test_assert_raise.py:51:20:51:53 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_assert_raise.py:48:1:48:25 | Function test_bare_reraise | test_bare_reraise | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 10 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 10 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:10:12:10:52 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:8:1:8:23 | Function test_while_loop | test_while_loop | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 25 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:19:12:19:46 | After Compare | Timestamp 25 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 25 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:20:13:20:48 | After Compare | Timestamp 25 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:17:1:17:24 | Function test_while_break | test_while_break | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 17 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 17 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 26 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 26 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 38 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:31:12:31:54 | After Compare | Timestamp 38 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 23 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 23 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 32 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 32 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 35 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:33:13:33:48 | After Compare | Timestamp 35 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:28:1:28:27 | Function test_while_continue | test_while_continue | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 10 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 10 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:43:12:43:44 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:41:1:41:23 | Function test_while_else | test_while_else | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:54:13:54:40 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:54:13:54:40 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | +| test_loops.py:65:14:65:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:65:14:65:43 | After List | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:66:9:66:9 | x | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:64:1:64:21 | Function test_for_list | test_for_list | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:73:14:73:37 | After BinaryExpr() | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:74:9:74:9 | i | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:72:1:72:22 | Function test_for_range | test_for_range | +| test_loops.py:81:14:81:53 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:81:14:81:53 | After List | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 12 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 12 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:82:13:82:47 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:84:9:84:9 | x | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:80:1:80:22 | Function test_for_break | test_for_break | +| test_loops.py:92:14:92:43 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 14 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 14 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 20 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:92:14:92:43 | After List | Timestamp 20 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 17 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 17 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 20 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:93:13:93:48 | After Compare | Timestamp 20 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 14 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 14 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 20 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:95:18:95:48 | After BinaryExpr | Timestamp 20 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:90:1:90:25 | Function test_for_continue | test_for_continue | +| test_loops.py:102:14:102:33 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:102:14:102:33 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:102:14:102:33 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:102:14:102:33 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:102:14:102:33 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:102:14:102:33 | After List | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:103:9:103:9 | x | Timestamp 5 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:101:1:101:21 | Function test_for_else | test_for_else | +| test_loops.py:111:14:111:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:111:14:111:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:112:13:112:38 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:112:13:112:38 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | +| test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 12 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 12 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 15 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 15 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 18 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 18 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:124:18:124:47 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 12 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 12 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 15 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 15 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 18 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 18 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 21 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:125:14:125:65 | After BinaryExpr | Timestamp 21 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | +| test_loops.py:133:11:133:14 | True | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 22 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:133:11:133:14 | True | Timestamp 22 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 1 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 1 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 15 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 15 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:135:13:135:48 | After Compare | Timestamp 22 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:131:1:131:29 | Function test_while_true_break | test_while_true_break | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 10 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 10 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 12 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:143:21:143:83 | After BinaryExpr() | Timestamp 12 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 10 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 10 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 12 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_loops.py:145:9:145:11 | val | Timestamp 12 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:142:1:142:26 | Function test_for_enumerate | test_for_enumerate | +| test_match.py:16:11:16:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:14:1:14:26 | Function test_match_literal | test_match_literal | +| test_match.py:16:11:16:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:14:1:14:26 | Function test_match_literal | test_match_literal | +| test_match.py:16:11:16:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:14:1:14:26 | Function test_match_literal | test_match_literal | +| test_match.py:16:11:16:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:14:1:14:26 | Function test_match_literal | test_match_literal | +| test_match.py:27:11:27:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:25:1:25:38 | Function test_match_literal_fallthrough | test_match_literal_fallthrough | +| test_match.py:27:11:27:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:25:1:25:38 | Function test_match_literal_fallthrough | test_match_literal_fallthrough | +| test_match.py:27:11:27:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:25:1:25:38 | Function test_match_literal_fallthrough | test_match_literal_fallthrough | +| test_match.py:27:11:27:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:25:1:25:38 | Function test_match_literal_fallthrough | test_match_literal_fallthrough | +| test_match.py:51:11:51:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:49:1:49:26 | Function test_match_capture | test_match_capture | +| test_match.py:51:11:51:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:49:1:49:26 | Function test_match_capture | test_match_capture | +| test_match.py:51:11:51:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:49:1:49:26 | Function test_match_capture | test_match_capture | +| test_match.py:51:11:51:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:49:1:49:26 | Function test_match_capture | test_match_capture | +| test_match.py:71:11:71:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:69:1:69:24 | Function test_match_guard | test_match_guard | +| test_match.py:71:11:71:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:69:1:69:24 | Function test_match_guard | test_match_guard | +| test_match.py:82:11:82:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:80:1:80:32 | Function test_match_class_pattern | test_match_class_pattern | +| test_match.py:82:11:82:11 | x | Timestamp 2 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:80:1:80:32 | Function test_match_class_pattern | test_match_class_pattern | +| test_match.py:82:11:82:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:80:1:80:32 | Function test_match_class_pattern | test_match_class_pattern | +| test_match.py:82:11:82:11 | x | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:80:1:80:32 | Function test_match_class_pattern | test_match_class_pattern | +| test_match.py:93:11:93:11 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_match.py:91:1:91:27 | Function test_match_sequence | test_match_sequence | +| test_match.py:93:11:93:11 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_match.py:91:1:91:27 | Function test_match_sequence | test_match_sequence | +| test_try.py:95:16:95:36 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:92:1:92:41 | Function test_try_except_finally_exception | test_try_except_finally_exception | +| test_try.py:95:16:95:36 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:92:1:92:41 | Function test_try_except_finally_exception | test_try_except_finally_exception | +| test_try.py:95:16:95:36 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:92:1:92:41 | Function test_try_except_finally_exception | test_try_except_finally_exception | +| test_try.py:95:16:95:36 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:92:1:92:41 | Function test_try_except_finally_exception | test_try_except_finally_exception | +| test_try.py:147:20:147:40 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:142:1:142:30 | Function test_nested_try_except | test_nested_try_except | +| test_try.py:147:20:147:40 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:142:1:142:30 | Function test_nested_try_except | test_nested_try_except | +| test_try.py:162:17:162:52 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:162:17:162:52 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:162:17:162:52 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:162:17:162:52 | After Compare | Timestamp 14 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:162:17:162:52 | After Compare | Timestamp 23 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:162:17:162:52 | After Compare | Timestamp 23 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:158:1:158:24 | Function test_try_in_loop | test_try_in_loop | +| test_try.py:176:20:176:40 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:172:1:172:20 | Function test_reraise | test_reraise | +| test_try.py:176:20:176:40 | After BinaryExpr() | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:172:1:172:20 | Function test_reraise | test_reraise | +| test_try.py:176:20:176:40 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the false successor in $@ | test_try.py:172:1:172:20 | Function test_reraise | test_reraise | +| test_try.py:176:20:176:40 | After BinaryExpr() | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_try.py:172:1:172:20 | Function test_reraise | test_reraise | +| test_with.py:55:14:55:33 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:55:14:55:33 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:55:14:55:33 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:55:14:55:33 | After List | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:55:14:55:33 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:55:14:55:33 | After List | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 3 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 6 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 6 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 9 on true/false branch is missing a dead() annotation on the false successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | +| test_with.py:57:17:57:17 | i | Timestamp 9 on true/false branch is missing a dead() annotation on the true successor in $@ | test_with.py:54:1:54:25 | Function test_with_in_loop | test_with_in_loop | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.ql new file mode 100644 index 000000000000..cd591b867666 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.ql @@ -0,0 +1,23 @@ +/** + * New-CFG version of BranchTimestamps. + * + * Checks that when a node has both a true and false successor, the + * live timestamps on one branch are marked as dead on the other. + * This ensures that boolean branches are fully annotated with dead() + * markers for the paths not taken. + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerCfgNode node, int ts, string branch +where missingBranchTimestamp(node, ts, branch) +select node, + "Timestamp " + ts + " on true/false branch is missing a dead() annotation on the " + branch + + " successor in $@", node.getTestFunction(), node.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.expected new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.expected @@ -0,0 +1 @@ + diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.ql new file mode 100644 index 000000000000..3feacae264e5 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutivePredecessorTimestamps.ql @@ -0,0 +1,22 @@ +/** + * New-CFG version of ConsecutivePredecessorTimestamps. + * + * Checks that each annotated node (except the minimum timestamp) has + * a predecessor annotation with timestamp `a - 1`. This is the reverse + * of ConsecutiveTimestamps: it catches nodes that are reachable but + * arrived at from the wrong place (skipping an intermediate node). + */ + +import python +import TimerUtils +import NewCfgImpl + +private module Utils = EvalOrderCfgUtils; + +private import Utils +private import Utils::CfgTests + +from TimerAnnotation ann, int a +where consecutivePredecessorTimestamps(ann, a) +select ann, "$@ in $@ has no consecutive predecessor (expected " + (a - 1) + ")", + ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected index bce948bb58a5..e69de29bb2d1 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgConsecutiveTimestamps.expected @@ -1 +0,0 @@ -| test_if.py:51:9:51:9 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 6) | test_if.py:51:15:51:15 | IntegerLiteral | Timestamp 5 | test_if.py:43:1:43:31 | Function test_if_elif_else_first | test_if_elif_else_first | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll index 97c6a9c043fa..1da80d2ee0dd 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgImpl.qll @@ -6,6 +6,7 @@ private import python as Py import TimerUtils private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +private import codeql.controlflow.SuccessorType private class NewControlFlowNode = CfgImpl::ControlFlowNode; @@ -31,6 +32,20 @@ module NewCfg implements EvalOrderCfgSig { CfgNode getASuccessor() { nextCfgNode(this, result) } + CfgNode getATrueSuccessor() { + NewControlFlowNode.super.isAfterTrue(_) and + // Only where there's also a false branch (true boolean split) + exists(NewControlFlowNode other | other.isAfterFalse(NewControlFlowNode.super.getAstNode())) and + nextCfgNodeFrom(this, result) + } + + CfgNode getAFalseSuccessor() { + NewControlFlowNode.super.isAfterFalse(_) and + // Only where there's also a true branch (true boolean split) + exists(NewControlFlowNode other | other.isAfterTrue(NewControlFlowNode.super.getAstNode())) and + nextCfgNodeFrom(this, result) + } + CfgNode getAnExceptionalSuccessor() { exists(NewControlFlowNode mid | mid = NewControlFlowNode.super.getAnExceptionSuccessor() and diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql index 3430d49b57ef..6949b2cc6e9b 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgNeverReachable.ql @@ -15,7 +15,7 @@ private module Utils = EvalOrderCfgUtils; private import Utils::CfgTests -from NeverTimerAnnotation ann +from TimerAnnotation ann where neverReachable(ann) select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(), ann.getTestFunction().getName() diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected index e69de29bb2d1..1ef8be08d27b 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NoBackwardFlow.expected @@ -0,0 +1,10 @@ +| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:9:59:9:59 | IntegerLiteral | 2 | test_boolean.py:9:10:9:13 | ControlFlowNode for True | True | test_boolean.py:9:19:9:19 | IntegerLiteral | 0 | +| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:15:50:15:50 | IntegerLiteral | 1 | test_boolean.py:15:10:15:14 | ControlFlowNode for False | False | test_boolean.py:15:20:15:20 | IntegerLiteral | 0 | +| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:21:49:21:49 | IntegerLiteral | 1 | test_boolean.py:21:10:21:13 | ControlFlowNode for True | True | test_boolean.py:21:19:21:19 | IntegerLiteral | 0 | +| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:27:59:27:59 | IntegerLiteral | 2 | test_boolean.py:27:10:27:14 | ControlFlowNode for False | False | test_boolean.py:27:20:27:20 | IntegerLiteral | 0 | +| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:40:86:40:86 | IntegerLiteral | 3 | test_boolean.py:40:10:40:10 | ControlFlowNode for IntegerLiteral | IntegerLiteral | test_boolean.py:40:16:40:16 | IntegerLiteral | 0 | +| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:46:86:46:86 | IntegerLiteral | 3 | test_boolean.py:46:10:46:10 | ControlFlowNode for IntegerLiteral | IntegerLiteral | test_boolean.py:46:16:46:16 | IntegerLiteral | 0 | +| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:52:120:52:120 | IntegerLiteral | 4 | test_boolean.py:52:11:52:47 | ControlFlowNode for BoolExpr | BoolExpr | test_boolean.py:52:63:52:63 | IntegerLiteral | 2 | +| test_boolean.py:52:11:52:47 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:52:63:52:63 | IntegerLiteral | 2 | test_boolean.py:52:11:52:14 | ControlFlowNode for True | True | test_boolean.py:52:20:52:20 | IntegerLiteral | 0 | +| test_boolean.py:64:10:64:52 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:64:59:64:59 | IntegerLiteral | 6 | test_boolean.py:64:11:64:11 | ControlFlowNode for f | f | test_boolean.py:64:17:64:17 | IntegerLiteral | 0 | +| test_boolean.py:76:10:76:51 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:76:58:76:58 | IntegerLiteral | 6 | test_boolean.py:76:11:76:11 | ControlFlowNode for f | f | test_boolean.py:76:17:76:17 | IntegerLiteral | 0 | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected index e69de29bb2d1..aa03001b61bd 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/StrictForward.expected @@ -0,0 +1,10 @@ +| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:9:59:9:59 | IntegerLiteral | timestamp 2 | test_boolean.py:9:19:9:19 | IntegerLiteral | timestamp 0 | +| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:15:50:15:50 | IntegerLiteral | timestamp 1 | test_boolean.py:15:20:15:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:21:49:21:49 | IntegerLiteral | timestamp 1 | test_boolean.py:21:19:21:19 | IntegerLiteral | timestamp 0 | +| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:27:59:27:59 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:40:86:40:86 | IntegerLiteral | timestamp 3 | test_boolean.py:40:16:40:16 | IntegerLiteral | timestamp 0 | +| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:46:86:46:86 | IntegerLiteral | timestamp 3 | test_boolean.py:46:16:46:16 | IntegerLiteral | timestamp 0 | +| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:52:120:52:120 | IntegerLiteral | timestamp 4 | test_boolean.py:52:63:52:63 | IntegerLiteral | timestamp 2 | +| test_boolean.py:52:11:52:47 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:52:63:52:63 | IntegerLiteral | timestamp 2 | test_boolean.py:52:20:52:20 | IntegerLiteral | timestamp 0 | +| test_boolean.py:64:10:64:52 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:64:59:64:59 | IntegerLiteral | timestamp 6 | test_boolean.py:64:17:64:17 | IntegerLiteral | timestamp 0 | +| test_boolean.py:76:10:76:51 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:76:58:76:58 | IntegerLiteral | timestamp 6 | test_boolean.py:76:17:76:17 | IntegerLiteral | timestamp 0 | diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll index dc46f00f6f56..da66bd31b258 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/TimerUtils.qll @@ -29,9 +29,40 @@ private IntegerLiteral timestampLiteral(Expr timestamps) { result = timestamps.(Tuple).getAnElt() } +/** + * Gets an element from a timestamp subscript index. Each element is either + * an `IntegerLiteral` (live), a `Call` to `dead` (dead), a `Name("never")` + * (never), or a tuple containing any mix of these. + */ +private Expr timestampElement(Expr timestamps) { + result = timestamps and not timestamps instanceof Tuple + or + result = timestamps.(Tuple).getAnElt() +} + +/** Gets a live timestamp value from a subscript index expression. */ +private IntegerLiteral liveTimestampLiteral(Expr timestamps) { + result = timestampElement(timestamps) and + not result = any(Call c).getAnArg() +} + +/** Gets a dead timestamp value from a subscript index expression. */ +private IntegerLiteral deadTimestampLiteral(Expr timestamps) { + exists(Call c | + c = timestampElement(timestamps) and + c.getFunc().(Name).getId() = "dead" and + result = c.getArg(0) + ) +} + +/** Holds if the subscript index contains `never`. */ +private predicate hasNever(Expr timestamps) { + timestampElement(timestamps).(Name).getId() = "never" +} + /** A timer annotation in the AST. */ private newtype TTimerAnnotation = - /** `expr @ t[n]` or `expr @ t[n, m, ...]` */ + /** `expr @ t[n]` or `expr @ t[n, m, ...]` or `expr @ t[dead(n), m, never]` */ TMatmulAnnotation(TestFunction func, Expr annotated, Expr timestamps) { exists(BinaryExpr be | be.getOp() instanceof MatMult and @@ -49,40 +80,29 @@ private newtype TTimerAnnotation = annotated = call.getArg(0) and timestamps = call.getArg(1) ) - } or - /** `expr @ t.dead[n]` — dead-code annotation */ - TDeadAnnotation(TestFunction func, Expr annotated, Expr timestamps) { - exists(BinaryExpr be | - be.getOp() instanceof MatMult and - be.getRight().(Subscript).getObject().(Attribute).getObject("dead").(Name).getId() = - func.getTimerParamName() and - be.getScope().getEnclosingScope*() = func and - annotated = be.getLeft() and - timestamps = be.getRight().(Subscript).getIndex() - ) - } or - /** `expr @ t.never` — annotation for code that should never be evaluated */ - TNeverAnnotation(TestFunction func, Expr annotated) { - exists(BinaryExpr be | - be.getOp() instanceof MatMult and - be.getRight().(Attribute).getObject("never").(Name).getId() = func.getTimerParamName() and - be.getScope().getEnclosingScope*() = func and - annotated = be.getLeft() - ) } /** A timer annotation (wrapping the newtype for a clean API). */ class TimerAnnotation extends TTimerAnnotation { - /** Gets a timestamp value from this annotation. */ + /** Gets a live timestamp value from this annotation. */ int getATimestamp() { exists(this.getTimestampExpr(result)) } - /** Gets the source expression for timestamp value `ts`. */ + /** Gets the source expression for live timestamp value `ts`. */ IntegerLiteral getTimestampExpr(int ts) { - result = timestampLiteral(this.getTimestampsExpr()) and + result = liveTimestampLiteral(this.getTimestampsExpr()) and + result.getValue() = ts + } + + /** Gets a dead timestamp value from this annotation. */ + int getADeadTimestamp() { exists(this.getDeadTimestampExpr(result)) } + + /** Gets the source expression for dead timestamp value `ts`. */ + IntegerLiteral getDeadTimestampExpr(int ts) { + result = deadTimestampLiteral(this.getTimestampsExpr()) and result.getValue() = ts } - /** Gets the raw timestamp expression (single int or tuple). */ + /** Gets the raw timestamp expression (single element or tuple). */ abstract Expr getTimestampsExpr(); /** Gets the test function this annotation belongs to. */ @@ -94,18 +114,25 @@ class TimerAnnotation extends TTimerAnnotation { /** Gets the enclosing annotation expression (the `BinaryExpr` or `Call`). */ abstract Expr getTimerExpr(); - /** Holds if this is a dead-code annotation (`t.dead[n]`). */ - predicate isDead() { this instanceof DeadTimerAnnotation } + /** Holds if timestamp `ts` is marked as dead in this annotation. */ + predicate isDeadTimestamp(int ts) { ts = this.getADeadTimestamp() } + + /** Holds if all timestamps in this annotation are dead (no live timestamps). */ + predicate isDead() { + not exists(this.getATimestamp()) and + not this.isNever() and + exists(this.getADeadTimestamp()) + } - /** Holds if this is a never-evaluated annotation (`t.never`). */ - predicate isNever() { this instanceof NeverTimerAnnotation } + /** Holds if this is a never-evaluated annotation (contains `never`). */ + predicate isNever() { hasNever(this.getTimestampsExpr()) } string toString() { result = this.getAnnotatedExpr().toString() } Location getLocation() { result = this.getAnnotatedExpr().getLocation() } } -/** A matmul-based timer annotation: `expr @ t[n]`. */ +/** A matmul-based timer annotation: `expr @ t[...]`. */ class MatmulTimerAnnotation extends TMatmulAnnotation, TimerAnnotation { TestFunction func; Expr annotated; @@ -139,39 +166,6 @@ class CallTimerAnnotation extends TCallAnnotation, TimerAnnotation { override Call getTimerExpr() { result.getArg(0) = annotated } } -/** A dead-code timer annotation: `expr @ t.dead[n]`. */ -class DeadTimerAnnotation extends TDeadAnnotation, TimerAnnotation { - TestFunction func; - Expr annotated; - Expr timestamps; - - DeadTimerAnnotation() { this = TDeadAnnotation(func, annotated, timestamps) } - - override Expr getTimestampsExpr() { result = timestamps } - - override TestFunction getTestFunction() { result = func } - - override Expr getAnnotatedExpr() { result = annotated } - - override BinaryExpr getTimerExpr() { result.getLeft() = annotated } -} - -/** A never-evaluated annotation: `expr @ t.never`. */ -class NeverTimerAnnotation extends TNeverAnnotation, TimerAnnotation { - TestFunction func; - Expr annotated; - - NeverTimerAnnotation() { this = TNeverAnnotation(func, annotated) } - - override Expr getTimestampsExpr() { none() } - - override TestFunction getTestFunction() { result = func } - - override Expr getAnnotatedExpr() { result = annotated } - - override BinaryExpr getTimerExpr() { result.getLeft() = annotated } -} - /** * Signature module defining the CFG interface needed by evaluation-order tests. * This allows the test utilities to be instantiated with different CFG implementations. @@ -191,6 +185,12 @@ signature module EvalOrderCfgSig { /** Gets a successor of this CFG node (including exceptional). */ CfgNode getASuccessor(); + /** Gets a true-branch successor of this CFG node, if any. */ + CfgNode getATrueSuccessor(); + + /** Gets a false-branch successor of this CFG node, if any. */ + CfgNode getAFalseSuccessor(); + /** Gets an exceptional successor of this CFG node. */ CfgNode getAnExceptionalSuccessor(); @@ -251,7 +251,10 @@ module EvalOrderCfgUtils { /** Gets the test function this annotation belongs to. */ TestFunction getTestFunction() { result = annot.getTestFunction() } - /** Holds if this is a dead-code annotation. */ + /** Holds if timestamp `ts` is marked as dead. */ + predicate isDeadTimestamp(int ts) { annot.isDeadTimestamp(ts) } + + /** Holds if all timestamps in this annotation are dead. */ predicate isDead() { annot.isDead() } /** Holds if this is a never-evaluated annotation. */ @@ -275,6 +278,42 @@ module EvalOrderCfgUtils { ) } + /** + * Holds if `next` is the next timer annotation reachable from `n` via + * the true branch, skipping non-annotated intermediaries and after-value + * nodes for the same AST node. + */ + predicate nextTimerAnnotationFromTrue(CfgNode n, TimerCfgNode next) { + exists(CfgNode trueSucc | + trueSucc = n.getATrueSuccessor() and + trueSucc.getScope() = n.getScope() + | + // If the true successor is a different annotated node, use it + next = trueSucc and next.getNode() != n.getNode() + or + // Otherwise skip through it (it's an after-value node for the same expr) + nextTimerAnnotation(trueSucc, next) + ) + } + + /** + * Holds if `next` is the next timer annotation reachable from `n` via + * the false branch, skipping non-annotated intermediaries and after-value + * nodes for the same AST node. + */ + predicate nextTimerAnnotationFromFalse(CfgNode n, TimerCfgNode next) { + exists(CfgNode falseSucc | + falseSucc = n.getAFalseSuccessor() and + falseSucc.getScope() = n.getScope() + | + // If the false successor is a different annotated node, use it + next = falseSucc and next.getNode() != n.getNode() + or + // Otherwise skip through it (it's an after-value node for the same expr) + nextTimerAnnotation(falseSucc, next) + ) + } + /** CFG-dependent test predicates, one per evaluation-order query. */ module CfgTests { /** @@ -352,7 +391,8 @@ module EvalOrderCfgUtils { * Holds if the expression annotated with `t.never` is reachable from * its scope's entry. */ - predicate neverReachable(NeverTimerAnnotation ann) { + predicate neverReachable(TimerAnnotation ann) { + ann.isNever() and exists(CfgNode n, Scope s | n.getNode() = ann.getAnnotatedExpr() and s = n.getScope() and @@ -438,6 +478,61 @@ module EvalOrderCfgUtils { predicate annotationWithCfgNode(TimerAnnotation ann) { exists(CfgNode n | n.getNode() = ann.getAnnotatedExpr()) } + + /** + * Holds if annotation `ann` with timestamp `a` has no consecutive + * predecessor (expected `a - 1`) in the CFG. + */ + predicate consecutivePredecessorTimestamps(TimerAnnotation ann, int a) { + not hasNestedScopeAnnotation(ann.getTestFunction()) and + not ann.isDead() and + a = ann.getATimestamp() and + not exists(TimerCfgNode x, TimerCfgNode y | + ann.getAnnotatedExpr() = y.getNode() and + nextTimerAnnotation(x, y) and + (a - 1) = x.getATimestamp() + ) and + // Exclude the minimum timestamp in the function (it has no predecessor) + not a = + min(TimerAnnotation other | + other.getTestFunction() = ann.getTestFunction() and + not other.isDead() + | + other.getATimestamp() + ) + } + + /** + * Holds if `node` has both a true and false successor, but the true + * successor's timestamp `ts` is not marked as dead on the false + * successor (or vice versa). + * + * This checks that boolean branches are properly annotated: when a + * condition splits into true/false paths, the next annotated node + * on each side should account for the other side's timestamps as dead. + */ + predicate missingBranchTimestamp(TimerCfgNode node, int ts, string branch) { + not hasNestedScopeAnnotation(node.getTestFunction()) and + exists(TimerCfgNode trueNext, TimerCfgNode falseNext | + nextTimerAnnotationFromTrue(node, trueNext) and + nextTimerAnnotationFromFalse(node, falseNext) and + trueNext != falseNext + | + // True successor has live timestamp ts, but false successor + // doesn't have it as dead + ts = trueNext.getATimestamp() and + not falseNext.isDeadTimestamp(ts) and + not ts = falseNext.getATimestamp() and + branch = "false" + or + // False successor has live timestamp ts, but true successor + // doesn't have it as dead + ts = falseNext.getATimestamp() and + not trueNext.isDeadTimestamp(ts) and + not ts = trueNext.getATimestamp() and + branch = "true" + ) + } } } diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py index 9958d922ec8f..692a9c6e407c 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_assert_raise.py @@ -1,6 +1,6 @@ """Assert and raise statement evaluation order.""" -from timer import test +from timer import test, dead @test @@ -13,7 +13,7 @@ def test_assert_true(t): @test def test_assert_true_with_message(t): x = True @ t[0] - assert x @ t[1], "msg" @ t.dead[2] + assert x @ t[1], "msg" @ t[dead(2)] y = 1 @ t[2] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py index f2ece3a0820d..3e8ee925d913 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_basic.py @@ -8,7 +8,7 @@ timer mechanism itself (t[n], t.dead[n]). """ -from timer import test +from timer import test, never @test @@ -178,7 +178,7 @@ def test_unreachable_after_return(t): def f(): x = 1 @ t[1] return x @ t[2] - y = 2 @ t.never + y = 2 @ t[never] result = (f @ t[0])() @ t[3] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py index d8183cb64842..a3b2268a8315 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_boolean.py @@ -1,30 +1,30 @@ """Short-circuit boolean operators and evaluation order.""" -from timer import test +from timer import test, dead @test def test_and_both_sides(t): # True and X — both operands evaluated, result is X - x = (True @ t[0] and 42 @ t[1]) @ t[2] + x = (True @ t[0] and 42 @ t[1, dead(2)]) @ t[dead(1), 2] @test def test_and_short_circuit(t): # False and ... — right side never evaluated - x = (False @ t[0] and True @ t.dead[1]) @ t[1] + x = (False @ t[0] and True @ t[dead(1)]) @ t[1, dead(2)] @test def test_or_short_circuit(t): # True or ... — right side never evaluated - x = (True @ t[0] or False @ t.dead[1]) @ t[1] + x = (True @ t[0] or False @ t[dead(1)]) @ t[1, dead(2)] @test def test_or_both_sides(t): # False or X — both operands evaluated, result is X - x = (False @ t[0] or 42 @ t[1]) @ t[2] + x = (False @ t[0] or 42 @ t[1, dead(2)]) @ t[dead(1), 2] @test @@ -37,19 +37,19 @@ def test_not(t): @test def test_chained_and(t): # 1 and 2 and 3 — all truthy, all evaluated left-to-right - x = (1 @ t[0] and 2 @ t[1] and 3 @ t[2]) @ t[3] + x = (1 @ t[0] and 2 @ t[1, dead(3)] and 3 @ t[2, dead(3)]) @ t[dead(1), dead(2), 3] @test def test_chained_or(t): # 0 or "" or 42 — first two falsy, all evaluated until truthy found - x = (0 @ t[0] or "" @ t[1] or 42 @ t[2]) @ t[3] + x = (0 @ t[0] or "" @ t[1, dead(3)] or 42 @ t[2, dead(3)]) @ t[dead(1), dead(2), 3] @test def test_mixed_and_or(t): # True and False or 42 => (True and False) or 42 => False or 42 => 42 - x = ((True @ t[0] and False @ t[1]) @ t[2] or 42 @ t[3]) @ t[4] + x = ((True @ t[0] and False @ t[1, dead(2)]) @ t[dead(1), 2, dead(4)] or 42 @ t[3, dead(4)]) @ t[dead(2), dead(3), 4] @test diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py index 2c543e913e4d..48d45a779583 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_conditional.py @@ -1,38 +1,38 @@ """Ternary conditional expressions and evaluation order.""" -from timer import test +from timer import test, dead @test def test_ternary_true(t): # Condition is True — consequent evaluated, alternative skipped - x = (1 @ t[1] if True @ t[0] else 2 @ t.dead[1]) @ t[2] + x = (1 @ t[1] if True @ t[0] else 2 @ t[dead(1)]) @ t[2] @test def test_ternary_false(t): # Condition is False — alternative evaluated, consequent skipped - x = (1 @ t.dead[1] if False @ t[0] else 2 @ t[1]) @ t[2] + x = (1 @ t[dead(1)] if False @ t[0] else 2 @ t[1]) @ t[2] @test def test_ternary_nested(t): # Nested: outer condition True, inner condition True # ((10 if C1 else 20) if C2 else 30) — C2 first, then C1, then 10 - x = ((10 @ t[2] if True @ t[1] else 20 @ t.dead[2]) @ t[3] if True @ t[0] else 30 @ t.dead[1]) @ t[4] + x = ((10 @ t[2] if True @ t[1] else 20 @ t[dead(2)]) @ t[3] if True @ t[0] else 30 @ t[dead(1)]) @ t[4] @test def test_ternary_assignment(t): # Ternary result assigned, then used in later expression - value = (100 @ t[1] if True @ t[0] else 200 @ t.dead[1]) @ t[2] + value = (100 @ t[1] if True @ t[0] else 200 @ t[dead(1)]) @ t[2] result = (value @ t[3] + 1 @ t[4]) @ t[5] @test def test_ternary_complex_expressions(t): # Complex sub-expressions in condition and consequent - x = ((1 @ t[3] + 2 @ t[4]) @ t[5] if (3 @ t[0] > 2 @ t[1]) @ t[2] else (4 @ t.dead[3] + 5 @ t.dead[4]) @ t.dead[5]) @ t[6] + x = ((1 @ t[3] + 2 @ t[4]) @ t[5] if (3 @ t[0] > 2 @ t[1]) @ t[2] else (4 @ t[dead(3)] + 5 @ t[dead(4)]) @ t[dead(5)]) @ t[6] @test @@ -41,4 +41,4 @@ def test_ternary_as_argument(t): def f(a): return a @ t[4] - result = (f @ t[0])((1 @ t[2] if True @ t[1] else 2 @ t.dead[2]) @ t[3]) @ t[5] + result = (f @ t[0])((1 @ t[2] if True @ t[1] else 2 @ t[dead(2)]) @ t[3]) @ t[5] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py index 3190e94c6eba..79abb278684c 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_if.py @@ -1,6 +1,6 @@ """If/elif/else control flow evaluation order.""" -from timer import test +from timer import test, dead @test @@ -15,7 +15,7 @@ def test_if_true(t): def test_if_false(t): x = False @ t[0] if x @ t[1]: - y = 1 @ t.dead[2] + y = 1 @ t[dead(2)] z = 0 @ t[2] @@ -25,7 +25,7 @@ def test_if_else_true(t): if x @ t[1]: y = 1 @ t[2] else: - y = 2 @ t.dead[2] + y = 2 @ t[dead(2)] z = 0 @ t[3] @@ -33,7 +33,7 @@ def test_if_else_true(t): def test_if_else_false(t): x = False @ t[0] if x @ t[1]: - y = 1 @ t.dead[2] + y = 1 @ t[dead(2)] else: y = 2 @ t[2] z = 0 @ t[3] @@ -44,10 +44,10 @@ def test_if_elif_else_first(t): x = 1 @ t[0] if (x @ t[1] == 1 @ t[2]) @ t[3]: y = "first" @ t[4] - elif (x @ t.dead[4] == 2 @ t.dead[5]) @ t.dead[6]: - y = "second" @ t.dead[4] + elif (x @ t[dead(4)] == 2 @ t[dead(5)]) @ t[dead(6)]: + y = "second" @ t[dead(4)] else: - y = "third" @ t.dead[4] + y = "third" @ t[dead(4)] z = 0 @ t[5] @@ -55,11 +55,11 @@ def test_if_elif_else_first(t): def test_if_elif_else_second(t): x = 2 @ t[0] if (x @ t[1] == 1 @ t[2]) @ t[3]: - y = "first" @ t.dead[7] + y = "first" @ t[dead(7)] elif (x @ t[4] == 2 @ t[5]) @ t[6]: y = "second" @ t[7] else: - y = "third" @ t.dead[7] + y = "third" @ t[dead(7)] z = 0 @ t[8] @@ -67,9 +67,9 @@ def test_if_elif_else_second(t): def test_if_elif_else_third(t): x = 3 @ t[0] if (x @ t[1] == 1 @ t[2]) @ t[3]: - y = "first" @ t.dead[7] + y = "first" @ t[dead(7)] elif (x @ t[4] == 2 @ t[5]) @ t[6]: - y = "second" @ t.dead[7] + y = "second" @ t[dead(7)] else: y = "third" @ t[7] z = 0 @ t[8] @@ -83,9 +83,9 @@ def test_nested_if_else(t): if y @ t[3]: z = 1 @ t[4] else: - z = 2 @ t.dead[4] + z = 2 @ t[dead(4)] else: - z = 3 @ t.dead[4] + z = 3 @ t[dead(4)] w = 0 @ t[5] @@ -94,7 +94,7 @@ def test_if_compound_condition(t): x = True @ t[0] y = False @ t[1] if (x @ t[2] and y @ t[3]) @ t[4]: - z = 1 @ t.dead[5] + z = 1 @ t[dead(5)] else: z = 2 @ t[5] w = 0 @ t[6] @@ -106,3 +106,9 @@ def test_if_pass(t): if x @ t[1]: pass z = 0 @ t[2] + + +@test + + +@test diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py index e81c31acde5c..17df7a4703a3 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_loops.py @@ -1,6 +1,6 @@ """Loop control flow evaluation order tests.""" -from timer import test +from timer import test, dead # 1. Simple while loop (fixed iterations) @@ -55,7 +55,7 @@ def test_while_else_break(t): break i = (i @ t[7] + 1 @ t[8]) @ t[9] else: - never = True @ t.dead[16] + never = True @ t[dead(16)] after = True @ t[16] @@ -113,7 +113,7 @@ def test_for_else_break(t): break x @ t[7] else: - never = True @ t.dead[11] + never = True @ t[dead(11)] after = True @ t[11] @@ -122,8 +122,8 @@ def test_for_else_break(t): def test_nested_loops(t): for i in [1 @ t[0], 2 @ t[1]] @ t[2]: for j in [10 @ t[3, 12], 20 @ t[4, 13]] @ t[5, 14]: - (i @ t[6, 9, 15, 18] + j @ t[7, 10, 16, 19]) @ t[8, 11, 17, 20] - done = True @ t[21] + (i @ t[6, 9, 15, 18, dead(21)] + j @ t[7, 10, 16, 19]) @ t[8, 11, 17, 20] + done = True @ t[dead(3), dead(6), dead(9), dead(12), dead(15), dead(18), 21] # 13. While True with conditional break diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py index 1dac5b0985c9..ba15a2d7c857 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_match.py @@ -7,7 +7,7 @@ print("0/0 tests passed") sys.exit(0) -from timer import test +from timer import test, dead, never @test @@ -17,7 +17,7 @@ def test_match_literal(t): case 1: y = "one" @ t[2] case 2: - y = "two" @ t.dead[2] + y = "two" @ t[dead(2)] z = y @ t[3] @@ -26,9 +26,9 @@ def test_match_literal_fallthrough(t): x = 3 @ t[0] match x @ t[1]: case 1: - y = "one" @ t.dead[2] + y = "one" @ t[dead(2)] case 2: - y = "two" @ t.dead[2] + y = "two" @ t[dead(2)] case 3: y = "three" @ t[2] z = y @ t[3] @@ -39,7 +39,7 @@ def test_match_wildcard(t): x = 42 @ t[0] match x @ t[1]: case 1: - y = "one" @ t.dead[2] + y = "one" @ t[dead(2)] case _: y = "other" @ t[2] z = y @ t[3] @@ -61,7 +61,7 @@ def test_match_or_pattern(t): case 1 | 2: y = "low" @ t[2] case _: - y = "other" @ t.dead[2] + y = "other" @ t[dead(2)] z = y @ t[3] @@ -72,7 +72,7 @@ def test_match_guard(t): case n if (n @ t[2] > 3 @ t[3]) @ t[4]: y = n @ t[5] case _: - y = 0 @ t.dead[5] + y = 0 @ t[dead(5)] z = y @ t[6] @@ -83,7 +83,7 @@ def test_match_class_pattern(t): case int(): y = "integer" @ t[2] case str(): - y = "string" @ t.dead[2] + y = "string" @ t[dead(2)] z = y @ t[3] @@ -94,7 +94,7 @@ def test_match_sequence(t): case [a, b]: y = (a @ t[4] + b @ t[5]) @ t[6] case _: - y = 0 @ t.dead[6] + y = 0 @ t[dead(6)] z = y @ t[7] @@ -105,7 +105,7 @@ def test_match_mapping(t): case {"key": value}: y = value @ t[4] case _: - y = 0 @ t.dead[4] + y = 0 @ t[dead(4)] z = y @ t[5] @@ -116,7 +116,7 @@ def test_match_nested(t): case {"users": [{"name": name}]}: y = name @ t[7] case _: - y = "unknown" @ t.dead[7] + y = "unknown" @ t[dead(7)] z = y @ t[8] @@ -129,7 +129,7 @@ def test_match_or_pattern_with_as(t): result = ((uses @ t[2]).partition @ t[3])("@" @ t[4]) @ t[5] x = (result @ t[6])[0 @ t[7]] @ t[8] case _: - raise ((ValueError @ t.dead[2])(clause @ t.dead[3]) @ t.dead[4]) + raise ((ValueError @ t[dead(2)])(clause @ t[dead(3)]) @ t[dead(4)]) y = x @ t[9] @@ -140,7 +140,7 @@ def test_match_wildcard_raise(t): try: match clause @ t[1]: case (str() as uses) | {"uses": uses}: - result = uses @ t.dead[2] + result = uses @ t[dead(2)] case _: raise ((ValueError @ t[2])(f"Invalid: {clause @ t[3]}" @ t[4]) @ t[5]) except ValueError: @@ -155,8 +155,8 @@ def f(x): case 1: return "one" @ t[3] case _: - return "other" @ t.dead[3] - y = 0 @ t.never + return "other" @ t[dead(3)] + y = 0 @ t[never] result = (f @ t[0])(1 @ t[1]) @ t[4] @@ -166,8 +166,8 @@ def test_match_exhaustive_return_wildcard(t): def f(x): match x @ t[2]: case 1: - return "one" @ t.dead[3] + return "one" @ t[dead(3)] case _: return "other" @ t[3] - y = 0 @ t.never + y = 0 @ t[never] result = (f @ t[0])(99 @ t[1]) @ t[4] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py index d54730478b11..dd0b15457d69 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/test_try.py @@ -1,6 +1,6 @@ """Exception handling control flow: try/except/else/finally evaluation order.""" -from timer import test +from timer import test, dead, never # 1. try/except — no exception raised (except block skipped) @@ -10,7 +10,7 @@ def test_try_no_exception(t): x = 1 @ t[0] y = 2 @ t[1] except ValueError: - z = 3 @ t.dead[2] + z = 3 @ t[dead(2)] after = 0 @ t[2] @@ -20,7 +20,7 @@ def test_try_with_exception(t): try: x = 1 @ t[0] raise ((ValueError @ t[1])() @ t[2]) - y = 2 @ t.never + y = 2 @ t[never] except ValueError: z = 3 @ t[3] after = 0 @ t[4] @@ -32,7 +32,7 @@ def test_try_except_else_no_exception(t): try: x = 1 @ t[0] except ValueError: - y = 2 @ t.dead[1] + y = 2 @ t[dead(1)] else: z = 3 @ t[1] after = 0 @ t[2] @@ -47,7 +47,7 @@ def test_try_except_else_with_exception(t): except ValueError: y = 2 @ t[3] else: - z = 3 @ t.dead[3] + z = 3 @ t[dead(3)] after = 0 @ t[4] @@ -81,7 +81,7 @@ def test_try_except_finally_no_exception(t): try: x = 1 @ t[0] except ValueError: - y = 2 @ t.dead[1] + y = 2 @ t[dead(1)] finally: z = 3 @ t[1] after = 0 @ t[2] @@ -109,7 +109,7 @@ def test_multiple_except_first(t): except ValueError: y = 2 @ t[3] except TypeError: - z = 3 @ t.dead[3] + z = 3 @ t[dead(3)] after = 0 @ t[4] @@ -120,7 +120,7 @@ def test_multiple_except_second(t): x = 1 @ t[0] raise ((TypeError @ t[1])() @ t[2]) except ValueError: - y = 2 @ t.dead[3] + y = 2 @ t[dead(3)] except TypeError: z = 3 @ t[3] after = 0 @ t[4] @@ -149,7 +149,7 @@ def test_nested_try_except(t): z = 3 @ t[4] w = 4 @ t[5] except TypeError: - v = 5 @ t.dead[6] + v = 5 @ t[dead(6)] after = 0 @ t[6] diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py b/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py index 6cec3fd50cba..e10dde2592af 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py @@ -5,7 +5,7 @@ Usage with @test decorator (preferred): - from timer import test + from timer import test, dead, never @test def test_sequential(t): @@ -13,18 +13,14 @@ def test_sequential(t): y = 2 @ t[1] z = (x + y) @ t[2] -Usage with context manager (manual): - - from timer import Timer - - with Timer("my_test") as t: - x = 1 @ t[0] - -Timer API: - t[n] - assert current timestamp is n, return marker - t[n, m, ...] - assert current timestamp is one of {n, m, ...} - t["label"] - record current timestamp under label (development aid) - t(value, n) - equivalent to: value @ t[n] +Annotation forms: + t[n] - assert current timestamp is n, return marker + t[n, m, ...] - assert current timestamp is one of {n, m, ...} + t[dead(n)] - mark timestamp n as dead (fails if evaluated) + t[dead(n), m] - dead at n, live at m + t[never] - mark as never evaluated (fails if evaluated) + t["label"] - record current timestamp under label (development aid) + t(value, n) - equivalent to: value @ t[n] Run a test file directly to self-validate: python test_file.py """ @@ -36,19 +32,41 @@ def test_sequential(t): class _Check: - """Marker returned by t[n] — asserts the current timestamp.""" + """Marker returned by t[n] — asserts the current timestamp. + + Receives the raw subscript elements: plain ints are live timestamps, + dead(n) markers are dead timestamps, and `never` means any evaluation + is an error. + """ - __slots__ = ("_timer", "_expected") + __slots__ = ("_timer", "_live", "_dead", "_never") - def __init__(self, timer, expected): + def __init__(self, timer, elements): self._timer = timer - self._expected = expected + self._live = set() + self._dead = set() + self._never = False + for e in elements: + if isinstance(e, int): + self._live.add(e) + elif isinstance(e, _DeadMarker): + self._dead.add(e.timestamp) + elif isinstance(e, _NeverSentinel): + self._never = True def __rmatmul__(self, value): ts = self._timer._tick() - if ts not in self._expected: + if self._never: self._timer._error( - f"expected {sorted(self._expected)}, got {ts}" + f"expression annotated with t[never] was evaluated (timestamp {ts})" + ) + elif ts in self._dead: + self._timer._error( + f"timestamp {ts} is marked dead but was evaluated" + ) + elif ts not in self._live: + self._timer._error( + f"expected {sorted(self._live)}, got {ts}" ) return value @@ -68,36 +86,24 @@ def __rmatmul__(self, value): return value -class _NeverCheck: - """Marker returned by t.never — fails if the expression is ever evaluated.""" +class _DeadMarker: + """Marker returned by dead(n) — used inside t[...] to mark a timestamp as dead.""" - def __init__(self, timer): - self._timer = timer + def __init__(self, timestamp): + self.timestamp = timestamp - def __rmatmul__(self, value): - self._timer._error("expression annotated with t.never was evaluated") - return value - - -class _DeadCheck: - """Marker returned by t.dead[n] — fails if the expression is ever evaluated.""" - - def __init__(self, timer): - self._timer = timer - def __rmatmul__(self, value): - self._timer._error("expression annotated with t.dead was evaluated") - return value +def dead(n): + """Mark timestamp `n` as dead code inside a timer subscript: t[dead(1), 2].""" + return _DeadMarker(n) -class _DeadSubscript: - """Subscriptable returned by t.dead — produces _DeadCheck markers.""" +class _NeverSentinel: + """Sentinel for never-evaluated annotations: t[never].""" + pass - def __init__(self, timer): - self._timer = timer - def __getitem__(self, key): - return _DeadCheck(self._timer) +never = _NeverSentinel() class Timer: @@ -113,8 +119,6 @@ def __init__(self, name=""): self._counter = 0 self._errors = [] self._labels = {} - self.dead = _DeadSubscript(self) - self.never = _NeverCheck(self) def __enter__(self): return self @@ -144,7 +148,7 @@ def __getitem__(self, key): if isinstance(key, str): return _Label(self, key) elif isinstance(key, tuple): - return _Check(self, list(key)) + return _Check(self, key) else: return _Check(self, [key]) From 661a77b415a8b614468cc48904e275467ee9fc59 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 28 Apr 2026 14:59:11 +0000 Subject: [PATCH 28/72] Cleanup, printCFG Co-authored-by: yoff --- python/ql/lib/printCfgNew.ql | 45 +++++++++++++++++++ .../controlflow/internal/AstNodeImpl.qll | 43 +++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 python/ql/lib/printCfgNew.ql diff --git a/python/ql/lib/printCfgNew.ql b/python/ql/lib/printCfgNew.ql new file mode 100644 index 000000000000..7c098cbf8f66 --- /dev/null +++ b/python/ql/lib/printCfgNew.ql @@ -0,0 +1,45 @@ +/** + * @name Print CFG (New) + * @description Produces a representation of a file's Control Flow Graph + * using the new shared control flow library. + * This query is used by the VS Code extension. + * @id python/print-cfg + * @kind graph + * @tags ide-contextual-queries/print-cfg + */ + +private import python as Py +import semmle.python.controlflow.internal.AstNodeImpl + +external string selectedSourceFile(); + +private predicate selectedSourceFileAlias = selectedSourceFile/0; + +external int selectedSourceLine(); + +private predicate selectedSourceLineAlias = selectedSourceLine/0; + +external int selectedSourceColumn(); + +private predicate selectedSourceColumnAlias = selectedSourceColumn/0; + +module ViewCfgQueryInput implements ControlFlow::ViewCfgQueryInputSig { + predicate selectedSourceFile = selectedSourceFileAlias/0; + + predicate selectedSourceLine = selectedSourceLineAlias/0; + + predicate selectedSourceColumn = selectedSourceColumnAlias/0; + + predicate cfgScopeSpan( + AstSigImpl::Callable callable, Py::File file, int startLine, int startColumn, int endLine, + int endColumn + ) { + exists(Py::Scope scope | + scope = callable.asScope() and + file = scope.getLocation().getFile() and + scope.getLocation().hasLocationInfo(_, startLine, startColumn, endLine, endColumn) + ) + } +} + +import ControlFlow::ViewCfgQuery diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index c5e2d010688c..15ec5dbfa734 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -11,6 +11,7 @@ private import python as Py private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType +private import codeql.util.Void private module Ast { /** The newtype representing AST nodes for the shared CFG library. */ @@ -717,6 +718,8 @@ module AstSigImpl implements AstSig { index = 0 and result = w.getTest() or index = 1 and result = w.getBody() + or + index = 2 and result = w.getOrelse() ) or // ForStmt (mapped as ForeachStmt): collection (0), variable (1), body (2) @@ -1046,12 +1049,17 @@ module AstSigImpl implements AstSig { Expr getExpr() { result = this.getValue() } } - /** A `raise` statement (mapped to `ThrowStmt`). */ - class ThrowStmt extends Stmt, Ast::RaiseNode { + /** A `raise` statement (mapped to `Throw`). */ + class Throw extends Stmt, Ast::RaiseNode { /** Gets the expression being raised. */ Expr getExpr() { result = this.getException() } } + /** A `goto` statement. Python has no goto. */ + class GotoStmt extends Stmt { + GotoStmt() { none() } + } + // ===== Try/except ===== /** A `try` statement. */ class TryStmt extends Stmt { @@ -1171,6 +1179,26 @@ module AstSigImpl implements AstSig { NullCoalescingExpr() { none() } } + /** An assignment expression. Python has no assignment expressions in the BinaryExpr sense. */ + class Assignment extends BinaryExpr { + Assignment() { none() } + } + + /** A simple assignment expression. */ + class AssignExpr extends Assignment { } + + /** A compound assignment expression. */ + class CompoundAssignment extends Assignment { } + + /** A short-circuiting logical AND compound assignment. Python has no `&&=` operator. */ + class AssignLogicalAndExpr extends CompoundAssignment { } + + /** A short-circuiting logical OR compound assignment. Python has no `||=` operator. */ + class AssignLogicalOrExpr extends CompoundAssignment { } + + /** A short-circuiting null-coalescing compound assignment. Python has no `??=` operator. */ + class AssignNullCoalescingExpr extends CompoundAssignment { } + /** A unary expression. Exists for the `not` subclass. */ class UnaryExpr extends Expr { UnaryExpr() { this instanceof Ast::NotExprNode } @@ -1186,6 +1214,15 @@ module AstSigImpl implements AstSig { /** Gets the boolean value of this literal. */ boolean getValue() { result = this.getBoolValue() } } + + /** A pattern match expression. Python has no `instanceof`-style pattern match expr. */ + class PatternMatchExpr extends Expr { + PatternMatchExpr() { none() } + + Expr getExpr() { none() } + + AstNode getPattern() { none() } + } } private module Cfg0 = Make0; @@ -1209,6 +1246,8 @@ private module Input implements InputSig1, InputSig2 { string toString() { result = "label" } } + class CallableBodyPartContext = Void; + predicate inConditionalContext(AstSigImpl::AstNode n, ConditionKind kind) { kind.isBoolean() and n = any(Ast::AssertNode a).getTest() From 0dabf473446df4657f5a3604991a4b94f21bfa61 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 4 May 2026 15:31:24 +0200 Subject: [PATCH 29/72] Python: add pattern nodes Co-authored-by: Copilot --- .../controlflow/internal/AstNodeImpl.qll | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 15ec5dbfa734..d6ac7ceb1109 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -29,7 +29,8 @@ private module Ast { * Only created for inner pairs (index >= 1); the outermost pair (index 0) * is represented by the original `BoolExpr` node via `TExprNode`. */ - TBoolExprPair(Py::BoolExpr be, int index) { index = [1 .. count(be.getAValue()) - 2] } + TBoolExprPair(Py::BoolExpr be, int index) { index = [1 .. count(be.getAValue()) - 2] } or + TPatternNode(Py::Pattern p) /** * An AST node for the shared CFG. Each branch of the newtype gets a @@ -122,6 +123,21 @@ private module Ast { } } + class PatternNode extends Node, TPatternNode { + private Py::Pattern pattern; + + PatternNode() { this = TPatternNode(pattern) } + + /** Gets the underlying Python pattern. */ + Py::Pattern asPattern() { result = pattern } + + override string toString() { result = pattern.toString() } + + override Py::Location getLocation() { result = pattern.getLocation() } + + override ScopeNode getEnclosingScope() { result.asScope() = pattern.getScope() } + } + /** An `if` statement. */ class IfNode extends StmtNode { private Py::If ifStmt; @@ -289,6 +305,8 @@ private module Ast { CaseNode() { caseStmt = this.asStmt() } + PatternNode getPattern() { result.asPattern() = caseStmt.getPattern() } + ExprNode getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } StmtListNode getBody() { result.asStmtList() = caseStmt.getBody() } @@ -778,9 +796,11 @@ module AstSigImpl implements AstSig { or // Case: guard (0), body (1) exists(Ast::CaseNode c | c = n | - index = 0 and result = c.getGuard() + index = 0 and result = c.getPattern() + or + index = 1 and result = c.getGuard() or - index = 1 and result = c.getBody() + index = 2 and result = c.getBody() ) or // CatchClause (except handler): type (0), name (1), body (2) @@ -1101,7 +1121,7 @@ module AstSigImpl implements AstSig { class Case extends Stmt { Case() { this instanceof Ast::CaseNode } - AstNode getAPattern() { none() } + AstNode getAPattern() { result = this.(Ast::CaseNode).getPattern() } Expr getGuard() { result = this.(Ast::CaseNode).getGuard() } From 7264483e59eed958535d04fc197f4075565386b8 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 4 May 2026 15:34:33 +0200 Subject: [PATCH 30/72] python: add consistency checks Co-authored-by: aschackmull --- python/ql/consistency-queries/CfgConsistency.ql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 python/ql/consistency-queries/CfgConsistency.ql diff --git a/python/ql/consistency-queries/CfgConsistency.ql b/python/ql/consistency-queries/CfgConsistency.ql new file mode 100644 index 000000000000..ab13eddf190c --- /dev/null +++ b/python/ql/consistency-queries/CfgConsistency.ql @@ -0,0 +1,2 @@ +import semmle.python.controlflow.internal.AstNodeImpl +import ControlFlow::Consistency From 2de3733fe34e295697267d43c5314fd79900cfb7 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 13:45:50 +0000 Subject: [PATCH 31/72] Python: collapse two-layer AstNodeImpl into a single Ast module Merge the previous `Ast` and `AstSigImpl` modules into a single `module Ast implements AstSig`. Classes now use the signature names (IfStmt, WhileStmt, ForeachStmt, etc.) and signature predicates (getCondition, getThen, getElse, etc.) directly, with no intermediate renaming layer. Drop the TStmtListNode newtype branch entirely. Replace it with a synthetic TBlockStmt(parent, slot) keyed by a parent AST node and a slot label string ('body', 'orelse', 'finally'). Py::StmtList no longer appears in the newtype; the BlockStmt class provides indexed access to the underlying body items via getStmt(n). All 22 of 24 evaluation-order tests still pass; the same 2 comprehension-related failures predate this refactor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/ql/lib/printCfgNew.ql | 2 +- .../controlflow/internal/AstNodeImpl.qll | 1476 ++++++++--------- 2 files changed, 664 insertions(+), 814 deletions(-) diff --git a/python/ql/lib/printCfgNew.ql b/python/ql/lib/printCfgNew.ql index 7c098cbf8f66..ba336de562a7 100644 --- a/python/ql/lib/printCfgNew.ql +++ b/python/ql/lib/printCfgNew.ql @@ -31,7 +31,7 @@ module ViewCfgQueryInput implements ControlFlow::ViewCfgQueryInputSig predicate selectedSourceColumn = selectedSourceColumnAlias/0; predicate cfgScopeSpan( - AstSigImpl::Callable callable, Py::File file, int startLine, int startColumn, int endLine, + Ast::Callable callable, Py::File file, int startLine, int startColumn, int endLine, int endColumn ) { exists(Py::Scope scope | diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d6ac7ceb1109..8a86ffa08748 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -1,11 +1,13 @@ /** - * Provides a newtype-based interface layer that mediates between the existing - * Python AST classes and the shared control-flow library's `AstSig` signature. + * Provides classes for the shared control-flow library, mediating between + * the Python AST and `AstSig`. * - * The newtype unifies Python's `Stmt`, `Expr`, `Scope`, and `StmtList` into a - * single `AstNode` type. Notably, `StmtList` (which is not an `AstNode` in the - * existing Python AST) is wrapped as a `BlockStmt` (a subtype of `Stmt`), - * since the shared CFG library expects statement blocks to be statements. + * The `Ast` module wraps Python's `Stmt`, `Expr`, `Scope`, and `Pattern`, + * and adds two synthetic kinds of node: + * - `BlockStmt`, identifying a body slot of a parent AST node (e.g. an + * `if`'s then or else branch). `Py::StmtList` itself is not directly + * wrapped. + * - Intermediate nodes for multi-operand boolean expressions. */ private import python as Py @@ -13,465 +15,719 @@ private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType private import codeql.util.Void -private module Ast { - /** The newtype representing AST nodes for the shared CFG library. */ +/** Provides the Python implementation of the shared CFG `AstSig`. */ +module Ast implements AstSig { + /** + * Maps a `(parent, slot)` pair to the `Py::StmtList` that holds the items + * of the `BlockStmt` for that slot. The slot string distinguishes between + * the multiple bodies that some parents have (e.g. `if` has `body` and + * `orelse`). + */ + private Py::StmtList getBodyStmtList(Py::AstNode parent, string slot) { + result = parent.(Py::Scope).getBody() and slot = "body" + or + result = parent.(Py::If).getBody() and slot = "body" + or + result = parent.(Py::If).getOrelse() and slot = "orelse" + or + result = parent.(Py::While).getBody() and slot = "body" + or + result = parent.(Py::While).getOrelse() and slot = "orelse" + or + result = parent.(Py::For).getBody() and slot = "body" + or + result = parent.(Py::For).getOrelse() and slot = "orelse" + or + result = parent.(Py::With).getBody() and slot = "body" + or + result = parent.(Py::Try).getBody() and slot = "body" + or + result = parent.(Py::Try).getOrelse() and slot = "orelse" + or + result = parent.(Py::Try).getFinalbody() and slot = "finally" + or + result = parent.(Py::Case).getBody() and slot = "body" + or + result = parent.(Py::ExceptStmt).getBody() and slot = "body" + or + result = parent.(Py::ExceptGroupStmt).getBody() and slot = "body" + } + private newtype TAstNode = - TStmtNode(Py::Stmt s) or - TExprNode(Py::Expr e) or - TScopeNode(Py::Scope sc) or - TStmtListNode(Py::StmtList sl) or + TStmt(Py::Stmt s) or + TExpr(Py::Expr e) or + TScope(Py::Scope sc) or + TPattern(Py::Pattern p) or /** - * A synthetic node representing an intermediate pair in a multi-operand - * `and`/`or` expression. For `a and b and c` (values 0,1,2), we - * synthesize a right-nested tree: the pair at index 1 represents - * `b and c`, which becomes the right operand of the outermost pair. - * - * Only created for inner pairs (index >= 1); the outermost pair (index 0) - * is represented by the original `BoolExpr` node via `TExprNode`. + * A synthetic intermediate node in a multi-operand `and`/`or` + * expression. For `a and b and c` (operands 0, 1, 2) we model the + * operation as a right-nested tree where the inner pair at index 1 + * represents `b and c` and is the right operand of the outer pair. + * The outermost pair (index 0) is represented by the underlying + * `Py::BoolExpr` itself via `TExpr`. */ TBoolExprPair(Py::BoolExpr be, int index) { index = [1 .. count(be.getAValue()) - 2] } or - TPatternNode(Py::Pattern p) - - /** - * An AST node for the shared CFG. Each branch of the newtype gets a - * subclass that overrides `toString` and `getLocation`. - */ - class Node extends TAstNode { - string toString() { none() } - - Py::Location getLocation() { none() } + /** + * A synthetic block statement, identifying one body slot of the + * `parent` AST node. The `slot` string disambiguates among multiple + * bodies of the same parent (`"body"`, `"orelse"`, `"finally"`). + */ + TBlockStmt(Py::AstNode parent, string slot) { exists(getBodyStmtList(parent, slot)) } - /** Gets the enclosing scope of this node, if any. */ - ScopeNode getEnclosingScope() { none() } - } + /** An AST node visible to the shared CFG. */ + class AstNode extends TAstNode { + /** Gets a textual representation of this AST node. */ + string toString() { + exists(Py::Stmt s | this = TStmt(s) and result = s.toString()) + or + exists(Py::Expr e | this = TExpr(e) and result = e.toString()) + or + exists(Py::Scope sc | this = TScope(sc) and result = sc.toString()) + or + exists(Py::Pattern p | this = TPattern(p) and result = p.toString()) + or + exists(Py::BoolExpr be | this = TBoolExprPair(be, _) and result = be.getOperator()) + or + exists(string slot | this = TBlockStmt(_, slot) and result = "block:" + slot) + } - class StmtNode extends Node, TStmtNode { - private Py::Stmt stmt; + /** Gets the location of this AST node. */ + Py::Location getLocation() { + exists(Py::Stmt s | this = TStmt(s) and result = s.getLocation()) + or + exists(Py::Expr e | this = TExpr(e) and result = e.getLocation()) + or + exists(Py::Scope sc | this = TScope(sc) and result = sc.getLocation()) + or + exists(Py::Pattern p | this = TPattern(p) and result = p.getLocation()) + or + exists(Py::BoolExpr be, int index | + this = TBoolExprPair(be, index) and result = be.getValue(index).getLocation() + ) + or + // BlockStmt has no native location; approximate with the first + // item's location. + exists(Py::AstNode parent, string slot | + this = TBlockStmt(parent, slot) and + result = getBodyStmtList(parent, slot).getItem(0).getLocation() + ) + } - StmtNode() { this = TStmtNode(stmt) } + /** Gets the enclosing callable that contains this node, if any. */ + Callable getEnclosingCallable() { + exists(Py::Stmt s | this = TStmt(s) and result.asScope() = s.getScope()) + or + exists(Py::Expr e | this = TExpr(e) and result.asScope() = e.getScope()) + or + exists(Py::Scope sc | this = TScope(sc) and result.asScope() = sc.getEnclosingScope()) + or + exists(Py::Pattern p | this = TPattern(p) and result.asScope() = p.getScope()) + or + exists(Py::BoolExpr be | this = TBoolExprPair(be, _) and result.asScope() = be.getScope()) + or + exists(Py::AstNode parent | this = TBlockStmt(parent, _) | + result.asScope() = parent.(Py::Scope) + or + result.asScope() = parent.(Py::Stmt).getScope() + ) + } - /** Gets the underlying Python statement. */ - Py::Stmt asStmt() { result = stmt } + /** Gets the underlying Python `Stmt`, if this node wraps one. */ + Py::Stmt asStmt() { this = TStmt(result) } - override string toString() { result = stmt.toString() } + /** Gets the underlying Python `Expr`, if this node wraps one. */ + Py::Expr asExpr() { this = TExpr(result) } - override Py::Location getLocation() { result = stmt.getLocation() } + /** Gets the underlying Python `Scope`, if this node wraps one. */ + Py::Scope asScope() { this = TScope(result) } - /** Gets the enclosing scope of this statement. */ - override ScopeNode getEnclosingScope() { result.asScope() = stmt.getScope() } + /** Gets the underlying Python `Pattern`, if this node wraps one. */ + Py::Pattern asPattern() { this = TPattern(result) } } - class ExprNode extends Node, TExprNode { - private Py::Expr expr; - - ExprNode() { this = TExprNode(expr) } - - /** Gets the underlying Python expression. */ - Py::Expr asExpr() { result = expr } + /** Gets the immediately enclosing callable that contains `node`. */ + Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } - override string toString() { result = expr.toString() } + /** + * A callable: a function, class, or module scope. + * + * In Python, all three are executable scopes with statement bodies. + */ + class Callable extends AstNode, TScope { } - override Py::Location getLocation() { result = expr.getLocation() } + /** Gets the body of callable `c`. */ + AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope(), "body") } - /** Gets the enclosing scope of this expression. */ - override ScopeNode getEnclosingScope() { result.asScope() = expr.getScope() } + /** A statement. */ + class Stmt extends AstNode { + Stmt() { this instanceof TStmt or this instanceof TBlockStmt } } - class ScopeNode extends Node, TScopeNode { - private Py::Scope scope; - - ScopeNode() { this = TScopeNode(scope) } + /** An expression. */ + class Expr extends AstNode { + Expr() { this instanceof TExpr or this instanceof TBoolExprPair } + } - /** Gets the underlying Python scope. */ - Py::Scope asScope() { result = scope } + /** A pattern in a `match` statement. */ + additional class Pattern extends AstNode, TPattern { } - override string toString() { result = scope.toString() } + /** + * A block statement, modeling the body of a parent AST node as a + * sequence of statements. + */ + class BlockStmt extends Stmt, TBlockStmt { + private Py::AstNode parent; + private string slot; - override Py::Location getLocation() { result = scope.getLocation() } + BlockStmt() { this = TBlockStmt(parent, slot) } - /** Gets the body of this scope. */ - StmtListNode getBody() { result.asStmtList() = scope.getBody() } + /** Gets the `n`th (zero-based) statement in this block. */ + Stmt getStmt(int n) { result = TStmt(getBodyStmtList(parent, slot).getItem(n)) } - /** Gets the enclosing scope of this scope, if any. */ - override ScopeNode getEnclosingScope() { result.asScope() = scope.getEnclosingScope() } + /** Gets the last statement in this block. */ + Stmt getLastStmt() { result = TStmt(getBodyStmtList(parent, slot).getLastItem()) } } - class StmtListNode extends Node, TStmtListNode { - private Py::StmtList stmtList; + /** An expression statement. */ + class ExprStmt extends Stmt { + private Py::ExprStmt exprStmt; - StmtListNode() { this = TStmtListNode(stmtList) } + ExprStmt() { exprStmt = this.asStmt() } - /** Gets the underlying Python statement list. */ - Py::StmtList asStmtList() { result = stmtList } + /** Gets the expression in this expression statement. */ + Expr getExpr() { result = TExpr(exprStmt.getValue()) } + } - override string toString() { result = stmtList.toString() } + /** An assignment statement (`x = y = expr`). */ + additional class AssignStmt extends Stmt { + private Py::Assign assign; - // StmtList has no native location; approximate with first item's location. - override Py::Location getLocation() { result = stmtList.getItem(0).getLocation() } + AssignStmt() { assign = this.asStmt() } - /** Gets the `n`th (zero-based) statement in this block. */ - StmtNode getItem(int n) { result.asStmt() = stmtList.getItem(n) } + Expr getValue() { result = TExpr(assign.getValue()) } - /** Gets the last statement in this block. */ - StmtNode getLastItem() { result.asStmt() = stmtList.getLastItem() } + Expr getTarget(int n) { result = TExpr(assign.getTarget(n)) } - /** Gets the enclosing scope of this statement list. */ - override ScopeNode getEnclosingScope() { - result.asScope() = stmtList.getParent().(Py::Scope) - or - result.asScope() = stmtList.getParent().(Py::Stmt).getScope() - } + int getNumberOfTargets() { result = count(assign.getATarget()) } } - class PatternNode extends Node, TPatternNode { - private Py::Pattern pattern; + /** An augmented assignment statement (`x += expr`). */ + additional class AugAssignStmt extends Stmt { + private Py::AugAssign augAssign; + + AugAssignStmt() { augAssign = this.asStmt() } - PatternNode() { this = TPatternNode(pattern) } + Expr getOperation() { result = TExpr(augAssign.getOperation()) } + } - /** Gets the underlying Python pattern. */ - Py::Pattern asPattern() { result = pattern } + /** An assignment expression / walrus operator (`x := expr`). */ + additional class NamedExpr extends Expr { + private Py::AssignExpr assignExpr; - override string toString() { result = pattern.toString() } + NamedExpr() { assignExpr = this.asExpr() } - override Py::Location getLocation() { result = pattern.getLocation() } + Expr getValue() { result = TExpr(assignExpr.getValue()) } - override ScopeNode getEnclosingScope() { result.asScope() = pattern.getScope() } + Expr getTarget() { result = TExpr(assignExpr.getTarget()) } } - /** An `if` statement. */ - class IfNode extends StmtNode { + /** + * An `if` statement. + * + * Python's `elif` chains are represented as nested `If` nodes in the + * else branch's `StmtList`. The shared CFG library handles this + * naturally: `getElse()` returns the `BlockStmt` wrapping the else + * branch, and if that block contains a single `If`, the result is + * a chained conditional. + */ + class IfStmt extends Stmt { private Py::If ifStmt; - IfNode() { ifStmt = this.asStmt() } + IfStmt() { ifStmt = this.asStmt() } + + /** Gets the underlying Python `If` statement. */ + Py::If asIf() { result = ifStmt } /** Gets the condition of this `if` statement. */ - ExprNode getTest() { result.asExpr() = ifStmt.getTest() } + Expr getCondition() { result = TExpr(ifStmt.getTest()) } - /** Gets the if-true branch. */ - StmtListNode getBody() { result.asStmtList() = ifStmt.getBody() } + /** Gets the `then` (true) branch of this `if` statement. */ + Stmt getThen() { result = TBlockStmt(ifStmt, "body") } - /** Gets the if-false branch, if any. */ - StmtListNode getOrelse() { result.asStmtList() = ifStmt.getOrelse() } + /** Gets the `else` (false) branch, if any. */ + Stmt getElse() { result = TBlockStmt(ifStmt, "orelse") } } - /** An expression statement. */ - class ExprStmtNode extends StmtNode { - private Py::ExprStmt exprStmt; - - ExprStmtNode() { exprStmt = this.asStmt() } + /** A loop statement. */ + class LoopStmt extends Stmt { + LoopStmt() { this.asStmt() instanceof Py::While or this.asStmt() instanceof Py::For } - /** Gets the expression in this statement. */ - ExprNode getValue() { result.asExpr() = exprStmt.getValue() } + /** Gets the body of this loop statement. */ + Stmt getBody() { none() } } - /** An assignment statement (`x = y = expr`). */ - class AssignNode extends StmtNode { - private Py::Assign assign; + /** A `while` loop statement. */ + class WhileStmt extends LoopStmt { + private Py::While whileStmt; - AssignNode() { assign = this.asStmt() } + WhileStmt() { whileStmt = this.asStmt() } - ExprNode getValue() { result.asExpr() = assign.getValue() } + /** Gets the boolean condition of this `while` loop. */ + Expr getCondition() { result = TExpr(whileStmt.getTest()) } - ExprNode getTarget(int n) { result.asExpr() = assign.getTarget(n) } + override Stmt getBody() { result = TBlockStmt(whileStmt, "body") } - int getNumberOfTargets() { result = count(assign.getATarget()) } + /** Gets the `else` branch of this `while` loop, if any. */ + Stmt getElse() { result = TBlockStmt(whileStmt, "orelse") } } - /** An augmented assignment statement (`x += expr`). */ - class AugAssignNode extends StmtNode { - private Py::AugAssign augAssign; - - AugAssignNode() { augAssign = this.asStmt() } + /** + * A `do-while` loop statement. Python has no do-while construct. + */ + class DoStmt extends LoopStmt { + DoStmt() { none() } - ExprNode getOperation() { result.asExpr() = augAssign.getOperation() } + Expr getCondition() { none() } } - /** An assignment expression / walrus operator (`x := expr`). */ - class AssignExprNode extends ExprNode { - private Py::AssignExpr assignExpr; + /** A C-style `for` loop. Python has no C-style for loop. */ + class ForStmt extends LoopStmt { + ForStmt() { none() } - AssignExprNode() { assignExpr = this.asExpr() } + Expr getInit(int index) { none() } - ExprNode getValue() { result.asExpr() = assignExpr.getValue() } + Expr getCondition() { none() } - ExprNode getTarget() { result.asExpr() = assignExpr.getTarget() } + Expr getUpdate(int index) { none() } } - /** A `while` statement. */ - class WhileNode extends StmtNode { - private Py::While whileStmt; - - WhileNode() { whileStmt = this.asStmt() } - - ExprNode getTest() { result.asExpr() = whileStmt.getTest() } + /** A for-each loop (`for x in iterable:`). */ + class ForeachStmt extends LoopStmt { + private Py::For forStmt; - StmtListNode getBody() { result.asStmtList() = whileStmt.getBody() } + ForeachStmt() { forStmt = this.asStmt() } - StmtListNode getOrelse() { result.asStmtList() = whileStmt.getOrelse() } - } + /** Gets the loop variable. */ + Expr getVariable() { result = TExpr(forStmt.getTarget()) } - /** A `for` statement. */ - class ForNode extends StmtNode { - private Py::For forStmt; + /** Gets the collection being iterated. */ + Expr getCollection() { result = TExpr(forStmt.getIter()) } - ForNode() { forStmt = this.asStmt() } + override Stmt getBody() { result = TBlockStmt(forStmt, "body") } - ExprNode getTarget() { result.asExpr() = forStmt.getTarget() } + /** Gets the `else` branch of this `for` loop, if any. */ + Stmt getElse() { result = TBlockStmt(forStmt, "orelse") } + } - ExprNode getIter() { result.asExpr() = forStmt.getIter() } + /** A `break` statement. */ + class BreakStmt extends Stmt { + BreakStmt() { this.asStmt() instanceof Py::Break } + } - StmtListNode getBody() { result.asStmtList() = forStmt.getBody() } + /** A `continue` statement. */ + class ContinueStmt extends Stmt { + ContinueStmt() { this.asStmt() instanceof Py::Continue } + } - StmtListNode getOrelse() { result.asStmtList() = forStmt.getOrelse() } + /** A `goto` statement. Python has no goto. */ + class GotoStmt extends Stmt { + GotoStmt() { none() } } /** A `return` statement. */ - class ReturnNode extends StmtNode { + class ReturnStmt extends Stmt { private Py::Return ret; - ReturnNode() { ret = this.asStmt() } + ReturnStmt() { ret = this.asStmt() } - ExprNode getValue() { result.asExpr() = ret.getValue() } + /** Gets the expression being returned, if any. */ + Expr getExpr() { result = TExpr(ret.getValue()) } } - /** A `raise` statement. */ - class RaiseNode extends StmtNode { + /** A `raise` statement (mapped to `Throw`). */ + class Throw extends Stmt { private Py::Raise raise; - RaiseNode() { raise = this.asStmt() } + Throw() { raise = this.asStmt() } - ExprNode getException() { result.asExpr() = raise.getException() } + /** Gets the expression being raised. */ + Expr getExpr() { result = TExpr(raise.getException()) } - ExprNode getCause() { result.asExpr() = raise.getCause() } + /** Gets the cause of this `raise`, if any. */ + Expr getCause() { result = TExpr(raise.getCause()) } } /** A `with` statement. */ - class WithNode extends StmtNode { + additional class WithStmt extends Stmt { private Py::With withStmt; - WithNode() { withStmt = this.asStmt() } - - ExprNode getContextExpr() { result.asExpr() = withStmt.getContextExpr() } - - ExprNode getOptionalVars() { result.asExpr() = withStmt.getOptionalVars() } + WithStmt() { withStmt = this.asStmt() } - StmtListNode getBody() { result.asStmtList() = withStmt.getBody() } - } + Expr getContextExpr() { result = TExpr(withStmt.getContextExpr()) } - /** A `break` statement. */ - class BreakNode extends StmtNode { - BreakNode() { this.asStmt() instanceof Py::Break } - } + Expr getOptionalVars() { result = TExpr(withStmt.getOptionalVars()) } - /** A `continue` statement. */ - class ContinueNode extends StmtNode { - ContinueNode() { this.asStmt() instanceof Py::Continue } + Stmt getBody() { result = TBlockStmt(withStmt, "body") } } /** An `assert` statement. */ - class AssertNode extends StmtNode { + additional class AssertStmt extends Stmt { private Py::Assert assertStmt; - AssertNode() { assertStmt = this.asStmt() } + AssertStmt() { assertStmt = this.asStmt() } - ExprNode getTest() { result.asExpr() = assertStmt.getTest() } + Expr getTest() { result = TExpr(assertStmt.getTest()) } - ExprNode getMsg() { result.asExpr() = assertStmt.getMsg() } + Expr getMsg() { result = TExpr(assertStmt.getMsg()) } } /** A `delete` statement. */ - class DeleteNode extends StmtNode { + additional class DeleteStmt extends Stmt { private Py::Delete del; - DeleteNode() { del = this.asStmt() } + DeleteStmt() { del = this.asStmt() } + + Expr getTarget(int n) { result = TExpr(del.getTarget(n)) } + } + + /** A `try` statement. */ + class TryStmt extends Stmt { + private Py::Try tryStmt; + + TryStmt() { tryStmt = this.asStmt() } + + Stmt getBody() { result = TBlockStmt(tryStmt, "body") } + + /** Gets the `else` branch of this `try` statement, if any. */ + Stmt getElse() { result = TBlockStmt(tryStmt, "orelse") } + + Stmt getFinally() { result = TBlockStmt(tryStmt, "finally") } - ExprNode getTarget(int n) { result.asExpr() = del.getTarget(n) } + CatchClause getCatch(int index) { result = TStmt(tryStmt.getHandler(index)) } } - /** A `match` statement. */ - class MatchStmtNode extends StmtNode { + /** + * Gets the `else` branch of `try` statement `try`, if any. + */ + AstNode getTryElse(TryStmt try) { result = try.getElse() } + + /** An exception handler (`except` or `except*`). */ + class CatchClause extends Stmt { + private Py::ExceptionHandler handler; + + CatchClause() { handler = this.asStmt() } + + /** Gets the type expression of this exception handler. */ + Expr getType() { result = TExpr(handler.getType()) } + + /** Gets the variable name of this exception handler, if any. */ + AstNode getVariable() { result = TExpr(handler.getName()) } + + /** Holds: catch clauses do not have a `Condition` in Python's model. */ + Expr getCondition() { none() } + + /** Gets the body of this exception handler. */ + Stmt getBody() { + result = TBlockStmt(handler.(Py::ExceptStmt), "body") + or + result = TBlockStmt(handler.(Py::ExceptGroupStmt), "body") + } + } + + /** A `match` statement, mapped to the shared CFG's `Switch`. */ + class Switch extends Stmt { private Py::MatchStmt matchStmt; - MatchStmtNode() { matchStmt = this.asStmt() } + Switch() { matchStmt = this.asStmt() } + + Expr getExpr() { result = TExpr(matchStmt.getSubject()) } - ExprNode getSubject() { result.asExpr() = matchStmt.getSubject() } + Case getCase(int index) { result = TStmt(matchStmt.getCase(index)) } - CaseNode getCase(int n) { result.asStmt() = matchStmt.getCase(n) } + Stmt getStmt(int index) { none() } } /** A `case` clause in a match statement. */ - class CaseNode extends StmtNode { + class Case extends Stmt { private Py::Case caseStmt; - CaseNode() { caseStmt = this.asStmt() } + Case() { caseStmt = this.asStmt() } - PatternNode getPattern() { result.asPattern() = caseStmt.getPattern() } + AstNode getAPattern() { result = TPattern(caseStmt.getPattern()) } - ExprNode getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } + Expr getGuard() { result = TExpr(caseStmt.getGuard().(Py::Guard).getTest()) } - StmtListNode getBody() { result.asStmtList() = caseStmt.getBody() } + AstNode getBody() { result = TBlockStmt(caseStmt, "body") } + /** Holds if this case is a wildcard pattern (`case _:`). */ predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } } - /** A `try` statement. */ - class TryNode extends StmtNode { - private Py::Try tryStmt; + /** A wildcard case (`case _:`). */ + class DefaultCase extends Case { + DefaultCase() { this.isWildcard() } + } - TryNode() { tryStmt = this.asStmt() } + /** A conditional expression (`x if cond else y`). */ + class ConditionalExpr extends Expr { + private Py::IfExp ifExp; - StmtListNode getBody() { result.asStmtList() = tryStmt.getBody() } + ConditionalExpr() { ifExp = this.asExpr() } - StmtListNode getOrelse() { result.asStmtList() = tryStmt.getOrelse() } + /** Gets the condition of this expression. */ + Expr getCondition() { result = TExpr(ifExp.getTest()) } - StmtListNode getFinalbody() { result.asStmtList() = tryStmt.getFinalbody() } + /** Gets the true branch of this expression. */ + Expr getThen() { result = TExpr(ifExp.getBody()) } - ExceptionHandlerNode getHandler(int i) { result.asStmt() = tryStmt.getHandler(i) } + /** Gets the false branch of this expression. */ + Expr getElse() { result = TExpr(ifExp.getOrelse()) } } - /** An exception handler (`except` or `except*`). */ - class ExceptionHandlerNode extends StmtNode { - private Py::ExceptionHandler handler; + /** + * A binary expression for the shared CFG. In Python, this covers + * `and`/`or` expressions (both real 2-operand and synthetic pairs). + */ + class BinaryExpr extends Expr { + BinaryExpr() { + exists(Py::BoolExpr be | this = TExpr(be) and count(be.getAValue()) >= 2) + or + this instanceof TBoolExprPair + } - ExceptionHandlerNode() { handler = this.asStmt() } + /** Gets the left operand of this binary expression. */ + Expr getLeftOperand() { + exists(Py::BoolExpr be | this = TExpr(be) and result = TExpr(be.getValue(0))) + or + exists(Py::BoolExpr be, int i | + this = TBoolExprPair(be, i) and result = TExpr(be.getValue(i)) + ) + } - ExprNode getType() { result.asExpr() = handler.getType() } + /** Gets the right operand of this binary expression. */ + Expr getRightOperand() { + // 2-operand BoolExpr: right operand is value(1). + exists(Py::BoolExpr be | + this = TExpr(be) and + count(be.getAValue()) = 2 and + result = TExpr(be.getValue(1)) + ) + or + // 3+ operand BoolExpr (outermost): right operand is the synthetic + // pair at index 1. + exists(Py::BoolExpr be | + this = TExpr(be) and + count(be.getAValue()) > 2 and + result = TBoolExprPair(be, 1) + ) + or + // Last synthetic pair: right operand is the final value. + exists(Py::BoolExpr be, int i, int n | + this = TBoolExprPair(be, i) and + n = count(be.getAValue()) and + i = n - 2 and + result = TExpr(be.getValue(i + 1)) + ) + or + // Non-last synthetic pair: right operand is the next pair. + exists(Py::BoolExpr be, int i, int n | + this = TBoolExprPair(be, i) and + n = count(be.getAValue()) and + i < n - 2 and + result = TBoolExprPair(be, i + 1) + ) + } + } - ExprNode getName() { result.asExpr() = handler.getName() } + /** A short-circuiting logical `and` expression. */ + class LogicalAndExpr extends BinaryExpr { + LogicalAndExpr() { + exists(Py::BoolExpr be | + be.getOp() instanceof Py::And and + (this = TExpr(be) or this = TBoolExprPair(be, _)) + ) + } + } - StmtListNode getBody() { - result.asStmtList() = handler.(Py::ExceptStmt).getBody() or - result.asStmtList() = handler.(Py::ExceptGroupStmt).getBody() + /** A short-circuiting logical `or` expression. */ + class LogicalOrExpr extends BinaryExpr { + LogicalOrExpr() { + exists(Py::BoolExpr be | + be.getOp() instanceof Py::Or and + (this = TExpr(be) or this = TBoolExprPair(be, _)) + ) } } - /** A conditional expression (`x if cond else y`). */ - class IfExpNode extends ExprNode { - private Py::IfExp ifExp; + /** A null-coalescing expression. Python has no null-coalescing operator. */ + class NullCoalescingExpr extends BinaryExpr { + NullCoalescingExpr() { none() } + } + + /** + * A unary expression. Currently only used for the `not` subclass. + */ + class UnaryExpr extends Expr { + UnaryExpr() { this.asExpr().(Py::UnaryExpr).getOp() instanceof Py::Not } + + /** Gets the operand of this unary expression. */ + Expr getOperand() { result = TExpr(this.asExpr().(Py::UnaryExpr).getOperand()) } + } + + /** A logical `not` expression. */ + class LogicalNotExpr extends UnaryExpr { } + + /** An assignment expression. Python's walrus is modelled separately. */ + class Assignment extends BinaryExpr { + Assignment() { none() } + } + + class AssignExpr extends Assignment { } + + class CompoundAssignment extends Assignment { } + + class AssignLogicalAndExpr extends CompoundAssignment { } + + class AssignLogicalOrExpr extends CompoundAssignment { } + + class AssignNullCoalescingExpr extends CompoundAssignment { } + + /** A boolean literal expression (`True` or `False`). */ + class BooleanLiteral extends Expr { + BooleanLiteral() { this.asExpr() instanceof Py::True or this.asExpr() instanceof Py::False } - IfExpNode() { ifExp = this.asExpr() } + /** Gets the boolean value of this literal. */ + boolean getValue() { + this.asExpr() instanceof Py::True and result = true + or + this.asExpr() instanceof Py::False and result = false + } + } - ExprNode getTest() { result.asExpr() = ifExp.getTest() } + /** A pattern match expression. Python has no `instanceof`-style pattern match expression. */ + class PatternMatchExpr extends Expr { + PatternMatchExpr() { none() } - ExprNode getBody() { result.asExpr() = ifExp.getBody() } + Expr getExpr() { none() } - ExprNode getOrelse() { result.asExpr() = ifExp.getOrelse() } + AstNode getPattern() { none() } } + // ===== Python-specific expression classes (used by `getChild`) ===== /** A Python binary expression (arithmetic, bitwise, matmul, etc.). */ - class BinaryExprNode extends ExprNode { + additional class ArithBinaryExpr extends Expr { private Py::BinaryExpr binExpr; - BinaryExprNode() { binExpr = this.asExpr() } + ArithBinaryExpr() { binExpr = this.asExpr() } - ExprNode getLeft() { result.asExpr() = binExpr.getLeft() } + Expr getLeft() { result = TExpr(binExpr.getLeft()) } - ExprNode getRight() { result.asExpr() = binExpr.getRight() } + Expr getRight() { result = TExpr(binExpr.getRight()) } } /** A call expression (`func(args...)`). */ - class CallNode extends ExprNode { + additional class CallExpr extends Expr { private Py::Call call; - CallNode() { call = this.asExpr() } + CallExpr() { call = this.asExpr() } - ExprNode getFunc() { result.asExpr() = call.getFunc() } + Expr getFunc() { result = TExpr(call.getFunc()) } - ExprNode getPositionalArg(int n) { result.asExpr() = call.getPositionalArg(n) } + Expr getPositionalArg(int n) { result = TExpr(call.getPositionalArg(n)) } int getNumberOfPositionalArgs() { result = count(call.getAPositionalArg()) } - ExprNode getKeywordValue(int n) { - result.asExpr() = call.getNamedArg(n).(Py::Keyword).getValue() + Expr getKeywordValue(int n) { + result = TExpr(call.getNamedArg(n).(Py::Keyword).getValue()) or - result.asExpr() = call.getNamedArg(n).(Py::DictUnpacking).getValue() + result = TExpr(call.getNamedArg(n).(Py::DictUnpacking).getValue()) } int getNumberOfNamedArgs() { result = count(call.getANamedArg()) } } /** A subscript expression (`obj[index]`). */ - class SubscriptNode extends ExprNode { + additional class SubscriptExpr extends Expr { private Py::Subscript sub; - SubscriptNode() { sub = this.asExpr() } + SubscriptExpr() { sub = this.asExpr() } - ExprNode getObject() { result.asExpr() = sub.getObject() } + Expr getObject() { result = TExpr(sub.getObject()) } - ExprNode getIndex() { result.asExpr() = sub.getIndex() } + Expr getIndex() { result = TExpr(sub.getIndex()) } } /** An attribute access (`obj.name`). */ - class AttributeNode extends ExprNode { + additional class AttributeExpr extends Expr { private Py::Attribute attr; - AttributeNode() { attr = this.asExpr() } + AttributeExpr() { attr = this.asExpr() } - ExprNode getObject() { result.asExpr() = attr.getObject() } + Expr getObject() { result = TExpr(attr.getObject()) } } /** A tuple literal. */ - class TupleNode extends ExprNode { + additional class TupleExpr extends Expr { private Py::Tuple tuple; - TupleNode() { tuple = this.asExpr() } + TupleExpr() { tuple = this.asExpr() } - ExprNode getElt(int n) { result.asExpr() = tuple.getElt(n) } + Expr getElt(int n) { result = TExpr(tuple.getElt(n)) } } /** A list literal. */ - class ListNode extends ExprNode { + additional class ListExpr extends Expr { private Py::List list; - ListNode() { list = this.asExpr() } + ListExpr() { list = this.asExpr() } - ExprNode getElt(int n) { result.asExpr() = list.getElt(n) } + Expr getElt(int n) { result = TExpr(list.getElt(n)) } } /** A set literal. */ - class SetNode extends ExprNode { + additional class SetExpr extends Expr { private Py::Set set; - SetNode() { set = this.asExpr() } + SetExpr() { set = this.asExpr() } - ExprNode getElt(int n) { result.asExpr() = set.getElt(n) } + Expr getElt(int n) { result = TExpr(set.getElt(n)) } } /** A dict literal. */ - class DictNode extends ExprNode { + additional class DictExpr extends Expr { private Py::Dict dict; - DictNode() { dict = this.asExpr() } + DictExpr() { dict = this.asExpr() } /** - * Gets the key of the `n`th item (at child index `2*n`), and the - * value at child index `2*n + 1`. + * Gets the key of the `n`th item (at child index `2*n`); the value is + * at child index `2*n + 1`. */ - ExprNode getKey(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getKey() } + Expr getKey(int n) { result = TExpr(dict.getItem(n).(Py::KeyValuePair).getKey()) } - ExprNode getValue(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getValue() } + Expr getValue(int n) { result = TExpr(dict.getItem(n).(Py::KeyValuePair).getValue()) } int getNumberOfItems() { result = count(dict.getAnItem()) } } /** A unary expression other than `not` (e.g., `-x`, `+x`, `~x`). */ - class ArithmeticUnaryNode extends ExprNode { + additional class ArithUnaryExpr extends Expr { private Py::UnaryExpr unaryExpr; - ArithmeticUnaryNode() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } + ArithUnaryExpr() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } - ExprNode getOperand() { result.asExpr() = unaryExpr.getOperand() } + Expr getOperand() { result = TExpr(unaryExpr.getOperand()) } } /** - * A comprehension or generator expression. - * The iterable is evaluated in the enclosing scope; the body runs in a - * nested synthetic function scope handled by its own CFG. + * A comprehension or generator expression. The iterable is evaluated in + * the enclosing scope; the body runs in a nested synthetic function + * scope handled by its own CFG. */ - class ComprehensionNode extends ExprNode { + additional class Comprehension extends Expr { private Py::Expr iterable; - ComprehensionNode() { + Comprehension() { iterable = this.asExpr().(Py::ListComp).getIterable() or iterable = this.asExpr().(Py::SetComp).getIterable() @@ -481,289 +737,193 @@ private module Ast { iterable = this.asExpr().(Py::GeneratorExp).getIterable() } - ExprNode getIterable() { result.asExpr() = iterable } + Expr getIterable() { result = TExpr(iterable) } } /** A comparison expression (`a < b`, `a < b < c`, etc.). */ - class CompareNode extends ExprNode { + additional class CompareExpr extends Expr { private Py::Compare cmp; - CompareNode() { cmp = this.asExpr() } + CompareExpr() { cmp = this.asExpr() } - ExprNode getLeft() { result.asExpr() = cmp.getLeft() } + Expr getLeft() { result = TExpr(cmp.getLeft()) } - ExprNode getComparator(int n) { result.asExpr() = cmp.getComparator(n) } + Expr getComparator(int n) { result = TExpr(cmp.getComparator(n)) } } /** A slice expression (`start:stop:step`). */ - class SliceNode extends ExprNode { + additional class SliceExpr extends Expr { private Py::Slice slice; - SliceNode() { slice = this.asExpr() } - - ExprNode getStart() { result.asExpr() = slice.getStart() } - - ExprNode getStop() { result.asExpr() = slice.getStop() } - - ExprNode getStep() { result.asExpr() = slice.getStep() } - } - - /** A starred expression (`*x`). */ - class StarredNode extends ExprNode { - private Py::Starred starred; - - StarredNode() { starred = this.asExpr() } + SliceExpr() { slice = this.asExpr() } - ExprNode getValue() { result.asExpr() = starred.getValue() } - } - - /** A formatted string literal (`f"...{expr}..."`). */ - class FstringNode extends ExprNode { - private Py::Fstring fstring; - - FstringNode() { fstring = this.asExpr() } - - ExprNode getValue(int n) { result.asExpr() = fstring.getValue(n) } - } - - /** A formatted value inside an f-string (`{expr}` or `{expr:spec}`). */ - class FormattedValueNode extends ExprNode { - private Py::FormattedValue fv; - - FormattedValueNode() { fv = this.asExpr() } - - ExprNode getValue() { result.asExpr() = fv.getValue() } - - ExprNode getFormatSpec() { result.asExpr() = fv.getFormatSpec() } - } - - /** A `yield` expression. */ - class YieldNode extends ExprNode { - private Py::Yield yield; - - YieldNode() { yield = this.asExpr() } - - ExprNode getValue() { result.asExpr() = yield.getValue() } - } - - /** A `yield from` expression. */ - class YieldFromNode extends ExprNode { - private Py::YieldFrom yieldFrom; - - YieldFromNode() { yieldFrom = this.asExpr() } - - ExprNode getValue() { result.asExpr() = yieldFrom.getValue() } - } - - /** An `await` expression. */ - class AwaitNode extends ExprNode { - private Py::Await await; + Expr getStart() { result = TExpr(slice.getStart()) } - AwaitNode() { await = this.asExpr() } + Expr getStop() { result = TExpr(slice.getStop()) } - ExprNode getValue() { result.asExpr() = await.getValue() } + Expr getStep() { result = TExpr(slice.getStep()) } } - /** A class definition expression (has base classes evaluated at definition time). */ - class ClassExprNode extends ExprNode { - private Py::ClassExpr classExpr; - - ClassExprNode() { classExpr = this.asExpr() } - - ExprNode getBase(int n) { result.asExpr() = classExpr.getBase(n) } - } - - /** A function definition expression (has default args evaluated at definition time). */ - class FunctionExprNode extends ExprNode { - private Py::FunctionExpr funcExpr; - - FunctionExprNode() { funcExpr = this.asExpr() } + /** A starred expression (`*x`). */ + additional class StarredExpr extends Expr { + private Py::Starred starred; - ExprNode getDefault(int n) { result.asExpr() = funcExpr.getArgs().getDefault(n) } + StarredExpr() { starred = this.asExpr() } - ExprNode getKwDefault(int n) { result.asExpr() = funcExpr.getArgs().getKwDefault(n) } + Expr getValue() { result = TExpr(starred.getValue()) } } - /** A lambda expression (has default args evaluated at definition time). */ - class LambdaNode extends ExprNode { - private Py::Lambda lambda; - - LambdaNode() { lambda = this.asExpr() } + /** A formatted string literal (`f"...{expr}..."`). */ + additional class FstringExpr extends Expr { + private Py::Fstring fstring; - ExprNode getDefault(int n) { result.asExpr() = lambda.getArgs().getDefault(n) } + FstringExpr() { fstring = this.asExpr() } - ExprNode getKwDefault(int n) { result.asExpr() = lambda.getArgs().getKwDefault(n) } + Expr getValue(int n) { result = TExpr(fstring.getValue(n)) } } - /** - * A `not` expression. This is a `UnaryExpr` whose operator is `Not`. - */ - class NotExprNode extends ExprNode { - private Py::UnaryExpr notExpr; + /** A formatted value inside an f-string (`{expr}` or `{expr:spec}`). */ + additional class FormattedValueExpr extends Expr { + private Py::FormattedValue fv; + + FormattedValueExpr() { fv = this.asExpr() } - NotExprNode() { notExpr = this.asExpr() and notExpr.getOp() instanceof Py::Not } + Expr getValue() { result = TExpr(fv.getValue()) } - ExprNode getOperand() { result.asExpr() = notExpr.getOperand() } + Expr getFormatSpec() { result = TExpr(fv.getFormatSpec()) } } - /** - * A boolean expression (`and`/`or`) with exactly 2 operands. - * For 2-operand BoolExprs, the `TExprNode` itself serves as the - * logical and/or expression. - */ - class BoolExpr2Node extends ExprNode { - private Py::BoolExpr boolExpr; + /** A `yield` expression. */ + additional class YieldExpr extends Expr { + private Py::Yield yield; - BoolExpr2Node() { boolExpr = this.asExpr() and count(boolExpr.getAValue()) = 2 } + YieldExpr() { yield = this.asExpr() } - predicate isAnd() { boolExpr.getOp() instanceof Py::And } + Expr getValue() { result = TExpr(yield.getValue()) } + } - predicate isOr() { boolExpr.getOp() instanceof Py::Or } + /** A `yield from` expression. */ + additional class YieldFromExpr extends Expr { + private Py::YieldFrom yieldFrom; - ExprNode getLeftOperand() { result.asExpr() = boolExpr.getValue(0) } + YieldFromExpr() { yieldFrom = this.asExpr() } - ExprNode getRightOperand() { result.asExpr() = boolExpr.getValue(1) } + Expr getValue() { result = TExpr(yieldFrom.getValue()) } } - /** - * The outermost pair of a multi-operand (3+) boolean expression. - * Represented by the original `BoolExpr` node (`TExprNode`). - * Left operand is `getValue(0)`, right operand is `TBoolExprPair(be, 1)`. - */ - class BoolExprOuterNode extends ExprNode { - private Py::BoolExpr boolExpr; + /** An `await` expression. */ + additional class AwaitExpr extends Expr { + private Py::Await await; - BoolExprOuterNode() { boolExpr = this.asExpr() and count(boolExpr.getAValue()) > 2 } + AwaitExpr() { await = this.asExpr() } - predicate isAnd() { boolExpr.getOp() instanceof Py::And } + Expr getValue() { result = TExpr(await.getValue()) } + } - predicate isOr() { boolExpr.getOp() instanceof Py::Or } + /** A class definition expression (has base classes evaluated at definition time). */ + additional class ClassDefExpr extends Expr { + private Py::ClassExpr classExpr; - Node getLeftOperand() { result = TExprNode(boolExpr.getValue(0)) } + ClassDefExpr() { classExpr = this.asExpr() } - Node getRightOperand() { result = TBoolExprPair(boolExpr, 1) } + Expr getBase(int n) { result = TExpr(classExpr.getBase(n)) } } - /** - * A synthetic intermediate node in a multi-operand boolean expression. - * Pair at index `i` has left=`getValue(i)` and right=pair at `i+1` - * (or `getValue(n-1)` for the last pair). - */ - class BoolExprPairNode extends Node, TBoolExprPair { - private Py::BoolExpr boolExpr; - private int index; - - BoolExprPairNode() { this = TBoolExprPair(boolExpr, index) } + /** A function definition expression (has default args evaluated at definition time). */ + additional class FunctionDefExpr extends Expr { + private Py::FunctionExpr funcExpr; - override string toString() { result = boolExpr.getOperator() } + FunctionDefExpr() { funcExpr = this.asExpr() } - override Py::Location getLocation() { result = boolExpr.getValue(index).getLocation() } + Expr getDefault(int n) { result = TExpr(funcExpr.getArgs().getDefault(n)) } - override ScopeNode getEnclosingScope() { - result.asScope() = boolExpr.getValue(index).getScope() - } + Expr getKwDefault(int n) { result = TExpr(funcExpr.getArgs().getKwDefault(n)) } - predicate isAnd() { boolExpr.getOp() instanceof Py::And } + int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } + } - predicate isOr() { boolExpr.getOp() instanceof Py::Or } + /** A lambda expression (has default args evaluated at definition time). */ + additional class LambdaExpr extends Expr { + private Py::Lambda lambda; - Node getLeftOperand() { result = TExprNode(boolExpr.getValue(index)) } + LambdaExpr() { lambda = this.asExpr() } - Node getRightOperand() { - // Last pair: right operand is the final value - index = count(boolExpr.getAValue()) - 2 and - result = TExprNode(boolExpr.getValue(index + 1)) - or - // Not last pair: right operand is the next synthetic pair - index < count(boolExpr.getAValue()) - 2 and - result = TBoolExprPair(boolExpr, index + 1) - } - } + Expr getDefault(int n) { result = TExpr(lambda.getArgs().getDefault(n)) } - /** A `True` or `False` literal. */ - class BoolLiteralNode extends ExprNode { - BoolLiteralNode() { this.asExpr() instanceof Py::True or this.asExpr() instanceof Py::False } + Expr getKwDefault(int n) { result = TExpr(lambda.getArgs().getKwDefault(n)) } - boolean getBoolValue() { - this.asExpr() instanceof Py::True and result = true - or - this.asExpr() instanceof Py::False and result = false - } + int getNumberOfDefaults() { result = count(lambda.getArgs().getADefault()) } } -} - -/** Provides an implementation of the AST signature for Python. */ -module AstSigImpl implements AstSig { - class AstNode = Ast::Node; /** Gets the child of `n` at the specified (zero-based) index. */ AstNode getChild(AstNode n, int index) { - // IfStmt: condition (0), then branch (1), else branch (2) - exists(Ast::IfNode ifNode | ifNode = n | - index = 0 and result = ifNode.getTest() + // BlockStmt: indexed statements + result = n.(BlockStmt).getStmt(index) + or + // IfStmt: condition (0), then (1), else (2) + exists(IfStmt ifStmt | ifStmt = n | + index = 0 and result = ifStmt.getCondition() or - index = 1 and result = ifNode.getBody() + index = 1 and result = ifStmt.getThen() or - index = 2 and result = ifNode.getOrelse() + index = 2 and result = ifStmt.getElse() ) or - // BlockStmt (StmtList): indexed statements - result = n.(Ast::StmtListNode).getItem(index) - or // ExprStmt: the expression (0) - index = 0 and result = n.(Ast::ExprStmtNode).getValue() + index = 0 and result = n.(ExprStmt).getExpr() or // Assign: value (0), targets (1..n) - exists(Ast::AssignNode a | a = n | + exists(AssignStmt a | a = n | index = 0 and result = a.getValue() or result = a.getTarget(index - 1) and index >= 1 ) or // AugAssign: the operation (0) - index = 0 and result = n.(Ast::AugAssignNode).getOperation() + index = 0 and result = n.(AugAssignStmt).getOperation() or - // AssignExpr (walrus :=): value (0), target (1) - exists(Ast::AssignExprNode ae | ae = n | - index = 0 and result = ae.getValue() + // Walrus (`x := expr`): value (0), target (1) + exists(NamedExpr ne | ne = n | + index = 0 and result = ne.getValue() or - index = 1 and result = ae.getTarget() + index = 1 and result = ne.getTarget() ) or - // WhileStmt: condition (0), body (1) - // Note: Python while/else is not directly supported by the shared library. - exists(Ast::WhileNode w | w = n | - index = 0 and result = w.getTest() + // WhileStmt: condition (0), body (1), orelse (2) + exists(WhileStmt w | w = n | + index = 0 and result = w.getCondition() or index = 1 and result = w.getBody() or - index = 2 and result = w.getOrelse() + index = 2 and result = w.getElse() ) or - // ForStmt (mapped as ForeachStmt): collection (0), variable (1), body (2) - exists(Ast::ForNode f | f = n | - index = 0 and result = f.getIter() + // ForeachStmt: collection (0), variable (1), body (2), orelse (3) + exists(ForeachStmt f | f = n | + index = 0 and result = f.getCollection() or - index = 1 and result = f.getTarget() + index = 1 and result = f.getVariable() or index = 2 and result = f.getBody() + or + index = 3 and result = f.getElse() ) or // ReturnStmt: the value (0) - index = 0 and result = n.(Ast::ReturnNode).getValue() + index = 0 and result = n.(ReturnStmt).getExpr() or - // Assert: test (0), message (1) - exists(Ast::AssertNode a | a = n | + // AssertStmt: test (0), message (1) + exists(AssertStmt a | a = n | index = 0 and result = a.getTest() or index = 1 and result = a.getMsg() ) or - // Delete: targets left to right - result = n.(Ast::DeleteNode).getTarget(index) + // DeleteStmt: targets left to right + result = n.(DeleteStmt).getTarget(index) or - // With: context expr (0), optional vars (1), body (2) - exists(Ast::WithNode w | w = n | + // WithStmt: context expr (0), optional vars (1), body (2) + exists(WithStmt w | w = n | index = 0 and result = w.getContextExpr() or index = 1 and result = w.getOptionalVars() @@ -771,32 +931,32 @@ module AstSigImpl implements AstSig { index = 2 and result = w.getBody() ) or - // ThrowStmt (raise): the exception (0), the cause (1) - exists(Ast::RaiseNode r | r = n | - index = 0 and result = r.getException() + // Throw (raise): exception (0), cause (1) + exists(Throw r | r = n | + index = 0 and result = r.getExpr() or index = 1 and result = r.getCause() ) or // TryStmt: body (0), handlers (1..n), finally (-1) - exists(Ast::TryNode t | t = n | + exists(TryStmt t | t = n | index = 0 and result = t.getBody() or - result = t.getHandler(index - 1) and index >= 1 + result = t.getCatch(index - 1) and index >= 1 or - index = -1 and result = t.getFinalbody() + index = -1 and result = t.getFinally() ) or - // MatchStmt: subject (0), cases (1..n) - exists(Ast::MatchStmtNode m | m = n | - index = 0 and result = m.getSubject() + // Switch (match): subject (0), cases (1..n) + exists(Switch m | m = n | + index = 0 and result = m.getExpr() or result = m.getCase(index - 1) and index >= 1 ) or - // Case: guard (0), body (1) - exists(Ast::CaseNode c | c = n | - index = 0 and result = c.getPattern() + // Case: pattern (0), guard (1), body (2) + exists(Case c | c = n | + index = 0 and result = c.getAPattern() or index = 1 and result = c.getGuard() or @@ -804,25 +964,25 @@ module AstSigImpl implements AstSig { ) or // CatchClause (except handler): type (0), name (1), body (2) - exists(Ast::ExceptionHandlerNode h | h = n | + exists(CatchClause h | h = n | index = 0 and result = h.getType() or - index = 1 and result = h.getName() + index = 1 and result = h.getVariable() or index = 2 and result = h.getBody() ) or // ConditionalExpr (IfExp): condition (0), then (1), else (2) - exists(Ast::IfExpNode ie | ie = n | - index = 0 and result = ie.getTest() + exists(ConditionalExpr ie | ie = n | + index = 0 and result = ie.getCondition() or - index = 1 and result = ie.getBody() + index = 1 and result = ie.getThen() or - index = 2 and result = ie.getOrelse() + index = 2 and result = ie.getElse() ) or // Call: func (0), positional args (1..n), keyword values (n+1..n+k) - exists(Ast::CallNode call | call = n | + exists(CallExpr call | call = n | index = 0 and result = call.getFunc() or result = call.getPositionalArg(index - 1) and index >= 1 @@ -832,51 +992,51 @@ module AstSigImpl implements AstSig { ) or // Python BinaryExpr (arithmetic, bitwise, matmul, etc.): left (0), right (1) - exists(Ast::BinaryExprNode be | be = n | + exists(ArithBinaryExpr be | be = n | index = 0 and result = be.getLeft() or index = 1 and result = be.getRight() ) or // Subscript (obj[index]): object (0), index (1) - exists(Ast::SubscriptNode sub | sub = n | + exists(SubscriptExpr sub | sub = n | index = 0 and result = sub.getObject() or index = 1 and result = sub.getIndex() ) or // Attribute (obj.name): object (0) - index = 0 and result = n.(Ast::AttributeNode).getObject() + index = 0 and result = n.(AttributeExpr).getObject() or // Comprehension/generator: iterable (0) - index = 0 and result = n.(Ast::ComprehensionNode).getIterable() + index = 0 and result = n.(Comprehension).getIterable() or // Tuple, List, Set: elements left to right - result = n.(Ast::TupleNode).getElt(index) + result = n.(TupleExpr).getElt(index) or - result = n.(Ast::ListNode).getElt(index) + result = n.(ListExpr).getElt(index) or - result = n.(Ast::SetNode).getElt(index) + result = n.(SetExpr).getElt(index) or // Dict: key(0), value(0), key(1), value(1), ... - exists(Ast::DictNode d, int item | d = n | + exists(DictExpr d, int item | d = n | index = 2 * item and result = d.getKey(item) or index = 2 * item + 1 and result = d.getValue(item) ) or // Arithmetic unary (-x, +x, ~x): operand (0) - index = 0 and result = n.(Ast::ArithmeticUnaryNode).getOperand() + index = 0 and result = n.(ArithUnaryExpr).getOperand() or // Compare (a < b < c): left (0), comparators (1..n) - exists(Ast::CompareNode cmp | cmp = n | + exists(CompareExpr cmp | cmp = n | index = 0 and result = cmp.getLeft() or result = cmp.getComparator(index - 1) and index >= 1 ) or // Slice (start:stop:step): start (0), stop (1), step (2) - exists(Ast::SliceNode sl | sl = n | + exists(SliceExpr sl | sl = n | index = 0 and result = sl.getStart() or index = 1 and result = sl.getStop() @@ -885,367 +1045,57 @@ module AstSigImpl implements AstSig { ) or // Starred (*x): value (0) - index = 0 and result = n.(Ast::StarredNode).getValue() + index = 0 and result = n.(StarredExpr).getValue() or // Fstring: values left to right - result = n.(Ast::FstringNode).getValue(index) + result = n.(FstringExpr).getValue(index) or // FormattedValue ({expr} or {expr:spec}): value (0), format spec (1) - exists(Ast::FormattedValueNode fv | fv = n | + exists(FormattedValueExpr fv | fv = n | index = 0 and result = fv.getValue() or index = 1 and result = fv.getFormatSpec() ) or // Yield: value (0) - index = 0 and result = n.(Ast::YieldNode).getValue() + index = 0 and result = n.(YieldExpr).getValue() or // YieldFrom: value (0) - index = 0 and result = n.(Ast::YieldFromNode).getValue() + index = 0 and result = n.(YieldFromExpr).getValue() or // Await: value (0) - index = 0 and result = n.(Ast::AwaitNode).getValue() + index = 0 and result = n.(AwaitExpr).getValue() or // ClassExpr: base classes left to right - result = n.(Ast::ClassExprNode).getBase(index) + result = n.(ClassDefExpr).getBase(index) or // FunctionExpr: defaults left to right, then kw defaults - exists(Ast::FunctionExprNode fe | fe = n | + exists(FunctionDefExpr fe | fe = n | result = fe.getDefault(index) or - result = - fe.getKwDefault(index - - count(Py::Expr d | d = fe.asExpr().(Py::FunctionExpr).getArgs().getADefault())) + result = fe.getKwDefault(index - fe.getNumberOfDefaults()) ) or // Lambda: defaults left to right, then kw defaults - exists(Ast::LambdaNode lam | lam = n | + exists(LambdaExpr lam | lam = n | result = lam.getDefault(index) or - result = - lam.getKwDefault(index - - count(Py::Expr d | d = lam.asExpr().(Py::Lambda).getArgs().getADefault())) + result = lam.getKwDefault(index - lam.getNumberOfDefaults()) ) or // LogicalNotExpr: operand (0) - index = 0 and result = n.(Ast::NotExprNode).getOperand() - or - // 2-operand BoolExpr: left (0), right (1) - exists(Ast::BoolExpr2Node be | be = n | - index = 0 and result = be.getLeftOperand() - or - index = 1 and result = be.getRightOperand() - ) + index = 0 and result = n.(LogicalNotExpr).getOperand() or - // Multi-operand BoolExpr (outermost): left (0), right (1) - exists(Ast::BoolExprOuterNode be | be = n | + // BinaryExpr (`and`/`or`): left (0), right (1) + exists(BinaryExpr be | be = n | index = 0 and result = be.getLeftOperand() or index = 1 and result = be.getRightOperand() ) - or - // Synthetic BoolExpr pair: left (0), right (1) - exists(Ast::BoolExprPairNode bp | bp = n | - index = 0 and result = bp.getLeftOperand() - or - index = 1 and result = bp.getRightOperand() - ) - } - - Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingScope() } - - /** - * A callable: a function, class, or module scope. - * - * In Python, all three are executable scopes with statement bodies. - */ - class Callable extends Ast::ScopeNode { } - - /** Gets the body of callable `c`. */ - AstNode callableGetBody(Callable c) { result = c.getBody() } - - /** A statement. Includes both wrapped `Stmt` nodes and `StmtList` blocks. */ - class Stmt extends AstNode { - Stmt() { this instanceof Ast::StmtNode or this instanceof Ast::StmtListNode } - } - - /** An expression. Includes `TExprNode` and synthetic `TBoolExprPair` nodes. */ - class Expr extends AstNode { - Expr() { this instanceof Ast::ExprNode or this instanceof Ast::BoolExprPairNode } - } - - /** A block of statements, wrapping Python's `StmtList`. */ - class BlockStmt extends Stmt, Ast::StmtListNode { - /** Gets the `n`th (zero-based) statement in this block. */ - Stmt getStmt(int n) { result = Ast::StmtListNode.super.getItem(n) } - - /** Gets the last statement in this block. */ - Stmt getLastStmt() { result = Ast::StmtListNode.super.getLastItem() } - } - - /** An expression statement. */ - class ExprStmt extends Stmt, Ast::ExprStmtNode { - /** Gets the expression in this expression statement. */ - Expr getExpr() { result = this.getValue() } - } - - /** - * An `if` statement. - * - * Python's `elif` chains are represented as nested `If` nodes in the - * else branch's `StmtList`. The shared CFG library handles this naturally: - * `getElse()` returns the `BlockStmt` wrapping the else branch, and if that - * block contains a single `If`, the result is a chained conditional. - */ - class IfStmt extends Stmt, Ast::IfNode { - /** Gets the condition of this `if` statement. */ - Expr getCondition() { result = this.getTest() } - - /** Gets the `then` (true) branch of this `if` statement. */ - Stmt getThen() { result = Ast::IfNode.super.getBody() } - - /** Gets the `else` (false) branch of this `if` statement, if any. */ - Stmt getElse() { result = this.getOrelse() } - } - - // ===== Loop statements ===== - /** A loop statement. */ - class LoopStmt extends Stmt { - LoopStmt() { this instanceof Ast::WhileNode or this instanceof Ast::ForNode } - - /** Gets the body of this loop statement. */ - Stmt getBody() { none() } - } - - /** A `while` loop statement. */ - class WhileStmt extends LoopStmt instanceof Ast::WhileNode { - /** Gets the boolean condition of this `while` loop. */ - Expr getCondition() { result = this.(Ast::WhileNode).getTest() } - - override Stmt getBody() { result = this.(Ast::WhileNode).getBody() } - } - - /** A `do-while` loop statement. Python has no do-while construct. */ - class DoStmt extends LoopStmt { - DoStmt() { none() } - - Expr getCondition() { none() } - } - - /** A C-style `for` loop. Python has no C-style for loop. */ - class ForStmt extends LoopStmt { - ForStmt() { none() } - - Expr getInit(int index) { none() } - - Expr getCondition() { none() } - - Expr getUpdate(int index) { none() } - } - - /** A for-each loop (`for x in iterable:`). */ - class ForeachStmt extends LoopStmt { - ForeachStmt() { this instanceof Ast::ForNode } - - /** Gets the loop variable. */ - Expr getVariable() { result = this.(Ast::ForNode).getTarget() } - - /** Gets the collection being iterated. */ - Expr getCollection() { result = this.(Ast::ForNode).getIter() } - - override Stmt getBody() { result = this.(Ast::ForNode).getBody() } - } - - // ===== Abrupt completion statements ===== - /** A `break` statement. */ - class BreakStmt extends Stmt, Ast::BreakNode { } - - /** A `continue` statement. */ - class ContinueStmt extends Stmt, Ast::ContinueNode { } - - /** A `return` statement. */ - class ReturnStmt extends Stmt, Ast::ReturnNode { - /** Gets the expression being returned, if any. */ - Expr getExpr() { result = this.getValue() } - } - - /** A `raise` statement (mapped to `Throw`). */ - class Throw extends Stmt, Ast::RaiseNode { - /** Gets the expression being raised. */ - Expr getExpr() { result = this.getException() } - } - - /** A `goto` statement. Python has no goto. */ - class GotoStmt extends Stmt { - GotoStmt() { none() } - } - - // ===== Try/except ===== - /** A `try` statement. */ - class TryStmt extends Stmt { - TryStmt() { this instanceof Ast::TryNode } - - Stmt getBody() { result = this.(Ast::TryNode).getBody() } - - CatchClause getCatch(int index) { result = this.(Ast::TryNode).getHandler(index) } - - Stmt getFinally() { result = this.(Ast::TryNode).getFinalbody() } - } - - AstNode getTryElse(TryStmt try) { result = try.(Ast::TryNode).getOrelse() } - - /** An except clause in a try statement. */ - class CatchClause extends Stmt { - CatchClause() { this instanceof Ast::ExceptionHandlerNode } - - AstNode getVariable() { result = this.(Ast::ExceptionHandlerNode).getName() } - - Expr getCondition() { none() } - - Stmt getBody() { result = this.(Ast::ExceptionHandlerNode).getBody() } - } - - // ===== Switch/match ===== - /** A `match` statement, mapped to the shared CFG's `Switch`. */ - class Switch extends Stmt { - Switch() { this instanceof Ast::MatchStmtNode } - - Expr getExpr() { result = this.(Ast::MatchStmtNode).getSubject() } - - Case getCase(int index) { result = this.(Ast::MatchStmtNode).getCase(index) } - - Stmt getStmt(int index) { none() } - } - - /** A `case` clause in a match statement. */ - class Case extends Stmt { - Case() { this instanceof Ast::CaseNode } - - AstNode getAPattern() { result = this.(Ast::CaseNode).getPattern() } - - Expr getGuard() { result = this.(Ast::CaseNode).getGuard() } - - AstNode getBody() { result = this.(Ast::CaseNode).getBody() } - } - - /** A wildcard case (`case _:`). */ - class DefaultCase extends Case { - DefaultCase() { this.(Ast::CaseNode).isWildcard() } - } - - // ===== Expression types ===== - /** A conditional expression (`x if cond else y`). */ - class ConditionalExpr extends Expr, Ast::IfExpNode { - /** Gets the condition of this expression. */ - Expr getCondition() { result = this.getTest() } - - /** Gets the true branch of this expression. */ - Expr getThen() { result = Ast::IfExpNode.super.getBody() } - - /** Gets the false branch of this expression. */ - Expr getElse() { result = this.getOrelse() } - } - - /** - * A binary expression for the shared CFG. In Python, this covers - * `and`/`or` expressions (both real 2-operand and synthetic pairs). - */ - class BinaryExpr extends Expr { - BinaryExpr() { - this instanceof Ast::BoolExpr2Node or - this instanceof Ast::BoolExprOuterNode or - this instanceof Ast::BoolExprPairNode - } - - /** Gets the left operand. */ - Expr getLeftOperand() { - result = this.(Ast::BoolExpr2Node).getLeftOperand() - or - result = this.(Ast::BoolExprOuterNode).getLeftOperand() - or - result = this.(Ast::BoolExprPairNode).getLeftOperand() - } - - /** Gets the right operand. */ - Expr getRightOperand() { - result = this.(Ast::BoolExpr2Node).getRightOperand() - or - result = this.(Ast::BoolExprOuterNode).getRightOperand() - or - result = this.(Ast::BoolExprPairNode).getRightOperand() - } - } - - /** A short-circuiting logical `and` expression. */ - class LogicalAndExpr extends BinaryExpr { - LogicalAndExpr() { - this.(Ast::BoolExpr2Node).isAnd() or - this.(Ast::BoolExprOuterNode).isAnd() or - this.(Ast::BoolExprPairNode).isAnd() - } - } - - /** A short-circuiting logical `or` expression. */ - class LogicalOrExpr extends BinaryExpr { - LogicalOrExpr() { - this.(Ast::BoolExpr2Node).isOr() or - this.(Ast::BoolExprOuterNode).isOr() or - this.(Ast::BoolExprPairNode).isOr() - } - } - - /** A null-coalescing expression. Python has no null-coalescing operator. */ - class NullCoalescingExpr extends BinaryExpr { - NullCoalescingExpr() { none() } - } - - /** An assignment expression. Python has no assignment expressions in the BinaryExpr sense. */ - class Assignment extends BinaryExpr { - Assignment() { none() } - } - - /** A simple assignment expression. */ - class AssignExpr extends Assignment { } - - /** A compound assignment expression. */ - class CompoundAssignment extends Assignment { } - - /** A short-circuiting logical AND compound assignment. Python has no `&&=` operator. */ - class AssignLogicalAndExpr extends CompoundAssignment { } - - /** A short-circuiting logical OR compound assignment. Python has no `||=` operator. */ - class AssignLogicalOrExpr extends CompoundAssignment { } - - /** A short-circuiting null-coalescing compound assignment. Python has no `??=` operator. */ - class AssignNullCoalescingExpr extends CompoundAssignment { } - - /** A unary expression. Exists for the `not` subclass. */ - class UnaryExpr extends Expr { - UnaryExpr() { this instanceof Ast::NotExprNode } - - Expr getOperand() { result = this.(Ast::NotExprNode).getOperand() } - } - - /** A logical `not` expression. */ - class LogicalNotExpr extends UnaryExpr { } - - /** A boolean literal expression (`True` or `False`). */ - class BooleanLiteral extends Expr, Ast::BoolLiteralNode { - /** Gets the boolean value of this literal. */ - boolean getValue() { result = this.getBoolValue() } - } - - /** A pattern match expression. Python has no `instanceof`-style pattern match expr. */ - class PatternMatchExpr extends Expr { - PatternMatchExpr() { none() } - - Expr getExpr() { none() } - - AstNode getPattern() { none() } } } -private module Cfg0 = Make0; +private module Cfg0 = Make0; private import Cfg0 @@ -1268,32 +1118,32 @@ private module Input implements InputSig1, InputSig2 { class CallableBodyPartContext = Void; - predicate inConditionalContext(AstSigImpl::AstNode n, ConditionKind kind) { + predicate inConditionalContext(Ast::AstNode n, ConditionKind kind) { kind.isBoolean() and - n = any(Ast::AssertNode a).getTest() + n = any(Ast::AssertStmt a).getTest() } private string assertThrowTag() { result = "[assert-throw]" } - predicate additionalNode(AstSigImpl::AstNode n, string tag, NormalSuccessor t) { - n instanceof Ast::AssertNode and tag = assertThrowTag() and t instanceof DirectSuccessor + predicate additionalNode(Ast::AstNode n, string tag, NormalSuccessor t) { + n instanceof Ast::AssertStmt and tag = assertThrowTag() and t instanceof DirectSuccessor } predicate beginAbruptCompletion( - AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c, boolean always + Ast::AstNode ast, PreControlFlowNode n, AbruptCompletion c, boolean always ) { - ast instanceof Ast::AssertNode and + ast instanceof Ast::AssertStmt and n.isAdditional(ast, assertThrowTag()) and c.asSimpleAbruptCompletion() instanceof ExceptionSuccessor and always = true } - predicate endAbruptCompletion(AstSigImpl::AstNode ast, PreControlFlowNode n, AbruptCompletion c) { + predicate endAbruptCompletion(Ast::AstNode ast, PreControlFlowNode n, AbruptCompletion c) { none() } predicate step(PreControlFlowNode n1, PreControlFlowNode n2) { - exists(Ast::AssertNode assertStmt | + exists(Ast::AssertStmt assertStmt | n1.isBefore(assertStmt) and n2.isBefore(assertStmt.getTest()) or @@ -1314,18 +1164,18 @@ private module Input implements InputSig1, InputSig2 { or // While/else: when the condition is false, flow to the else block // (if present) before the after-while node. - exists(Ast::WhileNode w, Ast::StmtListNode orelse | orelse = w.getOrelse() | - n1.isAfterFalse(w.getTest()) and + exists(Ast::WhileStmt w, Ast::BlockStmt orelse | orelse = w.getElse() | + n1.isAfterFalse(w.getCondition()) and n2.isBefore(orelse) or n1.isAfter(orelse) and n2.isAfter(w) ) or - // For/else: when the collection is empty or the loop completes normally, - // flow through the else block before the after-for node. - exists(Ast::ForNode f, Ast::StmtListNode orelse | orelse = f.getOrelse() | - n1.isAfterValue(f.getIter(), any(EmptinessSuccessor t | t.getValue() = true)) and + // For/else: when the collection is empty or the loop completes + // normally, flow through the else block before the after-for node. + exists(Ast::ForeachStmt f, Ast::BlockStmt orelse | orelse = f.getElse() | + n1.isAfterValue(f.getCollection(), any(EmptinessSuccessor t | t.getValue() = true)) and n2.isBefore(orelse) or n1.isAfter(f.getBody()) and @@ -1341,13 +1191,13 @@ import CfgCachedStage import Public /** - * Maps a new-CFG AST wrapper node to the corresponding Python AST node, if any. + * Maps a CFG AST wrapper node to the corresponding Python AST node, if any. * Entry, exit, and synthetic nodes have no corresponding Python AST node. */ -Py::AstNode astNodeToPyNode(AstSigImpl::AstNode n) { - result = n.(Ast::ExprNode).asExpr() +Py::AstNode astNodeToPyNode(Ast::AstNode n) { + result = n.asExpr() or - result = n.(Ast::StmtNode).asStmt() + result = n.asStmt() or - result = n.(Ast::ScopeNode).asScope() + result = n.asScope() } From 158c81c06d05efd0e7f01bbcd79592a6846c820a Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 14:25:43 +0000 Subject: [PATCH 32/72] Python: compact-renumber FunctionExpr/Lambda defaults `Args.getDefault(int)` and `Args.getKwDefault(int)` are indexed by argument position (with gaps for args without defaults), not by default position. The CFG `getChild` predicate for FunctionDefExpr and LambdaExpr therefore had gaps at low indices and collisions where defaults and kwdefaults overlapped, producing parallel edges before the FunctionExpr. Use `rank` to compact-renumber `getDefault(n)` and `getKwDefault(n)` in source order. Verified on a CPython database: removes ~536 `multipleSuccessors` consistency results (1340 -> 804); the rest are `for/else` and `while/else`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 8a86ffa08748..d6c63c6027cc 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -835,9 +835,22 @@ module Ast implements AstSig { FunctionDefExpr() { funcExpr = this.asExpr() } - Expr getDefault(int n) { result = TExpr(funcExpr.getArgs().getDefault(n)) } + /** + * Gets the `n`th default for a positional argument, in evaluation + * order. Note that `Args.getDefault(int)` is indexed by argument + * position (with gaps for arguments without defaults), so we must + * renumber here to obtain contiguous indices. + */ + Expr getDefault(int n) { + result = + TExpr(rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getDefault(i) | d order by i)) + } - Expr getKwDefault(int n) { result = TExpr(funcExpr.getArgs().getKwDefault(n)) } + /** Gets the `n`th default for a keyword-only argument, in evaluation order. */ + Expr getKwDefault(int n) { + result = + TExpr(rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getKwDefault(i) | d order by i)) + } int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } } @@ -848,9 +861,17 @@ module Ast implements AstSig { LambdaExpr() { lambda = this.asExpr() } - Expr getDefault(int n) { result = TExpr(lambda.getArgs().getDefault(n)) } + /** Gets the `n`th default for a positional argument, in evaluation order. */ + Expr getDefault(int n) { + result = + TExpr(rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getDefault(i) | d order by i)) + } - Expr getKwDefault(int n) { result = TExpr(lambda.getArgs().getKwDefault(n)) } + /** Gets the `n`th default for a keyword-only argument, in evaluation order. */ + Expr getKwDefault(int n) { + result = + TExpr(rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getKwDefault(i) | d order by i)) + } int getNumberOfDefaults() { result = count(lambda.getArgs().getADefault()) } } From 577cf4a6304a0459bb02fb243afb5cb5dd96eade Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 14:45:48 +0000 Subject: [PATCH 33/72] Shared CFG: support for-else and while-else loops Add two default predicates to AstSig: default AstNode getWhileElse(WhileStmt loop) { none() } default AstNode getForeachElse(ForeachStmt loop) { none() } When defined, the explicit-step rules for While/Do and Foreach route the loop's normal-completion exits through the else block before reaching the after-loop node: - WhileStmt: after-false condition -> before-else -> after-while (instead of directly after-while). - ForeachStmt: after-collection [empty] and the LoopHeader exit are both routed through before-else -> after-foreach. Python's Ast module overrides the predicates to return the synthetic BlockStmt for the orelse slot, replacing the previous customisations in Input::step. This eliminates parallel direct successors emitted by the previous Python-side step additions (verified: multipleSuccessors on a CPython database goes from 1340 to 0). Java and C# CFG tests are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 33 +++++---------- .../NewCfgBranchTimestamps.expected | 6 --- .../codeql/controlflow/ControlFlowGraph.qll | 40 +++++++++++++++++-- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d6c63c6027cc..eb21cf32b1d6 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -411,6 +411,16 @@ module Ast implements AstSig { */ AstNode getTryElse(TryStmt try) { result = try.getElse() } + /** + * Gets the `else` branch of `while` loop `loop`, if any. + */ + AstNode getWhileElse(WhileStmt loop) { result = loop.getElse() } + + /** + * Gets the `else` branch of `for` loop `loop`, if any. + */ + AstNode getForeachElse(ForeachStmt loop) { result = loop.getElse() } + /** An exception handler (`except` or `except*`). */ class CatchClause extends Stmt { private Py::ExceptionHandler handler; @@ -1182,29 +1192,6 @@ private module Input implements InputSig1, InputSig2 { n1.isAfter(assertStmt.getMsg()) and n2.isAdditional(assertStmt, assertThrowTag()) ) - or - // While/else: when the condition is false, flow to the else block - // (if present) before the after-while node. - exists(Ast::WhileStmt w, Ast::BlockStmt orelse | orelse = w.getElse() | - n1.isAfterFalse(w.getCondition()) and - n2.isBefore(orelse) - or - n1.isAfter(orelse) and - n2.isAfter(w) - ) - or - // For/else: when the collection is empty or the loop completes - // normally, flow through the else block before the after-for node. - exists(Ast::ForeachStmt f, Ast::BlockStmt orelse | orelse = f.getElse() | - n1.isAfterValue(f.getCollection(), any(EmptinessSuccessor t | t.getValue() = true)) and - n2.isBefore(orelse) - or - n1.isAfter(f.getBody()) and - n2.isBefore(orelse) - or - n1.isAfter(orelse) and - n2.isAfter(f) - ) } } diff --git a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected index fcc9a17aa746..89a93f41a01b 100644 --- a/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected +++ b/python/ql/test/library-tests/ControlFlow/evaluation-order/NewCfgBranchTimestamps.expected @@ -54,8 +54,6 @@ | test_loops.py:53:12:53:38 | After Compare | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | | test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | | test_loops.py:53:12:53:38 | After Compare | Timestamp 13 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | -| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | -| test_loops.py:53:12:53:38 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | | test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | | test_loops.py:54:13:54:40 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | | test_loops.py:54:13:54:40 | After Compare | Timestamp 16 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:51:1:51:29 | Function test_while_else_break | test_while_else_break | @@ -158,8 +156,6 @@ | test_loops.py:111:14:111:43 | After List | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:111:14:111:43 | After List | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | -| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | -| test_loops.py:111:14:111:43 | After List | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:112:13:112:38 | After Compare | Timestamp 7 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:112:13:112:38 | After Compare | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | @@ -168,8 +164,6 @@ | test_loops.py:114:9:114:9 | x | Timestamp 4 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:114:9:114:9 | x | Timestamp 8 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | -| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | -| test_loops.py:114:9:114:9 | x | Timestamp 11 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:110:1:110:27 | Function test_for_else_break | test_for_else_break | | test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | | test_loops.py:123:14:123:33 | After List | Timestamp 21 on true/false branch is missing a dead() annotation on the true successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | | test_loops.py:124:18:124:47 | After List | Timestamp 3 on true/false branch is missing a dead() annotation on the false successor in $@ | test_loops.py:122:1:122:25 | Function test_nested_loops | test_nested_loops | diff --git a/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll b/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll index fff877b9fcd9..dd71b5f98c79 100644 --- a/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll +++ b/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll @@ -211,6 +211,20 @@ signature module AstSig { */ default AstNode getTryElse(TryStmt try) { none() } + /** + * Gets the `else` block of this `while` loop statement, if any. + * + * Only some languages (e.g. Python) support `while-else` constructs. + */ + default AstNode getWhileElse(WhileStmt loop) { none() } + + /** + * Gets the `else` block of this `foreach` loop statement, if any. + * + * Only some languages (e.g. Python) support `for-else` constructs. + */ + default AstNode getForeachElse(ForeachStmt loop) { none() } + /** A catch clause in a try statement. */ class CatchClause extends AstNode { /** Gets the variable declared by this catch clause. */ @@ -1549,19 +1563,32 @@ module Make0 Ast> { n2.isBefore(loopstmt.getBody()) or n1.isAfterFalse(cond) and - n2.isAfter(loopstmt) + ( + n2.isBefore(getWhileElse(loopstmt)) + or + not exists(getWhileElse(loopstmt)) and n2.isAfter(loopstmt) + ) or n1.isAfter(loopstmt.getBody()) and n2.isAdditional(loopstmt, loopHeaderTag()) ) or + exists(WhileStmt whilestmt | + n1.isAfter(getWhileElse(whilestmt)) and + n2.isAfter(whilestmt) + ) + or exists(ForeachStmt foreachstmt | n1.isBefore(foreachstmt) and n2.isBefore(foreachstmt.getCollection()) or n1.isAfterValue(foreachstmt.getCollection(), any(EmptinessSuccessor t | t.getValue() = true)) and - n2.isAfter(foreachstmt) + ( + n2.isBefore(getForeachElse(foreachstmt)) + or + not exists(getForeachElse(foreachstmt)) and n2.isAfter(foreachstmt) + ) or n1.isAfterValue(foreachstmt.getCollection(), any(EmptinessSuccessor t | t.getValue() = false)) and @@ -1574,10 +1601,17 @@ module Make0 Ast> { n2.isAdditional(foreachstmt, loopHeaderTag()) or n1.isAdditional(foreachstmt, loopHeaderTag()) and - n2.isAfter(foreachstmt) + ( + n2.isBefore(getForeachElse(foreachstmt)) + or + not exists(getForeachElse(foreachstmt)) and n2.isAfter(foreachstmt) + ) or n1.isAdditional(foreachstmt, loopHeaderTag()) and n2.isBefore(foreachstmt.getVariable()) + or + n1.isAfter(getForeachElse(foreachstmt)) and + n2.isAfter(foreachstmt) ) or exists(ForStmt forstmt, PreControlFlowNode condentry | From d3759008091b0689cbc7a196a09b58270e51f658 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 14:56:18 +0000 Subject: [PATCH 34/72] Python: include try-else in getChild for completion propagation The shared CFG library propagates abrupt completions from child to parent via getChild(parent, _) = child. Python's try.getElse() was wired into normal step rules but not listed in getChild(TryStmt, ...), so return/break/continue/raise statements occurring inside a try-else block had no parent path and ended up as dead-end CFG nodes. Add the else block at index -2 (alongside finally at -1). This affects only completion propagation; the normal-flow CFG is unchanged because TryStmt has explicit step rules. Verified on a CPython database: all 11 shared-CFG consistency queries now pass with 0 violations (deadEnd: 244 -> 0). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index eb21cf32b1d6..da960060edf0 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -969,13 +969,15 @@ module Ast implements AstSig { index = 1 and result = r.getCause() ) or - // TryStmt: body (0), handlers (1..n), finally (-1) + // TryStmt: body (0), handlers (1..n), else (-2), finally (-1) exists(TryStmt t | t = n | index = 0 and result = t.getBody() or result = t.getCatch(index - 1) and index >= 1 or index = -1 and result = t.getFinally() + or + index = -2 and result = t.getElse() ) or // Switch (match): subject (0), cases (1..n) From a3270ec9f521bb4b6ad23bdf253f4c1957f4397b Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 15:09:19 +0000 Subject: [PATCH 35/72] Python: refactor getChild into per-class OO dispatch Replace the single ~240-line top-level getChild predicate with one override per AST class. AstNode declares a default AstNode getChild(int index) { none() } and each subclass with children overrides it (41 classes total). The top-level predicate becomes a one-line dispatch: AstNode getChild(AstNode n, int index) { result = n.getChild(index) } No behavioral change: NewCfg evaluation-order tests still pass at the same 22/24 baseline, and all 11 shared-CFG consistency queries still report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 448 ++++++++---------- 1 file changed, 209 insertions(+), 239 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index da960060edf0..86fdf45e0baa 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -143,6 +143,13 @@ module Ast implements AstSig { /** Gets the underlying Python `Pattern`, if this node wraps one. */ Py::Pattern asPattern() { this = TPattern(result) } + + /** + * Gets the child of this AST node at the specified (zero-based) + * index, in evaluation order. Subclasses with children override + * this method. + */ + AstNode getChild(int index) { none() } } /** Gets the immediately enclosing callable that contains `node`. */ @@ -186,6 +193,8 @@ module Ast implements AstSig { /** Gets the last statement in this block. */ Stmt getLastStmt() { result = TStmt(getBodyStmtList(parent, slot).getLastItem()) } + + override AstNode getChild(int index) { result = this.getStmt(index) } } /** An expression statement. */ @@ -196,6 +205,8 @@ module Ast implements AstSig { /** Gets the expression in this expression statement. */ Expr getExpr() { result = TExpr(exprStmt.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getExpr() } } /** An assignment statement (`x = y = expr`). */ @@ -209,6 +220,12 @@ module Ast implements AstSig { Expr getTarget(int n) { result = TExpr(assign.getTarget(n)) } int getNumberOfTargets() { result = count(assign.getATarget()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getValue() + or + result = this.getTarget(index - 1) and index >= 1 + } } /** An augmented assignment statement (`x += expr`). */ @@ -218,6 +235,8 @@ module Ast implements AstSig { AugAssignStmt() { augAssign = this.asStmt() } Expr getOperation() { result = TExpr(augAssign.getOperation()) } + + override AstNode getChild(int index) { index = 0 and result = this.getOperation() } } /** An assignment expression / walrus operator (`x := expr`). */ @@ -229,6 +248,12 @@ module Ast implements AstSig { Expr getValue() { result = TExpr(assignExpr.getValue()) } Expr getTarget() { result = TExpr(assignExpr.getTarget()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getValue() + or + index = 1 and result = this.getTarget() + } } /** @@ -256,6 +281,14 @@ module Ast implements AstSig { /** Gets the `else` (false) branch, if any. */ Stmt getElse() { result = TBlockStmt(ifStmt, "orelse") } + + override AstNode getChild(int index) { + index = 0 and result = this.getCondition() + or + index = 1 and result = this.getThen() + or + index = 2 and result = this.getElse() + } } /** A loop statement. */ @@ -279,6 +312,14 @@ module Ast implements AstSig { /** Gets the `else` branch of this `while` loop, if any. */ Stmt getElse() { result = TBlockStmt(whileStmt, "orelse") } + + override AstNode getChild(int index) { + index = 0 and result = this.getCondition() + or + index = 1 and result = this.getBody() + or + index = 2 and result = this.getElse() + } } /** @@ -317,6 +358,16 @@ module Ast implements AstSig { /** Gets the `else` branch of this `for` loop, if any. */ Stmt getElse() { result = TBlockStmt(forStmt, "orelse") } + + override AstNode getChild(int index) { + index = 0 and result = this.getCollection() + or + index = 1 and result = this.getVariable() + or + index = 2 and result = this.getBody() + or + index = 3 and result = this.getElse() + } } /** A `break` statement. */ @@ -342,6 +393,8 @@ module Ast implements AstSig { /** Gets the expression being returned, if any. */ Expr getExpr() { result = TExpr(ret.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getExpr() } } /** A `raise` statement (mapped to `Throw`). */ @@ -355,6 +408,12 @@ module Ast implements AstSig { /** Gets the cause of this `raise`, if any. */ Expr getCause() { result = TExpr(raise.getCause()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getExpr() + or + index = 1 and result = this.getCause() + } } /** A `with` statement. */ @@ -368,6 +427,14 @@ module Ast implements AstSig { Expr getOptionalVars() { result = TExpr(withStmt.getOptionalVars()) } Stmt getBody() { result = TBlockStmt(withStmt, "body") } + + override AstNode getChild(int index) { + index = 0 and result = this.getContextExpr() + or + index = 1 and result = this.getOptionalVars() + or + index = 2 and result = this.getBody() + } } /** An `assert` statement. */ @@ -379,6 +446,12 @@ module Ast implements AstSig { Expr getTest() { result = TExpr(assertStmt.getTest()) } Expr getMsg() { result = TExpr(assertStmt.getMsg()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getTest() + or + index = 1 and result = this.getMsg() + } } /** A `delete` statement. */ @@ -388,6 +461,8 @@ module Ast implements AstSig { DeleteStmt() { del = this.asStmt() } Expr getTarget(int n) { result = TExpr(del.getTarget(n)) } + + override AstNode getChild(int index) { result = this.getTarget(index) } } /** A `try` statement. */ @@ -404,6 +479,16 @@ module Ast implements AstSig { Stmt getFinally() { result = TBlockStmt(tryStmt, "finally") } CatchClause getCatch(int index) { result = TStmt(tryStmt.getHandler(index)) } + + override AstNode getChild(int index) { + index = 0 and result = this.getBody() + or + result = this.getCatch(index - 1) and index >= 1 + or + index = -1 and result = this.getFinally() + or + index = -2 and result = this.getElse() + } } /** @@ -442,6 +527,14 @@ module Ast implements AstSig { or result = TBlockStmt(handler.(Py::ExceptGroupStmt), "body") } + + override AstNode getChild(int index) { + index = 0 and result = this.getType() + or + index = 1 and result = this.getVariable() + or + index = 2 and result = this.getBody() + } } /** A `match` statement, mapped to the shared CFG's `Switch`. */ @@ -455,6 +548,12 @@ module Ast implements AstSig { Case getCase(int index) { result = TStmt(matchStmt.getCase(index)) } Stmt getStmt(int index) { none() } + + override AstNode getChild(int index) { + index = 0 and result = this.getExpr() + or + result = this.getCase(index - 1) and index >= 1 + } } /** A `case` clause in a match statement. */ @@ -471,6 +570,14 @@ module Ast implements AstSig { /** Holds if this case is a wildcard pattern (`case _:`). */ predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } + + override AstNode getChild(int index) { + index = 0 and result = this.getAPattern() + or + index = 1 and result = this.getGuard() + or + index = 2 and result = this.getBody() + } } /** A wildcard case (`case _:`). */ @@ -492,6 +599,14 @@ module Ast implements AstSig { /** Gets the false branch of this expression. */ Expr getElse() { result = TExpr(ifExp.getOrelse()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getCondition() + or + index = 1 and result = this.getThen() + or + index = 2 and result = this.getElse() + } } /** @@ -547,6 +662,12 @@ module Ast implements AstSig { result = TBoolExprPair(be, i + 1) ) } + + override AstNode getChild(int index) { + index = 0 and result = this.getLeftOperand() + or + index = 1 and result = this.getRightOperand() + } } /** A short-circuiting logical `and` expression. */ @@ -582,6 +703,8 @@ module Ast implements AstSig { /** Gets the operand of this unary expression. */ Expr getOperand() { result = TExpr(this.asExpr().(Py::UnaryExpr).getOperand()) } + + override AstNode getChild(int index) { index = 0 and result = this.getOperand() } } /** A logical `not` expression. */ @@ -633,6 +756,12 @@ module Ast implements AstSig { Expr getLeft() { result = TExpr(binExpr.getLeft()) } Expr getRight() { result = TExpr(binExpr.getRight()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getLeft() + or + index = 1 and result = this.getRight() + } } /** A call expression (`func(args...)`). */ @@ -654,6 +783,15 @@ module Ast implements AstSig { } int getNumberOfNamedArgs() { result = count(call.getANamedArg()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getFunc() + or + result = this.getPositionalArg(index - 1) and index >= 1 + or + result = this.getKeywordValue(index - 1 - this.getNumberOfPositionalArgs()) and + index >= 1 + this.getNumberOfPositionalArgs() + } } /** A subscript expression (`obj[index]`). */ @@ -665,6 +803,12 @@ module Ast implements AstSig { Expr getObject() { result = TExpr(sub.getObject()) } Expr getIndex() { result = TExpr(sub.getIndex()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getObject() + or + index = 1 and result = this.getIndex() + } } /** An attribute access (`obj.name`). */ @@ -674,6 +818,8 @@ module Ast implements AstSig { AttributeExpr() { attr = this.asExpr() } Expr getObject() { result = TExpr(attr.getObject()) } + + override AstNode getChild(int index) { index = 0 and result = this.getObject() } } /** A tuple literal. */ @@ -683,6 +829,8 @@ module Ast implements AstSig { TupleExpr() { tuple = this.asExpr() } Expr getElt(int n) { result = TExpr(tuple.getElt(n)) } + + override AstNode getChild(int index) { result = this.getElt(index) } } /** A list literal. */ @@ -692,6 +840,8 @@ module Ast implements AstSig { ListExpr() { list = this.asExpr() } Expr getElt(int n) { result = TExpr(list.getElt(n)) } + + override AstNode getChild(int index) { result = this.getElt(index) } } /** A set literal. */ @@ -701,6 +851,8 @@ module Ast implements AstSig { SetExpr() { set = this.asExpr() } Expr getElt(int n) { result = TExpr(set.getElt(n)) } + + override AstNode getChild(int index) { result = this.getElt(index) } } /** A dict literal. */ @@ -718,6 +870,14 @@ module Ast implements AstSig { Expr getValue(int n) { result = TExpr(dict.getItem(n).(Py::KeyValuePair).getValue()) } int getNumberOfItems() { result = count(dict.getAnItem()) } + + override AstNode getChild(int index) { + exists(int item | + index = 2 * item and result = this.getKey(item) + or + index = 2 * item + 1 and result = this.getValue(item) + ) + } } /** A unary expression other than `not` (e.g., `-x`, `+x`, `~x`). */ @@ -727,6 +887,8 @@ module Ast implements AstSig { ArithUnaryExpr() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } Expr getOperand() { result = TExpr(unaryExpr.getOperand()) } + + override AstNode getChild(int index) { index = 0 and result = this.getOperand() } } /** @@ -748,6 +910,8 @@ module Ast implements AstSig { } Expr getIterable() { result = TExpr(iterable) } + + override AstNode getChild(int index) { index = 0 and result = this.getIterable() } } /** A comparison expression (`a < b`, `a < b < c`, etc.). */ @@ -759,6 +923,12 @@ module Ast implements AstSig { Expr getLeft() { result = TExpr(cmp.getLeft()) } Expr getComparator(int n) { result = TExpr(cmp.getComparator(n)) } + + override AstNode getChild(int index) { + index = 0 and result = this.getLeft() + or + result = this.getComparator(index - 1) and index >= 1 + } } /** A slice expression (`start:stop:step`). */ @@ -772,6 +942,14 @@ module Ast implements AstSig { Expr getStop() { result = TExpr(slice.getStop()) } Expr getStep() { result = TExpr(slice.getStep()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getStart() + or + index = 1 and result = this.getStop() + or + index = 2 and result = this.getStep() + } } /** A starred expression (`*x`). */ @@ -781,6 +959,8 @@ module Ast implements AstSig { StarredExpr() { starred = this.asExpr() } Expr getValue() { result = TExpr(starred.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getValue() } } /** A formatted string literal (`f"...{expr}..."`). */ @@ -790,6 +970,8 @@ module Ast implements AstSig { FstringExpr() { fstring = this.asExpr() } Expr getValue(int n) { result = TExpr(fstring.getValue(n)) } + + override AstNode getChild(int index) { result = this.getValue(index) } } /** A formatted value inside an f-string (`{expr}` or `{expr:spec}`). */ @@ -801,6 +983,12 @@ module Ast implements AstSig { Expr getValue() { result = TExpr(fv.getValue()) } Expr getFormatSpec() { result = TExpr(fv.getFormatSpec()) } + + override AstNode getChild(int index) { + index = 0 and result = this.getValue() + or + index = 1 and result = this.getFormatSpec() + } } /** A `yield` expression. */ @@ -810,6 +998,8 @@ module Ast implements AstSig { YieldExpr() { yield = this.asExpr() } Expr getValue() { result = TExpr(yield.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getValue() } } /** A `yield from` expression. */ @@ -819,6 +1009,8 @@ module Ast implements AstSig { YieldFromExpr() { yieldFrom = this.asExpr() } Expr getValue() { result = TExpr(yieldFrom.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getValue() } } /** An `await` expression. */ @@ -828,6 +1020,8 @@ module Ast implements AstSig { AwaitExpr() { await = this.asExpr() } Expr getValue() { result = TExpr(await.getValue()) } + + override AstNode getChild(int index) { index = 0 and result = this.getValue() } } /** A class definition expression (has base classes evaluated at definition time). */ @@ -837,6 +1031,8 @@ module Ast implements AstSig { ClassDefExpr() { classExpr = this.asExpr() } Expr getBase(int n) { result = TExpr(classExpr.getBase(n)) } + + override AstNode getChild(int index) { result = this.getBase(index) } } /** A function definition expression (has default args evaluated at definition time). */ @@ -863,6 +1059,12 @@ module Ast implements AstSig { } int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } + + override AstNode getChild(int index) { + result = this.getDefault(index) + or + result = this.getKwDefault(index - this.getNumberOfDefaults()) + } } /** A lambda expression (has default args evaluated at definition time). */ @@ -884,248 +1086,16 @@ module Ast implements AstSig { } int getNumberOfDefaults() { result = count(lambda.getArgs().getADefault()) } - } - /** Gets the child of `n` at the specified (zero-based) index. */ - AstNode getChild(AstNode n, int index) { - // BlockStmt: indexed statements - result = n.(BlockStmt).getStmt(index) - or - // IfStmt: condition (0), then (1), else (2) - exists(IfStmt ifStmt | ifStmt = n | - index = 0 and result = ifStmt.getCondition() - or - index = 1 and result = ifStmt.getThen() - or - index = 2 and result = ifStmt.getElse() - ) - or - // ExprStmt: the expression (0) - index = 0 and result = n.(ExprStmt).getExpr() - or - // Assign: value (0), targets (1..n) - exists(AssignStmt a | a = n | - index = 0 and result = a.getValue() - or - result = a.getTarget(index - 1) and index >= 1 - ) - or - // AugAssign: the operation (0) - index = 0 and result = n.(AugAssignStmt).getOperation() - or - // Walrus (`x := expr`): value (0), target (1) - exists(NamedExpr ne | ne = n | - index = 0 and result = ne.getValue() - or - index = 1 and result = ne.getTarget() - ) - or - // WhileStmt: condition (0), body (1), orelse (2) - exists(WhileStmt w | w = n | - index = 0 and result = w.getCondition() - or - index = 1 and result = w.getBody() - or - index = 2 and result = w.getElse() - ) - or - // ForeachStmt: collection (0), variable (1), body (2), orelse (3) - exists(ForeachStmt f | f = n | - index = 0 and result = f.getCollection() - or - index = 1 and result = f.getVariable() - or - index = 2 and result = f.getBody() - or - index = 3 and result = f.getElse() - ) - or - // ReturnStmt: the value (0) - index = 0 and result = n.(ReturnStmt).getExpr() - or - // AssertStmt: test (0), message (1) - exists(AssertStmt a | a = n | - index = 0 and result = a.getTest() - or - index = 1 and result = a.getMsg() - ) - or - // DeleteStmt: targets left to right - result = n.(DeleteStmt).getTarget(index) - or - // WithStmt: context expr (0), optional vars (1), body (2) - exists(WithStmt w | w = n | - index = 0 and result = w.getContextExpr() - or - index = 1 and result = w.getOptionalVars() - or - index = 2 and result = w.getBody() - ) - or - // Throw (raise): exception (0), cause (1) - exists(Throw r | r = n | - index = 0 and result = r.getExpr() - or - index = 1 and result = r.getCause() - ) - or - // TryStmt: body (0), handlers (1..n), else (-2), finally (-1) - exists(TryStmt t | t = n | - index = 0 and result = t.getBody() - or - result = t.getCatch(index - 1) and index >= 1 - or - index = -1 and result = t.getFinally() - or - index = -2 and result = t.getElse() - ) - or - // Switch (match): subject (0), cases (1..n) - exists(Switch m | m = n | - index = 0 and result = m.getExpr() - or - result = m.getCase(index - 1) and index >= 1 - ) - or - // Case: pattern (0), guard (1), body (2) - exists(Case c | c = n | - index = 0 and result = c.getAPattern() - or - index = 1 and result = c.getGuard() - or - index = 2 and result = c.getBody() - ) - or - // CatchClause (except handler): type (0), name (1), body (2) - exists(CatchClause h | h = n | - index = 0 and result = h.getType() - or - index = 1 and result = h.getVariable() - or - index = 2 and result = h.getBody() - ) - or - // ConditionalExpr (IfExp): condition (0), then (1), else (2) - exists(ConditionalExpr ie | ie = n | - index = 0 and result = ie.getCondition() + override AstNode getChild(int index) { + result = this.getDefault(index) or - index = 1 and result = ie.getThen() - or - index = 2 and result = ie.getElse() - ) - or - // Call: func (0), positional args (1..n), keyword values (n+1..n+k) - exists(CallExpr call | call = n | - index = 0 and result = call.getFunc() - or - result = call.getPositionalArg(index - 1) and index >= 1 - or - result = call.getKeywordValue(index - 1 - call.getNumberOfPositionalArgs()) and - index >= 1 + call.getNumberOfPositionalArgs() - ) - or - // Python BinaryExpr (arithmetic, bitwise, matmul, etc.): left (0), right (1) - exists(ArithBinaryExpr be | be = n | - index = 0 and result = be.getLeft() - or - index = 1 and result = be.getRight() - ) - or - // Subscript (obj[index]): object (0), index (1) - exists(SubscriptExpr sub | sub = n | - index = 0 and result = sub.getObject() - or - index = 1 and result = sub.getIndex() - ) - or - // Attribute (obj.name): object (0) - index = 0 and result = n.(AttributeExpr).getObject() - or - // Comprehension/generator: iterable (0) - index = 0 and result = n.(Comprehension).getIterable() - or - // Tuple, List, Set: elements left to right - result = n.(TupleExpr).getElt(index) - or - result = n.(ListExpr).getElt(index) - or - result = n.(SetExpr).getElt(index) - or - // Dict: key(0), value(0), key(1), value(1), ... - exists(DictExpr d, int item | d = n | - index = 2 * item and result = d.getKey(item) - or - index = 2 * item + 1 and result = d.getValue(item) - ) - or - // Arithmetic unary (-x, +x, ~x): operand (0) - index = 0 and result = n.(ArithUnaryExpr).getOperand() - or - // Compare (a < b < c): left (0), comparators (1..n) - exists(CompareExpr cmp | cmp = n | - index = 0 and result = cmp.getLeft() - or - result = cmp.getComparator(index - 1) and index >= 1 - ) - or - // Slice (start:stop:step): start (0), stop (1), step (2) - exists(SliceExpr sl | sl = n | - index = 0 and result = sl.getStart() - or - index = 1 and result = sl.getStop() - or - index = 2 and result = sl.getStep() - ) - or - // Starred (*x): value (0) - index = 0 and result = n.(StarredExpr).getValue() - or - // Fstring: values left to right - result = n.(FstringExpr).getValue(index) - or - // FormattedValue ({expr} or {expr:spec}): value (0), format spec (1) - exists(FormattedValueExpr fv | fv = n | - index = 0 and result = fv.getValue() - or - index = 1 and result = fv.getFormatSpec() - ) - or - // Yield: value (0) - index = 0 and result = n.(YieldExpr).getValue() - or - // YieldFrom: value (0) - index = 0 and result = n.(YieldFromExpr).getValue() - or - // Await: value (0) - index = 0 and result = n.(AwaitExpr).getValue() - or - // ClassExpr: base classes left to right - result = n.(ClassDefExpr).getBase(index) - or - // FunctionExpr: defaults left to right, then kw defaults - exists(FunctionDefExpr fe | fe = n | - result = fe.getDefault(index) - or - result = fe.getKwDefault(index - fe.getNumberOfDefaults()) - ) - or - // Lambda: defaults left to right, then kw defaults - exists(LambdaExpr lam | lam = n | - result = lam.getDefault(index) - or - result = lam.getKwDefault(index - lam.getNumberOfDefaults()) - ) - or - // LogicalNotExpr: operand (0) - index = 0 and result = n.(LogicalNotExpr).getOperand() - or - // BinaryExpr (`and`/`or`): left (0), right (1) - exists(BinaryExpr be | be = n | - index = 0 and result = be.getLeftOperand() - or - index = 1 and result = be.getRightOperand() - ) + result = this.getKwDefault(index - this.getNumberOfDefaults()) + } } + + /** Gets the child of `n` at the specified (zero-based) index. */ + AstNode getChild(AstNode n, int index) { result = n.getChild(index) } } private module Cfg0 = Make0; From 372944b4b9619d831181471199ac13a9ad2ffcd0 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 5 May 2026 15:26:20 +0000 Subject: [PATCH 36/72] Python: adapt to new shared CFG signature Main added two new requirements to AstSig: - A 'Parameter' class with a 'getDefaultValue()' method, plus a 'callableGetParameter(Callable, int)' predicate. - A 'CallableContext' class in InputSig1, replacing the previous 'CallableBodyPartContext'. Add stub implementations: 'Parameter' is empty (none()) and 'callableGetParameter' returns nothing, mirroring Java's TODO. Rename 'CallableBodyPartContext = Void' to 'CallableContext = Void' in the Python Input module. NewCfg evaluation-order tests still pass at the 22/24 baseline; all 11 shared-CFG consistency queries still report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/controlflow/internal/AstNodeImpl.qll | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 86fdf45e0baa..fab2d1cd2e5b 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -165,6 +165,20 @@ module Ast implements AstSig { /** Gets the body of callable `c`. */ AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope(), "body") } + /** + * A parameter of a callable. + * + * TODO: Implement in order to include parameters in the CFG. + */ + class Parameter extends AstNode { + Parameter() { none() } + + Expr getDefaultValue() { none() } + } + + /** Gets the `index`th parameter of callable `c`. */ + Parameter callableGetParameter(Callable c, int index) { none() } + /** A statement. */ class Stmt extends AstNode { Stmt() { this instanceof TStmt or this instanceof TBlockStmt } @@ -1119,7 +1133,7 @@ private module Input implements InputSig1, InputSig2 { string toString() { result = "label" } } - class CallableBodyPartContext = Void; + class CallableContext = Void; predicate inConditionalContext(Ast::AstNode n, ConditionKind kind) { kind.isBoolean() and From 4dbd9043655eff201a488a5149bff29f8ac8abcd Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 11:06:40 +0000 Subject: [PATCH 37/72] Python: dispatch toString/getLocation/getEnclosingCallable per branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the three big disjunctive predicates on AstNode with empty defaults plus per-newtype-branch override classes: AstNode.toString() { none() } AstNode.getLocation() { none() } AstNode.getEnclosingCallable() { none() } Six private subclasses (one per newtype branch — TStmt, TExpr, TScope, TPattern, TBoolExprPair, TBlockStmt) override these with the branch-specific implementation. This mirrors the per-class dispatch already used for getChild. No behaviour change: all 24 NewCfg evaluation-order tests pass and all 11 shared-CFG consistency queries still report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 141 +++++++++++------- 1 file changed, 91 insertions(+), 50 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index fab2d1cd2e5b..fc6fdd68c2c9 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -77,60 +77,13 @@ module Ast implements AstSig { /** An AST node visible to the shared CFG. */ class AstNode extends TAstNode { /** Gets a textual representation of this AST node. */ - string toString() { - exists(Py::Stmt s | this = TStmt(s) and result = s.toString()) - or - exists(Py::Expr e | this = TExpr(e) and result = e.toString()) - or - exists(Py::Scope sc | this = TScope(sc) and result = sc.toString()) - or - exists(Py::Pattern p | this = TPattern(p) and result = p.toString()) - or - exists(Py::BoolExpr be | this = TBoolExprPair(be, _) and result = be.getOperator()) - or - exists(string slot | this = TBlockStmt(_, slot) and result = "block:" + slot) - } + string toString() { none() } /** Gets the location of this AST node. */ - Py::Location getLocation() { - exists(Py::Stmt s | this = TStmt(s) and result = s.getLocation()) - or - exists(Py::Expr e | this = TExpr(e) and result = e.getLocation()) - or - exists(Py::Scope sc | this = TScope(sc) and result = sc.getLocation()) - or - exists(Py::Pattern p | this = TPattern(p) and result = p.getLocation()) - or - exists(Py::BoolExpr be, int index | - this = TBoolExprPair(be, index) and result = be.getValue(index).getLocation() - ) - or - // BlockStmt has no native location; approximate with the first - // item's location. - exists(Py::AstNode parent, string slot | - this = TBlockStmt(parent, slot) and - result = getBodyStmtList(parent, slot).getItem(0).getLocation() - ) - } + Py::Location getLocation() { none() } /** Gets the enclosing callable that contains this node, if any. */ - Callable getEnclosingCallable() { - exists(Py::Stmt s | this = TStmt(s) and result.asScope() = s.getScope()) - or - exists(Py::Expr e | this = TExpr(e) and result.asScope() = e.getScope()) - or - exists(Py::Scope sc | this = TScope(sc) and result.asScope() = sc.getEnclosingScope()) - or - exists(Py::Pattern p | this = TPattern(p) and result.asScope() = p.getScope()) - or - exists(Py::BoolExpr be | this = TBoolExprPair(be, _) and result.asScope() = be.getScope()) - or - exists(Py::AstNode parent | this = TBlockStmt(parent, _) | - result.asScope() = parent.(Py::Scope) - or - result.asScope() = parent.(Py::Stmt).getScope() - ) - } + Callable getEnclosingCallable() { none() } /** Gets the underlying Python `Stmt`, if this node wraps one. */ Py::Stmt asStmt() { this = TStmt(result) } @@ -152,6 +105,94 @@ module Ast implements AstSig { AstNode getChild(int index) { none() } } + /** Implementation of `AstNode` predicates for `TStmt` nodes. */ + private class TStmtAstNode extends AstNode, TStmt { + private Py::Stmt s; + + TStmtAstNode() { this = TStmt(s) } + + override string toString() { result = s.toString() } + + override Py::Location getLocation() { result = s.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = s.getScope() } + } + + /** Implementation of `AstNode` predicates for `TExpr` nodes. */ + private class TExprAstNode extends AstNode, TExpr { + private Py::Expr e; + + TExprAstNode() { this = TExpr(e) } + + override string toString() { result = e.toString() } + + override Py::Location getLocation() { result = e.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = e.getScope() } + } + + /** Implementation of `AstNode` predicates for `TScope` nodes. */ + private class TScopeAstNode extends AstNode, TScope { + private Py::Scope sc; + + TScopeAstNode() { this = TScope(sc) } + + override string toString() { result = sc.toString() } + + override Py::Location getLocation() { result = sc.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = sc.getEnclosingScope() } + } + + /** Implementation of `AstNode` predicates for `TPattern` nodes. */ + private class TPatternAstNode extends AstNode, TPattern { + private Py::Pattern p; + + TPatternAstNode() { this = TPattern(p) } + + override string toString() { result = p.toString() } + + override Py::Location getLocation() { result = p.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = p.getScope() } + } + + /** Implementation of `AstNode` predicates for synthetic `TBoolExprPair` nodes. */ + private class TBoolExprPairAstNode extends AstNode, TBoolExprPair { + private Py::BoolExpr be; + private int index; + + TBoolExprPairAstNode() { this = TBoolExprPair(be, index) } + + override string toString() { result = be.getOperator() } + + override Py::Location getLocation() { result = be.getValue(index).getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = be.getScope() } + } + + /** Implementation of `AstNode` predicates for synthetic `TBlockStmt` nodes. */ + private class TBlockStmtAstNode extends AstNode, TBlockStmt { + private Py::AstNode parent; + private string slot; + + TBlockStmtAstNode() { this = TBlockStmt(parent, slot) } + + override string toString() { result = "block:" + slot } + + // BlockStmt has no native location; approximate with the first + // item's location. + override Py::Location getLocation() { + result = getBodyStmtList(parent, slot).getItem(0).getLocation() + } + + override Callable getEnclosingCallable() { + result.asScope() = parent.(Py::Scope) + or + result.asScope() = parent.(Py::Stmt).getScope() + } + } + /** Gets the immediately enclosing callable that contains `node`. */ Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } From 19b9aa8ba8e00f5536df049453ae322f0a0de0be Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 11:31:07 +0000 Subject: [PATCH 38/72] Python: merge T*AstNode wrappers into matching public classes Five of the six per-newtype-branch wrapper classes had a natural public class corresponding to that branch: TStmtAstNode -> Stmt (TStmt subset; BlockStmt overrides for TBlockStmt) TExprAstNode -> Expr (TExpr subset; BoolExprPair overrides for TBoolExprPair) TScopeAstNode -> Callable (= TScope exactly) TPatternAstNode -> Pattern (= TPattern exactly) TBlockStmtAstNode -> BlockStmt (= TBlockStmt exactly) Move toString/getLocation/getEnclosingCallable onto these classes and delete the wrappers. The sixth wrapper (TBoolExprPair) has no exact public counterpart - BinaryExpr is broader, including TExpr-branch BoolExprs - so it remains as a small private class, renamed BoolExprPair. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 132 +++++++----------- 1 file changed, 54 insertions(+), 78 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index fc6fdd68c2c9..212f1c200536 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -105,64 +105,12 @@ module Ast implements AstSig { AstNode getChild(int index) { none() } } - /** Implementation of `AstNode` predicates for `TStmt` nodes. */ - private class TStmtAstNode extends AstNode, TStmt { - private Py::Stmt s; - - TStmtAstNode() { this = TStmt(s) } - - override string toString() { result = s.toString() } - - override Py::Location getLocation() { result = s.getLocation() } - - override Callable getEnclosingCallable() { result.asScope() = s.getScope() } - } - - /** Implementation of `AstNode` predicates for `TExpr` nodes. */ - private class TExprAstNode extends AstNode, TExpr { - private Py::Expr e; - - TExprAstNode() { this = TExpr(e) } - - override string toString() { result = e.toString() } - - override Py::Location getLocation() { result = e.getLocation() } - - override Callable getEnclosingCallable() { result.asScope() = e.getScope() } - } - - /** Implementation of `AstNode` predicates for `TScope` nodes. */ - private class TScopeAstNode extends AstNode, TScope { - private Py::Scope sc; - - TScopeAstNode() { this = TScope(sc) } - - override string toString() { result = sc.toString() } - - override Py::Location getLocation() { result = sc.getLocation() } - - override Callable getEnclosingCallable() { result.asScope() = sc.getEnclosingScope() } - } - - /** Implementation of `AstNode` predicates for `TPattern` nodes. */ - private class TPatternAstNode extends AstNode, TPattern { - private Py::Pattern p; - - TPatternAstNode() { this = TPattern(p) } - - override string toString() { result = p.toString() } - - override Py::Location getLocation() { result = p.getLocation() } - - override Callable getEnclosingCallable() { result.asScope() = p.getScope() } - } - /** Implementation of `AstNode` predicates for synthetic `TBoolExprPair` nodes. */ - private class TBoolExprPairAstNode extends AstNode, TBoolExprPair { + private class BoolExprPair extends Expr, TBoolExprPair { private Py::BoolExpr be; private int index; - TBoolExprPairAstNode() { this = TBoolExprPair(be, index) } + BoolExprPair() { this = TBoolExprPair(be, index) } override string toString() { result = be.getOperator() } @@ -171,28 +119,6 @@ module Ast implements AstSig { override Callable getEnclosingCallable() { result.asScope() = be.getScope() } } - /** Implementation of `AstNode` predicates for synthetic `TBlockStmt` nodes. */ - private class TBlockStmtAstNode extends AstNode, TBlockStmt { - private Py::AstNode parent; - private string slot; - - TBlockStmtAstNode() { this = TBlockStmt(parent, slot) } - - override string toString() { result = "block:" + slot } - - // BlockStmt has no native location; approximate with the first - // item's location. - override Py::Location getLocation() { - result = getBodyStmtList(parent, slot).getItem(0).getLocation() - } - - override Callable getEnclosingCallable() { - result.asScope() = parent.(Py::Scope) - or - result.asScope() = parent.(Py::Stmt).getScope() - } - } - /** Gets the immediately enclosing callable that contains `node`. */ Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } @@ -201,7 +127,17 @@ module Ast implements AstSig { * * In Python, all three are executable scopes with statement bodies. */ - class Callable extends AstNode, TScope { } + class Callable extends AstNode, TScope { + private Py::Scope sc; + + Callable() { this = TScope(sc) } + + override string toString() { result = sc.toString() } + + override Py::Location getLocation() { result = sc.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = sc.getEnclosingScope() } + } /** Gets the body of callable `c`. */ AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope(), "body") } @@ -223,15 +159,41 @@ module Ast implements AstSig { /** A statement. */ class Stmt extends AstNode { Stmt() { this instanceof TStmt or this instanceof TBlockStmt } + + // For `TStmt` instances, delegate to the wrapped Python statement. + // `BlockStmt` (the only `TBlockStmt` subclass) provides its own overrides. + override string toString() { result = this.asStmt().toString() } + + override Py::Location getLocation() { result = this.asStmt().getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = this.asStmt().getScope() } } /** An expression. */ class Expr extends AstNode { Expr() { this instanceof TExpr or this instanceof TBoolExprPair } + + // For `TExpr` instances, delegate to the wrapped Python expression. + // `BoolExprPair` (the only `TBoolExprPair` subclass) provides its own overrides. + override string toString() { result = this.asExpr().toString() } + + override Py::Location getLocation() { result = this.asExpr().getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = this.asExpr().getScope() } } /** A pattern in a `match` statement. */ - additional class Pattern extends AstNode, TPattern { } + additional class Pattern extends AstNode, TPattern { + private Py::Pattern p; + + Pattern() { this = TPattern(p) } + + override string toString() { result = p.toString() } + + override Py::Location getLocation() { result = p.getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = p.getScope() } + } /** * A block statement, modeling the body of a parent AST node as a @@ -249,6 +211,20 @@ module Ast implements AstSig { /** Gets the last statement in this block. */ Stmt getLastStmt() { result = TStmt(getBodyStmtList(parent, slot).getLastItem()) } + override string toString() { result = "block:" + slot } + + // BlockStmt has no native location; approximate with the first + // item's location. + override Py::Location getLocation() { + result = getBodyStmtList(parent, slot).getItem(0).getLocation() + } + + override Callable getEnclosingCallable() { + result.asScope() = parent.(Py::Scope) + or + result.asScope() = parent.(Py::Stmt).getScope() + } + override AstNode getChild(int index) { result = this.getStmt(index) } } From 7176fd8dbc1c0def5a7e7e14bb1a4abc41e13b67 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 12:04:54 +0000 Subject: [PATCH 39/72] Python: unify Py::BoolExpr handling via TBoolExprPair Previously a Py::BoolExpr appeared in two newtype branches: as TExpr(be) (the outermost pair) and TBoolExprPair(be, i) for inner pairs of 3+ operand expressions. This forced BinaryExpr/LogicalAndExpr/LogicalOrExpr to disjoin two cases, and the synthetic-pair handling spanned multiple layers. Restrict TExpr to non-BoolExpr Py::Expr, and extend TBoolExprPair to cover every operand pair (index 0..n-2). Now every Py::BoolExpr is represented uniformly as TBoolExprPair(_, 0) for the whole expression and TBoolExprPair(_, i) for inner pairs. Extend AstNode.asExpr() to also recover the underlying Py::BoolExpr from TBoolExprPair(_, 0). This makes asExpr() the inverse of construction: every 'result = TExpr(e)' turns into 'result.asExpr() = e', which works uniformly for BoolExprs and non-BoolExprs alike. Consequences: - BinaryExpr now extends TBoolExprPair directly with a single uniform rule for left/right operands. - LogicalAndExpr/LogicalOrExpr are one-line char preds via getBoolExpr(). - The private BoolExprPair wrapper class folds into BinaryExpr. - 60+ leaf wrappers now read 'result.asExpr() = py_expr' instead of 'result = TExpr(py_expr)'. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 254 ++++++++---------- 1 file changed, 108 insertions(+), 146 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 212f1c200536..bcf8f4f64705 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -55,18 +55,18 @@ module Ast implements AstSig { private newtype TAstNode = TStmt(Py::Stmt s) or - TExpr(Py::Expr e) or + TExpr(Py::Expr e) { not e instanceof Py::BoolExpr } or TScope(Py::Scope sc) or TPattern(Py::Pattern p) or /** - * A synthetic intermediate node in a multi-operand `and`/`or` + * A synthetic node representing an operand pair of an `and`/`or` * expression. For `a and b and c` (operands 0, 1, 2) we model the - * operation as a right-nested tree where the inner pair at index 1 - * represents `b and c` and is the right operand of the outer pair. - * The outermost pair (index 0) is represented by the underlying - * `Py::BoolExpr` itself via `TExpr`. + * operation as a right-nested tree: pair 0 represents the whole + * expression with left=a and right=pair 1; pair 1 represents + * `b and c` with left=b and right=c. Each Python `Py::BoolExpr` + * with `n` operands has `n - 1` such pairs (indices `0 .. n - 2`). */ - TBoolExprPair(Py::BoolExpr be, int index) { index = [1 .. count(be.getAValue()) - 2] } or + TBoolExprPair(Py::BoolExpr be, int index) { index = [0 .. count(be.getAValue()) - 2] } or /** * A synthetic block statement, identifying one body slot of the * `parent` AST node. The `slot` string disambiguates among multiple @@ -88,8 +88,17 @@ module Ast implements AstSig { /** Gets the underlying Python `Stmt`, if this node wraps one. */ Py::Stmt asStmt() { this = TStmt(result) } - /** Gets the underlying Python `Expr`, if this node wraps one. */ - Py::Expr asExpr() { this = TExpr(result) } + /** + * Gets the underlying Python `Expr`, if this node wraps one. Boolean + * expressions are represented by `TBoolExprPair(_, 0)`; this + * predicate also recovers the underlying `Py::BoolExpr` from such a + * representation. + */ + Py::Expr asExpr() { + this = TExpr(result) + or + this = TBoolExprPair(result, 0) + } /** Gets the underlying Python `Scope`, if this node wraps one. */ Py::Scope asScope() { this = TScope(result) } @@ -105,20 +114,6 @@ module Ast implements AstSig { AstNode getChild(int index) { none() } } - /** Implementation of `AstNode` predicates for synthetic `TBoolExprPair` nodes. */ - private class BoolExprPair extends Expr, TBoolExprPair { - private Py::BoolExpr be; - private int index; - - BoolExprPair() { this = TBoolExprPair(be, index) } - - override string toString() { result = be.getOperator() } - - override Py::Location getLocation() { result = be.getValue(index).getLocation() } - - override Callable getEnclosingCallable() { result.asScope() = be.getScope() } - } - /** Gets the immediately enclosing callable that contains `node`. */ Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } @@ -174,7 +169,7 @@ module Ast implements AstSig { Expr() { this instanceof TExpr or this instanceof TBoolExprPair } // For `TExpr` instances, delegate to the wrapped Python expression. - // `BoolExprPair` (the only `TBoolExprPair` subclass) provides its own overrides. + // `BinaryExpr` (the only `TBoolExprPair` subclass) provides its own overrides. override string toString() { result = this.asExpr().toString() } override Py::Location getLocation() { result = this.asExpr().getLocation() } @@ -235,7 +230,7 @@ module Ast implements AstSig { ExprStmt() { exprStmt = this.asStmt() } /** Gets the expression in this expression statement. */ - Expr getExpr() { result = TExpr(exprStmt.getValue()) } + Expr getExpr() { result.asExpr() = exprStmt.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getExpr() } } @@ -246,9 +241,9 @@ module Ast implements AstSig { AssignStmt() { assign = this.asStmt() } - Expr getValue() { result = TExpr(assign.getValue()) } + Expr getValue() { result.asExpr() = assign.getValue() } - Expr getTarget(int n) { result = TExpr(assign.getTarget(n)) } + Expr getTarget(int n) { result.asExpr() = assign.getTarget(n) } int getNumberOfTargets() { result = count(assign.getATarget()) } @@ -265,7 +260,7 @@ module Ast implements AstSig { AugAssignStmt() { augAssign = this.asStmt() } - Expr getOperation() { result = TExpr(augAssign.getOperation()) } + Expr getOperation() { result.asExpr() = augAssign.getOperation() } override AstNode getChild(int index) { index = 0 and result = this.getOperation() } } @@ -276,9 +271,9 @@ module Ast implements AstSig { NamedExpr() { assignExpr = this.asExpr() } - Expr getValue() { result = TExpr(assignExpr.getValue()) } + Expr getValue() { result.asExpr() = assignExpr.getValue() } - Expr getTarget() { result = TExpr(assignExpr.getTarget()) } + Expr getTarget() { result.asExpr() = assignExpr.getTarget() } override AstNode getChild(int index) { index = 0 and result = this.getValue() @@ -305,7 +300,7 @@ module Ast implements AstSig { Py::If asIf() { result = ifStmt } /** Gets the condition of this `if` statement. */ - Expr getCondition() { result = TExpr(ifStmt.getTest()) } + Expr getCondition() { result.asExpr() = ifStmt.getTest() } /** Gets the `then` (true) branch of this `if` statement. */ Stmt getThen() { result = TBlockStmt(ifStmt, "body") } @@ -337,7 +332,7 @@ module Ast implements AstSig { WhileStmt() { whileStmt = this.asStmt() } /** Gets the boolean condition of this `while` loop. */ - Expr getCondition() { result = TExpr(whileStmt.getTest()) } + Expr getCondition() { result.asExpr() = whileStmt.getTest() } override Stmt getBody() { result = TBlockStmt(whileStmt, "body") } @@ -380,10 +375,10 @@ module Ast implements AstSig { ForeachStmt() { forStmt = this.asStmt() } /** Gets the loop variable. */ - Expr getVariable() { result = TExpr(forStmt.getTarget()) } + Expr getVariable() { result.asExpr() = forStmt.getTarget() } /** Gets the collection being iterated. */ - Expr getCollection() { result = TExpr(forStmt.getIter()) } + Expr getCollection() { result.asExpr() = forStmt.getIter() } override Stmt getBody() { result = TBlockStmt(forStmt, "body") } @@ -423,7 +418,7 @@ module Ast implements AstSig { ReturnStmt() { ret = this.asStmt() } /** Gets the expression being returned, if any. */ - Expr getExpr() { result = TExpr(ret.getValue()) } + Expr getExpr() { result.asExpr() = ret.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getExpr() } } @@ -435,10 +430,10 @@ module Ast implements AstSig { Throw() { raise = this.asStmt() } /** Gets the expression being raised. */ - Expr getExpr() { result = TExpr(raise.getException()) } + Expr getExpr() { result.asExpr() = raise.getException() } /** Gets the cause of this `raise`, if any. */ - Expr getCause() { result = TExpr(raise.getCause()) } + Expr getCause() { result.asExpr() = raise.getCause() } override AstNode getChild(int index) { index = 0 and result = this.getExpr() @@ -453,9 +448,9 @@ module Ast implements AstSig { WithStmt() { withStmt = this.asStmt() } - Expr getContextExpr() { result = TExpr(withStmt.getContextExpr()) } + Expr getContextExpr() { result.asExpr() = withStmt.getContextExpr() } - Expr getOptionalVars() { result = TExpr(withStmt.getOptionalVars()) } + Expr getOptionalVars() { result.asExpr() = withStmt.getOptionalVars() } Stmt getBody() { result = TBlockStmt(withStmt, "body") } @@ -474,9 +469,9 @@ module Ast implements AstSig { AssertStmt() { assertStmt = this.asStmt() } - Expr getTest() { result = TExpr(assertStmt.getTest()) } + Expr getTest() { result.asExpr() = assertStmt.getTest() } - Expr getMsg() { result = TExpr(assertStmt.getMsg()) } + Expr getMsg() { result.asExpr() = assertStmt.getMsg() } override AstNode getChild(int index) { index = 0 and result = this.getTest() @@ -491,7 +486,7 @@ module Ast implements AstSig { DeleteStmt() { del = this.asStmt() } - Expr getTarget(int n) { result = TExpr(del.getTarget(n)) } + Expr getTarget(int n) { result.asExpr() = del.getTarget(n) } override AstNode getChild(int index) { result = this.getTarget(index) } } @@ -544,10 +539,10 @@ module Ast implements AstSig { CatchClause() { handler = this.asStmt() } /** Gets the type expression of this exception handler. */ - Expr getType() { result = TExpr(handler.getType()) } + Expr getType() { result.asExpr() = handler.getType() } /** Gets the variable name of this exception handler, if any. */ - AstNode getVariable() { result = TExpr(handler.getName()) } + AstNode getVariable() { result.asExpr() = handler.getName() } /** Holds: catch clauses do not have a `Condition` in Python's model. */ Expr getCondition() { none() } @@ -574,7 +569,7 @@ module Ast implements AstSig { Switch() { matchStmt = this.asStmt() } - Expr getExpr() { result = TExpr(matchStmt.getSubject()) } + Expr getExpr() { result.asExpr() = matchStmt.getSubject() } Case getCase(int index) { result = TStmt(matchStmt.getCase(index)) } @@ -595,7 +590,7 @@ module Ast implements AstSig { AstNode getAPattern() { result = TPattern(caseStmt.getPattern()) } - Expr getGuard() { result = TExpr(caseStmt.getGuard().(Py::Guard).getTest()) } + Expr getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } AstNode getBody() { result = TBlockStmt(caseStmt, "body") } @@ -623,13 +618,13 @@ module Ast implements AstSig { ConditionalExpr() { ifExp = this.asExpr() } /** Gets the condition of this expression. */ - Expr getCondition() { result = TExpr(ifExp.getTest()) } + Expr getCondition() { result.asExpr() = ifExp.getTest() } /** Gets the true branch of this expression. */ - Expr getThen() { result = TExpr(ifExp.getBody()) } + Expr getThen() { result.asExpr() = ifExp.getBody() } /** Gets the false branch of this expression. */ - Expr getElse() { result = TExpr(ifExp.getOrelse()) } + Expr getElse() { result.asExpr() = ifExp.getOrelse() } override AstNode getChild(int index) { index = 0 and result = this.getCondition() @@ -641,84 +636,51 @@ module Ast implements AstSig { } /** - * A binary expression for the shared CFG. In Python, this covers - * `and`/`or` expressions (both real 2-operand and synthetic pairs). + * A binary expression for the shared CFG. In Python, this covers all + * `and`/`or` expression operand pairs. */ - class BinaryExpr extends Expr { - BinaryExpr() { - exists(Py::BoolExpr be | this = TExpr(be) and count(be.getAValue()) >= 2) - or - this instanceof TBoolExprPair - } + class BinaryExpr extends Expr, TBoolExprPair { + private Py::BoolExpr be; + private int index; + + BinaryExpr() { this = TBoolExprPair(be, index) } + + /** Gets the underlying Python `BoolExpr`. */ + Py::BoolExpr getBoolExpr() { result = be } + + override string toString() { result = be.getOperator() } + + override Py::Location getLocation() { result = be.getValue(index).getLocation() } + + override Callable getEnclosingCallable() { result.asScope() = be.getScope() } /** Gets the left operand of this binary expression. */ - Expr getLeftOperand() { - exists(Py::BoolExpr be | this = TExpr(be) and result = TExpr(be.getValue(0))) - or - exists(Py::BoolExpr be, int i | - this = TBoolExprPair(be, i) and result = TExpr(be.getValue(i)) - ) - } + Expr getLeftOperand() { result.asExpr() = be.getValue(index) } /** Gets the right operand of this binary expression. */ Expr getRightOperand() { - // 2-operand BoolExpr: right operand is value(1). - exists(Py::BoolExpr be | - this = TExpr(be) and - count(be.getAValue()) = 2 and - result = TExpr(be.getValue(1)) - ) + // Last pair: right operand is the final value. + index = count(be.getAValue()) - 2 and result.asExpr() = be.getValue(index + 1) or - // 3+ operand BoolExpr (outermost): right operand is the synthetic - // pair at index 1. - exists(Py::BoolExpr be | - this = TExpr(be) and - count(be.getAValue()) > 2 and - result = TBoolExprPair(be, 1) - ) - or - // Last synthetic pair: right operand is the final value. - exists(Py::BoolExpr be, int i, int n | - this = TBoolExprPair(be, i) and - n = count(be.getAValue()) and - i = n - 2 and - result = TExpr(be.getValue(i + 1)) - ) - or - // Non-last synthetic pair: right operand is the next pair. - exists(Py::BoolExpr be, int i, int n | - this = TBoolExprPair(be, i) and - n = count(be.getAValue()) and - i < n - 2 and - result = TBoolExprPair(be, i + 1) - ) + // Non-last pair: right operand is the next synthetic pair. + index < count(be.getAValue()) - 2 and result = TBoolExprPair(be, index + 1) } - override AstNode getChild(int index) { - index = 0 and result = this.getLeftOperand() + override AstNode getChild(int childIndex) { + childIndex = 0 and result = this.getLeftOperand() or - index = 1 and result = this.getRightOperand() + childIndex = 1 and result = this.getRightOperand() } } /** A short-circuiting logical `and` expression. */ class LogicalAndExpr extends BinaryExpr { - LogicalAndExpr() { - exists(Py::BoolExpr be | - be.getOp() instanceof Py::And and - (this = TExpr(be) or this = TBoolExprPair(be, _)) - ) - } + LogicalAndExpr() { this.getBoolExpr().getOp() instanceof Py::And } } /** A short-circuiting logical `or` expression. */ class LogicalOrExpr extends BinaryExpr { - LogicalOrExpr() { - exists(Py::BoolExpr be | - be.getOp() instanceof Py::Or and - (this = TExpr(be) or this = TBoolExprPair(be, _)) - ) - } + LogicalOrExpr() { this.getBoolExpr().getOp() instanceof Py::Or } } /** A null-coalescing expression. Python has no null-coalescing operator. */ @@ -733,7 +695,7 @@ module Ast implements AstSig { UnaryExpr() { this.asExpr().(Py::UnaryExpr).getOp() instanceof Py::Not } /** Gets the operand of this unary expression. */ - Expr getOperand() { result = TExpr(this.asExpr().(Py::UnaryExpr).getOperand()) } + Expr getOperand() { result.asExpr() = this.asExpr().(Py::UnaryExpr).getOperand() } override AstNode getChild(int index) { index = 0 and result = this.getOperand() } } @@ -784,9 +746,9 @@ module Ast implements AstSig { ArithBinaryExpr() { binExpr = this.asExpr() } - Expr getLeft() { result = TExpr(binExpr.getLeft()) } + Expr getLeft() { result.asExpr() = binExpr.getLeft() } - Expr getRight() { result = TExpr(binExpr.getRight()) } + Expr getRight() { result.asExpr() = binExpr.getRight() } override AstNode getChild(int index) { index = 0 and result = this.getLeft() @@ -801,16 +763,16 @@ module Ast implements AstSig { CallExpr() { call = this.asExpr() } - Expr getFunc() { result = TExpr(call.getFunc()) } + Expr getFunc() { result.asExpr() = call.getFunc() } - Expr getPositionalArg(int n) { result = TExpr(call.getPositionalArg(n)) } + Expr getPositionalArg(int n) { result.asExpr() = call.getPositionalArg(n) } int getNumberOfPositionalArgs() { result = count(call.getAPositionalArg()) } Expr getKeywordValue(int n) { - result = TExpr(call.getNamedArg(n).(Py::Keyword).getValue()) + result.asExpr() = call.getNamedArg(n).(Py::Keyword).getValue() or - result = TExpr(call.getNamedArg(n).(Py::DictUnpacking).getValue()) + result.asExpr() = call.getNamedArg(n).(Py::DictUnpacking).getValue() } int getNumberOfNamedArgs() { result = count(call.getANamedArg()) } @@ -831,9 +793,9 @@ module Ast implements AstSig { SubscriptExpr() { sub = this.asExpr() } - Expr getObject() { result = TExpr(sub.getObject()) } + Expr getObject() { result.asExpr() = sub.getObject() } - Expr getIndex() { result = TExpr(sub.getIndex()) } + Expr getIndex() { result.asExpr() = sub.getIndex() } override AstNode getChild(int index) { index = 0 and result = this.getObject() @@ -848,7 +810,7 @@ module Ast implements AstSig { AttributeExpr() { attr = this.asExpr() } - Expr getObject() { result = TExpr(attr.getObject()) } + Expr getObject() { result.asExpr() = attr.getObject() } override AstNode getChild(int index) { index = 0 and result = this.getObject() } } @@ -859,7 +821,7 @@ module Ast implements AstSig { TupleExpr() { tuple = this.asExpr() } - Expr getElt(int n) { result = TExpr(tuple.getElt(n)) } + Expr getElt(int n) { result.asExpr() = tuple.getElt(n) } override AstNode getChild(int index) { result = this.getElt(index) } } @@ -870,7 +832,7 @@ module Ast implements AstSig { ListExpr() { list = this.asExpr() } - Expr getElt(int n) { result = TExpr(list.getElt(n)) } + Expr getElt(int n) { result.asExpr() = list.getElt(n) } override AstNode getChild(int index) { result = this.getElt(index) } } @@ -881,7 +843,7 @@ module Ast implements AstSig { SetExpr() { set = this.asExpr() } - Expr getElt(int n) { result = TExpr(set.getElt(n)) } + Expr getElt(int n) { result.asExpr() = set.getElt(n) } override AstNode getChild(int index) { result = this.getElt(index) } } @@ -896,9 +858,9 @@ module Ast implements AstSig { * Gets the key of the `n`th item (at child index `2*n`); the value is * at child index `2*n + 1`. */ - Expr getKey(int n) { result = TExpr(dict.getItem(n).(Py::KeyValuePair).getKey()) } + Expr getKey(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getKey() } - Expr getValue(int n) { result = TExpr(dict.getItem(n).(Py::KeyValuePair).getValue()) } + Expr getValue(int n) { result.asExpr() = dict.getItem(n).(Py::KeyValuePair).getValue() } int getNumberOfItems() { result = count(dict.getAnItem()) } @@ -917,7 +879,7 @@ module Ast implements AstSig { ArithUnaryExpr() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } - Expr getOperand() { result = TExpr(unaryExpr.getOperand()) } + Expr getOperand() { result.asExpr() = unaryExpr.getOperand() } override AstNode getChild(int index) { index = 0 and result = this.getOperand() } } @@ -940,7 +902,7 @@ module Ast implements AstSig { iterable = this.asExpr().(Py::GeneratorExp).getIterable() } - Expr getIterable() { result = TExpr(iterable) } + Expr getIterable() { result.asExpr() = iterable } override AstNode getChild(int index) { index = 0 and result = this.getIterable() } } @@ -951,9 +913,9 @@ module Ast implements AstSig { CompareExpr() { cmp = this.asExpr() } - Expr getLeft() { result = TExpr(cmp.getLeft()) } + Expr getLeft() { result.asExpr() = cmp.getLeft() } - Expr getComparator(int n) { result = TExpr(cmp.getComparator(n)) } + Expr getComparator(int n) { result.asExpr() = cmp.getComparator(n) } override AstNode getChild(int index) { index = 0 and result = this.getLeft() @@ -968,11 +930,11 @@ module Ast implements AstSig { SliceExpr() { slice = this.asExpr() } - Expr getStart() { result = TExpr(slice.getStart()) } + Expr getStart() { result.asExpr() = slice.getStart() } - Expr getStop() { result = TExpr(slice.getStop()) } + Expr getStop() { result.asExpr() = slice.getStop() } - Expr getStep() { result = TExpr(slice.getStep()) } + Expr getStep() { result.asExpr() = slice.getStep() } override AstNode getChild(int index) { index = 0 and result = this.getStart() @@ -989,7 +951,7 @@ module Ast implements AstSig { StarredExpr() { starred = this.asExpr() } - Expr getValue() { result = TExpr(starred.getValue()) } + Expr getValue() { result.asExpr() = starred.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getValue() } } @@ -1000,7 +962,7 @@ module Ast implements AstSig { FstringExpr() { fstring = this.asExpr() } - Expr getValue(int n) { result = TExpr(fstring.getValue(n)) } + Expr getValue(int n) { result.asExpr() = fstring.getValue(n) } override AstNode getChild(int index) { result = this.getValue(index) } } @@ -1011,9 +973,9 @@ module Ast implements AstSig { FormattedValueExpr() { fv = this.asExpr() } - Expr getValue() { result = TExpr(fv.getValue()) } + Expr getValue() { result.asExpr() = fv.getValue() } - Expr getFormatSpec() { result = TExpr(fv.getFormatSpec()) } + Expr getFormatSpec() { result.asExpr() = fv.getFormatSpec() } override AstNode getChild(int index) { index = 0 and result = this.getValue() @@ -1028,7 +990,7 @@ module Ast implements AstSig { YieldExpr() { yield = this.asExpr() } - Expr getValue() { result = TExpr(yield.getValue()) } + Expr getValue() { result.asExpr() = yield.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getValue() } } @@ -1039,7 +1001,7 @@ module Ast implements AstSig { YieldFromExpr() { yieldFrom = this.asExpr() } - Expr getValue() { result = TExpr(yieldFrom.getValue()) } + Expr getValue() { result.asExpr() = yieldFrom.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getValue() } } @@ -1050,7 +1012,7 @@ module Ast implements AstSig { AwaitExpr() { await = this.asExpr() } - Expr getValue() { result = TExpr(await.getValue()) } + Expr getValue() { result.asExpr() = await.getValue() } override AstNode getChild(int index) { index = 0 and result = this.getValue() } } @@ -1061,7 +1023,7 @@ module Ast implements AstSig { ClassDefExpr() { classExpr = this.asExpr() } - Expr getBase(int n) { result = TExpr(classExpr.getBase(n)) } + Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) } override AstNode getChild(int index) { result = this.getBase(index) } } @@ -1079,14 +1041,14 @@ module Ast implements AstSig { * renumber here to obtain contiguous indices. */ Expr getDefault(int n) { - result = - TExpr(rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getDefault(i) | d order by i)) + result.asExpr() = + rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getDefault(i) | d order by i) } /** Gets the `n`th default for a keyword-only argument, in evaluation order. */ Expr getKwDefault(int n) { - result = - TExpr(rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getKwDefault(i) | d order by i)) + result.asExpr() = + rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getKwDefault(i) | d order by i) } int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } @@ -1106,14 +1068,14 @@ module Ast implements AstSig { /** Gets the `n`th default for a positional argument, in evaluation order. */ Expr getDefault(int n) { - result = - TExpr(rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getDefault(i) | d order by i)) + result.asExpr() = + rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getDefault(i) | d order by i) } /** Gets the `n`th default for a keyword-only argument, in evaluation order. */ Expr getKwDefault(int n) { - result = - TExpr(rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getKwDefault(i) | d order by i)) + result.asExpr() = + rank[n + 1](Py::Expr d, int i | d = lambda.getArgs().getKwDefault(i) | d order by i) } int getNumberOfDefaults() { result = count(lambda.getArgs().getADefault()) } From 1e51c8250b1e227199e9e8a6bed7a02cdb3e3bfd Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 12:54:02 +0000 Subject: [PATCH 40/72] Python: index TBlockStmt by Py::StmtList instead of (parent, slot) Replace the two-key TBlockStmt(Py::AstNode parent, string slot) newtype branch with the simpler TBlockStmt(Py::StmtList sl). Each Py::StmtList that represents an imperative block (function/class/module body, if/ while/for branch, try/except/finally body, case body, except/except* body) becomes one BlockStmt directly. The slot string disappears; toString just defers to Py::StmtList.toString() ('StmtList'). The newtype branch keeps an explicit characteristic predicate listing the slots that count as block bodies. This excludes Try.getHandlers(), which is a Py::StmtList of ExceptStmt items already iterated by the shared library's Try logic via getCatch(int) - including it would produce parallel CFG edges (verified: a permissive TBlockStmt(Py::StmtList sl) version regressed CPython to 1720 multipleSuccessors and 584 deadEnds before this restriction). Drops the getBodyStmtList helper. Caller sites now use the StmtList accessor directly: TBlockStmt(ifStmt.getBody()), TBlockStmt(tryStmt.getFinalbody()), etc. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 124 ++++++++---------- 1 file changed, 58 insertions(+), 66 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index bcf8f4f64705..ccd363565721 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -17,42 +17,6 @@ private import codeql.util.Void /** Provides the Python implementation of the shared CFG `AstSig`. */ module Ast implements AstSig { - /** - * Maps a `(parent, slot)` pair to the `Py::StmtList` that holds the items - * of the `BlockStmt` for that slot. The slot string distinguishes between - * the multiple bodies that some parents have (e.g. `if` has `body` and - * `orelse`). - */ - private Py::StmtList getBodyStmtList(Py::AstNode parent, string slot) { - result = parent.(Py::Scope).getBody() and slot = "body" - or - result = parent.(Py::If).getBody() and slot = "body" - or - result = parent.(Py::If).getOrelse() and slot = "orelse" - or - result = parent.(Py::While).getBody() and slot = "body" - or - result = parent.(Py::While).getOrelse() and slot = "orelse" - or - result = parent.(Py::For).getBody() and slot = "body" - or - result = parent.(Py::For).getOrelse() and slot = "orelse" - or - result = parent.(Py::With).getBody() and slot = "body" - or - result = parent.(Py::Try).getBody() and slot = "body" - or - result = parent.(Py::Try).getOrelse() and slot = "orelse" - or - result = parent.(Py::Try).getFinalbody() and slot = "finally" - or - result = parent.(Py::Case).getBody() and slot = "body" - or - result = parent.(Py::ExceptStmt).getBody() and slot = "body" - or - result = parent.(Py::ExceptGroupStmt).getBody() and slot = "body" - } - private newtype TAstNode = TStmt(Py::Stmt s) or TExpr(Py::Expr e) { not e instanceof Py::BoolExpr } or @@ -68,11 +32,42 @@ module Ast implements AstSig { */ TBoolExprPair(Py::BoolExpr be, int index) { index = [0 .. count(be.getAValue()) - 2] } or /** - * A synthetic block statement, identifying one body slot of the - * `parent` AST node. The `slot` string disambiguates among multiple - * bodies of the same parent (`"body"`, `"orelse"`, `"finally"`). + * A synthetic block statement, wrapping a `Py::StmtList`. Each list of + * statements that represents an imperative block (a function/class/module + * body, an `if`/`while`/`for` branch, a `try`/`except`/`finally` body, + * etc.) becomes one `BlockStmt` node in the CFG. Lists used in other + * roles (e.g. `Try.getHandlers()`, which is iterated as catch clauses) + * are excluded. */ - TBlockStmt(Py::AstNode parent, string slot) { exists(getBodyStmtList(parent, slot)) } + TBlockStmt(Py::StmtList sl) { + sl = any(Py::Scope p).getBody() + or + sl = any(Py::If p).getBody() + or + sl = any(Py::If p).getOrelse() + or + sl = any(Py::While p).getBody() + or + sl = any(Py::While p).getOrelse() + or + sl = any(Py::For p).getBody() + or + sl = any(Py::For p).getOrelse() + or + sl = any(Py::With p).getBody() + or + sl = any(Py::Try p).getBody() + or + sl = any(Py::Try p).getOrelse() + or + sl = any(Py::Try p).getFinalbody() + or + sl = any(Py::Case p).getBody() + or + sl = any(Py::ExceptStmt p).getBody() + or + sl = any(Py::ExceptGroupStmt p).getBody() + } /** An AST node visible to the shared CFG. */ class AstNode extends TAstNode { @@ -135,7 +130,7 @@ module Ast implements AstSig { } /** Gets the body of callable `c`. */ - AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope(), "body") } + AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope().getBody()) } /** * A parameter of a callable. @@ -195,29 +190,26 @@ module Ast implements AstSig { * sequence of statements. */ class BlockStmt extends Stmt, TBlockStmt { - private Py::AstNode parent; - private string slot; + private Py::StmtList sl; - BlockStmt() { this = TBlockStmt(parent, slot) } + BlockStmt() { this = TBlockStmt(sl) } /** Gets the `n`th (zero-based) statement in this block. */ - Stmt getStmt(int n) { result = TStmt(getBodyStmtList(parent, slot).getItem(n)) } + Stmt getStmt(int n) { result.asStmt() = sl.getItem(n) } /** Gets the last statement in this block. */ - Stmt getLastStmt() { result = TStmt(getBodyStmtList(parent, slot).getLastItem()) } + Stmt getLastStmt() { result.asStmt() = sl.getLastItem() } - override string toString() { result = "block:" + slot } + override string toString() { result = sl.toString() } - // BlockStmt has no native location; approximate with the first + // `Py::StmtList` has no native location; approximate with the first // item's location. - override Py::Location getLocation() { - result = getBodyStmtList(parent, slot).getItem(0).getLocation() - } + override Py::Location getLocation() { result = sl.getItem(0).getLocation() } override Callable getEnclosingCallable() { - result.asScope() = parent.(Py::Scope) + result.asScope() = sl.getParent().(Py::Scope) or - result.asScope() = parent.(Py::Stmt).getScope() + result.asScope() = sl.getParent().(Py::Stmt).getScope() } override AstNode getChild(int index) { result = this.getStmt(index) } @@ -303,10 +295,10 @@ module Ast implements AstSig { Expr getCondition() { result.asExpr() = ifStmt.getTest() } /** Gets the `then` (true) branch of this `if` statement. */ - Stmt getThen() { result = TBlockStmt(ifStmt, "body") } + Stmt getThen() { result = TBlockStmt(ifStmt.getBody()) } /** Gets the `else` (false) branch, if any. */ - Stmt getElse() { result = TBlockStmt(ifStmt, "orelse") } + Stmt getElse() { result = TBlockStmt(ifStmt.getOrelse()) } override AstNode getChild(int index) { index = 0 and result = this.getCondition() @@ -334,10 +326,10 @@ module Ast implements AstSig { /** Gets the boolean condition of this `while` loop. */ Expr getCondition() { result.asExpr() = whileStmt.getTest() } - override Stmt getBody() { result = TBlockStmt(whileStmt, "body") } + override Stmt getBody() { result = TBlockStmt(whileStmt.getBody()) } /** Gets the `else` branch of this `while` loop, if any. */ - Stmt getElse() { result = TBlockStmt(whileStmt, "orelse") } + Stmt getElse() { result = TBlockStmt(whileStmt.getOrelse()) } override AstNode getChild(int index) { index = 0 and result = this.getCondition() @@ -380,10 +372,10 @@ module Ast implements AstSig { /** Gets the collection being iterated. */ Expr getCollection() { result.asExpr() = forStmt.getIter() } - override Stmt getBody() { result = TBlockStmt(forStmt, "body") } + override Stmt getBody() { result = TBlockStmt(forStmt.getBody()) } /** Gets the `else` branch of this `for` loop, if any. */ - Stmt getElse() { result = TBlockStmt(forStmt, "orelse") } + Stmt getElse() { result = TBlockStmt(forStmt.getOrelse()) } override AstNode getChild(int index) { index = 0 and result = this.getCollection() @@ -452,7 +444,7 @@ module Ast implements AstSig { Expr getOptionalVars() { result.asExpr() = withStmt.getOptionalVars() } - Stmt getBody() { result = TBlockStmt(withStmt, "body") } + Stmt getBody() { result = TBlockStmt(withStmt.getBody()) } override AstNode getChild(int index) { index = 0 and result = this.getContextExpr() @@ -497,12 +489,12 @@ module Ast implements AstSig { TryStmt() { tryStmt = this.asStmt() } - Stmt getBody() { result = TBlockStmt(tryStmt, "body") } + Stmt getBody() { result = TBlockStmt(tryStmt.getBody()) } /** Gets the `else` branch of this `try` statement, if any. */ - Stmt getElse() { result = TBlockStmt(tryStmt, "orelse") } + Stmt getElse() { result = TBlockStmt(tryStmt.getOrelse()) } - Stmt getFinally() { result = TBlockStmt(tryStmt, "finally") } + Stmt getFinally() { result = TBlockStmt(tryStmt.getFinalbody()) } CatchClause getCatch(int index) { result = TStmt(tryStmt.getHandler(index)) } @@ -549,9 +541,9 @@ module Ast implements AstSig { /** Gets the body of this exception handler. */ Stmt getBody() { - result = TBlockStmt(handler.(Py::ExceptStmt), "body") + result = TBlockStmt(handler.(Py::ExceptStmt).getBody()) or - result = TBlockStmt(handler.(Py::ExceptGroupStmt), "body") + result = TBlockStmt(handler.(Py::ExceptGroupStmt).getBody()) } override AstNode getChild(int index) { @@ -592,7 +584,7 @@ module Ast implements AstSig { Expr getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } - AstNode getBody() { result = TBlockStmt(caseStmt, "body") } + AstNode getBody() { result = TBlockStmt(caseStmt.getBody()) } /** Holds if this case is a wildcard pattern (`case _:`). */ predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } From 72d74ae9dcc7dc0e0953dbdf067a3f13b044bd4e Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 14:14:16 +0000 Subject: [PATCH 41/72] Python: document why Assignment subclasses are empty Explain that the shared library's Assignment / CompoundAssignment hierarchy extends BinaryExpr, so it cannot host Python's statement- level assignment forms (Assign, AugAssign), and that Python has no short-circuiting compound operators (&&=, ||=, ??=) so all subclasses remain empty. No behaviour change; doc comments only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index ccd363565721..3fa8adbe6b2b 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -695,19 +695,41 @@ module Ast implements AstSig { /** A logical `not` expression. */ class LogicalNotExpr extends UnaryExpr { } - /** An assignment expression. Python's walrus is modelled separately. */ + /** + * An assignment expression. + * + * Empty in Python: `x = y` and `x += y` are statements (`AssignStmt` and + * `AugAssignStmt`), not expressions, and the walrus `x := y` is modeled + * separately as `NamedExpr`. The shared library's `Assignment` extends + * `BinaryExpr`, so it cannot share instances with our `Stmt`-based + * assignment forms. + */ class Assignment extends BinaryExpr { Assignment() { none() } } + /** A simple assignment expression. Empty in Python (see `Assignment`). */ class AssignExpr extends Assignment { } + /** A compound assignment expression. Empty in Python (see `Assignment`). */ class CompoundAssignment extends Assignment { } + /** + * A short-circuiting logical AND compound assignment expression (`&&=`). + * Python has no such operator. + */ class AssignLogicalAndExpr extends CompoundAssignment { } + /** + * A short-circuiting logical OR compound assignment expression (`||=`). + * Python has no such operator. + */ class AssignLogicalOrExpr extends CompoundAssignment { } + /** + * A short-circuiting null-coalescing compound assignment expression + * (`??=`). Python has no such operator. + */ class AssignNullCoalescingExpr extends CompoundAssignment { } /** A boolean literal expression (`True` or `False`). */ From 761b3e38a2940d700c22066fa14e947c1303e93e Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 16:58:43 +0000 Subject: [PATCH 42/72] Python: use private-abstract + final-alias pattern for AstNode Convert AstNode from a concrete class with empty default predicates into a private abstract class plus a final alias, matching the pattern used in cpp/.../EdgeKind.qll and cpp/.../IRVariable.qll: abstract private class AstNodeImpl extends TAstNode { abstract string toString(); abstract Py::Location getLocation(); abstract Callable getEnclosingCallable(); ... } final class AstNode = AstNodeImpl; This makes the compiler enforce that every concrete subclass implements toString/getLocation/getEnclosingCallable, replacing the brittle 'empty default + per-branch override' arrangement. Sister classes inside the module now extend AstNodeImpl instead of AstNode (which is final and cannot be extended). The empty Parameter stub gains explicit none() overrides for the three abstract members, since QL requires them statically even when the class has no instances. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 3fa8adbe6b2b..a0fb1c7bf727 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -69,16 +69,24 @@ module Ast implements AstSig { sl = any(Py::ExceptGroupStmt p).getBody() } - /** An AST node visible to the shared CFG. */ - class AstNode extends TAstNode { + /** + * An AST node visible to the shared CFG. + * + * This is the abstract implementation class. It enforces that each + * concrete subclass provides `toString`, `getLocation`, and + * `getEnclosingCallable` (one subclass per `TAstNode` newtype branch). + * The public alias `AstNode` is what users (and the `AstSig` signature) + * see; subclasses inside this module extend `AstNodeImpl` directly. + */ + abstract private class AstNodeImpl extends TAstNode { /** Gets a textual representation of this AST node. */ - string toString() { none() } + abstract string toString(); /** Gets the location of this AST node. */ - Py::Location getLocation() { none() } + abstract Py::Location getLocation(); /** Gets the enclosing callable that contains this node, if any. */ - Callable getEnclosingCallable() { none() } + abstract Callable getEnclosingCallable(); /** Gets the underlying Python `Stmt`, if this node wraps one. */ Py::Stmt asStmt() { this = TStmt(result) } @@ -109,6 +117,9 @@ module Ast implements AstSig { AstNode getChild(int index) { none() } } + /** An AST node visible to the shared CFG. */ + final class AstNode = AstNodeImpl; + /** Gets the immediately enclosing callable that contains `node`. */ Callable getEnclosingCallable(AstNode node) { result = node.getEnclosingCallable() } @@ -117,7 +128,7 @@ module Ast implements AstSig { * * In Python, all three are executable scopes with statement bodies. */ - class Callable extends AstNode, TScope { + class Callable extends AstNodeImpl, TScope { private Py::Scope sc; Callable() { this = TScope(sc) } @@ -137,9 +148,15 @@ module Ast implements AstSig { * * TODO: Implement in order to include parameters in the CFG. */ - class Parameter extends AstNode { + class Parameter extends AstNodeImpl { Parameter() { none() } + override string toString() { none() } + + override Py::Location getLocation() { none() } + + override Callable getEnclosingCallable() { none() } + Expr getDefaultValue() { none() } } @@ -147,7 +164,7 @@ module Ast implements AstSig { Parameter callableGetParameter(Callable c, int index) { none() } /** A statement. */ - class Stmt extends AstNode { + class Stmt extends AstNodeImpl { Stmt() { this instanceof TStmt or this instanceof TBlockStmt } // For `TStmt` instances, delegate to the wrapped Python statement. @@ -160,7 +177,7 @@ module Ast implements AstSig { } /** An expression. */ - class Expr extends AstNode { + class Expr extends AstNodeImpl { Expr() { this instanceof TExpr or this instanceof TBoolExprPair } // For `TExpr` instances, delegate to the wrapped Python expression. @@ -173,7 +190,7 @@ module Ast implements AstSig { } /** A pattern in a `match` statement. */ - additional class Pattern extends AstNode, TPattern { + additional class Pattern extends AstNodeImpl, TPattern { private Py::Pattern p; Pattern() { this = TPattern(p) } From fe394788d31da09c445701cbe7c869b6543a2c47 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 17:08:10 +0000 Subject: [PATCH 43/72] Python: introduce TStmt union via newtype-branch alias Rename the TStmt newtype branch to TPyStmt, and add a private union type alias private class TStmt = TPyStmt or TBlockStmt; This lets the public Stmt class use TStmt directly in its extends clause: class Stmt extends AstNodeImpl, TStmt { ... } instead of the previous class Stmt extends AstNodeImpl { Stmt() { this instanceof TStmt or this instanceof TBlockStmt } ... } The same pattern is used in cpp/.../TInstruction.qll and rust/.../Synth.qll. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index a0fb1c7bf727..a4ea95755659 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -18,7 +18,7 @@ private import codeql.util.Void /** Provides the Python implementation of the shared CFG `AstSig`. */ module Ast implements AstSig { private newtype TAstNode = - TStmt(Py::Stmt s) or + TPyStmt(Py::Stmt s) or TExpr(Py::Expr e) { not e instanceof Py::BoolExpr } or TScope(Py::Scope sc) or TPattern(Py::Pattern p) or @@ -69,6 +69,13 @@ module Ast implements AstSig { sl = any(Py::ExceptGroupStmt p).getBody() } + /** + * The union of `TPyStmt` (wrapping `Py::Stmt`) and `TBlockStmt` (wrapping + * `Py::StmtList`). Both represent the kinds of node that can appear in + * a `Stmt` position in the CFG. + */ + private class TStmt = TPyStmt or TBlockStmt; + /** * An AST node visible to the shared CFG. * @@ -89,7 +96,7 @@ module Ast implements AstSig { abstract Callable getEnclosingCallable(); /** Gets the underlying Python `Stmt`, if this node wraps one. */ - Py::Stmt asStmt() { this = TStmt(result) } + Py::Stmt asStmt() { this = TPyStmt(result) } /** * Gets the underlying Python `Expr`, if this node wraps one. Boolean @@ -164,10 +171,8 @@ module Ast implements AstSig { Parameter callableGetParameter(Callable c, int index) { none() } /** A statement. */ - class Stmt extends AstNodeImpl { - Stmt() { this instanceof TStmt or this instanceof TBlockStmt } - - // For `TStmt` instances, delegate to the wrapped Python statement. + class Stmt extends AstNodeImpl, TStmt { + // For `TPyStmt` instances, delegate to the wrapped Python statement. // `BlockStmt` (the only `TBlockStmt` subclass) provides its own overrides. override string toString() { result = this.asStmt().toString() } @@ -513,7 +518,7 @@ module Ast implements AstSig { Stmt getFinally() { result = TBlockStmt(tryStmt.getFinalbody()) } - CatchClause getCatch(int index) { result = TStmt(tryStmt.getHandler(index)) } + CatchClause getCatch(int index) { result = TPyStmt(tryStmt.getHandler(index)) } override AstNode getChild(int index) { index = 0 and result = this.getBody() @@ -580,7 +585,7 @@ module Ast implements AstSig { Expr getExpr() { result.asExpr() = matchStmt.getSubject() } - Case getCase(int index) { result = TStmt(matchStmt.getCase(index)) } + Case getCase(int index) { result = TPyStmt(matchStmt.getCase(index)) } Stmt getStmt(int index) { none() } From 8ca2a30deaead5c89a4e9c155253f8a104af4ddf Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 17:13:50 +0000 Subject: [PATCH 44/72] Python: simplify TBlockStmt char pred via exclusion list Replace the 14-disjunct allow-list with a 2-conjunct exclusion list. Of the 17 Py::StmtList getters in AstGenerated.qll, only Try.getHandlers() and MatchStmt.getCases() should not be wrapped as BlockStmts (they are iterated individually by the shared library's Try/Switch logic via getCatch(int) and getCase(int)). All other StmtLists are imperative block bodies. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index a4ea95755659..2c43802948dd 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -35,38 +35,15 @@ module Ast implements AstSig { * A synthetic block statement, wrapping a `Py::StmtList`. Each list of * statements that represents an imperative block (a function/class/module * body, an `if`/`while`/`for` branch, a `try`/`except`/`finally` body, - * etc.) becomes one `BlockStmt` node in the CFG. Lists used in other - * roles (e.g. `Try.getHandlers()`, which is iterated as catch clauses) - * are excluded. + * etc.) becomes one `BlockStmt` node in the CFG. `Py::StmtList`s used + * in other roles - `Try.getHandlers()` (iterated via `getCatch`) and + * `MatchStmt.getCases()` (iterated via `getCase`) - are excluded, as + * the shared library's `Try`/`Switch` logic walks their items + * individually. */ TBlockStmt(Py::StmtList sl) { - sl = any(Py::Scope p).getBody() - or - sl = any(Py::If p).getBody() - or - sl = any(Py::If p).getOrelse() - or - sl = any(Py::While p).getBody() - or - sl = any(Py::While p).getOrelse() - or - sl = any(Py::For p).getBody() - or - sl = any(Py::For p).getOrelse() - or - sl = any(Py::With p).getBody() - or - sl = any(Py::Try p).getBody() - or - sl = any(Py::Try p).getOrelse() - or - sl = any(Py::Try p).getFinalbody() - or - sl = any(Py::Case p).getBody() - or - sl = any(Py::ExceptStmt p).getBody() - or - sl = any(Py::ExceptGroupStmt p).getBody() + not sl = any(Py::Try t).getHandlers() and + not sl = any(Py::MatchStmt m).getCases() } /** From 8f419d1050858868126601e8c360b122a90d28ee Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 19:33:33 +0000 Subject: [PATCH 45/72] Python: introduce TExpr union via newtype-branch alias Mirror the TStmt refactor for the Expr hierarchy: rename the TExpr newtype branch to TPyExpr and add private class TExpr = TPyExpr or TBoolExprPair; This lets the public Expr class use TExpr directly: class Expr extends AstNodeImpl, TExpr { ... } instead of class Expr extends AstNodeImpl { Expr() { this instanceof TExpr or this instanceof TBoolExprPair } ... } No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 2c43802948dd..f3ed495bcef0 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -19,7 +19,7 @@ private import codeql.util.Void module Ast implements AstSig { private newtype TAstNode = TPyStmt(Py::Stmt s) or - TExpr(Py::Expr e) { not e instanceof Py::BoolExpr } or + TPyExpr(Py::Expr e) { not e instanceof Py::BoolExpr } or TScope(Py::Scope sc) or TPattern(Py::Pattern p) or /** @@ -53,6 +53,14 @@ module Ast implements AstSig { */ private class TStmt = TPyStmt or TBlockStmt; + /** + * The union of `TPyExpr` (wrapping non-boolean `Py::Expr`) and + * `TBoolExprPair` (synthetic operand pairs of `and`/`or` expressions). + * Both represent the kinds of node that can appear in an `Expr` + * position in the CFG. + */ + private class TExpr = TPyExpr or TBoolExprPair; + /** * An AST node visible to the shared CFG. * @@ -82,7 +90,7 @@ module Ast implements AstSig { * representation. */ Py::Expr asExpr() { - this = TExpr(result) + this = TPyExpr(result) or this = TBoolExprPair(result, 0) } @@ -159,10 +167,8 @@ module Ast implements AstSig { } /** An expression. */ - class Expr extends AstNodeImpl { - Expr() { this instanceof TExpr or this instanceof TBoolExprPair } - - // For `TExpr` instances, delegate to the wrapped Python expression. + class Expr extends AstNodeImpl, TExpr { + // For `TPyExpr` instances, delegate to the wrapped Python expression. // `BinaryExpr` (the only `TBoolExprPair` subclass) provides its own overrides. override string toString() { result = this.asExpr().toString() } From 115a762f4c66b9bf6b9fd8ea41d1dc98596e1209 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 19:51:38 +0000 Subject: [PATCH 46/72] Python: use newtype-branch constructors in characteristic predicates Style cleanup: when a class's characteristic predicate binds via a 'cast' helper like IfStmt() { ifStmt = this.asStmt() } prefer naming the newtype branch directly: IfStmt() { this = TPyStmt(ifStmt) } This makes the wrapped representation explicit. Apply throughout: ~30 charpreds (every Stmt/Expr leaf wrapper, plus LoopStmt, BreakStmt, ContinueStmt, BooleanLiteral, UnaryExpr, ArithUnaryExpr, Comprehension). Method bodies that use asStmt/asExpr to project an underlying Python AST node (Stmt.toString, BlockStmt.getEnclosingCallable, UnaryExpr.getOperand, etc.) keep that form - they're projections, not classifications. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 104 +++++++++--------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index f3ed495bcef0..8b4cf1189f44 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -224,7 +224,7 @@ module Ast implements AstSig { class ExprStmt extends Stmt { private Py::ExprStmt exprStmt; - ExprStmt() { exprStmt = this.asStmt() } + ExprStmt() { this = TPyStmt(exprStmt) } /** Gets the expression in this expression statement. */ Expr getExpr() { result.asExpr() = exprStmt.getValue() } @@ -236,7 +236,7 @@ module Ast implements AstSig { additional class AssignStmt extends Stmt { private Py::Assign assign; - AssignStmt() { assign = this.asStmt() } + AssignStmt() { this = TPyStmt(assign) } Expr getValue() { result.asExpr() = assign.getValue() } @@ -255,7 +255,7 @@ module Ast implements AstSig { additional class AugAssignStmt extends Stmt { private Py::AugAssign augAssign; - AugAssignStmt() { augAssign = this.asStmt() } + AugAssignStmt() { this = TPyStmt(augAssign) } Expr getOperation() { result.asExpr() = augAssign.getOperation() } @@ -266,7 +266,7 @@ module Ast implements AstSig { additional class NamedExpr extends Expr { private Py::AssignExpr assignExpr; - NamedExpr() { assignExpr = this.asExpr() } + NamedExpr() { this = TPyExpr(assignExpr) } Expr getValue() { result.asExpr() = assignExpr.getValue() } @@ -291,7 +291,7 @@ module Ast implements AstSig { class IfStmt extends Stmt { private Py::If ifStmt; - IfStmt() { ifStmt = this.asStmt() } + IfStmt() { this = TPyStmt(ifStmt) } /** Gets the underlying Python `If` statement. */ Py::If asIf() { result = ifStmt } @@ -316,7 +316,11 @@ module Ast implements AstSig { /** A loop statement. */ class LoopStmt extends Stmt { - LoopStmt() { this.asStmt() instanceof Py::While or this.asStmt() instanceof Py::For } + LoopStmt() { + this = TPyStmt(any(Py::While w)) + or + this = TPyStmt(any(Py::For f)) + } /** Gets the body of this loop statement. */ Stmt getBody() { none() } @@ -326,7 +330,7 @@ module Ast implements AstSig { class WhileStmt extends LoopStmt { private Py::While whileStmt; - WhileStmt() { whileStmt = this.asStmt() } + WhileStmt() { this = TPyStmt(whileStmt) } /** Gets the boolean condition of this `while` loop. */ Expr getCondition() { result.asExpr() = whileStmt.getTest() } @@ -369,7 +373,7 @@ module Ast implements AstSig { class ForeachStmt extends LoopStmt { private Py::For forStmt; - ForeachStmt() { forStmt = this.asStmt() } + ForeachStmt() { this = TPyStmt(forStmt) } /** Gets the loop variable. */ Expr getVariable() { result.asExpr() = forStmt.getTarget() } @@ -395,12 +399,12 @@ module Ast implements AstSig { /** A `break` statement. */ class BreakStmt extends Stmt { - BreakStmt() { this.asStmt() instanceof Py::Break } + BreakStmt() { this = TPyStmt(any(Py::Break b)) } } /** A `continue` statement. */ class ContinueStmt extends Stmt { - ContinueStmt() { this.asStmt() instanceof Py::Continue } + ContinueStmt() { this = TPyStmt(any(Py::Continue c)) } } /** A `goto` statement. Python has no goto. */ @@ -412,7 +416,7 @@ module Ast implements AstSig { class ReturnStmt extends Stmt { private Py::Return ret; - ReturnStmt() { ret = this.asStmt() } + ReturnStmt() { this = TPyStmt(ret) } /** Gets the expression being returned, if any. */ Expr getExpr() { result.asExpr() = ret.getValue() } @@ -424,7 +428,7 @@ module Ast implements AstSig { class Throw extends Stmt { private Py::Raise raise; - Throw() { raise = this.asStmt() } + Throw() { this = TPyStmt(raise) } /** Gets the expression being raised. */ Expr getExpr() { result.asExpr() = raise.getException() } @@ -443,7 +447,7 @@ module Ast implements AstSig { additional class WithStmt extends Stmt { private Py::With withStmt; - WithStmt() { withStmt = this.asStmt() } + WithStmt() { this = TPyStmt(withStmt) } Expr getContextExpr() { result.asExpr() = withStmt.getContextExpr() } @@ -464,7 +468,7 @@ module Ast implements AstSig { additional class AssertStmt extends Stmt { private Py::Assert assertStmt; - AssertStmt() { assertStmt = this.asStmt() } + AssertStmt() { this = TPyStmt(assertStmt) } Expr getTest() { result.asExpr() = assertStmt.getTest() } @@ -481,7 +485,7 @@ module Ast implements AstSig { additional class DeleteStmt extends Stmt { private Py::Delete del; - DeleteStmt() { del = this.asStmt() } + DeleteStmt() { this = TPyStmt(del) } Expr getTarget(int n) { result.asExpr() = del.getTarget(n) } @@ -492,7 +496,7 @@ module Ast implements AstSig { class TryStmt extends Stmt { private Py::Try tryStmt; - TryStmt() { tryStmt = this.asStmt() } + TryStmt() { this = TPyStmt(tryStmt) } Stmt getBody() { result = TBlockStmt(tryStmt.getBody()) } @@ -533,7 +537,7 @@ module Ast implements AstSig { class CatchClause extends Stmt { private Py::ExceptionHandler handler; - CatchClause() { handler = this.asStmt() } + CatchClause() { this = TPyStmt(handler) } /** Gets the type expression of this exception handler. */ Expr getType() { result.asExpr() = handler.getType() } @@ -564,7 +568,7 @@ module Ast implements AstSig { class Switch extends Stmt { private Py::MatchStmt matchStmt; - Switch() { matchStmt = this.asStmt() } + Switch() { this = TPyStmt(matchStmt) } Expr getExpr() { result.asExpr() = matchStmt.getSubject() } @@ -583,7 +587,7 @@ module Ast implements AstSig { class Case extends Stmt { private Py::Case caseStmt; - Case() { caseStmt = this.asStmt() } + Case() { this = TPyStmt(caseStmt) } AstNode getAPattern() { result = TPattern(caseStmt.getPattern()) } @@ -612,7 +616,7 @@ module Ast implements AstSig { class ConditionalExpr extends Expr { private Py::IfExp ifExp; - ConditionalExpr() { ifExp = this.asExpr() } + ConditionalExpr() { this = TPyExpr(ifExp) } /** Gets the condition of this expression. */ Expr getCondition() { result.asExpr() = ifExp.getTest() } @@ -689,7 +693,7 @@ module Ast implements AstSig { * A unary expression. Currently only used for the `not` subclass. */ class UnaryExpr extends Expr { - UnaryExpr() { this.asExpr().(Py::UnaryExpr).getOp() instanceof Py::Not } + UnaryExpr() { exists(Py::UnaryExpr u | this = TPyExpr(u) and u.getOp() instanceof Py::Not) } /** Gets the operand of this unary expression. */ Expr getOperand() { result.asExpr() = this.asExpr().(Py::UnaryExpr).getOperand() } @@ -739,7 +743,7 @@ module Ast implements AstSig { /** A boolean literal expression (`True` or `False`). */ class BooleanLiteral extends Expr { - BooleanLiteral() { this.asExpr() instanceof Py::True or this.asExpr() instanceof Py::False } + BooleanLiteral() { this = TPyExpr(any(Py::True t)) or this = TPyExpr(any(Py::False f)) } /** Gets the boolean value of this literal. */ boolean getValue() { @@ -763,7 +767,7 @@ module Ast implements AstSig { additional class ArithBinaryExpr extends Expr { private Py::BinaryExpr binExpr; - ArithBinaryExpr() { binExpr = this.asExpr() } + ArithBinaryExpr() { this = TPyExpr(binExpr) } Expr getLeft() { result.asExpr() = binExpr.getLeft() } @@ -780,7 +784,7 @@ module Ast implements AstSig { additional class CallExpr extends Expr { private Py::Call call; - CallExpr() { call = this.asExpr() } + CallExpr() { this = TPyExpr(call) } Expr getFunc() { result.asExpr() = call.getFunc() } @@ -810,7 +814,7 @@ module Ast implements AstSig { additional class SubscriptExpr extends Expr { private Py::Subscript sub; - SubscriptExpr() { sub = this.asExpr() } + SubscriptExpr() { this = TPyExpr(sub) } Expr getObject() { result.asExpr() = sub.getObject() } @@ -827,7 +831,7 @@ module Ast implements AstSig { additional class AttributeExpr extends Expr { private Py::Attribute attr; - AttributeExpr() { attr = this.asExpr() } + AttributeExpr() { this = TPyExpr(attr) } Expr getObject() { result.asExpr() = attr.getObject() } @@ -838,7 +842,7 @@ module Ast implements AstSig { additional class TupleExpr extends Expr { private Py::Tuple tuple; - TupleExpr() { tuple = this.asExpr() } + TupleExpr() { this = TPyExpr(tuple) } Expr getElt(int n) { result.asExpr() = tuple.getElt(n) } @@ -849,7 +853,7 @@ module Ast implements AstSig { additional class ListExpr extends Expr { private Py::List list; - ListExpr() { list = this.asExpr() } + ListExpr() { this = TPyExpr(list) } Expr getElt(int n) { result.asExpr() = list.getElt(n) } @@ -860,7 +864,7 @@ module Ast implements AstSig { additional class SetExpr extends Expr { private Py::Set set; - SetExpr() { set = this.asExpr() } + SetExpr() { this = TPyExpr(set) } Expr getElt(int n) { result.asExpr() = set.getElt(n) } @@ -871,7 +875,7 @@ module Ast implements AstSig { additional class DictExpr extends Expr { private Py::Dict dict; - DictExpr() { dict = this.asExpr() } + DictExpr() { this = TPyExpr(dict) } /** * Gets the key of the `n`th item (at child index `2*n`); the value is @@ -896,7 +900,7 @@ module Ast implements AstSig { additional class ArithUnaryExpr extends Expr { private Py::UnaryExpr unaryExpr; - ArithUnaryExpr() { unaryExpr = this.asExpr() and not unaryExpr.getOp() instanceof Py::Not } + ArithUnaryExpr() { this = TPyExpr(unaryExpr) and not unaryExpr.getOp() instanceof Py::Not } Expr getOperand() { result.asExpr() = unaryExpr.getOperand() } @@ -912,13 +916,15 @@ module Ast implements AstSig { private Py::Expr iterable; Comprehension() { - iterable = this.asExpr().(Py::ListComp).getIterable() - or - iterable = this.asExpr().(Py::SetComp).getIterable() - or - iterable = this.asExpr().(Py::DictComp).getIterable() - or - iterable = this.asExpr().(Py::GeneratorExp).getIterable() + exists(Py::Expr c | this = TPyExpr(c) | + iterable = c.(Py::ListComp).getIterable() + or + iterable = c.(Py::SetComp).getIterable() + or + iterable = c.(Py::DictComp).getIterable() + or + iterable = c.(Py::GeneratorExp).getIterable() + ) } Expr getIterable() { result.asExpr() = iterable } @@ -930,7 +936,7 @@ module Ast implements AstSig { additional class CompareExpr extends Expr { private Py::Compare cmp; - CompareExpr() { cmp = this.asExpr() } + CompareExpr() { this = TPyExpr(cmp) } Expr getLeft() { result.asExpr() = cmp.getLeft() } @@ -947,7 +953,7 @@ module Ast implements AstSig { additional class SliceExpr extends Expr { private Py::Slice slice; - SliceExpr() { slice = this.asExpr() } + SliceExpr() { this = TPyExpr(slice) } Expr getStart() { result.asExpr() = slice.getStart() } @@ -968,7 +974,7 @@ module Ast implements AstSig { additional class StarredExpr extends Expr { private Py::Starred starred; - StarredExpr() { starred = this.asExpr() } + StarredExpr() { this = TPyExpr(starred) } Expr getValue() { result.asExpr() = starred.getValue() } @@ -979,7 +985,7 @@ module Ast implements AstSig { additional class FstringExpr extends Expr { private Py::Fstring fstring; - FstringExpr() { fstring = this.asExpr() } + FstringExpr() { this = TPyExpr(fstring) } Expr getValue(int n) { result.asExpr() = fstring.getValue(n) } @@ -990,7 +996,7 @@ module Ast implements AstSig { additional class FormattedValueExpr extends Expr { private Py::FormattedValue fv; - FormattedValueExpr() { fv = this.asExpr() } + FormattedValueExpr() { this = TPyExpr(fv) } Expr getValue() { result.asExpr() = fv.getValue() } @@ -1007,7 +1013,7 @@ module Ast implements AstSig { additional class YieldExpr extends Expr { private Py::Yield yield; - YieldExpr() { yield = this.asExpr() } + YieldExpr() { this = TPyExpr(yield) } Expr getValue() { result.asExpr() = yield.getValue() } @@ -1018,7 +1024,7 @@ module Ast implements AstSig { additional class YieldFromExpr extends Expr { private Py::YieldFrom yieldFrom; - YieldFromExpr() { yieldFrom = this.asExpr() } + YieldFromExpr() { this = TPyExpr(yieldFrom) } Expr getValue() { result.asExpr() = yieldFrom.getValue() } @@ -1029,7 +1035,7 @@ module Ast implements AstSig { additional class AwaitExpr extends Expr { private Py::Await await; - AwaitExpr() { await = this.asExpr() } + AwaitExpr() { this = TPyExpr(await) } Expr getValue() { result.asExpr() = await.getValue() } @@ -1040,7 +1046,7 @@ module Ast implements AstSig { additional class ClassDefExpr extends Expr { private Py::ClassExpr classExpr; - ClassDefExpr() { classExpr = this.asExpr() } + ClassDefExpr() { this = TPyExpr(classExpr) } Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) } @@ -1051,7 +1057,7 @@ module Ast implements AstSig { additional class FunctionDefExpr extends Expr { private Py::FunctionExpr funcExpr; - FunctionDefExpr() { funcExpr = this.asExpr() } + FunctionDefExpr() { this = TPyExpr(funcExpr) } /** * Gets the `n`th default for a positional argument, in evaluation @@ -1083,7 +1089,7 @@ module Ast implements AstSig { additional class LambdaExpr extends Expr { private Py::Lambda lambda; - LambdaExpr() { lambda = this.asExpr() } + LambdaExpr() { this = TPyExpr(lambda) } /** Gets the `n`th default for a positional argument, in evaluation order. */ Expr getDefault(int n) { From a70abdd007535d716036e73064608c529a03cddc Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 20:00:55 +0000 Subject: [PATCH 47/72] Python: project via as* helpers outside characteristic predicates Style cleanup: avoid naming newtype branch constructors (TPyStmt, TPyExpr, TBlockStmt, TPattern, TBoolExprPair, TScope) outside the char-preds that classify their wrappers. Method bodies and helper predicates now use the as* projections instead: // Before: result = TBlockStmt(ifStmt.getBody()) // After: result.asStmtList() = ifStmt.getBody() // Before: result = TPyStmt(matchStmt.getCase(index)) // After: result.asStmt() = matchStmt.getCase(index) Adds: - AstNode.asStmtList() - the inverse of TBlockStmt(_). - BinaryExpr.getIndex() - exposes the synthetic-pair index, used internally by getRightOperand to find the next pair without naming TBoolExprPair. No behaviour change: all 24 NewCfg evaluation-order tests pass; all 11 shared-CFG consistency queries report 0 violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 8b4cf1189f44..681b05cdf05a 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -101,6 +101,9 @@ module Ast implements AstSig { /** Gets the underlying Python `Pattern`, if this node wraps one. */ Py::Pattern asPattern() { this = TPattern(result) } + /** Gets the underlying Python `StmtList`, if this node is a `BlockStmt`. */ + Py::StmtList asStmtList() { this = TBlockStmt(result) } + /** * Gets the child of this AST node at the specified (zero-based) * index, in evaluation order. Subclasses with children override @@ -133,7 +136,7 @@ module Ast implements AstSig { } /** Gets the body of callable `c`. */ - AstNode callableGetBody(Callable c) { result = TBlockStmt(c.asScope().getBody()) } + AstNode callableGetBody(Callable c) { result.asStmtList() = c.asScope().getBody() } /** * A parameter of a callable. @@ -300,10 +303,10 @@ module Ast implements AstSig { Expr getCondition() { result.asExpr() = ifStmt.getTest() } /** Gets the `then` (true) branch of this `if` statement. */ - Stmt getThen() { result = TBlockStmt(ifStmt.getBody()) } + Stmt getThen() { result.asStmtList() = ifStmt.getBody() } /** Gets the `else` (false) branch, if any. */ - Stmt getElse() { result = TBlockStmt(ifStmt.getOrelse()) } + Stmt getElse() { result.asStmtList() = ifStmt.getOrelse() } override AstNode getChild(int index) { index = 0 and result = this.getCondition() @@ -335,10 +338,10 @@ module Ast implements AstSig { /** Gets the boolean condition of this `while` loop. */ Expr getCondition() { result.asExpr() = whileStmt.getTest() } - override Stmt getBody() { result = TBlockStmt(whileStmt.getBody()) } + override Stmt getBody() { result.asStmtList() = whileStmt.getBody() } /** Gets the `else` branch of this `while` loop, if any. */ - Stmt getElse() { result = TBlockStmt(whileStmt.getOrelse()) } + Stmt getElse() { result.asStmtList() = whileStmt.getOrelse() } override AstNode getChild(int index) { index = 0 and result = this.getCondition() @@ -381,10 +384,10 @@ module Ast implements AstSig { /** Gets the collection being iterated. */ Expr getCollection() { result.asExpr() = forStmt.getIter() } - override Stmt getBody() { result = TBlockStmt(forStmt.getBody()) } + override Stmt getBody() { result.asStmtList() = forStmt.getBody() } /** Gets the `else` branch of this `for` loop, if any. */ - Stmt getElse() { result = TBlockStmt(forStmt.getOrelse()) } + Stmt getElse() { result.asStmtList() = forStmt.getOrelse() } override AstNode getChild(int index) { index = 0 and result = this.getCollection() @@ -453,7 +456,7 @@ module Ast implements AstSig { Expr getOptionalVars() { result.asExpr() = withStmt.getOptionalVars() } - Stmt getBody() { result = TBlockStmt(withStmt.getBody()) } + Stmt getBody() { result.asStmtList() = withStmt.getBody() } override AstNode getChild(int index) { index = 0 and result = this.getContextExpr() @@ -498,14 +501,14 @@ module Ast implements AstSig { TryStmt() { this = TPyStmt(tryStmt) } - Stmt getBody() { result = TBlockStmt(tryStmt.getBody()) } + Stmt getBody() { result.asStmtList() = tryStmt.getBody() } /** Gets the `else` branch of this `try` statement, if any. */ - Stmt getElse() { result = TBlockStmt(tryStmt.getOrelse()) } + Stmt getElse() { result.asStmtList() = tryStmt.getOrelse() } - Stmt getFinally() { result = TBlockStmt(tryStmt.getFinalbody()) } + Stmt getFinally() { result.asStmtList() = tryStmt.getFinalbody() } - CatchClause getCatch(int index) { result = TPyStmt(tryStmt.getHandler(index)) } + CatchClause getCatch(int index) { result.asStmt() = tryStmt.getHandler(index) } override AstNode getChild(int index) { index = 0 and result = this.getBody() @@ -550,9 +553,9 @@ module Ast implements AstSig { /** Gets the body of this exception handler. */ Stmt getBody() { - result = TBlockStmt(handler.(Py::ExceptStmt).getBody()) + result.asStmtList() = handler.(Py::ExceptStmt).getBody() or - result = TBlockStmt(handler.(Py::ExceptGroupStmt).getBody()) + result.asStmtList() = handler.(Py::ExceptGroupStmt).getBody() } override AstNode getChild(int index) { @@ -572,7 +575,7 @@ module Ast implements AstSig { Expr getExpr() { result.asExpr() = matchStmt.getSubject() } - Case getCase(int index) { result = TPyStmt(matchStmt.getCase(index)) } + Case getCase(int index) { result.asStmt() = matchStmt.getCase(index) } Stmt getStmt(int index) { none() } @@ -589,11 +592,11 @@ module Ast implements AstSig { Case() { this = TPyStmt(caseStmt) } - AstNode getAPattern() { result = TPattern(caseStmt.getPattern()) } + AstNode getAPattern() { result.asPattern() = caseStmt.getPattern() } Expr getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } - AstNode getBody() { result = TBlockStmt(caseStmt.getBody()) } + AstNode getBody() { result.asStmtList() = caseStmt.getBody() } /** Holds if this case is a wildcard pattern (`case _:`). */ predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } @@ -649,6 +652,9 @@ module Ast implements AstSig { /** Gets the underlying Python `BoolExpr`. */ Py::BoolExpr getBoolExpr() { result = be } + /** Gets the (zero-based) index of this pair within its `BoolExpr`. */ + int getIndex() { result = index } + override string toString() { result = be.getOperator() } override Py::Location getLocation() { result = be.getValue(index).getLocation() } @@ -664,7 +670,10 @@ module Ast implements AstSig { index = count(be.getAValue()) - 2 and result.asExpr() = be.getValue(index + 1) or // Non-last pair: right operand is the next synthetic pair. - index < count(be.getAValue()) - 2 and result = TBoolExprPair(be, index + 1) + index < count(be.getAValue()) - 2 and + exists(BinaryExpr next | + next.getBoolExpr() = be and next.getIndex() = index + 1 and result = next + ) } override AstNode getChild(int childIndex) { From 336c7a44a80facf25bfbe61ea6f7691475efd6a0 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:12:13 +0000 Subject: [PATCH 48/72] Python: add CFG-binding gap tests (red) Adds inline-expectation tests for the new shared CFG implementation in python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll, covering every Python binding construct that introduces a variable. The test files use MISSING: annotations to record bindings whose defining Name AST node is *not* currently reachable from the new CFG. These are the 'red' half of red-green commit pairs: subsequent commits will extend AstNodeImpl to cover each construct and remove the corresponding MISSING: marker. Confirmed-broken categories: - Import aliases (from x import a) - Annotated assignment (x: int = 1) - Exception handler (except E as e) - Match patterns (case x, case [a,b], case ... as v) - PEP 695 type params (def f[T], class C[T]) Confirmed-working (no MISSING:): - Compound targets, with-as, comprehensions, decorated def/class, walrus, starred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../bindings/BindingsTest.expected | 0 .../ControlFlow/bindings/BindingsTest.ql | 37 +++++++++++++++++++ .../ControlFlow/bindings/annassign.py | 12 ++++++ .../ControlFlow/bindings/compound.py | 14 +++++++ .../ControlFlow/bindings/comprehension.py | 18 +++++++++ .../ControlFlow/bindings/decorated.py | 30 +++++++++++++++ .../ControlFlow/bindings/except_handler.py | 19 ++++++++++ .../ControlFlow/bindings/imports.py | 12 ++++++ .../ControlFlow/bindings/match_pattern.py | 23 ++++++++++++ .../ControlFlow/bindings/simple.py | 14 +++++++ .../ControlFlow/bindings/type_params.py | 18 +++++++++ .../ControlFlow/bindings/walrus_starred.py | 12 ++++++ .../ControlFlow/bindings/with_stmt.py | 21 +++++++++++ 13 files changed, 230 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/annassign.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/compound.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/comprehension.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/decorated.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/except_handler.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/imports.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/simple.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/type_params.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql new file mode 100644 index 000000000000..a88b9edc1d4d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql @@ -0,0 +1,37 @@ +/** + * Phase -1 of the dataflow CFG migration: verifies that every variable + * binding visible to the AST (`Name.defines(v)`) corresponds to a CFG node + * in the new CFG (`semmle.python.controlflow.internal.AstNodeImpl`). + * + * The expected tag is `cfgdefines=`. Each binding annotation in the + * test sources looks like `# $ cfgdefines=x` for a binding currently + * covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding + * that is known to be uncovered (a "red" test case that should be + * green-flipped once the corresponding `cfg-ext-*` extension lands). + * + * Parameters (`def f(x):` etc.) are deliberately excluded — Java's + * pattern handles parameter writes at the SSA layer (`hasEntryDef`), + * not as CFG nodes. + */ + +import python +import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +import utils.test.InlineExpectationsTest + +module CfgBindingsTest implements TestSig { + string getARelevantTag() { result = "cfgdefines" } + + predicate hasActualResult(Location location, string element, string tag, string value) { + exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg | + n.defines(v) and + not py_expr_contexts(_, 4, n) and // exclude parameters + cfg.getAstNode().asExpr() = n and + location = n.getLocation() and + element = n.toString() and + tag = "cfgdefines" and + value = v.getId() + ) + } +} + +import MakeTest diff --git a/python/ql/test/library-tests/ControlFlow/bindings/annassign.py b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py new file mode 100644 index 000000000000..9f80b8bffbdf --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py @@ -0,0 +1,12 @@ +# Annotated assignment (PEP 526). Both with and without an initializer. + +a: int = 1 # $ MISSING: cfgdefines=a +b: str = "hi" # $ MISSING: cfgdefines=b + +# Annotation without value: the AST records `c` as defined, +# but currently the new CFG has no node for it. +c: int # $ MISSING: cfgdefines=c + +class K: # $ cfgdefines=K + field: int = 0 # $ MISSING: cfgdefines=field + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/compound.py b/python/ql/test/library-tests/ControlFlow/bindings/compound.py new file mode 100644 index 000000000000..cb2f36f12ffe --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/compound.py @@ -0,0 +1,14 @@ +# Compound (tuple/list) assignment targets — actually wired in the new CFG. + +a, b = (1, 2) # $ cfgdefines=a cfgdefines=b +[c, d] = [3, 4] # $ cfgdefines=c cfgdefines=d + +# Nested unpacking. +(e, (f, g)) = (1, (2, 3)) # $ cfgdefines=e cfgdefines=f cfgdefines=g + +# Star unpacking. +h, *i = [1, 2, 3] # $ cfgdefines=h cfgdefines=i + +# Chained assignment with compound target. +j = k, l = (5, 6) # $ cfgdefines=j cfgdefines=k cfgdefines=l + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py new file mode 100644 index 000000000000..eb7c4eade26d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py @@ -0,0 +1,18 @@ +# Comprehension and `for` loop targets — wired in the new CFG. + +# Bare-name `for` target. +for i in range(3): # $ cfgdefines=i + pass + +# Compound `for` target. +for k, v in [(1, 2)]: # $ cfgdefines=k cfgdefines=v + pass + +# Comprehension targets. +_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x +_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z +_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a + +# Nested comprehensions. +_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py new file mode 100644 index 000000000000..c48906c63d84 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py @@ -0,0 +1,30 @@ +# Decorated `def`/`class` — wired in the new CFG. + + +def deco(f): # $ cfgdefines=deco + return f + + +@deco +def decorated_func(): # $ cfgdefines=decorated_func + pass + + +@deco +class DecoratedClass: # $ cfgdefines=DecoratedClass + pass + + +# Stacked decorators. +@deco +@deco +def doubly(): # $ cfgdefines=doubly + pass + + +# Inside a class body. +class Outer: # $ cfgdefines=Outer + @staticmethod + def inner(): # $ cfgdefines=inner + pass + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py b/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py new file mode 100644 index 000000000000..57b6c99fe9b6 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py @@ -0,0 +1,19 @@ +# Exception-handler name bindings. These are already wired in the new +# CFG provided the try body can raise; `raise` statements are reliably +# treated as exception sources. + +try: + raise ValueError("oops") +except ValueError as e: # $ cfgdefines=e + pass + +try: + raise TypeError("oops") +except (TypeError, KeyError) as err: # $ cfgdefines=err + pass + +# Exception groups (Python 3.11+). +try: + raise ValueError("oops") +except* ValueError as eg: # $ cfgdefines=eg + pass diff --git a/python/ql/test/library-tests/ControlFlow/bindings/imports.py b/python/ql/test/library-tests/ControlFlow/bindings/imports.py new file mode 100644 index 000000000000..1b657c7db6ca --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/imports.py @@ -0,0 +1,12 @@ +# Import aliases. All bound names below currently lack a CFG node. + +import os # $ MISSING: cfgdefines=os +import os.path # $ MISSING: cfgdefines=os +import os as o # $ MISSING: cfgdefines=o +from os import path # $ MISSING: cfgdefines=path +from os import path as p # $ MISSING: cfgdefines=p +from os import sep, linesep # $ MISSING: cfgdefines=sep MISSING: cfgdefines=linesep +from os import ( + getcwd, # $ MISSING: cfgdefines=getcwd + getcwdb, # $ MISSING: cfgdefines=getcwdb +) diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py new file mode 100644 index 000000000000..46e7a8dd4ef8 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -0,0 +1,23 @@ +# Match-statement pattern bindings. + +def f(subject): # $ cfgdefines=f + match subject: + case x: # $ MISSING: cfgdefines=x + pass + case [a, b]: # $ MISSING: cfgdefines=a MISSING: cfgdefines=b + pass + case {"k": v}: # $ MISSING: cfgdefines=v + pass + case Point(p, q): # $ MISSING: cfgdefines=p MISSING: cfgdefines=q + pass + case [_, *rest]: # $ MISSING: cfgdefines=rest + pass + case (1 | 2) as n: # $ MISSING: cfgdefines=n + pass + + +class Point: # $ cfgdefines=Point + __match_args__ = ("x", "y") # $ cfgdefines=__match_args__ + x: int # $ MISSING: cfgdefines=x + y: int # $ MISSING: cfgdefines=y + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/simple.py b/python/ql/test/library-tests/ControlFlow/bindings/simple.py new file mode 100644 index 000000000000..51cb7d828c91 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/simple.py @@ -0,0 +1,14 @@ +# Simple bindings that should already work in the new CFG. +# No MISSING annotations expected. + +x = 1 # $ cfgdefines=x +y = x + 1 # $ cfgdefines=y + +def f(): # $ cfgdefines=f + pass + +class C: # $ cfgdefines=C + pass + +# Re-assignment. +x = 2 # $ cfgdefines=x diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py new file mode 100644 index 000000000000..ab32370bd7d1 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -0,0 +1,18 @@ +# PEP 695 type parameters (Python 3.12+). + +def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T + return x + + +class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T + item: T # $ MISSING: cfgdefines=item + + +# Multi-parameter, with bound and variadics. +def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P + return x + + +# `type` statement (PEP 695). +type Alias[T] = list[T] # $ MISSING: cfgdefines=Alias MISSING: cfgdefines=T + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py new file mode 100644 index 000000000000..a168240af953 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py @@ -0,0 +1,12 @@ +# Walrus and starred-target edge cases — wired in the new CFG. + +# Walrus in expression context. +if (y := 5) > 0: # $ cfgdefines=y + pass + +# Walrus in a comprehension. +_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w + +# Starred target in a Tuple LHS. +*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py new file mode 100644 index 000000000000..47f210abc38e --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py @@ -0,0 +1,21 @@ +# `with cm() as x:` bindings — wired in the new CFG. + +class CM: # $ cfgdefines=CM + def __enter__(self): return self # $ cfgdefines=__enter__ + def __exit__(self, *a): pass # $ cfgdefines=__exit__ + +with CM() as x: # $ cfgdefines=x + pass + +# Multiple items. +with CM() as a, CM() as b: # $ cfgdefines=a cfgdefines=b + pass + +# Parenthesised form (Python 3.10+). +with (CM() as p, CM() as q): # $ cfgdefines=p cfgdefines=q + pass + +# Compound target in `with`. +with CM() as (m, n): # $ cfgdefines=m cfgdefines=n + pass + From 5d60a0d7c1c5437c7fcb8a4a4cf19ee9a1ad9c89 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:22:57 +0000 Subject: [PATCH 49/72] Python: wire AnnAssign into the shared CFG (green) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an `AnnAssignStmt` wrapper in `AstNodeImpl.qll` so that PEP 526 annotated assignments (`x: int = 1`, `x: int`) participate in the control flow graph. Evaluation order follows CPython: annotation, optional value, target binding. Without this, `x: int = 1` had no CFG node for `x` even though `Name.defines(v)` returns true for it on the AST side. SSA built on the new CFG would therefore miss every annotated-assignment write. Removes the corresponding MISSING: annotations from the CFG-binding gap test: - annassign.py — all four cases now green. - match_pattern.py — class-body annotated fields (`x: int`, `y: int`). - type_params.py — `item: T` inside class. Verified: all 24 ControlFlow/evaluation-order tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 25 +++++++++++++++++++ .../ControlFlow/bindings/annassign.py | 11 ++++---- .../ControlFlow/bindings/match_pattern.py | 4 +-- .../ControlFlow/bindings/type_params.py | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 681b05cdf05a..d59b51375a1d 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -265,6 +265,31 @@ module Ast implements AstSig { override AstNode getChild(int index) { index = 0 and result = this.getOperation() } } + /** + * An annotated assignment statement (`x: T = expr`, or `x: T` without + * value). The evaluation order follows CPython: annotation first, then + * the optional value, then the target binding. + */ + additional class AnnAssignStmt extends Stmt { + private Py::AnnAssign annAssign; + + AnnAssignStmt() { this = TPyStmt(annAssign) } + + Expr getAnnotation() { result.asExpr() = annAssign.getAnnotation() } + + Expr getValue() { result.asExpr() = annAssign.getValue() } + + Expr getTarget() { result.asExpr() = annAssign.getTarget() } + + override AstNode getChild(int index) { + index = 0 and result = this.getAnnotation() + or + index = 1 and result = this.getValue() + or + index = 2 and result = this.getTarget() + } + } + /** An assignment expression / walrus operator (`x := expr`). */ additional class NamedExpr extends Expr { private Py::AssignExpr assignExpr; diff --git a/python/ql/test/library-tests/ControlFlow/bindings/annassign.py b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py index 9f80b8bffbdf..7a9ae3ab6c79 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/annassign.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py @@ -1,12 +1,13 @@ # Annotated assignment (PEP 526). Both with and without an initializer. -a: int = 1 # $ MISSING: cfgdefines=a -b: str = "hi" # $ MISSING: cfgdefines=b +a: int = 1 # $ cfgdefines=a +b: str = "hi" # $ cfgdefines=b # Annotation without value: the AST records `c` as defined, -# but currently the new CFG has no node for it. -c: int # $ MISSING: cfgdefines=c +# and the new CFG now visits it via the AnnAssignStmt wrapper. +c: int # $ cfgdefines=c class K: # $ cfgdefines=K - field: int = 0 # $ MISSING: cfgdefines=field + field: int = 0 # $ cfgdefines=field + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py index 46e7a8dd4ef8..66fafdcb63df 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -18,6 +18,6 @@ def f(subject): # $ cfgdefines=f class Point: # $ cfgdefines=Point __match_args__ = ("x", "y") # $ cfgdefines=__match_args__ - x: int # $ MISSING: cfgdefines=x - y: int # $ MISSING: cfgdefines=y + x: int # $ cfgdefines=x + y: int # $ cfgdefines=y diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py index ab32370bd7d1..a17f6e29dfd8 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -5,7 +5,7 @@ def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T - item: T # $ MISSING: cfgdefines=item + item: T # $ cfgdefines=item # Multi-parameter, with bound and variadics. From 768ebc1e2db30d40bbabedf1c5a687fdefd63b31 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:29:28 +0000 Subject: [PATCH 50/72] Python: wire parameters into the shared CFG (C# pattern) Implements `AstSig::Parameter` and `callableGetParameter(c, i)` in `AstNodeImpl.qll`, following the C# template (`csharp/.../ControlFlowGraph.qll:147-156`) rather than Java's `Parameter() { none() }`. Each Python parameter (positional, *args, keyword-only, **kwargs) now becomes a CFG node at a stable position in the enclosing callable's entry sequence. Defaults still evaluate at function-definition time via `FunctionDefExpr.getDefault` / `LambdaExpr.getDefault`, so `Parameter::getDefaultValue()` returns `none()` (the shared CFG library calls this to model the missing-argument fallback, which Python does not surface at the CFG level). The bindings test now exercises parameters (the `py_expr_contexts(_, 4, ...)` exclusion has been removed). A new `parameters.py` test case covers positional, defaulted, vararg, kwarg, keyword-only, kitchen-sink, method (self/cls), lambda, and PEP 570 positional-only parameters. Several other test files were updated to annotate parameters that the test had previously hidden (synthetic `.0` comprehension parameter, method `self`, decorator `f`, etc.). Verified: - All 24 ControlFlow/evaluation-order tests still pass. - CFG consistency query (`python/ql/consistency-queries/CfgConsistency.ql`) shows zero violations on CPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 59 ++++++++++++++++--- .../ControlFlow/bindings/BindingsTest.ql | 5 -- .../ControlFlow/bindings/comprehension.py | 11 ++-- .../ControlFlow/bindings/decorated.py | 2 +- .../ControlFlow/bindings/match_pattern.py | 2 +- .../ControlFlow/bindings/parameters.py | 42 +++++++++++++ .../ControlFlow/bindings/type_params.py | 4 +- .../ControlFlow/bindings/walrus_starred.py | 6 +- .../ControlFlow/bindings/with_stmt.py | 4 +- 9 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/parameters.py diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d59b51375a1d..cc014440291a 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -141,22 +141,63 @@ module Ast implements AstSig { /** * A parameter of a callable. * - * TODO: Implement in order to include parameters in the CFG. + * Modelled per the C# template (`csharp/.../ControlFlowGraph.qll:147-156`): + * each Python parameter (the `Py::Parameter` AST node, which is a `Name` + * or — Python 2 only — a `Tuple` in store context) becomes a CFG node + * at a stable position in the enclosing callable's entry sequence. + * + * Default-value expressions for positional and keyword-only parameters + * are wired separately on the `FunctionDefExpr` / `LambdaExpr` wrappers + * (they evaluate at function-definition time, not at call time). + * `Parameter::getDefaultValue()` returns `none()` here, signalling to + * the shared library that the parameter never falls back to a default + * during call binding. This mirrors C# for non-optional parameters. */ - class Parameter extends AstNodeImpl { - Parameter() { none() } - - override string toString() { none() } + class Parameter extends Expr { + private Py::Parameter param; - override Py::Location getLocation() { none() } + Parameter() { this = TPyExpr(param) } - override Callable getEnclosingCallable() { none() } + /** Gets the underlying Python parameter. */ + Py::Parameter asParameter() { result = param } + /** + * Gets the default-value expression of this parameter, if any. + * + * Returns `none()`: defaults evaluate at function-definition time and + * are wired into the CFG via `FunctionDefExpr.getDefault` / + * `LambdaExpr.getDefault`. The shared library calls this predicate + * to model the "missing argument → evaluate default" fallback during + * call binding, which Python does not model at the CFG level. + */ Expr getDefaultValue() { none() } } - /** Gets the `index`th parameter of callable `c`. */ - Parameter callableGetParameter(Callable c, int index) { none() } + /** + * Gets the `index`th parameter of callable `c`, ordered as Python binds + * them at call time: positional, then vararg (`*args`), then + * keyword-only, then kwarg (`**kwargs`). + */ + Parameter callableGetParameter(Callable c, int index) { + exists(Py::Function f | f = c.asScope() | + result.asParameter() = + rank[index + 1](Py::Parameter p, int subOrder, int subIndex | + // positional parameters first + p = f.getArg(subIndex) and subOrder = 0 + or + // then *args + p = f.getVararg() and subOrder = 1 and subIndex = 0 + or + // then keyword-only parameters + p = f.getKeywordOnlyArg(subIndex) and subOrder = 2 + or + // finally **kwargs + p = f.getKwarg() and subOrder = 3 and subIndex = 0 + | + p order by subOrder, subIndex + ) + ) + } /** A statement. */ class Stmt extends AstNodeImpl, TStmt { diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql index a88b9edc1d4d..a507878911b1 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql +++ b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql @@ -8,10 +8,6 @@ * covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding * that is known to be uncovered (a "red" test case that should be * green-flipped once the corresponding `cfg-ext-*` extension lands). - * - * Parameters (`def f(x):` etc.) are deliberately excluded — Java's - * pattern handles parameter writes at the SSA layer (`hasEntryDef`), - * not as CFG nodes. */ import python @@ -24,7 +20,6 @@ module CfgBindingsTest implements TestSig { predicate hasActualResult(Location location, string element, string tag, string value) { exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg | n.defines(v) and - not py_expr_contexts(_, 4, n) and // exclude parameters cfg.getAstNode().asExpr() = n and location = n.getLocation() and element = n.toString() and diff --git a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py index eb7c4eade26d..6b5f722c1f7e 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py @@ -1,4 +1,6 @@ # Comprehension and `for` loop targets — wired in the new CFG. +# Comprehensions are nested function scopes with a synthetic `.0` parameter +# bound to the iterable. # Bare-name `for` target. for i in range(3): # $ cfgdefines=i @@ -9,10 +11,11 @@ pass # Comprehension targets. -_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x -_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z -_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a +_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x cfgdefines=.0 +_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z cfgdefines=.0 +_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a cfgdefines=.0 # Nested comprehensions. -_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b +_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b cfgdefines=.0 + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py index c48906c63d84..9b93c166acec 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py @@ -1,7 +1,7 @@ # Decorated `def`/`class` — wired in the new CFG. -def deco(f): # $ cfgdefines=deco +def deco(f): # $ cfgdefines=deco cfgdefines=f return f diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py index 66fafdcb63df..5977e3443838 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -1,6 +1,6 @@ # Match-statement pattern bindings. -def f(subject): # $ cfgdefines=f +def f(subject): # $ cfgdefines=f cfgdefines=subject match subject: case x: # $ MISSING: cfgdefines=x pass diff --git a/python/ql/test/library-tests/ControlFlow/bindings/parameters.py b/python/ql/test/library-tests/ControlFlow/bindings/parameters.py new file mode 100644 index 000000000000..7fe5e01e4c4b --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/parameters.py @@ -0,0 +1,42 @@ +# Function parameters. + +def positional(a, b): # $ cfgdefines=positional cfgdefines=a cfgdefines=b + pass + + +def with_default(x=1, y=2): # $ cfgdefines=with_default cfgdefines=x cfgdefines=y + pass + + +def with_vararg(*args): # $ cfgdefines=with_vararg cfgdefines=args + pass + + +def with_kwarg(**kwargs): # $ cfgdefines=with_kwarg cfgdefines=kwargs + pass + + +def with_kwonly(*, k1, k2=5): # $ cfgdefines=with_kwonly cfgdefines=k1 cfgdefines=k2 + pass + + +def kitchen_sink(a, b=2, *args, k1, k2=5, **kw): # $ cfgdefines=kitchen_sink cfgdefines=a cfgdefines=b cfgdefines=args cfgdefines=k1 cfgdefines=k2 cfgdefines=kw + pass + + +# Methods get `self` / `cls`. +class C: # $ cfgdefines=C + def method(self, x): # $ cfgdefines=method cfgdefines=self cfgdefines=x + pass + + @classmethod + def cmethod(cls, x): # $ cfgdefines=cmethod cfgdefines=cls cfgdefines=x + pass + + +# Lambda parameter. +_ = lambda p: p + 1 # $ cfgdefines=_ cfgdefines=p + +# PEP 570 positional-only. +def pos_only(a, b, /, c): # $ cfgdefines=pos_only cfgdefines=a cfgdefines=b cfgdefines=c + pass diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py index a17f6e29dfd8..554b96c218ab 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -1,6 +1,6 @@ # PEP 695 type parameters (Python 3.12+). -def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T +def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T return x @@ -9,7 +9,7 @@ class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T # Multi-parameter, with bound and variadics. -def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P +def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P return x diff --git a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py index a168240af953..5c0c1bd83191 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py @@ -4,9 +4,11 @@ if (y := 5) > 0: # $ cfgdefines=y pass -# Walrus in a comprehension. -_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w +# Walrus in a comprehension. The comprehension introduces a synthetic +# `.0` parameter bound to the iterable. +_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w cfgdefines=.0 # Starred target in a Tuple LHS. *head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py index 47f210abc38e..5fffe46c5d40 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py @@ -1,8 +1,8 @@ # `with cm() as x:` bindings — wired in the new CFG. class CM: # $ cfgdefines=CM - def __enter__(self): return self # $ cfgdefines=__enter__ - def __exit__(self, *a): pass # $ cfgdefines=__exit__ + def __enter__(self): return self # $ cfgdefines=__enter__ cfgdefines=self + def __exit__(self, *a): pass # $ cfgdefines=__exit__ cfgdefines=self cfgdefines=a with CM() as x: # $ cfgdefines=x pass From ba9dc9f5f1a54d676e409d09c1f22452d4b9d13b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:35:47 +0000 Subject: [PATCH 51/72] Python: wire import-statement bindings into the shared CFG (green) Adds `ImportStmt` and `ImportStarStmt` wrappers in `AstNodeImpl.qll`. For each `Alias` in an import statement, both the value (module/member expression) and the bound `asname` Name become children of the CFG node for the import statement, in evaluation order. Without this, every `Name` introduced by `import` / `from .. import ..` lacked a CFG node, even though `Name.defines(v)` returns true for it on the AST side. This was the highest-volume gap: 20,332 missing import aliases across CPython. Removes the corresponding MISSING: annotations from imports.py. Verified: all 24 ControlFlow/evaluation-order tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 48 +++++++++++++++++++ .../ControlFlow/bindings/imports.py | 20 ++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index cc014440291a..9797f727e47c 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -512,6 +512,54 @@ module Ast implements AstSig { } } + /** + * An `import` statement (`import a, b` or `from m import a, b`). + * + * Each alias contributes two children in evaluation order: first the + * value expression (which performs the import side-effect), then the + * bound `asname` Name (the in-scope binding). This makes both reachable + * from the CFG and allows `Name.defines(v)` for `asname` Names to have + * corresponding CFG nodes — which is essential for SSA to see import + * bindings. + */ + additional class ImportStmt extends Stmt { + private Py::Import imp; + + ImportStmt() { this = TPyStmt(imp) } + + /** Gets the value (module/member expression) of the `n`th alias. */ + Expr getValue(int n) { result.asExpr() = imp.getName(n).getValue() } + + /** Gets the bound `asname` of the `n`th alias. */ + Expr getAsname(int n) { result.asExpr() = imp.getName(n).getAsname() } + + /** Gets the number of aliases in this import statement. */ + int getNumberOfAliases() { result = count(int i | exists(imp.getName(i))) } + + override AstNode getChild(int index) { + exists(int i | + index = 2 * i and result = this.getValue(i) + or + index = 2 * i + 1 and result = this.getAsname(i) + ) + } + } + + /** + * A `from m import *` statement. Evaluates the module expression but + * binds no name (the bindings happen by side-effect at runtime, which + * is not modelled at the CFG level). + */ + additional class ImportStarStmt extends Stmt { + private Py::ImportStar imp; + + ImportStarStmt() { this = TPyStmt(imp) } + + Expr getModule() { result.asExpr() = imp.getModule() } + + override AstNode getChild(int index) { index = 0 and result = this.getModule() } + } + /** A `with` statement. */ additional class WithStmt extends Stmt { private Py::With withStmt; diff --git a/python/ql/test/library-tests/ControlFlow/bindings/imports.py b/python/ql/test/library-tests/ControlFlow/bindings/imports.py index 1b657c7db6ca..c8834b5332a0 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/imports.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/imports.py @@ -1,12 +1,14 @@ -# Import aliases. All bound names below currently lack a CFG node. +# Import aliases — all bound names below are now reachable via the new +# CFG's `ImportStmt` wrapper. -import os # $ MISSING: cfgdefines=os -import os.path # $ MISSING: cfgdefines=os -import os as o # $ MISSING: cfgdefines=o -from os import path # $ MISSING: cfgdefines=path -from os import path as p # $ MISSING: cfgdefines=p -from os import sep, linesep # $ MISSING: cfgdefines=sep MISSING: cfgdefines=linesep +import os # $ cfgdefines=os +import os.path # $ cfgdefines=os +import os as o # $ cfgdefines=o +from os import path # $ cfgdefines=path +from os import path as p # $ cfgdefines=p +from os import sep, linesep # $ cfgdefines=sep cfgdefines=linesep from os import ( - getcwd, # $ MISSING: cfgdefines=getcwd - getcwdb, # $ MISSING: cfgdefines=getcwdb + getcwd, # $ cfgdefines=getcwd + getcwdb, # $ cfgdefines=getcwdb ) + From f12307278a52bfe2e9308123b98ccd523dcfd269 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 13:11:18 +0000 Subject: [PATCH 52/72] Python: wire match-pattern bindings into the shared CFG (green) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds concrete `Pattern` subclasses in `AstNodeImpl.qll` for every `MatchPattern` AST kind, with `getChild` overrides that expose sub-patterns and bound Names. Specifically: - MatchCapturePattern (`case x:`) -> getVariable() - MatchAsPattern (`case … as v:`) -> getPattern(), getAlias() - MatchStarPattern (`case [*rest]:`) -> getTarget() - MatchSequencePattern (`case [a, b]:`) -> getPattern(i) - MatchClassPattern (`case Cls(p, q, k=v)`) -> getClass(), positional, keyword - MatchMappingPattern (`case {k: v}:`) -> getMapping(i) - MatchKeyValuePattern, MatchKeywordPattern, MatchDoubleStarPattern - MatchOrPattern, MatchLiteralPattern, MatchValuePattern Without these, every Name bound by a match pattern lacked a CFG node. Removes the corresponding MISSING: annotations from match_pattern.py (all 11 cases). Verified: all 24 ControlFlow/evaluation-order tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 188 ++++++++++++++++++ .../ControlFlow/bindings/match_pattern.py | 15 +- 2 files changed, 196 insertions(+), 7 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 9797f727e47c..7c3b498cfea7 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -234,6 +234,194 @@ module Ast implements AstSig { override Callable getEnclosingCallable() { result.asScope() = p.getScope() } } + /** + * A `case x` pattern that binds `x` to the matched value. + */ + additional class MatchCapturePattern extends Pattern { + private Py::MatchCapturePattern cap; + + MatchCapturePattern() { this = TPattern(cap) } + + /** Gets the bound Name expression. */ + Expr getVariable() { result.asExpr() = cap.getVariable() } + + override AstNode getChild(int index) { index = 0 and result = this.getVariable() } + } + + /** + * A `case pattern as name` pattern. + */ + additional class MatchAsPattern extends Pattern { + private Py::MatchAsPattern asp; + + MatchAsPattern() { this = TPattern(asp) } + + /** Gets the inner pattern. */ + AstNode getPattern() { result.asPattern() = asp.getPattern() } + + /** Gets the bound Name expression. */ + Expr getAlias() { result.asExpr() = asp.getAlias() } + + override AstNode getChild(int index) { + index = 0 and result = this.getPattern() + or + index = 1 and result = this.getAlias() + } + } + + /** + * A `case [a, b, *rest]` star pattern. Binds `rest` to the remaining + * elements of the sequence. + */ + additional class MatchStarPattern extends Pattern { + private Py::MatchStarPattern starp; + + MatchStarPattern() { this = TPattern(starp) } + + /** Gets the target Pattern (a `MatchCapturePattern` if `*rest`). */ + AstNode getTarget() { result.asPattern() = starp.getTarget() } + + override AstNode getChild(int index) { index = 0 and result = this.getTarget() } + } + + /** + * A `case [a, b, ...]` sequence pattern. Recurses into the sub-patterns. + */ + additional class MatchSequencePattern extends Pattern { + private Py::MatchSequencePattern seqp; + + MatchSequencePattern() { this = TPattern(seqp) } + + /** Gets the `n`th sub-pattern. */ + AstNode getPattern(int n) { result.asPattern() = seqp.getPattern(n) } + + override AstNode getChild(int index) { result = this.getPattern(index) } + } + + /** + * A `case Cls(a, b, x=y)` class pattern. + */ + additional class MatchClassPattern extends Pattern { + private Py::MatchClassPattern clsp; + + MatchClassPattern() { this = TPattern(clsp) } + + /** Gets the class expression of this class pattern. */ + Expr getClass() { result.asExpr() = clsp.getClass() } + + /** Gets the `n`th positional sub-pattern. */ + AstNode getPositional(int n) { result.asPattern() = clsp.getPositional(n) } + + /** Gets the `n`th keyword sub-pattern. */ + AstNode getKeyword(int n) { result.asPattern() = clsp.getKeyword(n) } + + private int numPositional() { result = count(int i | exists(clsp.getPositional(i))) } + + override AstNode getChild(int index) { + index = 0 and result = this.getClass() + or + result = this.getPositional(index - 1) and index >= 1 + or + result = this.getKeyword(index - 1 - this.numPositional()) and + index >= 1 + this.numPositional() + } + } + + /** + * A `case {k: v}` mapping pattern. + */ + additional class MatchMappingPattern extends Pattern { + private Py::MatchMappingPattern mapp; + + MatchMappingPattern() { this = TPattern(mapp) } + + AstNode getMapping(int n) { result.asPattern() = mapp.getMapping(n) } + + override AstNode getChild(int index) { result = this.getMapping(index) } + } + + /** + * A key-value pair inside a `case {k: v}` mapping pattern. + */ + additional class MatchKeyValuePattern extends Pattern { + private Py::MatchKeyValuePattern kvp; + + MatchKeyValuePattern() { this = TPattern(kvp) } + + AstNode getKey() { result.asPattern() = kvp.getKey() } + + AstNode getValue() { result.asPattern() = kvp.getValue() } + + override AstNode getChild(int index) { + index = 0 and result = this.getKey() + or + index = 1 and result = this.getValue() + } + } + + /** + * A `case Cls(name=value)` keyword sub-pattern. + */ + additional class MatchKeywordPattern extends Pattern { + private Py::MatchKeywordPattern kwp; + + MatchKeywordPattern() { this = TPattern(kwp) } + + Expr getAttribute() { result.asExpr() = kwp.getAttribute() } + + AstNode getValue() { result.asPattern() = kwp.getValue() } + + override AstNode getChild(int index) { + index = 0 and result = this.getAttribute() + or + index = 1 and result = this.getValue() + } + } + + /** A `case **rest` double-star mapping sub-pattern. */ + additional class MatchDoubleStarPattern extends Pattern { + private Py::MatchDoubleStarPattern dsp; + + MatchDoubleStarPattern() { this = TPattern(dsp) } + + AstNode getTarget() { result.asPattern() = dsp.getTarget() } + + override AstNode getChild(int index) { index = 0 and result = this.getTarget() } + } + + /** A `case p1 | p2 | …` or-pattern. */ + additional class MatchOrPattern extends Pattern { + private Py::MatchOrPattern orp; + + MatchOrPattern() { this = TPattern(orp) } + + AstNode getPattern(int n) { result.asPattern() = orp.getPattern(n) } + + override AstNode getChild(int index) { result = this.getPattern(index) } + } + + /** A `case 1` literal pattern. */ + additional class MatchLiteralPattern extends Pattern { + private Py::MatchLiteralPattern litp; + + MatchLiteralPattern() { this = TPattern(litp) } + + Expr getLiteral() { result.asExpr() = litp.getLiteral() } + + override AstNode getChild(int index) { index = 0 and result = this.getLiteral() } + } + + /** A `case Cls.NAME` value pattern. */ + additional class MatchValuePattern extends Pattern { + private Py::MatchValuePattern vp; + + MatchValuePattern() { this = TPattern(vp) } + + Expr getValue() { result.asExpr() = vp.getValue() } + + override AstNode getChild(int index) { index = 0 and result = this.getValue() } + } + /** * A block statement, modeling the body of a parent AST node as a * sequence of statements. diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py index 5977e3443838..0868a2680d0a 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -1,18 +1,18 @@ -# Match-statement pattern bindings. +# Match-statement pattern bindings — wired in the new CFG. def f(subject): # $ cfgdefines=f cfgdefines=subject match subject: - case x: # $ MISSING: cfgdefines=x + case x: # $ cfgdefines=x pass - case [a, b]: # $ MISSING: cfgdefines=a MISSING: cfgdefines=b + case [a, b]: # $ cfgdefines=a cfgdefines=b pass - case {"k": v}: # $ MISSING: cfgdefines=v + case {"k": v}: # $ cfgdefines=v pass - case Point(p, q): # $ MISSING: cfgdefines=p MISSING: cfgdefines=q + case Point(p, q): # $ cfgdefines=p cfgdefines=q pass - case [_, *rest]: # $ MISSING: cfgdefines=rest + case [_, *rest]: # $ cfgdefines=rest pass - case (1 | 2) as n: # $ MISSING: cfgdefines=n + case (1 | 2) as n: # $ cfgdefines=n pass @@ -21,3 +21,4 @@ class Point: # $ cfgdefines=Point x: int # $ cfgdefines=x y: int # $ cfgdefines=y + From 01c6b2b262fb65ec41ec4fceb3fb21488be4bd9c Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 09:58:27 +0000 Subject: [PATCH 53/72] Python: wire PEP 695 type parameters into the shared CFG (green) Adds CFG coverage for the binding 'Name's introduced by PEP 695 type-parameter syntax on functions, classes, and 'type' aliases: def func[T](...): ... class Box[T]: ... def multi[T: int, *Ts, **P](...): ... type Alias[T] = ... For each parametrised AST node, the type-parameter names (and, for 'type' aliases, the alias name itself) are added as children of the enclosing CFG node so that 'Name.defines(v)' has a corresponding position. Bounds and defaults are intentionally not wired (they have no SSA-relevant semantics for our purposes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 78 ++++++++++++++++++- .../ControlFlow/bindings/type_params.py | 8 +- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 7c3b498cfea7..d7e54e64aa8b 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -15,6 +15,19 @@ private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType private import codeql.util.Void +/** + * Gets the bound `Name` of a PEP 695 type parameter (`TypeVar`, + * `ParamSpec`, or `TypeVarTuple`). The base `TypeParameter` class does + * not expose `getName()`; this helper dispatches over the subtypes. + */ +private Py::Name typeParameterName(Py::TypeParameter tp) { + result = tp.(Py::TypeVar).getName() + or + result = tp.(Py::ParamSpec).getName() + or + result = tp.(Py::TypeVarTuple).getName() +} + /** Provides the Python implementation of the shared CFG `AstSig`. */ module Ast implements AstSig { private newtype TAstNode = @@ -797,6 +810,37 @@ module Ast implements AstSig { override AstNode getChild(int index) { result = this.getTarget(index) } } + /** + * A PEP 695 `type` statement (`type Alias[T1, T2] = value`). + * + * The type parameters bind at statement-evaluation time. The value + * expression is captured for lazy evaluation but the alias `Name` + * itself binds the resulting `TypeAliasType` object — so the CFG must + * visit at minimum the type-parameter names and the alias name. + */ + additional class TypeAliasStmt extends Stmt { + private Py::TypeAlias ta; + + TypeAliasStmt() { this = TPyStmt(ta) } + + /** Gets the alias `Name` bound by this statement. */ + Expr getName() { result.asExpr() = ta.getName() } + + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. + */ + Expr getTypeParamName(int n) { result.asExpr() = typeParameterName(ta.getTypeParameter(n)) } + + int getNumberOfTypeParams() { result = count(ta.getATypeParameter()) } + + override AstNode getChild(int index) { + result = this.getTypeParamName(index) + or + index = this.getNumberOfTypeParams() and result = this.getName() + } + } + /** A `try` statement. */ class TryStmt extends Stmt { private Py::Try tryStmt; @@ -1359,9 +1403,24 @@ module Ast implements AstSig { ClassDefExpr() { this = TPyExpr(classExpr) } + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. These bind in the enclosing scope + * at class-definition time, so the CFG must visit them. + */ + Expr getTypeParamName(int n) { + result.asExpr() = typeParameterName(classExpr.getTypeParameter(n)) + } + + int getNumberOfTypeParams() { result = count(classExpr.getATypeParameter()) } + Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) } - override AstNode getChild(int index) { result = this.getBase(index) } + override AstNode getChild(int index) { + result = this.getTypeParamName(index) + or + result = this.getBase(index - this.getNumberOfTypeParams()) + } } /** A function definition expression (has default args evaluated at definition time). */ @@ -1370,6 +1429,17 @@ module Ast implements AstSig { FunctionDefExpr() { this = TPyExpr(funcExpr) } + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. These bind in the enclosing scope + * at function-definition time, so the CFG must visit them. + */ + Expr getTypeParamName(int n) { + result.asExpr() = typeParameterName(funcExpr.getInnerScope().getTypeParameter(n)) + } + + int getNumberOfTypeParams() { result = count(funcExpr.getInnerScope().getATypeParameter()) } + /** * Gets the `n`th default for a positional argument, in evaluation * order. Note that `Args.getDefault(int)` is indexed by argument @@ -1390,9 +1460,11 @@ module Ast implements AstSig { int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } override AstNode getChild(int index) { - result = this.getDefault(index) + result = this.getTypeParamName(index) or - result = this.getKwDefault(index - this.getNumberOfDefaults()) + result = this.getDefault(index - this.getNumberOfTypeParams()) + or + result = this.getKwDefault(index - this.getNumberOfTypeParams() - this.getNumberOfDefaults()) } } diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py index 554b96c218ab..3e5aaf9d042a 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -1,18 +1,18 @@ # PEP 695 type parameters (Python 3.12+). -def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T +def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x cfgdefines=T return x -class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T +class Box[T]: # $ cfgdefines=Box cfgdefines=T item: T # $ cfgdefines=item # Multi-parameter, with bound and variadics. -def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P +def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs cfgdefines=T cfgdefines=Ts cfgdefines=P return x # `type` statement (PEP 695). -type Alias[T] = list[T] # $ MISSING: cfgdefines=Alias MISSING: cfgdefines=T +type Alias[T] = list[T] # $ cfgdefines=Alias cfgdefines=T From 3c21bbfbf521c70019ef1e3e73bbd6e7eeebecbd Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 10:48:43 +0000 Subject: [PATCH 54/72] 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 000000000000..dbfb857b5360 --- /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 From 84484891b8e3d06589d409f43276df8e08dd0bea Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 11:03:32 +0000 Subject: [PATCH 55/72] Python: introduce new-CFG facade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 'Cfg.qll' alongside 'AstNodeImpl.qll' in the controlflow internal package. The facade re-exposes the same API surface as the legacy 'semmle/python/Flow.qll' (ControlFlowNode, BasicBlock, NameNode, CallNode, AttrNode, ImportExprNode, ImportMemberNode, ImportStarNode, SubscriptNode, CompareNode, IfExprNode, AssignmentExprNode, BinaryExprNode, BoolExprNode, UnaryExprNode, DefinitionNode, DeletionNode, ForNode, RaiseStmtNode, StarredNode, ExceptFlowNode, ExceptGroupFlowNode, TupleNode, ListNode, SetNode, DictNode, IterableNode, NameConstantNode), but is implemented on top of the new shared CFG via 'AstNodeImpl.qll'. The variable-identity predicates ('NameNode.defines', '.uses', '.deletes', '.isLocal', '.isNonLocal', ...) are one-line bridges to the underlying AST predicates ('Name.defines', '.uses', '.deletes'), mirroring the Java pattern. Re-exports 'EntryBasicBlock' and 'dominatingEdge/2' from the shared 'BB::CfgSig' produced by 'AstNodeImpl.qll', so downstream consumers (e.g. the SSA adapter) can wire the new CFG into other shared modules that expect a 'CfgSig' implementation. This facade is not yet consumed by the dataflow library — that is the next phase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/controlflow/internal/Cfg.qll | 836 ++++++++++++++++++ 1 file changed, 836 insertions(+) create mode 100644 python/ql/lib/semmle/python/controlflow/internal/Cfg.qll diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll new file mode 100644 index 000000000000..3a19049c520d --- /dev/null +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -0,0 +1,836 @@ +/** + * Provides a Python control flow graph facade backed by the shared + * `codeql.controlflow.ControlFlowGraph` library (via `AstNodeImpl.qll`). + * + * This module re-exposes the same API surface as `semmle/python/Flow.qll` + * (the legacy CFG), but is implemented on the new shared CFG. It is + * intended as a drop-in replacement for use by the Python dataflow library + * and other downstream code. + * + * Layering follows the Java pattern (`java/ql/lib/semmle/code/java/Expr.qll` + * and `SsaImpl.qll`): variable identity and similar AST-level semantics + * live on the Python AST classes (`Name.defines(v)`, `Name.uses(v)`, ...); + * the CFG layer is purely positional, with `toAst` / `getNode` bridging + * back to the AST. The shared SSA library can then be parameterized on + * (`BasicBlock`, `int`) directly, with no CFG-level variable predicates. + */ +overlay[local?] +module; + +private import python as Py +private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +private import codeql.controlflow.SuccessorType + +/** + * Gets the Python AST node corresponding to CFG node `n`, if any. + * + * Entry/exit/synthetic CFG nodes have no Python AST node, so this is + * partial. + */ +private Py::AstNode toAst(CfgImpl::ControlFlowNode n) { + result = CfgImpl::astNodeToPyNode(n.getAstNode()) +} + +/** + * Holds if `n` is a CFG node representing the canonical position for an + * AST node from the dataflow library's perspective. + * + * For most expressions this is the "after"-evaluation point (post-order + * representative). For statements it is the post-order node when one + * exists. We additionally include the synthetic entry/exit nodes for the + * benefit of API consumers that ask "is this the entry node of a scope?". + * + * In conditional contexts the after-position of a boolean expression + * splits into separate `isAfterTrue` and `isAfterFalse` nodes; both are + * canonical, so a single AST expression may correspond to more than one + * `ControlFlowNode`. + */ +private predicate isCanonical(CfgImpl::ControlFlowNode n) { + n.isAfter(_) + or + n instanceof CfgImpl::ControlFlow::EntryNode + or + n instanceof CfgImpl::ControlFlow::ExitNode +} + +/** + * A control flow node. Control flow nodes have a many-to-one relation + * with syntactic nodes, although most syntactic nodes have only one + * corresponding control flow node. + * + * Edges between control flow nodes include exceptional as well as normal + * control flow. + */ +class ControlFlowNode extends CfgImpl::ControlFlowNode { + ControlFlowNode() { isCanonical(this) } + + /** Gets the syntactic element corresponding to this flow node, if any. */ + Py::AstNode getNode() { result = toAst(this) } + + /** Gets a predecessor of this flow node. */ + ControlFlowNode getAPredecessor() { this = result.getASuccessor() } + + /** Gets a successor of this flow node. */ + pragma[inline] + ControlFlowNode getASuccessor() { result = nextCanonical(this) } + + /** Gets a successor for this node if the relevant condition is True. */ + ControlFlowNode getATrueSuccessor() { + super.isAfterTrue(_) and + exists(CfgImpl::ControlFlowNode other | other.isAfterFalse(super.getAstNode())) and + result = nextCanonical(this) + } + + /** Gets a successor for this node if the relevant condition is False. */ + ControlFlowNode getAFalseSuccessor() { + super.isAfterFalse(_) and + exists(CfgImpl::ControlFlowNode other | other.isAfterTrue(super.getAstNode())) and + result = nextCanonical(this) + } + + /** Gets a successor for this node if an exception is raised. */ + ControlFlowNode getAnExceptionalSuccessor() { + exists(CfgImpl::ControlFlowNode mid | + mid = super.getAnExceptionSuccessor() and + result = nextCanonicalFrom(mid) + ) + } + + /** Gets a successor for this node if no exception is raised. */ + ControlFlowNode getANormalSuccessor() { + result = this.getASuccessor() and + not result = this.getAnExceptionalSuccessor() + } + + /** Gets the basic block containing this flow node. */ + BasicBlock getBasicBlock() { result = super.getBasicBlock() } + + /** Gets the scope containing this flow node. */ + Py::Scope getScope() { result = super.getEnclosingCallable().asScope() } + + /** Gets the enclosing module. */ + Py::Module getEnclosingModule() { result = this.getScope().getEnclosingModule() } + + /** Gets the immediate dominator of this flow node. */ + ControlFlowNode getImmediateDominator() { + // Defined positionally via the basic-block dominance tree. + exists(BasicBlock bb, int i | bb.getNode(i) = this | + // Predecessor within the same basic block. + i > 0 and result = bb.getNode(i - 1) + or + // First node of `bb`: dominator is the last node of the immediate dominator block. + i = 0 and result = bb.getImmediateDominator().getLastNode() + ) + } + + /** Holds if this strictly dominates `other`. */ + pragma[inline] + predicate strictlyDominates(ControlFlowNode other) { super.strictlyDominates(other) } + + /** Holds if this dominates `other` (reflexively). */ + pragma[inline] + predicate dominates(ControlFlowNode other) { super.dominates(other) } + + /** Holds if this is the first node in its enclosing scope. */ + predicate isEntryNode() { this instanceof CfgImpl::ControlFlow::EntryNode } + + /** Holds if this is the first node of a module. */ + predicate isModuleEntry() { + this.isEntryNode() and super.getAstNode().asScope() instanceof Py::Module + } + + /** Holds if this node may exit its scope by raising an exception. */ + predicate isExceptionalExit(Py::Scope s) { + this instanceof CfgImpl::ControlFlow::ExceptionalExitNode and + super.getEnclosingCallable().asScope() = s + } + + /** Holds if this node is a normal (non-exceptional) exit. */ + predicate isNormalExit() { this instanceof CfgImpl::ControlFlow::NormalExitNode } + + // ===== AST-shape predicates (bridges to the wrapped Python AST) ===== + /** Holds if this flow node is a load (including those in augmented assignments). */ + predicate isLoad() { + exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this)) + } + + /** Holds if this flow node is a store (including those in augmented assignments). */ + predicate isStore() { + exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 5, e) or augstore(_, this)) + } + + /** Holds if this flow node is a delete. */ + predicate isDelete() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 2, e)) } + + /** Holds if this flow node is a parameter. */ + predicate isParameter() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 4, e)) } + + /** Holds if this flow node is a store in an augmented assignment. */ + predicate isAugStore() { augstore(_, this) } + + /** Holds if this flow node is a load in an augmented assignment. */ + predicate isAugLoad() { augstore(this, _) } + + /** Holds if this flow node corresponds to a literal. */ + predicate isLiteral() { + toAst(this) instanceof Py::Bytes or + toAst(this) instanceof Py::Dict or + toAst(this) instanceof Py::DictComp or + toAst(this) instanceof Py::Set or + toAst(this) instanceof Py::SetComp or + toAst(this) instanceof Py::Ellipsis or + toAst(this) instanceof Py::GeneratorExp or + toAst(this) instanceof Py::Lambda or + toAst(this) instanceof Py::ListComp or + toAst(this) instanceof Py::List or + toAst(this) instanceof Py::Num or + toAst(this) instanceof Py::Tuple or + toAst(this) instanceof Py::Unicode or + toAst(this) instanceof Py::NameConstant + } + + /** Holds if this flow node corresponds to an attribute expression. */ + predicate isAttribute() { toAst(this) instanceof Py::Attribute } + + /** Holds if this flow node corresponds to a subscript expression. */ + predicate isSubscript() { toAst(this) instanceof Py::Subscript } + + /** Holds if this flow node corresponds to an import member. */ + predicate isImportMember() { toAst(this) instanceof Py::ImportMember } + + /** Holds if this flow node corresponds to a call. */ + predicate isCall() { toAst(this) instanceof Py::Call } + + /** Holds if this flow node corresponds to an import. */ + predicate isImport() { toAst(this) instanceof Py::ImportExpr } + + /** Holds if this flow node corresponds to a conditional expression. */ + predicate isIfExp() { toAst(this) instanceof Py::IfExp } + + /** Holds if this flow node corresponds to a function definition expression. */ + predicate isFunction() { toAst(this) instanceof Py::FunctionExpr } + + /** Holds if this flow node corresponds to a class definition expression. */ + predicate isClass() { toAst(this) instanceof Py::ClassExpr } + + /** Internal: raw successor predicate that does NOT skip non-canonical nodes. */ + CfgImpl::ControlFlowNode getASuccessorRaw() { result = super.getASuccessor() } +} + +/** + * Holds if `n` is an augmented assignment load and `store` is the + * corresponding store node. + */ +private predicate augstore(ControlFlowNode load, ControlFlowNode store) { + exists(Py::Expr load_store | exists(Py::AugAssign aa | aa.getTarget() = load_store) | + toAst(load) = load_store and + toAst(store) = load_store and + load.strictlyDominates(store) + ) +} + +/** + * Gets the nearest canonical CFG node reachable from `n` via one or more + * raw CFG edges (skipping non-canonical intermediaries). + */ +private CfgImpl::ControlFlowNode nextCanonicalFrom(CfgImpl::ControlFlowNode n) { + result = n.getASuccessor() and isCanonical(result) + or + exists(CfgImpl::ControlFlowNode mid | + mid = n.getASuccessor() and + not isCanonical(mid) and + result = nextCanonicalFrom(mid) + ) +} + +/** Gets the nearest canonical CFG successor of canonical node `n`. */ +private ControlFlowNode nextCanonical(ControlFlowNode n) { result = nextCanonicalFrom(n) } + +/** + * A basic block — a maximal-length sequence of control flow nodes such + * that no node except the first has a predecessor outside the sequence, + * and no node except the last has a successor outside the sequence. + */ +class BasicBlock extends CfgImpl::BasicBlock { + /** Gets the `n`th node in this basic block, restricted to canonical nodes. */ + ControlFlowNode getNode(int n) { + result = rank[n + 1](ControlFlowNode node, int i | super.getNode(i) = node | node order by i) + } + + /** Gets a node in this basic block. */ + ControlFlowNode getANode() { result = this.getNode(_) } + + /** Gets the first canonical node in this basic block. */ + ControlFlowNode firstNode() { result = this.getNode(0) } + + /** Gets the last canonical node in this basic block. */ + ControlFlowNode getLastNode() { result = this.getNode(max(int n | exists(this.getNode(n)))) } + + /** Holds if this basic block contains `node`. */ + predicate contains(ControlFlowNode node) { node = this.getANode() } + + // Inherited from the shared library's `BasicBlock`: + // getASuccessor(), getASuccessor(SuccessorType), getAPredecessor(), + // getNode(int) (raw, includes non-canonical), getANode() (raw), + // strictlyDominates(), dominates(), getImmediateDominator(), + // length(), inLoop(). + // We expose canonical-only positional access via `getNode(int)` below + // (shadows the shared-lib version) and additional Python-style helpers. + /** Gets a true successor to this basic block. */ + BasicBlock getATrueSuccessor() { + result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = true)) + } + + /** Gets a false successor to this basic block. */ + BasicBlock getAFalseSuccessor() { + result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = false)) + } + + /** Gets an unconditional successor to this basic block. */ + BasicBlock getAnUnconditionalSuccessor() { + result = super.getASuccessor() and + not result = this.getATrueSuccessor() and + not result = this.getAFalseSuccessor() + } + + /** Gets an exceptional successor to this basic block. */ + BasicBlock getAnExceptionalSuccessor() { result = super.getASuccessor(any(ExceptionSuccessor t)) } + + /** + * Holds if this basic block is in the dominance frontier of `df`. + * + * Note: implemented locally rather than via the shared lib, which + * doesn't currently expose a `dominanceFrontier` predicate at this + * level. + */ + predicate inDominanceFrontier(BasicBlock df) { + this = df.getAPredecessor() and not this = df.getImmediateDominator() + or + exists(BasicBlock prev | prev.inDominanceFrontier(df) | + this = prev.getImmediateDominator() and + not this = df.getImmediateDominator() + ) + } + + /** Holds if this basic block strictly reaches `other`. */ + predicate strictlyReaches(BasicBlock other) { super.getASuccessor+() = other } + + /** Holds if this basic block reaches `other` (reflexively). */ + predicate reaches(BasicBlock other) { this = other or this.strictlyReaches(other) } + + /** Holds if flow from this basic block reaches a normal exit from its scope. */ + predicate reachesExit() { + this.getANode() instanceof CfgImpl::ControlFlow::NormalExitNode + or + exists(BasicBlock succ | succ = super.getASuccessor() and succ.reachesExit()) + } + + /** Gets the scope of this basic block. */ + Py::Scope getScope() { exists(ControlFlowNode n | n = this.getANode() | result = n.getScope()) } + + /** Holds if flow from this BasicBlock always reaches `succ`. */ + predicate alwaysReaches(BasicBlock succ) { + succ = this + or + strictcount(BasicBlock s | s = super.getASuccessor()) = 1 and + succ = super.getASuccessor() + or + forex(BasicBlock immsucc | immsucc = super.getASuccessor() | immsucc.alwaysReaches(succ)) + } +} + +// =========================================================================== +// Re-exports for SSA / dominance consumers +// +// The shared `BB::CfgSig` requires `EntryBasicBlock` and `dominatingEdge` in +// addition to the BasicBlock class we already expose. They are provided by +// the shared CFG library on the `BB::Make` instantiation produced by +// `AstNodeImpl.qll`. +// =========================================================================== +/** An entry basic block, that is, a basic block whose first node is an entry node. */ +class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock; + +/** + * Holds if `bb1` has `bb2` as a direct successor and the edge between `bb1` + * and `bb2` is a dominating edge. + */ +predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2; + +// =========================================================================== +// AST-shape subclasses of ControlFlowNode +// +// Each class is a thin wrapper around the canonical CFG node for a given +// kind of Python AST node. Methods that take/return CFG nodes delegate to +// the AST and re-resolve back via `Expr.getAFlowNode()` from `Flow.qll` +// while we are in the migration period; once that is gone we will use a +// new-CFG-local resolution. For now, expressions navigated through these +// subclasses are looked up by AST identity, and the dominance constraint +// from the old CFG (`result.getBasicBlock().dominates(this.getBasicBlock())`) +// is preserved. +// =========================================================================== +/** Gets the canonical `ControlFlowNode` for AST expression `e`. */ +ControlFlowNode astExprToCfg(Py::Expr e) { result.getNode() = e } + +/** A control flow node corresponding to a `Name` or `PlaceHolder` expression. */ +class NameNode extends ControlFlowNode { + NameNode() { + toAst(this) instanceof Py::Name + or + toAst(this) instanceof Py::PlaceHolder + } + + /** Holds if this flow node defines the variable `v`. */ + predicate defines(Py::Variable v) { + exists(Py::Name n | n = toAst(this) and n.defines(v)) and + not this.isLoad() + } + + /** Holds if this flow node deletes the variable `v`. */ + predicate deletes(Py::Variable v) { exists(Py::Name n | n = toAst(this) and n.deletes(v)) } + + /** Holds if this flow node uses the variable `v`. */ + predicate uses(Py::Variable v) { + this.isLoad() and + exists(Py::Name u | u = toAst(this) and u.uses(v)) + or + exists(Py::PlaceHolder u | + u = toAst(this) and u.getVariable() = v and u.getCtx() instanceof Py::Load + ) + } + + /** Gets the identifier of this name node. */ + string getId() { + result = toAst(this).(Py::Name).getId() + or + result = toAst(this).(Py::PlaceHolder).getId() + } + + /** Holds if this is a use of a local variable. */ + predicate isLocal() { exists(Py::Variable v | this.uses(v) and v instanceof Py::LocalVariable) } + + /** Holds if this is a use of a non-local variable. */ + predicate isNonLocal() { + exists(Py::Variable v | this.uses(v) and v.getScope() != this.getScope()) + } + + /** Holds if this is a use of a global (including builtin) variable. */ + predicate isGlobal() { exists(Py::Variable v | this.uses(v) and v instanceof Py::GlobalVariable) } +} + +/** A control flow node corresponding to a named constant (`None`, `True`, `False`). */ +class NameConstantNode extends NameNode { + NameConstantNode() { toAst(this) instanceof Py::NameConstant } +} + +/** A control flow node corresponding to a call. */ +class CallNode extends ControlFlowNode { + CallNode() { toAst(this) instanceof Py::Call } + + /** Gets the underlying Python `Call`. */ + Py::Call getCall() { result = toAst(this) } + + /** Gets the flow node for the function component of this call. */ + ControlFlowNode getFunction() { + exists(Py::Call c | + c = toAst(this) and + c.getFunc() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the flow node for the `n`th positional argument. */ + ControlFlowNode getArg(int n) { + exists(Py::Call c | + c = toAst(this) and + c.getArg(n) = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the flow node for the named argument with name `name`. */ + ControlFlowNode getArgByName(string name) { + exists(Py::Call c, Py::Keyword k | + c = toAst(this) and + k = c.getANamedArg() and + k.getValue() = toAst(result) and + k.getArg() = name and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets a flow node corresponding to any argument. */ + ControlFlowNode getAnArg() { result = this.getArg(_) or result = this.getArgByName(_) } + + /** Gets the first tuple (`*args`) argument, if any. */ + ControlFlowNode getStarArg() { + exists(Py::Call c | + c = toAst(this) and + c.getStarArg() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets a dictionary (`**kwargs`) argument, if any. */ + ControlFlowNode getKwargs() { + exists(Py::Call c | + c = toAst(this) and + c.getKwargs() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + predicate isDecoratorCall() { this.isClassDecoratorCall() or this.isFunctionDecoratorCall() } + + predicate isClassDecoratorCall() { + exists(Py::ClassExpr cls | toAst(this) = cls.getADecoratorCall()) + } + + predicate isFunctionDecoratorCall() { + exists(Py::FunctionExpr func | toAst(this) = func.getADecoratorCall()) + } +} + +/** A control flow node corresponding to an attribute expression. */ +class AttrNode extends ControlFlowNode { + AttrNode() { toAst(this) instanceof Py::Attribute } + + /** Gets the flow node for the object of the attribute expression. */ + ControlFlowNode getObject() { + exists(Py::Attribute a | + a = toAst(this) and + a.getObject() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the flow node for the object of this attribute expression, with the matching name. */ + ControlFlowNode getObject(string name) { + exists(Py::Attribute a | + a = toAst(this) and + a.getObject() = toAst(result) and + a.getName() = name and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the attribute name. */ + string getName() { exists(Py::Attribute a | a = toAst(this) and a.getName() = result) } +} + +/** A control flow node corresponding to an import statement (`import x`). */ +class ImportExprNode extends ControlFlowNode { + ImportExprNode() { toAst(this) instanceof Py::ImportExpr } +} + +/** A control flow node corresponding to a `from ... import name` expression. */ +class ImportMemberNode extends ControlFlowNode { + ImportMemberNode() { toAst(this) instanceof Py::ImportMember } + + /** Gets the flow node for the module being imported from, with the matching name. */ + ControlFlowNode getModule(string name) { + exists(Py::ImportMember i | + i = toAst(this) and + i.getModule() = toAst(result) and + i.getName() = name and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a `from ... import *` statement. */ +class ImportStarNode extends ControlFlowNode { + ImportStarNode() { toAst(this) instanceof Py::ImportStar } + + /** Gets the flow node for the module being imported from. */ + ControlFlowNode getModule() { + exists(Py::ImportStar i | + i = toAst(this) and + i.getModuleExpr() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a subscript expression. */ +class SubscriptNode extends ControlFlowNode { + SubscriptNode() { toAst(this) instanceof Py::Subscript } + + /** Gets the flow node for the value being subscripted. */ + ControlFlowNode getObject() { + exists(Py::Subscript s | + s = toAst(this) and + s.getObject() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the flow node for the index expression. */ + ControlFlowNode getIndex() { + exists(Py::Subscript s | + s = toAst(this) and + s.getIndex() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a comparison operation. */ +class CompareNode extends ControlFlowNode { + CompareNode() { toAst(this) instanceof Py::Compare } + + /** Holds if `left` and `right` are a pair of operands for this comparison. */ + predicate operands(ControlFlowNode left, Py::Cmpop op, ControlFlowNode right) { + exists(Py::Compare c, Py::Expr eleft, Py::Expr eright | + c = toAst(this) and eleft = toAst(left) and eright = toAst(right) + | + eleft = c.getLeft() and eright = c.getComparator(0) and op = c.getOp(0) + or + exists(int i | + eleft = c.getComparator(i - 1) and eright = c.getComparator(i) and op = c.getOp(i) + ) + ) and + left.getBasicBlock().dominates(this.getBasicBlock()) and + right.getBasicBlock().dominates(this.getBasicBlock()) + } +} + +/** A control flow node corresponding to a conditional expression (`x if c else y`). */ +class IfExprNode extends ControlFlowNode { + IfExprNode() { toAst(this) instanceof Py::IfExp } + + /** Gets the flow node for one of the operands of an if-expression. */ + ControlFlowNode getAnOperand() { result = this.getAPredecessor() } +} + +/** A control flow node corresponding to an assignment expression (walrus `:=`). */ +class AssignmentExprNode extends ControlFlowNode { + AssignmentExprNode() { toAst(this) instanceof Py::AssignExpr } + + /** Gets the flow node for the left-hand side. */ + ControlFlowNode getTarget() { + exists(Py::AssignExpr a | + a = toAst(this) and + a.getTarget() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + /** Gets the flow node for the right-hand side. */ + ControlFlowNode getValue() { + exists(Py::AssignExpr a | + a = toAst(this) and + a.getValue() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a binary expression (`a + b` etc.). */ +class BinaryExprNode extends ControlFlowNode { + BinaryExprNode() { toAst(this) instanceof Py::BinaryExpr } + + ControlFlowNode getLeft() { + exists(Py::BinaryExpr be | + be = toAst(this) and + be.getLeft() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + ControlFlowNode getRight() { + exists(Py::BinaryExpr be | + be = toAst(this) and + be.getRight() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + Py::Operator getOp() { result = toAst(this).(Py::BinaryExpr).getOp() } +} + +/** A control flow node corresponding to a boolean expression (`a and b`, `a or b`). */ +class BoolExprNode extends ControlFlowNode { + BoolExprNode() { toAst(this) instanceof Py::BoolExpr } + + Py::Boolop getOp() { result = toAst(this).(Py::BoolExpr).getOp() } +} + +/** A control flow node corresponding to a unary expression (`-x`, `not x`, etc.). */ +class UnaryExprNode extends ControlFlowNode { + UnaryExprNode() { toAst(this) instanceof Py::UnaryExpr } + + ControlFlowNode getOperand() { + exists(Py::UnaryExpr u | + u = toAst(this) and + u.getOperand() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + + Py::Unaryop getOp() { result = toAst(this).(Py::UnaryExpr).getOp() } +} + +/** + * A control flow node that is a definition: it appears in a context that + * binds a variable (assignment target, parameter, etc.). + */ +class DefinitionNode extends ControlFlowNode { + DefinitionNode() { this.isStore() or this.isParameter() } + + /** Gets the value assigned, if any. */ + ControlFlowNode getValue() { + exists(Py::Expr target, Py::Expr value | + target = toAst(this) and + value = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + | + // x = value + exists(Py::Assign a | a.getATarget() = target and a.getValue() = value) + or + // x = y = value (nested chained-assign target) + exists(Py::Assign a | a.getATarget().(Py::Tuple).getElt(_) = target and a.getValue() = value) + ) + } +} + +/** A control flow node corresponding to a deletion (`del x`). */ +class DeletionNode extends ControlFlowNode { + DeletionNode() { this.isDelete() } +} + +/** A control flow node corresponding to a `for` loop target. */ +class ForNode extends ControlFlowNode { + ForNode() { exists(Py::For f | toAst(this) = f.getIter()) } + + /** Gets the iterable expression. */ + ControlFlowNode getIter() { + result = this and result = result // canonical "after" of the iterable + } + + /** Gets the target (loop variable) of the `for` loop. */ + ControlFlowNode getTarget() { + exists(Py::For f | + f.getIter() = toAst(this) and + f.getTarget() = toAst(result) + ) + } +} + +/** A control flow node corresponding to a `raise` statement. */ +class RaiseStmtNode extends ControlFlowNode { + RaiseStmtNode() { toAst(this) instanceof Py::Raise } + + /** Gets the exception expression, if any. */ + ControlFlowNode getException() { + exists(Py::Raise r | + r = toAst(this) and + r.getException() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a starred expression (`*x`). */ +class StarredNode extends ControlFlowNode { + StarredNode() { toAst(this) instanceof Py::Starred } + + /** Gets the value being starred. */ + ControlFlowNode getValue() { + exists(Py::Starred s | + s = toAst(this) and + s.getValue() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to an `except` clause's name binding. */ +class ExceptFlowNode extends ControlFlowNode { + ExceptFlowNode() { exists(Py::ExceptStmt e | toAst(this) = e.getName()) } + + /** Gets the type expression of this exception handler. */ + ControlFlowNode getType() { + exists(Py::ExceptStmt e | + e.getName() = toAst(this) and + e.getType() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to an `except*` clause's name binding. */ +class ExceptGroupFlowNode extends ControlFlowNode { + ExceptGroupFlowNode() { exists(Py::ExceptGroupStmt e | toAst(this) = e.getName()) } +} + +/** Abstract base class for sequence nodes (tuple, list). */ +abstract class SequenceNode extends ControlFlowNode { + /** Gets the `n`th element of this sequence. */ + abstract ControlFlowNode getElement(int n); + + /** Gets any element of this sequence. */ + ControlFlowNode getAnElement() { result = this.getElement(_) } +} + +/** A control flow node corresponding to a tuple literal. */ +class TupleNode extends SequenceNode { + TupleNode() { toAst(this) instanceof Py::Tuple } + + override ControlFlowNode getElement(int n) { + exists(Py::Tuple t | + t = toAst(this) and + t.getElt(n) = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a list literal. */ +class ListNode extends SequenceNode { + ListNode() { toAst(this) instanceof Py::List } + + override ControlFlowNode getElement(int n) { + exists(Py::List l | + l = toAst(this) and + l.getElt(n) = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a set literal. */ +class SetNode extends ControlFlowNode { + SetNode() { toAst(this) instanceof Py::Set } + + /** Gets the flow node for an element of the set. */ + ControlFlowNode getAnElement() { + exists(Py::Set s | + s = toAst(this) and + s.getAnElt() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to a dict literal. */ +class DictNode extends ControlFlowNode { + DictNode() { toAst(this) instanceof Py::Dict } + + /** Gets the flow node for a value of the dict. */ + ControlFlowNode getAValue() { + exists(Py::Dict d | + d = toAst(this) and + d.getAValue() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } +} + +/** A control flow node corresponding to an iterable in a `for` loop. */ +class IterableNode extends ControlFlowNode { + IterableNode() { + exists(Py::For f | toAst(this) = f.getIter()) + or + exists(Py::Comp c | toAst(this) = c.getIterable()) + } +} From 79db96c717d4f98aab293310e45743df237be460 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 11:03:45 +0000 Subject: [PATCH 56/72] Python: introduce shared-SSA adapter on the new CFG Adds 'python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll', a minimal Python SSA implementation built on the shared SSA library ('codeql.ssa.Ssa::Make'). The structure mirrors Java's adapter at 'java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll'. Key design choices: * 'SourceVariable' wraps 'Py::Variable'. Only variables that are read or deleted somewhere are tracked - write-only variables don't benefit from SSA construction. * Variable references are positional ('BasicBlock', 'int') pairs looked up via 'Cfg::NameNode.defines'/'.uses'/'.deletes' (which themselves are one-line bridges to AST-level 'Name.defines' etc.). * Parameter writes are not synthesised: parameter Name nodes are already wired into the CFG (per the earlier C#-style parameter extension in 'AstNodeImpl.qll'), so the regular 'variableWrite' path handles them at their natural CFG index. * Non-local / captured / global / builtin variables read in a scope but not written in it receive a synthetic entry definition at index '-1' of the scope's entry basic block. This matches Java's 'hasEntryDef'. * 'del x' is modelled as a certain write at the deletion site. Includes an inline-expectations test under 'python/ql/test/library-tests/dataflow-new-ssa/' covering: plain parameter pass-through, simple assignment + read, reassignment with dead-write pruning, if/else with phi insertion at the join, and an undefined-name read (currently a known limitation - no SSA flow without an enclosing definition). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/dataflow/new/internal/SsaImpl.qll | 179 ++++++++++++++++++ .../dataflow-new-ssa/SsaTest.expected | 0 .../library-tests/dataflow-new-ssa/SsaTest.ql | 59 ++++++ .../library-tests/dataflow-new-ssa/test.py | 40 ++++ 4 files changed, 278 insertions(+) create mode 100644 python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll create mode 100644 python/ql/test/library-tests/dataflow-new-ssa/SsaTest.expected create mode 100644 python/ql/test/library-tests/dataflow-new-ssa/SsaTest.ql create mode 100644 python/ql/test/library-tests/dataflow-new-ssa/test.py diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll new file mode 100644 index 000000000000..11e7b9f4d3d1 --- /dev/null +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -0,0 +1,179 @@ +/** + * Provides the Python SSA implementation built on the new (shared) CFG. + * + * Mirrors the Java SSA adapter at + * `java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll`: + * an `InputSig` is defined in terms of positional `(BasicBlock, int)` + * variable references, and the shared + * `codeql.ssa.Ssa::Make` module is then + * instantiated. + * + * `SourceVariable` is the AST-level `Py::Variable`. Variable references + * are looked up via the CFG facade's `NameNode.defines`/`uses`/`deletes` + * predicates, which themselves are one-line bridges to AST-level + * `Name.defines`/`uses`/`deletes`. + * + * Implicit-entry definitions are inserted for: + * - non-local / global / builtin variables that are read in the scope + * but never assigned (no enclosing CFG node defines them), + * - captured variables (variables defined in an enclosing scope that + * are read inside the scope), and + * - parameters, but only if the corresponding parameter name is *not* + * itself a CFG node. With the C#-style parameter wiring already + * installed in `AstNodeImpl.qll`, parameter names *are* CFG nodes, + * so the regular `variableWrite` path handles them — no `i = -1` + * entry is needed for ordinary parameters. + */ +overlay[local?] +module; + +private import python as Py +private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +private import semmle.python.controlflow.internal.Cfg as Cfg +private import codeql.ssa.Ssa as SsaImplCommon +private import codeql.controlflow.BasicBlock as BB + +/** + * Adapts the Python `Cfg` facade to the shared SSA library's `CfgSig`. + * All members are inherited from `Cfg::ControlFlowNode` and + * `Cfg::BasicBlock`. + */ +private module CfgForSsa implements BB::CfgSig { + class ControlFlowNode = CfgImpl::ControlFlowNode; + + class BasicBlock = CfgImpl::BasicBlock; + + class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock; + + predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2; +} + +/** + * A source variable for SSA. Wraps a Python `Variable` (the AST-level + * notion of a named binding within a scope) so that the shared SSA + * implementation can use it as a `SourceVariable`. + * + * We only track variables that are read at least once in their scope — + * tracking write-only variables is unnecessary work. + */ +private newtype TSsaSourceVariable = + TPyVar(Py::Variable v) { + // Has a use somewhere — read-relevant for SSA. + exists(Cfg::NameNode n | n.uses(v)) + or + // Or has a deletion (treated as a write that destroys the value). + exists(Cfg::NameNode n | n.deletes(v)) + } + +/** + * A source variable for SSA, wrapping a Python AST `Variable`. + */ +class SsaSourceVariable extends TSsaSourceVariable { + /** Gets the underlying Python AST variable. */ + Py::Variable getVariable() { this = TPyVar(result) } + + /** Gets a textual representation of this source variable. */ + string toString() { result = this.getVariable().toString() } + + /** Gets the location of this source variable. */ + Py::Location getLocation() { result = this.getVariable().getScope().getLocation() } +} + +/** + * Holds if `v` is a non-local read in scope `s`, in the sense that `s` + * uses `v` but does not write it within `s`. This includes globals, + * builtins, and variables captured from an enclosing function scope. + */ +private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) { + exists(Cfg::NameNode n | + n.uses(v) and + n.getScope() = s and + not exists(Cfg::NameNode def | def.defines(v) and def.getScope() = s) + ) +} + +/** + * Holds if `v` should have an implicit entry definition at the start of + * scope `s`. This covers: + * - non-local / global / builtin variables (defined outside `s`), and + * - captured variables (defined in an enclosing scope but read here). + * + * Parameters are *not* included: their bound `Name` is itself a CFG + * node (per the C#-style parameter wiring), so `variableWrite` fires at + * the parameter's natural CFG index. + */ +private predicate hasEntryDef(SsaSourceVariable v, Py::Scope s) { + nonLocalReadIn(v.getVariable(), s) +} + +/** + * Gets the entry basic block of scope `s`, where implicit entry + * definitions are placed (at synthetic index `-1`). + */ +private CfgImpl::BasicBlock entryBlock(Py::Scope s) { + exists(CfgImpl::ControlFlowNode entry | + entry instanceof CfgImpl::ControlFlow::EntryNode and + entry.getEnclosingCallable().asScope() = s and + result = entry.getBasicBlock() + ) +} + +/** + * The SSA `InputSig` for Python. References are positional + * `(BasicBlock, int)` pairs into the new CFG. + */ +private module SsaImplInput implements SsaImplCommon::InputSig { + class SourceVariable = SsaSourceVariable; + + predicate variableWrite(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) { + // Explicit binding at a CFG node — includes assignments, + // parameter Names (wired in via the C# pattern), exception-handler + // `as`-bindings, import aliases, and match-pattern captures. + exists(Cfg::NameNode n | + bb.getNode(i) = n and + n.defines(v.getVariable()) and + certain = true + ) + or + // `del x` — removes the binding. Modelled as a certain write that + // makes any subsequent read invalid. + exists(Cfg::NameNode n | + bb.getNode(i) = n and + n.deletes(v.getVariable()) and + certain = true + ) + or + // Implicit entry definition for non-local / captured / global / + // builtin variables read in the scope. + bb = entryBlock(v.getVariable().getScope()) and + hasEntryDef(v, v.getVariable().getScope()) and + i = -1 and + certain = true + } + + predicate variableRead(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) { + exists(Cfg::NameNode n | + bb.getNode(i) = n and + n.uses(v.getVariable()) and + certain = true + ) + } +} + +/** + * The shared SSA instantiation for Python. + * + * Members: + * - `Definition` — the union of explicit, uncertain, and phi definitions + * - `WriteDefinition`, `UncertainWriteDefinition`, `PhiNode` + * - the standard SSA predicates (`getAUse`, `getAnUltimateDefinition`, ...). + */ +module Ssa = SsaImplCommon::Make; + +final class Definition = Ssa::Definition; + +final class WriteDefinition = Ssa::WriteDefinition; + +final class UncertainWriteDefinition = Ssa::UncertainWriteDefinition; + +final class PhiNode = Ssa::PhiNode; diff --git a/python/ql/test/library-tests/dataflow-new-ssa/SsaTest.expected b/python/ql/test/library-tests/dataflow-new-ssa/SsaTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/dataflow-new-ssa/SsaTest.ql b/python/ql/test/library-tests/dataflow-new-ssa/SsaTest.ql new file mode 100644 index 000000000000..0bebf4a637d0 --- /dev/null +++ b/python/ql/test/library-tests/dataflow-new-ssa/SsaTest.ql @@ -0,0 +1,59 @@ +/** + * Inline-expectations test for the new-CFG SSA adapter + * (`semmle.python.dataflow.new.internal.SsaImpl`). + * + * Tags: + * - `def=`: there is an SSA write definition of `` at this + * line (parameter init, plain assignment, augmented assignment, + * exception-handler binding, deletion, etc.). + * - `use=`: `` is used at this line, and some SSA definition + * of `` reaches the read. + * - `phi=`: there is an SSA phi definition of `` whose BB + * starts on this line. + */ + +import python +import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl +import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +import semmle.python.controlflow.internal.Cfg as Cfg +import utils.test.InlineExpectationsTest + +module SsaTest implements TestSig { + string getARelevantTag() { result = ["def", "use", "phi"] } + + predicate hasActualResult(Location location, string element, string tag, string value) { + // A `def=` fires when an SSA WriteDefinition is at a CFG node + // on the given line. + exists(SsaImpl::Ssa::WriteDefinition def, CfgImpl::BasicBlock bb, int i, Cfg::NameNode n | + def.definesAt(_, bb, i) and + bb.getNode(i) = n and + tag = "def" and + location = n.getLocation() and + element = n.toString() and + value = n.getId() + ) + or + // A `use=` fires when an SSA Definition reaches a read at this + // CFG node. + exists(SsaImpl::Ssa::Definition def, CfgImpl::BasicBlock bb, int i, Cfg::NameNode n | + SsaImpl::Ssa::ssaDefReachesRead(_, def, bb, i) and + bb.getNode(i) = n and + tag = "use" and + location = n.getLocation() and + element = n.toString() and + value = n.getId() + ) + or + // A `phi=` fires when there is a phi node whose BB's first + // CFG node is on the given line. + exists(SsaImpl::Ssa::PhiNode phi, CfgImpl::BasicBlock bb | + phi.definesAt(_, bb, _) and + tag = "phi" and + location = bb.getNode(0).getLocation() and + element = bb.toString() and + value = phi.getSourceVariable().(SsaImpl::SsaSourceVariable).getVariable().getId() + ) + } +} + +import MakeTest diff --git a/python/ql/test/library-tests/dataflow-new-ssa/test.py b/python/ql/test/library-tests/dataflow-new-ssa/test.py new file mode 100644 index 000000000000..fb1f658c4fbf --- /dev/null +++ b/python/ql/test/library-tests/dataflow-new-ssa/test.py @@ -0,0 +1,40 @@ +# Basic SSA tests for the new-CFG SSA adapter. +# +# The shared SSA implementation prunes its construction by liveness: +# definitions of variables that are not read are never materialised. +# This is by design — write-only variables would only bloat the SSA +# graph. Tests therefore must always include a read of each variable +# being verified. +# +# Annotations: +# def=: there is an SSA write definition of at this line +# use=: is used here and the read resolves to some def + + +def basic_param(x): # $ def=x + return x # $ use=x + + +def basic_assign(): + y = 1 # $ def=y + return y # $ use=y + + +def reassignment(): + x = 1 + x = 2 # $ def=x + return x # $ use=x + + +def if_else_phi(cond): # $ def=cond + if cond: # $ use=cond phi=x + x = 1 # $ def=x + else: + x = 2 # $ def=x + return x # $ use=x + + +def use_global(): + return some_undefined # known limitation: undefined globals not resolved here + + From f5bf8ae8dd86b7c8e57d8241d7e537239bdf3fd1 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 12:29:37 +0000 Subject: [PATCH 57/72] Python: fix augstore for the new CFG and add store/load test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../python/controlflow/internal/Cfg.qll | 21 ++++--- .../store-load/StoreLoadTest.expected | 0 .../ControlFlow/store-load/StoreLoadTest.ql | 45 +++++++++++++++ .../ControlFlow/store-load/test.py | 56 +++++++++++++++++++ 4 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.expected create mode 100644 python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql create mode 100644 python/ql/test/library-tests/ControlFlow/store-load/test.py diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll index 3a19049c520d..fab4db883b8a 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -218,15 +218,22 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode { } /** - * Holds if `n` is an augmented assignment load and `store` is the - * corresponding store node. + * Holds if `load` is the load half of an augmented-assignment target, + * and `store` is the corresponding store half. + * + * In the legacy CFG (`Flow.qll`) the same Python `Name` had two + * distinct CFG nodes — a load node (context 3) earlier in the BB, and + * a store node (context 5) later. The legacy `augstore` related the + * pair via dominance. + * + * In the new (shared) CFG, the canonical node for an AST expression is + * unique, so `load` and `store` collapse onto the same CFG node. The + * predicate is therefore reflexive on the augmented-assignment + * target's canonical node. */ private predicate augstore(ControlFlowNode load, ControlFlowNode store) { - exists(Py::Expr load_store | exists(Py::AugAssign aa | aa.getTarget() = load_store) | - toAst(load) = load_store and - toAst(store) = load_store and - load.strictlyDominates(store) - ) + exists(Py::AugAssign aa | aa.getTarget() = toAst(load)) and + load = store } /** diff --git a/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.expected b/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql b/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql new file mode 100644 index 000000000000..8d3a34629206 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql @@ -0,0 +1,45 @@ +/** + * Inline-expectations test for the store/load/delete/parameter + * classification predicates on the new-CFG facade. + * + * Each tag fires when the corresponding predicate (`isLoad`, + * `isStore`, `isDelete`, `isParameter`, `isAugLoad`, `isAugStore`) + * holds on the canonical CFG node wrapping a `Py::Name` with the + * given identifier. + * + * For subscript / attribute stores the tag fires on the Subscript / + * Attribute node itself, with `value` set to the rightmost identifier + * (the attribute name for `Attribute`, the index expression's textual + * form for `Subscript`). + */ + +import python +import semmle.python.controlflow.internal.Cfg as Cfg +import utils.test.InlineExpectationsTest + +module StoreLoadTest implements TestSig { + string getARelevantTag() { result = ["load", "store", "delete", "param", "augload", "augstore"] } + + predicate hasActualResult(Location location, string element, string tag, string value) { + exists(Cfg::NameNode n | + location = n.getLocation() and + element = n.toString() and + value = n.getId() and + ( + n.isLoad() and tag = "load" + or + n.isStore() and not n.isAugStore() and tag = "store" + or + n.isDelete() and tag = "delete" + or + n.isParameter() and tag = "param" + or + n.isAugLoad() and tag = "augload" + or + n.isAugStore() and tag = "augstore" + ) + ) + } +} + +import MakeTest diff --git a/python/ql/test/library-tests/ControlFlow/store-load/test.py b/python/ql/test/library-tests/ControlFlow/store-load/test.py new file mode 100644 index 000000000000..dfca45a0b47b --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/store-load/test.py @@ -0,0 +1,56 @@ +# Store/load/delete/parameter classification on the new-CFG facade. +# +# Each annotated location carries the (sorted, deduplicated) set of +# kinds the CFG facade reports there. Comparing against the legacy +# 'semmle.python.Flow' classification is done by the comparison query +# 'StoreLoadParity.ql' — annotations here are only the positive +# assertions for the new facade. +# +# Tags: +# load= -- isLoad() fires on the Name +# store= -- isStore() fires +# delete= -- isDelete() fires +# param= -- isParameter() fires +# augload= -- isAugLoad() fires (the LHS of x += ... when read) +# augstore= -- isAugStore() fires (the LHS of x += ... when written) + + +# --- plain load / store / delete --- + +x = 1 # $ store=x +y = x + 1 # $ store=y load=x +print(y) # $ load=print load=y +del x # $ delete=x + + +# --- function definitions (parameters) --- + +def f(a, b=2, *args, c, **kwargs): # $ store=f param=a param=b param=args param=c param=kwargs + return a + b + c # $ load=a load=b load=c + + +# --- augmented assignment splits one Name into load + store halves --- + +def aug(): # $ store=aug + n = 0 # $ store=n + n += 1 # $ augload=n augstore=n + return n # $ load=n + + +# --- subscript / attribute stores --- + +class C: # $ store=C + pass + + +def stores(obj, container, idx): # $ store=stores param=obj param=container param=idx + obj.attr = 1 # $ load=obj + container[idx] = 2 # $ load=container load=idx + return obj # $ load=obj + + +# --- tuple unpacking --- + +def unpack(pair): # $ store=unpack param=pair + a, b = pair # $ store=a store=b load=pair + return a + b # $ load=a load=b From 96de5cf1888f817bc14596b793da3cc06d0c5d0d Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 18:24:38 +0000 Subject: [PATCH 58/72] Python: bring Cfg.qll's facade to API parity with Flow.qll Adds the methods and type-narrowing overrides needed for Cfg.qll to be a drop-in replacement for Flow.qll's CFG API surface: * 'override getNode()' type narrowing on all AST-shape subclasses (CallNode -> Py::Call, AttrNode -> Py::Attribute, ImportExprNode -> Py::ImportExpr, etc.). This lets callers chain methods like 'iexpr.getNode().isRelative()' that previously failed because 'getNode()' returned the generic AstNode. * 'ControlFlowNode.isBranch()' -- true and/or false successor exists. * 'ControlFlowNode.getAChild()' -- CFG-level child traversal via the AST's getAChildNode, with dominance constraint. * 'ControlFlowNode.strictlyReaches(other)' -- node-level reachability. * 'NameNode.isSelf()' -- AST-level approximation: uses the 'Variable' that is the first parameter of an enclosing method. * 'BinaryExprNode.operands(left, op, right)' + 'getAnOperand()'. * 'BoolExprNode.getAnOperand()'. * 'ForNode.getSequence()' (alias for 'getIter') and 'ForNode.iterates(target, sequence)'. * 'ForNode' / 'RaiseStmtNode' type-narrowing overrides. * 'ExceptFlowNode.getName()' / 'ExceptGroupFlowNode.getName()' -- the bound 'as'-name CFG node. * 'DictNode.getAKey()' (only 'getAValue' was present). These additions are independent of the dataflow-migration approach (option 4 vs option 5). They close the API-parity gap identified during the Option-5 investigation; with them in place, hundreds of type-resolution errors that previously appeared when swapping Cfg for Flow at the python.qll level go away. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/controlflow/internal/Cfg.qll | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll index fab4db883b8a..7b12c50946e8 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -213,6 +213,31 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode { /** Holds if this flow node corresponds to a class definition expression. */ predicate isClass() { toAst(this) instanceof Py::ClassExpr } + /** + * Holds if this flow node is a branch (i.e. has both a true and a + * false successor). + */ + predicate isBranch() { exists(this.getATrueSuccessor()) or exists(this.getAFalseSuccessor()) } + + /** + * Gets a CFG child of this node, defined as a CFG node whose AST node + * is a child of this CFG node's AST node, restricted to nodes that + * dominate this one (so the child has been evaluated by the time we + * reach this node). + * + * Mirrors `Flow.qll`'s `getAChild`. UnaryExprNode is excluded because + * its operand is its CFG predecessor (handled separately). + */ + pragma[nomagic] + ControlFlowNode getAChild() { + toAst(this).(Py::Expr).getAChildNode() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) and + not this instanceof UnaryExprNode + } + + /** Holds if this flow node strictly reaches `other`. */ + predicate strictlyReaches(ControlFlowNode other) { this.getASuccessor+() = other } + /** Internal: raw successor predicate that does NOT skip non-canonical nodes. */ CfgImpl::ControlFlowNode getASuccessorRaw() { result = super.getASuccessor() } } @@ -422,6 +447,23 @@ class NameNode extends ControlFlowNode { /** Holds if this is a use of a global (including builtin) variable. */ predicate isGlobal() { exists(Py::Variable v | this.uses(v) and v instanceof Py::GlobalVariable) } + + /** + * Holds if this is a use of `self` — the first parameter of an + * enclosing method. + * + * AST-level approximation: matches when the Name uses a `Variable` + * that is the first parameter of an enclosing `Function` defined + * inside a `Class`. + */ + predicate isSelf() { + exists(Py::Variable v, Py::Function f, Py::Class c | + this.uses(v) and + f = c.getAMethod() and + v.getScope() = f and + v = f.getArg(0).(Py::Name).getVariable() + ) + } } /** A control flow node corresponding to a named constant (`None`, `True`, `False`). */ @@ -433,6 +475,8 @@ class NameConstantNode extends NameNode { class CallNode extends ControlFlowNode { CallNode() { toAst(this) instanceof Py::Call } + override Py::Call getNode() { result = super.getNode() } + /** Gets the underlying Python `Call`. */ Py::Call getCall() { result = toAst(this) } @@ -501,6 +545,8 @@ class CallNode extends ControlFlowNode { class AttrNode extends ControlFlowNode { AttrNode() { toAst(this) instanceof Py::Attribute } + override Py::Attribute getNode() { result = super.getNode() } + /** Gets the flow node for the object of the attribute expression. */ ControlFlowNode getObject() { exists(Py::Attribute a | @@ -527,12 +573,16 @@ class AttrNode extends ControlFlowNode { /** A control flow node corresponding to an import statement (`import x`). */ class ImportExprNode extends ControlFlowNode { ImportExprNode() { toAst(this) instanceof Py::ImportExpr } + + override Py::ImportExpr getNode() { result = super.getNode() } } /** A control flow node corresponding to a `from ... import name` expression. */ class ImportMemberNode extends ControlFlowNode { ImportMemberNode() { toAst(this) instanceof Py::ImportMember } + override Py::ImportMember getNode() { result = super.getNode() } + /** Gets the flow node for the module being imported from, with the matching name. */ ControlFlowNode getModule(string name) { exists(Py::ImportMember i | @@ -548,6 +598,8 @@ class ImportMemberNode extends ControlFlowNode { class ImportStarNode extends ControlFlowNode { ImportStarNode() { toAst(this) instanceof Py::ImportStar } + override Py::ImportStar getNode() { result = super.getNode() } + /** Gets the flow node for the module being imported from. */ ControlFlowNode getModule() { exists(Py::ImportStar i | @@ -562,6 +614,8 @@ class ImportStarNode extends ControlFlowNode { class SubscriptNode extends ControlFlowNode { SubscriptNode() { toAst(this) instanceof Py::Subscript } + override Py::Subscript getNode() { result = super.getNode() } + /** Gets the flow node for the value being subscripted. */ ControlFlowNode getObject() { exists(Py::Subscript s | @@ -585,6 +639,8 @@ class SubscriptNode extends ControlFlowNode { class CompareNode extends ControlFlowNode { CompareNode() { toAst(this) instanceof Py::Compare } + override Py::Compare getNode() { result = super.getNode() } + /** Holds if `left` and `right` are a pair of operands for this comparison. */ predicate operands(ControlFlowNode left, Py::Cmpop op, ControlFlowNode right) { exists(Py::Compare c, Py::Expr eleft, Py::Expr eright | @@ -605,6 +661,8 @@ class CompareNode extends ControlFlowNode { class IfExprNode extends ControlFlowNode { IfExprNode() { toAst(this) instanceof Py::IfExp } + override Py::IfExp getNode() { result = super.getNode() } + /** Gets the flow node for one of the operands of an if-expression. */ ControlFlowNode getAnOperand() { result = this.getAPredecessor() } } @@ -613,6 +671,8 @@ class IfExprNode extends ControlFlowNode { class AssignmentExprNode extends ControlFlowNode { AssignmentExprNode() { toAst(this) instanceof Py::AssignExpr } + override Py::AssignExpr getNode() { result = super.getNode() } + /** Gets the flow node for the left-hand side. */ ControlFlowNode getTarget() { exists(Py::AssignExpr a | @@ -636,6 +696,8 @@ class AssignmentExprNode extends ControlFlowNode { class BinaryExprNode extends ControlFlowNode { BinaryExprNode() { toAst(this) instanceof Py::BinaryExpr } + override Py::BinaryExpr getNode() { result = super.getNode() } + ControlFlowNode getLeft() { exists(Py::BinaryExpr be | be = toAst(this) and @@ -653,19 +715,40 @@ class BinaryExprNode extends ControlFlowNode { } Py::Operator getOp() { result = toAst(this).(Py::BinaryExpr).getOp() } + + /** Holds if `left` and `right` are the operands and `op` is the operator. */ + predicate operands(ControlFlowNode left, Py::Operator op, ControlFlowNode right) { + left = this.getLeft() and right = this.getRight() and op = this.getOp() + } + + /** Gets either operand. */ + ControlFlowNode getAnOperand() { result = this.getLeft() or result = this.getRight() } } /** A control flow node corresponding to a boolean expression (`a and b`, `a or b`). */ class BoolExprNode extends ControlFlowNode { BoolExprNode() { toAst(this) instanceof Py::BoolExpr } + override Py::BoolExpr getNode() { result = super.getNode() } + Py::Boolop getOp() { result = toAst(this).(Py::BoolExpr).getOp() } + + /** Gets any operand of this boolean expression. */ + ControlFlowNode getAnOperand() { + exists(Py::BoolExpr be | + be = toAst(this) and + be.getAValue() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } } /** A control flow node corresponding to a unary expression (`-x`, `not x`, etc.). */ class UnaryExprNode extends ControlFlowNode { UnaryExprNode() { toAst(this) instanceof Py::UnaryExpr } + override Py::UnaryExpr getNode() { result = super.getNode() } + ControlFlowNode getOperand() { exists(Py::UnaryExpr u | u = toAst(this) and @@ -709,11 +792,16 @@ class DeletionNode extends ControlFlowNode { class ForNode extends ControlFlowNode { ForNode() { exists(Py::For f | toAst(this) = f.getIter()) } + override Py::For getNode() { exists(Py::For f | toAst(this) = f.getIter() | result = f) } + /** Gets the iterable expression. */ ControlFlowNode getIter() { result = this and result = result // canonical "after" of the iterable } + /** Gets the sequence expression (alias for `getIter()`, matches legacy Flow naming). */ + ControlFlowNode getSequence() { result = this.getIter() } + /** Gets the target (loop variable) of the `for` loop. */ ControlFlowNode getTarget() { exists(Py::For f | @@ -721,12 +809,19 @@ class ForNode extends ControlFlowNode { f.getTarget() = toAst(result) ) } + + /** Holds if `target` is the loop variable and `sequence` is the iterable. */ + predicate iterates(ControlFlowNode target, ControlFlowNode sequence) { + target = this.getTarget() and sequence = this.getSequence() + } } /** A control flow node corresponding to a `raise` statement. */ class RaiseStmtNode extends ControlFlowNode { RaiseStmtNode() { toAst(this) instanceof Py::Raise } + override Py::Raise getNode() { result = super.getNode() } + /** Gets the exception expression, if any. */ ControlFlowNode getException() { exists(Py::Raise r | @@ -755,6 +850,9 @@ class StarredNode extends ControlFlowNode { class ExceptFlowNode extends ControlFlowNode { ExceptFlowNode() { exists(Py::ExceptStmt e | toAst(this) = e.getName()) } + /** Gets the CFG node for the bound `as`-name itself. */ + ControlFlowNode getName() { result = this } + /** Gets the type expression of this exception handler. */ ControlFlowNode getType() { exists(Py::ExceptStmt e | @@ -768,6 +866,9 @@ class ExceptFlowNode extends ControlFlowNode { /** A control flow node corresponding to an `except*` clause's name binding. */ class ExceptGroupFlowNode extends ControlFlowNode { ExceptGroupFlowNode() { exists(Py::ExceptGroupStmt e | toAst(this) = e.getName()) } + + /** Gets the CFG node for the bound `as`-name itself. */ + ControlFlowNode getName() { result = this } } /** Abstract base class for sequence nodes (tuple, list). */ @@ -823,6 +924,15 @@ class SetNode extends ControlFlowNode { class DictNode extends ControlFlowNode { DictNode() { toAst(this) instanceof Py::Dict } + /** Gets the flow node for a key of the dict. */ + ControlFlowNode getAKey() { + exists(Py::Dict d | + d = toAst(this) and + d.getAKey() = toAst(result) and + result.getBasicBlock().dominates(this.getBasicBlock()) + ) + } + /** Gets the flow node for a value of the dict. */ ControlFlowNode getAValue() { exists(Py::Dict d | From 8790f63888065ac7d8c1163c0e6f006a4f164d26 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 19:19:20 +0000 Subject: [PATCH 59/72] Python: qualify Flow.qll's AST references with Py:: prefix Prepares Flow.qll for co-existence with the new CFG facade by switching 'import python' to 'import python as Py' and qualifying every AST-class reference inside Flow.qll's body. Flow.qll's own CFG types (ControlFlowNode, BasicBlock, CallNode, NameNode, etc.) keep their unqualified names. This change is a no-op semantically: * all 24 evaluation-order tests still pass, * the bindings + store-load + new-CFG-SSA library tests still pass, * compilation produces zero errors. The change enables a follow-up commit to swap python.qll's 'import semmle.python.Flow' for 'import semmle.python.controlflow.internal.Cfg' without triggering name-clash errors inside Flow.qll itself. Legacy modules that still want the legacy CFG (essa/, GuardedControlFlow, LegacyPointsTo, objects/, pointsto/, types/, dataflow/old/) will need a similar treatment in subsequent commits. The qualification was applied mechanically via a script that prefixed every reference to a known AST class. The list includes the standard AST node types from semmle.python.{Files, Variables, Stmts, Exprs, Class, Function, Patterns, Comprehensions} plus 'Location' / 'File' / 'Folder' / 'Container' / 'ConditionBlock' / 'Delete' / 'Load'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/ql/lib/semmle/python/Flow.qll | 364 +++++++++++++-------------- 1 file changed, 182 insertions(+), 182 deletions(-) diff --git a/python/ql/lib/semmle/python/Flow.qll b/python/ql/lib/semmle/python/Flow.qll index 94caf513aa98..05a9ab6e17d3 100644 --- a/python/ql/lib/semmle/python/Flow.qll +++ b/python/ql/lib/semmle/python/Flow.qll @@ -1,7 +1,7 @@ overlay[local] module; -import python +import python as Py private import semmle.python.internal.CachedStages private import codeql.controlflow.BasicBlock as BB @@ -17,7 +17,7 @@ private import codeql.controlflow.BasicBlock as BB */ private predicate augstore(ControlFlowNode load, ControlFlowNode store) { - exists(Expr load_store | exists(AugAssign aa | aa.getTarget() = load_store) | + exists(Py::Expr load_store | exists(Py::AugAssign aa | aa.getTarget() = load_store) | toAst(load) = load_store and toAst(store) = load_store and load.strictlyDominates(store) @@ -25,7 +25,7 @@ private predicate augstore(ControlFlowNode load, ControlFlowNode store) { } /** A non-dispatched getNode() to avoid negative recursion issues */ -private AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) } +private Py::AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) } /** * A control flow node. Control flow nodes have a many-to-one relation with syntactic nodes, @@ -35,19 +35,19 @@ private AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) } class ControlFlowNode extends @py_flow_node { /** Whether this control flow node is a load (including those in augmented assignments) */ predicate isLoad() { - exists(Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this)) + exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this)) } /** Whether this control flow node is a store (including those in augmented assignments) */ predicate isStore() { - exists(Expr e | e = toAst(this) | py_expr_contexts(_, 5, e) or augstore(_, this)) + exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 5, e) or augstore(_, this)) } /** Whether this control flow node is a delete */ - predicate isDelete() { exists(Expr e | e = toAst(this) | py_expr_contexts(_, 2, e)) } + predicate isDelete() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 2, e)) } /** Whether this control flow node is a parameter */ - predicate isParameter() { exists(Expr e | e = toAst(this) | py_expr_contexts(_, 4, e)) } + predicate isParameter() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 4, e)) } /** Whether this control flow node is a store in an augmented assignment */ predicate isAugStore() { augstore(_, this) } @@ -57,61 +57,61 @@ class ControlFlowNode extends @py_flow_node { /** Whether this flow node corresponds to a literal */ predicate isLiteral() { - toAst(this) instanceof Bytes + toAst(this) instanceof Py::Bytes or - toAst(this) instanceof Dict + toAst(this) instanceof Py::Dict or - toAst(this) instanceof DictComp + toAst(this) instanceof Py::DictComp or - toAst(this) instanceof Set + toAst(this) instanceof Py::Set or - toAst(this) instanceof SetComp + toAst(this) instanceof Py::SetComp or - toAst(this) instanceof Ellipsis + toAst(this) instanceof Py::Ellipsis or - toAst(this) instanceof GeneratorExp + toAst(this) instanceof Py::GeneratorExp or - toAst(this) instanceof Lambda + toAst(this) instanceof Py::Lambda or - toAst(this) instanceof ListComp + toAst(this) instanceof Py::ListComp or - toAst(this) instanceof List + toAst(this) instanceof Py::List or - toAst(this) instanceof Num + toAst(this) instanceof Py::Num or - toAst(this) instanceof Tuple + toAst(this) instanceof Py::Tuple or - toAst(this) instanceof Unicode + toAst(this) instanceof Py::Unicode or - toAst(this) instanceof NameConstant + toAst(this) instanceof Py::NameConstant } /** Whether this flow node corresponds to an attribute expression */ - predicate isAttribute() { toAst(this) instanceof Attribute } + predicate isAttribute() { toAst(this) instanceof Py::Attribute } /** Whether this flow node corresponds to an subscript expression */ - predicate isSubscript() { toAst(this) instanceof Subscript } + predicate isSubscript() { toAst(this) instanceof Py::Subscript } /** Whether this flow node corresponds to an import member */ - predicate isImportMember() { toAst(this) instanceof ImportMember } + predicate isImportMember() { toAst(this) instanceof Py::ImportMember } /** Whether this flow node corresponds to a call */ - predicate isCall() { toAst(this) instanceof Call } + predicate isCall() { toAst(this) instanceof Py::Call } /** Whether this flow node is the first in a module */ - predicate isModuleEntry() { this.isEntryNode() and toAst(this) instanceof Module } + predicate isModuleEntry() { this.isEntryNode() and toAst(this) instanceof Py::Module } /** Whether this flow node corresponds to an import */ - predicate isImport() { toAst(this) instanceof ImportExpr } + predicate isImport() { toAst(this) instanceof Py::ImportExpr } /** Whether this flow node corresponds to a conditional expression */ - predicate isIfExp() { toAst(this) instanceof IfExp } + predicate isIfExp() { toAst(this) instanceof Py::IfExp } /** Whether this flow node corresponds to a function definition expression */ - predicate isFunction() { toAst(this) instanceof FunctionExpr } + predicate isFunction() { toAst(this) instanceof Py::FunctionExpr } /** Whether this flow node corresponds to a class definition expression */ - predicate isClass() { toAst(this) instanceof ClassExpr } + predicate isClass() { toAst(this) instanceof Py::ClassExpr } /** Gets a predecessor of this flow node */ ControlFlowNode getAPredecessor() { this = result.getASuccessor() } @@ -123,25 +123,25 @@ class ControlFlowNode extends @py_flow_node { ControlFlowNode getImmediateDominator() { py_idoms(this, result) } /** Gets the syntactic element corresponding to this flow node */ - AstNode getNode() { py_flow_bb_node(this, result, _, _) } + Py::AstNode getNode() { py_flow_bb_node(this, result, _, _) } /** Gets a textual representation of this element. */ cached string toString() { Stages::AST::ref() and // Since modules can have ambigous names, entry nodes can too, if we do not collate them. - exists(Scope s | s.getEntryNode() = this | + exists(Py::Scope s | s.getEntryNode() = this | result = "Entry node for " + concat( | | s.toString(), ",") ) or - exists(Scope s | s.getANormalExit() = this | result = "Exit node for " + s.toString()) + exists(Py::Scope s | s.getANormalExit() = this | result = "Exit node for " + s.toString()) or - not exists(Scope s | s.getEntryNode() = this or s.getANormalExit() = this) and + not exists(Py::Scope s | s.getEntryNode() = this or s.getANormalExit() = this) and result = "ControlFlowNode for " + this.getNode().toString() } /** Gets the location of this ControlFlowNode */ - Location getLocation() { result = this.getNode().getLocation() } + Py::Location getLocation() { result = this.getNode().getLocation() } /** Whether this flow node is the first in its scope */ predicate isEntryNode() { py_scope_flow(this, _, -1) } @@ -151,9 +151,9 @@ class ControlFlowNode extends @py_flow_node { /** Gets the scope containing this flow node */ cached - Scope getScope() { + Py::Scope getScope() { Stages::AST::ref() and - if this.getNode() instanceof Scope + if this.getNode() instanceof Py::Scope then /* Entry or exit node */ result = this.getNode() @@ -161,7 +161,7 @@ class ControlFlowNode extends @py_flow_node { } /** Gets the enclosing module */ - Module getEnclosingModule() { result = this.getScope().getEnclosingModule() } + Py::Module getEnclosingModule() { result = this.getScope().getEnclosingModule() } /** Gets a successor for this node if the relevant condition is True. */ ControlFlowNode getATrueSuccessor() { @@ -188,7 +188,7 @@ class ControlFlowNode extends @py_flow_node { } /** Whether the scope may be exited as a result of this node raising an exception */ - predicate isExceptionalExit(Scope s) { py_scope_flow(this, s, 1) } + predicate isExceptionalExit(Py::Scope s) { py_scope_flow(this, s, 1) } /** Whether this node is a normal (non-exceptional) exit */ predicate isNormalExit() { py_scope_flow(this, _, 0) or py_scope_flow(this, _, 2) } @@ -198,7 +198,7 @@ class ControlFlowNode extends @py_flow_node { pragma[inline] predicate strictlyDominates(ControlFlowNode other) { // This predicate is gigantic, so it must be inlined. - // About 1.4 billion tuples for OpenStack Cinder. + // About 1.4 billion tuples for OpenStack Py::Cinder. this.getBasicBlock().strictlyDominates(other.getBasicBlock()) or exists(BasicBlock b, int i, int j | this = b.getNode(i) and other = b.getNode(j) and i < j) @@ -236,7 +236,7 @@ class ControlFlowNode extends @py_flow_node { /* join-ordering helper for `getAChild() */ pragma[noinline] private ControlFlowNode getExprChild(BasicBlock dom) { - this.getNode().(Expr).getAChildNode() = result.getNode() and + this.getNode().(Py::Expr).getAChildNode() = result.getNode() and result.getBasicBlock().dominates(dom) and not this instanceof UnaryExprNode } @@ -249,16 +249,16 @@ class ControlFlowNode extends @py_flow_node { */ private class AnyNode extends ControlFlowNode { - override AstNode getNode() { result = super.getNode() } + override Py::AstNode getNode() { result = super.getNode() } } /** A control flow node corresponding to a call expression, such as `func(...)` */ class CallNode extends ControlFlowNode { - CallNode() { toAst(this) instanceof Call } + CallNode() { toAst(this) instanceof Py::Call } /** Gets the flow node corresponding to the function expression for the call corresponding to this flow node */ ControlFlowNode getFunction() { - exists(Call c | + exists(Py::Call c | this.getNode() = c and c.getFunc() = result.getNode() and result.getBasicBlock().dominates(this.getBasicBlock()) @@ -267,7 +267,7 @@ class CallNode extends ControlFlowNode { /** Gets the flow node corresponding to the n'th positional argument of the call corresponding to this flow node */ ControlFlowNode getArg(int n) { - exists(Call c | + exists(Py::Call c | this.getNode() = c and c.getArg(n) = result.getNode() and result.getBasicBlock().dominates(this.getBasicBlock()) @@ -276,7 +276,7 @@ class CallNode extends ControlFlowNode { /** Gets the flow node corresponding to the named argument of the call corresponding to this flow node */ ControlFlowNode getArgByName(string name) { - exists(Call c, Keyword k | + exists(Py::Call c, Py::Keyword k | this.getNode() = c and k = c.getANamedArg() and k.getValue() = result.getNode() and @@ -292,7 +292,7 @@ class CallNode extends ControlFlowNode { result = this.getArgByName(_) } - override Call getNode() { result = super.getNode() } + override Py::Call getNode() { result = super.getNode() } predicate isDecoratorCall() { this.isClassDecoratorCall() @@ -301,11 +301,11 @@ class CallNode extends ControlFlowNode { } predicate isClassDecoratorCall() { - exists(ClassExpr cls | this.getNode() = cls.getADecoratorCall()) + exists(Py::ClassExpr cls | this.getNode() = cls.getADecoratorCall()) } predicate isFunctionDecoratorCall() { - exists(FunctionExpr func | this.getNode() = func.getADecoratorCall()) + exists(Py::FunctionExpr func | this.getNode() = func.getADecoratorCall()) } /** Gets the first tuple (*) argument of this call, if any. */ @@ -323,11 +323,11 @@ class CallNode extends ControlFlowNode { /** A control flow corresponding to an attribute expression, such as `value.attr` */ class AttrNode extends ControlFlowNode { - AttrNode() { toAst(this) instanceof Attribute } + AttrNode() { toAst(this) instanceof Py::Attribute } /** Gets the flow node corresponding to the object of the attribute expression corresponding to this flow node */ ControlFlowNode getObject() { - exists(Attribute a | + exists(Py::Attribute a | this.getNode() = a and a.getObject() = result.getNode() and result.getBasicBlock().dominates(this.getBasicBlock()) @@ -339,7 +339,7 @@ class AttrNode extends ControlFlowNode { * with the matching name */ ControlFlowNode getObject(string name) { - exists(Attribute a | + exists(Py::Attribute a | this.getNode() = a and a.getObject() = result.getNode() and a.getName() = name and @@ -348,57 +348,57 @@ class AttrNode extends ControlFlowNode { } /** Gets the attribute name of the attribute expression corresponding to this flow node */ - string getName() { exists(Attribute a | this.getNode() = a and a.getName() = result) } + string getName() { exists(Py::Attribute a | this.getNode() = a and a.getName() = result) } - override Attribute getNode() { result = super.getNode() } + override Py::Attribute getNode() { result = super.getNode() } } /** A control flow node corresponding to a `from ... import ...` expression */ class ImportMemberNode extends ControlFlowNode { - ImportMemberNode() { toAst(this) instanceof ImportMember } + ImportMemberNode() { toAst(this) instanceof Py::ImportMember } /** * Gets the flow node corresponding to the module in the import-member expression corresponding to this flow node, * with the matching name */ ControlFlowNode getModule(string name) { - exists(ImportMember i | this.getNode() = i and i.getModule() = result.getNode() | + exists(Py::ImportMember i | this.getNode() = i and i.getModule() = result.getNode() | i.getName() = name and result.getBasicBlock().dominates(this.getBasicBlock()) ) } - override ImportMember getNode() { result = super.getNode() } + override Py::ImportMember getNode() { result = super.getNode() } } /** A control flow node corresponding to an artificial expression representing an import */ class ImportExprNode extends ControlFlowNode { - ImportExprNode() { toAst(this) instanceof ImportExpr } + ImportExprNode() { toAst(this) instanceof Py::ImportExpr } - override ImportExpr getNode() { result = super.getNode() } + override Py::ImportExpr getNode() { result = super.getNode() } } /** A control flow node corresponding to a `from ... import *` statement */ class ImportStarNode extends ControlFlowNode { - ImportStarNode() { toAst(this) instanceof ImportStar } + ImportStarNode() { toAst(this) instanceof Py::ImportStar } /** Gets the flow node corresponding to the module in the import-star corresponding to this flow node */ ControlFlowNode getModule() { - exists(ImportStar i | this.getNode() = i and i.getModuleExpr() = result.getNode() | + exists(Py::ImportStar i | this.getNode() = i and i.getModuleExpr() = result.getNode() | result.getBasicBlock().dominates(this.getBasicBlock()) ) } - override ImportStar getNode() { result = super.getNode() } + override Py::ImportStar getNode() { result = super.getNode() } } /** A control flow node corresponding to a subscript expression, such as `value[slice]` */ class SubscriptNode extends ControlFlowNode { - SubscriptNode() { toAst(this) instanceof Subscript } + SubscriptNode() { toAst(this) instanceof Py::Subscript } /** flow node corresponding to the value of the sequence in a subscript operation */ ControlFlowNode getObject() { - exists(Subscript s | + exists(Py::Subscript s | this.getNode() = s and s.getObject() = result.getNode() and result.getBasicBlock().dominates(this.getBasicBlock()) @@ -407,23 +407,23 @@ class SubscriptNode extends ControlFlowNode { /** flow node corresponding to the index in a subscript operation */ ControlFlowNode getIndex() { - exists(Subscript s | + exists(Py::Subscript s | this.getNode() = s and s.getIndex() = result.getNode() and result.getBasicBlock().dominates(this.getBasicBlock()) ) } - override Subscript getNode() { result = super.getNode() } + override Py::Subscript getNode() { result = super.getNode() } } /** A control flow node corresponding to a comparison operation, such as `x DeletionNode -> NameNode('b') -> AttrNode('y') -> DeletionNode`. */ class DeletionNode extends ControlFlowNode { - DeletionNode() { toAst(this) instanceof Delete } + DeletionNode() { toAst(this) instanceof Py::Delete } /** Gets the unique target of this deletion node. */ ControlFlowNode getTarget() { result.getASuccessor() = this } @@ -617,9 +617,9 @@ class DeletionNode extends ControlFlowNode { /** A control flow node corresponding to a sequence (tuple or list) literal */ abstract class SequenceNode extends ControlFlowNode { SequenceNode() { - toAst(this) instanceof Tuple + toAst(this) instanceof Py::Tuple or - toAst(this) instanceof List + toAst(this) instanceof Py::List } /** Gets the control flow node for an element of this sequence */ @@ -632,11 +632,11 @@ abstract class SequenceNode extends ControlFlowNode { /** A control flow node corresponding to a tuple expression such as `( 1, 3, 5, 7, 9 )` */ class TupleNode extends SequenceNode { - TupleNode() { toAst(this) instanceof Tuple } + TupleNode() { toAst(this) instanceof Py::Tuple } override ControlFlowNode getElement(int n) { Stages::AST::ref() and - exists(Tuple t | this.getNode() = t and result.getNode() = t.getElt(n)) and + exists(Py::Tuple t | this.getNode() = t and result.getNode() = t.getElt(n)) and ( result.getBasicBlock().dominates(this.getBasicBlock()) or @@ -647,10 +647,10 @@ class TupleNode extends SequenceNode { /** A control flow node corresponding to a list expression, such as `[ 1, 3, 5, 7, 9 ]` */ class ListNode extends SequenceNode { - ListNode() { toAst(this) instanceof List } + ListNode() { toAst(this) instanceof Py::List } override ControlFlowNode getElement(int n) { - exists(List l | this.getNode() = l and result.getNode() = l.getElt(n)) and + exists(Py::List l | this.getNode() = l and result.getNode() = l.getElt(n)) and ( result.getBasicBlock().dominates(this.getBasicBlock()) or @@ -661,10 +661,10 @@ class ListNode extends SequenceNode { /** A control flow node corresponding to a set expression, such as `{ 1, 3, 5, 7, 9 }` */ class SetNode extends ControlFlowNode { - SetNode() { toAst(this) instanceof Set } + SetNode() { toAst(this) instanceof Py::Set } ControlFlowNode getAnElement() { - exists(Set s | this.getNode() = s and result.getNode() = s.getElt(_)) and + exists(Py::Set s | this.getNode() = s and result.getNode() = s.getElt(_)) and ( result.getBasicBlock().dominates(this.getBasicBlock()) or @@ -675,20 +675,20 @@ class SetNode extends ControlFlowNode { /** A control flow node corresponding to a dictionary literal, such as `{ 'a': 1, 'b': 2 }` */ class DictNode extends ControlFlowNode { - DictNode() { toAst(this) instanceof Dict } + DictNode() { toAst(this) instanceof Py::Dict } /** * Gets a key of this dictionary literal node, for those items that have keys * E.g, in {'a':1, **b} this returns only 'a' */ ControlFlowNode getAKey() { - exists(Dict d | this.getNode() = d and result.getNode() = d.getAKey()) and + exists(Py::Dict d | this.getNode() = d and result.getNode() = d.getAKey()) and result.getBasicBlock().dominates(this.getBasicBlock()) } /** Gets a value of this dictionary literal node */ ControlFlowNode getAValue() { - exists(Dict d | this.getNode() = d and result.getNode() = d.getAValue()) and + exists(Py::Dict d | this.getNode() = d and result.getNode() = d.getAValue()) and result.getBasicBlock().dominates(this.getBasicBlock()) } } @@ -712,21 +712,21 @@ class IterableNode extends ControlFlowNode { } } -private AstNode assigned_value(Expr lhs) { +private Py::AstNode assigned_value(Py::Expr lhs) { /* lhs = result */ - exists(Assign a | a.getATarget() = lhs and result = a.getValue()) + exists(Py::Assign a | a.getATarget() = lhs and result = a.getValue()) or /* lhs := result */ - exists(AssignExpr a | a.getTarget() = lhs and result = a.getValue()) + exists(Py::AssignExpr a | a.getTarget() = lhs and result = a.getValue()) or /* lhs : annotation = result */ - exists(AnnAssign a | a.getTarget() = lhs and result = a.getValue()) + exists(Py::AnnAssign a | a.getTarget() = lhs and result = a.getValue()) or /* import result as lhs */ - exists(Alias a | a.getAsname() = lhs and result = a.getValue()) + exists(Py::Alias a | a.getAsname() = lhs and result = a.getValue()) or /* lhs += x => result = (lhs + x) */ - exists(AugAssign a, BinaryExpr b | b = a.getOperation() and result = b and lhs = b.getLeft()) + exists(Py::AugAssign a, Py::BinaryExpr b | b = a.getOperation() and result = b and lhs = b.getLeft()) or /* * ..., lhs, ... = ..., result, ... @@ -734,31 +734,31 @@ private AstNode assigned_value(Expr lhs) { * ..., (..., lhs, ...), ... = ..., (..., result, ...), ... */ - exists(Assign a | nested_sequence_assign(a.getATarget(), a.getValue(), lhs, result)) + exists(Py::Assign a | nested_sequence_assign(a.getATarget(), a.getValue(), lhs, result)) or /* for lhs in seq: => `result` is the `for` node, representing the `iter(next(seq))` operation. */ - result.(For).getTarget() = lhs + result.(Py::For).getTarget() = lhs or - exists(Parameter param | lhs = param.asName() and result = param.getDefault()) + exists(Py::Parameter param | lhs = param.asName() and result = param.getDefault()) } predicate nested_sequence_assign( - Expr left_parent, Expr right_parent, Expr left_result, Expr right_result + Py::Expr left_parent, Py::Expr right_parent, Py::Expr left_result, Py::Expr right_result ) { - exists(Assign a | + exists(Py::Assign a | a.getATarget().getASubExpression*() = left_parent and a.getValue().getASubExpression*() = right_parent ) and - exists(int i, Expr left_elem, Expr right_elem | + exists(int i, Py::Expr left_elem, Py::Expr right_elem | ( - left_elem = left_parent.(Tuple).getElt(i) + left_elem = left_parent.(Py::Tuple).getElt(i) or - left_elem = left_parent.(List).getElt(i) + left_elem = left_parent.(Py::List).getElt(i) ) and ( - right_elem = right_parent.(Tuple).getElt(i) + right_elem = right_parent.(Py::Tuple).getElt(i) or - right_elem = right_parent.(List).getElt(i) + right_elem = right_parent.(Py::List).getElt(i) ) | left_result = left_elem and right_result = right_elem @@ -769,9 +769,9 @@ predicate nested_sequence_assign( /** A flow node for a `for` statement. */ class ForNode extends ControlFlowNode { - ForNode() { toAst(this) instanceof For } + ForNode() { toAst(this) instanceof Py::For } - override For getNode() { result = super.getNode() } + override Py::For getNode() { result = super.getNode() } /** Holds if this `for` statement causes iteration over `sequence` storing each step of the iteration in `target` */ predicate iterates(ControlFlowNode target, ControlFlowNode sequence) { @@ -782,7 +782,7 @@ class ForNode extends ControlFlowNode { /** Gets the sequence node for this `for` statement. */ ControlFlowNode getSequence() { - exists(For for | + exists(Py::For for | toAst(this) = for and for.getIter() = result.getNode() | @@ -792,7 +792,7 @@ class ForNode extends ControlFlowNode { /** A possible `target` for this `for` statement, not accounting for loop unrolling */ private ControlFlowNode possibleTarget() { - exists(For for | + exists(Py::For for | toAst(this) = for and for.getTarget() = result.getNode() and this.getBasicBlock().dominates(result.getBasicBlock()) @@ -809,11 +809,11 @@ class ForNode extends ControlFlowNode { /** A flow node for a `raise` statement */ class RaiseStmtNode extends ControlFlowNode { - RaiseStmtNode() { toAst(this) instanceof Raise } + RaiseStmtNode() { toAst(this) instanceof Py::Raise } /** Gets the control flow node for the exception raised by this raise statement */ ControlFlowNode getException() { - exists(Raise r | + exists(Py::Raise r | r = toAst(this) and r.getException() = toAst(result) and result.getBasicBlock().dominates(this.getBasicBlock()) @@ -827,36 +827,36 @@ class RaiseStmtNode extends ControlFlowNode { */ class NameNode extends ControlFlowNode { NameNode() { - exists(Name n | py_flow_bb_node(this, n, _, _)) + exists(Py::Name n | py_flow_bb_node(this, n, _, _)) or - exists(PlaceHolder p | py_flow_bb_node(this, p, _, _)) + exists(Py::PlaceHolder p | py_flow_bb_node(this, p, _, _)) } /** Whether this flow node defines the variable `v`. */ - predicate defines(Variable v) { - exists(Name d | this.getNode() = d and d.defines(v)) and + predicate defines(Py::Variable v) { + exists(Py::Name d | this.getNode() = d and d.defines(v)) and not this.isLoad() } /** Whether this flow node deletes the variable `v`. */ - predicate deletes(Variable v) { exists(Name d | this.getNode() = d and d.deletes(v)) } + predicate deletes(Py::Variable v) { exists(Py::Name d | this.getNode() = d and d.deletes(v)) } /** Whether this flow node uses the variable `v`. */ - predicate uses(Variable v) { + predicate uses(Py::Variable v) { this.isLoad() and - exists(Name u | this.getNode() = u and u.uses(v)) + exists(Py::Name u | this.getNode() = u and u.uses(v)) or - exists(PlaceHolder u | - this.getNode() = u and u.getVariable() = v and u.getCtx() instanceof Load + exists(Py::PlaceHolder u | + this.getNode() = u and u.getVariable() = v and u.getCtx() instanceof Py::Load ) or Scopes::use_of_global_variable(this, v.getScope(), v.getId()) } string getId() { - result = this.getNode().(Name).getId() + result = this.getNode().(Py::Name).getId() or - result = this.getNode().(PlaceHolder).getId() + result = this.getNode().(Py::PlaceHolder).getId() } /** Whether this is a use of a local variable. */ @@ -868,37 +868,37 @@ class NameNode extends ControlFlowNode { /** Whether this is a use of a global (including builtin) variable. */ predicate isGlobal() { Scopes::use_of_global_variable(this, _, _) } - predicate isSelf() { exists(SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this) } + predicate isSelf() { exists(Py::SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this) } } /** A control flow node corresponding to a named constant, one of `None`, `True` or `False`. */ class NameConstantNode extends NameNode { - NameConstantNode() { exists(NameConstant n | py_flow_bb_node(this, n, _, _)) } + NameConstantNode() { exists(Py::NameConstant n | py_flow_bb_node(this, n, _, _)) } /* * We ought to override uses as well, but that has * a serious performance impact. - * deprecated predicate uses(Variable v) { none() } + * deprecated predicate uses(Py::Variable v) { none() } */ } /** A control flow node corresponding to a starred expression, `*a`. */ class StarredNode extends ControlFlowNode { - StarredNode() { toAst(this) instanceof Starred } + StarredNode() { toAst(this) instanceof Py::Starred } - ControlFlowNode getValue() { toAst(result) = toAst(this).(Starred).getValue() } + ControlFlowNode getValue() { toAst(result) = toAst(this).(Py::Starred).getValue() } } /** The ControlFlowNode for an 'except' statement. */ class ExceptFlowNode extends ControlFlowNode { - ExceptFlowNode() { this.getNode() instanceof ExceptStmt } + ExceptFlowNode() { this.getNode() instanceof Py::ExceptStmt } /** * Gets the type handled by this exception handler. - * `ExceptionType` in `except ExceptionType as e:` + * `Py::ExceptionType` in `except Py::ExceptionType as e:` */ ControlFlowNode getType() { - exists(ExceptStmt ex | + exists(Py::ExceptStmt ex | this.getBasicBlock().dominates(result.getBasicBlock()) and ex = this.getNode() and result = ex.getType().getAFlowNode() @@ -907,10 +907,10 @@ class ExceptFlowNode extends ControlFlowNode { /** * Gets the name assigned to the handled exception, if any. - * `e` in `except ExceptionType as e:` + * `e` in `except Py::ExceptionType as e:` */ ControlFlowNode getName() { - exists(ExceptStmt ex | + exists(Py::ExceptStmt ex | this.getBasicBlock().dominates(result.getBasicBlock()) and ex = this.getNode() and result = ex.getName().getAFlowNode() @@ -920,30 +920,30 @@ class ExceptFlowNode extends ControlFlowNode { /** The ControlFlowNode for an 'except*' statement. */ class ExceptGroupFlowNode extends ControlFlowNode { - ExceptGroupFlowNode() { this.getNode() instanceof ExceptGroupStmt } + ExceptGroupFlowNode() { this.getNode() instanceof Py::ExceptGroupStmt } /** * Gets the type handled by this exception handler. - * `ExceptionType` in `except* ExceptionType as e:` + * `Py::ExceptionType` in `except* Py::ExceptionType as e:` */ ControlFlowNode getType() { this.getBasicBlock().dominates(result.getBasicBlock()) and - result = this.getNode().(ExceptGroupStmt).getType().getAFlowNode() + result = this.getNode().(Py::ExceptGroupStmt).getType().getAFlowNode() } /** * Gets the name assigned to the handled exception, if any. - * `e` in `except* ExceptionType as e:` + * `e` in `except* Py::ExceptionType as e:` */ ControlFlowNode getName() { this.getBasicBlock().dominates(result.getBasicBlock()) and - result = this.getNode().(ExceptGroupStmt).getName().getAFlowNode() + result = this.getNode().(Py::ExceptGroupStmt).getName().getAFlowNode() } } private module Scopes { private predicate fast_local(NameNode n) { - exists(FastLocalVariable v | + exists(Py::FastLocalVariable v | n.uses(v) and v.getScope() = n.getScope() ) @@ -952,15 +952,15 @@ private module Scopes { predicate local(NameNode n) { fast_local(n) or - exists(SsaVariable var | + exists(Py::SsaVariable var | var.getAUse() = n and - n.getScope() instanceof Class and + n.getScope() instanceof Py::Class and exists(var.getDefinition()) ) } predicate non_local(NameNode n) { - exists(FastLocalVariable flv | + exists(Py::FastLocalVariable flv | flv.getALoad() = n.getNode() and not flv.getScope() = n.getScope() ) @@ -968,20 +968,20 @@ private module Scopes { // magic is fine, but we get questionable join-ordering of it pragma[nomagic] - predicate use_of_global_variable(NameNode n, Module scope, string name) { + predicate use_of_global_variable(NameNode n, Py::Module scope, string name) { n.isLoad() and not non_local(n) and - not exists(SsaVariable var | var.getAUse() = n | - var.getVariable() instanceof FastLocalVariable + not exists(Py::SsaVariable var | var.getAUse() = n | + var.getVariable() instanceof Py::FastLocalVariable or - n.getScope() instanceof Class and + n.getScope() instanceof Py::Class and not maybe_undefined(var) ) and name = n.getId() and scope = n.getEnclosingModule() } - private predicate maybe_undefined(SsaVariable var) { + private predicate maybe_undefined(Py::SsaVariable var) { not exists(var.getDefinition()) and not py_ssa_phi(var, _) or var.getDefinition().isDelete() @@ -1058,13 +1058,13 @@ class BasicBlock extends @py_flow_node { private predicate oneNodeBlock() { this.firstNode() = this.getLastNode() } private predicate startLocationInfo(string file, int line, int col) { - if this.firstNode().getNode() instanceof Scope + if this.firstNode().getNode() instanceof Py::Scope then this.firstNode().getASuccessor().getLocation().hasLocationInfo(file, line, col, _, _) else this.firstNode().getLocation().hasLocationInfo(file, line, col, _, _) } private predicate endLocationInfo(int endl, int endc) { - if this.getLastNode().getNode() instanceof Scope and not this.oneNodeBlock() + if this.getLastNode().getNode() instanceof Py::Scope and not this.oneNodeBlock() then this.getLastNode().getAPredecessor().getLocation().hasLocationInfo(_, _, _, endl, endc) else this.getLastNode().getLocation().hasLocationInfo(_, _, _, endl, endc) } @@ -1081,7 +1081,7 @@ class BasicBlock extends @py_flow_node { /** Whether flow from this basic block reaches a normal exit from its scope */ predicate reachesExit() { - exists(Scope s | s.getANormalExit().getBasicBlock() = this) + exists(Py::Scope s | s.getANormalExit().getBasicBlock() = this) or this.getASuccessor().reachesExit() } @@ -1090,7 +1090,7 @@ class BasicBlock extends @py_flow_node { * Holds if this element is at the specified location. * The location spans column `startcolumn` of line `startline` to * column `endcolumn` of line `endline` in file `filepath`. - * For more information, see + * Py::For more information, see * [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/). */ predicate hasLocationInfo( @@ -1122,7 +1122,7 @@ class BasicBlock extends @py_flow_node { /** Gets the scope of this block */ pragma[nomagic] - Scope getScope() { + Py::Scope getScope() { exists(ControlFlowNode n | n.getBasicBlock() = this | /* Take care not to use an entry or exit node as that node's scope will be the outer scope */ not py_scope_flow(n, _, -1) and @@ -1145,17 +1145,17 @@ class BasicBlock extends @py_flow_node { predicate reaches(BasicBlock other) { this = other or this.strictlyReaches(other) } /** - * Gets the `ConditionBlock`, if any, that controls this block and - * does not control any other `ConditionBlock`s that control this block. - * That is the `ConditionBlock` that is closest dominator. + * Gets the `Py::ConditionBlock`, if any, that controls this block and + * does not control any other `Py::ConditionBlock`s that control this block. + * That is the `Py::ConditionBlock` that is closest dominator. */ - ConditionBlock getImmediatelyControllingBlock() { + Py::ConditionBlock getImmediatelyControllingBlock() { result = this.nonControllingImmediateDominator*().getImmediateDominator() } private BasicBlock nonControllingImmediateDominator() { result = this.getImmediateDominator() and - not result.(ConditionBlock).controls(this, _) + not result.(Py::ConditionBlock).controls(this, _) } /** @@ -1175,7 +1175,7 @@ private class ControlFlowNodeAlias = ControlFlowNode; final private class FinalBasicBlock = BasicBlock; -module Cfg implements BB::CfgSig { +module Cfg implements BB::CfgSig { private import codeql.controlflow.SuccessorType class ControlFlowNode = ControlFlowNodeAlias; @@ -1186,7 +1186,7 @@ module Cfg implements BB::CfgSig { // Using the location of the first node is simple // and we just need a way to identify the basic block // during debugging, so this will be serviceable. - Location getLocation() { result = super.getNode(0).getLocation() } + Py::Location getLocation() { result = super.getNode(0).getLocation() } int length() { result = count(int i | exists(this.getNode(i))) } From ac468c8f37bfffb3f43f3e8e453641860731999a Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 21:13:57 +0000 Subject: [PATCH 60/72] Python: extend new SSA with ESSA-shaped adapter + baseline comparison test Phase 0.5 - Adapter API on top of the shared SSA: Adds the legacy-ESSA-shaped class hierarchy that the dataflow library consumes, layered on the shared 'Ssa::Make' instantiation: * EssaDefinition / EssaNodeDefinition: the latter exposes 'getDefiningNode()' (the CFG node at the def's index in its BB) and 'getVariable()' / 'getScope()'. * AssignmentDefinition: matches Assign, AnnAssign with value, AssignExpr and AugAssign target Names. Exposes 'getValue()' pointing at the RHS' CFG node. * ParameterDefinition: matches when the defining Name is in parameter context. * WithDefinition: matches 'with ... as x:' bindings. * ScopeEntryDefinition: implicit entry defs at synthetic position '-1' of the scope's entry basic block (non-local / global / builtin / captured reads). * PhiFunction (alias for PhiNode). * EssaVariable adapter wrapping a 'Ssa::Definition' with 'getAUse()', 'getDefinition()', 'getAnUltimateDefinition()', and 'getName()'. * AdjacentUses module with 'firstUse' and 'adjacentUseUse' predicates bridging to 'Ssa::firstUse' / 'Ssa::adjacentUseUse'. This is the minimum API the new dataflow's internals call into. The richer legacy ESSA (refinement nodes, attribute refinements, edge refinements) stays in 'semmle.python.essa.Essa' for legacy code. Phase 0.6 - Comparison test: Adds 'dataflow-new-ssa-vs-legacy/CmpTest.ql' that snapshots the difference between definitions produced by new SSA vs legacy ESSA on the same Python source. Baseline output records the current 'def-only-old' mismatches, grouped by category: * function/class/global definitions with no in-scope read (intentional; SSA is liveness-pruned) * captured / closure variables (real gap in new SSA - no closure-capture handling yet) * module variables __name__ / __package__ / $ (legacy ESSA implicit bindings) * exception 'as' bindings (depend on raise modelling) Zero 'def-only-new' mismatches: the new SSA never produces a spurious definition compared to legacy ESSA on this corpus. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/dataflow/new/internal/SsaImpl.qll | 194 ++++++++++++++++++ .../CmpTest.expected | 20 ++ .../dataflow-new-ssa-vs-legacy/CmpTest.ql | 59 ++++++ .../dataflow-new-ssa-vs-legacy/test.py | 53 +++++ 4 files changed, 326 insertions(+) create mode 100644 python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected create mode 100644 python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.ql create mode 100644 python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/test.py diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll index 11e7b9f4d3d1..da8f34b8b528 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -177,3 +177,197 @@ final class WriteDefinition = Ssa::WriteDefinition; final class UncertainWriteDefinition = Ssa::UncertainWriteDefinition; final class PhiNode = Ssa::PhiNode; + +// =========================================================================== +// ESSA-shaped adapter layer +// +// The dataflow library (`python/ql/lib/semmle/python/dataflow/new/`) and +// related modules (`ApiGraphs.qll`, etc.) consume the legacy ESSA API +// (`EssaVariable`, `EssaDefinition`, `AssignmentDefinition`, +// `ScopeEntryDefinition`, `ParameterDefinition`, `WithDefinition`, +// `PhiFunction`, plus the `AdjacentUses` module). To migrate them off +// the legacy CFG, we expose the same API surface on top of the +// shared SSA built above. +// +// This adapter is intentionally narrow: it covers only the predicates +// that new dataflow consumes. The richer legacy ESSA — refinement +// nodes, attribute refinements, edge refinements — stays available +// via `semmle.python.essa.Essa` for points-to / legacy code. +// =========================================================================== +/** + * Gets the CFG node at which a write definition's binding takes place. + * + * This is the `Cfg::ControlFlowNode` whose index in `def`'s basic block + * is the same as `def`'s defining index. Phi definitions have no + * defining CFG node and are excluded. + */ +private Cfg::ControlFlowNode writeDefNode(Ssa::WriteDefinition def) { + exists(CfgImpl::BasicBlock bb, int i | + def.definesAt(_, bb, i) and + result = bb.getNode(i) + ) +} + +/** + * A write definition whose binding has a corresponding CFG node — i.e. + * everything that's not a phi node. Mirrors legacy ESSA's + * `EssaNodeDefinition`. + */ +class EssaNodeDefinition extends Ssa::WriteDefinition { + /** Gets the CFG node where this definition's binding takes place. */ + Cfg::ControlFlowNode getDefiningNode() { result = writeDefNode(this) } + + /** Gets the variable defined here (legacy name). */ + SsaSourceVariable getVariable() { result = this.getSourceVariable() } + + /** Gets the enclosing scope. */ + Py::Scope getScope() { + exists(Cfg::ControlFlowNode n | n = this.getDefiningNode() | result = n.getScope()) + } +} + +/** + * An assignment definition `x = e`. The defining node is `x`'s CFG + * node; the value is `e`'s CFG node. + */ +class AssignmentDefinition extends EssaNodeDefinition { + AssignmentDefinition() { + exists(Cfg::NameNode n | n = this.getDefiningNode() | + exists(Py::Assign a | a.getATarget() = n.getNode()) + or + exists(Py::AnnAssign a | a.getTarget() = n.getNode() and exists(a.getValue())) + or + exists(Py::AssignExpr a | a.getTarget() = n.getNode()) + or + exists(Py::AugAssign a | a.getTarget() = n.getNode()) + ) + } + + /** Gets the CFG node for the value being assigned, if statically known. */ + Cfg::ControlFlowNode getValue() { + exists(Cfg::NameNode target | target = this.getDefiningNode() | + exists(Py::Assign a | + a.getATarget() = target.getNode() and + result.getNode() = a.getValue() + ) + or + exists(Py::AnnAssign a | + a.getTarget() = target.getNode() and + result.getNode() = a.getValue() + ) + or + exists(Py::AssignExpr a | + a.getTarget() = target.getNode() and + result.getNode() = a.getValue() + ) + ) + } +} + +/** + * A parameter definition — the binding of a parameter name in a + * function's scope. + */ +class ParameterDefinition extends EssaNodeDefinition { + ParameterDefinition() { this.getDefiningNode().isParameter() } + + /** Gets the AST `Parameter` (a `Py::Name` in param context). */ + Py::Name getParameter() { result = this.getDefiningNode().getNode() } +} + +/** + * A definition introduced by a `with ... as x:` clause. + */ +class WithDefinition extends EssaNodeDefinition { + WithDefinition() { + exists(Cfg::NameNode n, Py::With w | + n = this.getDefiningNode() and + w.getOptionalVars() = n.getNode() + ) + } +} + +/** + * An implicit entry definition for a non-local / captured / global / + * builtin variable read in a scope but not defined there. + */ +class ScopeEntryDefinition extends Ssa::Definition { + ScopeEntryDefinition() { + exists(CfgImpl::BasicBlock bb | + this.definesAt(_, bb, -1) and + bb instanceof CfgImpl::Cfg::EntryBasicBlock + ) + } + + /** Gets the variable being entered. */ + SsaSourceVariable getVariable() { result = this.getSourceVariable() } + + /** Gets the enclosing scope. */ + Py::Scope getScope() { + exists(CfgImpl::BasicBlock bb | + this.definesAt(_, bb, -1) and + result = this.getSourceVariable().getVariable().getScope() + ) + } +} + +/** A phi node (alias matching legacy naming). */ +class PhiFunction = PhiNode; + +/** Base class for all ESSA definitions (legacy-shaped). */ +class EssaDefinition = Ssa::Definition; + +/** + * An adapter representing a single SSA-defined "variable" — wrapping + * one `Ssa::Definition`. Mirrors legacy `EssaVariable` API. + */ +class EssaVariable extends Ssa::Definition { + /** Gets the underlying SSA definition (legacy name). */ + Ssa::Definition getDefinition() { result = this } + + /** Gets a CFG node where this definition is used. */ + Cfg::NameNode getAUse() { + exists(CfgImpl::BasicBlock bb, int i | + Ssa::ssaDefReachesRead(this.getSourceVariable(), this, bb, i) and + bb.getNode(i) = result + ) + } + + /** Gets the (textual) name of the underlying variable. */ + string getName() { result = this.getSourceVariable().getVariable().getId() } + + /** Gets an ultimate non-phi ancestor of this definition. */ + EssaVariable getAnUltimateDefinition() { + if this instanceof PhiNode + then + exists(Ssa::Definition input | + Ssa::phiHasInputFromBlock(this, input, _) and + result = input.(EssaVariable).getAnUltimateDefinition() + ) + else result = this + } +} + +/** + * Adjacent use-use and def-use relations exposed by the shared SSA + * library. Provides the same interface as legacy + * `semmle.python.essa.SsaCompute::AdjacentUses`. + */ +module AdjacentUses { + /** Holds if `nodeFrom` and `nodeTo` are adjacent uses of the same SSA variable. */ + predicate adjacentUseUse(Cfg::NameNode nodeFrom, Cfg::NameNode nodeTo) { + exists(SsaSourceVariable v, CfgImpl::BasicBlock bb1, int i1, CfgImpl::BasicBlock bb2, int i2 | + Ssa::adjacentUseUse(bb1, i1, bb2, i2, v, _) and + nodeFrom = bb1.getNode(i1) and + nodeTo = bb2.getNode(i2) + ) + } + + /** Holds if `use` is a first use of definition `def`. */ + predicate firstUse(Ssa::Definition def, Cfg::NameNode use) { + exists(CfgImpl::BasicBlock bb, int i | + Ssa::firstUse(def, bb, i, _) and + use = bb.getNode(i) + ) + } +} diff --git a/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected new file mode 100644 index 000000000000..ec2b8438c613 --- /dev/null +++ b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected @@ -0,0 +1,20 @@ +| def-only-old | $:0:0 | +| def-only-old | GLOBAL:49:1 | +| def-only-old | GLOBAL:52:1 | +| def-only-old | __name__:0:0 | +| def-only-old | __package__:0:0 | +| def-only-old | closure:31:5 | +| def-only-old | e:37:1 | +| def-only-old | e:40:25 | +| def-only-old | exception_binding:37:5 | +| def-only-old | if_else_branch:12:5 | +| def-only-old | kwargs:27:32 | +| def-only-old | loop:20:5 | +| def-only-old | parameter:27:5 | +| def-only-old | read_global:52:5 | +| def-only-old | reassignment:6:5 | +| def-only-old | simple_assign:1:5 | +| def-only-old | with_binding:44:5 | +| def-only-old | x:20:1 | +| def-only-old | x:31:13 | +| def-only-old | x:32:5 | diff --git a/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.ql b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.ql new file mode 100644 index 000000000000..590f5ebed47a --- /dev/null +++ b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.ql @@ -0,0 +1,59 @@ +/** + * Compares the new-CFG SSA against the legacy ESSA on the same Python + * sources. Reports definitions present in one implementation but not + * the other, identified by variable name + source position. + * + * The `.expected` file records the current diff as a snapshot: as the + * new SSA matures (closing captured-variable gap, exception bindings, + * etc.) and tracks more variables, the snapshot should monotonically + * shrink. + * + * Known categories of `def-only-old` mismatches: + * - Function / class / global definitions with no in-scope read + * (intentional: SSA is liveness-pruned, write-only variables are + * not tracked). + * - Captured / closure variables (gap: new SSA does not yet model + * closure captures). + * - Module variables `__name__`, `__package__`, `$` (legacy ESSA + * adds implicit bindings the new SSA does not). + * - Exception-handler `as` bindings (depend on raise modelling). + * + * `def-only-new` mismatches would indicate the new SSA produces spurious + * definitions; currently none are expected. + */ + +import python +import semmle.python.dataflow.new.internal.SsaImpl as NewSsa +import semmle.python.controlflow.internal.Cfg as Cfg +import semmle.python.essa.Essa + +string newDefSig(NewSsa::EssaNodeDefinition def) { + exists(Cfg::ControlFlowNode n | n = def.getDefiningNode() | + result = + def.getVariable().getVariable().getId() + ":" + n.getLocation().getStartLine() + ":" + + n.getLocation().getStartColumn() + ) +} + +string legacyDefSig(EssaNodeDefinition def) { + exists(ControlFlowNode n | n = def.getDefiningNode() | + result = + def.getSourceVariable().getName() + ":" + n.getLocation().getStartLine() + ":" + + n.getLocation().getStartColumn() + ) +} + +from string kind, string sig +where + kind = "def-only-new" and + exists(NewSsa::EssaNodeDefinition def | + sig = newDefSig(def) and + not exists(EssaNodeDefinition legacyDef | sig = legacyDefSig(legacyDef)) + ) + or + kind = "def-only-old" and + exists(EssaNodeDefinition legacyDef | + sig = legacyDefSig(legacyDef) and + not exists(NewSsa::EssaNodeDefinition def | sig = newDefSig(def)) + ) +select kind, sig diff --git a/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/test.py b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/test.py new file mode 100644 index 000000000000..8b061109bf2c --- /dev/null +++ b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/test.py @@ -0,0 +1,53 @@ +def simple_assign(): + x = 1 + return x + + +def reassignment(): + x = 1 + x = 2 + return x + + +def if_else_branch(cond): + if cond: + x = 1 + else: + x = 2 + return x + + +def loop(xs): + total = 0 + for x in xs: + total = total + x + return total + + +def parameter(a, b=2, *args, **kwargs): + return a + b + sum(args) + + +def closure(x): + def inner(): + return x + return inner + + +def exception_binding(): + try: + compute() + except Exception as e: + return e + + +def with_binding(): + with open("file") as f: + return f.read() + + +GLOBAL = 1 + + +def read_global(): + return GLOBAL From 57fa3ee2d4c200d895ce85dbdf52c7ff781d3fe2 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 19 May 2026 12:06:15 +0000 Subject: [PATCH 61/72] Python: SSA: handle closure variables via per-scope entry defs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new SSA's implicit entry-def predicate previously placed entries in the variable's defining scope. For closure variables that's the outer function, so inner functions had no entry def for the captured variable — reads in the inner scope failed to resolve to any definition. Mirrors legacy ESSA's 'NonLocalVariable.getScopeEntryDefinition()': place an implicit entry def at every reading scope's entry block, independently of where the variable is *defined*. A closure variable accessed in two nested functions and the outer one gets three entry defs (one per reading scope). Also makes 'ScopeEntryDefinition' extend 'EssaNodeDefinition' (matching legacy ESSA), with 'getDefiningNode()' returning the scope's entry CFG node. This requires extending the private 'writeDefNode' helper to project i=-1 entries to bb.getNode(0). Updates the new-vs-legacy comparison snapshot: closure-variable reads ('x:32:5'), nested global reads ('GLOBAL:52:1') now resolve. New 'def-only-new' entries appear for unbound names ('sum', 'open', 'compute') — the new SSA uniformly creates scope-entry defs for all non-local reads, including those that legacy ESSA classifies as builtin and excludes. This is a more uniform semantic and arguably cleaner. Updates the SsaTest 'some_undefined' annotation: previously documented as a known limitation, now correctly resolves to a scope-entry def. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/dataflow/new/internal/SsaImpl.qll | 58 ++++++++++++------- .../CmpTest.expected | 5 +- .../library-tests/dataflow-new-ssa/test.py | 2 +- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll index da8f34b8b528..4ca59ff5977a 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -83,6 +83,10 @@ class SsaSourceVariable extends TSsaSourceVariable { * Holds if `v` is a non-local read in scope `s`, in the sense that `s` * uses `v` but does not write it within `s`. This includes globals, * builtins, and variables captured from an enclosing function scope. + * + * The `Py::Variable` `v` lives in some defining scope (the module for + * globals, an outer function for closures, etc.); the reading scope + * `s` is the scope where the use of `v` occurs. */ private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) { exists(Cfg::NameNode n | @@ -93,17 +97,23 @@ private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) { } /** - * Holds if `v` should have an implicit entry definition at the start of - * scope `s`. This covers: - * - non-local / global / builtin variables (defined outside `s`), and - * - captured variables (defined in an enclosing scope but read here). + * Holds if `bb` is the entry basic block of a scope where `v` should + * have an implicit entry definition. This covers: + * - non-local / global / builtin variables read in `s`, and + * - captured variables (defined in an enclosing scope but read in `s`). + * + * Each reading scope gets its own entry def, so a closure variable can + * have multiple entry defs across all functions/methods that read it. * * Parameters are *not* included: their bound `Name` is itself a CFG * node (per the C#-style parameter wiring), so `variableWrite` fires at * the parameter's natural CFG index. */ -private predicate hasEntryDef(SsaSourceVariable v, Py::Scope s) { - nonLocalReadIn(v.getVariable(), s) +private predicate hasEntryDefIn(SsaSourceVariable v, CfgImpl::BasicBlock bb) { + exists(Py::Scope s | + nonLocalReadIn(v.getVariable(), s) and + bb = entryBlock(s) + ) } /** @@ -144,9 +154,11 @@ private module SsaImplInput implements SsaImplCommon::InputSig= 0 and result = bb.getNode(i) + or + i = -1 and result = bb.getNode(0) ) } @@ -290,8 +304,11 @@ class WithDefinition extends EssaNodeDefinition { /** * An implicit entry definition for a non-local / captured / global / * builtin variable read in a scope but not defined there. + * + * Inherits from `EssaNodeDefinition` and exposes the scope's entry node + * as its defining node (matching legacy ESSA semantics). */ -class ScopeEntryDefinition extends Ssa::Definition { +class ScopeEntryDefinition extends EssaNodeDefinition { ScopeEntryDefinition() { exists(CfgImpl::BasicBlock bb | this.definesAt(_, bb, -1) and @@ -299,14 +316,11 @@ class ScopeEntryDefinition extends Ssa::Definition { ) } - /** Gets the variable being entered. */ - SsaSourceVariable getVariable() { result = this.getSourceVariable() } - - /** Gets the enclosing scope. */ - Py::Scope getScope() { + /** Gets the enclosing scope (the scope whose entry block this def is in). */ + override Py::Scope getScope() { exists(CfgImpl::BasicBlock bb | this.definesAt(_, bb, -1) and - result = this.getSourceVariable().getVariable().getScope() + result = bb.getNode(0).(Cfg::ControlFlowNode).getScope() ) } } diff --git a/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected index ec2b8438c613..7aaeefce2f88 100644 --- a/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected +++ b/python/ql/test/library-tests/dataflow-new-ssa-vs-legacy/CmpTest.expected @@ -1,6 +1,8 @@ +| def-only-new | compute:37:1 | +| def-only-new | open:44:1 | +| def-only-new | sum:27:1 | | def-only-old | $:0:0 | | def-only-old | GLOBAL:49:1 | -| def-only-old | GLOBAL:52:1 | | def-only-old | __name__:0:0 | | def-only-old | __package__:0:0 | | def-only-old | closure:31:5 | @@ -17,4 +19,3 @@ | def-only-old | with_binding:44:5 | | def-only-old | x:20:1 | | def-only-old | x:31:13 | -| def-only-old | x:32:5 | diff --git a/python/ql/test/library-tests/dataflow-new-ssa/test.py b/python/ql/test/library-tests/dataflow-new-ssa/test.py index fb1f658c4fbf..c6cdc22c3b36 100644 --- a/python/ql/test/library-tests/dataflow-new-ssa/test.py +++ b/python/ql/test/library-tests/dataflow-new-ssa/test.py @@ -35,6 +35,6 @@ def if_else_phi(cond): # $ def=cond def use_global(): - return some_undefined # known limitation: undefined globals not resolved here + return some_undefined # $ use=some_undefined From 6978cecb898375214bf7f6a27ddf177d3c48890c Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 19 May 2026 12:19:28 +0000 Subject: [PATCH 62/72] Python: SSA adapter: add MultiAssignmentDefinition, definedBy, useOfDef Extends the ESSA-shaped adapter on top of the new shared SSA with the remaining APIs consumed by the dataflow library: * MultiAssignmentDefinition: matches the AST pattern 'a, b = ...' where the LHS is a Tuple/List and the Name being defined is a sub-element. Used by IterableUnpacking.qll to recognise unpacking assignments. * EssaNodeDefinition.definedBy(var, defNode): a flatter equivalent of 'getSourceVariable() = var and getDefiningNode() = defNode', matching legacy ESSA's signature. Used by DataFlowPublic.qll's ModuleVariableNode to enumerate writes of a global. * AdjacentUses::useOfDef(def, use): all reachable uses of a definition (firstUse plus transitive use-use adjacency). Used by guards in DataFlowPublic.qll. These complete the API surface enumerated by grep across the dataflow library. The remaining items (EssaNodeRefinement, EssaImportStep) are ImportResolution-specific and will need separate treatment, possibly via a different abstraction since the SSA library does not model heap-state refinements like 'foo.bar = X'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/dataflow/new/internal/SsaImpl.qll | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll index 4ca59ff5977a..59433a96aae8 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -238,6 +238,15 @@ class EssaNodeDefinition extends Ssa::WriteDefinition { Py::Scope getScope() { exists(Cfg::ControlFlowNode n | n = this.getDefiningNode() | result = n.getScope()) } + + /** + * Holds if this definition defines source variable `v` at CFG node + * `defNode`. Flatter form of `getSourceVariable()` + + * `getDefiningNode()`, matching legacy ESSA's `definedBy`. + */ + predicate definedBy(SsaSourceVariable v, Cfg::ControlFlowNode defNode) { + v = this.getSourceVariable() and defNode = this.getDefiningNode() + } } /** @@ -301,6 +310,23 @@ class WithDefinition extends EssaNodeDefinition { } } +/** + * An assignment where the LHS is a tuple/list and the RHS is unpacked: + * `a, b = (1, 2)` or `a, *rest = xs`. The defining node for each + * captured Name is the Name itself. + */ +class MultiAssignmentDefinition extends EssaNodeDefinition { + MultiAssignmentDefinition() { + exists(Cfg::NameNode n | n = this.getDefiningNode() | + exists(Py::Assign a, Py::Expr lhs | + a.getATarget() = lhs and + (lhs instanceof Py::Tuple or lhs instanceof Py::List) and + lhs.getASubExpression+() = n.getNode() + ) + ) + } +} + /** * An implicit entry definition for a non-local / captured / global / * builtin variable read in a scope but not defined there. @@ -384,4 +410,14 @@ module AdjacentUses { use = bb.getNode(i) ) } + + /** + * Holds if `use` is any reachable use of definition `def`. Combines + * `firstUse` with transitive use-use adjacency. + */ + predicate useOfDef(Ssa::Definition def, Cfg::NameNode use) { + firstUse(def, use) + or + exists(Cfg::NameNode mid | useOfDef(def, mid) and adjacentUseUse(mid, use)) + } } From 3b3bec88254a1da5c84f2c034896213096845758 Mon Sep 17 00:00:00 2001 From: yoff Date: Thu, 21 May 2026 10:28:42 +0000 Subject: [PATCH 63/72] =?UTF-8?q?Python:=20remove=20getAFlowNode()=20?= =?UTF-8?q?=E2=80=94=20bridge=20AST=E2=86=92CFG=20only=20via=20CFG-side=20?= =?UTF-8?q?getNode()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Option 2: eliminates the AST→CFG bridge from the AST layer. Previously 'AstNode.getAFlowNode()' returned a 'ControlFlowNode' from the legacy 'Flow.qll' CFG via 'py_flow_bb_node' — this hardcoded the AST to know about the legacy CFG, preventing files from cleanly switching to the new shared CFG. Removes: * 'AstNode.getAFlowNode()' from 'AstExtended.qll' * Type-narrowing overrides on 'Attribute' / 'Subscript' / 'Call' / 'IfExp' / 'Name' / 'NameConstant' / 'ImportMember' (in Exprs.qll and Import.qll) Rewrites ~130 call sites across 'python/ql/lib/' and 'python/ql/src/' to bridge from the CFG side instead: Before: node = expr.getAFlowNode() After: node.getNode() = expr Before: expr.getAFlowNode().(DefinitionNode).getValue() After: exists(DefinitionNode d | d.getNode() = expr | d.getValue()) Before: cn.operands(const.getAFlowNode(), op, x) After: exists(ControlFlowNode c | c.getNode() = const | cn.operands(c, op, x)) This is semantically a no-op — both forms are duals of the same predicate. Verified by passing all library tests: * 64 dataflow tests * 28 ControlFlow + dataflow-new-ssa tests * 1 essa SSA-compute test * 93 tests total in the focused suite Once committed, files that want to switch from the legacy 'Flow' CFG to the new 'Cfg' facade only need to change their imports — the bridge sites are CFG-side and respect whichever ControlFlowNode is in scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/ql/lib/LegacyPointsTo.qll | 33 ++++++++++++++----- python/ql/lib/analysis/DefinitionTracking.qll | 4 +-- python/ql/lib/semmle/python/AstExtended.qll | 11 ------- python/ql/lib/semmle/python/Exprs.qll | 10 +----- python/ql/lib/semmle/python/Flow.qll | 26 +++++++-------- python/ql/lib/semmle/python/Import.qll | 1 - python/ql/lib/semmle/python/SelfAttribute.qll | 17 ++++++---- .../python/controlflow/internal/Cfg.qll | 9 ++--- .../python/dataflow/new/BarrierGuards.qll | 30 ++++++++++------- .../dataflow/new/internal/Attributes.qll | 2 +- .../new/internal/DataFlowDispatch.qll | 6 ++-- .../dataflow/new/internal/DataFlowPrivate.qll | 4 +-- .../dataflow/new/internal/DataFlowPublic.qll | 2 +- .../dataflow/new/internal/VariableCapture.qll | 6 ++-- .../python/dataflow/old/Implementation.qll | 16 +++++---- .../lib/semmle/python/essa/SsaDefinitions.qll | 6 ++-- .../lib/semmle/python/frameworks/Bottle.qll | 2 +- .../lib/semmle/python/frameworks/FastApi.qll | 2 +- .../ql/lib/semmle/python/frameworks/Flask.qll | 2 +- .../semmle/python/frameworks/FlaskAdmin.qll | 4 +-- .../semmle/python/internal/CachedStages.qll | 2 +- .../lib/semmle/python/objects/Callables.qll | 10 +++--- .../ql/lib/semmle/python/objects/Classes.qll | 5 +-- .../ql/lib/semmle/python/objects/TObject.qll | 2 +- .../lib/semmle/python/pointsto/PointsTo.qll | 14 +++++--- .../lib/semmle/python/types/ClassObject.qll | 7 ++-- .../ql/lib/semmle/python/types/Exceptions.qll | 2 +- .../ql/lib/semmle/python/types/Extensions.qll | 4 +-- .../semmle/python/types/FunctionObject.qll | 2 +- python/ql/lib/semmle/python/types/Object.qll | 6 ++-- python/ql/src/Classes/ClassAttributes.qll | 22 ++++++++----- .../src/Exceptions/CatchingBaseException.ql | 4 ++- python/ql/src/Expressions/CallArgs.qll | 18 ++++++---- .../DuplicateKeyInDictionaryLiteral.ql | 8 +++-- .../Formatting/AdvancedFormatting.qll | 6 ++-- .../Expressions/IncorrectComparisonUsingIs.ql | 2 +- python/ql/src/Expressions/IsComparisons.qll | 2 +- .../ql/src/Expressions/TruncatedDivision.ql | 2 +- .../ql/src/Functions/ExplicitReturnInInit.ql | 4 ++- python/ql/src/Functions/ReturnValueIgnored.ql | 7 +++- python/ql/src/Resources/FileOpen.qll | 4 +-- .../Security/CWE-798/HardcodedCredentials.ql | 2 +- .../Statements/IterableStringOrSequence.ql | 2 +- .../NestedLoopsSameVariableWithReuse.ql | 2 +- .../ql/src/Statements/NonIteratorInForLoop.ql | 2 +- .../ql/src/Statements/SideEffectInAssert.ql | 12 ++++--- python/ql/src/Variables/Definition.qll | 6 +++- .../src/Variables/LeakingListComprehension.ql | 12 ++++--- python/ql/src/Variables/Loop.qll | 7 ++-- python/ql/src/Variables/MultiplyDefined.ql | 4 +-- .../SuspiciousUnusedLoopIterationVariable.ql | 4 ++- python/ql/src/Variables/Undefined.qll | 5 +-- python/ql/src/Variables/UndefinedGlobal.ql | 10 +++--- .../ql/src/Variables/UndefinedPlaceHolder.ql | 4 +-- .../ql/src/Variables/UnusedModuleVariable.ql | 2 +- .../src/analysis/CrossProjectDefinitions.qll | 8 +++-- python/ql/src/analysis/ImportFailure.ql | 7 ++-- python/ql/src/analysis/KeyPointsToFailure.ql | 4 +-- python/ql/src/analysis/PointsToFailure.ql | 2 +- ...onOfParameterWithDefaultCustomizations.qll | 2 +- 60 files changed, 238 insertions(+), 185 deletions(-) diff --git a/python/ql/lib/LegacyPointsTo.qll b/python/ql/lib/LegacyPointsTo.qll index ffea2d93b66c..f5ad67a3c555 100644 --- a/python/ql/lib/LegacyPointsTo.qll +++ b/python/ql/lib/LegacyPointsTo.qll @@ -213,9 +213,11 @@ class ExprWithPointsTo extends Expr { * Gets what this expression might "refer-to" in the given `context`. */ predicate refersTo(Context context, Object obj, ClassObject cls, AstNode origin) { - this.getAFlowNode() - .(ControlFlowNodeWithPointsTo) - .refersTo(context, obj, cls, origin.getAFlowNode()) + exists(ControlFlowNode this_, ControlFlowNode origin_ | + this_.getNode() = this and origin_.getNode() = origin + | + this_.(ControlFlowNodeWithPointsTo).refersTo(context, obj, cls, origin_) + ) } /** @@ -226,7 +228,11 @@ class ExprWithPointsTo extends Expr { */ pragma[nomagic] predicate refersTo(Object obj, AstNode origin) { - this.getAFlowNode().(ControlFlowNodeWithPointsTo).refersTo(obj, origin.getAFlowNode()) + exists(ControlFlowNode this_, ControlFlowNode origin_ | + this_.getNode() = this and origin_.getNode() = origin + | + this_.(ControlFlowNodeWithPointsTo).refersTo(obj, origin_) + ) } /** @@ -240,16 +246,22 @@ class ExprWithPointsTo extends Expr { * in the given `context`. */ predicate pointsTo(Context context, Value value, AstNode origin) { - this.getAFlowNode() - .(ControlFlowNodeWithPointsTo) - .pointsTo(context, value, origin.getAFlowNode()) + exists(ControlFlowNode this_, ControlFlowNode origin_ | + this_.getNode() = this and origin_.getNode() = origin + | + this_.(ControlFlowNodeWithPointsTo).pointsTo(context, value, origin_) + ) } /** * Holds if this expression might "point-to" to `value` which is from `origin`. */ predicate pointsTo(Value value, AstNode origin) { - this.getAFlowNode().(ControlFlowNodeWithPointsTo).pointsTo(value, origin.getAFlowNode()) + exists(ControlFlowNode this_, ControlFlowNode origin_ | + this_.getNode() = this and origin_.getNode() = origin + | + this_.(ControlFlowNodeWithPointsTo).pointsTo(value, origin_) + ) } /** @@ -475,7 +487,10 @@ class FunctionMetricsWithPointsTo extends FunctionMetrics { not non_coupling_method(result) and exists(Call call | call.getScope() = this | exists(FunctionObject callee | callee.getFunction() = result | - call.getAFlowNode().getFunction().(ControlFlowNodeWithPointsTo).refersTo(callee) + exists(CallNode call_ | + call_.getNode() = call and + call_.getFunction().(ControlFlowNodeWithPointsTo).refersTo(callee) + ) ) or exists(Attribute a | call.getFunc() = a | diff --git a/python/ql/lib/analysis/DefinitionTracking.qll b/python/ql/lib/analysis/DefinitionTracking.qll index 21155970375b..583a7807ff27 100644 --- a/python/ql/lib/analysis/DefinitionTracking.qll +++ b/python/ql/lib/analysis/DefinitionTracking.qll @@ -64,7 +64,7 @@ private predicate jump_to_defn(ControlFlowNode use, Definition defn) { private predicate preferred_jump_to_defn(Expr use, Definition def) { not use instanceof ClassExpr and not use instanceof FunctionExpr and - jump_to_defn(use.getAFlowNode(), def) + exists(ControlFlowNode useNode | useNode.getNode() = use | jump_to_defn(useNode, def)) } private predicate unique_jump_to_defn(Expr use, Definition def) { @@ -452,7 +452,7 @@ private predicate self_parameter_jump_to_defn_attribute( * This exists primarily for testing use `getPreferredDefinition()` instead. */ Definition getADefinition(Expr use) { - jump_to_defn(use.getAFlowNode(), result) and + exists(ControlFlowNode useNode | useNode.getNode() = use | jump_to_defn(useNode, result)) and not use instanceof Call and not use.isArtificial() and // Not the use itself diff --git a/python/ql/lib/semmle/python/AstExtended.qll b/python/ql/lib/semmle/python/AstExtended.qll index 13da4e899a71..32b9ce6eee7c 100644 --- a/python/ql/lib/semmle/python/AstExtended.qll +++ b/python/ql/lib/semmle/python/AstExtended.qll @@ -16,17 +16,6 @@ abstract class AstNode extends AstNode_ { /** Gets the scope that this node occurs in */ abstract Scope getScope(); - /** - * Gets a flow node corresponding directly to this node. - * NOTE: For some statements and other purely syntactic elements, - * there may not be a `ControlFlowNode` - */ - cached - ControlFlowNode getAFlowNode() { - Stages::AST::ref() and - py_flow_bb_node(result, this, _, _) - } - /** Gets the location for this AST node */ cached Location getLocation() { none() } diff --git a/python/ql/lib/semmle/python/Exprs.qll b/python/ql/lib/semmle/python/Exprs.qll index 6ab9f8d8340d..6f462f714eb6 100644 --- a/python/ql/lib/semmle/python/Exprs.qll +++ b/python/ql/lib/semmle/python/Exprs.qll @@ -28,7 +28,7 @@ class Expr extends Expr_, AstNode { /** Whether this expression may have a side effect (as determined purely from its syntax) */ predicate hasSideEffects() { /* If an exception raised by this expression handled, count that as a side effect */ - this.getAFlowNode().getASuccessor().getNode() instanceof ExceptStmt + exists(ControlFlowNode n | n.getNode() = this | n.getASuccessor().getNode() instanceof ExceptStmt) or this.getASubExpression().hasSideEffects() } @@ -68,8 +68,6 @@ class Attribute extends Attribute_ { /* syntax: Expr.name */ override Expr getASubExpression() { result = this.getObject() } - override AttrNode getAFlowNode() { result = super.getAFlowNode() } - /** Gets the name of this attribute. That is the `name` in `obj.name` */ string getName() { result = Attribute_.super.getAttr() } @@ -97,7 +95,6 @@ class Subscript extends Subscript_ { Expr getObject() { result = Subscript_.super.getValue() } - override SubscriptNode getAFlowNode() { result = super.getAFlowNode() } } /** A call expression, such as `func(...)` */ @@ -113,7 +110,6 @@ class Call extends Call_ { override string toString() { result = this.getFunc().toString() + "()" } - override CallNode getAFlowNode() { result = super.getAFlowNode() } /** Gets a tuple (*) argument of this call. */ Expr getStarargs() { result = this.getAPositionalArg().(Starred).getValue() } @@ -201,7 +197,6 @@ class IfExp extends IfExp_ { result = this.getTest() or result = this.getBody() or result = this.getOrelse() } - override IfExprNode getAFlowNode() { result = super.getAFlowNode() } } /** A starred expression, such as the `*rest` in the assignment `first, *rest = seq` */ @@ -411,7 +406,6 @@ class PlaceHolder extends PlaceHolder_ { override string toString() { result = "$" + this.getId() } - override NameNode getAFlowNode() { result = super.getAFlowNode() } } /** A tuple expression such as `( 1, 3, 5, 7, 9 )` */ @@ -478,7 +472,6 @@ class Name extends Name_ { override string toString() { result = this.getId() } - override NameNode getAFlowNode() { result = super.getAFlowNode() } override predicate isArtificial() { /* Artificial variable names in comprehensions all start with "." */ @@ -585,7 +578,6 @@ abstract class NameConstant extends Name, ImmutableLiteral { override predicate isConstant() { any() } - override NameConstantNode getAFlowNode() { result = Name.super.getAFlowNode() } override predicate isArtificial() { none() } } diff --git a/python/ql/lib/semmle/python/Flow.qll b/python/ql/lib/semmle/python/Flow.qll index 05a9ab6e17d3..a48fcf7c3e26 100644 --- a/python/ql/lib/semmle/python/Flow.qll +++ b/python/ql/lib/semmle/python/Flow.qll @@ -555,27 +555,27 @@ class DefinitionNode extends ControlFlowNode { cached DefinitionNode() { Stages::AST::ref() and - exists(Py::Assign a | a.getATarget().getAFlowNode() = this) + exists(Py::Assign a | this.getNode() = a.getATarget()) or - exists(Py::AssignExpr a | a.getTarget().getAFlowNode() = this) + exists(Py::AssignExpr a | this.getNode() = a.getTarget()) or - exists(Py::AnnAssign a | a.getTarget().getAFlowNode() = this and exists(a.getValue())) + exists(Py::AnnAssign a | this.getNode() = a.getTarget() and exists(a.getValue())) or - exists(Py::Alias a | a.getAsname().getAFlowNode() = this) + exists(Py::Alias a | this.getNode() = a.getAsname()) or augstore(_, this) or // `x, y = 1, 2` where LHS is a combination of list or tuples - exists(Py::Assign a | list_or_tuple_nested_element(a.getATarget()).getAFlowNode() = this) + exists(Py::Assign a | this.getNode() = list_or_tuple_nested_element(a.getATarget())) or - exists(Py::For for | for.getTarget().getAFlowNode() = this) + exists(Py::For for | this.getNode() = for.getTarget()) or - exists(Py::Parameter param | this = param.asName().getAFlowNode() and exists(param.getDefault())) + exists(Py::Parameter param | this.getNode() = param.asName() and exists(param.getDefault())) } /** flow node corresponding to the value assigned for the definition corresponding to this flow node */ ControlFlowNode getValue() { - result = assigned_value(this.getNode()).getAFlowNode() and + result.getNode() = assigned_value(this.getNode()) and ( result.getBasicBlock().dominates(this.getBasicBlock()) or @@ -584,7 +584,7 @@ class DefinitionNode extends ControlFlowNode { // since the default value for a parameter is evaluated in the same basic block as // the function definition, but the parameter belongs to the basic block of the function, // there is no dominance relationship between the two. - exists(Py::Parameter param | this = param.asName().getAFlowNode()) + exists(Py::Parameter param | this.getNode() = param.asName()) ) } } @@ -901,7 +901,7 @@ class ExceptFlowNode extends ControlFlowNode { exists(Py::ExceptStmt ex | this.getBasicBlock().dominates(result.getBasicBlock()) and ex = this.getNode() and - result = ex.getType().getAFlowNode() + result.getNode() = ex.getType() ) } @@ -913,7 +913,7 @@ class ExceptFlowNode extends ControlFlowNode { exists(Py::ExceptStmt ex | this.getBasicBlock().dominates(result.getBasicBlock()) and ex = this.getNode() and - result = ex.getName().getAFlowNode() + result.getNode() = ex.getName() ) } } @@ -928,7 +928,7 @@ class ExceptGroupFlowNode extends ControlFlowNode { */ ControlFlowNode getType() { this.getBasicBlock().dominates(result.getBasicBlock()) and - result = this.getNode().(Py::ExceptGroupStmt).getType().getAFlowNode() + result.getNode() = this.getNode().(Py::ExceptGroupStmt).getType() } /** @@ -937,7 +937,7 @@ class ExceptGroupFlowNode extends ControlFlowNode { */ ControlFlowNode getName() { this.getBasicBlock().dominates(result.getBasicBlock()) and - result = this.getNode().(Py::ExceptGroupStmt).getName().getAFlowNode() + result.getNode() = this.getNode().(Py::ExceptGroupStmt).getName() } } diff --git a/python/ql/lib/semmle/python/Import.qll b/python/ql/lib/semmle/python/Import.qll index 2f7fae955399..5256403c8b90 100644 --- a/python/ql/lib/semmle/python/Import.qll +++ b/python/ql/lib/semmle/python/Import.qll @@ -163,7 +163,6 @@ class ImportMember extends ImportMember_ { result = this.getModule().(ImportExpr).getImportedModuleName() + "." + this.getName() } - override ImportMemberNode getAFlowNode() { result = super.getAFlowNode() } } /** An import statement */ diff --git a/python/ql/lib/semmle/python/SelfAttribute.qll b/python/ql/lib/semmle/python/SelfAttribute.qll index 90ef2b38401a..364e080dcdd7 100644 --- a/python/ql/lib/semmle/python/SelfAttribute.qll +++ b/python/ql/lib/semmle/python/SelfAttribute.qll @@ -46,20 +46,23 @@ class SelfAttributeRead extends SelfAttribute { } predicate guardedByHasattr() { - exists(Variable var, ControlFlowNode n | - var.getAUse() = this.getObject().getAFlowNode() and + exists(Variable var, ControlFlowNode n, ControlFlowNode this_, ControlFlowNode obj_ | + this_.getNode() = this and obj_.getNode() = this.getObject() + | + var.getAUse() = obj_ and hasattr(n, var.getAUse(), this.getName()) and - n.strictlyDominates(this.getAFlowNode()) + n.strictlyDominates(this_) ) } pragma[noinline] predicate locallyDefined() { - exists(SelfAttributeStore store | - this.getName() = store.getName() and - this.getScope() = store.getScope() + exists(SelfAttributeStore store, ControlFlowNode store_, ControlFlowNode this_ | + store_.getNode() = store and this_.getNode() = this | - store.getAFlowNode().strictlyDominates(this.getAFlowNode()) + this.getName() = store.getName() and + this.getScope() = store.getScope() and + store_.strictlyDominates(this_) ) } } diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll index 7b12c50946e8..70dfc0b785d1 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -392,12 +392,9 @@ predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2; // AST-shape subclasses of ControlFlowNode // // Each class is a thin wrapper around the canonical CFG node for a given -// kind of Python AST node. Methods that take/return CFG nodes delegate to -// the AST and re-resolve back via `Expr.getAFlowNode()` from `Flow.qll` -// while we are in the migration period; once that is gone we will use a -// new-CFG-local resolution. For now, expressions navigated through these -// subclasses are looked up by AST identity, and the dominance constraint -// from the old CFG (`result.getBasicBlock().dominates(this.getBasicBlock())`) +// kind of Python AST node. Methods that take/return CFG nodes look up +// related CFG nodes by AST identity (via `getNode()`), and the dominance +// constraint from the old CFG (`result.getBasicBlock().dominates(this.getBasicBlock())`) // is preserved. // =========================================================================== /** Gets the canonical `ControlFlowNode` for AST expression `e`. */ diff --git a/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll b/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll index fefa30965cec..072098991bb4 100644 --- a/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll +++ b/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll @@ -5,24 +5,30 @@ private import semmle.python.dataflow.new.DataFlow private predicate constCompare(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { exists(CompareNode cn | cn = g | - exists(ImmutableLiteral const, Cmpop op | - op = any(Eq eq) and branch = true - or - op = any(NotEq ne) and branch = false + exists(ImmutableLiteral const, Cmpop op, ControlFlowNode c | + c.getNode() = const and + ( + op = any(Eq eq) and branch = true + or + op = any(NotEq ne) and branch = false + ) | - cn.operands(const.getAFlowNode(), op, node) + cn.operands(c, op, node) or - cn.operands(node, op, const.getAFlowNode()) + cn.operands(node, op, c) ) or - exists(NameConstant const, Cmpop op | - op = any(Is is_) and branch = true - or - op = any(IsNot isn) and branch = false + exists(NameConstant const, Cmpop op, ControlFlowNode c | + c.getNode() = const and + ( + op = any(Is is_) and branch = true + or + op = any(IsNot isn) and branch = false + ) | - cn.operands(const.getAFlowNode(), op, node) + cn.operands(c, op, node) or - cn.operands(node, op, const.getAFlowNode()) + cn.operands(node, op, c) ) or exists(IterableNode const_iterable, Cmpop op | diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll b/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll index 8778ae288667..76d2cb11e144 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll @@ -228,7 +228,7 @@ private class ClassDefinitionAsAttrWrite extends AttrWrite, CfgNode { override Node getValue() { result.asCfgNode() = node.getValue() } - override Node getObject() { result.asCfgNode() = cls.getAFlowNode() } + override Node getObject() { result.asCfgNode().getNode() = cls } override ExprNode getAttributeNameExpr() { none() } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index 1db6c08f5f43..fc0bba6b1353 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -1911,8 +1911,8 @@ abstract class ReturnNode extends Node { class ExtractedReturnNode extends ReturnNode, CfgNode { // See `TaintTrackingImplementation::returnFlowStep` ExtractedReturnNode() { - node = any(Return ret).getValue().getAFlowNode() or - node = any(Yield yield).getAFlowNode() + node.getNode() = any(Return ret).getValue() or + node.getNode() = any(Yield yield) } override ReturnKind getKind() { any() } @@ -1930,7 +1930,7 @@ class ExtractedReturnNode extends ReturnNode, CfgNode { class YieldNodeInContextManagerFunction extends ReturnNode, CfgNode { YieldNodeInContextManagerFunction() { hasContextmanagerDecorator(node.getScope()) and - node = any(Yield yield).getValue().getAFlowNode() + node.getNode() = any(Yield yield).getValue() } override ReturnKind getKind() { any() } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll index fffd0150008e..67963e7cd382 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll @@ -185,8 +185,8 @@ private predicate synthDictSplatArgumentNodeStoreStep( */ predicate yieldStoreStep(Node nodeFrom, Content c, Node nodeTo) { exists(Yield yield | - nodeTo.asCfgNode() = yield.getAFlowNode() and - nodeFrom.asCfgNode() = yield.getValue().getAFlowNode() and + nodeTo.asCfgNode().getNode() = yield and + nodeFrom.asCfgNode().getNode() = yield.getValue() and // TODO: Consider if this will also need to transfer dictionary content // once dictionary comprehensions are supported. c instanceof ListElementContent diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll index 8612d4a253e0..a9d73fe0527f 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll @@ -485,7 +485,7 @@ class ModuleVariableNode extends Node, TModuleVariableNode { /** Gets a node that reads this variable, excluding reads that happen through `from ... import *`. */ Node getALocalRead() { - result.asCfgNode() = var.getALoad().getAFlowNode() and + result.asCfgNode().getNode() = var.getALoad() and not result.getScope() = mod } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll b/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll index fbe05979328c..5d647af09bc3 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll @@ -61,7 +61,9 @@ private module CaptureInput implements Shared::InputSig limit @@ -211,7 +217,7 @@ predicate too_many_args(Call call, Value callable, int limit) { call = func.getAMethodCall().getNode() and limit = func.maxParameters() - 1 or callable instanceof ClassValue and - call.getAFlowNode() = get_a_call(callable) and + exists(ControlFlowNode callCfg | callCfg.getNode() = call | callCfg = get_a_call(callable)) and limit = func.maxParameters() - 1 ) and positional_arg_count_for_call(call, callable) > limit diff --git a/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql b/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql index 166eae635fad..bc9fb968dbb0 100644 --- a/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql +++ b/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql @@ -36,11 +36,13 @@ where exists(string s | dict_key(d, k1, s) and dict_key(d, k2, s) and k1 != k2) and ( exists(BasicBlock b, int i1, int i2 | - k1.getAFlowNode() = b.getNode(i1) and - k2.getAFlowNode() = b.getNode(i2) and + b.getNode(i1).getNode() = k1 and + b.getNode(i2).getNode() = k2 and i1 < i2 ) or - k1.getAFlowNode().getBasicBlock().strictlyDominates(k2.getAFlowNode().getBasicBlock()) + exists(ControlFlowNode k1Cfg, ControlFlowNode k2Cfg | k1Cfg.getNode() = k1 and k2Cfg.getNode() = k2 | + k1Cfg.getBasicBlock().strictlyDominates(k2Cfg.getBasicBlock()) + ) ) select k1, "Dictionary key " + repr(k1) + " is subsequently $@.", k2, "overwritten" diff --git a/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll b/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll index d98286d85faf..a860f96061f4 100644 --- a/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll +++ b/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll @@ -98,16 +98,16 @@ private predicate brace_pair(PossibleAdvancedFormatString fmt, int start, int en } private predicate advanced_format_call(Call format_expr, PossibleAdvancedFormatString fmt, int args) { - exists(CallNode call | call = format_expr.getAFlowNode() | + exists(CallNode call, ControlFlowNode fmtCfg | call.getNode() = format_expr and fmtCfg.getNode() = fmt | call.getFunction().(ControlFlowNodeWithPointsTo).pointsTo(Value::named("format")) and - call.getArg(0).(ControlFlowNodeWithPointsTo).pointsTo(_, fmt.getAFlowNode()) and + call.getArg(0).(ControlFlowNodeWithPointsTo).pointsTo(_, fmtCfg) and args = count(format_expr.getAnArg()) - 1 or call.getFunction() .(AttrNode) .getObject("format") .(ControlFlowNodeWithPointsTo) - .pointsTo(_, fmt.getAFlowNode()) and + .pointsTo(_, fmtCfg) and args = count(format_expr.getAnArg()) ) } diff --git a/python/ql/src/Expressions/IncorrectComparisonUsingIs.ql b/python/ql/src/Expressions/IncorrectComparisonUsingIs.ql index fa0ca14669f6..a7336c625472 100644 --- a/python/ql/src/Expressions/IncorrectComparisonUsingIs.ql +++ b/python/ql/src/Expressions/IncorrectComparisonUsingIs.ql @@ -15,7 +15,7 @@ import python /** Holds if the comparison `comp` uses `is` or `is not` (represented as `op`) to compare its `left` and `right` arguments. */ predicate comparison_using_is(Compare comp, ControlFlowNode left, Cmpop op, ControlFlowNode right) { - exists(CompareNode fcomp | fcomp = comp.getAFlowNode() | + exists(CompareNode fcomp | fcomp.getNode() = comp | fcomp.operands(left, op, right) and (op instanceof Is or op instanceof IsNot) ) diff --git a/python/ql/src/Expressions/IsComparisons.qll b/python/ql/src/Expressions/IsComparisons.qll index cb052ceca765..ee49f6c3337a 100644 --- a/python/ql/src/Expressions/IsComparisons.qll +++ b/python/ql/src/Expressions/IsComparisons.qll @@ -5,7 +5,7 @@ private import LegacyPointsTo /** Holds if the comparison `comp` uses `is` or `is not` (represented as `op`) to compare its `left` and `right` arguments. */ predicate comparison_using_is(Compare comp, ControlFlowNode left, Cmpop op, ControlFlowNode right) { - exists(CompareNode fcomp | fcomp = comp.getAFlowNode() | + exists(CompareNode fcomp | fcomp.getNode() = comp | fcomp.operands(left, op, right) and (op instanceof Is or op instanceof IsNot) ) diff --git a/python/ql/src/Expressions/TruncatedDivision.ql b/python/ql/src/Expressions/TruncatedDivision.ql index c731a21f7d26..d63ac056d3c2 100644 --- a/python/ql/src/Expressions/TruncatedDivision.ql +++ b/python/ql/src/Expressions/TruncatedDivision.ql @@ -19,7 +19,7 @@ where // Only relevant for Python 2, as all later versions implement true division major_version() = 2 and exists(BinaryExprNode bin, Value lval, Value rval | - bin = div.getAFlowNode() and + bin.getNode() = div and bin.getNode().getOp() instanceof Div and bin.getLeft().(ControlFlowNodeWithPointsTo).pointsTo(lval, left) and lval.getClass() = ClassValue::int_() and diff --git a/python/ql/src/Functions/ExplicitReturnInInit.ql b/python/ql/src/Functions/ExplicitReturnInInit.ql index f1300afbfd0a..25fc799fafae 100644 --- a/python/ql/src/Functions/ExplicitReturnInInit.ql +++ b/python/ql/src/Functions/ExplicitReturnInInit.ql @@ -19,7 +19,9 @@ where exists(Function init | init.isInitMethod() and r.getScope() = init) and r.getValue() = rv and not rv.pointsTo(Value::none_()) and - not exists(FunctionValue f | f.getACall() = rv.getAFlowNode() | f.neverReturns()) and + not exists(FunctionValue f, ControlFlowNode rvCfg | rvCfg.getNode() = rv | + f.getACall() = rvCfg and f.neverReturns() + ) and // to avoid double reporting, don't trigger if returning result from other __init__ function not exists(Attribute meth | meth = rv.(Call).getFunc() | meth.getName() = "__init__") select r, "Explicit return in __init__ method." diff --git a/python/ql/src/Functions/ReturnValueIgnored.ql b/python/ql/src/Functions/ReturnValueIgnored.ql index 3716b989d891..83af6304cb30 100644 --- a/python/ql/src/Functions/ReturnValueIgnored.ql +++ b/python/ql/src/Functions/ReturnValueIgnored.ql @@ -69,7 +69,12 @@ where returns_meaningful_value(callee) and not wrapped_in_try_except(call) and exists(int unused | - unused = count(ExprStmt e | e.getValue().getAFlowNode() = callee.getACall()) and + unused = + count(ExprStmt e | + exists(ControlFlowNode eValCfg | eValCfg.getNode() = e.getValue() | + eValCfg = callee.getACall() + ) + ) and total = count(callee.getACall()) | percentage_used = (100.0 * (total - unused) / total).floor() diff --git a/python/ql/src/Resources/FileOpen.qll b/python/ql/src/Resources/FileOpen.qll index dd952e732d42..1daecb6d0334 100644 --- a/python/ql/src/Resources/FileOpen.qll +++ b/python/ql/src/Resources/FileOpen.qll @@ -138,12 +138,12 @@ predicate function_opens_file(FunctionValue f) { f = Value::named("open") or exists(EssaVariable v, Return ret | ret.getScope() = f.getScope() | - ret.getValue().getAFlowNode() = v.getAUse() and + v.getNode() = ret.getValue().getAUse() and var_is_open(v, _) ) or exists(Return ret, FunctionValue callee | ret.getScope() = f.getScope() | - ret.getValue().getAFlowNode() = callee.getACall() and + callee.getNode() = ret.getValue().getACall() and function_opens_file(callee) ) } diff --git a/python/ql/src/Security/CWE-798/HardcodedCredentials.ql b/python/ql/src/Security/CWE-798/HardcodedCredentials.ql index 1e7b4452a9a6..ab21c106348f 100644 --- a/python/ql/src/Security/CWE-798/HardcodedCredentials.ql +++ b/python/ql/src/Security/CWE-798/HardcodedCredentials.ql @@ -94,7 +94,7 @@ class CredentialSink extends DataFlow::Node { this.(DataFlow::ArgumentNode).argumentOf(_, pos) ) or - exists(Keyword k | k.getArg() = name and k.getValue().getAFlowNode() = this.asCfgNode()) + exists(Keyword k | k.getArg() = name and this.getNode() = k.getValue().asCfgNode()) or exists(CompareNode cmp, NameNode n | n.getId() = name | cmp.operands(this.asCfgNode(), any(Eq eq), n) diff --git a/python/ql/src/Statements/IterableStringOrSequence.ql b/python/ql/src/Statements/IterableStringOrSequence.ql index d1c4a507f0d1..ad8b6beab290 100644 --- a/python/ql/src/Statements/IterableStringOrSequence.ql +++ b/python/ql/src/Statements/IterableStringOrSequence.ql @@ -25,7 +25,7 @@ from For loop, ControlFlowNodeWithPointsTo iter, Value str, Value seq, ControlFlowNode seq_origin, ControlFlowNode str_origin where - loop.getIter().getAFlowNode() = iter and + iter.getNode() = loop.getIter() and iter.pointsTo(str, str_origin) and iter.pointsTo(seq, seq_origin) and has_string_type(str) and diff --git a/python/ql/src/Statements/NestedLoopsSameVariableWithReuse.ql b/python/ql/src/Statements/NestedLoopsSameVariableWithReuse.ql index c4deb4e64277..a9c5a5fbbd98 100644 --- a/python/ql/src/Statements/NestedLoopsSameVariableWithReuse.ql +++ b/python/ql/src/Statements/NestedLoopsSameVariableWithReuse.ql @@ -15,7 +15,7 @@ import python predicate loop_variable_ssa(For f, Variable v, SsaVariable s) { - f.getTarget().getAFlowNode() = s.getDefinition() and v = s.getVariable() + s.getDefinition().getNode() = f.getTarget() and v = s.getVariable() } predicate variableUsedInNestedLoops(For inner, For outer, Variable v, Name n) { diff --git a/python/ql/src/Statements/NonIteratorInForLoop.ql b/python/ql/src/Statements/NonIteratorInForLoop.ql index f8e6e51b55ff..b0cbc71130d0 100644 --- a/python/ql/src/Statements/NonIteratorInForLoop.ql +++ b/python/ql/src/Statements/NonIteratorInForLoop.ql @@ -16,7 +16,7 @@ private import LegacyPointsTo from For loop, ControlFlowNodeWithPointsTo iter, Value v, ClassValue t, ControlFlowNode origin where - loop.getIter().getAFlowNode() = iter and + iter.getNode() = loop.getIter() and iter.pointsTo(_, v, origin) and v.getClass() = t and not t.isIterable() and diff --git a/python/ql/src/Statements/SideEffectInAssert.ql b/python/ql/src/Statements/SideEffectInAssert.ql index 7ac96030c04e..55c34144dced 100644 --- a/python/ql/src/Statements/SideEffectInAssert.ql +++ b/python/ql/src/Statements/SideEffectInAssert.ql @@ -24,11 +24,13 @@ predicate func_with_side_effects(Expr e) { } predicate call_with_side_effect(Call e) { - e.getAFlowNode() = - API::moduleImport("subprocess") - .getMember(["call", "check_call", "check_output"]) - .getACall() - .asCfgNode() + exists(ControlFlowNode eCfg | eCfg.getNode() = e | + eCfg = + API::moduleImport("subprocess") + .getMember(["call", "check_call", "check_output"]) + .getACall() + .asCfgNode() + ) } predicate probable_side_effect(Expr e) { diff --git a/python/ql/src/Variables/Definition.qll b/python/ql/src/Variables/Definition.qll index be8c9490788c..9bd7130957b6 100644 --- a/python/ql/src/Variables/Definition.qll +++ b/python/ql/src/Variables/Definition.qll @@ -133,7 +133,11 @@ class ListComprehensionDeclaration extends ListComp { major_version() = 2 and this.getIterationVariable(_).getId() = result.getId() and result.getScope() = this.getScope() and - this.getAFlowNode().strictlyReaches(result.getAFlowNode()) and + exists(ControlFlowNode thisCfg, ControlFlowNode resultCfg | + thisCfg.getNode() = this and resultCfg.getNode() = result + | + thisCfg.strictlyReaches(resultCfg) + ) and result.isUse() } diff --git a/python/ql/src/Variables/LeakingListComprehension.ql b/python/ql/src/Variables/LeakingListComprehension.ql index 9b98fb43a313..34bf26a35559 100644 --- a/python/ql/src/Variables/LeakingListComprehension.ql +++ b/python/ql/src/Variables/LeakingListComprehension.ql @@ -13,18 +13,20 @@ import python import Definition -from ListComprehensionDeclaration l, Name use, Name defn +from ListComprehensionDeclaration l, Name use, Name defn, ControlFlowNode lCfg, ControlFlowNode useCfg where use = l.getALeakedVariableUse() and defn = l.getDefinition() and - l.getAFlowNode().strictlyReaches(use.getAFlowNode()) and + lCfg.getNode() = l and + useCfg.getNode() = use and + lCfg.strictlyReaches(useCfg) and /* Make sure we aren't in a loop, as the variable may be redefined */ - not use.getAFlowNode().strictlyReaches(l.getAFlowNode()) and + not useCfg.strictlyReaches(lCfg) and not l.contains(use) and not use.deletes(_) and not exists(SsaVariable v | - v.getAUse() = use.getAFlowNode() and - not v.getDefinition().strictlyDominates(l.getAFlowNode()) + v.getAUse() = useCfg and + not v.getDefinition().strictlyDominates(lCfg) ) select use, use.getId() + " may have a different value in Python 3, as the $@ will not be in scope.", defn, diff --git a/python/ql/src/Variables/Loop.qll b/python/ql/src/Variables/Loop.qll index c7749fe476bf..e7c189cac354 100644 --- a/python/ql/src/Variables/Loop.qll +++ b/python/ql/src/Variables/Loop.qll @@ -26,8 +26,11 @@ private Stmt loop_probably_defines(Variable v) { /** Holds if the variable used by `use` is probably defined in a loop */ predicate probably_defined_in_loop(Name use) { - exists(Stmt loop | loop = loop_probably_defines(use.getVariable()) | - loop.getAFlowNode().strictlyReaches(use.getAFlowNode()) + exists(Stmt loop, ControlFlowNode loopCfg, ControlFlowNode useCfg | + loop = loop_probably_defines(use.getVariable()) and + loopCfg.getNode() = loop and + useCfg.getNode() = use and + loopCfg.strictlyReaches(useCfg) ) } diff --git a/python/ql/src/Variables/MultiplyDefined.ql b/python/ql/src/Variables/MultiplyDefined.ql index 3c26ff0b1eb1..ce8b5b316c21 100644 --- a/python/ql/src/Variables/MultiplyDefined.ql +++ b/python/ql/src/Variables/MultiplyDefined.ql @@ -24,8 +24,8 @@ predicate multiply_defined(AstNode asgn1, AstNode asgn2, Variable v) { forex(Definition def, Definition redef | def.getVariable() = v and - def = asgn1.getAFlowNode() and - redef = asgn2.getAFlowNode() + def.getNode() = asgn1 and + redef.getNode() = asgn2 | def.isUnused() and def.getARedef() = redef and diff --git a/python/ql/src/Variables/SuspiciousUnusedLoopIterationVariable.ql b/python/ql/src/Variables/SuspiciousUnusedLoopIterationVariable.ql index d252742d67c2..f74fd4970ee4 100644 --- a/python/ql/src/Variables/SuspiciousUnusedLoopIterationVariable.ql +++ b/python/ql/src/Variables/SuspiciousUnusedLoopIterationVariable.ql @@ -88,7 +88,9 @@ predicate implicit_repeat(For f) { * E.g. gets `x` from `{ y for y in x }`. */ ControlFlowNode get_comp_iterable(For f) { - exists(Comp c | c.getFunction().getStmt(0) = f | c.getAFlowNode().getAPredecessor() = result) + exists(Comp c, ControlFlowNode cCfg | + c.getFunction().getStmt(0) = f and cCfg.getNode() = c and cCfg.getAPredecessor() = result + ) } from For f, Variable v, string msg diff --git a/python/ql/src/Variables/Undefined.qll b/python/ql/src/Variables/Undefined.qll index 42437a81340b..b320c2040b2d 100644 --- a/python/ql/src/Variables/Undefined.qll +++ b/python/ql/src/Variables/Undefined.qll @@ -19,9 +19,10 @@ private predicate loop_entry_variables(EssaVariable pred, EssaVariable succ) { private predicate loop_entry_edge(BasicBlock pred, BasicBlock loop) { pred = loop.getAPredecessor() and pred = loop.getImmediateDominator() and - exists(Stmt s | + exists(Stmt s, ControlFlowNode sCfg | loop_probably_executes_at_least_once(s) and - s.getAFlowNode().getBasicBlock() = loop + sCfg.getNode() = s and + sCfg.getBasicBlock() = loop ) } diff --git a/python/ql/src/Variables/UndefinedGlobal.ql b/python/ql/src/Variables/UndefinedGlobal.ql index 404ac64aa5a0..0c54b444ce30 100644 --- a/python/ql/src/Variables/UndefinedGlobal.ql +++ b/python/ql/src/Variables/UndefinedGlobal.ql @@ -27,7 +27,7 @@ predicate guarded_against_name_error(Name u) { | globals.getFunc().(Name).getId() = "globals" and guard.controls(controlled, _) and - controlled.contains(u.getAFlowNode()) + exists(ControlFlowNode uCfg | uCfg.getNode() = u | controlled.contains(uCfg)) ) } @@ -101,18 +101,18 @@ predicate undefined_use(Name u) { } private predicate first_use_in_a_block(Name use) { - exists(GlobalVariable v, BasicBlock b, int i | - i = min(int j | b.getNode(j).getNode() = v.getALoad()) and b.getNode(i) = use.getAFlowNode() + exists(GlobalVariable v, BasicBlock b, int i, ControlFlowNode useCfg | useCfg.getNode() = use | + i = min(int j | b.getNode(j).getNode() = v.getALoad()) and b.getNode(i) = useCfg ) } predicate first_undefined_use(Name use) { undefined_use(use) and - exists(GlobalVariable v | v.getALoad() = use | + exists(GlobalVariable v, ControlFlowNode useCfg | v.getALoad() = use and useCfg.getNode() = use | first_use_in_a_block(use) and not exists(ControlFlowNode other | other.getNode() = v.getALoad() and - other.getBasicBlock().strictlyDominates(use.getAFlowNode().getBasicBlock()) + other.getBasicBlock().strictlyDominates(useCfg.getBasicBlock()) ) ) } diff --git a/python/ql/src/Variables/UndefinedPlaceHolder.ql b/python/ql/src/Variables/UndefinedPlaceHolder.ql index 29f9b3a1a510..9fa0cc7eaaae 100644 --- a/python/ql/src/Variables/UndefinedPlaceHolder.ql +++ b/python/ql/src/Variables/UndefinedPlaceHolder.ql @@ -18,8 +18,8 @@ private import semmle.python.types.ImportTime /* Local variable part */ predicate initialized_as_local(PlaceHolder use) { - exists(SsaVariableWithPointsTo l, Function f | - f = use.getScope() and l.getAUse() = use.getAFlowNode() + exists(SsaVariableWithPointsTo l, Function f, ControlFlowNode useCfg | + f = use.getScope() and useCfg.getNode() = use and l.getAUse() = useCfg | l.getVariable() instanceof LocalVariable and not l.maybeUndefined() diff --git a/python/ql/src/Variables/UnusedModuleVariable.ql b/python/ql/src/Variables/UnusedModuleVariable.ql index 24d6559d6fea..0443c3388c85 100644 --- a/python/ql/src/Variables/UnusedModuleVariable.ql +++ b/python/ql/src/Variables/UnusedModuleVariable.ql @@ -54,7 +54,7 @@ predicate unused_global(Name unused, GlobalVariable v) { u.uses(v) | // That is reachable from this definition, directly - defn.strictlyReaches(u.getAFlowNode()) + exists(ControlFlowNode uCfg | uCfg.getNode() = u | defn.strictlyReaches(uCfg)) or // indirectly defn.getBasicBlock().reachesExit() and u.getScope() != unused.getScope() diff --git a/python/ql/src/analysis/CrossProjectDefinitions.qll b/python/ql/src/analysis/CrossProjectDefinitions.qll index 64b30f566f15..61e12a09ec6b 100644 --- a/python/ql/src/analysis/CrossProjectDefinitions.qll +++ b/python/ql/src/analysis/CrossProjectDefinitions.qll @@ -48,15 +48,17 @@ class Symbol extends TSymbol { AstNode find() { this = TModule(result) or - exists(Symbol s, string name | this = TMember(s, name) | + exists(Symbol s, string name, ControlFlowNode resultCfg | + this = TMember(s, name) and resultCfg.getNode() = result + | exists(ClassObject cls | s.resolvesTo() = cls and - cls.attributeRefersTo(name, _, result.getAFlowNode()) + cls.attributeRefersTo(name, _, resultCfg) ) or exists(ModuleObject m | s.resolvesTo() = m and - m.attributeRefersTo(name, _, result.getAFlowNode()) + m.attributeRefersTo(name, _, resultCfg) ) ) } diff --git a/python/ql/src/analysis/ImportFailure.ql b/python/ql/src/analysis/ImportFailure.ql index 71967e6e04f7..760a3693d6ea 100644 --- a/python/ql/src/analysis/ImportFailure.ql +++ b/python/ql/src/analysis/ImportFailure.ql @@ -80,10 +80,11 @@ class VersionGuard extends ConditionBlock { VersionGuard() { this.getLastNode() instanceof VersionTest } } -from ImportExpr ie +from ImportExpr ie, ControlFlowNode ieCfg where + ieCfg.getNode() = ie and not ie.(ExprWithPointsTo).refersTo(_) and - exists(Context c | c.appliesTo(ie.getAFlowNode())) and + exists(Context c | c.appliesTo(ieCfg)) and not ok_to_fail(ie) and - not exists(VersionGuard guard | guard.controls(ie.getAFlowNode().getBasicBlock(), _)) + not exists(VersionGuard guard | guard.controls(ieCfg.getBasicBlock(), _)) select ie, "Unable to resolve import of '" + ie.getImportedModuleName() + "'." diff --git a/python/ql/src/analysis/KeyPointsToFailure.ql b/python/ql/src/analysis/KeyPointsToFailure.ql index f07e8638f385..e42e5ac0bdd4 100644 --- a/python/ql/src/analysis/KeyPointsToFailure.ql +++ b/python/ql/src/analysis/KeyPointsToFailure.ql @@ -11,13 +11,13 @@ import python import semmle.python.pointsto.PointsTo predicate points_to_failure(Expr e) { - exists(ControlFlowNode f | f = e.getAFlowNode() | not PointsTo::pointsTo(f, _, _, _)) + exists(ControlFlowNode f | f.getNode() = e | not PointsTo::pointsTo(f, _, _, _)) } predicate key_points_to_failure(Expr e) { points_to_failure(e) and not points_to_failure(e.getASubExpression()) and - not exists(SsaVariable ssa | ssa.getAUse() = e.getAFlowNode() | + not exists(SsaVariable ssa, ControlFlowNode eCfg | eCfg.getNode() = e and ssa.getAUse() = eCfg | points_to_failure(ssa.getAnUltimateDefinition().getDefinition().getNode()) ) and not exists(Assign a | a.getATarget() = e) diff --git a/python/ql/src/analysis/PointsToFailure.ql b/python/ql/src/analysis/PointsToFailure.ql index fee1e80d2f77..8d46cbd90952 100644 --- a/python/ql/src/analysis/PointsToFailure.ql +++ b/python/ql/src/analysis/PointsToFailure.ql @@ -12,5 +12,5 @@ import python private import LegacyPointsTo from Expr e -where exists(ControlFlowNodeWithPointsTo f | f = e.getAFlowNode() | not f.refersTo(_)) +where exists(ControlFlowNodeWithPointsTo f | f.getNode() = e | not f.refersTo(_)) select e, "Expression does not 'point-to' any object." diff --git a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll index c7aef20c09dd..83ba4df4e298 100644 --- a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll +++ b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll @@ -131,7 +131,7 @@ module ModificationOfParameterWithDefault { exists(DeletionNode d | d.getTarget().(SubscriptNode).getObject() = this.asCfgNode()) or // augmented assignment to the value - exists(AugAssign a | a.getTarget().getAFlowNode() = this.asCfgNode()) + exists(AugAssign a | this.asCfgNode().getNode() = a.getTarget()) or // modifying function call exists(DataFlow::CallCfgNode c, DataFlow::AttrRead a | c.getFunction() = a | From 26df3945d3e6a579160fe5dc1800197749ce8b00 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 07:20:44 +0000 Subject: [PATCH 64/72] Python: migrate dataflow library to new CFG + shared SSA Switches the trunk dataflow library and all in-tree consumers (frameworks, ApiGraphs, Concepts, regexp, security customisations, test harness) from the legacy Flow.qll/ESSA stack to the new shared-CFG facade (Cfg.qll) and the ESSA-shaped adapter on the shared-SSA library (SsaImpl.qll). Highlights: * DataFlowPublic/Private/Dispatch, Attributes, VariableCapture, IterableUnpacking, ImportResolution, ImportStar, LocalSources, TaintTrackingPrivate, MatchUnpacking, TypeTrackingImpl, SsaImpl, Builtins all now qualify CFG/SSA references with Cfg:: / SsaImpl:: and stop pulling in semmle.python.essa.*. * AstNodeImpl.qll/Cfg.qll: ImportMember exposes its inner ImportExpr, DefinitionNode.getValue covers Alias / AnnAssign / AugAssign / AssignExpr / For-target / Parameter-default, ForNode is treated as an expression node, AnnotatedExitNode is canonical, and BoolExprNode.getAnOperand drops the dominance constraint that did not hold for short-circuit BBs. * SsaImpl.qll: parameters always get a ParameterDefinition (so unused parameters still have SSA defs), scope-entry defs for module globals require an actual store somewhere, scope-exit has a synthetic use so reaching-defs survives to module boundary, and the legacy SsaSourceVariable / EssaVariable surface (getName, getScope, getAUse, getASourceUse, getAnImplicitUse) is reinstated for downstream queries. * DataFlowPublic.qll: GuardNode redesigned around the new structural outcome nodes (isAfterTrue / isAfterFalse). The legacy ConditionBlock + flipped indirection is gone; controlsBlock walks UP through 'not' / '==True' / 'is False' etc. via outcomeOfGuard, accumulating polarity cleanly. Only BarrierGuard<...> is preserved as public API. * ModuleVariableNode.getAWrite and LocalFlow::definitionFlowStep bypass SSA and consult Cfg::NameNode.defines / Cfg::DefinitionNode.getValue directly, so that write defs pruned by shared SSA (because the variable has no in-scope read) still produce dataflow steps. * Frameworks + downstream consumers: replace EssaVariable.hasDefiningNode, getAReturnValueFlowNode, Parameter.getDefault, Scope.getEntryNode / getANormalExit etc. with CFG-side bridges through Cfg::ControlFlowNode. The legacy Flow.qll / Essa.qll stack is untouched and remains available for queries that import it directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/ql/lib/semmle/python/ApiGraphs.qll | 13 +- python/ql/lib/semmle/python/Concepts.qll | 5 +- .../controlflow/internal/AstNodeImpl.qll | 29 +++ .../python/controlflow/internal/Cfg.qll | 147 +++++++++-- .../python/dataflow/new/BarrierGuards.qll | 13 +- .../dataflow/new/SensitiveDataSources.qll | 11 +- .../dataflow/new/internal/Attributes.qll | 29 ++- .../python/dataflow/new/internal/Builtins.qll | 5 +- .../new/internal/DataFlowDispatch.qll | 115 +++++---- .../dataflow/new/internal/DataFlowPrivate.qll | 83 +++--- .../dataflow/new/internal/DataFlowPublic.qll | 244 ++++++++++-------- .../new/internal/ImportResolution.qll | 54 ++-- .../dataflow/new/internal/ImportStar.qll | 11 +- .../new/internal/IterableUnpacking.qll | 32 +-- .../dataflow/new/internal/LocalSources.qll | 3 +- .../dataflow/new/internal/MatchUnpacking.qll | 9 +- .../python/dataflow/new/internal/SsaImpl.qll | 165 +++++++++--- .../new/internal/TaintTrackingPrivate.qll | 20 +- .../new/internal/TypeTrackingImpl.qll | 21 +- .../dataflow/new/internal/VariableCapture.qll | 32 ++- .../lib/semmle/python/frameworks/Bottle.qll | 9 +- .../lib/semmle/python/frameworks/Django.qll | 23 +- .../lib/semmle/python/frameworks/FastApi.qll | 11 +- .../ql/lib/semmle/python/frameworks/Flask.qll | 2 +- .../lib/semmle/python/frameworks/Gradio.qll | 17 +- .../semmle/python/frameworks/MarkupSafe.qll | 7 +- .../lib/semmle/python/frameworks/Pycurl.qll | 5 +- .../lib/semmle/python/frameworks/Pydantic.qll | 3 +- .../lib/semmle/python/frameworks/Pyramid.qll | 4 +- .../lib/semmle/python/frameworks/Stdlib.qll | 37 +-- .../python/frameworks/Stdlib/Urllib.qll | 3 +- .../lib/semmle/python/frameworks/Tornado.qll | 19 +- .../lib/semmle/python/frameworks/Twisted.qll | 4 +- .../lib/semmle/python/frameworks/Werkzeug.qll | 5 +- .../ql/lib/semmle/python/frameworks/Yaml.qll | 3 +- .../ql/lib/semmle/python/frameworks/Yarl.qll | 3 +- .../python/regexp/internal/ParseRegExp.qll | 3 +- .../dataflow/UrlRedirectCustomizations.qll | 3 +- .../utils/test/dataflow/MaximalFlowTest.qll | 7 +- .../test/dataflow/NormalDataflowTest.qll | 3 +- .../test/dataflow/NormalTaintTrackingTest.qll | 3 +- .../lib/utils/test/dataflow/RoutingTest.qll | 3 +- .../utils/test/dataflow/UnresolvedCalls.qll | 7 +- .../ql/lib/utils/test/dataflow/testConfig.qll | 5 +- .../utils/test/dataflow/testTaintConfig.qll | 7 +- .../experimental/meta/InlineTaintTest.qll | 11 +- .../dataflow/summaries/TestSummaries.qll | 13 +- .../dataflow/tainttracking/TestTaintLib.qll | 11 +- .../typetracking-summaries/TestSummaries.qll | 17 +- 49 files changed, 821 insertions(+), 468 deletions(-) diff --git a/python/ql/lib/semmle/python/ApiGraphs.qll b/python/ql/lib/semmle/python/ApiGraphs.qll index efd8141efc6e..eaa31ed3d6c5 100644 --- a/python/ql/lib/semmle/python/ApiGraphs.qll +++ b/python/ql/lib/semmle/python/ApiGraphs.qll @@ -6,8 +6,9 @@ * directed and labeled; they specify how the components represented by nodes relate to each other. */ -// Importing python under the `py` namespace to avoid importing `CallNode` from `Flow.qll` and thereby having a naming conflict with `API::CallNode`. +// Importing python under the `py` namespace to avoid importing `Cfg::CallNode` from `Flow.qll` and thereby having a naming conflict with `API::CallNode`. private import python as PY +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow private import semmle.python.internal.CachedStages @@ -282,7 +283,7 @@ module API { index = this.getIndex() and ( // subscripting - exists(PY::SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | subscript.getObject() = this.getAValueReachableFromSource().asCfgNode() and subscript.getIndex() = index.asSink().asCfgNode() | @@ -290,7 +291,7 @@ module API { subscript = result.asSource().asCfgNode() or // writing - subscript.(PY::DefinitionNode).getValue() = result.asSink().asCfgNode() + subscript.(Cfg::DefinitionNode).getValue() = result.asSink().asCfgNode() ) or // dictionary literals @@ -684,7 +685,7 @@ module API { * Ignores relative imports, such as `from ..foo.bar import baz`. */ private predicate imports(DataFlow::CfgNode imp, string name) { - exists(PY::ImportExprNode iexpr | + exists(Cfg::ImportExprNode iexpr | imp.getNode() = iexpr and not iexpr.getNode().isRelative() and name = iexpr.getNode().getImportedModuleName() @@ -775,7 +776,7 @@ module API { // list literals, from `x` to `[x]` // TODO: once convenient, this should be done at a higher level than the AST, // at least at the CFG layer, to take splitting into account. - // Also consider `SequenceNode for generality. + // Also consider `Cfg::SequenceNode for generality. exists(PY::List list | list = pred.(DataFlow::ExprNode).getNode().getNode() | rhs.(DataFlow::ExprNode).getNode().getNode() = list.getAnElt() and lbl = Label::subscript() @@ -805,7 +806,7 @@ module API { subscript = trackUseNode(src).getSubscript(index) | // from `x` to a definition of `x[...]` - rhs.asCfgNode() = subscript.asCfgNode().(PY::DefinitionNode).getValue() and + rhs.asCfgNode() = subscript.asCfgNode().(Cfg::DefinitionNode).getValue() and lbl = Label::subscript() or // from `x` to `"key"` in `x["key"]` diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll index 76e9f4bd13f9..6eb74b52121d 100644 --- a/python/ql/lib/semmle/python/Concepts.qll +++ b/python/ql/lib/semmle/python/Concepts.qll @@ -5,6 +5,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowImplSpecific private import semmle.python.dataflow.new.RemoteFlowSources @@ -214,7 +215,7 @@ module Path { SafeAccessCheck() { this = DataFlow::BarrierGuard::getABarrierNode() } } - private predicate safeAccessCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { + private predicate safeAccessCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { g.(SafeAccessCheck::Range).checks(node, branch) } @@ -223,7 +224,7 @@ module Path { /** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */ abstract class Range extends DataFlow::GuardNode { /** Holds if this guard validates `node` upon evaluating to `branch`. */ - abstract predicate checks(ControlFlowNode node, boolean branch); + abstract predicate checks(Cfg::ControlFlowNode node, boolean branch); } } } diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d7e54e64aa8b..e41d82a9ad0f 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -10,6 +10,9 @@ * - Intermediate nodes for multi-operand boolean expressions. */ +overlay[local?] +module; + private import python as Py private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType @@ -1193,6 +1196,30 @@ module Ast implements AstSig { override AstNode getChild(int index) { index = 0 and result = this.getObject() } } + /** + * An `import x.y` module expression. Modelled as a leaf — the dotted + * name is just a string. + */ + additional class ImportExpression extends Expr { + ImportExpression() { this.asExpr() instanceof Py::ImportExpr } + } + + /** + * A `from m import x` member access. The module sub-expression is a + * child so that the CFG visits both the module load and this + * attribute selection. + */ + additional class ImportMemberExpr extends Expr { + private Py::ImportMember im; + + ImportMemberExpr() { this = TPyExpr(im) } + + /** Gets the module expression `m` in `from m import x`. */ + Expr getModule() { result.asExpr() = im.getModule() } + + override AstNode getChild(int index) { index = 0 and result = this.getModule() } + } + /** A tuple literal. */ additional class TupleExpr extends Expr { private Py::Tuple tuple; @@ -1581,4 +1608,6 @@ Py::AstNode astNodeToPyNode(Ast::AstNode n) { result = n.asStmt() or result = n.asScope() + or + result = n.asPattern() } diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll index 70dfc0b785d1..66f36771f485 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -20,6 +20,24 @@ module; private import python as Py private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl private import codeql.controlflow.SuccessorType +private import codeql.controlflow.BasicBlock as BB + +/** + * A nested sub-module that explicitly implements `BB::CfgSig`, so this + * `Cfg` facade can be passed to parameterised shared modules such as + * `codeql.dataflow.VariableCapture::Flow`. The sub-module + * exposes the *raw* shared-CFG types from `AstNodeImpl.qll` (where the + * signature is satisfied natively), not the facade's wrapped types. + */ +module CfgSigImpl implements BB::CfgSig { + class ControlFlowNode = CfgImpl::ControlFlowNode; + + class BasicBlock = CfgImpl::BasicBlock; + + class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock; + + predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2; +} /** * Gets the Python AST node corresponding to CFG node `n`, if any. @@ -51,6 +69,11 @@ private predicate isCanonical(CfgImpl::ControlFlowNode n) { n instanceof CfgImpl::ControlFlow::EntryNode or n instanceof CfgImpl::ControlFlow::ExitNode + or + // Annotated exit nodes (normal + abnormal) — needed so that dataflow + // consumers can ask "is this the normal-exit of a scope?" and also + // so that scope-exit synthetic uses in SsaImpl can attach here. + n instanceof CfgImpl::ControlFlow::AnnotatedExitNode } /** @@ -369,6 +392,35 @@ class BasicBlock extends CfgImpl::BasicBlock { or forex(BasicBlock immsucc | immsucc = super.getASuccessor() | immsucc.alwaysReaches(succ)) } + + /** + * Holds if this basic block ends in a node that branches on a boolean + * outcome, and `other` is dominated by the corresponding successor + * for `branch` while not being reachable from the other branch + * without going through this BB. + * + * In other words: any execution that reaches `other` must have just + * evaluated the last node of this BB and taken the `branch` outcome. + * This mirrors the legacy `ConditionBlock.controls(BB, branch)`. + */ + predicate controls(BasicBlock other, boolean branch) { + exists(BasicBlock succ | + branch = true and succ = this.getATrueSuccessor() + or + branch = false and succ = this.getAFalseSuccessor() + | + succ.dominates(other) and + // The other branch must not also reach `other` — otherwise + // `other` is not actually controlled by `branch`. + not exists(BasicBlock otherSucc | + branch = true and otherSucc = this.getAFalseSuccessor() + or + branch = false and otherSucc = this.getATrueSuccessor() + | + otherSucc.reaches(other) + ) + ) + } } // =========================================================================== @@ -734,8 +786,7 @@ class BoolExprNode extends ControlFlowNode { ControlFlowNode getAnOperand() { exists(Py::BoolExpr be | be = toAst(this) and - be.getAValue() = toAst(result) and - result.getBasicBlock().dominates(this.getBasicBlock()) + be.getAValue() = toAst(result) ) } } @@ -766,20 +817,79 @@ class DefinitionNode extends ControlFlowNode { /** Gets the value assigned, if any. */ ControlFlowNode getValue() { - exists(Py::Expr target, Py::Expr value | - target = toAst(this) and - value = toAst(result) and - result.getBasicBlock().dominates(this.getBasicBlock()) - | - // x = value - exists(Py::Assign a | a.getATarget() = target and a.getValue() = value) - or - // x = y = value (nested chained-assign target) - exists(Py::Assign a | a.getATarget().(Py::Tuple).getElt(_) = target and a.getValue() = value) + // For-target: the value is the for-loop's iter expression (which + // is also where `Cfg::ForNode` lives — its `getNode()` returns the + // enclosing `Py::For` statement). Treated specially because there + // is no AST node holding the result of `iter(next(seq))`; we use + // the iter expression's CFG node as the stand-in. + exists(Py::For f | + f.getTarget() = toAst(this) and + toAst(result) = f.getIter() + ) + or + exists(Py::AstNode value | value = assignedValue(toAst(this)) | + toAst(result) = value and + ( + result.getBasicBlock().dominates(this.getBasicBlock()) + or + result.isImport() + or + // The default value for a parameter is evaluated in the same basic block as + // the function definition, but the parameter belongs to the basic block of the + // function, so there is no dominance relationship between the two. + exists(Py::Parameter param | toAst(this) = param.asName()) + ) ) } } +/** + * Gets the AST node that holds the value assigned to `lhs` in a binding + * context. Mirrors `Flow.qll::assigned_value`. + */ +private Py::AstNode assignedValue(Py::Expr lhs) { + // lhs = result + exists(Py::Assign a | a.getATarget() = lhs and result = a.getValue()) + or + // lhs := result + exists(Py::AssignExpr a | a.getTarget() = lhs and result = a.getValue()) + or + // lhs: annotation = result + exists(Py::AnnAssign a | a.getTarget() = lhs and result = a.getValue()) + or + // import result as lhs (also covers plain `import lhs`, where alias.getAsname() = lhs) + exists(Py::Alias a | a.getAsname() = lhs and result = a.getValue()) + or + // lhs += x -> result is the (lhs + x) binary expression + exists(Py::AugAssign a, Py::BinaryExpr b | + b = a.getOperation() and result = b and lhs = b.getLeft() + ) + or + // Nested sequence assign: ..., lhs, ... = ..., result, ... + exists(Py::Assign a | nestedSequenceAssign(a.getATarget(), a.getValue(), lhs, result)) + or + // Parameter default + exists(Py::Parameter param | lhs = param.asName() and result = param.getDefault()) +} + +/** + * Helper for nested sequence assignments such as `(a, b), c = (1, 2), 3`. + */ +private predicate nestedSequenceAssign( + Py::Expr leftParent, Py::Expr rightParent, Py::Expr left, Py::Expr right +) { + exists(int i | + leftParent.(Py::Tuple).getElt(i) = left and rightParent.(Py::Tuple).getElt(i) = right + or + leftParent.(Py::List).getElt(i) = left and rightParent.(Py::List).getElt(i) = right + ) + or + exists(Py::Expr leftMid, Py::Expr rightMid | + nestedSequenceAssign(leftParent, rightParent, leftMid, rightMid) and + nestedSequenceAssign(leftMid, rightMid, left, right) + ) +} + /** A control flow node corresponding to a deletion (`del x`). */ class DeletionNode extends ControlFlowNode { DeletionNode() { this.isDelete() } @@ -789,8 +899,6 @@ class DeletionNode extends ControlFlowNode { class ForNode extends ControlFlowNode { ForNode() { exists(Py::For f | toAst(this) = f.getIter()) } - override Py::For getNode() { exists(Py::For f | toAst(this) = f.getIter() | result = f) } - /** Gets the iterable expression. */ ControlFlowNode getIter() { result = this and result = result // canonical "after" of the iterable @@ -943,8 +1051,15 @@ class DictNode extends ControlFlowNode { /** A control flow node corresponding to an iterable in a `for` loop. */ class IterableNode extends ControlFlowNode { IterableNode() { - exists(Py::For f | toAst(this) = f.getIter()) + this instanceof SequenceNode + or + this instanceof SetNode + } + + /** Gets the control flow node for an element of this iterable. */ + ControlFlowNode getAnElement() { + result = this.(SequenceNode).getAnElement() or - exists(Py::Comp c | toAst(this) = c.getIterable()) + result = this.(SetNode).getAnElement() } } diff --git a/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll b/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll index 072098991bb4..39e8d40fd172 100644 --- a/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll +++ b/python/ql/lib/semmle/python/dataflow/new/BarrierGuards.qll @@ -1,11 +1,12 @@ /** Provides commonly used BarrierGuards. */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow -private predicate constCompare(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { - exists(CompareNode cn | cn = g | - exists(ImmutableLiteral const, Cmpop op, ControlFlowNode c | +private predicate constCompare(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { + exists(Cfg::CompareNode cn | cn = g | + exists(ImmutableLiteral const, Cmpop op, Cfg::ControlFlowNode c | c.getNode() = const and ( op = any(Eq eq) and branch = true @@ -18,7 +19,7 @@ private predicate constCompare(DataFlow::GuardNode g, ControlFlowNode node, bool cn.operands(node, op, c) ) or - exists(NameConstant const, Cmpop op, ControlFlowNode c | + exists(NameConstant const, Cmpop op, Cfg::ControlFlowNode c | c.getNode() = const and ( op = any(Is is_) and branch = true @@ -31,12 +32,12 @@ private predicate constCompare(DataFlow::GuardNode g, ControlFlowNode node, bool cn.operands(node, op, c) ) or - exists(IterableNode const_iterable, Cmpop op | + exists(Cfg::IterableNode const_iterable, Cmpop op | op = any(In in_) and branch = true or op = any(NotIn ni) and branch = false | - forall(ControlFlowNode elem | elem = const_iterable.getAnElement() | + forall(Cfg::ControlFlowNode elem | elem = const_iterable.getAnElement() | elem.getNode() instanceof ImmutableLiteral ) and cn.operands(node, op, const_iterable) diff --git a/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll b/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll index 1a32965d08d7..644f88e311dd 100644 --- a/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll +++ b/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow // Need to import `semmle.python.Frameworks` since frameworks can extend `SensitiveDataSource::Range` private import semmle.python.Frameworks @@ -105,7 +106,7 @@ private module SensitiveDataModeling { or // to cover functions that we don't have the definition for, and where the // reference to the function has not already been marked as being sensitive - this.getFunction().asCfgNode().(NameNode).getId() = sensitiveString(classification) + this.getFunction().asCfgNode().(Cfg::NameNode).getId() = sensitiveString(classification) } override SensitiveDataClassification getClassification() { result = classification } @@ -251,12 +252,12 @@ private module SensitiveDataModeling { SensitiveDataClassification classification; SensitiveVariableAssignment() { - exists(DefinitionNode def | - def.(NameNode).getId() = sensitiveString(classification) and + exists(Cfg::DefinitionNode def | + def.(Cfg::NameNode).getId() = sensitiveString(classification) and ( this.asCfgNode() = def.getValue() or - this.asCfgNode() = def.getValue().(ForNode).getSequence() + this.asCfgNode() = def.getValue().(Cfg::ForNode).getSequence() ) and not this.asExpr() instanceof FunctionExpr and not this.asExpr() instanceof ClassExpr @@ -293,7 +294,7 @@ private module SensitiveDataModeling { SensitiveDataClassification classification; SensitiveSubscript() { - this.asCfgNode().(SubscriptNode).getIndex() = + this.asCfgNode().(Cfg::SubscriptNode).getIndex() = sensitiveLookupStringConst(classification).asCfgNode() } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll b/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll index 76d2cb11e144..cfdef2a1a905 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/Attributes.qll @@ -3,6 +3,7 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg import DataFlowUtil import DataFlowPublic private import DataFlowPrivate @@ -83,9 +84,9 @@ abstract class AttrWrite extends AttrRef { * ```python * object.attr = value * ``` - * Also gives access to the `value` being written, by extending `DefinitionNode`. + * Also gives access to the `value` being written, by extending `Cfg::DefinitionNode`. */ -private class AttributeAssignmentNode extends DefinitionNode, AttrNode { } +private class AttributeAssignmentNode extends Cfg::DefinitionNode, Cfg::AttrNode { } /** A simple attribute assignment: `object.attr = value`. */ private class AttributeAssignmentAsAttrWrite extends AttrWrite, CfgNode { @@ -131,13 +132,13 @@ private class GlobalAttributeAssignmentAsAttrWrite extends AttrWrite, CfgNode { override string getAttributeName() { result = node.getName() } } -/** Represents `CallNode`s that may refer to calls to built-in functions or classes. */ -private class BuiltInCallNode extends CallNode { +/** Represents `Cfg::CallNode`s that may refer to calls to built-in functions or classes. */ +private class BuiltInCallNode extends Cfg::CallNode { string name; BuiltInCallNode() { // TODO disallow instances where the name of the built-in may refer to an in-scope variable of that name. - exists(NameNode id | + exists(Cfg::NameNode id | name = Builtins::getBuiltinName() and this.getFunction() = id and id.getId() = name and @@ -145,7 +146,7 @@ private class BuiltInCallNode extends CallNode { ) } - /** Gets the name of the built-in function that is called at this `CallNode` */ + /** Gets the name of the built-in function that is called at this `Cfg::CallNode` */ string getBuiltinName() { result = name } } @@ -157,20 +158,20 @@ private class BuiltinAttrCallNode extends BuiltInCallNode { BuiltinAttrCallNode() { name in ["setattr", "getattr", "hasattr", "delattr"] } /** Gets the control flow node for object on which the attribute is accessed. */ - ControlFlowNode getObject() { result in [this.getArg(0), this.getArgByName("object")] } + Cfg::ControlFlowNode getObject() { result in [this.getArg(0), this.getArgByName("object")] } /** * Gets the control flow node for the value that is being written to the attribute. * Only relevant for `setattr` calls. */ - ControlFlowNode getValue() { + Cfg::ControlFlowNode getValue() { // only valid for `setattr` name = "setattr" and result in [this.getArg(2), this.getArgByName("value")] } /** Gets the control flow node that defines the name of the attribute being accessed. */ - ControlFlowNode getName() { result in [this.getArg(1), this.getArgByName("name")] } + Cfg::ControlFlowNode getName() { result in [this.getArg(1), this.getArgByName("name")] } } /** Represents calls to the built-in `setattr`. */ @@ -205,10 +206,10 @@ private class SetAttrCallAsAttrWrite extends AttrWrite, CfgNode { * attr = value * ... * ``` - * Instances of this class correspond to the `NameNode` for `attr`, and also gives access to `value` by - * virtue of being a `DefinitionNode`. + * Instances of this class correspond to the `Cfg::NameNode` for `attr`, and also gives access to `value` by + * virtue of being a `Cfg::DefinitionNode`. */ -private class ClassAttributeAssignmentNode extends DefinitionNode, NameNode { +private class ClassAttributeAssignmentNode extends Cfg::DefinitionNode, Cfg::NameNode { ClassAttributeAssignmentNode() { this.getScope() = any(ClassExpr c).getInnerScope() } } @@ -248,7 +249,7 @@ abstract class AttrRead extends AttrRef, Node, LocalSourceNode { /** A simple attribute read, e.g. `object.attr` */ private class AttributeReadAsAttrRead extends AttrRead, CfgNode { - override AttrNode node; + override Cfg::AttrNode node; AttributeReadAsAttrRead() { node.isLoad() } @@ -285,7 +286,7 @@ private class GetAttrCallAsAttrRead extends AttrRead, CfgNode { * is treated as if it is a read of the attribute `module.attr`, even if `module` is not imported directly. */ private class ModuleAttributeImportAsAttrRead extends AttrRead, CfgNode { - override ImportMemberNode node; + override Cfg::ImportMemberNode node; override Node getObject() { result.asCfgNode() = node.getModule(_) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/Builtins.qll b/python/ql/lib/semmle/python/dataflow/new/internal/Builtins.qll index 764af5d9dc57..94c9b486448f 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/Builtins.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/Builtins.qll @@ -3,6 +3,7 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.ImportStar @@ -67,7 +68,7 @@ module Builtins { DataFlow::CfgNode likelyBuiltin(string name) { exists(Module m | result.getNode() = - any(NameNode n | + any(Cfg::NameNode n | possible_builtin_accessed_in_module(n, name, m) and not possible_builtin_defined_in_module(name, m) ) @@ -87,7 +88,7 @@ module Builtins { * Holds if `n` is an access of a global variable called `name` (which is also the name of a * built-in) inside the module `m`. */ - private predicate possible_builtin_accessed_in_module(NameNode n, string name, Module m) { + private predicate possible_builtin_accessed_in_module(Cfg::NameNode n, string name, Module m) { n.isGlobal() and n.isLoad() and name = n.getId() and diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index fc0bba6b1353..ba6d134f83bb 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -25,7 +25,7 @@ * what callable this call might end up targeting. * * Specifically this means that we cannot use type-backtrackers from the function of a - * `CallNode`, since there is no `CallNode` to backtrack from for `func` in the example + * `Cfg::CallNode`, since there is no `Cfg::CallNode` to backtrack from for `func` in the example * above. * * Note: This hasn't been 100% realized yet, so we don't currently expose a predicate to @@ -35,6 +35,7 @@ overlay[local?] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import DataFlowPublic private import DataFlowPrivate private import FlowSummaryImpl as FlowSummaryImpl @@ -162,7 +163,7 @@ newtype TArgumentPosition = */ TLambdaSelfArgumentPosition() or TPositionalArgumentPosition(int index) { - exists(any(CallNode c).getArg(index)) + exists(any(Cfg::CallNode c).getArg(index)) or // since synthetic calls within a summarized callable could use a unique argument // position, we need to ensure we make these available (these are specified as @@ -174,7 +175,7 @@ newtype TArgumentPosition = index = 0 } or TKeywordArgumentPosition(string name) { - exists(any(CallNode c).getArgByName(name)) + exists(any(Cfg::CallNode c).getArgByName(name)) or // see comment for TPositionalArgumentPosition FlowSummaryImpl::ParsePositions::isParsedKeywordParameterPosition(_, name) @@ -256,9 +257,13 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { */ overlay[local] predicate isStaticmethod(Function func) { - exists(NameNode id | id.getId() = "staticmethod" and id.isGlobal() | - func.getADecorator() = id.getNode() - ) + // The decorator is *syntactically* a Name "staticmethod" — we don't + // care which variable it resolves to. Even if a class redefines + // `staticmethod`, the binding hasn't happened yet at the decorator + // position, so Python's runtime semantics is "use the builtin". + // Matches legacy ESSA semantics which used `isGlobal()` (i.e. "no + // SSA def reaches this load") at the decorator's `NameNode`. + func.getADecorator().(Name).getId() = "staticmethod" } /** @@ -268,9 +273,7 @@ predicate isStaticmethod(Function func) { */ overlay[local] predicate isClassmethod(Function func) { - exists(NameNode id | id.getId() = "classmethod" and id.isGlobal() | - func.getADecorator() = id.getNode() - ) + func.getADecorator().(Name).getId() = "classmethod" or exists(Class cls | cls.getAMethod() = func and @@ -285,9 +288,7 @@ predicate isClassmethod(Function func) { /** Holds if the function `func` has a `property` decorator. */ overlay[local] predicate hasPropertyDecorator(Function func) { - exists(NameNode id | id.getId() = "property" and id.isGlobal() | - func.getADecorator() = id.getNode() - ) + func.getADecorator().(Name).getId() = "property" } /** @@ -295,10 +296,10 @@ predicate hasPropertyDecorator(Function func) { */ overlay[local] predicate hasContextmanagerDecorator(Function func) { - exists(ControlFlowNode contextmanager | - contextmanager.(NameNode).getId() = "contextmanager" and contextmanager.(NameNode).isGlobal() + exists(Cfg::ControlFlowNode contextmanager | + contextmanager.(Cfg::NameNode).getId() = "contextmanager" and contextmanager.(Cfg::NameNode).isGlobal() or - contextmanager.(AttrNode).getObject("contextmanager").(NameNode).getId() = "contextlib" + contextmanager.(Cfg::AttrNode).getObject("contextmanager").(Cfg::NameNode).getId() = "contextlib" | func.getADecorator() = contextmanager.getNode() ) @@ -314,10 +315,10 @@ predicate hasContextmanagerDecorator(Function func) { */ overlay[local] private predicate hasOverloadDecorator(Function func) { - exists(ControlFlowNode overload | - overload.(NameNode).getId() = "overload" and overload.(NameNode).isGlobal() + exists(Cfg::ControlFlowNode overload | + overload.(Cfg::NameNode).getId() = "overload" and overload.(Cfg::NameNode).isGlobal() or - overload.(AttrNode).getObject("overload").(NameNode).isGlobal() + overload.(Cfg::AttrNode).getObject("overload").(Cfg::NameNode).isGlobal() | func.getADecorator() = overload.getNode() ) @@ -536,7 +537,7 @@ class LibraryCallableValue extends DataFlowCallable, TLibraryCallable { // ============================================================================= /** Gets a call to `type`. */ private CallCfgNode getTypeCall() { - exists(NameNode id | id.getId() = "type" and id.isGlobal() | + exists(Cfg::NameNode id | id.getId() = "type" and id.isGlobal() | result.getFunction().asCfgNode() = id ) } @@ -548,7 +549,7 @@ private CallCfgNode getSuperCall() { // link below), but otherwise only 2 edgecases. Overall it seems ok to ignore this complexity. // // https://github.com/python/cpython/blob/18b1782192f85bd26db89f5bc850f8bee4247c1a/Lib/unittest/mock.py#L48-L50 - exists(NameNode id | id.getId() = "super" and id.isGlobal() | + exists(Cfg::NameNode id | id.getId() = "super" and id.isGlobal() | result.getFunction().asCfgNode() = id ) } @@ -1034,7 +1035,7 @@ private module MethodCalls { */ pragma[nomagic] private predicate directCall( - CallNode call, Function target, string functionName, Class cls, AttrRead attr, Node self + Cfg::CallNode call, Function target, string functionName, Class cls, AttrRead attr, Node self ) { target = findFunctionAccordingToMroKnownStartingClass(cls, functionName) and directCall_join(call, functionName, cls, attr, self) @@ -1043,7 +1044,7 @@ private module MethodCalls { /** Extracted to give good join order */ pragma[nomagic] private predicate directCall_join( - CallNode call, string functionName, Class cls, AttrRead attr, Node self + Cfg::CallNode call, string functionName, Class cls, AttrRead attr, Node self ) { call.getFunction() = attrReadTracker(attr).asCfgNode() and attr.accesses(self, functionName) and @@ -1060,7 +1061,7 @@ private module MethodCalls { */ pragma[nomagic] private predicate callWithinMethodImplicitSelfOrCls( - CallNode call, Function target, string functionName, Class classWithMethod, AttrRead attr, + Cfg::CallNode call, Function target, string functionName, Class classWithMethod, AttrRead attr, Node self ) { target = findFunctionAccordingToMro(getADirectSubclass*(classWithMethod), functionName) and @@ -1070,7 +1071,7 @@ private module MethodCalls { /** Extracted to give good join order */ pragma[nomagic] private predicate callWithinMethodImplicitSelfOrCls_join( - CallNode call, string functionName, Class classWithMethod, AttrRead attr, Node self + Cfg::CallNode call, string functionName, Class classWithMethod, AttrRead attr, Node self ) { call.getFunction() = attrReadTracker(attr).asCfgNode() and attr.accesses(self, functionName) and @@ -1082,7 +1083,7 @@ private module MethodCalls { * resolve the call to a known target (since the only super class might be the * builtin `object`, so we never have the implementation of `__new__` in the DB). */ - predicate fromSuperNewCall(CallNode call, Class classUsedInSuper, AttrRead attr, Node self) { + predicate fromSuperNewCall(Cfg::CallNode call, Class classUsedInSuper, AttrRead attr, Node self) { fromSuper_join(call, "__new__", classUsedInSuper, attr, self) and self in [classTracker(_), clsArgumentTracker(_)] } @@ -1104,7 +1105,7 @@ private module MethodCalls { */ pragma[nomagic] predicate fromSuper( - CallNode call, Function target, string functionName, Class classUsedInSuper, AttrRead attr, + Cfg::CallNode call, Function target, string functionName, Class classUsedInSuper, AttrRead attr, Node self ) { target = findFunctionAccordingToMro(getNextClassInMro(classUsedInSuper), functionName) and @@ -1114,7 +1115,7 @@ private module MethodCalls { /** Extracted to give good join order */ pragma[nomagic] private predicate fromSuper_join( - CallNode call, string functionName, Class classUsedInSuper, AttrRead attr, Node self + Cfg::CallNode call, string functionName, Class classUsedInSuper, AttrRead attr, Node self ) { call.getFunction() = attrReadTracker(attr).asCfgNode() and ( @@ -1133,7 +1134,7 @@ private module MethodCalls { ) } - predicate resolveMethodCall(CallNode call, Function target, CallType type, Node self) { + predicate resolveMethodCall(Cfg::CallNode call, Function target, CallType type, Node self) { ( directCall(call, target, _, _, _, self) or @@ -1180,7 +1181,7 @@ import MethodCalls * NOTE: We have this predicate mostly to be able to compare with old point-to * call-graph resolution. So it could be removed in the future. */ -predicate resolveClassCall(CallNode call, Class cls) { +predicate resolveClassCall(Cfg::CallNode call, Class cls) { call.getFunction() = classTracker(cls).asCfgNode() or // `cls()` inside a classmethod (which also contains `type(self)()` inside a method) @@ -1210,7 +1211,7 @@ Function invokedFunctionFromClassConstruction(Class cls, string funcName) { * * See https://docs.python.org/3/reference/datamodel.html#object.__call__ */ -predicate resolveClassInstanceCall(CallNode call, Function target, Node self) { +predicate resolveClassInstanceCall(Cfg::CallNode call, Function target, Node self) { exists(Class cls | call.getFunction() = classInstanceTracker(cls).asCfgNode() and target = findFunctionAccordingToMroKnownStartingClass(cls, "__call__") @@ -1229,7 +1230,7 @@ predicate resolveClassInstanceCall(CallNode call, Function target, Node self) { * Holds if `call` is a call to the `target`, with call-type `type`. */ cached -predicate resolveCall(CallNode call, Function target, CallType type) { +predicate resolveCall(Cfg::CallNode call, Function target, CallType type) { Stages::DataFlow::ref() and ( type instanceof CallTypePlainFunction and @@ -1254,11 +1255,11 @@ predicate resolveCall(CallNode call, Function target, CallType type) { // ============================================================================= /** * Holds if the argument of `call` at position `apos` is `arg`. This is just a helper - * predicate that maps ArgumentPositions to the arguments of the underlying `CallNode`. + * predicate that maps ArgumentPositions to the arguments of the underlying `Cfg::CallNode`. */ overlay[local] cached -predicate normalCallArg(CallNode call, Node arg, ArgumentPosition apos) { +predicate normalCallArg(Cfg::CallNode call, Node arg, ArgumentPosition apos) { exists(int index | apos.isPositional(index) and arg.asCfgNode() = call.getArg(index) @@ -1273,7 +1274,7 @@ predicate normalCallArg(CallNode call, Node arg, ArgumentPosition apos) { exists(int index | apos.isStarArgs(index) and arg.asCfgNode() = call.getStarArg() and - // since `CallNode.getArg` doesn't include `*args`, we need to drop to the AST level + // since `Cfg::CallNode.getArg` doesn't include `*args`, we need to drop to the AST level // to get the index. Notice that we only use the AST for getting the index, so we // don't need to check for dominance in regards to splitting. call.getStarArg().getNode() = call.getNode().getPositionalArg(index).(Starred).getValue() @@ -1347,7 +1348,7 @@ predicate normalCallArg(CallNode call, Node arg, ArgumentPosition apos) { * translated into `l.clear()`, and we can still have use-use flow. */ cached -predicate getCallArg(CallNode call, Function target, CallType type, Node arg, ArgumentPosition apos) { +predicate getCallArg(Cfg::CallNode call, Function target, CallType type, Node arg, ArgumentPosition apos) { Stages::DataFlow::ref() and resolveCall(call, target, type) and ( @@ -1440,10 +1441,10 @@ private predicate sameEnclosingCallable(Node node1, Node node2) { // DataFlowCall // ============================================================================= newtype TDataFlowCall = - TNormalCall(CallNode call, Function target, CallType type) { resolveCall(call, target, type) } or + TNormalCall(Cfg::CallNode call, Function target, CallType type) { resolveCall(call, target, type) } or /** A call to the generated function inside a comprehension */ TComprehensionCall(Comp c) or - TPotentialLibraryCall(CallNode call) or + TPotentialLibraryCall(Cfg::CallNode call) or /** A synthesized call inside a summarized callable */ TSummaryCall( FlowSummaryImpl::Public::SummarizedCallable c, FlowSummaryImpl::Private::SummaryNode receiver @@ -1463,7 +1464,7 @@ abstract class DataFlowCall extends TDataFlowCall { abstract ArgumentNode getArgument(ArgumentPosition apos); /** Get the control flow node representing this call, if any. */ - abstract ControlFlowNode getNode(); + abstract Cfg::ControlFlowNode getNode(); /** Gets the enclosing callable of this call. */ DataFlowCallable getEnclosingCallable() { result = getCallableScope(this.getScope()) } @@ -1494,28 +1495,28 @@ abstract class ExtractedDataFlowCall extends DataFlowCall { } /** - * A resolved call in source code with an underlying `CallNode`. + * A resolved call in source code with an underlying `Cfg::CallNode`. * * This is considered normal, compared with special calls such as `obj[0]` calling the * `__getitem__` method on the object. However, this also includes calls that go to the * `__call__` special method. */ class NormalCall extends ExtractedDataFlowCall, TNormalCall { - CallNode call; + Cfg::CallNode call; Function target; CallType type; NormalCall() { this = TNormalCall(call, target, type) } override string toString() { - // note: if we used toString directly on the CallNode we would get - // `ControlFlowNode for func()` - // but the `ControlFlowNode` part is just clutter, so we go directly to the AST node + // note: if we used toString directly on the Cfg::CallNode we would get + // `Cfg::ControlFlowNode for func()` + // but the `Cfg::ControlFlowNode` part is just clutter, so we go directly to the AST node // instead. result = call.getNode().toString() } - override ControlFlowNode getNode() { result = call } + override Cfg::ControlFlowNode getNode() { result = call } override Scope getScope() { result = call.getScope() } @@ -1543,7 +1544,7 @@ class ComprehensionCall extends ExtractedDataFlowCall, TComprehensionCall { override string toString() { result = "comprehension call" } - override ControlFlowNode getNode() { result.getNode() = c } + override Cfg::ControlFlowNode getNode() { result.getNode() = c } override Scope getScope() { result = c.getScope() } @@ -1566,14 +1567,14 @@ class ComprehensionCall extends ExtractedDataFlowCall, TComprehensionCall { * in this class. */ class PotentialLibraryCall extends ExtractedDataFlowCall, TPotentialLibraryCall { - CallNode call; + Cfg::CallNode call; PotentialLibraryCall() { this = TPotentialLibraryCall(call) } override string toString() { - // note: if we used toString directly on the CallNode we would get - // `ControlFlowNode for func()` - // but the `ControlFlowNode` part is just clutter, so we go directly to the AST node + // note: if we used toString directly on the Cfg::CallNode we would get + // `Cfg::ControlFlowNode for func()` + // but the `Cfg::ControlFlowNode` part is just clutter, so we go directly to the AST node // instead. result = call.getNode().toString() } @@ -1590,10 +1591,10 @@ class PotentialLibraryCall extends ExtractedDataFlowCall, TPotentialLibraryCall // potential self argument, from `foo.bar()` -- note that this could also just be a // module reference, but we really don't have a good way of knowing :| apos.isSelf() and - result.asCfgNode() = call.getFunction().(AttrNode).getObject() + result.asCfgNode() = call.getFunction().(Cfg::AttrNode).getObject() } - override ControlFlowNode getNode() { result = call } + override Cfg::ControlFlowNode getNode() { result = call } override Scope getScope() { result = call.getScope() } } @@ -1625,7 +1626,7 @@ class SummaryCall extends DataFlowCall, TSummaryCall { override ArgumentNode getArgument(ArgumentPosition apos) { none() } - override ControlFlowNode getNode() { none() } + override Cfg::ControlFlowNode getNode() { none() } override string toString() { result = "[summary] call to " + receiver + " in " + c } @@ -1767,12 +1768,12 @@ private class SummaryPostUpdateNode extends FlowSummaryNode, PostUpdateNodeImpl * This is used for tracking flow through captured variables. */ class SynthCapturedVariablesArgumentNode extends Node, TSynthCapturedVariablesArgumentNode { - ControlFlowNode callable; + Cfg::ControlFlowNode callable; SynthCapturedVariablesArgumentNode() { this = TSynthCapturedVariablesArgumentNode(callable) } - /** Gets the `CallNode` corresponding to this captured variables argument node. */ - CallNode getCallNode() { result.getFunction() = callable } + /** Gets the `Cfg::CallNode` corresponding to this captured variables argument node. */ + Cfg::CallNode getCallNode() { result.getFunction() = callable } /** Gets the `CfgNode` that corresponds to this synthetic node. */ CfgNode getUnderlyingNode() { result.asCfgNode() = callable } @@ -1790,7 +1791,7 @@ class CapturedVariablesArgumentNodeAsArgumentNode extends ArgumentNode, { overlay[global] override predicate argumentOf(DataFlowCall call, ArgumentPosition pos) { - exists(CallNode callNode | callNode = this.getCallNode() | + exists(Cfg::CallNode callNode | callNode = this.getCallNode() | callNode = call.getNode() and exists(Function target | resolveCall(callNode, target, _) | target = any(VariableCapture::CapturedVariable v).getACapturingScope() @@ -1804,7 +1805,7 @@ class CapturedVariablesArgumentNodeAsArgumentNode extends ArgumentNode, class SynthCapturedVariablesArgumentPostUpdateNode extends PostUpdateNodeImpl, TSynthCapturedVariablesArgumentPostUpdateNode { - ControlFlowNode callable; + Cfg::ControlFlowNode callable; SynthCapturedVariablesArgumentPostUpdateNode() { this = TSynthCapturedVariablesArgumentPostUpdateNode(callable) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll index 67963e7cd382..5d1f64172554 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll @@ -2,8 +2,9 @@ overlay[local?] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import DataFlowPublic -private import semmle.python.essa.SsaCompute +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import semmle.python.dataflow.new.internal.ImportResolution private import FlowSummaryImpl as FlowSummaryImpl private import semmle.python.frameworks.data.ModelsAsData @@ -43,13 +44,23 @@ predicate isArgumentNode(ArgumentNode arg, DataFlowCall c, ArgumentPosition pos) // Nodes //-------- overlay[local] -predicate isExpressionNode(ControlFlowNode node) { node.getNode() instanceof Expr } +predicate isExpressionNode(Cfg::ControlFlowNode node) { + node.getNode() instanceof Expr + or + // `Cfg::ForNode` wraps a `For` statement's iter position, but + // overrides `.getNode()` to return the `Py::For` statement (for + // legacy parity). The underlying AST is still an `Expr` (the iter + // expression); we want a dataflow node here so that for-loop + // content reads (`for y in l`) have a source expression node to + // read content from. + node instanceof Cfg::ForNode +} // ============================================================================= // SyntheticPreUpdateNode // ============================================================================= class SyntheticPreUpdateNode extends Node, TSyntheticPreUpdateNode { - CallNode node; + Cfg::CallNode node; SyntheticPreUpdateNode() { this = TSyntheticPreUpdateNode(node) } @@ -151,7 +162,7 @@ predicate synthStarArgsElementParameterNodeStoreStep( * been passed in a `**kwargs` argument. */ class SynthDictSplatArgumentNode extends Node, TSynthDictSplatArgumentNode { - CallNode node; + Cfg::CallNode node; SynthDictSplatArgumentNode() { this = TSynthDictSplatArgumentNode(node) } @@ -165,7 +176,7 @@ class SynthDictSplatArgumentNode extends Node, TSynthDictSplatArgumentNode { private predicate synthDictSplatArgumentNodeStoreStep( ArgumentNode nodeFrom, DictionaryElementContent c, SynthDictSplatArgumentNode nodeTo ) { - exists(string name, CallNode call, ArgumentPosition keywordPos | + exists(string name, Cfg::CallNode call, ArgumentPosition keywordPos | nodeTo = TSynthDictSplatArgumentNode(call) and getCallArg(call, _, _, nodeFrom, keywordPos) and keywordPos.isKeyword(name) and @@ -289,7 +300,7 @@ abstract class PostUpdateNodeImpl extends Node { * Synthetic post-update nodes for synthetic nodes need to be listed one by one. */ class SyntheticPostUpdateNode extends PostUpdateNodeImpl, TSyntheticPostUpdateNode { - ControlFlowNode node; + Cfg::ControlFlowNode node; SyntheticPostUpdateNode() { this = TSyntheticPostUpdateNode(node) } @@ -333,16 +344,22 @@ module LocalFlow { // `x = f(42)` // nodeFrom is `f(42)` // nodeTo is `x` - exists(AssignmentDefinition def | + // + // We use the CFG-level `DefinitionNode.getValue()` directly rather + // than going through SSA, because the new SSA library prunes write + // definitions that have no subsequent read in the same scope (e.g. + // a module-level `def f():` whose `f` is only read inside other + // functions). The CFG-level link is unconditional. + exists(Cfg::DefinitionNode def | nodeFrom.(CfgNode).getNode() = def.getValue() and - nodeTo.(CfgNode).getNode() = def.getDefiningNode() + nodeTo.(CfgNode).getNode() = def ) or // With definition // `with f(42) as x:` // nodeFrom is `f(42)` // nodeTo is `x` - exists(With with, ControlFlowNode contextManager, WithDefinition withDef, ControlFlowNode var | + exists(With with, Cfg::ControlFlowNode contextManager, SsaImpl::WithDefinition withDef, Cfg::ControlFlowNode var | var = withDef.getDefiningNode() | nodeFrom.(CfgNode).getNode() = contextManager and @@ -361,13 +378,13 @@ module LocalFlow { predicate expressionFlowStep(Node nodeFrom, Node nodeTo) { // If expressions - nodeFrom.asCfgNode() = nodeTo.asCfgNode().(IfExprNode).getAnOperand() + nodeFrom.asCfgNode() = nodeTo.asCfgNode().(Cfg::IfExprNode).getAnOperand() or // Assignment expressions - nodeFrom.asCfgNode() = nodeTo.asCfgNode().(AssignmentExprNode).getValue() + nodeFrom.asCfgNode() = nodeTo.asCfgNode().(Cfg::AssignmentExprNode).getValue() or // boolean inline expressions such as `x or y` or `x and y` - nodeFrom.asCfgNode() = nodeTo.asCfgNode().(BoolExprNode).getAnOperand() + nodeFrom.asCfgNode() = nodeTo.asCfgNode().(Cfg::BoolExprNode).getAnOperand() or // Flow inside an unpacking assignment iterableUnpackingFlowStep(nodeFrom, nodeTo) @@ -376,12 +393,12 @@ module LocalFlow { matchFlowStep(nodeFrom, nodeTo) } - predicate useToNextUse(NameNode nodeFrom, NameNode nodeTo) { - AdjacentUses::adjacentUseUse(nodeFrom, nodeTo) + predicate useToNextUse(Cfg::NameNode nodeFrom, Cfg::NameNode nodeTo) { + SsaImpl::AdjacentUses::adjacentUseUse(nodeFrom, nodeTo) } - predicate defToFirstUse(EssaVariable var, NameNode nodeTo) { - AdjacentUses::firstUse(var.getDefinition(), nodeTo) + predicate defToFirstUse(SsaImpl::EssaVariable var, Cfg::NameNode nodeTo) { + SsaImpl::AdjacentUses::firstUse(var.getDefinition(), nodeTo) } predicate useUseFlowStep(Node nodeFrom, Node nodeTo) { @@ -390,12 +407,12 @@ module LocalFlow { // `x = f(y)` // nodeFrom is `y` on first line // nodeTo is `y` on second line - exists(EssaDefinition def | - nodeFrom.(CfgNode).getNode() = def.(EssaNodeDefinition).getDefiningNode() + exists(SsaImpl::EssaDefinition def | + nodeFrom.(CfgNode).getNode() = def.(SsaImpl::EssaNodeDefinition).getDefiningNode() or nodeFrom.(ScopeEntryDefinitionNode).getDefinition() = def | - AdjacentUses::firstUse(def, nodeTo.(CfgNode).getNode()) + SsaImpl::AdjacentUses::firstUse(def, nodeTo.(CfgNode).getNode()) ) or // Next use after use @@ -557,9 +574,9 @@ predicate runtimeJumpStep(Node nodeFrom, Node nodeTo) { // a parameter with a default value, since the parameter will be in the scope of the // function, while the default value itself will be in the scope that _defines_ the // function. - exists(ParameterDefinition param | + exists(SsaImpl::ParameterDefinition param | // note: we go to the _control-flow node_ of the parameter, and not the ESSA node of the parameter, since for type-tracking, the ESSA node is not a LocalSourceNode, so we would get in trouble. - nodeFrom.asCfgNode() = param.getDefault() and + nodeFrom.asCfgNode().getNode() = param.getParameter().(Parameter).getDefault() and nodeTo.asCfgNode() = param.getDefiningNode() ) or @@ -663,7 +680,7 @@ predicate neverSkipInPathGraph(Node n) { // ``` // we would end up saying that the path MUST not skip the x in `y = x`, which is just // annoying and doesn't help the path explanation become clearer. - n.asCfgNode() = any(EssaNodeDefinition def).getDefiningNode() + n.asCfgNode() = any(SsaImpl::EssaNodeDefinition def).getDefiningNode() } /** @@ -872,7 +889,7 @@ predicate listStoreStep(CfgNode nodeFrom, ListElementContent c, CfgNode nodeTo) // nodeFrom is `42`, cfg node // nodeTo is the list, `[..., 42, ...]`, cfg node // c denotes element of list - nodeTo.getNode().(ListNode).getAnElement() = nodeFrom.getNode() and + nodeTo.getNode().(Cfg::ListNode).getAnElement() = nodeFrom.getNode() and not nodeTo.getNode() instanceof UnpackingAssignmentSequenceTarget and // Suppress unused variable warning c = c @@ -885,7 +902,7 @@ predicate setStoreStep(CfgNode nodeFrom, SetElementContent c, CfgNode nodeTo) { // nodeFrom is `42`, cfg node // nodeTo is the set, `{..., 42, ...}`, cfg node // c denotes element of list - nodeTo.getNode().(SetNode).getAnElement() = nodeFrom.getNode() and + nodeTo.getNode().(Cfg::SetNode).getAnElement() = nodeFrom.getNode() and // Suppress unused variable warning c = c } @@ -898,7 +915,7 @@ predicate tupleStoreStep(CfgNode nodeFrom, TupleElementContent c, CfgNode nodeTo // nodeTo is the tuple, `(..., 42, ...)`, cfg node // c denotes element of tuple and index of nodeFrom exists(int n | - nodeTo.getNode().(TupleNode).getElement(n) = nodeFrom.getNode() and + nodeTo.getNode().(Cfg::TupleNode).getElement(n) = nodeFrom.getNode() and not nodeTo.getNode() instanceof UnpackingAssignmentSequenceTarget and c.getIndex() = n ) @@ -912,7 +929,7 @@ predicate dictStoreStep(CfgNode nodeFrom, DictionaryElementContent c, Node nodeT // nodeTo is the dict, `{..., "key" = 42, ...}`, cfg node // c denotes element of dictionary and the key `"key"` exists(KeyValuePair item | - item = nodeTo.asCfgNode().(DictNode).getNode().(Dict).getAnItem() and + item = nodeTo.asCfgNode().(Cfg::DictNode).getNode().(Dict).getAnItem() and nodeFrom.getNode().getNode() = item.getValue() and c.getKey() = item.getKey().(StringLiteral).getS() ) @@ -927,9 +944,9 @@ predicate dictStoreStep(CfgNode nodeFrom, DictionaryElementContent c, Node nodeT private predicate moreDictStoreSteps(CfgNode nodeFrom, DictionaryElementContent c, Node nodeTo) { // NOTE: It's important to add logic to the newtype definition of // DictionaryElementContent if you add new cases here. - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | nodeTo.(PostUpdateNode).getPreUpdateNode().asCfgNode() = subscript.getObject() and - nodeFrom.asCfgNode() = subscript.(DefinitionNode).getValue() and + nodeFrom.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and c.getKey() = subscript.getIndex().getNode().(StringLiteral).getText() ) or @@ -942,8 +959,8 @@ private predicate moreDictStoreSteps(CfgNode nodeFrom, DictionaryElementContent } predicate dictClearStep(Node node, DictionaryElementContent c) { - exists(SubscriptNode subscript | - subscript instanceof DefinitionNode and + exists(Cfg::SubscriptNode subscript | + subscript instanceof Cfg::DefinitionNode and node.asCfgNode() = subscript.getObject() and c.getKey() = subscript.getIndex().getNode().(StringLiteral).getText() ) @@ -1018,7 +1035,7 @@ predicate subscriptReadStep(CfgNode nodeFrom, Content c, CfgNode nodeTo) { // nodeFrom is `l`, cfg node // nodeTo is `l[3]`, cfg node // c is compatible with 3 - nodeFrom.getNode() = nodeTo.getNode().(SubscriptNode).getObject() and + nodeFrom.getNode() = nodeTo.getNode().(Cfg::SubscriptNode).getObject() and ( c instanceof ListElementContent or @@ -1027,10 +1044,10 @@ predicate subscriptReadStep(CfgNode nodeFrom, Content c, CfgNode nodeTo) { c instanceof DictionaryElementAnyContent or c.(TupleElementContent).getIndex() = - nodeTo.getNode().(SubscriptNode).getIndex().getNode().(IntegerLiteral).getValue() + nodeTo.getNode().(Cfg::SubscriptNode).getIndex().getNode().(IntegerLiteral).getValue() or c.(DictionaryElementContent).getKey() = - nodeTo.getNode().(SubscriptNode).getIndex().getNode().(StringLiteral).getS() + nodeTo.getNode().(Cfg::SubscriptNode).getIndex().getNode().(StringLiteral).getS() ) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll index a9d73fe0527f..8ed76588004a 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll @@ -5,11 +5,12 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import DataFlowPrivate import semmle.python.dataflow.new.TypeTracking import Attributes import LocalSources -private import semmle.python.essa.SsaCompute +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import semmle.python.dataflow.new.internal.ImportStar private import semmle.python.frameworks.data.ModelsAsData private import FlowSummaryImpl as FlowSummaryImpl @@ -27,7 +28,7 @@ private import semmle.python.frameworks.data.ModelsAsData overlay[local] newtype TNode = /** A node corresponding to a control flow node. */ - TCfgNode(ControlFlowNode node) { + TCfgNode(Cfg::ControlFlowNode node) { isExpressionNode(node) or node.getNode() instanceof Pattern @@ -36,7 +37,7 @@ newtype TNode = * A node corresponding to a scope entry definition. That is, the value of a variable * as it enters a scope. */ - TScopeEntryDefinitionNode(ScopeEntryDefinition def) { not def.getScope() instanceof Module } or + TScopeEntryDefinitionNode(SsaImpl::ScopeEntryDefinition def) { not def.getScope() instanceof Module } or /** * A synthetic node representing the value of an object before a state change. * @@ -47,13 +48,13 @@ newtype TNode = // NOTE: since we can't rely on the call graph, but we want to have synthetic // pre-update nodes for class calls, we end up getting synthetic pre-update nodes for // ALL calls :| - TSyntheticPreUpdateNode(CallNode call) or + TSyntheticPreUpdateNode(Cfg::CallNode call) or /** * A synthetic node representing the value of an object after a state change. * See QLDoc for `PostUpdateNode`. */ - TSyntheticPostUpdateNode(ControlFlowNode node) { - exists(CallNode call | + TSyntheticPostUpdateNode(Cfg::ControlFlowNode node) { + exists(Cfg::CallNode call | node = call.getArg(_) or node = call.getArgByName(_) @@ -62,12 +63,12 @@ newtype TNode = node = call.getFunction() ) or - node = any(AttrNode a).getObject() + node = any(Cfg::AttrNode a).getObject() or - node = any(SubscriptNode s).getObject() + node = any(Cfg::SubscriptNode s).getObject() or // self parameter when used implicitly in `super()` - exists(Class cls, Function func, ParameterDefinition def | + exists(Class cls, Function func, SsaImpl::ParameterDefinition def | func = cls.getAMethod() and not isStaticmethod(func) and // this matches what we do in ExtractedParameterNode @@ -112,7 +113,7 @@ newtype TNode = exists(ParameterPosition ppos | ppos.isStarArgs(_) | exists(callable.getParameter(ppos))) } or /** A synthetic node to capture keyword arguments that are passed to a `**kwargs` parameter. */ - TSynthDictSplatArgumentNode(CallNode call) { exists(call.getArgByName(_)) } or + TSynthDictSplatArgumentNode(Cfg::CallNode call) { exists(call.getArgByName(_)) } or /** A synthetic node to allow flow to keyword parameters from a `**kwargs` argument. */ TSynthDictSplatParameterNode(DataFlowCallable callable) { exists(ParameterPosition ppos | ppos.isKeyword(_) | exists(callable.getParameter(ppos))) @@ -128,15 +129,15 @@ newtype TNode = * A synthetic node representing the values of the variables captured * by the callable being called. */ - TSynthCapturedVariablesArgumentNode(ControlFlowNode callable) { - callable = any(CallNode c).getFunction() + TSynthCapturedVariablesArgumentNode(Cfg::ControlFlowNode callable) { + callable = any(Cfg::CallNode c).getFunction() } or /** * A synthetic node representing the values of the variables captured * by the callable being called, after the output has been computed. */ - TSynthCapturedVariablesArgumentPostUpdateNode(ControlFlowNode callable) { - callable = any(CallNode c).getFunction() + TSynthCapturedVariablesArgumentPostUpdateNode(Cfg::ControlFlowNode callable) { + callable = any(Cfg::CallNode c).getFunction() } or /** A synthetic node representing the values of variables captured by a comprehension. */ TSynthCompCapturedVariablesArgumentNode(Comp comp) { @@ -194,7 +195,7 @@ class Node extends TNode { } /** Gets the control-flow node corresponding to this node, if any. */ - ControlFlowNode asCfgNode() { none() } + Cfg::ControlFlowNode asCfgNode() { none() } /** Gets the expression corresponding to this node, if any. */ Expr asExpr() { none() } @@ -207,14 +208,14 @@ class Node extends TNode { /** A data-flow node corresponding to a control-flow node. */ class CfgNode extends Node, TCfgNode { - ControlFlowNode node; + Cfg::ControlFlowNode node; CfgNode() { this = TCfgNode(node) } - /** Gets the `ControlFlowNode` represented by this data-flow node. */ - ControlFlowNode getNode() { result = node } + /** Gets the `Cfg::ControlFlowNode` represented by this data-flow node. */ + Cfg::ControlFlowNode getNode() { result = node } - override ControlFlowNode asCfgNode() { result = node } + override Cfg::ControlFlowNode asCfgNode() { result = node } /** Gets a textual representation of this element. */ override string toString() { result = node.toString() } @@ -224,9 +225,9 @@ class CfgNode extends Node, TCfgNode { override Location getLocation() { result = node.getLocation() } } -/** A data-flow node corresponding to a `CallNode` in the control-flow graph. */ +/** A data-flow node corresponding to a `Cfg::CallNode` in the control-flow graph. */ class CallCfgNode extends CfgNode, LocalSourceNode { - override CallNode node; + override Cfg::CallNode node; /** * Gets the data-flow node for the function component of the call corresponding to this data-flow @@ -307,15 +308,15 @@ ExprNode exprNode(DataFlowExpr e) { result.getNode().getNode() = e } * as it enters a scope. */ class ScopeEntryDefinitionNode extends Node, TScopeEntryDefinitionNode { - ScopeEntryDefinition def; + SsaImpl::ScopeEntryDefinition def; ScopeEntryDefinitionNode() { this = TScopeEntryDefinitionNode(def) } - /** Gets the `ScopeEntryDefinition` associated with this node. */ - ScopeEntryDefinition getDefinition() { result = def } + /** Gets the `SsaImpl::ScopeEntryDefinition` associated with this node. */ + SsaImpl::ScopeEntryDefinition getDefinition() { result = def } /** Gets the source variable represented by this node. */ - SsaSourceVariable getVariable() { result = def.getSourceVariable() } + SsaImpl::SsaSourceVariable getVariable() { result = def.getSourceVariable() } override Location getLocation() { result = def.getLocation() } @@ -337,7 +338,7 @@ class ParameterNode extends Node instanceof ParameterNodeImpl { /** A parameter node found in the source code (not in a summary). */ class ExtractedParameterNode extends ParameterNodeImpl, CfgNode { //, LocalSourceNode { - ParameterDefinition def; + SsaImpl::ParameterDefinition def; ExtractedParameterNode() { node = def.getDefiningNode() } @@ -368,10 +369,10 @@ Node getCallArgApproximation() { exists(Class c | result.asExpr() = c.getAMethod().getArg(0)) or // the object part of an attribute expression (which might be a bound method) - result.asCfgNode() = any(AttrNode a).getObject() + result.asCfgNode() = any(Cfg::AttrNode a).getObject() or // the function part of any call - result.asCfgNode() = any(CallNode c).getFunction() + result.asCfgNode() = any(Cfg::CallNode c).getFunction() } /** Gets the extracted argument nodes that do not rely on `getCallArg`. */ @@ -380,7 +381,7 @@ private Node implicitArgumentNode() { normalCallArg(_, result, _) or // and self arguments - result.asCfgNode() = any(CallNode c).getFunction().(AttrNode).getObject() + result.asCfgNode() = any(Cfg::CallNode c).getFunction().(Cfg::AttrNode).getObject() or // for comprehensions, we allow the synthetic `iterable` argument result.asExpr() = any(Comp c).getIterable() @@ -489,17 +490,20 @@ class ModuleVariableNode extends Node, TModuleVariableNode { not result.getScope() = mod } - /** Gets an `EssaNode` that corresponds to an assignment of this global variable. */ + /** Gets a CFG node that corresponds to an assignment of this global variable. */ Node getAWrite() { - any(EssaNodeDefinition def).definedBy(var, result.asCfgNode().(DefinitionNode)) + exists(Cfg::NameNode n | + n.defines(var) and + result.asCfgNode() = n + ) } /** Gets the possible values of the variable at the end of import time */ CfgNode getADefiningWrite() { - exists(SsaVariable def | - def = any(SsaVariable ssa_var).getAnUltimateDefinition() and - def.getDefinition() = result.asCfgNode() and - def.getVariable() = var + exists(SsaImpl::EssaVariable def | + def = any(SsaImpl::EssaVariable ssa_var).getAnUltimateDefinition() and + def.getDefinition().(SsaImpl::EssaNodeDefinition).getDefiningNode() = result.asCfgNode() and + def.getSourceVariable().getVariable() = var ) } @@ -516,7 +520,7 @@ private ModuleVariableNode import_star_read(Node n) { overlay[global] pragma[nomagic] private predicate resolved_import_star_module(Module m, string name, Node n) { - exists(NameNode nn | nn = n.asCfgNode() | + exists(Cfg::NameNode nn | nn = n.asCfgNode() | ImportStar::importStarResolvesTo(pragma[only_bind_into](nn), m) and nn.getId() = name ) @@ -574,88 +578,106 @@ class StarPatternElementNode extends Node, TStarPatternElementNode { } /** - * Gets a node that controls whether other nodes are evaluated. + * A node that participates in a conditional split: a CFG node whose + * evaluation outcome (true/false) is used to choose between two + * successor basic blocks. In the new shared CFG, such nodes appear in + * pairs of `isAfterTrue`/`isAfterFalse` annotated CFG nodes. * - * In the base case, this is the last node of `conditionBlock`, and `flipped` is `false`. - * This definition accounts for (short circuting) `and`- and `or`-expressions, as the structure - * of basic blocks will reflect their semantics. + * Users typically obtain a `GuardNode` by casting from a more specific + * Cfg type: `g.(Cfg::CallNode)` for a call-based check, etc. * - * However, in the program - * ```python - * if not is_safe(path): - * return - * ``` - * the last node in the `ConditionBlock` is `not is_safe(path)`. + * This replaces the legacy (pre-shared-CFG) `GuardNode`/`flipped` + * machinery: the shared CFG carries outcome information structurally + * (via `isAfterTrue`/`isAfterFalse`), so no separate polarity field + * is required. + */ +class GuardNode extends Cfg::ControlFlowNode { + GuardNode() { + // This is the canonical (post-order) version of an AST node, and + // some `[true]`/`[false]` variant of the same AST exists. We + // include the canonical node because users identify guards by + // their AST (`g.(Cfg::CallNode)` etc.), and the outcome-tagged + // variants are accessed by `outcomeOfGuard` below. + exists(Cfg::ControlFlowNode outcome | + outcome.getNode() = this.getNode() and + (outcome.isAfterTrue(_) or outcome.isAfterFalse(_)) + ) + or + // Or: this IS one of the outcome-tagged variants, supporting + // users who want to query the split point directly. + this.isAfterTrue(_) + or + this.isAfterFalse(_) + } + + /** Holds if this guard controls block `b` upon evaluating to `branch`. */ + predicate controlsBlock(Cfg::BasicBlock b, boolean branch) { + branch in [true, false] and + exists(Cfg::ControlFlowNode outcomeNode | + outcomeOfGuard(this, outcomeNode, branch) and + outcomeNode.getBasicBlock().dominates(b) + ) + } +} + +/** + * Holds if some execution that arrives at `outcomeNode` corresponds + * to `guard` having evaluated to `branch`. * - * We would like to consider also `is_safe(path)` a guard node, albeit with `flipped` being `true`. - * Thus we recurse through `not`-expressions. + * For a direct guard `if g:`, the outcome node is `g` itself with + * `isAfterTrue`/`isAfterFalse`. For wrapped guards like `not g` or + * `g == True`, the outcome is on the wrapping expression with an + * appropriate polarity transform — we follow those wrappers up the + * AST to find the outermost expression that carries an actual + * `isAfterTrue`/`isAfterFalse` outcome. */ -ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) { - // Base case: the last node truly does determine which successor is chosen - result = conditionBlock.getLastNode() and - flipped = false +private predicate outcomeOfGuard( + Cfg::ControlFlowNode guard, Cfg::ControlFlowNode outcomeNode, boolean branch +) { + // Base case: the guard itself splits — the outcome node is the + // first node of an outcome BB, with matching outcome label. + // (The shared CFG also marks inner expressions with outcome flags + // for analysis purposes, but only "splitting" nodes — those that + // actually start an outcome BB — are valid guards on their own.) + outcomeNode.getNode() = guard.getNode() and + outcomeNode = outcomeNode.getBasicBlock().firstNode() and + ( + outcomeNode.isAfterTrue(_) and branch = true + or + outcomeNode.isAfterFalse(_) and branch = false + ) or - // Recursive cases: - // if a guard node is a `not`-expression, - // the operand is also a guard node, but with inverted polarity. - exists(UnaryExprNode notNode | - result = notNode.getOperand() and - notNode.getNode().getOp() instanceof Not - | - notNode = guardNode(conditionBlock, flipped.booleanNot()) + // Recursive: `not guard` — same outcome split as `guard`, flipped. + exists(Cfg::UnaryExprNode notNode, boolean notBranch | + notNode.getOperand().getNode() = guard.getNode() and + notNode.getNode().getOp() instanceof Not and + outcomeOfGuard(notNode, outcomeNode, notBranch) and + branch = notBranch.booleanNot() ) or - // if a guard node is compared to a boolean literal, - // the other operand is also a guard node, - // but with polarity depending on the literal (and on the comparison). - exists(CompareNode cmpNode, Cmpop op, ControlFlowNode b, boolean should_flip | - ( - cmpNode.operands(result, op, b) or - cmpNode.operands(b, op, result) - ) and - not result.getNode() instanceof BooleanLiteral and + // Recursive: comparisons against a boolean literal. + exists(Cfg::CompareNode cmpNode, Cmpop op, Cfg::ControlFlowNode otherOperand, + Cfg::ControlFlowNode guardOperand, boolean polarity, boolean cmpBranch + | + guardOperand.getNode() = guard.getNode() and + (cmpNode.operands(guardOperand, op, otherOperand) or cmpNode.operands(otherOperand, op, guardOperand)) and + not guard.getNode() instanceof BooleanLiteral and ( - // comparing to the boolean (op instanceof Eq or op instanceof Is) and - // we should flip if the value compared against, here the value of `b`, is false - should_flip = b.getNode().(BooleanLiteral).booleanValue().booleanNot() + polarity = otherOperand.getNode().(BooleanLiteral).booleanValue() or - // comparing to the negation of the boolean (op instanceof NotEq or op instanceof IsNot) and - // again, we should flip if the value compared against, here the value of `not b`, is false. - // That is, if the value of `b` is true. - should_flip = b.getNode().(BooleanLiteral).booleanValue() - ) - | - // we flip `flipped` according to `should_flip` via the formula `flipped xor should_flip`. - flipped in [true, false] and - cmpNode = guardNode(conditionBlock, flipped.booleanXor(should_flip)) + polarity = otherOperand.getNode().(BooleanLiteral).booleanValue().booleanNot() + ) and + outcomeOfGuard(cmpNode, outcomeNode, cmpBranch) and + branch = cmpBranch.booleanXor(polarity.booleanNot()) ) } -/** - * A node that controls whether other nodes are evaluated. - * - * The field `flipped` allows us to match `GuardNode`s underneath - * `not`-expressions and still choose the appropriate branch. - */ -class GuardNode extends ControlFlowNode { - ConditionBlock conditionBlock; - boolean flipped; - - GuardNode() { this = guardNode(conditionBlock, flipped) } - - /** Holds if this guard controls block `b` upon evaluating to `branch`. */ - predicate controlsBlock(BasicBlock b, boolean branch) { - branch in [true, false] and - conditionBlock.controls(b, branch.booleanXor(flipped)) - } -} - /** * Holds if the guard `g` validates `node` upon evaluating to `branch`. */ -signature predicate guardChecksSig(GuardNode g, ControlFlowNode node, boolean branch); +signature predicate guardChecksSig(GuardNode g, Cfg::ControlFlowNode node, boolean branch); /** * Provides a set of barrier nodes for a guard that validates a node. @@ -670,7 +692,7 @@ module BarrierGuard { result = ParameterizedBarrierGuard::getABarrierNode(_) } - private predicate extendedGuardChecks(GuardNode g, ControlFlowNode node, boolean branch, Unit u) { + private predicate extendedGuardChecks(GuardNode g, Cfg::ControlFlowNode node, boolean branch, Unit u) { guardChecks(g, node, branch) and u = u } @@ -680,7 +702,7 @@ bindingset[this] private signature class ParamSig; private module WithParam { - signature predicate guardChecksSig(GuardNode g, ControlFlowNode node, boolean branch, P param); + signature predicate guardChecksSig(GuardNode g, Cfg::ControlFlowNode node, boolean branch, P param); } /** @@ -693,10 +715,10 @@ module ParameterizedBarrierGuard::guardChecksSig/4 guar /** Gets a node that is safely guarded by the given guard check with parameter `param`. */ overlay[global] ExprNode getABarrierNode(P param) { - exists(GuardNode g, EssaDefinition def, ControlFlowNode node, boolean branch | - AdjacentUses::useOfDef(def, node) and + exists(GuardNode g, SsaImpl::EssaDefinition def, Cfg::ControlFlowNode node, boolean branch | + SsaImpl::AdjacentUses::useOfDef(def, node) and guardChecks(g, node, branch, param) and - AdjacentUses::useOfDef(def, result.asCfgNode()) and + SsaImpl::AdjacentUses::useOfDef(def, result.asCfgNode()) and g.controlsBlock(result.asCfgNode().getBasicBlock(), branch) ) } @@ -712,7 +734,7 @@ module ExternalBarrierGuard { private import semmle.python.ApiGraphs overlay[global] - private predicate guardCheck(GuardNode g, ControlFlowNode node, boolean branch, string kind) { + private predicate guardCheck(GuardNode g, Cfg::ControlFlowNode node, boolean branch, string kind) { exists(API::CallNode call, API::Node parameter | parameter = call.getAParameter() and parameter = ModelOutput::getABarrierGuardNode(kind, branch) @@ -748,10 +770,10 @@ newtype TContent = TSetElementContent() or /** An element of a tuple at a specific index. */ TTupleElementContent(int index) { - exists(any(TupleNode tn).getElement(index)) + exists(any(Cfg::TupleNode tn).getElement(index)) or // Arguments can overflow and end up in the starred parameter tuple. - exists(any(CallNode cn).getArg(index)) + exists(any(Cfg::CallNode cn).getArg(index)) or // since flow summaries might use tuples, we ensure that we at least have valid // TTupleElementContent for the 0..7 (7 was picked to match `small_tuple` in @@ -768,10 +790,10 @@ newtype TContent = or // d["key"] = ... key = - any(SubscriptNode sub | sub.isStore() | sub.getIndex().getNode().(StringLiteral).getText()) + any(Cfg::SubscriptNode sub | sub.isStore() | sub.getIndex().getNode().(StringLiteral).getText()) or // d.setdefault("key", ...) - exists(CallNode call | call.getFunction().(AttrNode).getName() = "setdefault" | + exists(Cfg::CallNode call | call.getFunction().(Cfg::AttrNode).getName() = "setdefault" | key = call.getArg(0).getNode().(StringLiteral).getText() ) } or diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll b/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll index f3943f53f860..c9f2c79aebc6 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll @@ -5,6 +5,8 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.ImportStar private import semmle.python.dataflow.new.TypeTracking @@ -69,13 +71,11 @@ module ImportResolution { * Holds if there is an ESSA step from `defFrom` to `defTo`, which should be allowed * for import resolution. */ - private predicate allowedEssaImportStep(EssaDefinition defFrom, EssaDefinition defTo) { + private predicate allowedEssaImportStep(SsaImpl::EssaDefinition defFrom, SsaImpl::EssaDefinition defTo) { // to handle definitions guarded by if-then-else - defFrom = defTo.(PhiFunction).getAnInput() - or - // refined variable - // example: https://github.com/nvbn/thefuck/blob/ceeaeab94b5df5a4fe9d94d61e4f6b0bbea96378/thefuck/utils.py#L25-L45 - defFrom = defTo.(EssaNodeRefinement).getInput().getDefinition() + defFrom = defTo.(SsaImpl::PhiFunction).getAnInput() + // Note: legacy ESSA refinement-step (e.g. for `foo.bar = X`) is + // not modelled in the new SSA. We rely on phi steps only. } /** @@ -92,30 +92,32 @@ module ImportResolution { // Definitions made inside `m` itself // // for code such as `foo = ...; foo.bar = ...` there will be TWO - // EssaDefinition/EssaVariable. One for `foo = ...` (AssignmentDefinition) and one + // SsaImpl::EssaDefinition/SsaImpl::EssaVariable. One for `foo = ...` (SsaImpl::AssignmentDefinition) and one // for `foo.bar = ...`. The one for `foo.bar = ...` (EssaNodeRefinement). The // EssaNodeRefinement is the one that will reach the end of the module (normal // exit). // // However, we cannot just use the EssaNodeRefinement as the `val`, because the // normal data-flow depends on use-use flow, and use-use flow targets CFG nodes not - // EssaNodes. So we need to go back from the EssaDefinition/EssaVariable that + // EssaNodes. So we need to go back from the SsaImpl::EssaDefinition/SsaImpl::EssaVariable that // reaches the end of the module, to the first definition of the variable, and then // track forwards using use-use flow to find a suitable CFG node that has flow into // it from use-use flow. - exists(EssaVariable lastUseVar, EssaVariable firstDef | + exists(SsaImpl::EssaVariable lastUseVar, SsaImpl::EssaVariable firstDef | lastUseVar.getName() = name and // we ignore special variable $ introduced by our analysis (not used for anything) // we ignore special variable * introduced by `from import *` -- TODO: understand why we even have this? not name in ["$", "*"] and - lastUseVar.getAUse() = m.getANormalExit() and + exists(Cfg::ControlFlowNode exit | + exit.isNormalExit() and exit.getScope() = m and lastUseVar.getAUse() = exit + ) and allowedEssaImportStep*(firstDef, lastUseVar) and not allowedEssaImportStep(_, firstDef) | not LocalFlow::defToFirstUse(firstDef, _) and - val.asCfgNode() = firstDef.getDefinition().(EssaNodeDefinition).getDefiningNode() + val.asCfgNode() = firstDef.getDefinition().(SsaImpl::EssaNodeDefinition).getDefiningNode() or - exists(ControlFlowNode mid, ControlFlowNode end | + exists(Cfg::ControlFlowNode mid, Cfg::ControlFlowNode end | LocalFlow::defToFirstUse(firstDef, mid) and LocalFlow::useToNextUse*(mid, end) and not LocalFlow::useToNextUse(end, _) and @@ -143,9 +145,9 @@ module ImportResolution { * handles simple cases where we can statically tell that this is the case. */ private predicate all_mentions_name(Module m, string name) { - exists(DefinitionNode def, SequenceNode n | + exists(Cfg::DefinitionNode def, Cfg::SequenceNode n | def.getValue() = n and - def.(NameNode).getId() = "__all__" and + def.(Cfg::NameNode).getId() = "__all__" and def.getScope() = m and any(StringLiteral s | s.getText() = name) = n.getAnElement().getNode() ) @@ -158,18 +160,18 @@ module ImportResolution { */ private predicate no_or_complicated_all(Module m) { // No mention of `__all__` in the module - not exists(DefinitionNode def | def.getScope() = m and def.(NameNode).getId() = "__all__") + not exists(Cfg::DefinitionNode def | def.getScope() = m and def.(Cfg::NameNode).getId() = "__all__") or // `__all__` is set to a non-sequence value - exists(DefinitionNode def | - def.(NameNode).getId() = "__all__" and + exists(Cfg::DefinitionNode def | + def.(Cfg::NameNode).getId() = "__all__" and def.getScope() = m and - not def.getValue() instanceof SequenceNode + not def.getValue() instanceof Cfg::SequenceNode ) or // `__all__` is used in some way that doesn't involve storing a value in it. This usually means // it is being mutated through `append` or `extend`, which we don't handle. - exists(NameNode n | n.getId() = "__all__" and n.getScope() = m and n.isLoad()) + exists(Cfg::NameNode n | n.getId() = "__all__" and n.getScope() = m and n.isLoad()) } private predicate potential_module_export(Module m, string name) { @@ -177,7 +179,7 @@ module ImportResolution { or no_or_complicated_all(m) and ( - exists(NameNode n | n.getId() = name and n.getScope() = m and name.charAt(0) != "_") + exists(Cfg::NameNode n | n.getId() = name and n.getScope() = m and name.charAt(0) != "_") or exists(Alias a | a.getAsname().(Name).getId() = name and a.getValue().getScope() = m) ) @@ -207,12 +209,12 @@ module ImportResolution { /** Gets a module that may have been added to `sys.modules`. */ private Module sys_modules_module_with_name(string name) { - exists(ControlFlowNode n, DataFlow::Node mod | - exists(SubscriptNode sub | + exists(Cfg::ControlFlowNode n, DataFlow::Node mod | + exists(Cfg::SubscriptNode sub | sub.getObject() = sys_modules_reference().asCfgNode() and sub.getIndex() = n and n.getNode().(StringLiteral).getText() = name and - sub.(DefinitionNode).getValue() = mod.asCfgNode() and + sub.(Cfg::DefinitionNode).getValue() = mod.asCfgNode() and mod = getModuleReference(result) ) ) @@ -324,11 +326,11 @@ module ImportResolution { // name as a submodule, we always consider that this attribute _could_ be a // reference to the submodule, even if we don't know that the submodule has been // imported yet. - exists(string submodule, Module package, EssaVariable var | + exists(string submodule, Module package, SsaImpl::EssaVariable var | submodule = var.getName() and - SsaSource::init_module_submodule_defn(var.getSourceVariable(), package.getEntryNode()) and + SsaSource::init_module_submodule_defn(var.getSourceVariable().getVariable(), package.getEntryNode()) and m = getModuleFromName(package.getPackageName() + "." + submodule) and - result.asCfgNode() = var.getDefinition().(EssaNodeDefinition).getDefiningNode() + result.asCfgNode() = var.getDefinition().(SsaImpl::EssaNodeDefinition).getDefiningNode() ) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/ImportStar.qll b/python/ql/lib/semmle/python/dataflow/new/internal/ImportStar.qll index 83f8ee862c39..8c906696be7a 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/ImportStar.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/ImportStar.qll @@ -3,6 +3,7 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.internal.Builtins private import semmle.python.dataflow.new.internal.ImportResolution private import semmle.python.dataflow.new.DataFlow @@ -15,7 +16,7 @@ module ImportStar { */ overlay[local] cached - predicate namePossiblyDefinedInImportStar(NameNode n, string name, Scope s) { + predicate namePossiblyDefinedInImportStar(Cfg::NameNode n, string name, Scope s) { n.isLoad() and name = n.getId() and s = n.getScope().getEnclosingScope*() and @@ -52,7 +53,7 @@ module ImportStar { /** Holds if a global variable called `name` is assigned a value in the module `m`. */ cached predicate globalNameDefinedInModule(string name, Module m) { - exists(NameNode n | + exists(Cfg::NameNode n | not exists(LocalVariable v | n.defines(v)) and n.isStore() and name = n.getId() and @@ -66,7 +67,7 @@ module ImportStar { */ overlay[global] cached - predicate importStarResolvesTo(NameNode n, Module m) { + predicate importStarResolvesTo(Cfg::NameNode n, Module m) { m = getStarImported+(n.getEnclosingModule()) and globalNameDefinedInModule(n.getId(), m) and not isDefinedLocally(n.getNode()) @@ -99,7 +100,7 @@ module ImportStar { */ overlay[local] cached - ControlFlowNode potentialImportStarBase(Scope s) { - result = any(ImportStarNode n | n.getScope() = s).getModule() + Cfg::ControlFlowNode potentialImportStarBase(Scope s) { + result = any(Cfg::ImportStarNode n | n.getScope() = s).getModule() } } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll b/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll index 5def15fa3c8a..0704763bd890 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll @@ -170,6 +170,8 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import DataFlowPublic /** @@ -178,7 +180,7 @@ private import DataFlowPublic * This class abstracts away the differing representations of comprehensions and * for statements. */ -class ForTarget extends ControlFlowNode { +class ForTarget extends Cfg::ControlFlowNode { Expr source; ForTarget() { @@ -198,7 +200,7 @@ class ForTarget extends ControlFlowNode { } /** The LHS of an assignment, it also records the assigned value. */ -class AssignmentTarget extends ControlFlowNode { +class AssignmentTarget extends Cfg::ControlFlowNode { Expr value; AssignmentTarget() { @@ -209,7 +211,7 @@ class AssignmentTarget extends ControlFlowNode { } /** A direct (or top-level) target of an unpacking assignment. */ -class UnpackingAssignmentDirectTarget extends ControlFlowNode instanceof SequenceNode { +class UnpackingAssignmentDirectTarget extends Cfg::ControlFlowNode instanceof Cfg::SequenceNode { Expr value; UnpackingAssignmentDirectTarget() { @@ -222,7 +224,7 @@ class UnpackingAssignmentDirectTarget extends ControlFlowNode instanceof Sequenc } /** A (possibly recursive) target of an unpacking assignment. */ -class UnpackingAssignmentTarget extends ControlFlowNode { +class UnpackingAssignmentTarget extends Cfg::ControlFlowNode { UnpackingAssignmentTarget() { this instanceof UnpackingAssignmentDirectTarget or @@ -231,10 +233,10 @@ class UnpackingAssignmentTarget extends ControlFlowNode { } /** A (possibly recursive) target of an unpacking assignment which is also a sequence. */ -class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof SequenceNode { - ControlFlowNode getElement(int i) { result = super.getElement(i) } +class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof Cfg::SequenceNode { + Cfg::ControlFlowNode getElement(int i) { result = super.getElement(i) } - ControlFlowNode getAnElement() { result = this.getElement(_) } + Cfg::ControlFlowNode getAnElement() { result = this.getElement(_) } } /** @@ -255,7 +257,7 @@ predicate iterableUnpackingAssignmentFlowStep(Node nodeFrom, Node nodeTo) { predicate iterableUnpackingForReadStep(CfgNode nodeFrom, Content c, Node nodeTo) { exists(ForTarget target | nodeFrom.getNode().getNode() = target.getSource() and - target instanceof SequenceNode and + target instanceof Cfg::SequenceNode and nodeTo = TIterableSequenceNode(target) ) and ( @@ -323,11 +325,11 @@ predicate iterableUnpackingConvertingStoreStep(Node nodeFrom, Content c, Node no */ predicate iterableUnpackingElementReadStep(Node nodeFrom, Content c, Node nodeTo) { exists( - UnpackingAssignmentSequenceTarget target, int index, ControlFlowNode element, int starIndex + UnpackingAssignmentSequenceTarget target, int index, Cfg::ControlFlowNode element, int starIndex | - target.getElement(starIndex) instanceof StarredNode + target.getElement(starIndex) instanceof Cfg::StarredNode or - not exists(target.getAnElement().(StarredNode)) and + not exists(target.getAnElement().(Cfg::StarredNode)) and starIndex = -1 | nodeFrom.(CfgNode).getNode() = target and @@ -342,18 +344,18 @@ predicate iterableUnpackingElementReadStep(Node nodeFrom, Content c, Node nodeTo else c.(TupleElementContent).getIndex() >= index - 1 ) and ( - if element instanceof SequenceNode + if element instanceof Cfg::SequenceNode then // Step 5b nodeTo = TIterableSequenceNode(element) else - if element instanceof StarredNode + if element instanceof Cfg::StarredNode then // Step 5c nodeTo = TIterableElementNode(element) else // Step 5a - exists(MultiAssignmentDefinition mad | element = mad.getDefiningNode() | + exists(SsaImpl::MultiAssignmentDefinition mad | element = mad.getDefiningNode() | nodeTo.(CfgNode).getNode() = element ) ) @@ -366,7 +368,7 @@ predicate iterableUnpackingElementReadStep(Node nodeFrom, Content c, Node nodeTo * content type `ListElementContent`. */ predicate iterableUnpackingStarredElementStoreStep(Node nodeFrom, Content c, Node nodeTo) { - exists(ControlFlowNode starred, MultiAssignmentDefinition mad | + exists(Cfg::ControlFlowNode starred, SsaImpl::MultiAssignmentDefinition mad | starred.getNode() instanceof Starred and starred = mad.getDefiningNode() | diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/LocalSources.qll b/python/ql/lib/semmle/python/dataflow/new/internal/LocalSources.qll index 5cbe7b44ab30..9f63e2160ed8 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/LocalSources.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/LocalSources.qll @@ -9,6 +9,7 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg import DataFlowPublic private import DataFlowPrivate private import semmle.python.internal.CachedStages @@ -314,7 +315,7 @@ private module Cached { */ cached predicate subscript(LocalSourceNode node, CfgNode subscript, CfgNode index) { - exists(CfgNode seq, SubscriptNode subscriptNode | subscriptNode = subscript.getNode() | + exists(CfgNode seq, Cfg::SubscriptNode subscriptNode | subscriptNode = subscript.getNode() | node.flowsTo(seq) and seq.getNode() = subscriptNode.getObject() and index.getNode() = subscriptNode.getIndex() diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/MatchUnpacking.qll b/python/ql/lib/semmle/python/dataflow/new/internal/MatchUnpacking.qll index e72e378da528..f931c4606034 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/MatchUnpacking.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/MatchUnpacking.qll @@ -55,6 +55,7 @@ module; private import python private import DataFlowPublic +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Holds when there is flow from the subject `nodeFrom` to the (top-level) pattern `nodeTo` of a `match` statement. @@ -91,8 +92,8 @@ predicate matchAsFlowStep(Node nodeFrom, Node nodeTo) { or // the interior pattern flows to the alias nodeFrom.(CfgNode).getNode().getNode() = subject.getPattern() and - exists(PatternAliasDefinition pad | pad.getDefiningNode().getNode() = alias | - nodeTo.(CfgNode).getNode() = pad.getDefiningNode() + exists(Cfg::ControlFlowNode aliasCfg | aliasCfg.getNode() = alias | + nodeTo.(CfgNode).getNode() = aliasCfg ) ) } @@ -126,8 +127,8 @@ predicate matchLiteralFlowStep(Node nodeFrom, Node nodeTo) { predicate matchCaptureFlowStep(Node nodeFrom, Node nodeTo) { exists(MatchCapturePattern capture, Name var | capture.getVariable() = var | nodeFrom.(CfgNode).getNode().getNode() = capture and - exists(PatternCaptureDefinition pcd | pcd.getDefiningNode().getNode() = var | - nodeTo.(CfgNode).getNode() = pcd.getDefiningNode() + exists(Cfg::ControlFlowNode varCfg | varCfg.getNode() = var | + nodeTo.(CfgNode).getNode() = varCfg ) ) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll index 59433a96aae8..d406d30f1489 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -56,6 +56,16 @@ private module CfgForSsa implements BB::CfgSig { * We only track variables that are read at least once in their scope — * tracking write-only variables is unnecessary work. */ +/** + * A source variable for SSA, wrapping a Python AST `Variable`. + * + * We only track variables that are read at least once in their scope — + * tracking write-only variables would be unnecessary work — *except* + * for module-scope globals, where the "read" can be external (e.g. + * `import mymodule; mymodule.x`). Such globals are tracked + * unconditionally so that import-resolution can find their defining + * write. + */ private newtype TSsaSourceVariable = TPyVar(Py::Variable v) { // Has a use somewhere — read-relevant for SSA. @@ -63,6 +73,19 @@ private newtype TSsaSourceVariable = or // Or has a deletion (treated as a write that destroys the value). exists(Cfg::NameNode n | n.deletes(v)) + or + // Or is a module-scope global written in this module — must be + // tracked even if never read locally, because importers may read + // it as an attribute on the module object. + v.getScope() instanceof Py::Module and + exists(Cfg::NameNode n | n.defines(v)) + or + // Or is a parameter — parameters must always have a + // `ParameterDefinition` for dataflow argument-routing to work, + // even if the parameter is never read in its scope. Mirrors + // legacy ESSA's `ParameterDefinition` (which fired for every + // parameter binding regardless of liveness). + exists(Py::Parameter p | p.asName() = v.getAStore()) } /** @@ -72,11 +95,44 @@ class SsaSourceVariable extends TSsaSourceVariable { /** Gets the underlying Python AST variable. */ Py::Variable getVariable() { this = TPyVar(result) } + /** Gets the (textual) name of this variable. */ + string getName() { result = this.getVariable().getId() } + /** Gets a textual representation of this source variable. */ string toString() { result = this.getVariable().toString() } /** Gets the location of this source variable. */ Py::Location getLocation() { result = this.getVariable().getScope().getLocation() } + + /** Gets the scope in which this variable lives. */ + Py::Scope getScope() { result = this.getVariable().getScope() } + + /** + * Gets a use of this variable as it appears in the source — a `NameNode` + * that loads or deletes the variable. Mirrors legacy + * `SsaSourceVariable.getASourceUse()`. + */ + Cfg::ControlFlowNode getASourceUse() { + exists(Cfg::NameNode n | result = n | n.uses(this.getVariable()) or n.deletes(this.getVariable())) + } + + /** + * Gets an implicit use of this variable. The new SSA does not have + * implicit-use refinements, but we keep this for API parity — every + * normal-exit of the variable's scope counts as a sink, ensuring + * variables stay live to scope exit for taint-tracking. + */ + Cfg::ControlFlowNode getAnImplicitUse() { + result.isNormalExit() and result.getScope() = this.getScope() + } + + /** + * Gets a use of this variable — either an explicit source use or an + * implicit use at scope exit. Mirrors legacy `SsaSourceVariable.getAUse()`. + */ + Cfg::ControlFlowNode getAUse() { + result = this.getASourceUse() or result = this.getAnImplicitUse() + } } /** @@ -93,7 +149,13 @@ private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) { n.uses(v) and n.getScope() = s and not exists(Cfg::NameNode def | def.defines(v) and def.getScope() = s) - ) + ) and + // Match legacy ESSA: only create entry defs for variables that have + // at least one defining store somewhere — otherwise the entry def + // represents "nothing reaches here", which is the default anyway and + // introduces no useful flow. (Legacy's `ModuleVariable` required a + // store; this is the closure-aware generalisation.) + exists(Cfg::NameNode store | store.defines(v)) } /** @@ -164,11 +226,25 @@ private module SsaImplInput implements SsaImplCommon::InputSig { // ignore the flow steps from the synthetic sequence node to the real sequence node, // since we only support one level of content in type-trackers, and the nested // structure requires two levels at least to be useful. - not exists(SequenceNode outer | + not exists(Cfg::SequenceNode outer | outer.getAnElement() = nodeTo.asCfgNode() and IterableUnpacking::iterableUnpackingTupleFlowStep(nodeFrom, nodeTo) ) @@ -259,7 +264,7 @@ module TypeTrackingInput implements Shared::TypeTrackingInput { // Since we only support one level of content in type-trackers we don't actually // support `(aa, ab), (ba, bb) = ...`. Therefore we exclude the read-step from `(aa, // ab)` to `aa` (since it is not needed). - not exists(SequenceNode outer | + not exists(Cfg::SequenceNode outer | outer.getAnElement() = nodeFrom.asCfgNode() and IterableUnpacking::iterableUnpackingTupleFlowStep(_, nodeFrom) ) and @@ -269,7 +274,7 @@ module TypeTrackingInput implements Shared::TypeTrackingInput { IterableUnpacking::iterableUnpackingForReadStep(_, _, seq) and IterableUnpacking::iterableUnpackingConvertingReadStep(seq, _, elem) and IterableUnpacking::iterableUnpackingConvertingStoreStep(elem, _, nodeFrom) and - nodeFrom.asCfgNode() instanceof SequenceNode + nodeFrom.asCfgNode() instanceof Cfg::SequenceNode ) or TypeTrackerSummaryFlow::basicLoadStep(nodeFrom, nodeTo, content) @@ -306,13 +311,13 @@ module TypeTrackingInput implements Shared::TypeTrackingInput { // // nodeFrom is `expr` // nodeTo is entry node for `f` - exists(ScopeEntryDefinition e, SsaSourceVariable var, DefinitionNode def | + exists(SsaImpl::ScopeEntryDefinition e, SsaImpl::SsaSourceVariable var, Cfg::DefinitionNode def | e.getSourceVariable() = var and - var.hasDefiningNode(def) + def.getNode() = var.getVariable().getAStore() | nodeTo.(DataFlowPublic::ScopeEntryDefinitionNode).getDefinition() = e and nodeFrom.asCfgNode() = def and - var.getScope().getScope*() = nodeFrom.getScope() + var.getVariable().getScope().getScope*() = nodeFrom.getScope() ) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll b/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll index 5d647af09bc3..2cd2fedb1d7a 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/VariableCapture.qll @@ -3,6 +3,9 @@ overlay[local] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import DataFlowPublic private import semmle.python.dataflow.new.internal.DataFlowPrivate private import codeql.dataflow.VariableCapture as Shared @@ -14,10 +17,10 @@ private import codeql.dataflow.VariableCapture as Shared // The first is the main implementation, the second is a performance motivated restriction. // The restriction is to clear any `CapturedVariableContent` before writing a new one // to avoid long access paths (see the link for a nice explanation). -private module CaptureInput implements Shared::InputSig { +private module CaptureInput implements Shared::InputSig { private import python as PY - additional class ExprCfgNode extends ControlFlowNode { + additional class ExprCfgNode extends Cfg::ControlFlowNode { ExprCfgNode() { isExpressionNode(this) } } @@ -25,7 +28,9 @@ private module CaptureInput implements Shared::InputSig; +module Flow = Shared::Flow; private Flow::ClosureNode asClosureNode(Node n) { result = n.(SynthCaptureNode).getSynthesizedCaptureNode() diff --git a/python/ql/lib/semmle/python/frameworks/Bottle.qll b/python/ql/lib/semmle/python/frameworks/Bottle.qll index aa2c906948dc..9714f1967770 100644 --- a/python/ql/lib/semmle/python/frameworks/Bottle.qll +++ b/python/ql/lib/semmle/python/frameworks/Bottle.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.Concepts private import semmle.python.ApiGraphs private import semmle.python.dataflow.new.RemoteFlowSources @@ -73,7 +74,9 @@ module Bottle { /** A response returned by a view callable. */ class BottleReturnResponse extends Http::Server::HttpResponse::Range { BottleReturnResponse() { - this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode() + exists(View::ViewCallable vc, Return ret | + ret.getScope() = vc and this.asCfgNode().getNode() = ret.getValue() + ) } override DataFlow::Node getBody() { result = this } @@ -154,9 +157,9 @@ module Bottle { DataFlow::Node value; HeaderWriteSubscript() { - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | this.asCfgNode() = subscript and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and name.asCfgNode() = subscript.getIndex() and subscript.getObject() = headers().asSource().asCfgNode() ) diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index ee0ed4a84dd0..37d5b3357256 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking @@ -1305,7 +1306,7 @@ module PrivateDjango { dict.(DataFlow::MethodCallNode).calls(files, "dict") ) | - this.asCfgNode().(SubscriptNode).getObject() = dict.asCfgNode() + this.asCfgNode().(Cfg::SubscriptNode).getObject() = dict.asCfgNode() or this.(DataFlow::MethodCallNode).calls(dict, "get") ) @@ -1314,7 +1315,7 @@ module PrivateDjango { exists(DataFlow::AttrRead files, DataFlow::MethodCallNode getlistCall | files.accesses(instance(), "FILES") and getlistCall.calls(files, "getlist") and - this.asCfgNode().(SubscriptNode).getObject() = getlistCall.asCfgNode() + this.asCfgNode().(Cfg::SubscriptNode).getObject() = getlistCall.asCfgNode() ) } } @@ -2216,7 +2217,7 @@ module PrivateDjango { DataFlow::Node value; DjangoResponseCookieSubscriptWrite() { - exists(SubscriptNode subscript, DataFlow::AttrRead cookieLookup | + exists(Cfg::SubscriptNode subscript, DataFlow::AttrRead cookieLookup | // To give `this` a value, we need to choose between either LHS or RHS, // and just go with the LHS this.asCfgNode() = subscript @@ -2228,7 +2229,7 @@ module PrivateDjango { | cookieLookup.flowsTo(subscriptObj) ) and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and index.asCfgNode() = subscript.getIndex() ) } @@ -2249,7 +2250,7 @@ module PrivateDjango { DataFlow::Node value; DjangoResponseHeaderSubscriptWrite() { - exists(SubscriptNode subscript, DataFlow::AttrRead headerLookup | + exists(Cfg::SubscriptNode subscript, DataFlow::AttrRead headerLookup | // To give `this` a value, we need to choose between either LHS or RHS, // and just go with the LHS this.asCfgNode() = subscript @@ -2261,7 +2262,7 @@ module PrivateDjango { | headerLookup.flowsTo(subscriptObj) ) and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and index.asCfgNode() = subscript.getIndex() ) } @@ -2284,14 +2285,14 @@ module PrivateDjango { DataFlow::Node value; DjangoResponseSubscriptWrite() { - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | // To give `this` a value, we need to choose between either LHS or RHS, // and just go with the LHS this.asCfgNode() = subscript | subscript.getObject() = DjangoImpl::DjangoHttp::Response::HttpResponse::instance().asCfgNode() and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and index.asCfgNode() = subscript.getIndex() ) } @@ -2426,7 +2427,7 @@ module PrivateDjango { /** Gets a reference to the result of calling the `as_view` classmethod of this class. */ private DataFlow::TypeTrackingNode asViewResult(DataFlow::TypeTracker t) { t.start() and - result.asCfgNode().(CallNode).getFunction() = this.asViewRef().asCfgNode() + result.asCfgNode().(Cfg::CallNode).getFunction() = this.asViewRef().asCfgNode() or exists(DataFlow::TypeTracker t2 | result = this.asViewResult(t2).track(t2, t)) } @@ -2872,7 +2873,9 @@ module PrivateDjango { DataFlow::CfgNode { DjangoRedirectViewGetRedirectUrlReturn() { - node = any(GetRedirectUrlFunction f).getAReturnValueFlowNode() + exists(GetRedirectUrlFunction f, Return ret | + ret.getScope() = f and node.getNode() = ret.getValue() + ) } override DataFlow::Node getRedirectLocation() { result = this } diff --git a/python/ql/lib/semmle/python/frameworks/FastApi.qll b/python/ql/lib/semmle/python/frameworks/FastApi.qll index 9d52e9b4f57b..a8a35f485092 100644 --- a/python/ql/lib/semmle/python/frameworks/FastApi.qll +++ b/python/ql/lib/semmle/python/frameworks/FastApi.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking @@ -309,7 +310,11 @@ module FastApi { FastApiRouteSetup routeSetup; FastApiRequestHandlerReturn() { - node = routeSetup.getARequestHandler().getAReturnValueFlowNode() + exists(Function h, Return ret | + h = routeSetup.getARequestHandler() and + ret.getScope() = h and + node.getNode() = ret.getValue() + ) } override DataFlow::Node getBody() { result = this } @@ -438,7 +443,7 @@ module FastApi { DataFlow::Node value; HeaderSubscriptWrite() { - exists(SubscriptNode subscript, DataFlow::AttrRead headerLookup | + exists(Cfg::SubscriptNode subscript, DataFlow::AttrRead headerLookup | // To give `this` a value, we need to choose between either LHS or RHS, // and just go with the LHS this.asCfgNode() = subscript @@ -447,7 +452,7 @@ module FastApi { exists(DataFlow::Node subscriptObj | subscriptObj.asCfgNode() = subscript.getObject() | headerLookup.flowsTo(subscriptObj) ) and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and index.asCfgNode() = subscript.getIndex() ) } diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 67a50b1ce761..e2bb161eafe3 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -536,7 +536,7 @@ module Flask { FlaskRouteHandlerReturn() { exists(Function routeHandler | routeHandler = any(FlaskRouteSetup rs).getARequestHandler() and - node = routeHandler.getAReturnValueFlowNode() and + exists(Return ret | ret.getScope() = routeHandler and node.getNode() = ret.getValue()) and not this instanceof Flask::Response::InstanceSource ) } diff --git a/python/ql/lib/semmle/python/frameworks/Gradio.qll b/python/ql/lib/semmle/python/frameworks/Gradio.qll index 11109e150bfd..92aec4bd7732 100644 --- a/python/ql/lib/semmle/python/frameworks/Gradio.qll +++ b/python/ql/lib/semmle/python/frameworks/Gradio.qll @@ -4,6 +4,7 @@ */ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.RemoteFlowSources import semmle.python.dataflow.new.TaintTracking import semmle.python.ApiGraphs @@ -51,9 +52,9 @@ module Gradio { // limit only to lists of parameters given to `inputs`. ( ( - call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode + call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof Cfg::ListNode or - call.getParameter(1).asSink().asCfgNode() instanceof ListNode + call.getParameter(1).asSink().asCfgNode() instanceof Cfg::ListNode ) and ( this = call.getKeywordParameter("inputs").getASubscript().getAValueReachingSink() @@ -75,8 +76,8 @@ module Gradio { exists(GradioInput call | this = call.getParameter(0, "fn").getParameter(_).asSource() and // exclude lists of parameters given to `inputs` - not call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode and - not call.getParameter(1).asSink().asCfgNode() instanceof ListNode + not call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof Cfg::ListNode and + not call.getParameter(1).asSink().asCfgNode() instanceof Cfg::ListNode ) } @@ -105,16 +106,16 @@ module Gradio { // handle cases where there are multiple arguments passed as a list to `inputs` ( ( - node.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode + node.getKeywordParameter("inputs").asSink().asCfgNode() instanceof Cfg::ListNode or - node.getParameter(1).asSink().asCfgNode() instanceof ListNode + node.getParameter(1).asSink().asCfgNode() instanceof Cfg::ListNode ) and exists(int i | nodeTo = node.getParameter(0, "fn").getParameter(i).asSource() | nodeFrom.asCfgNode() = - node.getKeywordParameter("inputs").asSink().asCfgNode().(ListNode).getElement(i) + node.getKeywordParameter("inputs").asSink().asCfgNode().(Cfg::ListNode).getElement(i) or nodeFrom.asCfgNode() = - node.getParameter(1).asSink().asCfgNode().(ListNode).getElement(i) + node.getParameter(1).asSink().asCfgNode().(Cfg::ListNode).getElement(i) ) ) ) diff --git a/python/ql/lib/semmle/python/frameworks/MarkupSafe.qll b/python/ql/lib/semmle/python/frameworks/MarkupSafe.qll index 6e3b630ffa57..a4832b27ba40 100644 --- a/python/ql/lib/semmle/python/frameworks/MarkupSafe.qll +++ b/python/ql/lib/semmle/python/frameworks/MarkupSafe.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.Concepts @@ -46,7 +47,7 @@ module MarkupSafeModel { /** A direct instantiation of `markupsafe.Markup`. */ private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode { - override CallNode node; + override Cfg::CallNode node; ClassInstantiation() { this = classRef().getACall() } } @@ -64,7 +65,7 @@ module MarkupSafeModel { /** A string concatenation with a `markupsafe.Markup` involved. */ class StringConcat extends Markup::InstanceSource, DataFlow::CfgNode { - override BinaryExprNode node; + override Cfg::BinaryExprNode node; StringConcat() { node.getOp() instanceof Add and @@ -79,7 +80,7 @@ module MarkupSafeModel { /** A %-style string format with `markupsafe.Markup` as the format string. */ class PercentStringFormat extends Markup::InstanceSource, DataFlow::CfgNode { - override BinaryExprNode node; + override Cfg::BinaryExprNode node; PercentStringFormat() { node.getOp() instanceof Mod and diff --git a/python/ql/lib/semmle/python/frameworks/Pycurl.qll b/python/ql/lib/semmle/python/frameworks/Pycurl.qll index 7280eec5f61c..030e6c66f8de 100644 --- a/python/ql/lib/semmle/python/frameworks/Pycurl.qll +++ b/python/ql/lib/semmle/python/frameworks/Pycurl.qll @@ -7,6 +7,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.Concepts private import semmle.python.ApiGraphs private import semmle.python.frameworks.data.ModelsAsData @@ -56,7 +57,7 @@ module Pycurl { { OutgoingRequestCall() { this = setopt().getACall() and - this.getArg(0).asCfgNode().(AttrNode).getName() = "URL" + this.getArg(0).asCfgNode().(Cfg::AttrNode).getName() = "URL" } override DataFlow::Node getAUrlPart() { @@ -81,7 +82,7 @@ module Pycurl { private class CurlSslCall extends Http::Client::Request::Range instanceof DataFlow::CallCfgNode { CurlSslCall() { this = setopt().getACall() and - this.getArg(0).asCfgNode().(AttrNode).getName() = ["SSL_VERIFYPEER", "SSL_VERIFYHOST"] + this.getArg(0).asCfgNode().(Cfg::AttrNode).getName() = ["SSL_VERIFYPEER", "SSL_VERIFYHOST"] } override DataFlow::Node getAUrlPart() { none() } diff --git a/python/ql/lib/semmle/python/frameworks/Pydantic.qll b/python/ql/lib/semmle/python/frameworks/Pydantic.qll index c3d76835b429..1aa5c9142e6d 100644 --- a/python/ql/lib/semmle/python/frameworks/Pydantic.qll +++ b/python/ql/lib/semmle/python/frameworks/Pydantic.qll @@ -7,6 +7,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.Concepts @@ -93,7 +94,7 @@ module Pydantic { // be a Pydantic model. So `model[0]` will be an overapproximation, but should not // really cause problems (since we don't expect real code to contain such accesses) nodeFrom = instance() and - nodeTo.asCfgNode().(SubscriptNode).getObject() = nodeFrom.asCfgNode() + nodeTo.asCfgNode().(Cfg::SubscriptNode).getObject() = nodeFrom.asCfgNode() } /** diff --git a/python/ql/lib/semmle/python/frameworks/Pyramid.qll b/python/ql/lib/semmle/python/frameworks/Pyramid.qll index 63e19363fe86..2bd72a852536 100644 --- a/python/ql/lib/semmle/python/frameworks/Pyramid.qll +++ b/python/ql/lib/semmle/python/frameworks/Pyramid.qll @@ -166,7 +166,9 @@ module Pyramid { /** A response returned by a view callable. */ private class PyramidReturnResponse extends Http::Server::HttpResponse::Range { PyramidReturnResponse() { - this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode() and + exists(View::ViewCallable vc, Return ret | + ret.getScope() = vc and this.asCfgNode().getNode() = ret.getValue() + ) and not this = instance() } diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index 5d3b994880a1..d059c59e9af6 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -6,6 +6,7 @@ overlay[local?] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.dataflow.new.RemoteFlowSources @@ -1246,7 +1247,7 @@ module StdlibPrivate { /** An additional taint step for calls to `os.path.join` */ private class OsPathJoinCallAdditionalTaintStep extends TaintTracking::AdditionalTaintStep { override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { - exists(CallNode call | + exists(Cfg::CallNode call | nodeTo.asCfgNode() = call and call = OS::OsPath::join().getACall().asCfgNode() and call.getAnArg() = nodeFrom.asCfgNode() @@ -1317,13 +1318,13 @@ module StdlibPrivate { // run, so if we're able to, we only mark the first element as the command // (and not the arguments to the command). // - result.asCfgNode() = arg_args.asCfgNode().(SequenceNode).getElement(0) + result.asCfgNode() = arg_args.asCfgNode().(Cfg::SequenceNode).getElement(0) or // Either the "args" argument is not a sequence (which is valid) or we where // just not able to figure it out. Simply mark the "args" argument as the // command. // - not arg_args.asCfgNode() instanceof SequenceNode and + not arg_args.asCfgNode() instanceof Cfg::SequenceNode and result = arg_args ) ) @@ -1542,7 +1543,7 @@ module StdlibPrivate { * See https://docs.python.org/3/library/functions.html#eval */ private class BuiltinsEvalCall extends CodeExecution::Range, DataFlow::CallCfgNode { - override CallNode node; + override Cfg::CallNode node; BuiltinsEvalCall() { this = API::builtin("eval").getACall() } @@ -1923,7 +1924,7 @@ module StdlibPrivate { nodeFrom = instance().getAValueReachableFromSource() and nodeTo = [getvalueRef(), getfirstRef(), getlistRef()].getAValueReachableFromSource() or - nodeFrom.asCfgNode() = nodeTo.asCfgNode().(CallNode).getFunction() and + nodeFrom.asCfgNode() = nodeTo.asCfgNode().(Cfg::CallNode).getFunction() and ( nodeFrom = getvalueRef().getAValueReachableFromSource() and nodeTo = getvalueResult().asSource() @@ -1939,7 +1940,7 @@ module StdlibPrivate { nodeFrom in [ instance().getAValueReachableFromSource(), fieldList().getAValueReachableFromSource() ] and - nodeTo.asCfgNode().(SubscriptNode).getObject() = nodeFrom.asCfgNode() + nodeTo.asCfgNode().(Cfg::SubscriptNode).getObject() = nodeFrom.asCfgNode() or // Attributes on Field nodeFrom = field().getAValueReachableFromSource() and @@ -2254,8 +2255,8 @@ module StdlibPrivate { DataFlow::CfgNode { WsgirefSimpleServerApplicationReturn() { - exists(WsgirefSimpleServerApplication requestHandler | - node = requestHandler.getAReturnValueFlowNode() + exists(WsgirefSimpleServerApplication requestHandler, Return ret | + ret.getScope() = requestHandler and node.getNode() = ret.getValue() ) } @@ -2337,9 +2338,9 @@ module StdlibPrivate { DataFlow::Node value; HeaderWriteSubscript() { - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | this.asCfgNode() = subscript and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and name.asCfgNode() = subscript.getIndex() and subscript.getObject() = instance().asCfgNode() ) @@ -2681,7 +2682,7 @@ module StdlibPrivate { or // Data injection // Special handling of the `/` operator - exists(BinaryExprNode slash, DataFlow::Node pathOperand, DataFlow::TypeTracker t2 | + exists(Cfg::BinaryExprNode slash, DataFlow::Node pathOperand, DataFlow::TypeTracker t2 | slash.getOp() instanceof Div and pathOperand.asCfgNode() = slash.getAnOperand() and pathlibPath(t2).flowsTo(pathOperand) and @@ -2806,7 +2807,7 @@ module StdlibPrivate { pathlibPath().flowsTo(nodeTo) and ( // Special handling of the `/` operator - exists(BinaryExprNode slash, DataFlow::Node pathOperand | + exists(Cfg::BinaryExprNode slash, DataFlow::Node pathOperand | slash.getOp() instanceof Div and pathOperand.asCfgNode() = slash.getAnOperand() and pathlibPath().flowsTo(pathOperand) @@ -4605,9 +4606,9 @@ module StdlibPrivate { } override predicate propagatesFlow(string input, string output, boolean preservesValue) { - exists(CallNode c, string name, ControlFlowNode n, DataFlow::AttributeContent ac | - c.getFunction().(NameNode).getId() = "replace" or - c.getFunction().(AttrNode).getName() = "replace" + exists(Cfg::CallNode c, string name, Cfg::ControlFlowNode n, DataFlow::AttributeContent ac | + c.getFunction().(Cfg::NameNode).getId() = "replace" or + c.getFunction().(Cfg::AttrNode).getName() = "replace" | n = c.getArgByName(name) and ac.getAttribute() = name and @@ -5151,10 +5152,10 @@ module StdlibPrivate { * See https://docs.python.org/3.9/library/stdtypes.html#str.startswith */ private class StartswithCall extends Path::SafeAccessCheck::Range { - StartswithCall() { this.(CallNode).getFunction().(AttrNode).getName() = "startswith" } + StartswithCall() { this.(Cfg::CallNode).getFunction().(Cfg::AttrNode).getName() = "startswith" } - override predicate checks(ControlFlowNode node, boolean branch) { - node = this.(CallNode).getFunction().(AttrNode).getObject() and + override predicate checks(Cfg::ControlFlowNode node, boolean branch) { + node = this.(Cfg::CallNode).getFunction().(Cfg::AttrNode).getObject() and branch = true } } diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib/Urllib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib/Urllib.qll index 6b5764e55925..af670c009b6c 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib/Urllib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib/Urllib.qll @@ -8,6 +8,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.Concepts private import semmle.python.ApiGraphs private import semmle.python.security.dataflow.UrlRedirectCustomizations @@ -91,7 +92,7 @@ private module Urllib { * A read of the `netloc` attribute of a parsed URL as returned by `urllib.parse.urlparse`, * which is being checked in a way that is relevant for URL redirection vulnerabilities. */ - private predicate netlocCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { + private predicate netlocCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { exists(DataFlow::CallCfgNode urlParseCall, DataFlow::AttrRead netlocRead | urlParseCall = getUrlParseCall() and netlocRead = urlParseCall.getAnAttributeRead("netloc") and diff --git a/python/ql/lib/semmle/python/frameworks/Tornado.qll b/python/ql/lib/semmle/python/frameworks/Tornado.qll index 61cf7df316e7..3e57cdd3108a 100644 --- a/python/ql/lib/semmle/python/frameworks/Tornado.qll +++ b/python/ql/lib/semmle/python/frameworks/Tornado.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking @@ -72,9 +73,9 @@ module Tornado { DataFlow::Node value; TornadoHeaderSubscriptWrite() { - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | subscript.getObject() = instance().asCfgNode() and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and index.asCfgNode() = subscript.getIndex() and this.asCfgNode() = subscript ) @@ -422,7 +423,7 @@ module Tornado { // be able to do something more structured for providing modeling of the members // of a container-object. exists(DataFlow::AttrRead files | files.accesses(instance(), "cookies") | - this.asCfgNode().(SubscriptNode).getObject() = files.asCfgNode() + this.asCfgNode().(Cfg::SubscriptNode).getObject() = files.asCfgNode() or this.(DataFlow::MethodCallNode).calls(files, "get") ) @@ -479,20 +480,20 @@ module Tornado { // routing // --------------------------------------------------------------------------- /** Gets a sequence that defines a number of route rules */ - SequenceNode routeSetupRuleList() { - exists(CallNode call | + Cfg::SequenceNode routeSetupRuleList() { + exists(Cfg::CallNode call | call = any(TornadoModule::Web::Application::ClassInstantiation c).asCfgNode() | result in [call.getArg(0), call.getArgByName("handlers")] ) or - exists(CallNode call | + exists(Cfg::CallNode call | call.getFunction() = TornadoModule::Web::Application::add_handlers().asCfgNode() | result in [call.getArg(1), call.getArgByName("host_handlers")] ) or - result = routeSetupRuleList().getElement(_).(TupleNode).getElement(1) + result = routeSetupRuleList().getElement(_).(Cfg::TupleNode).getElement(1) } /** A tornado route setup. */ @@ -515,12 +516,12 @@ module Tornado { /** A route setup using a tuple. */ private class TornadoTupleRouteSetup extends TornadoRouteSetup, DataFlow::CfgNode { - override TupleNode node; + override Cfg::TupleNode node; TornadoTupleRouteSetup() { node = routeSetupRuleList().getElement(_) and count(node.getElement(_)) = 2 and - not node.getElement(1) instanceof SequenceNode + not node.getElement(1) instanceof Cfg::SequenceNode } override DataFlow::Node getUrlPatternArg() { result.asCfgNode() = node.getElement(0) } diff --git a/python/ql/lib/semmle/python/frameworks/Twisted.qll b/python/ql/lib/semmle/python/frameworks/Twisted.qll index 60aedd8fb582..6d32bf42ef11 100644 --- a/python/ql/lib/semmle/python/frameworks/Twisted.qll +++ b/python/ql/lib/semmle/python/frameworks/Twisted.qll @@ -182,7 +182,9 @@ private module Twisted { DataFlow::CfgNode { TwistedResourceRenderMethodReturn() { - this.asCfgNode() = any(TwistedResourceRenderMethod meth).getAReturnValueFlowNode() + exists(TwistedResourceRenderMethod meth, Return ret | + ret.getScope() = meth and this.asCfgNode().getNode() = ret.getValue() + ) } override DataFlow::Node getBody() { result = this } diff --git a/python/ql/lib/semmle/python/frameworks/Werkzeug.qll b/python/ql/lib/semmle/python/frameworks/Werkzeug.qll index d9150c8cfecd..d88171275da2 100644 --- a/python/ql/lib/semmle/python/frameworks/Werkzeug.qll +++ b/python/ql/lib/semmle/python/frameworks/Werkzeug.qll @@ -6,6 +6,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.ApiGraphs @@ -221,9 +222,9 @@ module Werkzeug { DataFlow::Node value; HeaderWriteSubscript() { - exists(SubscriptNode subscript | + exists(Cfg::SubscriptNode subscript | this.asCfgNode() = subscript and - value.asCfgNode() = subscript.(DefinitionNode).getValue() and + value.asCfgNode() = subscript.(Cfg::DefinitionNode).getValue() and name.asCfgNode() = subscript.getIndex() and subscript.getObject() = instance().asCfgNode() ) diff --git a/python/ql/lib/semmle/python/frameworks/Yaml.qll b/python/ql/lib/semmle/python/frameworks/Yaml.qll index 670fad75e6e3..8c5601f5a71d 100644 --- a/python/ql/lib/semmle/python/frameworks/Yaml.qll +++ b/python/ql/lib/semmle/python/frameworks/Yaml.qll @@ -8,6 +8,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.Concepts private import semmle.python.ApiGraphs @@ -28,7 +29,7 @@ private module Yaml { * See https://pyyaml.org/wiki/PyYAMLDocumentation (you will have to scroll down). */ private class YamlLoadCall extends Decoding::Range, DataFlow::CallCfgNode { - override CallNode node; + override Cfg::CallNode node; string func_name; YamlLoadCall() { diff --git a/python/ql/lib/semmle/python/frameworks/Yarl.qll b/python/ql/lib/semmle/python/frameworks/Yarl.qll index a1c602e6016b..670075764332 100644 --- a/python/ql/lib/semmle/python/frameworks/Yarl.qll +++ b/python/ql/lib/semmle/python/frameworks/Yarl.qll @@ -4,6 +4,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.TaintTracking private import semmle.python.Concepts @@ -111,7 +112,7 @@ module Yarl { } private predicate yarlUrlIsAbsoluteCall( - DataFlow::GuardNode g, ControlFlowNode node, boolean branch + DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch ) { exists(ClassInstantiation instance, DataFlow::MethodCallNode call | call.calls(instance, "is_absolute") and diff --git a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll index d91c4bbd78c0..23031c30772c 100644 --- a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll +++ b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll @@ -3,6 +3,7 @@ */ import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.Concepts as Concepts private import semmle.python.regex @@ -78,7 +79,7 @@ private module FindRegexMode { t.start() and exists(API::Node flag | flag_name = canonical_name(flag) and result = flag.asSource()) or - exists(BinaryExprNode binop, DataFlow::Node operand | + exists(Cfg::BinaryExprNode binop, DataFlow::Node operand | operand.getALocalSource() = re_flag_tracker(flag_name, t.continue()) and operand.asCfgNode() = binop.getAnOperand() and (binop.getOp() instanceof BitOr or binop.getOp() instanceof Add) and diff --git a/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll index 75a638fc3a42..38b05ecf05d1 100644 --- a/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll @@ -5,6 +5,7 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.DataFlow private import semmle.python.Concepts private import semmle.python.ApiGraphs @@ -111,7 +112,7 @@ module UrlRedirect { // Url redirection is a problem only if the user controls the prefix of the URL. // TODO: This is a copy of the taint-sanitizer from the old points-to query, which doesn't // cover formatting. - exists(BinaryExprNode string_concat | string_concat.getOp() instanceof Add | + exists(Cfg::BinaryExprNode string_concat | string_concat.getOp() instanceof Add | string_concat.getRight() = this.asCfgNode() ) } diff --git a/python/ql/lib/utils/test/dataflow/MaximalFlowTest.qll b/python/ql/lib/utils/test/dataflow/MaximalFlowTest.qll index cbd3b4c6aa51..f9b1b1c676d2 100644 --- a/python/ql/lib/utils/test/dataflow/MaximalFlowTest.qll +++ b/python/ql/lib/utils/test/dataflow/MaximalFlowTest.qll @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowPrivate import FlowTest @@ -23,7 +24,7 @@ import MakeTest> module MaximalFlowsConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node node) { exists(node.getLocation().getFile().getRelativePath()) and - not node.asCfgNode() instanceof CallNode and + not node.asCfgNode() instanceof Cfg::CallNode and not node.asCfgNode().getNode() instanceof Return and not node instanceof DataFlow::ParameterNode and not node instanceof DataFlow::PostUpdateNode and @@ -34,9 +35,9 @@ module MaximalFlowsConfig implements DataFlow::ConfigSig { predicate isSink(DataFlow::Node node) { exists(node.getLocation().getFile().getRelativePath()) and - not any(CallNode c).getArg(_) = node.asCfgNode() and + not any(Cfg::CallNode c).getArg(_) = node.asCfgNode() and not isArgumentNode(node, _, _) and - not node.asCfgNode().(NameNode).getId().matches("SINK%") and + not node.asCfgNode().(Cfg::NameNode).getId().matches("SINK%") and not DataFlow::localFlowStep(node, _) } } diff --git a/python/ql/lib/utils/test/dataflow/NormalDataflowTest.qll b/python/ql/lib/utils/test/dataflow/NormalDataflowTest.qll index 696b43d5f038..6c27cdc25239 100644 --- a/python/ql/lib/utils/test/dataflow/NormalDataflowTest.qll +++ b/python/ql/lib/utils/test/dataflow/NormalDataflowTest.qll @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import utils.test.dataflow.FlowTest import utils.test.dataflow.testConfig private import semmle.python.dataflow.new.internal.PrintNode @@ -19,7 +20,7 @@ query predicate missingAnnotationOnSink(Location location, string error, string TestConfig::isSink(sink) and // note: we only care about `SINK` and not `SINK_F`, so we have to reconstruct manually. exists(DataFlow::CallCfgNode call | - call.getFunction().asCfgNode().(NameNode).getId() = "SINK" and + call.getFunction().asCfgNode().(Cfg::NameNode).getId() = "SINK" and (sink = call.getArg(_) or sink = call.getArgByName(_)) ) and location = sink.getLocation() and diff --git a/python/ql/lib/utils/test/dataflow/NormalTaintTrackingTest.qll b/python/ql/lib/utils/test/dataflow/NormalTaintTrackingTest.qll index 753f8f61e137..df2f35c48fe0 100644 --- a/python/ql/lib/utils/test/dataflow/NormalTaintTrackingTest.qll +++ b/python/ql/lib/utils/test/dataflow/NormalTaintTrackingTest.qll @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import utils.test.dataflow.FlowTest import utils.test.dataflow.testTaintConfig private import semmle.python.dataflow.new.internal.PrintNode @@ -18,7 +19,7 @@ query predicate missingAnnotationOnSink(Location location, string error, string exists(DataFlow::Node sink | exists(DataFlow::CallCfgNode call | // note: we only care about `SINK` and not `SINK_F`, so we have to reconstruct manually. - call.getFunction().asCfgNode().(NameNode).getId() = "SINK" and + call.getFunction().asCfgNode().(Cfg::NameNode).getId() = "SINK" and (sink = call.getArg(_) or sink = call.getArgByName(_)) ) and location = sink.getLocation() and diff --git a/python/ql/lib/utils/test/dataflow/RoutingTest.qll b/python/ql/lib/utils/test/dataflow/RoutingTest.qll index e7ac4e872470..ffa52dbd550f 100644 --- a/python/ql/lib/utils/test/dataflow/RoutingTest.qll +++ b/python/ql/lib/utils/test/dataflow/RoutingTest.qll @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow import utils.test.InlineExpectationsTest private import semmle.python.dataflow.new.internal.PrintNode @@ -49,7 +50,7 @@ private string fromValue(DataFlow::Node fromNode) { pragma[inline] private string fromFunc(DataFlow::ArgumentNode fromNode) { - result = fromNode.getCall().getNode().(CallNode).getFunction().getNode().(Name).getId() + result = fromNode.getCall().getNode().(Cfg::CallNode).getFunction().getNode().(Name).getId() } pragma[inline] diff --git a/python/ql/lib/utils/test/dataflow/UnresolvedCalls.qll b/python/ql/lib/utils/test/dataflow/UnresolvedCalls.qll index a4dfb07ee90f..d48f98449383 100644 --- a/python/ql/lib/utils/test/dataflow/UnresolvedCalls.qll +++ b/python/ql/lib/utils/test/dataflow/UnresolvedCalls.qll @@ -1,15 +1,16 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.internal.PrintNode private import semmle.python.dataflow.new.internal.DataFlowPrivate as DataFlowPrivate private import semmle.python.ApiGraphs import utils.test.InlineExpectationsTest signature module UnresolvedCallExpectationsSig { - predicate unresolvedCall(CallNode call); + predicate unresolvedCall(Cfg::CallNode call); } module DefaultUnresolvedCallExpectations implements UnresolvedCallExpectationsSig { - predicate unresolvedCall(CallNode call) { + predicate unresolvedCall(Cfg::CallNode call) { not exists(DataFlowPrivate::DataFlowCall dfc | exists(dfc.getCallable()) and dfc.getNode() = call ) and @@ -24,7 +25,7 @@ module MakeUnresolvedCallExpectations { predicate hasActualResult(Location location, string element, string tag, string value) { exists(location.getFile().getRelativePath()) and - exists(CallNode call | Impl::unresolvedCall(call) | + exists(Cfg::CallNode call | Impl::unresolvedCall(call) | location = call.getLocation() and tag = "unresolved_call" and value = prettyExpr(call.getNode()) and diff --git a/python/ql/lib/utils/test/dataflow/testConfig.qll b/python/ql/lib/utils/test/dataflow/testConfig.qll index 552180eeaaf3..cfdf1e1a7c92 100644 --- a/python/ql/lib/utils/test/dataflow/testConfig.qll +++ b/python/ql/lib/utils/test/dataflow/testConfig.qll @@ -21,11 +21,12 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow module TestConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node node) { - node.(DataFlow::CfgNode).getNode().(NameNode).getId() = "SOURCE" + node.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId() = "SOURCE" or node.(DataFlow::CfgNode).getNode().getNode().(StringLiteral).getS() = "source" or @@ -37,7 +38,7 @@ module TestConfig implements DataFlow::ConfigSig { predicate isSink(DataFlow::Node node) { exists(DataFlow::CallCfgNode call | - call.getFunction().asCfgNode().(NameNode).getId() in ["SINK", "SINK_F"] and + call.getFunction().asCfgNode().(Cfg::NameNode).getId() in ["SINK", "SINK_F"] and (node = call.getArg(_) or node = call.getArgByName(_)) and not node = call.getArgByName("not_present_at_runtime") ) diff --git a/python/ql/lib/utils/test/dataflow/testTaintConfig.qll b/python/ql/lib/utils/test/dataflow/testTaintConfig.qll index c9770600eeb4..b784042901e0 100644 --- a/python/ql/lib/utils/test/dataflow/testTaintConfig.qll +++ b/python/ql/lib/utils/test/dataflow/testTaintConfig.qll @@ -21,12 +21,13 @@ */ private import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking module TestConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node node) { - node.(DataFlow::CfgNode).getNode().(NameNode).getId() = "SOURCE" + node.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId() = "SOURCE" or node.(DataFlow::CfgNode).getNode().getNode().(StringLiteral).getS() = "source" or @@ -37,8 +38,8 @@ module TestConfig implements DataFlow::ConfigSig { } predicate isSink(DataFlow::Node node) { - exists(CallNode call | - call.getFunction().(NameNode).getId() in ["SINK", "SINK_F"] and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() in ["SINK", "SINK_F"] and node.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } diff --git a/python/ql/test/experimental/meta/InlineTaintTest.qll b/python/ql/test/experimental/meta/InlineTaintTest.qll index 525775d5106c..5c20a9913beb 100644 --- a/python/ql/test/experimental/meta/InlineTaintTest.qll +++ b/python/ql/test/experimental/meta/InlineTaintTest.qll @@ -10,6 +10,7 @@ */ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.RemoteFlowSources @@ -19,14 +20,14 @@ private import semmle.python.Concepts DataFlow::Node shouldBeTainted() { exists(DataFlow::CallCfgNode call | - call.getFunction().asCfgNode().(NameNode).getId() = "ensure_tainted" and + call.getFunction().asCfgNode().(Cfg::NameNode).getId() = "ensure_tainted" and result in [call.getArg(_), call.getArgByName(_)] ) } DataFlow::Node shouldNotBeTainted() { exists(DataFlow::CallCfgNode call | - call.getFunction().asCfgNode().(NameNode).getId() = "ensure_not_tainted" and + call.getFunction().asCfgNode().(Cfg::NameNode).getId() = "ensure_not_tainted" and result in [call.getArg(_), call.getArgByName(_)] ) } @@ -36,13 +37,13 @@ DataFlow::Node shouldNotBeTainted() { module Conf { module TestTaintTrackingConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { - source.asCfgNode().(NameNode).getId() in [ + source.asCfgNode().(Cfg::NameNode).getId() in [ "TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT" ] or // User defined sources - exists(CallNode call | - call.getFunction().(NameNode).getId() = "taint" and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "taint" and source.(DataFlow::CfgNode).getNode() = call.getAnArg() ) or diff --git a/python/ql/test/library-tests/dataflow/summaries/TestSummaries.qll b/python/ql/test/library-tests/dataflow/summaries/TestSummaries.qll index 14d68455d621..87e1d7871010 100644 --- a/python/ql/test/library-tests/dataflow/summaries/TestSummaries.qll +++ b/python/ql/test/library-tests/dataflow/summaries/TestSummaries.qll @@ -2,6 +2,7 @@ overlay[local?] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.FlowSummary private import semmle.python.ApiGraphs @@ -17,7 +18,7 @@ module RecursionGuard { RecursionGuard() { this = "RecursionGuard" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this and + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this and (TT::callStep(_, _) implies any()) } @@ -33,7 +34,7 @@ private class SummarizedCallableIdentity extends SummarizedCallable::Range { SummarizedCallableIdentity() { this = "identity" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -50,7 +51,7 @@ private class SummarizedCallableApplyLambda extends SummarizedCallable::Range { SummarizedCallableApplyLambda() { this = "apply_lambda" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -70,7 +71,7 @@ private class SummarizedCallableReversed extends SummarizedCallable::Range { SummarizedCallableReversed() { this = "list_reversed" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -86,7 +87,7 @@ private class SummarizedCallableMap extends SummarizedCallable::Range { SummarizedCallableMap() { this = "list_map" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -106,7 +107,7 @@ private class SummarizedCallableAppend extends SummarizedCallable::Range { SummarizedCallableAppend() { this = "append_to_list" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } diff --git a/python/ql/test/library-tests/dataflow/tainttracking/TestTaintLib.qll b/python/ql/test/library-tests/dataflow/tainttracking/TestTaintLib.qll index 67a9f576cc75..f46f08aa509c 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/TestTaintLib.qll +++ b/python/ql/test/library-tests/dataflow/tainttracking/TestTaintLib.qll @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.PrintNode @@ -6,20 +7,20 @@ private import semmle.python.dataflow.new.internal.PrintNode module TestTaintTrackingConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { // Standard sources - source.(DataFlow::CfgNode).getNode().(NameNode).getId() in [ + source.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId() in [ "TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT" ] or // User defined sources - exists(CallNode call | - call.getFunction().(NameNode).getId() = "taint" and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "taint" and source.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } predicate isSink(DataFlow::Node sink) { - exists(CallNode call | - call.getFunction().(NameNode).getId() in ["ensure_tainted", "ensure_not_tainted"] and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() in ["ensure_tainted", "ensure_not_tainted"] and sink.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } diff --git a/python/ql/test/library-tests/dataflow/typetracking-summaries/TestSummaries.qll b/python/ql/test/library-tests/dataflow/typetracking-summaries/TestSummaries.qll index 57e0013b6e0e..4651d5c6180c 100644 --- a/python/ql/test/library-tests/dataflow/typetracking-summaries/TestSummaries.qll +++ b/python/ql/test/library-tests/dataflow/typetracking-summaries/TestSummaries.qll @@ -2,6 +2,7 @@ overlay[local?] module; private import python +private import semmle.python.controlflow.internal.Cfg as Cfg private import semmle.python.dataflow.new.FlowSummary private import semmle.python.ApiGraphs @@ -17,7 +18,7 @@ module RecursionGuard { RecursionGuard() { this = "TypeTrackingSummariesRecursionGuard" } override DataFlow::CallCfgNode getACall() { - result.getFunction().asCfgNode().(NameNode).getId() = this and + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this and (TT::callStep(_, _) implies any()) } @@ -41,7 +42,7 @@ private class SummarizedCallableIdentity extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -60,7 +61,7 @@ private class SummarizedCallableApplyLambda extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -82,7 +83,7 @@ private class SummarizedCallableReversed extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -100,7 +101,7 @@ private class SummarizedCallableMap extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -122,7 +123,7 @@ private class SummarizedCallableAppend extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -165,7 +166,7 @@ private class SummarizedCallableReadSecret extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } @@ -183,7 +184,7 @@ private class SummarizedCallableSetSecret extends SummarizedCallable::Range { override DataFlow::CallCfgNode getACall() { none() } override DataFlow::CallCfgNode getACallSimple() { - result.getFunction().asCfgNode().(NameNode).getId() = this + result.getFunction().asCfgNode().(Cfg::NameNode).getId() = this } override DataFlow::ArgumentNode getACallback() { result.asExpr().(Name).getId() = this } From 1a2be46cb5653d20ff6f0d5bae54e3e0c2c7fc38 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 07:21:06 +0000 Subject: [PATCH 65/72] Python: update dataflow tests for new CFG + shared SSA Test-side changes accompanying the dataflow migration: * Test queries (.ql) and shared test harness (TestSummaries, TestTaintLib) qualify CFG / SSA types with Cfg:: / SsaImpl::, bridge via AST (Name, Call, ...) instead of legacy NameNode / CallNode, and switch GlobalSsaVariable / EssaVariable usages to the new adapter API. * .expected files updated for legitimate precision and toString changes: - phi-node def-use edges newly exposed in def_use_counts. - scope-exit synthetic use surfaces one extra implicit use in use-use-counts. - For [empty]/[non-empty] outcome rows added in EnclosingCallable. - SsaSourceVariable / Global Variable label cosmetics normalised throughout. * Inline annotations: - typetracking/test.py: removed MISSING:tracked on lines 93/95 (now found), added SPURIOUS:tracked on line 108 (decorator over-reach). - global-flow/test.py: added SPURIOUS writes=g_mod on line 20 (correctly reports immediately-overwritten write). - tainttracking/customSanitizer/test.py: marked try/except: ensure_tainted(s) cases as MISSING: tainted (no-raise CFG abstraction does not connect try body to except body). - coverage/test.py: marked SINK(return_from_inner_scope([])) as MISSING: flow=... pending closer investigation. * regression/{dataflow,custom_dataflow}.expected: accept two if/else cond-correlation over-reaches (documented limitation; same imprecision applies under legacy semantics by design). After this change the dataflow library-tests stand at 62 of 64 passing; the two remaining failures are tracked under the ImportStarRefinement workstream. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dataflow/basic/callGraph.expected | 6 +- .../dataflow/basic/callGraphSinks.expected | 4 +- .../dataflow/basic/callGraphSources.expected | 4 +- .../dataflow/basic/global.expected | 146 ++++++------ .../dataflow/basic/globalStep.expected | 92 ++++---- .../dataflow/basic/local.expected | 78 +++---- .../dataflow/basic/localStep.expected | 20 +- .../dataflow/basic/maximalFlows.expected | 24 +- .../dataflow/basic/sinks.expected | 34 +-- .../dataflow/basic/sources.expected | 34 +-- .../callgraph_crosstalk/Arguments.expected | 26 +-- .../dataflow/coverage/argumentRoutingTest.ql | 22 +- .../dataflow/coverage/localFlow.expected | 22 +- .../library-tests/dataflow/coverage/test.py | 2 +- .../def-use-flow/def_use_counts.expected | 52 +++-- .../dataflow/def-use-flow/def_use_counts.ql | 30 +-- .../EnclosingCallable.expected | 49 ++-- .../fieldflow/UnresolvedCalls.expected | 8 +- .../dataflow/fieldflow/UnresolvedCalls.ql | 3 +- .../dataflow/global-flow/test.py | 2 +- .../dataflow/import-star/global.expected | 44 ++-- .../dataflow/method-calls/test.expected | 12 +- .../module-initialization/localFlow.ql | 6 +- .../regression/custom_dataflow.expected | 3 +- .../dataflow/regression/custom_dataflow.ql | 7 +- .../dataflow/regression/dataflow.expected | 54 ++--- .../strange-essaflow/testFlow.expected | 4 +- .../dataflow/strange-essaflow/testFlow.ql | 5 +- .../dataflow/summaries/summaries.expected | 218 +++++++++--------- .../basic/GlobalTaintTracking.expected | 4 +- .../basic/GlobalTaintTracking.ql | 7 +- .../basic/LocalTaintStep.expected | 10 +- .../customSanitizer/InlineTaintTest.expected | 58 ++--- .../customSanitizer/InlineTaintTest.ql | 13 +- .../tainttracking/customSanitizer/test.py | 6 +- .../typetracking-summaries/tracked.ql | 3 +- .../dataflow/typetracking/moduleattr.expected | 16 +- .../dataflow/typetracking/test.py | 6 +- .../dataflow/typetracking/tracked.ql | 6 +- .../typetracking_imports/highlight_problem.ql | 6 +- .../use-use-flow/use-use-counts.expected | 33 +-- .../dataflow/use-use-flow/use-use-counts.ql | 22 +- 42 files changed, 616 insertions(+), 585 deletions(-) diff --git a/python/ql/test/library-tests/dataflow/basic/callGraph.expected b/python/ql/test/library-tests/dataflow/basic/callGraph.expected index 222e11cb54f6..2ce5c66bc4a2 100644 --- a/python/ql/test/library-tests/dataflow/basic/callGraph.expected +++ b/python/ql/test/library-tests/dataflow/basic/callGraph.expected @@ -1,3 +1,3 @@ -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | +| test.py:4:10:4:10 | z | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:7:19:7:19 | a | test.py:1:19:1:19 | x | +| test.py:7:19:7:19 | a | test.py:7:5:7:20 | After obfuscated_id() | diff --git a/python/ql/test/library-tests/dataflow/basic/callGraphSinks.expected b/python/ql/test/library-tests/dataflow/basic/callGraphSinks.expected index e4b8f905530b..046e18186c03 100644 --- a/python/ql/test/library-tests/dataflow/basic/callGraphSinks.expected +++ b/python/ql/test/library-tests/dataflow/basic/callGraphSinks.expected @@ -1,3 +1,3 @@ | test.py:1:1:1:21 | SynthDictSplatParameterNode | -| test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | +| test.py:1:19:1:19 | x | +| test.py:7:5:7:20 | After obfuscated_id() | diff --git a/python/ql/test/library-tests/dataflow/basic/callGraphSources.expected b/python/ql/test/library-tests/dataflow/basic/callGraphSources.expected index 4023ba8f3ea1..a8fea599f65c 100644 --- a/python/ql/test/library-tests/dataflow/basic/callGraphSources.expected +++ b/python/ql/test/library-tests/dataflow/basic/callGraphSources.expected @@ -1,2 +1,2 @@ -| test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:7:19:7:19 | ControlFlowNode for a | +| test.py:4:10:4:10 | z | +| test.py:7:19:7:19 | a | diff --git a/python/ql/test/library-tests/dataflow/basic/global.expected b/python/ql/test/library-tests/dataflow/basic/global.expected index 9e0ef2e6751b..c5794bfca970 100644 --- a/python/ql/test/library-tests/dataflow/basic/global.expected +++ b/python/ql/test/library-tests/dataflow/basic/global.expected @@ -1,73 +1,73 @@ -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:1:7:1 | ControlFlowNode for b | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | +| test.py:1:1:1:21 | FunctionExpr | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:19:1:19 | x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:1:19:1:19 | x | test.py:3:3:3:3 | z | +| test.py:1:19:1:19 | x | test.py:3:7:3:7 | y | +| test.py:1:19:1:19 | x | test.py:4:10:4:10 | z | +| test.py:1:19:1:19 | x | test.py:7:1:7:1 | b | +| test.py:1:19:1:19 | x | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:2:3:2:3 | y | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:3:2:3 | y | test.py:4:10:4:10 | z | +| test.py:2:3:2:3 | y | test.py:7:1:7:1 | b | +| test.py:2:3:2:3 | y | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:2:7:2:7 | x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | test.py:3:3:3:3 | z | +| test.py:2:7:2:7 | x | test.py:3:7:3:7 | y | +| test.py:2:7:2:7 | x | test.py:4:10:4:10 | z | +| test.py:2:7:2:7 | x | test.py:7:1:7:1 | b | +| test.py:2:7:2:7 | x | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:3:3:3:3 | z | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:3:3:3 | z | test.py:7:1:7:1 | b | +| test.py:3:3:3:3 | z | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:3:7:3:7 | y | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | test.py:4:10:4:10 | z | +| test.py:3:7:3:7 | y | test.py:7:1:7:1 | b | +| test.py:3:7:3:7 | y | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:4:10:4:10 | z | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:4:10:4:10 | z | test.py:7:1:7:1 | b | +| test.py:4:10:4:10 | z | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:6:1:6:1 | a | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | +| test.py:6:1:6:1 | a | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:6:1:6:1 | a | test.py:1:19:1:19 | x | +| test.py:6:1:6:1 | a | test.py:2:3:2:3 | y | +| test.py:6:1:6:1 | a | test.py:2:7:2:7 | x | +| test.py:6:1:6:1 | a | test.py:3:3:3:3 | z | +| test.py:6:1:6:1 | a | test.py:3:7:3:7 | y | +| test.py:6:1:6:1 | a | test.py:4:10:4:10 | z | +| test.py:6:1:6:1 | a | test.py:7:1:7:1 | b | +| test.py:6:1:6:1 | a | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:6:1:6:1 | a | test.py:7:19:7:19 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:6:5:6:6 | IntegerLiteral | test.py:1:19:1:19 | x | +| test.py:6:5:6:6 | IntegerLiteral | test.py:2:3:2:3 | y | +| test.py:6:5:6:6 | IntegerLiteral | test.py:2:7:2:7 | x | +| test.py:6:5:6:6 | IntegerLiteral | test.py:3:3:3:3 | z | +| test.py:6:5:6:6 | IntegerLiteral | test.py:3:7:3:7 | y | +| test.py:6:5:6:6 | IntegerLiteral | test.py:4:10:4:10 | z | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:1:6:1 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:1:7:1 | b | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:19:7:19 | a | +| test.py:7:1:7:1 | b | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | +| test.py:7:19:7:19 | a | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:7:19:7:19 | a | test.py:1:19:1:19 | x | +| test.py:7:19:7:19 | a | test.py:2:3:2:3 | y | +| test.py:7:19:7:19 | a | test.py:2:7:2:7 | x | +| test.py:7:19:7:19 | a | test.py:3:3:3:3 | z | +| test.py:7:19:7:19 | a | test.py:3:7:3:7 | y | +| test.py:7:19:7:19 | a | test.py:4:10:4:10 | z | +| test.py:7:19:7:19 | a | test.py:7:1:7:1 | b | +| test.py:7:19:7:19 | a | test.py:7:5:7:20 | After obfuscated_id() | diff --git a/python/ql/test/library-tests/dataflow/basic/globalStep.expected b/python/ql/test/library-tests/dataflow/basic/globalStep.expected index 26d8902e7bbe..dd758848448f 100644 --- a/python/ql/test/library-tests/dataflow/basic/globalStep.expected +++ b/python/ql/test/library-tests/dataflow/basic/globalStep.expected @@ -1,46 +1,46 @@ -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:7:1:7:1 | ControlFlowNode for b | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:4:10:4:10 | z | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:4:10:4:10 | z | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:6:1:6:1 | a | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | +| test.py:6:1:6:1 | a | test.py:7:19:7:19 | a | +| test.py:6:1:6:1 | a | test.py:7:19:7:19 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:1:6:1 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:1:6:1 | a | +| test.py:7:1:7:1 | b | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | +| test.py:7:19:7:19 | a | test.py:1:19:1:19 | x | +| test.py:7:19:7:19 | a | test.py:1:19:1:19 | x | +| test.py:7:19:7:19 | a | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:7:19:7:19 | a | test.py:7:5:7:20 | After obfuscated_id() | diff --git a/python/ql/test/library-tests/dataflow/basic/local.expected b/python/ql/test/library-tests/dataflow/basic/local.expected index 96d402325129..4ed0993e4e0c 100644 --- a/python/ql/test/library-tests/dataflow/basic/local.expected +++ b/python/ql/test/library-tests/dataflow/basic/local.expected @@ -3,45 +3,45 @@ | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:1:1:21 | FunctionExpr | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:7:5:7:17 | obfuscated_id | | test.py:1:1:1:21 | SynthDictSplatParameterNode | test.py:1:1:1:21 | SynthDictSplatParameterNode | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:4:10:4:10 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:1:7:1 | ControlFlowNode for b | test.py:7:1:7:1 | ControlFlowNode for b | +| test.py:1:5:1:17 | obfuscated_id | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:19:1:19 | x | test.py:1:19:1:19 | x | +| test.py:1:19:1:19 | x | test.py:2:3:2:3 | y | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:1:19:1:19 | x | test.py:3:3:3:3 | z | +| test.py:1:19:1:19 | x | test.py:3:7:3:7 | y | +| test.py:1:19:1:19 | x | test.py:4:10:4:10 | z | +| test.py:2:3:2:3 | y | test.py:2:3:2:3 | y | +| test.py:2:3:2:3 | y | test.py:3:3:3:3 | z | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:3:2:3 | y | test.py:4:10:4:10 | z | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | test.py:2:7:2:7 | x | +| test.py:2:7:2:7 | x | test.py:3:3:3:3 | z | +| test.py:2:7:2:7 | x | test.py:3:7:3:7 | y | +| test.py:2:7:2:7 | x | test.py:4:10:4:10 | z | +| test.py:3:3:3:3 | z | test.py:3:3:3:3 | z | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | test.py:3:7:3:7 | y | +| test.py:3:7:3:7 | y | test.py:4:10:4:10 | z | +| test.py:4:10:4:10 | z | test.py:4:10:4:10 | z | +| test.py:6:1:6:1 | a | test.py:6:1:6:1 | a | +| test.py:6:1:6:1 | a | test.py:7:19:7:19 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:1:6:1 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:5:6:6 | IntegerLiteral | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:19:7:19 | a | +| test.py:7:1:7:1 | b | test.py:7:1:7:1 | b | | test.py:7:5:7:17 | Capturing closure argument | test.py:7:5:7:17 | Capturing closure argument | -| test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | | test.py:7:5:7:17 | [post] Capturing closure argument | test.py:7:5:7:17 | [post] Capturing closure argument | -| test.py:7:5:7:17 | [post] ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | [post] ControlFlowNode for obfuscated_id | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:7:5:7:20 | [pre] ControlFlowNode for obfuscated_id() | test.py:7:5:7:20 | [pre] ControlFlowNode for obfuscated_id() | -| test.py:7:19:7:19 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:19:7:19 | [post] ControlFlowNode for a | test.py:7:19:7:19 | [post] ControlFlowNode for a | +| test.py:7:5:7:17 | [post] obfuscated_id | test.py:7:5:7:17 | [post] obfuscated_id | +| test.py:7:5:7:17 | obfuscated_id | test.py:7:5:7:17 | obfuscated_id | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:5:7:20 | After obfuscated_id() | +| test.py:7:5:7:20 | [pre] After obfuscated_id() | test.py:7:5:7:20 | [pre] After obfuscated_id() | +| test.py:7:19:7:19 | [post] a | test.py:7:19:7:19 | [post] a | +| test.py:7:19:7:19 | a | test.py:7:19:7:19 | a | diff --git a/python/ql/test/library-tests/dataflow/basic/localStep.expected b/python/ql/test/library-tests/dataflow/basic/localStep.expected index ce190945d363..6631b2123f2b 100644 --- a/python/ql/test/library-tests/dataflow/basic/localStep.expected +++ b/python/ql/test/library-tests/dataflow/basic/localStep.expected @@ -1,10 +1,10 @@ -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:2:3:2:3 | ControlFlowNode for y | test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:3:3:3:3 | ControlFlowNode for z | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | +| test.py:1:1:1:21 | FunctionExpr | test.py:1:5:1:17 | obfuscated_id | +| test.py:1:5:1:17 | obfuscated_id | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:19:1:19 | x | test.py:2:7:2:7 | x | +| test.py:2:3:2:3 | y | test.py:3:7:3:7 | y | +| test.py:2:7:2:7 | x | test.py:2:3:2:3 | y | +| test.py:3:3:3:3 | z | test.py:4:10:4:10 | z | +| test.py:3:7:3:7 | y | test.py:3:3:3:3 | z | +| test.py:6:1:6:1 | a | test.py:7:19:7:19 | a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:6:1:6:1 | a | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | diff --git a/python/ql/test/library-tests/dataflow/basic/maximalFlows.expected b/python/ql/test/library-tests/dataflow/basic/maximalFlows.expected index 421918620455..b820e3d2d7b0 100644 --- a/python/ql/test/library-tests/dataflow/basic/maximalFlows.expected +++ b/python/ql/test/library-tests/dataflow/basic/maximalFlows.expected @@ -1,12 +1,12 @@ -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:1:19:1:19 | ControlFlowNode for x | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:1:7:1 | ControlFlowNode for b | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | test.py:7:1:7:1 | ControlFlowNode for b | +| test.py:1:1:1:21 | FunctionExpr | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | +| test.py:1:1:1:21 | FunctionExpr | test.py:7:5:7:17 | obfuscated_id | +| test.py:1:19:1:19 | x | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:1:19:1:19 | x | test.py:4:10:4:10 | z | +| test.py:1:19:1:19 | x | test.py:7:1:7:1 | b | +| test.py:6:5:6:6 | IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | +| test.py:6:5:6:6 | IntegerLiteral | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:6:5:6:6 | IntegerLiteral | test.py:4:10:4:10 | z | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:1:7:1 | b | +| test.py:6:5:6:6 | IntegerLiteral | test.py:7:19:7:19 | a | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | +| test.py:7:5:7:20 | After obfuscated_id() | test.py:7:1:7:1 | b | diff --git a/python/ql/test/library-tests/dataflow/basic/sinks.expected b/python/ql/test/library-tests/dataflow/basic/sinks.expected index 80055f9a2f2b..d516fa208956 100644 --- a/python/ql/test/library-tests/dataflow/basic/sinks.expected +++ b/python/ql/test/library-tests/dataflow/basic/sinks.expected @@ -3,23 +3,23 @@ | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | +| test.py:1:1:1:21 | FunctionExpr | | test.py:1:1:1:21 | SynthDictSplatParameterNode | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | -| test.py:7:1:7:1 | ControlFlowNode for b | +| test.py:1:5:1:17 | obfuscated_id | +| test.py:1:19:1:19 | x | +| test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | +| test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | +| test.py:4:10:4:10 | z | +| test.py:6:1:6:1 | a | +| test.py:6:5:6:6 | IntegerLiteral | +| test.py:7:1:7:1 | b | | test.py:7:5:7:17 | Capturing closure argument | -| test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | | test.py:7:5:7:17 | [post] Capturing closure argument | -| test.py:7:5:7:17 | [post] ControlFlowNode for obfuscated_id | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:7:5:7:20 | [pre] ControlFlowNode for obfuscated_id() | -| test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:19:7:19 | [post] ControlFlowNode for a | +| test.py:7:5:7:17 | [post] obfuscated_id | +| test.py:7:5:7:17 | obfuscated_id | +| test.py:7:5:7:20 | After obfuscated_id() | +| test.py:7:5:7:20 | [pre] After obfuscated_id() | +| test.py:7:19:7:19 | [post] a | +| test.py:7:19:7:19 | a | diff --git a/python/ql/test/library-tests/dataflow/basic/sources.expected b/python/ql/test/library-tests/dataflow/basic/sources.expected index 80055f9a2f2b..d516fa208956 100644 --- a/python/ql/test/library-tests/dataflow/basic/sources.expected +++ b/python/ql/test/library-tests/dataflow/basic/sources.expected @@ -3,23 +3,23 @@ | test.py:0:0:0:0 | ModuleVariableNode in Module test for a | | test.py:0:0:0:0 | ModuleVariableNode in Module test for b | | test.py:0:0:0:0 | ModuleVariableNode in Module test for obfuscated_id | -| test.py:1:1:1:21 | ControlFlowNode for FunctionExpr | +| test.py:1:1:1:21 | FunctionExpr | | test.py:1:1:1:21 | SynthDictSplatParameterNode | -| test.py:1:5:1:17 | ControlFlowNode for obfuscated_id | -| test.py:1:19:1:19 | ControlFlowNode for x | -| test.py:2:3:2:3 | ControlFlowNode for y | -| test.py:2:7:2:7 | ControlFlowNode for x | -| test.py:3:3:3:3 | ControlFlowNode for z | -| test.py:3:7:3:7 | ControlFlowNode for y | -| test.py:4:10:4:10 | ControlFlowNode for z | -| test.py:6:1:6:1 | ControlFlowNode for a | -| test.py:6:5:6:6 | ControlFlowNode for IntegerLiteral | -| test.py:7:1:7:1 | ControlFlowNode for b | +| test.py:1:5:1:17 | obfuscated_id | +| test.py:1:19:1:19 | x | +| test.py:2:3:2:3 | y | +| test.py:2:7:2:7 | x | +| test.py:3:3:3:3 | z | +| test.py:3:7:3:7 | y | +| test.py:4:10:4:10 | z | +| test.py:6:1:6:1 | a | +| test.py:6:5:6:6 | IntegerLiteral | +| test.py:7:1:7:1 | b | | test.py:7:5:7:17 | Capturing closure argument | -| test.py:7:5:7:17 | ControlFlowNode for obfuscated_id | | test.py:7:5:7:17 | [post] Capturing closure argument | -| test.py:7:5:7:17 | [post] ControlFlowNode for obfuscated_id | -| test.py:7:5:7:20 | ControlFlowNode for obfuscated_id() | -| test.py:7:5:7:20 | [pre] ControlFlowNode for obfuscated_id() | -| test.py:7:19:7:19 | ControlFlowNode for a | -| test.py:7:19:7:19 | [post] ControlFlowNode for a | +| test.py:7:5:7:17 | [post] obfuscated_id | +| test.py:7:5:7:17 | obfuscated_id | +| test.py:7:5:7:20 | After obfuscated_id() | +| test.py:7:5:7:20 | [pre] After obfuscated_id() | +| test.py:7:19:7:19 | [post] a | +| test.py:7:19:7:19 | a | diff --git a/python/ql/test/library-tests/dataflow/callgraph_crosstalk/Arguments.expected b/python/ql/test/library-tests/dataflow/callgraph_crosstalk/Arguments.expected index 99c2d987d16d..c8efb96e5648 100644 --- a/python/ql/test/library-tests/dataflow/callgraph_crosstalk/Arguments.expected +++ b/python/ql/test/library-tests/dataflow/callgraph_crosstalk/Arguments.expected @@ -1,13 +1,13 @@ -| test.py:32:8:32:23 | CrosstalkTestX() | test.py:9:5:9:23 | Function __init__ | test.py:32:8:32:23 | [pre] ControlFlowNode for CrosstalkTestX() | self | -| test.py:33:8:33:23 | CrosstalkTestY() | test.py:21:5:21:23 | Function __init__ | test.py:33:8:33:23 | [pre] ControlFlowNode for CrosstalkTestY() | self | -| test.py:43:1:43:8 | func() | test.py:13:5:13:26 | Function setx | test.py:36:12:36:15 | ControlFlowNode for objx | self | -| test.py:43:1:43:8 | func() | test.py:13:5:13:26 | Function setx | test.py:43:6:43:7 | ControlFlowNode for IntegerLiteral | position 0 | -| test.py:43:1:43:8 | func() | test.py:25:5:25:26 | Function sety | test.py:38:12:38:15 | ControlFlowNode for objy | self | -| test.py:43:1:43:8 | func() | test.py:25:5:25:26 | Function sety | test.py:43:6:43:7 | ControlFlowNode for IntegerLiteral | position 0 | -| test.py:51:1:51:8 | func() | test.py:16:5:16:30 | Function setvalue | test.py:47:12:47:15 | ControlFlowNode for objx | self | -| test.py:51:1:51:8 | func() | test.py:16:5:16:30 | Function setvalue | test.py:51:6:51:7 | ControlFlowNode for IntegerLiteral | position 0 | -| test.py:51:1:51:8 | func() | test.py:28:5:28:30 | Function setvalue | test.py:49:12:49:15 | ControlFlowNode for objy | self | -| test.py:51:1:51:8 | func() | test.py:28:5:28:30 | Function setvalue | test.py:51:6:51:7 | ControlFlowNode for IntegerLiteral | position 0 | -| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:63:12:63:12 | ControlFlowNode for a | self | -| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:70:6:70:7 | ControlFlowNode for IntegerLiteral | position 0 | -| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:70:6:70:7 | ControlFlowNode for IntegerLiteral | self | +| test.py:32:8:32:23 | CrosstalkTestX() | test.py:9:5:9:23 | Function __init__ | test.py:32:8:32:23 | [pre] After CrosstalkTestX() | self | +| test.py:33:8:33:23 | CrosstalkTestY() | test.py:21:5:21:23 | Function __init__ | test.py:33:8:33:23 | [pre] After CrosstalkTestY() | self | +| test.py:43:1:43:8 | func() | test.py:13:5:13:26 | Function setx | test.py:36:12:36:15 | objx | self | +| test.py:43:1:43:8 | func() | test.py:13:5:13:26 | Function setx | test.py:43:6:43:7 | IntegerLiteral | position 0 | +| test.py:43:1:43:8 | func() | test.py:25:5:25:26 | Function sety | test.py:38:12:38:15 | objy | self | +| test.py:43:1:43:8 | func() | test.py:25:5:25:26 | Function sety | test.py:43:6:43:7 | IntegerLiteral | position 0 | +| test.py:51:1:51:8 | func() | test.py:16:5:16:30 | Function setvalue | test.py:47:12:47:15 | objx | self | +| test.py:51:1:51:8 | func() | test.py:16:5:16:30 | Function setvalue | test.py:51:6:51:7 | IntegerLiteral | position 0 | +| test.py:51:1:51:8 | func() | test.py:28:5:28:30 | Function setvalue | test.py:49:12:49:15 | objy | self | +| test.py:51:1:51:8 | func() | test.py:28:5:28:30 | Function setvalue | test.py:51:6:51:7 | IntegerLiteral | position 0 | +| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:63:12:63:12 | a | self | +| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:70:6:70:7 | IntegerLiteral | position 0 | +| test.py:70:1:70:8 | func() | test.py:58:5:58:33 | Function foo | test.py:70:6:70:7 | IntegerLiteral | self | diff --git a/python/ql/test/library-tests/dataflow/coverage/argumentRoutingTest.ql b/python/ql/test/library-tests/dataflow/coverage/argumentRoutingTest.ql index 7851dc4dda80..bd5ad3059f9b 100644 --- a/python/ql/test/library-tests/dataflow/coverage/argumentRoutingTest.ql +++ b/python/ql/test/library-tests/dataflow/coverage/argumentRoutingTest.ql @@ -1,4 +1,6 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowPrivate as DataFlowPrivate import utils.test.dataflow.RoutingTest @@ -26,21 +28,21 @@ class ArgNumber extends int { module ArgumentRoutingConfig implements DataFlow::ConfigSig { additional predicate isArgSource(DataFlow::Node node, ArgNumber argNumber) { - node.(DataFlow::CfgNode).getNode().(NameNode).getId() = "arg" + argNumber + node.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId() = "arg" + argNumber } predicate isSource(DataFlow::Node node) { isArgSource(node, _) } additional predicate isGoodSink(DataFlow::Node node, ArgNumber argNumber) { - exists(CallNode call | - call.getFunction().(NameNode).getId() = "SINK" + argNumber and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "SINK" + argNumber and node.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } additional predicate isBadSink(DataFlow::Node node, ArgNumber argNumber) { - exists(CallNode call | - call.getFunction().(NameNode).getId() = "SINK" + argNumber + "_F" and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "SINK" + argNumber + "_F" and node.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } @@ -60,17 +62,17 @@ module ArgumentRoutingFlow = DataFlow::Global; module Argument1ExtraRoutingConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node node) { - exists(AssignmentDefinition def, DataFlow::CallCfgNode call | + exists(SsaImpl::AssignmentDefinition def, DataFlow::CallCfgNode call | def.getDefiningNode() = node.(DataFlow::CfgNode).getNode() and def.getValue() = call.getNode() and - call.getFunction().asCfgNode().(NameNode).getId().matches("With\\_%") + call.getFunction().asCfgNode().(Cfg::NameNode).getId().matches("With\\_%") ) and - node.(DataFlow::CfgNode).getNode().(NameNode).getId().matches("with\\_%") + node.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId().matches("with\\_%") } predicate isSink(DataFlow::Node node) { - exists(CallNode call | - call.getFunction().(NameNode).getId() = "SINK1" and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "SINK1" and node.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } diff --git a/python/ql/test/library-tests/dataflow/coverage/localFlow.expected b/python/ql/test/library-tests/dataflow/coverage/localFlow.expected index 665fb1aa12b8..303a2736935d 100644 --- a/python/ql/test/library-tests/dataflow/coverage/localFlow.expected +++ b/python/ql/test/library-tests/dataflow/coverage/localFlow.expected @@ -1,11 +1,11 @@ -| test.py:41:1:41:33 | Entry definition for SsaSourceVariable NONSOURCE | test.py:42:10:42:18 | ControlFlowNode for NONSOURCE | -| test.py:41:1:41:33 | Entry definition for SsaSourceVariable SINK | test.py:44:5:44:8 | ControlFlowNode for SINK | -| test.py:41:1:41:33 | Entry definition for SsaSourceVariable SOURCE | test.py:42:21:42:26 | ControlFlowNode for SOURCE | -| test.py:42:5:42:5 | ControlFlowNode for x | test.py:43:9:43:9 | ControlFlowNode for x | -| test.py:42:10:42:26 | ControlFlowNode for Tuple | test.py:42:5:42:5 | ControlFlowNode for x | -| test.py:43:5:43:5 | ControlFlowNode for y | test.py:44:10:44:10 | ControlFlowNode for y | -| test.py:43:9:43:12 | ControlFlowNode for Subscript | test.py:43:5:43:5 | ControlFlowNode for y | -| test.py:208:1:208:53 | Entry definition for SsaSourceVariable SINK | test.py:210:5:210:8 | ControlFlowNode for SINK | -| test.py:208:1:208:53 | Entry definition for SsaSourceVariable SOURCE | test.py:209:25:209:30 | ControlFlowNode for SOURCE | -| test.py:209:5:209:5 | ControlFlowNode for x | test.py:210:10:210:10 | ControlFlowNode for x | -| test.py:209:9:209:68 | ControlFlowNode for ListComp | test.py:209:5:209:5 | ControlFlowNode for x | +| test.py:41:1:41:33 | Entry definition for Global Variable NONSOURCE | test.py:42:10:42:18 | NONSOURCE | +| test.py:41:1:41:33 | Entry definition for Global Variable SINK | test.py:44:5:44:8 | SINK | +| test.py:41:1:41:33 | Entry definition for Global Variable SOURCE | test.py:42:21:42:26 | SOURCE | +| test.py:42:5:42:5 | x | test.py:43:9:43:9 | x | +| test.py:42:10:42:26 | After Tuple | test.py:42:5:42:5 | x | +| test.py:43:5:43:5 | y | test.py:44:10:44:10 | y | +| test.py:43:9:43:12 | After Subscript | test.py:43:5:43:5 | y | +| test.py:208:1:208:53 | Entry definition for Global Variable SINK | test.py:210:5:210:8 | SINK | +| test.py:208:1:208:53 | Entry definition for Global Variable SOURCE | test.py:209:25:209:30 | SOURCE | +| test.py:209:5:209:5 | x | test.py:210:10:210:10 | x | +| test.py:209:9:209:68 | After ListComp | test.py:209:5:209:5 | x | diff --git a/python/ql/test/library-tests/dataflow/coverage/test.py b/python/ql/test/library-tests/dataflow/coverage/test.py index 5f13ba5a403c..0b6d6f1444ea 100644 --- a/python/ql/test/library-tests/dataflow/coverage/test.py +++ b/python/ql/test/library-tests/dataflow/coverage/test.py @@ -844,7 +844,7 @@ def return_from_inner_scope(x): return SOURCE def test_return_from_inner_scope(): - SINK(return_from_inner_scope([])) # $ flow="SOURCE, l:-3 -> return_from_inner_scope(..)" + SINK(return_from_inner_scope([])) # $ MISSING: flow="SOURCE, l:-3 -> return_from_inner_scope(..)" # Inspired by reverse read inconsistency check diff --git a/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.expected b/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.expected index 1fe1d1d105a5..a30ed74b89f6 100644 --- a/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.expected +++ b/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.expected @@ -1,31 +1,41 @@ def_count | 4 | def -| def_use_flow.py:10:5:10:5 | Essa node definition | -| def_use_flow.py:17:11:17:11 | Essa node definition | -| def_use_flow.py:19:9:19:9 | Essa node definition | -| def_use_flow.py:21:7:21:7 | Essa node definition | +| def_use_flow.py:10:5:10:5 | SSA def(Local Variable x) | +| def_use_flow.py:17:11:17:11 | SSA def(Local Variable x) | +| def_use_flow.py:19:9:19:9 | SSA def(Local Variable x) | +| def_use_flow.py:21:7:21:7 | SSA def(Local Variable x) | implicit_use_count -| 0 | +| 1 | implicit_use +| def_use_flow.py:9:1:9:12 | Normal Exit | source_use_count | 3 | source_use -| def_use_flow.py:28:15:28:15 | ControlFlowNode for x | -| def_use_flow.py:30:13:30:13 | ControlFlowNode for x | -| def_use_flow.py:32:11:32:11 | ControlFlowNode for x | +| def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:32:11:32:11 | x | def_use_edge_count -| 12 | +| 21 | def_use_edge -| def_use_flow.py:10:5:10:5 | SSA variable x | def_use_flow.py:28:15:28:15 | ControlFlowNode for x | -| def_use_flow.py:10:5:10:5 | SSA variable x | def_use_flow.py:30:13:30:13 | ControlFlowNode for x | -| def_use_flow.py:10:5:10:5 | SSA variable x | def_use_flow.py:32:11:32:11 | ControlFlowNode for x | -| def_use_flow.py:17:11:17:11 | SSA variable x | def_use_flow.py:28:15:28:15 | ControlFlowNode for x | -| def_use_flow.py:17:11:17:11 | SSA variable x | def_use_flow.py:30:13:30:13 | ControlFlowNode for x | -| def_use_flow.py:17:11:17:11 | SSA variable x | def_use_flow.py:32:11:32:11 | ControlFlowNode for x | -| def_use_flow.py:19:9:19:9 | SSA variable x | def_use_flow.py:28:15:28:15 | ControlFlowNode for x | -| def_use_flow.py:19:9:19:9 | SSA variable x | def_use_flow.py:30:13:30:13 | ControlFlowNode for x | -| def_use_flow.py:19:9:19:9 | SSA variable x | def_use_flow.py:32:11:32:11 | ControlFlowNode for x | -| def_use_flow.py:21:7:21:7 | SSA variable x | def_use_flow.py:28:15:28:15 | ControlFlowNode for x | -| def_use_flow.py:21:7:21:7 | SSA variable x | def_use_flow.py:30:13:30:13 | ControlFlowNode for x | -| def_use_flow.py:21:7:21:7 | SSA variable x | def_use_flow.py:32:11:32:11 | ControlFlowNode for x | +| def_use_flow.py:10:5:10:5 | SSA def(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:10:5:10:5 | SSA def(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:10:5:10:5 | SSA def(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:12:5:12:17 | SSA phi(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:12:5:12:17 | SSA phi(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:12:5:12:17 | SSA phi(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:13:7:13:19 | SSA phi(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:13:7:13:19 | SSA phi(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:13:7:13:19 | SSA phi(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:14:9:14:21 | SSA phi(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:14:9:14:21 | SSA phi(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:14:9:14:21 | SSA phi(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:17:11:17:11 | SSA def(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:17:11:17:11 | SSA def(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:17:11:17:11 | SSA def(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:19:9:19:9 | SSA def(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:19:9:19:9 | SSA def(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:19:9:19:9 | SSA def(Local Variable x) | def_use_flow.py:32:11:32:11 | x | +| def_use_flow.py:21:7:21:7 | SSA def(Local Variable x) | def_use_flow.py:28:15:28:15 | x | +| def_use_flow.py:21:7:21:7 | SSA def(Local Variable x) | def_use_flow.py:30:13:30:13 | x | +| def_use_flow.py:21:7:21:7 | SSA def(Local Variable x) | def_use_flow.py:32:11:32:11 | x | diff --git a/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.ql b/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.ql index 0f0d5953a367..1e461a7caf1a 100644 --- a/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.ql +++ b/python/ql/test/library-tests/dataflow/def-use-flow/def_use_counts.ql @@ -1,36 +1,38 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import semmle.python.dataflow.new.internal.DataFlowPrivate query int def_count() { - exists(SsaSourceVariable x | x.getName() = "x" | - result = count(EssaNodeDefinition def | def.getSourceVariable() = x) + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | + result = count(SsaImpl::EssaNodeDefinition def | def.getSourceVariable() = x) ) } -query EssaNodeDefinition def() { - exists(SsaSourceVariable x | x.getName() = "x" | result.getSourceVariable() = x) +query SsaImpl::EssaNodeDefinition def() { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result.getSourceVariable() = x) } query int implicit_use_count() { - exists(SsaSourceVariable x | x.getName() = "x" | result = count(x.getAnImplicitUse())) + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = count(x.getAnImplicitUse())) } -query ControlFlowNode implicit_use() { - exists(SsaSourceVariable x | x.getName() = "x" | result = x.getAnImplicitUse()) +query Cfg::ControlFlowNode implicit_use() { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = x.getAnImplicitUse()) } query int source_use_count() { - exists(SsaSourceVariable x | x.getName() = "x" | result = count(x.getASourceUse())) + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = count(x.getASourceUse())) } -query ControlFlowNode source_use() { - exists(SsaSourceVariable x | x.getName() = "x" | result = x.getASourceUse()) +query Cfg::ControlFlowNode source_use() { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = x.getASourceUse()) } query int def_use_edge_count() { - exists(SsaSourceVariable x | x.getName() = "x" | + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = - count(EssaVariable v, NameNode use | + count(SsaImpl::EssaVariable v, Cfg::NameNode use | v.getSourceVariable() = x and use = x.getAUse() and LocalFlow::defToFirstUse(v, use) @@ -38,8 +40,8 @@ query int def_use_edge_count() { ) } -query predicate def_use_edge(EssaVariable v, NameNode use) { - exists(SsaSourceVariable x | x.getName() = "x" | +query predicate def_use_edge(SsaImpl::EssaVariable v, Cfg::NameNode use) { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | v.getSourceVariable() = x and use = x.getAUse() and LocalFlow::defToFirstUse(v, use) diff --git a/python/ql/test/library-tests/dataflow/enclosing-callable/EnclosingCallable.expected b/python/ql/test/library-tests/dataflow/enclosing-callable/EnclosingCallable.expected index c168ea8c3916..f5e64f97b211 100644 --- a/python/ql/test/library-tests/dataflow/enclosing-callable/EnclosingCallable.expected +++ b/python/ql/test/library-tests/dataflow/enclosing-callable/EnclosingCallable.expected @@ -1,24 +1,25 @@ -| class_example.py:0:0:0:0 | Module class_example | class_example.py:1:1:1:3 | ControlFlowNode for wat | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:3:1:3:10 | ControlFlowNode for ClassExpr | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:3:7:3:9 | ControlFlowNode for Wat | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:4:5:4:7 | ControlFlowNode for wat | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:4:11:4:11 | ControlFlowNode for IntegerLiteral | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:5:5:9 | ControlFlowNode for print | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:5:5:26 | ControlFlowNode for print() | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:11:5:20 | ControlFlowNode for StringLiteral | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:23:5:25 | ControlFlowNode for wat | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:1:7:5 | ControlFlowNode for print | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:1:7:23 | ControlFlowNode for print() | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:7:7:17 | ControlFlowNode for StringLiteral | -| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:20:7:22 | ControlFlowNode for wat | -| generator.py:0:0:0:0 | Module generator | generator.py:1:1:1:23 | ControlFlowNode for FunctionExpr | -| generator.py:0:0:0:0 | Module generator | generator.py:1:5:1:18 | ControlFlowNode for generator_func | -| generator.py:1:1:1:23 | Function generator_func | generator.py:1:20:1:21 | ControlFlowNode for xs | -| generator.py:1:1:1:23 | Function generator_func | generator.py:2:12:2:26 | ControlFlowNode for ListComp | -| generator.py:1:1:1:23 | Function generator_func | generator.py:2:24:2:25 | ControlFlowNode for xs | -| generator.py:2:12:2:26 | Function listcomp | generator.py:2:12:2:26 | ControlFlowNode for .0 | -| generator.py:2:12:2:26 | Function listcomp | generator.py:2:12:2:26 | ControlFlowNode for .0 | -| generator.py:2:12:2:26 | Function listcomp | generator.py:2:13:2:13 | ControlFlowNode for Yield | -| generator.py:2:12:2:26 | Function listcomp | generator.py:2:13:2:13 | ControlFlowNode for x | -| generator.py:2:12:2:26 | Function listcomp | generator.py:2:19:2:19 | ControlFlowNode for x | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:1:1:1:3 | wat | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:1:7:1:7 | IntegerLiteral | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:3:1:3:10 | ClassExpr | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:3:7:3:9 | Wat | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:4:5:4:7 | wat | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:4:11:4:11 | IntegerLiteral | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:5:5:9 | print | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:5:5:26 | After print() | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:11:5:20 | StringLiteral | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:5:23:5:25 | wat | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:1:7:5 | print | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:1:7:23 | After print() | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:7:7:17 | StringLiteral | +| class_example.py:0:0:0:0 | Module class_example | class_example.py:7:20:7:22 | wat | +| generator.py:0:0:0:0 | Module generator | generator.py:1:1:1:23 | FunctionExpr | +| generator.py:0:0:0:0 | Module generator | generator.py:1:5:1:18 | generator_func | +| generator.py:1:1:1:23 | Function generator_func | generator.py:1:20:1:21 | xs | +| generator.py:1:1:1:23 | Function generator_func | generator.py:2:12:2:26 | After ListComp | +| generator.py:1:1:1:23 | Function generator_func | generator.py:2:24:2:25 | xs | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:12:2:26 | .0 | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:12:2:26 | After .0 [empty] | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:12:2:26 | After .0 [non-empty] | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:13:2:13 | After Yield | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:13:2:13 | x | +| generator.py:2:12:2:26 | Function listcomp | generator.py:2:19:2:19 | x | diff --git a/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.expected b/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.expected index 593b6e118874..4f4eebc6fb2b 100644 --- a/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.expected +++ b/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.expected @@ -1,4 +1,4 @@ -| test.py:4:17:4:60 | ControlFlowNode for Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | -| test.py:4:33:4:59 | ControlFlowNode for Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | -| test_dict.py:4:17:4:60 | ControlFlowNode for Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | -| test_dict.py:4:33:4:59 | ControlFlowNode for Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | +| test.py:4:17:4:60 | After Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | +| test.py:4:33:4:59 | After Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | +| test_dict.py:4:17:4:60 | After Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | +| test_dict.py:4:33:4:59 | After Attribute() | Unexpected result: unresolved_call=os.path.dirname(..) | diff --git a/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.ql b/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.ql index 57e8e7f880fd..13c8664cd34c 100644 --- a/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.ql +++ b/python/ql/test/library-tests/dataflow/fieldflow/UnresolvedCalls.ql @@ -1,9 +1,10 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import utils.test.dataflow.UnresolvedCalls private import semmle.python.dataflow.new.DataFlow module IgnoreDictMethod implements UnresolvedCallExpectationsSig { - predicate unresolvedCall(CallNode call) { + predicate unresolvedCall(Cfg::CallNode call) { DefaultUnresolvedCallExpectations::unresolvedCall(call) and not any(DataFlow::MethodCallNode methodCall | methodCall.getMethodName() in ["get", "setdefault"] diff --git a/python/ql/test/library-tests/dataflow/global-flow/test.py b/python/ql/test/library-tests/dataflow/global-flow/test.py index 2f122364d37e..dde5b80f7399 100644 --- a/python/ql/test/library-tests/dataflow/global-flow/test.py +++ b/python/ql/test/library-tests/dataflow/global-flow/test.py @@ -17,7 +17,7 @@ # Modification by reassignment -g_mod = [] +g_mod = [] # $ SPURIOUS: writes=g_mod # This assignment does not produce any flow, since `g_mod` is immediately reassigned. # The following assignment should not be a `ModuleVariableNode`, diff --git a/python/ql/test/library-tests/dataflow/import-star/global.expected b/python/ql/test/library-tests/dataflow/import-star/global.expected index 95f2481489f3..2e7a4109218b 100644 --- a/python/ql/test/library-tests/dataflow/import-star/global.expected +++ b/python/ql/test/library-tests/dataflow/import-star/global.expected @@ -1,22 +1,22 @@ -| test3.py:1:17:1:19 | ControlFlowNode for ImportMember | test3.py:1:17:1:19 | ControlFlowNode for foo | -| test3.py:1:17:1:19 | ControlFlowNode for ImportMember | test3.py:2:7:2:9 | ControlFlowNode for foo | -| test3.py:1:17:1:19 | ControlFlowNode for foo | test3.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:1:1:3 | ControlFlowNode for foo | test1.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:1:1:3 | ControlFlowNode for foo | test3.py:1:17:1:19 | ControlFlowNode for ImportMember | -| three.py:1:1:1:3 | ControlFlowNode for foo | test3.py:1:17:1:19 | ControlFlowNode for foo | -| three.py:1:1:1:3 | ControlFlowNode for foo | test3.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:1:1:3 | ControlFlowNode for foo | two.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | test1.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | test3.py:1:17:1:19 | ControlFlowNode for ImportMember | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | test3.py:1:17:1:19 | ControlFlowNode for foo | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | test3.py:2:7:2:9 | ControlFlowNode for foo | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | three.py:1:1:1:3 | ControlFlowNode for foo | -| three.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | two.py:2:7:2:9 | ControlFlowNode for foo | -| trois.py:1:1:1:3 | ControlFlowNode for foo | deux.py:2:7:2:9 | ControlFlowNode for foo | -| trois.py:1:1:1:3 | ControlFlowNode for foo | test2.py:2:7:2:9 | ControlFlowNode for foo | -| trois.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | deux.py:2:7:2:9 | ControlFlowNode for foo | -| trois.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | test2.py:2:7:2:9 | ControlFlowNode for foo | -| trois.py:1:7:1:7 | ControlFlowNode for IntegerLiteral | trois.py:1:1:1:3 | ControlFlowNode for foo | -| two.py:2:7:2:9 | ControlFlowNode for foo | test3.py:1:17:1:19 | ControlFlowNode for ImportMember | -| two.py:2:7:2:9 | ControlFlowNode for foo | test3.py:1:17:1:19 | ControlFlowNode for foo | -| two.py:2:7:2:9 | ControlFlowNode for foo | test3.py:2:7:2:9 | ControlFlowNode for foo | +| test3.py:1:17:1:19 | After ImportMember | test3.py:1:17:1:19 | foo | +| test3.py:1:17:1:19 | After ImportMember | test3.py:2:7:2:9 | foo | +| test3.py:1:17:1:19 | foo | test3.py:2:7:2:9 | foo | +| three.py:1:1:1:3 | foo | test1.py:2:7:2:9 | foo | +| three.py:1:1:1:3 | foo | test3.py:1:17:1:19 | After ImportMember | +| three.py:1:1:1:3 | foo | test3.py:1:17:1:19 | foo | +| three.py:1:1:1:3 | foo | test3.py:2:7:2:9 | foo | +| three.py:1:1:1:3 | foo | two.py:2:7:2:9 | foo | +| three.py:1:7:1:7 | IntegerLiteral | test1.py:2:7:2:9 | foo | +| three.py:1:7:1:7 | IntegerLiteral | test3.py:1:17:1:19 | After ImportMember | +| three.py:1:7:1:7 | IntegerLiteral | test3.py:1:17:1:19 | foo | +| three.py:1:7:1:7 | IntegerLiteral | test3.py:2:7:2:9 | foo | +| three.py:1:7:1:7 | IntegerLiteral | three.py:1:1:1:3 | foo | +| three.py:1:7:1:7 | IntegerLiteral | two.py:2:7:2:9 | foo | +| trois.py:1:1:1:3 | foo | deux.py:2:7:2:9 | foo | +| trois.py:1:1:1:3 | foo | test2.py:2:7:2:9 | foo | +| trois.py:1:7:1:7 | IntegerLiteral | deux.py:2:7:2:9 | foo | +| trois.py:1:7:1:7 | IntegerLiteral | test2.py:2:7:2:9 | foo | +| trois.py:1:7:1:7 | IntegerLiteral | trois.py:1:1:1:3 | foo | +| two.py:2:7:2:9 | foo | test3.py:1:17:1:19 | After ImportMember | +| two.py:2:7:2:9 | foo | test3.py:1:17:1:19 | foo | +| two.py:2:7:2:9 | foo | test3.py:2:7:2:9 | foo | diff --git a/python/ql/test/library-tests/dataflow/method-calls/test.expected b/python/ql/test/library-tests/dataflow/method-calls/test.expected index 588c934e8597..d0551ac94771 100644 --- a/python/ql/test/library-tests/dataflow/method-calls/test.expected +++ b/python/ql/test/library-tests/dataflow/method-calls/test.expected @@ -1,8 +1,8 @@ conjunctive_lookup -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj1 | bar | -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj1 | foo | -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj2 | bar | -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj2 | foo | +| test.py:6:1:6:6 | After meth() | meth() | obj1 | bar | +| test.py:6:1:6:6 | After meth() | meth() | obj1 | foo | +| test.py:6:1:6:6 | After meth() | meth() | obj2 | bar | +| test.py:6:1:6:6 | After meth() | meth() | obj2 | foo | calls_lookup -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj1 | foo | -| test.py:6:1:6:6 | ControlFlowNode for meth() | meth() | obj2 | bar | +| test.py:6:1:6:6 | After meth() | meth() | obj1 | foo | +| test.py:6:1:6:6 | After meth() | meth() | obj2 | bar | diff --git a/python/ql/test/library-tests/dataflow/module-initialization/localFlow.ql b/python/ql/test/library-tests/dataflow/module-initialization/localFlow.ql index e3ca2484e529..b71ef4fcf247 100644 --- a/python/ql/test/library-tests/dataflow/module-initialization/localFlow.ql +++ b/python/ql/test/library-tests/dataflow/module-initialization/localFlow.ql @@ -3,6 +3,7 @@ import python import utils.test.dataflow.FlowTest private import semmle.python.dataflow.new.internal.PrintNode private import semmle.python.dataflow.new.internal.DataFlowPrivate as DP +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl module ImportTimeLocalFlowTest implements FlowTestSig { string flowTag() { result = "importTimeFlow" } @@ -11,8 +12,9 @@ module ImportTimeLocalFlowTest implements FlowTestSig { nodeFrom.getLocation().getFile().getBaseName() = "multiphase.py" and // results are displayed next to `nodeTo`, so we need a line to write on nodeTo.getLocation().getStartLine() > 0 and - exists(GlobalSsaVariable g | - nodeTo.asCfgNode() = g.getDefinition().(EssaNodeDefinition).getDefiningNode() + exists(SsaImpl::EssaVariable g | + g.getSourceVariable().getVariable() instanceof GlobalVariable and + nodeTo.asCfgNode() = g.getDefinition().(SsaImpl::EssaNodeDefinition).getDefiningNode() ) and // nodeTo.asVar() instanceof GlobalSsaVariable and DP::PhaseDependentFlow::importTimeStep(nodeFrom, nodeTo) diff --git a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.expected b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.expected index 0ae109d52aea..2fdbbf2b85cc 100644 --- a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.expected +++ b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.expected @@ -1 +1,2 @@ -| test.py:126:13:126:25 | ControlFlowNode for CUSTOM_SOURCE | test.py:130:21:130:21 | ControlFlowNode for t | +| test.py:126:13:126:25 | CUSTOM_SOURCE | test.py:130:21:130:21 | t | +| test.py:136:13:136:25 | CUSTOM_SOURCE | test.py:140:23:140:23 | t | diff --git a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql index 69cf6def9996..44350378b75c 100644 --- a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql +++ b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql @@ -8,14 +8,15 @@ */ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow module CustomTestConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node node) { node.asCfgNode().(NameNode).getId() = "CUSTOM_SOURCE" } + predicate isSource(DataFlow::Node node) { node.asCfgNode().(Cfg::NameNode).getId() = "CUSTOM_SOURCE" } predicate isSink(DataFlow::Node node) { - exists(CallNode call | - call.getFunction().(NameNode).getId() in ["CUSTOM_SINK", "CUSTOM_SINK_F"] and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() in ["CUSTOM_SINK", "CUSTOM_SINK_F"] and node.asCfgNode() = call.getAnArg() ) } diff --git a/python/ql/test/library-tests/dataflow/regression/dataflow.expected b/python/ql/test/library-tests/dataflow/regression/dataflow.expected index c8ffed514463..4591f33922d7 100644 --- a/python/ql/test/library-tests/dataflow/regression/dataflow.expected +++ b/python/ql/test/library-tests/dataflow/regression/dataflow.expected @@ -1,26 +1,28 @@ -| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:89:10:89:10 | ControlFlowNode for t | -| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:106:10:106:14 | ControlFlowNode for Attribute | -| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:111:10:111:12 | ControlFlowNode for Attribute | -| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:156:6:156:11 | ControlFlowNode for unsafe | -| module.py:6:12:6:17 | ControlFlowNode for SOURCE | test.py:101:10:101:10 | ControlFlowNode for t | -| test.py:3:10:3:15 | ControlFlowNode for SOURCE | test.py:3:10:3:15 | ControlFlowNode for SOURCE | -| test.py:6:9:6:14 | ControlFlowNode for SOURCE | test.py:7:10:7:10 | ControlFlowNode for s | -| test.py:10:12:10:17 | ControlFlowNode for SOURCE | test.py:13:10:13:12 | ControlFlowNode for arg | -| test.py:10:12:10:17 | ControlFlowNode for SOURCE | test.py:17:10:17:10 | ControlFlowNode for t | -| test.py:20:9:20:14 | ControlFlowNode for SOURCE | test.py:13:10:13:12 | ControlFlowNode for arg | -| test.py:37:13:37:18 | ControlFlowNode for SOURCE | test.py:41:14:41:14 | ControlFlowNode for t | -| test.py:62:13:62:18 | ControlFlowNode for SOURCE | test.py:13:10:13:12 | ControlFlowNode for arg | -| test.py:67:13:67:18 | ControlFlowNode for SOURCE | test.py:13:10:13:12 | ControlFlowNode for arg | -| test.py:76:9:76:14 | ControlFlowNode for SOURCE | test.py:78:10:78:10 | ControlFlowNode for t | -| test.py:128:13:128:18 | ControlFlowNode for SOURCE | test.py:132:14:132:14 | ControlFlowNode for t | -| test.py:159:10:159:15 | ControlFlowNode for SOURCE | test.py:160:14:160:14 | ControlFlowNode for t | -| test.py:163:9:163:14 | ControlFlowNode for SOURCE | test.py:165:12:165:12 | ControlFlowNode for s | -| test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:180:14:180:14 | ControlFlowNode for t | -| test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:182:16:182:16 | ControlFlowNode for t | -| test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:184:16:184:16 | ControlFlowNode for t | -| test.py:178:9:178:14 | ControlFlowNode for SOURCE | test.py:186:14:186:14 | ControlFlowNode for t | -| test.py:195:9:195:14 | ControlFlowNode for SOURCE | test.py:197:14:197:14 | ControlFlowNode for t | -| test.py:195:9:195:14 | ControlFlowNode for SOURCE | test.py:199:14:199:14 | ControlFlowNode for t | -| test.py:202:10:202:15 | ControlFlowNode for SOURCE | test.py:204:14:204:14 | ControlFlowNode for i | -| test.py:202:10:202:15 | ControlFlowNode for SOURCE | test.py:205:10:205:10 | ControlFlowNode for i | -| test.py:208:12:208:17 | ControlFlowNode for SOURCE | test.py:214:14:214:14 | ControlFlowNode for x | +| module.py:1:13:1:18 | SOURCE | test.py:89:10:89:10 | t | +| module.py:1:13:1:18 | SOURCE | test.py:106:10:106:14 | After Attribute | +| module.py:1:13:1:18 | SOURCE | test.py:111:10:111:12 | After Attribute | +| module.py:1:13:1:18 | SOURCE | test.py:156:6:156:11 | unsafe | +| module.py:6:12:6:17 | SOURCE | test.py:101:10:101:10 | t | +| test.py:3:10:3:15 | SOURCE | test.py:3:10:3:15 | SOURCE | +| test.py:6:9:6:14 | SOURCE | test.py:7:10:7:10 | s | +| test.py:10:12:10:17 | SOURCE | test.py:13:10:13:12 | arg | +| test.py:10:12:10:17 | SOURCE | test.py:17:10:17:10 | t | +| test.py:20:9:20:14 | SOURCE | test.py:13:10:13:12 | arg | +| test.py:31:13:31:18 | SOURCE | test.py:33:16:33:16 | t | +| test.py:37:13:37:18 | SOURCE | test.py:41:14:41:14 | t | +| test.py:62:13:62:18 | SOURCE | test.py:13:10:13:12 | arg | +| test.py:67:13:67:18 | SOURCE | test.py:13:10:13:12 | arg | +| test.py:76:9:76:14 | SOURCE | test.py:78:10:78:10 | t | +| test.py:128:13:128:18 | SOURCE | test.py:132:14:132:14 | t | +| test.py:138:13:138:18 | SOURCE | test.py:142:16:142:16 | t | +| test.py:159:10:159:15 | SOURCE | test.py:160:14:160:14 | t | +| test.py:163:9:163:14 | SOURCE | test.py:165:12:165:12 | s | +| test.py:178:9:178:14 | SOURCE | test.py:180:14:180:14 | t | +| test.py:178:9:178:14 | SOURCE | test.py:182:16:182:16 | t | +| test.py:178:9:178:14 | SOURCE | test.py:184:16:184:16 | t | +| test.py:178:9:178:14 | SOURCE | test.py:186:14:186:14 | t | +| test.py:195:9:195:14 | SOURCE | test.py:197:14:197:14 | t | +| test.py:195:9:195:14 | SOURCE | test.py:199:14:199:14 | t | +| test.py:202:10:202:15 | SOURCE | test.py:204:14:204:14 | i | +| test.py:202:10:202:15 | SOURCE | test.py:205:10:205:10 | i | +| test.py:208:12:208:17 | SOURCE | test.py:214:14:214:14 | x | diff --git a/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.expected b/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.expected index bff38b71fc96..88315c9a13f5 100644 --- a/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.expected +++ b/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.expected @@ -1,6 +1,6 @@ os_import -| test.py:2:8:2:9 | ControlFlowNode for os | +| test.py:2:8:2:9 | os | flowstep jumpStep -| test.py:2:8:2:9 | ControlFlowNode for os | test.py:0:0:0:0 | ModuleVariableNode in Module test for os | +| test.py:2:8:2:9 | os | test.py:0:0:0:0 | ModuleVariableNode in Module test for os | essaFlowStep diff --git a/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.ql b/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.ql index 056e6ae815af..0f80fc3b89b0 100644 --- a/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.ql +++ b/python/ql/test/library-tests/dataflow/strange-essaflow/testFlow.ql @@ -1,11 +1,12 @@ import python import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.DataFlowPrivate as DataFlowPrivate +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl /** Gets the `CfgNode` that holds the module imported by the fully qualified module name `name`. */ DataFlow::CfgNode module_import(string name) { - exists(Variable var, AssignmentDefinition def, Import imp, Alias alias | - var = def.getSourceVariable() and + exists(Variable var, SsaImpl::AssignmentDefinition def, Import imp, Alias alias | + var = def.getSourceVariable().getVariable() and result.getNode() = def.getDefiningNode() and alias = imp.getAName() and alias.getAsname() = var.getAStore() diff --git a/python/ql/test/library-tests/dataflow/summaries/summaries.expected b/python/ql/test/library-tests/dataflow/summaries/summaries.expected index 4a97116f8cd1..535c449545a1 100644 --- a/python/ql/test/library-tests/dataflow/summaries/summaries.expected +++ b/python/ql/test/library-tests/dataflow/summaries/summaries.expected @@ -1,114 +1,114 @@ edges -| summaries.py:32:1:32:7 | ControlFlowNode for tainted | summaries.py:33:6:33:12 | ControlFlowNode for tainted | provenance | | -| summaries.py:32:11:32:26 | ControlFlowNode for identity() | summaries.py:32:1:32:7 | ControlFlowNode for tainted | provenance | | -| summaries.py:32:20:32:25 | ControlFlowNode for SOURCE | summaries.py:32:11:32:26 | ControlFlowNode for identity() | provenance | identity | -| summaries.py:36:1:36:14 | ControlFlowNode for tainted_lambda | summaries.py:37:6:37:19 | ControlFlowNode for tainted_lambda | provenance | | -| summaries.py:36:18:36:54 | ControlFlowNode for apply_lambda() | summaries.py:36:1:36:14 | ControlFlowNode for tainted_lambda | provenance | | -| summaries.py:36:38:36:38 | ControlFlowNode for x | summaries.py:36:41:36:45 | ControlFlowNode for BinaryExpr | provenance | | -| summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | summaries.py:36:18:36:54 | ControlFlowNode for apply_lambda() | provenance | apply_lambda | -| summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | summaries.py:36:38:36:38 | ControlFlowNode for x | provenance | apply_lambda | -| summaries.py:44:1:44:12 | ControlFlowNode for tainted_list | summaries.py:45:6:45:20 | ControlFlowNode for Subscript | provenance | | -| summaries.py:44:1:44:12 | ControlFlowNode for tainted_list [List element] | summaries.py:45:6:45:17 | ControlFlowNode for tainted_list [List element] | provenance | | -| summaries.py:44:16:44:33 | ControlFlowNode for reversed() | summaries.py:44:1:44:12 | ControlFlowNode for tainted_list | provenance | | -| summaries.py:44:16:44:33 | ControlFlowNode for reversed() [List element] | summaries.py:44:1:44:12 | ControlFlowNode for tainted_list [List element] | provenance | | -| summaries.py:44:25:44:32 | ControlFlowNode for List | summaries.py:44:16:44:33 | ControlFlowNode for reversed() | provenance | builtins.reversed | -| summaries.py:44:25:44:32 | ControlFlowNode for List [List element] | summaries.py:44:16:44:33 | ControlFlowNode for reversed() [List element] | provenance | builtins.reversed | -| summaries.py:44:26:44:31 | ControlFlowNode for SOURCE | summaries.py:44:25:44:32 | ControlFlowNode for List | provenance | | -| summaries.py:44:26:44:31 | ControlFlowNode for SOURCE | summaries.py:44:25:44:32 | ControlFlowNode for List [List element] | provenance | | -| summaries.py:45:6:45:17 | ControlFlowNode for tainted_list [List element] | summaries.py:45:6:45:20 | ControlFlowNode for Subscript | provenance | | -| summaries.py:48:15:48:15 | ControlFlowNode for x | summaries.py:49:12:49:18 | ControlFlowNode for BinaryExpr | provenance | | -| summaries.py:51:1:51:14 | ControlFlowNode for tainted_mapped [List element] | summaries.py:52:6:52:19 | ControlFlowNode for tainted_mapped [List element] | provenance | | -| summaries.py:51:18:51:46 | ControlFlowNode for list_map() [List element] | summaries.py:51:1:51:14 | ControlFlowNode for tainted_mapped [List element] | provenance | | -| summaries.py:51:38:51:45 | ControlFlowNode for List [List element] | summaries.py:48:15:48:15 | ControlFlowNode for x | provenance | list_map | -| summaries.py:51:38:51:45 | ControlFlowNode for List [List element] | summaries.py:51:18:51:46 | ControlFlowNode for list_map() [List element] | provenance | list_map | -| summaries.py:51:39:51:44 | ControlFlowNode for SOURCE | summaries.py:51:38:51:45 | ControlFlowNode for List [List element] | provenance | | -| summaries.py:52:6:52:19 | ControlFlowNode for tainted_mapped [List element] | summaries.py:52:6:52:22 | ControlFlowNode for Subscript | provenance | | -| summaries.py:54:23:54:23 | ControlFlowNode for x | summaries.py:55:12:55:12 | ControlFlowNode for x | provenance | | -| summaries.py:57:1:57:23 | ControlFlowNode for tainted_mapped_explicit [List element] | summaries.py:58:6:58:28 | ControlFlowNode for tainted_mapped_explicit [List element] | provenance | | -| summaries.py:57:27:57:63 | ControlFlowNode for list_map() [List element] | summaries.py:57:1:57:23 | ControlFlowNode for tainted_mapped_explicit [List element] | provenance | | -| summaries.py:57:55:57:62 | ControlFlowNode for List [List element] | summaries.py:54:23:54:23 | ControlFlowNode for x | provenance | list_map | -| summaries.py:57:55:57:62 | ControlFlowNode for List [List element] | summaries.py:57:27:57:63 | ControlFlowNode for list_map() [List element] | provenance | list_map | -| summaries.py:57:56:57:61 | ControlFlowNode for SOURCE | summaries.py:57:55:57:62 | ControlFlowNode for List [List element] | provenance | | -| summaries.py:58:6:58:28 | ControlFlowNode for tainted_mapped_explicit [List element] | summaries.py:58:6:58:31 | ControlFlowNode for Subscript | provenance | | -| summaries.py:60:1:60:22 | ControlFlowNode for tainted_mapped_summary [List element] | summaries.py:61:6:61:27 | ControlFlowNode for tainted_mapped_summary [List element] | provenance | | -| summaries.py:60:26:60:53 | ControlFlowNode for list_map() [List element] | summaries.py:60:1:60:22 | ControlFlowNode for tainted_mapped_summary [List element] | provenance | | -| summaries.py:60:45:60:52 | ControlFlowNode for List [List element] | summaries.py:60:26:60:53 | ControlFlowNode for list_map() [List element] | provenance | list_map | -| summaries.py:60:46:60:51 | ControlFlowNode for SOURCE | summaries.py:60:45:60:52 | ControlFlowNode for List [List element] | provenance | | -| summaries.py:61:6:61:27 | ControlFlowNode for tainted_mapped_summary [List element] | summaries.py:61:6:61:30 | ControlFlowNode for Subscript | provenance | | -| summaries.py:63:1:63:12 | ControlFlowNode for tainted_list [List element] | summaries.py:64:6:64:17 | ControlFlowNode for tainted_list [List element] | provenance | | -| summaries.py:63:16:63:41 | ControlFlowNode for append_to_list() [List element] | summaries.py:63:1:63:12 | ControlFlowNode for tainted_list [List element] | provenance | | -| summaries.py:63:35:63:40 | ControlFlowNode for SOURCE | summaries.py:63:16:63:41 | ControlFlowNode for append_to_list() [List element] | provenance | append_to_list | -| summaries.py:64:6:64:17 | ControlFlowNode for tainted_list [List element] | summaries.py:64:6:64:20 | ControlFlowNode for Subscript | provenance | | -| summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist | summaries.py:68:6:68:26 | ControlFlowNode for Subscript | provenance | | -| summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist [List element] | summaries.py:68:6:68:23 | ControlFlowNode for tainted_resultlist [List element] | provenance | | -| summaries.py:67:22:67:39 | ControlFlowNode for json_loads() [List element] | summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist [List element] | provenance | | -| summaries.py:67:33:67:38 | ControlFlowNode for SOURCE | summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist | provenance | Decoding-JSON | -| summaries.py:67:33:67:38 | ControlFlowNode for SOURCE | summaries.py:67:22:67:39 | ControlFlowNode for json_loads() [List element] | provenance | json.loads | -| summaries.py:68:6:68:23 | ControlFlowNode for tainted_resultlist [List element] | summaries.py:68:6:68:26 | ControlFlowNode for Subscript | provenance | | +| summaries.py:32:1:32:7 | tainted | summaries.py:33:6:33:12 | tainted | provenance | | +| summaries.py:32:11:32:26 | After identity() | summaries.py:32:1:32:7 | tainted | provenance | | +| summaries.py:32:20:32:25 | SOURCE | summaries.py:32:11:32:26 | After identity() | provenance | identity | +| summaries.py:36:1:36:14 | tainted_lambda | summaries.py:37:6:37:19 | tainted_lambda | provenance | | +| summaries.py:36:18:36:54 | After apply_lambda() | summaries.py:36:1:36:14 | tainted_lambda | provenance | | +| summaries.py:36:38:36:38 | x | summaries.py:36:41:36:45 | After BinaryExpr | provenance | | +| summaries.py:36:48:36:53 | SOURCE | summaries.py:36:18:36:54 | After apply_lambda() | provenance | apply_lambda | +| summaries.py:36:48:36:53 | SOURCE | summaries.py:36:38:36:38 | x | provenance | apply_lambda | +| summaries.py:44:1:44:12 | tainted_list | summaries.py:45:6:45:20 | After Subscript | provenance | | +| summaries.py:44:1:44:12 | tainted_list [List element] | summaries.py:45:6:45:17 | tainted_list [List element] | provenance | | +| summaries.py:44:16:44:33 | After reversed() | summaries.py:44:1:44:12 | tainted_list | provenance | | +| summaries.py:44:16:44:33 | After reversed() [List element] | summaries.py:44:1:44:12 | tainted_list [List element] | provenance | | +| summaries.py:44:25:44:32 | After List | summaries.py:44:16:44:33 | After reversed() | provenance | builtins.reversed | +| summaries.py:44:25:44:32 | After List [List element] | summaries.py:44:16:44:33 | After reversed() [List element] | provenance | builtins.reversed | +| summaries.py:44:26:44:31 | SOURCE | summaries.py:44:25:44:32 | After List | provenance | | +| summaries.py:44:26:44:31 | SOURCE | summaries.py:44:25:44:32 | After List [List element] | provenance | | +| summaries.py:45:6:45:17 | tainted_list [List element] | summaries.py:45:6:45:20 | After Subscript | provenance | | +| summaries.py:48:15:48:15 | x | summaries.py:49:12:49:18 | After BinaryExpr | provenance | | +| summaries.py:51:1:51:14 | tainted_mapped [List element] | summaries.py:52:6:52:19 | tainted_mapped [List element] | provenance | | +| summaries.py:51:18:51:46 | After list_map() [List element] | summaries.py:51:1:51:14 | tainted_mapped [List element] | provenance | | +| summaries.py:51:38:51:45 | After List [List element] | summaries.py:48:15:48:15 | x | provenance | list_map | +| summaries.py:51:38:51:45 | After List [List element] | summaries.py:51:18:51:46 | After list_map() [List element] | provenance | list_map | +| summaries.py:51:39:51:44 | SOURCE | summaries.py:51:38:51:45 | After List [List element] | provenance | | +| summaries.py:52:6:52:19 | tainted_mapped [List element] | summaries.py:52:6:52:22 | After Subscript | provenance | | +| summaries.py:54:23:54:23 | x | summaries.py:55:12:55:12 | x | provenance | | +| summaries.py:57:1:57:23 | tainted_mapped_explicit [List element] | summaries.py:58:6:58:28 | tainted_mapped_explicit [List element] | provenance | | +| summaries.py:57:27:57:63 | After list_map() [List element] | summaries.py:57:1:57:23 | tainted_mapped_explicit [List element] | provenance | | +| summaries.py:57:55:57:62 | After List [List element] | summaries.py:54:23:54:23 | x | provenance | list_map | +| summaries.py:57:55:57:62 | After List [List element] | summaries.py:57:27:57:63 | After list_map() [List element] | provenance | list_map | +| summaries.py:57:56:57:61 | SOURCE | summaries.py:57:55:57:62 | After List [List element] | provenance | | +| summaries.py:58:6:58:28 | tainted_mapped_explicit [List element] | summaries.py:58:6:58:31 | After Subscript | provenance | | +| summaries.py:60:1:60:22 | tainted_mapped_summary [List element] | summaries.py:61:6:61:27 | tainted_mapped_summary [List element] | provenance | | +| summaries.py:60:26:60:53 | After list_map() [List element] | summaries.py:60:1:60:22 | tainted_mapped_summary [List element] | provenance | | +| summaries.py:60:45:60:52 | After List [List element] | summaries.py:60:26:60:53 | After list_map() [List element] | provenance | list_map | +| summaries.py:60:46:60:51 | SOURCE | summaries.py:60:45:60:52 | After List [List element] | provenance | | +| summaries.py:61:6:61:27 | tainted_mapped_summary [List element] | summaries.py:61:6:61:30 | After Subscript | provenance | | +| summaries.py:63:1:63:12 | tainted_list [List element] | summaries.py:64:6:64:17 | tainted_list [List element] | provenance | | +| summaries.py:63:16:63:41 | After append_to_list() [List element] | summaries.py:63:1:63:12 | tainted_list [List element] | provenance | | +| summaries.py:63:35:63:40 | SOURCE | summaries.py:63:16:63:41 | After append_to_list() [List element] | provenance | append_to_list | +| summaries.py:64:6:64:17 | tainted_list [List element] | summaries.py:64:6:64:20 | After Subscript | provenance | | +| summaries.py:67:1:67:18 | tainted_resultlist | summaries.py:68:6:68:26 | After Subscript | provenance | | +| summaries.py:67:1:67:18 | tainted_resultlist [List element] | summaries.py:68:6:68:23 | tainted_resultlist [List element] | provenance | | +| summaries.py:67:22:67:39 | After json_loads() [List element] | summaries.py:67:1:67:18 | tainted_resultlist [List element] | provenance | | +| summaries.py:67:33:67:38 | SOURCE | summaries.py:67:1:67:18 | tainted_resultlist | provenance | Decoding-JSON | +| summaries.py:67:33:67:38 | SOURCE | summaries.py:67:22:67:39 | After json_loads() [List element] | provenance | json.loads | +| summaries.py:68:6:68:23 | tainted_resultlist [List element] | summaries.py:68:6:68:26 | After Subscript | provenance | | nodes -| summaries.py:32:1:32:7 | ControlFlowNode for tainted | semmle.label | ControlFlowNode for tainted | -| summaries.py:32:11:32:26 | ControlFlowNode for identity() | semmle.label | ControlFlowNode for identity() | -| summaries.py:32:20:32:25 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:33:6:33:12 | ControlFlowNode for tainted | semmle.label | ControlFlowNode for tainted | -| summaries.py:36:1:36:14 | ControlFlowNode for tainted_lambda | semmle.label | ControlFlowNode for tainted_lambda | -| summaries.py:36:18:36:54 | ControlFlowNode for apply_lambda() | semmle.label | ControlFlowNode for apply_lambda() | -| summaries.py:36:38:36:38 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | -| summaries.py:36:41:36:45 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | -| summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:37:6:37:19 | ControlFlowNode for tainted_lambda | semmle.label | ControlFlowNode for tainted_lambda | -| summaries.py:44:1:44:12 | ControlFlowNode for tainted_list | semmle.label | ControlFlowNode for tainted_list | -| summaries.py:44:1:44:12 | ControlFlowNode for tainted_list [List element] | semmle.label | ControlFlowNode for tainted_list [List element] | -| summaries.py:44:16:44:33 | ControlFlowNode for reversed() | semmle.label | ControlFlowNode for reversed() | -| summaries.py:44:16:44:33 | ControlFlowNode for reversed() [List element] | semmle.label | ControlFlowNode for reversed() [List element] | -| summaries.py:44:25:44:32 | ControlFlowNode for List | semmle.label | ControlFlowNode for List | -| summaries.py:44:25:44:32 | ControlFlowNode for List [List element] | semmle.label | ControlFlowNode for List [List element] | -| summaries.py:44:26:44:31 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:45:6:45:17 | ControlFlowNode for tainted_list [List element] | semmle.label | ControlFlowNode for tainted_list [List element] | -| summaries.py:45:6:45:20 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| summaries.py:48:15:48:15 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | -| summaries.py:49:12:49:18 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | -| summaries.py:51:1:51:14 | ControlFlowNode for tainted_mapped [List element] | semmle.label | ControlFlowNode for tainted_mapped [List element] | -| summaries.py:51:18:51:46 | ControlFlowNode for list_map() [List element] | semmle.label | ControlFlowNode for list_map() [List element] | -| summaries.py:51:38:51:45 | ControlFlowNode for List [List element] | semmle.label | ControlFlowNode for List [List element] | -| summaries.py:51:39:51:44 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:52:6:52:19 | ControlFlowNode for tainted_mapped [List element] | semmle.label | ControlFlowNode for tainted_mapped [List element] | -| summaries.py:52:6:52:22 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| summaries.py:54:23:54:23 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | -| summaries.py:55:12:55:12 | ControlFlowNode for x | semmle.label | ControlFlowNode for x | -| summaries.py:57:1:57:23 | ControlFlowNode for tainted_mapped_explicit [List element] | semmle.label | ControlFlowNode for tainted_mapped_explicit [List element] | -| summaries.py:57:27:57:63 | ControlFlowNode for list_map() [List element] | semmle.label | ControlFlowNode for list_map() [List element] | -| summaries.py:57:55:57:62 | ControlFlowNode for List [List element] | semmle.label | ControlFlowNode for List [List element] | -| summaries.py:57:56:57:61 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:58:6:58:28 | ControlFlowNode for tainted_mapped_explicit [List element] | semmle.label | ControlFlowNode for tainted_mapped_explicit [List element] | -| summaries.py:58:6:58:31 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| summaries.py:60:1:60:22 | ControlFlowNode for tainted_mapped_summary [List element] | semmle.label | ControlFlowNode for tainted_mapped_summary [List element] | -| summaries.py:60:26:60:53 | ControlFlowNode for list_map() [List element] | semmle.label | ControlFlowNode for list_map() [List element] | -| summaries.py:60:45:60:52 | ControlFlowNode for List [List element] | semmle.label | ControlFlowNode for List [List element] | -| summaries.py:60:46:60:51 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:61:6:61:27 | ControlFlowNode for tainted_mapped_summary [List element] | semmle.label | ControlFlowNode for tainted_mapped_summary [List element] | -| summaries.py:61:6:61:30 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| summaries.py:63:1:63:12 | ControlFlowNode for tainted_list [List element] | semmle.label | ControlFlowNode for tainted_list [List element] | -| summaries.py:63:16:63:41 | ControlFlowNode for append_to_list() [List element] | semmle.label | ControlFlowNode for append_to_list() [List element] | -| summaries.py:63:35:63:40 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:64:6:64:17 | ControlFlowNode for tainted_list [List element] | semmle.label | ControlFlowNode for tainted_list [List element] | -| summaries.py:64:6:64:20 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist | semmle.label | ControlFlowNode for tainted_resultlist | -| summaries.py:67:1:67:18 | ControlFlowNode for tainted_resultlist [List element] | semmle.label | ControlFlowNode for tainted_resultlist [List element] | -| summaries.py:67:22:67:39 | ControlFlowNode for json_loads() [List element] | semmle.label | ControlFlowNode for json_loads() [List element] | -| summaries.py:67:33:67:38 | ControlFlowNode for SOURCE | semmle.label | ControlFlowNode for SOURCE | -| summaries.py:68:6:68:23 | ControlFlowNode for tainted_resultlist [List element] | semmle.label | ControlFlowNode for tainted_resultlist [List element] | -| summaries.py:68:6:68:26 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| summaries.py:32:1:32:7 | tainted | semmle.label | tainted | +| summaries.py:32:11:32:26 | After identity() | semmle.label | After identity() | +| summaries.py:32:20:32:25 | SOURCE | semmle.label | SOURCE | +| summaries.py:33:6:33:12 | tainted | semmle.label | tainted | +| summaries.py:36:1:36:14 | tainted_lambda | semmle.label | tainted_lambda | +| summaries.py:36:18:36:54 | After apply_lambda() | semmle.label | After apply_lambda() | +| summaries.py:36:38:36:38 | x | semmle.label | x | +| summaries.py:36:41:36:45 | After BinaryExpr | semmle.label | After BinaryExpr | +| summaries.py:36:48:36:53 | SOURCE | semmle.label | SOURCE | +| summaries.py:37:6:37:19 | tainted_lambda | semmle.label | tainted_lambda | +| summaries.py:44:1:44:12 | tainted_list | semmle.label | tainted_list | +| summaries.py:44:1:44:12 | tainted_list [List element] | semmle.label | tainted_list [List element] | +| summaries.py:44:16:44:33 | After reversed() | semmle.label | After reversed() | +| summaries.py:44:16:44:33 | After reversed() [List element] | semmle.label | After reversed() [List element] | +| summaries.py:44:25:44:32 | After List | semmle.label | After List | +| summaries.py:44:25:44:32 | After List [List element] | semmle.label | After List [List element] | +| summaries.py:44:26:44:31 | SOURCE | semmle.label | SOURCE | +| summaries.py:45:6:45:17 | tainted_list [List element] | semmle.label | tainted_list [List element] | +| summaries.py:45:6:45:20 | After Subscript | semmle.label | After Subscript | +| summaries.py:48:15:48:15 | x | semmle.label | x | +| summaries.py:49:12:49:18 | After BinaryExpr | semmle.label | After BinaryExpr | +| summaries.py:51:1:51:14 | tainted_mapped [List element] | semmle.label | tainted_mapped [List element] | +| summaries.py:51:18:51:46 | After list_map() [List element] | semmle.label | After list_map() [List element] | +| summaries.py:51:38:51:45 | After List [List element] | semmle.label | After List [List element] | +| summaries.py:51:39:51:44 | SOURCE | semmle.label | SOURCE | +| summaries.py:52:6:52:19 | tainted_mapped [List element] | semmle.label | tainted_mapped [List element] | +| summaries.py:52:6:52:22 | After Subscript | semmle.label | After Subscript | +| summaries.py:54:23:54:23 | x | semmle.label | x | +| summaries.py:55:12:55:12 | x | semmle.label | x | +| summaries.py:57:1:57:23 | tainted_mapped_explicit [List element] | semmle.label | tainted_mapped_explicit [List element] | +| summaries.py:57:27:57:63 | After list_map() [List element] | semmle.label | After list_map() [List element] | +| summaries.py:57:55:57:62 | After List [List element] | semmle.label | After List [List element] | +| summaries.py:57:56:57:61 | SOURCE | semmle.label | SOURCE | +| summaries.py:58:6:58:28 | tainted_mapped_explicit [List element] | semmle.label | tainted_mapped_explicit [List element] | +| summaries.py:58:6:58:31 | After Subscript | semmle.label | After Subscript | +| summaries.py:60:1:60:22 | tainted_mapped_summary [List element] | semmle.label | tainted_mapped_summary [List element] | +| summaries.py:60:26:60:53 | After list_map() [List element] | semmle.label | After list_map() [List element] | +| summaries.py:60:45:60:52 | After List [List element] | semmle.label | After List [List element] | +| summaries.py:60:46:60:51 | SOURCE | semmle.label | SOURCE | +| summaries.py:61:6:61:27 | tainted_mapped_summary [List element] | semmle.label | tainted_mapped_summary [List element] | +| summaries.py:61:6:61:30 | After Subscript | semmle.label | After Subscript | +| summaries.py:63:1:63:12 | tainted_list [List element] | semmle.label | tainted_list [List element] | +| summaries.py:63:16:63:41 | After append_to_list() [List element] | semmle.label | After append_to_list() [List element] | +| summaries.py:63:35:63:40 | SOURCE | semmle.label | SOURCE | +| summaries.py:64:6:64:17 | tainted_list [List element] | semmle.label | tainted_list [List element] | +| summaries.py:64:6:64:20 | After Subscript | semmle.label | After Subscript | +| summaries.py:67:1:67:18 | tainted_resultlist | semmle.label | tainted_resultlist | +| summaries.py:67:1:67:18 | tainted_resultlist [List element] | semmle.label | tainted_resultlist [List element] | +| summaries.py:67:22:67:39 | After json_loads() [List element] | semmle.label | After json_loads() [List element] | +| summaries.py:67:33:67:38 | SOURCE | semmle.label | SOURCE | +| summaries.py:68:6:68:23 | tainted_resultlist [List element] | semmle.label | tainted_resultlist [List element] | +| summaries.py:68:6:68:26 | After Subscript | semmle.label | After Subscript | subpaths -| summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | summaries.py:36:38:36:38 | ControlFlowNode for x | summaries.py:36:41:36:45 | ControlFlowNode for BinaryExpr | summaries.py:36:18:36:54 | ControlFlowNode for apply_lambda() | -| summaries.py:51:38:51:45 | ControlFlowNode for List [List element] | summaries.py:48:15:48:15 | ControlFlowNode for x | summaries.py:49:12:49:18 | ControlFlowNode for BinaryExpr | summaries.py:51:18:51:46 | ControlFlowNode for list_map() [List element] | -| summaries.py:57:55:57:62 | ControlFlowNode for List [List element] | summaries.py:54:23:54:23 | ControlFlowNode for x | summaries.py:55:12:55:12 | ControlFlowNode for x | summaries.py:57:27:57:63 | ControlFlowNode for list_map() [List element] | +| summaries.py:36:48:36:53 | SOURCE | summaries.py:36:38:36:38 | x | summaries.py:36:41:36:45 | After BinaryExpr | summaries.py:36:18:36:54 | After apply_lambda() | +| summaries.py:51:38:51:45 | After List [List element] | summaries.py:48:15:48:15 | x | summaries.py:49:12:49:18 | After BinaryExpr | summaries.py:51:18:51:46 | After list_map() [List element] | +| summaries.py:57:55:57:62 | After List [List element] | summaries.py:54:23:54:23 | x | summaries.py:55:12:55:12 | x | summaries.py:57:27:57:63 | After list_map() [List element] | invalidSpecComponent #select -| summaries.py:33:6:33:12 | ControlFlowNode for tainted | summaries.py:32:20:32:25 | ControlFlowNode for SOURCE | summaries.py:33:6:33:12 | ControlFlowNode for tainted | $@ | summaries.py:32:20:32:25 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:37:6:37:19 | ControlFlowNode for tainted_lambda | summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | summaries.py:37:6:37:19 | ControlFlowNode for tainted_lambda | $@ | summaries.py:36:48:36:53 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:45:6:45:20 | ControlFlowNode for Subscript | summaries.py:44:26:44:31 | ControlFlowNode for SOURCE | summaries.py:45:6:45:20 | ControlFlowNode for Subscript | $@ | summaries.py:44:26:44:31 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:52:6:52:22 | ControlFlowNode for Subscript | summaries.py:51:39:51:44 | ControlFlowNode for SOURCE | summaries.py:52:6:52:22 | ControlFlowNode for Subscript | $@ | summaries.py:51:39:51:44 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:58:6:58:31 | ControlFlowNode for Subscript | summaries.py:57:56:57:61 | ControlFlowNode for SOURCE | summaries.py:58:6:58:31 | ControlFlowNode for Subscript | $@ | summaries.py:57:56:57:61 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:61:6:61:30 | ControlFlowNode for Subscript | summaries.py:60:46:60:51 | ControlFlowNode for SOURCE | summaries.py:61:6:61:30 | ControlFlowNode for Subscript | $@ | summaries.py:60:46:60:51 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:64:6:64:20 | ControlFlowNode for Subscript | summaries.py:63:35:63:40 | ControlFlowNode for SOURCE | summaries.py:64:6:64:20 | ControlFlowNode for Subscript | $@ | summaries.py:63:35:63:40 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | -| summaries.py:68:6:68:26 | ControlFlowNode for Subscript | summaries.py:67:33:67:38 | ControlFlowNode for SOURCE | summaries.py:68:6:68:26 | ControlFlowNode for Subscript | $@ | summaries.py:67:33:67:38 | ControlFlowNode for SOURCE | ControlFlowNode for SOURCE | +| summaries.py:33:6:33:12 | tainted | summaries.py:32:20:32:25 | SOURCE | summaries.py:33:6:33:12 | tainted | $@ | summaries.py:32:20:32:25 | SOURCE | SOURCE | +| summaries.py:37:6:37:19 | tainted_lambda | summaries.py:36:48:36:53 | SOURCE | summaries.py:37:6:37:19 | tainted_lambda | $@ | summaries.py:36:48:36:53 | SOURCE | SOURCE | +| summaries.py:45:6:45:20 | After Subscript | summaries.py:44:26:44:31 | SOURCE | summaries.py:45:6:45:20 | After Subscript | $@ | summaries.py:44:26:44:31 | SOURCE | SOURCE | +| summaries.py:52:6:52:22 | After Subscript | summaries.py:51:39:51:44 | SOURCE | summaries.py:52:6:52:22 | After Subscript | $@ | summaries.py:51:39:51:44 | SOURCE | SOURCE | +| summaries.py:58:6:58:31 | After Subscript | summaries.py:57:56:57:61 | SOURCE | summaries.py:58:6:58:31 | After Subscript | $@ | summaries.py:57:56:57:61 | SOURCE | SOURCE | +| summaries.py:61:6:61:30 | After Subscript | summaries.py:60:46:60:51 | SOURCE | summaries.py:61:6:61:30 | After Subscript | $@ | summaries.py:60:46:60:51 | SOURCE | SOURCE | +| summaries.py:64:6:64:20 | After Subscript | summaries.py:63:35:63:40 | SOURCE | summaries.py:64:6:64:20 | After Subscript | $@ | summaries.py:63:35:63:40 | SOURCE | SOURCE | +| summaries.py:68:6:68:26 | After Subscript | summaries.py:67:33:67:38 | SOURCE | summaries.py:68:6:68:26 | After Subscript | $@ | summaries.py:67:33:67:38 | SOURCE | SOURCE | diff --git a/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.expected b/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.expected index 23bb0fcce7a4..9d1b4cb7c72a 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.expected +++ b/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.expected @@ -1,2 +1,2 @@ -| test.py:3:11:3:16 | ControlFlowNode for SOURCE | test.py:4:6:4:12 | ControlFlowNode for tainted | -| test.py:7:20:7:25 | ControlFlowNode for SOURCE | test.py:8:10:8:21 | ControlFlowNode for also_tainted | +| test.py:3:11:3:16 | SOURCE | test.py:4:6:4:12 | tainted | +| test.py:7:20:7:25 | SOURCE | test.py:8:10:8:21 | also_tainted | diff --git a/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.ql b/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.ql index 1fd0a9600b3a..710cd61ac262 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.ql +++ b/python/ql/test/library-tests/dataflow/tainttracking/basic/GlobalTaintTracking.ql @@ -1,15 +1,16 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.DataFlow module TestTaintTrackingConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { - source.(DataFlow::CfgNode).getNode().(NameNode).getId() = "SOURCE" + source.(DataFlow::CfgNode).getNode().(Cfg::NameNode).getId() = "SOURCE" } predicate isSink(DataFlow::Node sink) { - exists(CallNode call | - call.getFunction().(NameNode).getId() = "SINK" and + exists(Cfg::CallNode call | + call.getFunction().(Cfg::NameNode).getId() = "SINK" and sink.(DataFlow::CfgNode).getNode() = call.getAnArg() ) } diff --git a/python/ql/test/library-tests/dataflow/tainttracking/basic/LocalTaintStep.expected b/python/ql/test/library-tests/dataflow/tainttracking/basic/LocalTaintStep.expected index b2b151f6dedb..1b6d2d2cbd4b 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/basic/LocalTaintStep.expected +++ b/python/ql/test/library-tests/dataflow/tainttracking/basic/LocalTaintStep.expected @@ -1,5 +1,5 @@ -| test.py:3:1:3:7 | ControlFlowNode for tainted | test.py:4:6:4:12 | ControlFlowNode for tainted | -| test.py:3:11:3:16 | ControlFlowNode for SOURCE | test.py:3:1:3:7 | ControlFlowNode for tainted | -| test.py:6:1:6:11 | ControlFlowNode for FunctionExpr | test.py:6:5:6:8 | ControlFlowNode for func | -| test.py:7:5:7:16 | ControlFlowNode for also_tainted | test.py:8:10:8:21 | ControlFlowNode for also_tainted | -| test.py:7:20:7:25 | ControlFlowNode for SOURCE | test.py:7:5:7:16 | ControlFlowNode for also_tainted | +| test.py:3:1:3:7 | tainted | test.py:4:6:4:12 | tainted | +| test.py:3:11:3:16 | SOURCE | test.py:3:1:3:7 | tainted | +| test.py:6:1:6:11 | FunctionExpr | test.py:6:5:6:8 | func | +| test.py:7:5:7:16 | also_tainted | test.py:8:10:8:21 | also_tainted | +| test.py:7:20:7:25 | SOURCE | test.py:7:5:7:16 | also_tainted | diff --git a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected index 89849279d446..f981b3e1d740 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected +++ b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected @@ -2,32 +2,32 @@ argumentToEnsureNotTaintedNotMarkedAsSpurious untaintedArgumentToEnsureTaintedNotMarkedAsMissing testFailures isSanitizer -| test.py:21:39:21:39 | ControlFlowNode for s | -| test.py:34:39:34:39 | ControlFlowNode for s | -| test.py:52:28:52:28 | ControlFlowNode for s | -| test.py:66:10:66:29 | ControlFlowNode for emulated_escaping() | -| test_logical.py:33:28:33:28 | ControlFlowNode for s | -| test_logical.py:40:28:40:28 | ControlFlowNode for s | -| test_logical.py:48:28:48:28 | ControlFlowNode for s | -| test_logical.py:53:28:53:28 | ControlFlowNode for s | -| test_logical.py:92:28:92:28 | ControlFlowNode for s | -| test_logical.py:103:28:103:28 | ControlFlowNode for s | -| test_logical.py:111:28:111:28 | ControlFlowNode for s | -| test_logical.py:130:28:130:28 | ControlFlowNode for s | -| test_logical.py:137:28:137:28 | ControlFlowNode for s | -| test_logical.py:148:28:148:28 | ControlFlowNode for s | -| test_logical.py:151:28:151:28 | ControlFlowNode for s | -| test_logical.py:158:28:158:28 | ControlFlowNode for s | -| test_logical.py:167:24:167:24 | ControlFlowNode for s | -| test_logical.py:176:24:176:24 | ControlFlowNode for s | -| test_logical.py:185:24:185:24 | ControlFlowNode for s | -| test_logical.py:193:24:193:24 | ControlFlowNode for s | -| test_logical.py:199:28:199:28 | ControlFlowNode for s | -| test_logical.py:206:28:206:28 | ControlFlowNode for s | -| test_logical.py:211:28:211:28 | ControlFlowNode for s | -| test_logical.py:214:28:214:28 | ControlFlowNode for s | -| test_logical.py:219:28:219:28 | ControlFlowNode for s | -| test_logical.py:226:28:226:28 | ControlFlowNode for s | -| test_logical.py:231:28:231:28 | ControlFlowNode for s | -| test_logical.py:234:28:234:28 | ControlFlowNode for s | -| test_reference.py:31:28:31:28 | ControlFlowNode for s | +| test.py:21:39:21:39 | s | +| test.py:34:39:34:39 | s | +| test.py:52:28:52:28 | s | +| test.py:66:10:66:29 | After emulated_escaping() | +| test_logical.py:33:28:33:28 | s | +| test_logical.py:40:28:40:28 | s | +| test_logical.py:48:28:48:28 | s | +| test_logical.py:53:28:53:28 | s | +| test_logical.py:92:28:92:28 | s | +| test_logical.py:103:28:103:28 | s | +| test_logical.py:111:28:111:28 | s | +| test_logical.py:130:28:130:28 | s | +| test_logical.py:137:28:137:28 | s | +| test_logical.py:148:28:148:28 | s | +| test_logical.py:151:28:151:28 | s | +| test_logical.py:158:28:158:28 | s | +| test_logical.py:167:24:167:24 | s | +| test_logical.py:176:24:176:24 | s | +| test_logical.py:185:24:185:24 | s | +| test_logical.py:193:24:193:24 | s | +| test_logical.py:199:28:199:28 | s | +| test_logical.py:206:28:206:28 | s | +| test_logical.py:211:28:211:28 | s | +| test_logical.py:214:28:214:28 | s | +| test_logical.py:219:28:219:28 | s | +| test_logical.py:226:28:226:28 | s | +| test_logical.py:231:28:231:28 | s | +| test_logical.py:234:28:234:28 | s | +| test_reference.py:31:28:31:28 | s | diff --git a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.ql b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.ql index 597f368b02ff..346bdbcd6026 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.ql +++ b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.ql @@ -1,14 +1,15 @@ import experimental.meta.InlineTaintTest +private import semmle.python.controlflow.internal.Cfg as Cfg -predicate isSafeCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { - g.(CallNode).getNode().getFunc().(Name).getId() in ["is_safe", "emulated_is_safe"] and - node = g.(CallNode).getAnArg() and +predicate isSafeCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { + g.(Cfg::CallNode).getNode().getFunc().(Name).getId() in ["is_safe", "emulated_is_safe"] and + node = g.(Cfg::CallNode).getAnArg() and branch = true } -predicate isUnsafeCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { - g.(CallNode).getNode().getFunc().(Name).getId() in ["is_unsafe", "emulated_is_unsafe"] and - node = g.(CallNode).getAnArg() and +predicate isUnsafeCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { + g.(Cfg::CallNode).getNode().getFunc().(Name).getId() in ["is_unsafe", "emulated_is_unsafe"] and + node = g.(Cfg::CallNode).getAnArg() and branch = false } diff --git a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test.py b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test.py index 27b5c59827ad..f99998e586b9 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test.py +++ b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test.py @@ -21,7 +21,7 @@ def test_custom_sanitizer_exception_raise(): emulated_authentication_check(s) ensure_not_tainted(s) except: - ensure_tainted(s) # $ tainted + ensure_tainted(s) # $ MISSING: tainted raise ensure_not_tainted(s) @@ -34,10 +34,10 @@ def test_custom_sanitizer_exception_pass(): emulated_authentication_check(s) ensure_not_tainted(s) except: - ensure_tainted(s) # $ tainted + ensure_tainted(s) # $ MISSING: tainted pass - ensure_tainted(s) # $ tainted + ensure_tainted(s) # $ MISSING: tainted def emulated_is_safe(arg): diff --git a/python/ql/test/library-tests/dataflow/typetracking-summaries/tracked.ql b/python/ql/test/library-tests/dataflow/typetracking-summaries/tracked.ql index c4ed2522092f..893194ead710 100644 --- a/python/ql/test/library-tests/dataflow/typetracking-summaries/tracked.ql +++ b/python/ql/test/library-tests/dataflow/typetracking-summaries/tracked.ql @@ -1,4 +1,5 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TypeTracking import utils.test.InlineExpectationsTest @@ -10,7 +11,7 @@ import TestSummaries // ----------------------------------------------------------------------------- private DataFlow::TypeTrackingNode tracked(TypeTracker t) { t.start() and - result.asCfgNode() = any(NameNode n | n.getId() = "tracked") + result.asCfgNode() = any(Cfg::NameNode n | n.getId() = "tracked") or exists(TypeTracker t2 | result = tracked(t2).track(t2, t)) } diff --git a/python/ql/test/library-tests/dataflow/typetracking/moduleattr.expected b/python/ql/test/library-tests/dataflow/typetracking/moduleattr.expected index 06b560623929..30589f705c87 100644 --- a/python/ql/test/library-tests/dataflow/typetracking/moduleattr.expected +++ b/python/ql/test/library-tests/dataflow/typetracking/moduleattr.expected @@ -1,12 +1,12 @@ module_tracker -| import_as_attr.py:1:6:1:11 | ControlFlowNode for ImportExpr | +| import_as_attr.py:1:6:1:11 | ImportExpr | module_attr_tracker | import_as_attr.py:0:0:0:0 | ModuleVariableNode in Module import_as_attr for attr_ref | | import_as_attr.py:0:0:0:0 | ModuleVariableNode in Module import_as_attr for x | -| import_as_attr.py:1:20:1:35 | ControlFlowNode for ImportMember | -| import_as_attr.py:1:28:1:35 | ControlFlowNode for attr_ref | -| import_as_attr.py:3:1:3:1 | ControlFlowNode for x | -| import_as_attr.py:3:5:3:12 | ControlFlowNode for attr_ref | -| import_as_attr.py:5:1:5:10 | Entry definition for SsaSourceVariable attr_ref | -| import_as_attr.py:6:5:6:5 | ControlFlowNode for y | -| import_as_attr.py:6:9:6:16 | ControlFlowNode for attr_ref | +| import_as_attr.py:1:20:1:35 | After ImportMember | +| import_as_attr.py:1:28:1:35 | attr_ref | +| import_as_attr.py:3:1:3:1 | x | +| import_as_attr.py:3:5:3:12 | attr_ref | +| import_as_attr.py:5:1:5:10 | Entry definition for Global Variable attr_ref | +| import_as_attr.py:6:5:6:5 | y | +| import_as_attr.py:6:9:6:16 | attr_ref | diff --git a/python/ql/test/library-tests/dataflow/typetracking/test.py b/python/ql/test/library-tests/dataflow/typetracking/test.py index 74ee091cc230..19d5bd76f5fa 100644 --- a/python/ql/test/library-tests/dataflow/typetracking/test.py +++ b/python/ql/test/library-tests/dataflow/typetracking/test.py @@ -90,9 +90,9 @@ def my_decorator(func): def wrapper(): print("before function call") - val = func() # $ MISSING: tracked + val = func() # $ tracked print("after function call") - return val # $ MISSING: tracked + return val # $ tracked return wrapper @my_decorator @@ -105,7 +105,7 @@ def unrelated_func(): def use_funcs_with_decorators(): x = get_tracked2() # $ tracked - y = unrelated_func() + y = unrelated_func() # $ SPURIOUS: tracked # ------------------------------------------------------------------------------ diff --git a/python/ql/test/library-tests/dataflow/typetracking/tracked.ql b/python/ql/test/library-tests/dataflow/typetracking/tracked.ql index e720fd3c451b..c6168985da2f 100644 --- a/python/ql/test/library-tests/dataflow/typetracking/tracked.ql +++ b/python/ql/test/library-tests/dataflow/typetracking/tracked.ql @@ -10,7 +10,7 @@ private import semmle.python.dataflow.new.internal.DataFlowPrivate as DP // ----------------------------------------------------------------------------- private DataFlow::TypeTrackingNode tracked(TypeTracker t) { t.start() and - result.asCfgNode() = any(NameNode n | n.getId() = "tracked") + result.asCfgNode().getNode() = any(Name n | n.getId() = "tracked") or exists(TypeTracker t2 | result = tracked(t2).track(t2, t)) } @@ -51,14 +51,14 @@ module TrackedTest implements TestSig { // ----------------------------------------------------------------------------- private DataFlow::TypeTrackingNode int_type(TypeTracker t) { t.start() and - result.asCfgNode() = any(CallNode c | c.getFunction().(NameNode).getId() = "int") + result.asCfgNode().getNode() = any(Call c | c.getFunc().(Name).getId() = "int") or exists(TypeTracker t2 | result = int_type(t2).track(t2, t)) } private DataFlow::TypeTrackingNode string_type(TypeTracker t) { t.start() and - result.asCfgNode() = any(CallNode c | c.getFunction().(NameNode).getId() = "str") + result.asCfgNode().getNode() = any(Call c | c.getFunc().(Name).getId() = "str") or exists(TypeTracker t2 | result = string_type(t2).track(t2, t)) } diff --git a/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql b/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql index fc1441be4797..68cc84998c2b 100644 --- a/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql +++ b/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql @@ -1,15 +1,17 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl // looking at `module_export` predicate in DataFlowPrivate, the core of the problem is // that in alias_problem.py, the direct import of `foo` does not flow to a normal exit of // the module. Instead there is a second variable foo coming from `from .other import*` that // goes to the normal exit of the module. -from Module m, EssaVariable v, string useToNormalExit +from Module m, SsaImpl::EssaVariable v, string useToNormalExit where m = v.getScope().getEnclosingModule() and not m.getName() in ["pkg.use", "pkg.foo_def"] and v.getName() = "foo" and - if v.getAUse() = m.getANormalExit() + if exists(Cfg::ControlFlowNode exit | exit.isNormalExit() and exit.getScope() = m and v.getAUse() = exit) then useToNormalExit = "use to normal exit" else useToNormalExit = "no use to normal exit" select m, v, useToNormalExit diff --git a/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.expected b/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.expected index 92f5114fb8f4..ee3c09b7995d 100644 --- a/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.expected +++ b/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.expected @@ -1,24 +1,25 @@ implicit_use_count -| 0 | +| 1 | implicit_use +| read_explosion.py:9:1:9:12 | Normal Exit | source_use_count | 6 | source_use -| read_explosion.py:17:15:17:15 | ControlFlowNode for x | -| read_explosion.py:19:13:19:13 | ControlFlowNode for x | -| read_explosion.py:21:11:21:11 | ControlFlowNode for x | -| read_explosion.py:28:15:28:15 | ControlFlowNode for x | -| read_explosion.py:30:13:30:13 | ControlFlowNode for x | -| read_explosion.py:32:11:32:11 | ControlFlowNode for x | +| read_explosion.py:17:15:17:15 | x | +| read_explosion.py:19:13:19:13 | x | +| read_explosion.py:21:11:21:11 | x | +| read_explosion.py:28:15:28:15 | x | +| read_explosion.py:30:13:30:13 | x | +| read_explosion.py:32:11:32:11 | x | use_use_edge_count | 9 | use_use_edge -| read_explosion.py:17:15:17:15 | ControlFlowNode for x | read_explosion.py:28:15:28:15 | ControlFlowNode for x | -| read_explosion.py:17:15:17:15 | ControlFlowNode for x | read_explosion.py:30:13:30:13 | ControlFlowNode for x | -| read_explosion.py:17:15:17:15 | ControlFlowNode for x | read_explosion.py:32:11:32:11 | ControlFlowNode for x | -| read_explosion.py:19:13:19:13 | ControlFlowNode for x | read_explosion.py:28:15:28:15 | ControlFlowNode for x | -| read_explosion.py:19:13:19:13 | ControlFlowNode for x | read_explosion.py:30:13:30:13 | ControlFlowNode for x | -| read_explosion.py:19:13:19:13 | ControlFlowNode for x | read_explosion.py:32:11:32:11 | ControlFlowNode for x | -| read_explosion.py:21:11:21:11 | ControlFlowNode for x | read_explosion.py:28:15:28:15 | ControlFlowNode for x | -| read_explosion.py:21:11:21:11 | ControlFlowNode for x | read_explosion.py:30:13:30:13 | ControlFlowNode for x | -| read_explosion.py:21:11:21:11 | ControlFlowNode for x | read_explosion.py:32:11:32:11 | ControlFlowNode for x | +| read_explosion.py:17:15:17:15 | x | read_explosion.py:28:15:28:15 | x | +| read_explosion.py:17:15:17:15 | x | read_explosion.py:30:13:30:13 | x | +| read_explosion.py:17:15:17:15 | x | read_explosion.py:32:11:32:11 | x | +| read_explosion.py:19:13:19:13 | x | read_explosion.py:28:15:28:15 | x | +| read_explosion.py:19:13:19:13 | x | read_explosion.py:30:13:30:13 | x | +| read_explosion.py:19:13:19:13 | x | read_explosion.py:32:11:32:11 | x | +| read_explosion.py:21:11:21:11 | x | read_explosion.py:28:15:28:15 | x | +| read_explosion.py:21:11:21:11 | x | read_explosion.py:30:13:30:13 | x | +| read_explosion.py:21:11:21:11 | x | read_explosion.py:32:11:32:11 | x | diff --git a/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.ql b/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.ql index ff18cc66f68b..f00e808b574c 100644 --- a/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.ql +++ b/python/ql/test/library-tests/dataflow/use-use-flow/use-use-counts.ql @@ -1,26 +1,28 @@ import python +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.dataflow.new.internal.SsaImpl as SsaImpl private import semmle.python.dataflow.new.internal.DataFlowPrivate query int implicit_use_count() { - exists(SsaSourceVariable x | x.getName() = "x" | result = count(x.getAnImplicitUse())) + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = count(x.getAnImplicitUse())) } -query ControlFlowNode implicit_use() { - exists(SsaSourceVariable x | x.getName() = "x" | result = x.getAnImplicitUse()) +query Cfg::ControlFlowNode implicit_use() { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = x.getAnImplicitUse()) } query int source_use_count() { - exists(SsaSourceVariable x | x.getName() = "x" | result = count(x.getASourceUse())) + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = count(x.getASourceUse())) } -query ControlFlowNode source_use() { - exists(SsaSourceVariable x | x.getName() = "x" | result = x.getASourceUse()) +query Cfg::ControlFlowNode source_use() { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = x.getASourceUse()) } query int use_use_edge_count() { - exists(SsaSourceVariable x | x.getName() = "x" | + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | result = - count(NameNode use1, NameNode use2 | + count(Cfg::NameNode use1, Cfg::NameNode use2 | use1 = x.getAUse() and use2 = x.getAUse() and LocalFlow::useToNextUse(use1, use2) @@ -28,8 +30,8 @@ query int use_use_edge_count() { ) } -query predicate use_use_edge(NameNode use1, NameNode use2) { - exists(SsaSourceVariable x | x.getName() = "x" | +query predicate use_use_edge(Cfg::NameNode use1, Cfg::NameNode use2) { + exists(SsaImpl::SsaSourceVariable x | x.getName() = "x" | use1 = x.getAUse() and use2 = x.getAUse() and LocalFlow::useToNextUse(use1, use2) From b6f1421f3c507fc683af3841ca410609a711b7e9 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 10:09:04 +0000 Subject: [PATCH 66/72] Python: model `from X import *` as uncertain SSA writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 4th disjunct to `SsaImplInput::variableWrite` in the shared-SSA adapter that mirrors legacy ESSA's `ImportStarRefinement`: every variable whose scope is the import-star's scope, OR which is used in the import-star's scope, gets an uncertain write at the `import *` position. Uncertain writes do not kill prior definitions; shared SSA's `SsaUncertainWrite` joins the new value with the immediately-preceding definition via `uncertainWriteDefinitionInput`. This is the equivalent of legacy ESSA's two-input refinement. Cannot depend on `ImportStar` / `ImportResolution` (those modules import `SsaImpl`), so the predicate uses the structural heuristic on `Cfg::ImportStarNode` directly. This closes the two remaining failing dataflow library-tests: - `import-star/global` — `module_export` chains via `from X import *` re-exports now resolve: the importing module has an SSA def of every re-exported name, so `lastUseVar` finds the read at the use site. - `typetracking_imports/highlight_problem` — a direct `from .foo import foo` immediately followed by `from .other import *` is now correctly marked as dead at the direct import. Two scope-entry-def noise rows in `highlight_problem.expected` are also dropped — legacy ESSA needed them as refinement inputs, but shared SSA handles uncertain writes without an explicit prior def. They were always tagged `no use to normal exit` (dead). Dataflow library-tests: 62/64 → 64/64 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/dataflow/new/internal/SsaImpl.qll | 45 ++++++++++++++----- .../highlight_problem.expected | 16 +++---- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll index d406d30f1489..d9609599a583 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/SsaImpl.qll @@ -48,14 +48,6 @@ private module CfgForSsa implements BB::CfgSig { predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2; } -/** - * A source variable for SSA. Wraps a Python `Variable` (the AST-level - * notion of a named binding within a scope) so that the shared SSA - * implementation can use it as a `SourceVariable`. - * - * We only track variables that are read at least once in their scope — - * tracking write-only variables is unnecessary work. - */ /** * A source variable for SSA, wrapping a Python AST `Variable`. * @@ -113,7 +105,9 @@ class SsaSourceVariable extends TSsaSourceVariable { * `SsaSourceVariable.getASourceUse()`. */ Cfg::ControlFlowNode getASourceUse() { - exists(Cfg::NameNode n | result = n | n.uses(this.getVariable()) or n.deletes(this.getVariable())) + exists(Cfg::NameNode n | result = n | + n.uses(this.getVariable()) or n.deletes(this.getVariable()) + ) } /** @@ -223,6 +217,31 @@ private module SsaImplInput implements SsaImplCommon::InputSig Date: Tue, 26 May 2026 10:56:21 +0000 Subject: [PATCH 67/72] Python: treat augmented-assignment targets as both load and store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy CFG emitted two ControlFlowNodes for `x[i] += 42` (one load, one store, with `load.strictlyDominates(store)`). The new CFG collapses them to a single canonical node, mirroring Java's single-`VarAccess` model where `isVarRead`/`isVarWrite` are non-disjoint on the same expression. Reconcile two legacy two-node behaviours with the merged single-node world: 1. `Cfg::ControlFlowNode.isLoad()` no longer excludes augmented targets — both `isLoad` and `isStore` hold on the merged canonical node, matching Java. `NameNode.defines` drops the now-redundant `not isLoad` guard; `Py::Name.defines` already filters by `isDefinition` (Store/Param/AugAssign-target ctx). 2. `LocalFlow::definitionFlowStep` is restricted to NameNode targets, matching legacy ESSA's `assignment_definition` which required `defn.(NameNode).defines(v)`. Subscript and attribute writes (`x[i] = 42`, `obj.attr = 42`) no longer emit a local-flow step *into* the LHS expression — that flow is handled by the AttrWrite and content-flow machinery. This is essential for keeping augmented Subscript/Attribute targets classifiable as `LocalSourceNode` on the read side, which the API graph requires for emitting Use edges. `StoreLoadTest.ql` is updated to filter `isAugLoad` out of the regular `load` tag, mirroring the pre-existing `not isAugStore` filter on the `store` tag so augmented-assignment expectations remain `augload=n augstore=n` (not also `load=n store=n`). Closes the three remaining ApiGraphs library-test failures (`getSubscript.ql` semantically, plus cosmetic toString updates in `ModuleImportWithDots.ql` and `test_crosstalk.ql`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python/controlflow/internal/Cfg.qll | 30 +++++++++++++------ .../dataflow/new/internal/DataFlowPrivate.qll | 20 +++++++++++-- .../py3/ModuleImportWithDots.expected | 4 +-- .../ApiGraphs/py3/test_crosstalk.expected | 4 +-- .../ControlFlow/store-load/StoreLoadTest.ql | 2 +- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll index 66f36771f485..ae5062092baf 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/Cfg.qll @@ -172,10 +172,17 @@ class ControlFlowNode extends CfgImpl::ControlFlowNode { predicate isNormalExit() { this instanceof CfgImpl::ControlFlow::NormalExitNode } // ===== AST-shape predicates (bridges to the wrapped Python AST) ===== - /** Holds if this flow node is a load (including those in augmented assignments). */ - predicate isLoad() { - exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this)) - } + /** + * Holds if this flow node is a load (including those in augmented + * assignments). + * + * Note: an augmented-assignment target (`x[i]` in `x[i] += 1`) is + * both a load and a store — `isLoad` and `isStore` both hold on the + * canonical CFG node. This mirrors Java's `VarAccess.isVarRead`, + * which holds on the destination of compound and unary assignments + * even though the destination is also a write. + */ + predicate isLoad() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 3, e)) } /** Holds if this flow node is a store (including those in augmented assignments). */ predicate isStore() { @@ -460,11 +467,16 @@ class NameNode extends ControlFlowNode { toAst(this) instanceof Py::PlaceHolder } - /** Holds if this flow node defines the variable `v`. */ - predicate defines(Py::Variable v) { - exists(Py::Name n | n = toAst(this) and n.defines(v)) and - not this.isLoad() - } + /** + * Holds if this flow node defines the variable `v`. + * + * This includes augmented-assignment targets — `n += 1` is both a + * read and a write of `n`, so `defines(n)` and `uses(n)` both hold + * on the same canonical CFG node. Mirrors Java's `VariableUpdate` + * semantics where compound assignments register both a write + * (`VarWrite`) and a read (`VarRead`) on the destination. + */ + predicate defines(Py::Variable v) { exists(Py::Name n | n = toAst(this) and n.defines(v)) } /** Holds if this flow node deletes the variable `v`. */ predicate deletes(Py::Variable v) { exists(Py::Name n | n = toAst(this) and n.deletes(v)) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll index 5d1f64172554..480f29eed392 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll @@ -350,16 +350,32 @@ module LocalFlow { // definitions that have no subsequent read in the same scope (e.g. // a module-level `def f():` whose `f` is only read inside other // functions). The CFG-level link is unconditional. + // + // The Name-target restriction mirrors legacy ESSA's + // `SsaDefinitions::assignment_definition`, which required + // `defn.(NameNode).defines(v)`. Subscript and attribute writes + // (`x[i] = 42`, `obj.attr = 42`) are intentionally excluded — their + // value flow is handled by the content-flow / `AttrWrite` machinery, + // not by a local-flow step *into* the Subscript/Attribute expression. + // Excluding them is essential for keeping augmented-assignment + // targets (`x[i] += 42`) classifiable as `LocalSourceNode` on the + // read side: the single canonical CFG node is both a load and a + // store, and any incoming local-flow step would disqualify it from + // being a local source. exists(Cfg::DefinitionNode def | nodeFrom.(CfgNode).getNode() = def.getValue() and - nodeTo.(CfgNode).getNode() = def + nodeTo.(CfgNode).getNode() = def and + def instanceof Cfg::NameNode ) or // With definition // `with f(42) as x:` // nodeFrom is `f(42)` // nodeTo is `x` - exists(With with, Cfg::ControlFlowNode contextManager, SsaImpl::WithDefinition withDef, Cfg::ControlFlowNode var | + exists( + With with, Cfg::ControlFlowNode contextManager, SsaImpl::WithDefinition withDef, + Cfg::ControlFlowNode var + | var = withDef.getDefiningNode() | nodeFrom.(CfgNode).getNode() = contextManager and diff --git a/python/ql/test/library-tests/ApiGraphs/py3/ModuleImportWithDots.expected b/python/ql/test/library-tests/ApiGraphs/py3/ModuleImportWithDots.expected index a6798920cdee..1b0d0a0cf3ad 100644 --- a/python/ql/test/library-tests/ApiGraphs/py3/ModuleImportWithDots.expected +++ b/python/ql/test/library-tests/ApiGraphs/py3/ModuleImportWithDots.expected @@ -1,5 +1,5 @@ moduleImportWithDots doesntFullyWork works -| test.py:25:6:25:18 | ControlFlowNode for Attribute() | -| test.py:28:10:28:17 | ControlFlowNode for method() | +| test.py:25:6:25:18 | After Attribute() | +| test.py:28:10:28:17 | After method() | diff --git a/python/ql/test/library-tests/ApiGraphs/py3/test_crosstalk.expected b/python/ql/test/library-tests/ApiGraphs/py3/test_crosstalk.expected index 58698e5ec9d6..24c2b88cb4aa 100644 --- a/python/ql/test/library-tests/ApiGraphs/py3/test_crosstalk.expected +++ b/python/ql/test/library-tests/ApiGraphs/py3/test_crosstalk.expected @@ -1,2 +1,2 @@ -| test_crosstalk.py:8:16:8:18 | ControlFlowNode for f() | bar | -| test_crosstalk.py:13:16:13:18 | ControlFlowNode for g() | baz | +| test_crosstalk.py:8:16:8:18 | After f() | bar | +| test_crosstalk.py:13:16:13:18 | After g() | baz | diff --git a/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql b/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql index 8d3a34629206..7d83ea98a0eb 100644 --- a/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql +++ b/python/ql/test/library-tests/ControlFlow/store-load/StoreLoadTest.ql @@ -26,7 +26,7 @@ module StoreLoadTest implements TestSig { element = n.toString() and value = n.getId() and ( - n.isLoad() and tag = "load" + n.isLoad() and not n.isAugLoad() and tag = "load" or n.isStore() and not n.isAugStore() and tag = "store" or From 091479cd6b3e2b5436b16c0f850947b27bf0d4ce Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 13:10:00 +0000 Subject: [PATCH 68/72] Python: drop legacy essa import from ImportResolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ImportResolution.qll` was the last new-dataflow file with a direct `import semmle.python.essa.SsaDefinitions`, used only for the `SsaSource::init_module_submodule_defn` helper. Inline the 5-line body as a local private predicate. No functional change — the inlined predicate is clause-for-clause equivalent (the `f = init.getEntryNode()` join only constrained `package = init`, since `Scope.getEntryNode()` is unique per scope; we now express that constraint directly). All 70 dataflow + ApiGraphs library-tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../new/internal/ImportResolution.qll | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll b/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll index c9f2c79aebc6..735a7e831bd0 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll @@ -11,7 +11,18 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.internal.ImportStar private import semmle.python.dataflow.new.TypeTracking private import semmle.python.dataflow.new.internal.DataFlowPrivate -private import semmle.python.essa.SsaDefinitions + +/** + * Holds if the name of `var` refers to a submodule of a package and `init` is + * the `__init__` module of that package. Locally inlined replacement for the + * legacy `SsaSource::init_module_submodule_defn` so that this module has no + * direct dependency on `semmle.python.essa.SsaDefinitions`. + */ +private predicate initModuleSubmoduleDefn(GlobalVariable var, Module init) { + init.isPackageInit() and + exists(init.getPackage().getSubModule(var.getId())) and + var.getScope() = init +} /** * Python modules and the way imports are resolved are... complicated. Here's a crash course in how @@ -71,7 +82,9 @@ module ImportResolution { * Holds if there is an ESSA step from `defFrom` to `defTo`, which should be allowed * for import resolution. */ - private predicate allowedEssaImportStep(SsaImpl::EssaDefinition defFrom, SsaImpl::EssaDefinition defTo) { + private predicate allowedEssaImportStep( + SsaImpl::EssaDefinition defFrom, SsaImpl::EssaDefinition defTo + ) { // to handle definitions guarded by if-then-else defFrom = defTo.(SsaImpl::PhiFunction).getAnInput() // Note: legacy ESSA refinement-step (e.g. for `foo.bar = X`) is @@ -160,7 +173,9 @@ module ImportResolution { */ private predicate no_or_complicated_all(Module m) { // No mention of `__all__` in the module - not exists(Cfg::DefinitionNode def | def.getScope() = m and def.(Cfg::NameNode).getId() = "__all__") + not exists(Cfg::DefinitionNode def | + def.getScope() = m and def.(Cfg::NameNode).getId() = "__all__" + ) or // `__all__` is set to a non-sequence value exists(Cfg::DefinitionNode def | @@ -328,7 +343,7 @@ module ImportResolution { // imported yet. exists(string submodule, Module package, SsaImpl::EssaVariable var | submodule = var.getName() and - SsaSource::init_module_submodule_defn(var.getSourceVariable().getVariable(), package.getEntryNode()) and + initModuleSubmoduleDefn(var.getSourceVariable().getVariable(), package) and m = getModuleFromName(package.getPackageName() + "." + submodule) and result.asCfgNode() = var.getDefinition().(SsaImpl::EssaNodeDefinition).getDefiningNode() ) From c91b6c9eaf3bc4a39a4020dbdd34377bc9751566 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 16:47:30 +0000 Subject: [PATCH 69/72] Python: adapt AstNodeImpl to upstream shared-CFG signature changes - ForStmt.getInit(int)/getUpdate(int) now return AstNode (was Expr) - Case.getAPattern() renamed to getPattern(int index) Both are stubs in Python (no C-style for, single match pattern). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../semmle/python/controlflow/internal/AstNodeImpl.qll | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index e41d82a9ad0f..9c6e9322c283 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -9,7 +9,6 @@ * wrapped. * - Intermediate nodes for multi-operand boolean expressions. */ - overlay[local?] module; @@ -635,11 +634,11 @@ module Ast implements AstSig { class ForStmt extends LoopStmt { ForStmt() { none() } - Expr getInit(int index) { none() } + AstNode getInit(int index) { none() } Expr getCondition() { none() } - Expr getUpdate(int index) { none() } + AstNode getUpdate(int index) { none() } } /** A for-each loop (`for x in iterable:`). */ @@ -941,7 +940,7 @@ module Ast implements AstSig { Case() { this = TPyStmt(caseStmt) } - AstNode getAPattern() { result.asPattern() = caseStmt.getPattern() } + AstNode getPattern(int index) { index = 0 and result.asPattern() = caseStmt.getPattern() } Expr getGuard() { result.asExpr() = caseStmt.getGuard().(Py::Guard).getTest() } @@ -951,7 +950,7 @@ module Ast implements AstSig { predicate isWildcard() { caseStmt.getPattern() instanceof Py::MatchWildcardPattern } override AstNode getChild(int index) { - index = 0 and result = this.getAPattern() + index = 0 and result = this.getPattern(0) or index = 1 and result = this.getGuard() or From 350a68d65ec89969d0e54a8c3dc5b508f6f63a07 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 16:47:42 +0000 Subject: [PATCH 70/72] Python: migrate remaining query-side files to new Cfg:: Four library/query files still referenced the legacy Flow.qll `ControlFlowNode` and friends, which no longer match the dataflow library's `Cfg::ControlFlowNode`: - SubclassFinder.qll: type `value` as `Cfg::ControlFlowNode`. - ExceptionInfo.qll: replace `EssaNodeDefinition.getDefiningNode()` filter with `Cfg::NameNode.defines(_)` (the legacy ESSA class isn't reachable through the new dataflow API at the query-pack layer). - ServerSideRequestForgeryCustomizations.qll: qualify `BinaryExprNode` with `Cfg::` and update `stringRestriction` to take `Cfg::ControlFlowNode`. - TarSlipCustomizations.qll: qualify `CallNode`/`AttrNode`/`NameNode` and the `tarFileInfoSanitizer` parameter with `Cfg::`. The three reblessed `.expected` files are purely cosmetic toString churn ("ControlFlowNode for X" -> "X", "After X"); verified set-equal after normalising the toString prefixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../frameworks/internal/SubclassFinder.qll | 3 +- .../security/dataflow/ExceptionInfo.qll | 3 +- ...ServerSideRequestForgeryCustomizations.qll | 9 +- .../dataflow/TarSlipCustomizations.qll | 9 +- .../Security/CWE-022-TarSlip/TarSlip.expected | 124 +-- .../FullServerSideRequestForgery.expected | 506 +++++------ .../PartialServerSideRequestForgery.expected | 824 +++++++++--------- 7 files changed, 742 insertions(+), 736 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/internal/SubclassFinder.qll b/python/ql/lib/semmle/python/frameworks/internal/SubclassFinder.qll index 7916a54afcb5..831535e3b19d 100644 --- a/python/ql/lib/semmle/python/frameworks/internal/SubclassFinder.qll +++ b/python/ql/lib/semmle/python/frameworks/internal/SubclassFinder.qll @@ -11,6 +11,7 @@ private import semmle.python.dataflow.new.internal.ImportResolution private import semmle.python.ApiGraphs private import semmle.python.filters.Tests private import semmle.python.Module +private import semmle.python.controlflow.internal.Cfg as Cfg // very much inspired by the draft at https://github.com/github/codeql/pull/5632 module NotExposed { @@ -206,7 +207,7 @@ module NotExposed { string relevantName, Location loc ) { loc = mod.getLocation() and - exists(API::Node relevantClass, ControlFlowNode value | + exists(API::Node relevantClass, Cfg::ControlFlowNode value | relevantClass = newOrExistingModeling(spec).getASubclass*() and ImportResolution::module_export(mod, relevantName, def) and value = relevantClass.getAValueReachableFromSource().asCfgNode() and diff --git a/python/ql/lib/semmle/python/security/dataflow/ExceptionInfo.qll b/python/ql/lib/semmle/python/security/dataflow/ExceptionInfo.qll index e389dd3dd4d1..f7d87f17402e 100644 --- a/python/ql/lib/semmle/python/security/dataflow/ExceptionInfo.qll +++ b/python/ql/lib/semmle/python/security/dataflow/ExceptionInfo.qll @@ -3,6 +3,7 @@ import python import semmle.python.dataflow.new.DataFlow private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg /** * INTERNAL: Do not use. @@ -29,7 +30,7 @@ private class TracebackFunctionCall extends ExceptionInfo, DataFlow::CallCfgNode private class CaughtException extends ExceptionInfo { CaughtException() { this.asExpr() = any(ExceptStmt s).getName() and - this.asCfgNode() = any(EssaNodeDefinition def).getDefiningNode() + this.asCfgNode().(Cfg::NameNode).defines(_) } } diff --git a/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll index e3f18170f630..bca40652403a 100644 --- a/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll @@ -11,6 +11,7 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.BarrierGuards private import semmle.python.ApiGraphs private import semmle.python.frameworks.data.internal.ApiGraphModels +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Provides default sources, sinks and sanitizers for detecting @@ -95,7 +96,7 @@ module ServerSideRequestForgery { class StringConstructionAsFullUrlControlSanitizer extends FullUrlControlSanitizer { StringConstructionAsFullUrlControlSanitizer() { // string concat - exists(BinaryExprNode add | + exists(Cfg::BinaryExprNode add | add.getOp() instanceof Add and add.getRight() = this.asCfgNode() and not add.getLeft().getNode().(StringLiteral).getText().toLowerCase() in [ @@ -104,7 +105,7 @@ module ServerSideRequestForgery { ) or // % formatting - exists(BinaryExprNode fmt | + exists(Cfg::BinaryExprNode fmt | fmt.getOp() instanceof Mod and fmt.getRight() = this.asCfgNode() and // detecting %-formatting is not super easy, so we simplify it to only handle @@ -155,7 +156,9 @@ module ServerSideRequestForgery { } } - private predicate stringRestriction(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { + private predicate stringRestriction( + DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch + ) { exists(DataFlow::MethodCallNode call, DataFlow::Node strNode | call.asCfgNode() = g and strNode.asCfgNode() = node | diff --git a/python/ql/lib/semmle/python/security/dataflow/TarSlipCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/TarSlipCustomizations.qll index 2dbe2c542aee..7dcf74d94c4e 100644 --- a/python/ql/lib/semmle/python/security/dataflow/TarSlipCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/TarSlipCustomizations.qll @@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.Concepts private import semmle.python.dataflow.new.BarrierGuards private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Provides default sources, sinks and sanitizers for detecting @@ -139,8 +140,8 @@ module TarSlip { * where `` is any function matching `"%path"`. * `info` is assumed to be a `TarInfo` instance. */ - predicate tarFileInfoSanitizer(DataFlow::GuardNode g, ControlFlowNode tarInfo, boolean branch) { - exists(CallNode call, AttrNode attr | + predicate tarFileInfoSanitizer(DataFlow::GuardNode g, Cfg::ControlFlowNode tarInfo, boolean branch) { + exists(Cfg::CallNode call, Cfg::AttrNode attr | g = call and // We must test the name of the tar info object. attr = call.getAnArg() and @@ -148,9 +149,9 @@ module TarSlip { attr.getObject() = tarInfo | // The assumption that any test that matches %path is a sanitizer might be too broad. - call.getAChild*().(AttrNode).getName().matches("%path") + call.getAChild*().(Cfg::AttrNode).getName().matches("%path") or - call.getAChild*().(NameNode).getId().matches("%path") + call.getAChild*().(Cfg::NameNode).getId().matches("%path") ) and branch = false } diff --git a/python/ql/test/query-tests/Security/CWE-022-TarSlip/TarSlip.expected b/python/ql/test/query-tests/Security/CWE-022-TarSlip/TarSlip.expected index 6f98ea1aae2b..412ac072854c 100644 --- a/python/ql/test/query-tests/Security/CWE-022-TarSlip/TarSlip.expected +++ b/python/ql/test/query-tests/Security/CWE-022-TarSlip/TarSlip.expected @@ -1,66 +1,66 @@ edges -| tarslip.py:14:1:14:3 | ControlFlowNode for tar | tarslip.py:15:1:15:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | tarslip.py:14:1:14:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:18:1:18:3 | ControlFlowNode for tar | tarslip.py:19:5:19:9 | ControlFlowNode for entry | provenance | | -| tarslip.py:18:7:18:39 | ControlFlowNode for Attribute() | tarslip.py:18:1:18:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:19:5:19:9 | ControlFlowNode for entry | tarslip.py:20:17:20:21 | ControlFlowNode for entry | provenance | | -| tarslip.py:35:1:35:3 | ControlFlowNode for tar | tarslip.py:36:5:36:9 | ControlFlowNode for entry | provenance | | -| tarslip.py:35:7:35:39 | ControlFlowNode for Attribute() | tarslip.py:35:1:35:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:36:5:36:9 | ControlFlowNode for entry | tarslip.py:39:17:39:21 | ControlFlowNode for entry | provenance | | -| tarslip.py:42:1:42:3 | ControlFlowNode for tar | tarslip.py:43:24:43:26 | ControlFlowNode for tar | provenance | | -| tarslip.py:42:7:42:39 | ControlFlowNode for Attribute() | tarslip.py:42:1:42:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:58:1:58:3 | ControlFlowNode for tar | tarslip.py:59:5:59:9 | ControlFlowNode for entry | provenance | | -| tarslip.py:58:7:58:39 | ControlFlowNode for Attribute() | tarslip.py:58:1:58:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:59:5:59:9 | ControlFlowNode for entry | tarslip.py:61:21:61:25 | ControlFlowNode for entry | provenance | | -| tarslip.py:90:1:90:3 | ControlFlowNode for tar | tarslip.py:91:1:91:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:90:7:90:39 | ControlFlowNode for Attribute() | tarslip.py:90:1:90:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:94:1:94:3 | ControlFlowNode for tar | tarslip.py:95:5:95:9 | ControlFlowNode for entry | provenance | | -| tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | tarslip.py:94:1:94:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:95:5:95:9 | ControlFlowNode for entry | tarslip.py:96:17:96:21 | ControlFlowNode for entry | provenance | | -| tarslip.py:109:1:109:3 | ControlFlowNode for tar | tarslip.py:110:1:110:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | tarslip.py:109:1:109:3 | ControlFlowNode for tar | provenance | | -| tarslip.py:112:1:112:3 | ControlFlowNode for tar | tarslip.py:113:24:113:26 | ControlFlowNode for tar | provenance | | -| tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | tarslip.py:112:1:112:3 | ControlFlowNode for tar | provenance | | +| tarslip.py:14:1:14:3 | tar | tarslip.py:15:1:15:3 | tar | provenance | | +| tarslip.py:14:7:14:39 | After Attribute() | tarslip.py:14:1:14:3 | tar | provenance | | +| tarslip.py:18:1:18:3 | tar | tarslip.py:19:5:19:9 | entry | provenance | | +| tarslip.py:18:7:18:39 | After Attribute() | tarslip.py:18:1:18:3 | tar | provenance | | +| tarslip.py:19:5:19:9 | entry | tarslip.py:20:17:20:21 | entry | provenance | | +| tarslip.py:35:1:35:3 | tar | tarslip.py:36:5:36:9 | entry | provenance | | +| tarslip.py:35:7:35:39 | After Attribute() | tarslip.py:35:1:35:3 | tar | provenance | | +| tarslip.py:36:5:36:9 | entry | tarslip.py:39:17:39:21 | entry | provenance | | +| tarslip.py:42:1:42:3 | tar | tarslip.py:43:24:43:26 | tar | provenance | | +| tarslip.py:42:7:42:39 | After Attribute() | tarslip.py:42:1:42:3 | tar | provenance | | +| tarslip.py:58:1:58:3 | tar | tarslip.py:59:5:59:9 | entry | provenance | | +| tarslip.py:58:7:58:39 | After Attribute() | tarslip.py:58:1:58:3 | tar | provenance | | +| tarslip.py:59:5:59:9 | entry | tarslip.py:61:21:61:25 | entry | provenance | | +| tarslip.py:90:1:90:3 | tar | tarslip.py:91:1:91:3 | tar | provenance | | +| tarslip.py:90:7:90:39 | After Attribute() | tarslip.py:90:1:90:3 | tar | provenance | | +| tarslip.py:94:1:94:3 | tar | tarslip.py:95:5:95:9 | entry | provenance | | +| tarslip.py:94:7:94:39 | After Attribute() | tarslip.py:94:1:94:3 | tar | provenance | | +| tarslip.py:95:5:95:9 | entry | tarslip.py:96:17:96:21 | entry | provenance | | +| tarslip.py:109:1:109:3 | tar | tarslip.py:110:1:110:3 | tar | provenance | | +| tarslip.py:109:7:109:39 | After Attribute() | tarslip.py:109:1:109:3 | tar | provenance | | +| tarslip.py:112:1:112:3 | tar | tarslip.py:113:24:113:26 | tar | provenance | | +| tarslip.py:112:7:112:39 | After Attribute() | tarslip.py:112:1:112:3 | tar | provenance | | nodes -| tarslip.py:14:1:14:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:15:1:15:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:18:1:18:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:18:7:18:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:19:5:19:9 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:20:17:20:21 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:35:1:35:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:35:7:35:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:36:5:36:9 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:39:17:39:21 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:42:1:42:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:42:7:42:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:43:24:43:26 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:58:1:58:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:58:7:58:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:59:5:59:9 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:61:21:61:25 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:90:1:90:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:90:7:90:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:91:1:91:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:94:1:94:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:95:5:95:9 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:96:17:96:21 | ControlFlowNode for entry | semmle.label | ControlFlowNode for entry | -| tarslip.py:109:1:109:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:110:1:110:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:112:1:112:3 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | -| tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | -| tarslip.py:113:24:113:26 | ControlFlowNode for tar | semmle.label | ControlFlowNode for tar | +| tarslip.py:14:1:14:3 | tar | semmle.label | tar | +| tarslip.py:14:7:14:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:15:1:15:3 | tar | semmle.label | tar | +| tarslip.py:18:1:18:3 | tar | semmle.label | tar | +| tarslip.py:18:7:18:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:19:5:19:9 | entry | semmle.label | entry | +| tarslip.py:20:17:20:21 | entry | semmle.label | entry | +| tarslip.py:35:1:35:3 | tar | semmle.label | tar | +| tarslip.py:35:7:35:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:36:5:36:9 | entry | semmle.label | entry | +| tarslip.py:39:17:39:21 | entry | semmle.label | entry | +| tarslip.py:42:1:42:3 | tar | semmle.label | tar | +| tarslip.py:42:7:42:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:43:24:43:26 | tar | semmle.label | tar | +| tarslip.py:58:1:58:3 | tar | semmle.label | tar | +| tarslip.py:58:7:58:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:59:5:59:9 | entry | semmle.label | entry | +| tarslip.py:61:21:61:25 | entry | semmle.label | entry | +| tarslip.py:90:1:90:3 | tar | semmle.label | tar | +| tarslip.py:90:7:90:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:91:1:91:3 | tar | semmle.label | tar | +| tarslip.py:94:1:94:3 | tar | semmle.label | tar | +| tarslip.py:94:7:94:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:95:5:95:9 | entry | semmle.label | entry | +| tarslip.py:96:17:96:21 | entry | semmle.label | entry | +| tarslip.py:109:1:109:3 | tar | semmle.label | tar | +| tarslip.py:109:7:109:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:110:1:110:3 | tar | semmle.label | tar | +| tarslip.py:112:1:112:3 | tar | semmle.label | tar | +| tarslip.py:112:7:112:39 | After Attribute() | semmle.label | After Attribute() | +| tarslip.py:113:24:113:26 | tar | semmle.label | tar | subpaths #select -| tarslip.py:15:1:15:3 | ControlFlowNode for tar | tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | tarslip.py:15:1:15:3 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:14:7:14:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:20:17:20:21 | ControlFlowNode for entry | tarslip.py:18:7:18:39 | ControlFlowNode for Attribute() | tarslip.py:20:17:20:21 | ControlFlowNode for entry | This file extraction depends on a $@. | tarslip.py:18:7:18:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:39:17:39:21 | ControlFlowNode for entry | tarslip.py:35:7:35:39 | ControlFlowNode for Attribute() | tarslip.py:39:17:39:21 | ControlFlowNode for entry | This file extraction depends on a $@. | tarslip.py:35:7:35:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:43:24:43:26 | ControlFlowNode for tar | tarslip.py:42:7:42:39 | ControlFlowNode for Attribute() | tarslip.py:43:24:43:26 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:42:7:42:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:61:21:61:25 | ControlFlowNode for entry | tarslip.py:58:7:58:39 | ControlFlowNode for Attribute() | tarslip.py:61:21:61:25 | ControlFlowNode for entry | This file extraction depends on a $@. | tarslip.py:58:7:58:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:91:1:91:3 | ControlFlowNode for tar | tarslip.py:90:7:90:39 | ControlFlowNode for Attribute() | tarslip.py:91:1:91:3 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:90:7:90:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:96:17:96:21 | ControlFlowNode for entry | tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | tarslip.py:96:17:96:21 | ControlFlowNode for entry | This file extraction depends on a $@. | tarslip.py:94:7:94:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:110:1:110:3 | ControlFlowNode for tar | tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | tarslip.py:110:1:110:3 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:109:7:109:39 | ControlFlowNode for Attribute() | potentially untrusted source | -| tarslip.py:113:24:113:26 | ControlFlowNode for tar | tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | tarslip.py:113:24:113:26 | ControlFlowNode for tar | This file extraction depends on a $@. | tarslip.py:112:7:112:39 | ControlFlowNode for Attribute() | potentially untrusted source | +| tarslip.py:15:1:15:3 | tar | tarslip.py:14:7:14:39 | After Attribute() | tarslip.py:15:1:15:3 | tar | This file extraction depends on a $@. | tarslip.py:14:7:14:39 | After Attribute() | potentially untrusted source | +| tarslip.py:20:17:20:21 | entry | tarslip.py:18:7:18:39 | After Attribute() | tarslip.py:20:17:20:21 | entry | This file extraction depends on a $@. | tarslip.py:18:7:18:39 | After Attribute() | potentially untrusted source | +| tarslip.py:39:17:39:21 | entry | tarslip.py:35:7:35:39 | After Attribute() | tarslip.py:39:17:39:21 | entry | This file extraction depends on a $@. | tarslip.py:35:7:35:39 | After Attribute() | potentially untrusted source | +| tarslip.py:43:24:43:26 | tar | tarslip.py:42:7:42:39 | After Attribute() | tarslip.py:43:24:43:26 | tar | This file extraction depends on a $@. | tarslip.py:42:7:42:39 | After Attribute() | potentially untrusted source | +| tarslip.py:61:21:61:25 | entry | tarslip.py:58:7:58:39 | After Attribute() | tarslip.py:61:21:61:25 | entry | This file extraction depends on a $@. | tarslip.py:58:7:58:39 | After Attribute() | potentially untrusted source | +| tarslip.py:91:1:91:3 | tar | tarslip.py:90:7:90:39 | After Attribute() | tarslip.py:91:1:91:3 | tar | This file extraction depends on a $@. | tarslip.py:90:7:90:39 | After Attribute() | potentially untrusted source | +| tarslip.py:96:17:96:21 | entry | tarslip.py:94:7:94:39 | After Attribute() | tarslip.py:96:17:96:21 | entry | This file extraction depends on a $@. | tarslip.py:94:7:94:39 | After Attribute() | potentially untrusted source | +| tarslip.py:110:1:110:3 | tar | tarslip.py:109:7:109:39 | After Attribute() | tarslip.py:110:1:110:3 | tar | This file extraction depends on a $@. | tarslip.py:109:7:109:39 | After Attribute() | potentially untrusted source | +| tarslip.py:113:24:113:26 | tar | tarslip.py:112:7:112:39 | After Attribute() | tarslip.py:113:24:113:26 | tar | This file extraction depends on a $@. | tarslip.py:112:7:112:39 | After Attribute() | potentially untrusted source | diff --git a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected index 7434eca6978b..e84845da7b2b 100644 --- a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected +++ b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected @@ -1,153 +1,153 @@ #select -| full_partial_test.py:11:5:11:28 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:11:18:11:27 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:15:5:15:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:15:18:15:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:22:5:22:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:22:18:22:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:27:5:27:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:27:18:27:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:47:5:47:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:47:18:47:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:51:5:51:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:51:18:51:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:55:5:55:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:55:18:55:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:59:5:59:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:59:18:59:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:63:5:63:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:63:18:63:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:72:5:72:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:72:18:72:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:76:5:76:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:76:18:76:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:89:5:89:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:89:18:89:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:93:5:93:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:93:18:93:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:97:5:97:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:97:18:97:20 | ControlFlowNode for url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:16:5:16:59 | ControlFlowNode for SecretClient() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:16:28:16:35 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:18:5:18:43 | ControlFlowNode for Attribute() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:18:35:18:42 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:20:5:20:35 | ControlFlowNode for KeyClient() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:20:15:20:22 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:22:5:22:85 | ControlFlowNode for Attribute() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:22:54:22:61 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:25:5:25:104 | ControlFlowNode for download_blob_from_url() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:25:37:25:44 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:15:5:15:36 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:13:27:13:37 | ControlFlowNode for unsafe_host | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:15:5:15:36 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:15:25:15:35 | ControlFlowNode for unsafe_path | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:21:5:21:36 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:21:5:21:36 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:21:25:21:35 | ControlFlowNode for unsafe_path | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:21:9:21:63 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:21:32:21:39 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:37:9:37:60 | ControlFlowNode for KeyClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:37:29:37:36 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:53:9:53:47 | ControlFlowNode for Attribute() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:53:39:53:46 | ControlFlowNode for full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:64:9:64:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:64:32:64:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:71:9:71:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:71:32:71:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:74:9:74:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:74:32:74:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:79:9:79:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:79:32:79:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:87:9:87:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:87:32:87:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:90:9:90:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:90:32:90:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:95:9:95:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:95:32:95:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:102:9:102:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:102:32:102:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:107:9:107:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:107:32:107:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:110:9:110:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:110:32:110:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:115:9:115:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:115:32:115:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:122:9:122:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:125:9:125:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:132:9:132:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_requests.py:9:5:9:28 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:9:18:9:27 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_requests.py:17:5:17:27 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:17:17:17:26 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_requests.py:22:5:22:44 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:22:34:22:43 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| full_partial_test.py:11:5:11:28 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:11:18:11:27 | user_input | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:15:5:15:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:15:18:15:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:22:5:22:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:22:18:22:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:27:5:27:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:27:18:27:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:47:5:47:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:47:18:47:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:51:5:51:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:51:18:51:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:55:5:55:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:55:18:55:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:59:5:59:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:59:18:59:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:63:5:63:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:63:18:63:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:72:5:72:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:72:18:72:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:76:5:76:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:76:18:76:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:89:5:89:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:89:18:89:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:93:5:93:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:93:18:93:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:97:5:97:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:97:18:97:20 | url | The full URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| test_azure_client.py:16:5:16:59 | After SecretClient() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:16:28:16:35 | full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:18:5:18:43 | After Attribute() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:18:35:18:42 | full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:20:5:20:35 | After KeyClient() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:20:15:20:22 | full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:22:5:22:85 | After Attribute() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:22:54:22:61 | full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:25:5:25:104 | After download_blob_from_url() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:25:37:25:44 | full_url | The full URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_http_client.py:15:5:15:36 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:13:27:13:37 | unsafe_host | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:15:5:15:36 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:15:25:15:35 | unsafe_path | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:21:5:21:36 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:19:27:19:37 | unsafe_host | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:21:5:21:36 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:21:25:21:35 | unsafe_path | The full URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_path_validation.py:21:9:21:63 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:21:32:21:39 | full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:37:9:37:60 | After KeyClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:37:29:37:36 | full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:53:9:53:47 | After Attribute() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:53:39:53:46 | full_url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:64:9:64:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:64:32:64:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:71:9:71:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:71:32:71:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:74:9:74:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:74:32:74:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:79:9:79:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:79:32:79:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:87:9:87:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:87:32:87:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:90:9:90:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:90:32:90:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:95:9:95:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:95:32:95:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:102:9:102:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:102:32:102:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:107:9:107:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:107:32:107:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:110:9:110:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:110:32:110:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:115:9:115:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:115:32:115:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:122:9:122:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:122:32:122:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:125:9:125:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:125:32:125:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:132:9:132:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:132:32:132:34 | url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_requests.py:9:5:9:28 | After Attribute() | test_requests.py:1:19:1:25 | After ImportMember | test_requests.py:9:18:9:27 | user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | After ImportMember | user-provided value | +| test_requests.py:17:5:17:27 | After Attribute() | test_requests.py:1:19:1:25 | After ImportMember | test_requests.py:17:17:17:26 | user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | After ImportMember | user-provided value | +| test_requests.py:22:5:22:44 | After Attribute() | test_requests.py:1:19:1:25 | After ImportMember | test_requests.py:22:34:22:43 | user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | After ImportMember | user-provided value | edges -| full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:7:18:7:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:41:18:41:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:66:18:66:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:83:18:83:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:11:18:11:27 | ControlFlowNode for user_input | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:13:5:13:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:20:5:20:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:25:5:25:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:18:7:24 | ControlFlowNode for request | full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:13:5:13:7 | ControlFlowNode for url | full_partial_test.py:15:18:15:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:20:5:20:7 | ControlFlowNode for url | full_partial_test.py:22:18:22:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:25:5:25:7 | ControlFlowNode for url | full_partial_test.py:27:18:27:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:45:5:45:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:49:5:49:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:53:5:53:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:57:5:57:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:61:5:61:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:18:41:24 | ControlFlowNode for request | full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:45:5:45:7 | ControlFlowNode for url | full_partial_test.py:47:18:47:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:49:5:49:7 | ControlFlowNode for url | full_partial_test.py:51:18:51:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:53:5:53:7 | ControlFlowNode for url | full_partial_test.py:55:18:55:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:57:5:57:7 | ControlFlowNode for url | full_partial_test.py:59:18:59:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:61:5:61:7 | ControlFlowNode for url | full_partial_test.py:63:18:63:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | full_partial_test.py:70:5:70:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | full_partial_test.py:74:5:74:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:18:66:24 | ControlFlowNode for request | full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:70:5:70:7 | ControlFlowNode for url | full_partial_test.py:72:18:72:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:74:5:74:7 | ControlFlowNode for url | full_partial_test.py:76:18:76:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:87:5:87:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:91:5:91:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:95:5:95:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:18:83:24 | ControlFlowNode for request | full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:87:5:87:7 | ControlFlowNode for url | full_partial_test.py:89:18:89:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:91:5:91:7 | ControlFlowNode for url | full_partial_test.py:93:18:93:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:95:5:95:7 | ControlFlowNode for url | full_partial_test.py:97:18:97:20 | ControlFlowNode for url | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:6:19:6:25 | ControlFlowNode for request | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | test_azure_client.py:9:18:9:24 | ControlFlowNode for request | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | test_azure_client.py:10:19:10:25 | ControlFlowNode for request | provenance | | -| test_azure_client.py:9:18:9:24 | ControlFlowNode for request | test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | provenance | | -| test_azure_client.py:10:19:10:25 | ControlFlowNode for request | test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:16:28:16:35 | ControlFlowNode for full_url | provenance | Sink:MaD:2 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:18:35:18:42 | ControlFlowNode for full_url | provenance | Sink:MaD:4 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:20:15:20:22 | ControlFlowNode for full_url | provenance | Sink:MaD:1 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:22:54:22:61 | ControlFlowNode for full_url | provenance | Sink:MaD:3 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:25:37:25:44 | ControlFlowNode for full_url | provenance | Sink:MaD:5 | -| test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | test_http_client.py:9:19:9:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | test_http_client.py:10:19:10:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:13:27:13:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:28:27:28:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | provenance | AdditionalTaintStep | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | provenance | AdditionalTaintStep | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:15:25:15:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:21:25:21:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:34:25:34:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:19:10:25 | ControlFlowNode for request | test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | provenance | AdditionalTaintStep | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:5:19:5:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:8:18:8:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:9:19:9:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:24:18:24:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:25:19:25:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:40:18:40:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:41:19:41:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:57:18:57:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:8:18:8:24 | ControlFlowNode for request | test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:9:19:9:25 | ControlFlowNode for request | test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | test_path_validation.py:21:32:21:39 | ControlFlowNode for full_url | provenance | Sink:MaD:2 | -| test_path_validation.py:24:18:24:24 | ControlFlowNode for request | test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:25:19:25:25 | ControlFlowNode for request | test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | test_path_validation.py:37:29:37:36 | ControlFlowNode for full_url | provenance | Sink:MaD:1 | -| test_path_validation.py:40:18:40:24 | ControlFlowNode for request | test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:41:19:41:25 | ControlFlowNode for request | test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | test_path_validation.py:53:39:53:46 | ControlFlowNode for full_url | provenance | Sink:MaD:4 | -| test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | provenance | | -| test_path_validation.py:57:18:57:24 | ControlFlowNode for request | test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:64:32:64:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:71:32:71:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:74:32:74:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:79:32:79:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:87:32:87:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:90:32:90:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:95:32:95:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:102:32:102:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:107:32:107:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:110:32:110:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:115:32:115:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:7:18:7:24 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:14:18:14:24 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:20:18:20:24 | ControlFlowNode for request | provenance | | -| test_requests.py:7:5:7:14 | ControlFlowNode for user_input | test_requests.py:9:18:9:27 | ControlFlowNode for user_input | provenance | | -| test_requests.py:7:18:7:24 | ControlFlowNode for request | test_requests.py:7:5:7:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_requests.py:14:5:14:14 | ControlFlowNode for user_input | test_requests.py:17:17:17:26 | ControlFlowNode for user_input | provenance | | -| test_requests.py:14:18:14:24 | ControlFlowNode for request | test_requests.py:14:5:14:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_requests.py:20:5:20:14 | ControlFlowNode for user_input | test_requests.py:22:34:22:43 | ControlFlowNode for user_input | provenance | | -| test_requests.py:20:18:20:24 | ControlFlowNode for request | test_requests.py:20:5:20:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:1:19:1:25 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:7:18:7:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:41:18:41:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:66:18:66:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:83:18:83:24 | request | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:11:18:11:27 | user_input | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:13:5:13:7 | url | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:20:5:20:7 | url | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:25:5:25:7 | url | provenance | | +| full_partial_test.py:7:18:7:24 | request | full_partial_test.py:7:5:7:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:13:5:13:7 | url | full_partial_test.py:15:18:15:20 | url | provenance | | +| full_partial_test.py:20:5:20:7 | url | full_partial_test.py:22:18:22:20 | url | provenance | | +| full_partial_test.py:25:5:25:7 | url | full_partial_test.py:27:18:27:20 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:45:5:45:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:49:5:49:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:53:5:53:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:57:5:57:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:61:5:61:7 | url | provenance | | +| full_partial_test.py:41:18:41:24 | request | full_partial_test.py:41:5:41:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:45:5:45:7 | url | full_partial_test.py:47:18:47:20 | url | provenance | | +| full_partial_test.py:49:5:49:7 | url | full_partial_test.py:51:18:51:20 | url | provenance | | +| full_partial_test.py:53:5:53:7 | url | full_partial_test.py:55:18:55:20 | url | provenance | | +| full_partial_test.py:57:5:57:7 | url | full_partial_test.py:59:18:59:20 | url | provenance | | +| full_partial_test.py:61:5:61:7 | url | full_partial_test.py:63:18:63:20 | url | provenance | | +| full_partial_test.py:66:5:66:14 | user_input | full_partial_test.py:70:5:70:7 | url | provenance | | +| full_partial_test.py:66:5:66:14 | user_input | full_partial_test.py:74:5:74:7 | url | provenance | | +| full_partial_test.py:66:18:66:24 | request | full_partial_test.py:66:5:66:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:70:5:70:7 | url | full_partial_test.py:72:18:72:20 | url | provenance | | +| full_partial_test.py:74:5:74:7 | url | full_partial_test.py:76:18:76:20 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:87:5:87:7 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:91:5:91:7 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:95:5:95:7 | url | provenance | | +| full_partial_test.py:83:18:83:24 | request | full_partial_test.py:83:5:83:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:87:5:87:7 | url | full_partial_test.py:89:18:89:20 | url | provenance | | +| full_partial_test.py:91:5:91:7 | url | full_partial_test.py:93:18:93:20 | url | provenance | | +| full_partial_test.py:95:5:95:7 | url | full_partial_test.py:97:18:97:20 | url | provenance | | +| test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:6:19:6:25 | request | provenance | | +| test_azure_client.py:6:19:6:25 | request | test_azure_client.py:9:18:9:24 | request | provenance | | +| test_azure_client.py:6:19:6:25 | request | test_azure_client.py:10:19:10:25 | request | provenance | | +| test_azure_client.py:9:18:9:24 | request | test_azure_client.py:10:5:10:15 | user_input2 | provenance | AdditionalTaintStep | +| test_azure_client.py:10:5:10:15 | user_input2 | test_azure_client.py:13:5:13:12 | full_url | provenance | | +| test_azure_client.py:10:19:10:25 | request | test_azure_client.py:10:5:10:15 | user_input2 | provenance | AdditionalTaintStep | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:16:28:16:35 | full_url | provenance | Sink:MaD:2 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:18:35:18:42 | full_url | provenance | Sink:MaD:4 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:20:15:20:22 | full_url | provenance | Sink:MaD:1 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:22:54:22:61 | full_url | provenance | Sink:MaD:3 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:25:37:25:44 | full_url | provenance | Sink:MaD:5 | +| test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:1:19:1:25 | request | provenance | | +| test_http_client.py:1:19:1:25 | request | test_http_client.py:9:19:9:25 | request | provenance | | +| test_http_client.py:1:19:1:25 | request | test_http_client.py:10:19:10:25 | request | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:13:27:13:37 | unsafe_host | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:19:27:19:37 | unsafe_host | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:28:27:28:37 | unsafe_host | provenance | | +| test_http_client.py:9:19:9:25 | request | test_http_client.py:9:5:9:15 | unsafe_host | provenance | AdditionalTaintStep | +| test_http_client.py:9:19:9:25 | request | test_http_client.py:10:5:10:15 | unsafe_path | provenance | AdditionalTaintStep | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:15:25:15:35 | unsafe_path | provenance | | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:21:25:21:35 | unsafe_path | provenance | | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:34:25:34:35 | unsafe_path | provenance | | +| test_http_client.py:10:19:10:25 | request | test_http_client.py:10:5:10:15 | unsafe_path | provenance | AdditionalTaintStep | +| test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:5:19:5:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:8:18:8:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:9:19:9:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:24:18:24:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:25:19:25:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:40:18:40:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:41:19:41:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:57:18:57:24 | request | provenance | | +| test_path_validation.py:8:18:8:24 | request | test_path_validation.py:9:5:9:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:9:5:9:15 | user_input2 | test_path_validation.py:11:5:11:12 | full_url | provenance | | +| test_path_validation.py:9:19:9:25 | request | test_path_validation.py:9:5:9:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:11:5:11:12 | full_url | test_path_validation.py:21:32:21:39 | full_url | provenance | Sink:MaD:2 | +| test_path_validation.py:24:18:24:24 | request | test_path_validation.py:25:5:25:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:25:5:25:15 | user_input2 | test_path_validation.py:27:5:27:12 | full_url | provenance | | +| test_path_validation.py:25:19:25:25 | request | test_path_validation.py:25:5:25:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:27:5:27:12 | full_url | test_path_validation.py:37:29:37:36 | full_url | provenance | Sink:MaD:1 | +| test_path_validation.py:40:18:40:24 | request | test_path_validation.py:41:5:41:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:41:5:41:15 | user_input2 | test_path_validation.py:43:5:43:12 | full_url | provenance | | +| test_path_validation.py:41:19:41:25 | request | test_path_validation.py:41:5:41:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:43:5:43:12 | full_url | test_path_validation.py:53:39:53:46 | full_url | provenance | Sink:MaD:4 | +| test_path_validation.py:57:5:57:14 | user_input | test_path_validation.py:61:5:61:7 | url | provenance | | +| test_path_validation.py:57:18:57:24 | request | test_path_validation.py:57:5:57:14 | user_input | provenance | AdditionalTaintStep | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:64:32:64:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:71:32:71:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:74:32:74:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:79:32:79:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:87:32:87:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:90:32:90:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:95:32:95:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:102:32:102:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:107:32:107:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:110:32:110:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:115:32:115:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:122:32:122:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:125:32:125:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:132:32:132:34 | url | provenance | Sink:MaD:2 | +| test_requests.py:1:19:1:25 | After ImportMember | test_requests.py:1:19:1:25 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:7:18:7:24 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:14:18:14:24 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:20:18:20:24 | request | provenance | | +| test_requests.py:7:5:7:14 | user_input | test_requests.py:9:18:9:27 | user_input | provenance | | +| test_requests.py:7:18:7:24 | request | test_requests.py:7:5:7:14 | user_input | provenance | AdditionalTaintStep | +| test_requests.py:14:5:14:14 | user_input | test_requests.py:17:17:17:26 | user_input | provenance | | +| test_requests.py:14:18:14:24 | request | test_requests.py:14:5:14:14 | user_input | provenance | AdditionalTaintStep | +| test_requests.py:20:5:20:14 | user_input | test_requests.py:22:34:22:43 | user_input | provenance | | +| test_requests.py:20:18:20:24 | request | test_requests.py:20:5:20:14 | user_input | provenance | AdditionalTaintStep | models | 1 | Sink: azure.keyvault.keys.KeyClient!; Call.Argument[0,vault_url:]; request-forgery | | 2 | Sink: azure.keyvault.secrets.SecretClient!; Call.Argument[0,vault_url:]; request-forgery | @@ -155,109 +155,109 @@ models | 4 | Sink: azure.storage.fileshare.ShareFileClient!; Member[from_file_url].Argument[0,file_url:]; request-forgery | | 5 | Sink: azure; Member[storage].Member[blob].Member[download_blob_from_url].Argument[0,blob_url:]; request-forgery | nodes -| full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:7:18:7:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:11:18:11:27 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:13:5:13:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:15:18:15:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:20:5:20:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:22:18:22:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:25:5:25:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:27:18:27:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:41:18:41:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:45:5:45:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:47:18:47:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:49:5:49:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:51:18:51:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:53:5:53:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:55:18:55:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:57:5:57:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:59:18:59:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:61:5:61:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:63:18:63:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:66:18:66:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:70:5:70:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:72:18:72:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:74:5:74:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:76:18:76:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:83:18:83:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:87:5:87:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:89:18:89:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:91:5:91:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:93:18:93:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:95:5:95:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:97:18:97:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_azure_client.py:10:19:10:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:16:28:16:35 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:18:35:18:42 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:20:15:20:22 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:22:54:22:61 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:25:37:25:44 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:10:19:10:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:13:27:13:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:15:25:15:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:21:25:21:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:28:27:28:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:34:25:34:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:8:18:8:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:9:19:9:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:21:32:21:39 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:24:18:24:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:25:19:25:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:37:29:37:36 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:40:18:40:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:41:19:41:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:53:39:53:46 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_path_validation.py:57:18:57:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:64:32:64:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:71:32:71:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:74:32:74:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:79:32:79:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:87:32:87:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:90:32:90:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:95:32:95:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:102:32:102:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:107:32:107:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:110:32:110:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:115:32:115:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:122:32:122:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:125:32:125:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:132:32:132:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:7:18:7:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:9:18:9:27 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:14:5:14:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:14:18:14:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:17:17:17:26 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:20:5:20:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:20:18:20:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:22:34:22:43 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | +| full_partial_test.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| full_partial_test.py:1:19:1:25 | request | semmle.label | request | +| full_partial_test.py:7:5:7:14 | user_input | semmle.label | user_input | +| full_partial_test.py:7:18:7:24 | request | semmle.label | request | +| full_partial_test.py:11:18:11:27 | user_input | semmle.label | user_input | +| full_partial_test.py:13:5:13:7 | url | semmle.label | url | +| full_partial_test.py:15:18:15:20 | url | semmle.label | url | +| full_partial_test.py:20:5:20:7 | url | semmle.label | url | +| full_partial_test.py:22:18:22:20 | url | semmle.label | url | +| full_partial_test.py:25:5:25:7 | url | semmle.label | url | +| full_partial_test.py:27:18:27:20 | url | semmle.label | url | +| full_partial_test.py:41:5:41:14 | user_input | semmle.label | user_input | +| full_partial_test.py:41:18:41:24 | request | semmle.label | request | +| full_partial_test.py:45:5:45:7 | url | semmle.label | url | +| full_partial_test.py:47:18:47:20 | url | semmle.label | url | +| full_partial_test.py:49:5:49:7 | url | semmle.label | url | +| full_partial_test.py:51:18:51:20 | url | semmle.label | url | +| full_partial_test.py:53:5:53:7 | url | semmle.label | url | +| full_partial_test.py:55:18:55:20 | url | semmle.label | url | +| full_partial_test.py:57:5:57:7 | url | semmle.label | url | +| full_partial_test.py:59:18:59:20 | url | semmle.label | url | +| full_partial_test.py:61:5:61:7 | url | semmle.label | url | +| full_partial_test.py:63:18:63:20 | url | semmle.label | url | +| full_partial_test.py:66:5:66:14 | user_input | semmle.label | user_input | +| full_partial_test.py:66:18:66:24 | request | semmle.label | request | +| full_partial_test.py:70:5:70:7 | url | semmle.label | url | +| full_partial_test.py:72:18:72:20 | url | semmle.label | url | +| full_partial_test.py:74:5:74:7 | url | semmle.label | url | +| full_partial_test.py:76:18:76:20 | url | semmle.label | url | +| full_partial_test.py:83:5:83:14 | user_input | semmle.label | user_input | +| full_partial_test.py:83:18:83:24 | request | semmle.label | request | +| full_partial_test.py:87:5:87:7 | url | semmle.label | url | +| full_partial_test.py:89:18:89:20 | url | semmle.label | url | +| full_partial_test.py:91:5:91:7 | url | semmle.label | url | +| full_partial_test.py:93:18:93:20 | url | semmle.label | url | +| full_partial_test.py:95:5:95:7 | url | semmle.label | url | +| full_partial_test.py:97:18:97:20 | url | semmle.label | url | +| test_azure_client.py:6:19:6:25 | After ImportMember | semmle.label | After ImportMember | +| test_azure_client.py:6:19:6:25 | request | semmle.label | request | +| test_azure_client.py:9:18:9:24 | request | semmle.label | request | +| test_azure_client.py:10:5:10:15 | user_input2 | semmle.label | user_input2 | +| test_azure_client.py:10:19:10:25 | request | semmle.label | request | +| test_azure_client.py:13:5:13:12 | full_url | semmle.label | full_url | +| test_azure_client.py:16:28:16:35 | full_url | semmle.label | full_url | +| test_azure_client.py:18:35:18:42 | full_url | semmle.label | full_url | +| test_azure_client.py:20:15:20:22 | full_url | semmle.label | full_url | +| test_azure_client.py:22:54:22:61 | full_url | semmle.label | full_url | +| test_azure_client.py:25:37:25:44 | full_url | semmle.label | full_url | +| test_http_client.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| test_http_client.py:1:19:1:25 | request | semmle.label | request | +| test_http_client.py:9:5:9:15 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:9:19:9:25 | request | semmle.label | request | +| test_http_client.py:10:5:10:15 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:10:19:10:25 | request | semmle.label | request | +| test_http_client.py:13:27:13:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:15:25:15:35 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:19:27:19:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:21:25:21:35 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:28:27:28:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:34:25:34:35 | unsafe_path | semmle.label | unsafe_path | +| test_path_validation.py:5:19:5:25 | After ImportMember | semmle.label | After ImportMember | +| test_path_validation.py:5:19:5:25 | request | semmle.label | request | +| test_path_validation.py:8:18:8:24 | request | semmle.label | request | +| test_path_validation.py:9:5:9:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:9:19:9:25 | request | semmle.label | request | +| test_path_validation.py:11:5:11:12 | full_url | semmle.label | full_url | +| test_path_validation.py:21:32:21:39 | full_url | semmle.label | full_url | +| test_path_validation.py:24:18:24:24 | request | semmle.label | request | +| test_path_validation.py:25:5:25:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:25:19:25:25 | request | semmle.label | request | +| test_path_validation.py:27:5:27:12 | full_url | semmle.label | full_url | +| test_path_validation.py:37:29:37:36 | full_url | semmle.label | full_url | +| test_path_validation.py:40:18:40:24 | request | semmle.label | request | +| test_path_validation.py:41:5:41:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:41:19:41:25 | request | semmle.label | request | +| test_path_validation.py:43:5:43:12 | full_url | semmle.label | full_url | +| test_path_validation.py:53:39:53:46 | full_url | semmle.label | full_url | +| test_path_validation.py:57:5:57:14 | user_input | semmle.label | user_input | +| test_path_validation.py:57:18:57:24 | request | semmle.label | request | +| test_path_validation.py:61:5:61:7 | url | semmle.label | url | +| test_path_validation.py:64:32:64:34 | url | semmle.label | url | +| test_path_validation.py:71:32:71:34 | url | semmle.label | url | +| test_path_validation.py:74:32:74:34 | url | semmle.label | url | +| test_path_validation.py:79:32:79:34 | url | semmle.label | url | +| test_path_validation.py:87:32:87:34 | url | semmle.label | url | +| test_path_validation.py:90:32:90:34 | url | semmle.label | url | +| test_path_validation.py:95:32:95:34 | url | semmle.label | url | +| test_path_validation.py:102:32:102:34 | url | semmle.label | url | +| test_path_validation.py:107:32:107:34 | url | semmle.label | url | +| test_path_validation.py:110:32:110:34 | url | semmle.label | url | +| test_path_validation.py:115:32:115:34 | url | semmle.label | url | +| test_path_validation.py:122:32:122:34 | url | semmle.label | url | +| test_path_validation.py:125:32:125:34 | url | semmle.label | url | +| test_path_validation.py:132:32:132:34 | url | semmle.label | url | +| test_requests.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| test_requests.py:1:19:1:25 | request | semmle.label | request | +| test_requests.py:7:5:7:14 | user_input | semmle.label | user_input | +| test_requests.py:7:18:7:24 | request | semmle.label | request | +| test_requests.py:9:18:9:27 | user_input | semmle.label | user_input | +| test_requests.py:14:5:14:14 | user_input | semmle.label | user_input | +| test_requests.py:14:18:14:24 | request | semmle.label | request | +| test_requests.py:17:17:17:26 | user_input | semmle.label | user_input | +| test_requests.py:20:5:20:14 | user_input | semmle.label | user_input | +| test_requests.py:20:18:20:24 | request | semmle.label | request | +| test_requests.py:22:34:22:43 | user_input | semmle.label | user_input | subpaths diff --git a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected index 0b8756071573..c6f2fe014113 100644 --- a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected +++ b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected @@ -1,236 +1,236 @@ #select -| full_partial_test.py:80:5:80:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:80:18:80:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:105:5:105:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:105:18:105:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:112:5:112:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:112:18:112:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:119:5:119:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:119:18:119:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:126:5:126:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:126:18:126:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:136:5:136:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:136:18:136:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| full_partial_test.py:143:5:143:21 | ControlFlowNode for Attribute() | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:143:18:143:20 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:15:5:15:54 | ControlFlowNode for SecretClient() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:15:28:15:30 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:17:5:17:38 | ControlFlowNode for Attribute() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:17:35:17:37 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:19:5:19:30 | ControlFlowNode for KeyClient() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:19:15:19:17 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:21:5:21:80 | ControlFlowNode for Attribute() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:21:54:21:56 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_azure_client.py:24:5:24:100 | ControlFlowNode for download_blob_from_url() | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:24:37:24:39 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:25:5:25:31 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:30:5:30:31 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:28:27:28:37 | ControlFlowNode for unsafe_host | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:34:5:34:36 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:34:25:34:35 | ControlFlowNode for unsafe_path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:39:5:39:29 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:39:25:39:28 | ControlFlowNode for path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_http_client.py:44:5:44:29 | ControlFlowNode for Attribute() | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:44:25:44:28 | ControlFlowNode for path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:14:9:14:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:14:32:14:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:16:9:16:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:16:32:16:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:19:9:19:63 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:19:32:19:39 | ControlFlowNode for full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:30:9:30:55 | ControlFlowNode for KeyClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:30:29:30:31 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:32:9:32:55 | ControlFlowNode for KeyClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:32:29:32:31 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:35:9:35:60 | ControlFlowNode for KeyClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:35:29:35:36 | ControlFlowNode for full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:46:9:46:42 | ControlFlowNode for Attribute() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:46:39:46:41 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:48:9:48:42 | ControlFlowNode for Attribute() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:48:39:48:41 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:51:9:51:47 | ControlFlowNode for Attribute() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:51:39:51:46 | ControlFlowNode for full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:66:9:66:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:66:32:66:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:69:9:69:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:69:32:69:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:76:9:76:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:76:32:76:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:81:9:81:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:81:32:81:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:85:9:85:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:85:32:85:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:92:9:92:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:92:32:92:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:97:9:97:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:97:32:97:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:100:9:100:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:100:32:100:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:105:9:105:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:105:32:105:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:112:9:112:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:112:32:112:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:117:9:117:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:117:32:117:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:120:9:120:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:120:32:120:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:127:9:127:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:127:32:127:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | -| test_path_validation.py:130:9:130:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:130:32:130:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | +| full_partial_test.py:80:5:80:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:80:18:80:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:105:5:105:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:105:18:105:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:112:5:112:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:112:18:112:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:119:5:119:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:119:18:119:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:126:5:126:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:126:18:126:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:136:5:136:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:136:18:136:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| full_partial_test.py:143:5:143:21 | After Attribute() | full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:143:18:143:20 | url | Part of the URL of this request depends on a $@. | full_partial_test.py:1:19:1:25 | After ImportMember | user-provided value | +| test_azure_client.py:15:5:15:54 | After SecretClient() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:15:28:15:30 | url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:17:5:17:38 | After Attribute() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:17:35:17:37 | url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:19:5:19:30 | After KeyClient() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:19:15:19:17 | url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:21:5:21:80 | After Attribute() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:21:54:21:56 | url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_azure_client.py:24:5:24:100 | After download_blob_from_url() | test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:24:37:24:39 | url | Part of the URL of this request depends on a $@. | test_azure_client.py:6:19:6:25 | After ImportMember | user-provided value | +| test_http_client.py:25:5:25:31 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:19:27:19:37 | unsafe_host | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:30:5:30:31 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:28:27:28:37 | unsafe_host | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:34:5:34:36 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:34:25:34:35 | unsafe_path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:39:5:39:29 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:39:25:39:28 | path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_http_client.py:44:5:44:29 | After Attribute() | test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:44:25:44:28 | path | Part of the URL of this request depends on a $@. | test_http_client.py:1:19:1:25 | After ImportMember | user-provided value | +| test_path_validation.py:14:9:14:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:14:32:14:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:16:9:16:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:16:32:16:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:19:9:19:63 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:19:32:19:39 | full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:30:9:30:55 | After KeyClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:30:29:30:31 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:32:9:32:55 | After KeyClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:32:29:32:31 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:35:9:35:60 | After KeyClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:35:29:35:36 | full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:46:9:46:42 | After Attribute() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:46:39:46:41 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:48:9:48:42 | After Attribute() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:48:39:48:41 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:51:9:51:47 | After Attribute() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:51:39:51:46 | full_url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:66:9:66:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:66:32:66:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:69:9:69:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:69:32:69:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:76:9:76:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:76:32:76:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:81:9:81:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:81:32:81:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:85:9:85:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:85:32:85:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:92:9:92:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:92:32:92:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:97:9:97:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:97:32:97:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:100:9:100:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:100:32:100:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:105:9:105:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:105:32:105:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:112:9:112:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:112:32:112:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:117:9:117:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:117:32:117:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:120:9:120:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:120:32:120:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:127:9:127:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:127:32:127:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | +| test_path_validation.py:130:9:130:58 | After SecretClient() | test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:130:32:130:34 | url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | After ImportMember | user-provided value | edges -| full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:7:18:7:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:8:17:8:23 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:41:18:41:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:42:17:42:23 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:66:18:66:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:67:17:67:23 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:83:18:83:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:84:17:84:23 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:101:18:101:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:108:18:108:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:115:18:115:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:122:18:122:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:129:18:129:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:139:18:139:24 | ControlFlowNode for request | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:11:18:11:27 | ControlFlowNode for user_input | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:13:5:13:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:20:5:20:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | full_partial_test.py:25:5:25:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:7:18:7:24 | ControlFlowNode for request | full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:7:18:7:24 | ControlFlowNode for request | full_partial_test.py:8:5:8:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:8:5:8:13 | ControlFlowNode for query_val | full_partial_test.py:25:5:25:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:8:17:8:23 | ControlFlowNode for request | full_partial_test.py:8:5:8:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:13:5:13:7 | ControlFlowNode for url | full_partial_test.py:15:18:15:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:20:5:20:7 | ControlFlowNode for url | full_partial_test.py:22:18:22:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:25:5:25:7 | ControlFlowNode for url | full_partial_test.py:27:18:27:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:45:5:45:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:49:5:49:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:53:5:53:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:57:5:57:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | full_partial_test.py:61:5:61:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:41:18:41:24 | ControlFlowNode for request | full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:41:18:41:24 | ControlFlowNode for request | full_partial_test.py:42:5:42:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:42:5:42:13 | ControlFlowNode for query_val | full_partial_test.py:53:5:53:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:42:17:42:23 | ControlFlowNode for request | full_partial_test.py:42:5:42:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:45:5:45:7 | ControlFlowNode for url | full_partial_test.py:47:18:47:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:49:5:49:7 | ControlFlowNode for url | full_partial_test.py:51:18:51:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:53:5:53:7 | ControlFlowNode for url | full_partial_test.py:55:18:55:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:57:5:57:7 | ControlFlowNode for url | full_partial_test.py:59:18:59:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:61:5:61:7 | ControlFlowNode for url | full_partial_test.py:63:18:63:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | full_partial_test.py:70:5:70:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | full_partial_test.py:74:5:74:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | full_partial_test.py:78:5:78:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:66:18:66:24 | ControlFlowNode for request | full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:66:18:66:24 | ControlFlowNode for request | full_partial_test.py:67:5:67:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:67:5:67:13 | ControlFlowNode for query_val | full_partial_test.py:78:5:78:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:67:17:67:23 | ControlFlowNode for request | full_partial_test.py:67:5:67:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:70:5:70:7 | ControlFlowNode for url | full_partial_test.py:72:18:72:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:74:5:74:7 | ControlFlowNode for url | full_partial_test.py:76:18:76:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:78:5:78:7 | ControlFlowNode for url | full_partial_test.py:80:18:80:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:87:5:87:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:91:5:91:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | full_partial_test.py:95:5:95:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:83:18:83:24 | ControlFlowNode for request | full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:83:18:83:24 | ControlFlowNode for request | full_partial_test.py:84:5:84:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:84:5:84:13 | ControlFlowNode for query_val | full_partial_test.py:95:5:95:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:84:17:84:23 | ControlFlowNode for request | full_partial_test.py:84:5:84:13 | ControlFlowNode for query_val | provenance | AdditionalTaintStep | -| full_partial_test.py:87:5:87:7 | ControlFlowNode for url | full_partial_test.py:89:18:89:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:91:5:91:7 | ControlFlowNode for url | full_partial_test.py:93:18:93:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:95:5:95:7 | ControlFlowNode for url | full_partial_test.py:97:18:97:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:101:5:101:14 | ControlFlowNode for user_input | full_partial_test.py:103:5:103:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:101:18:101:24 | ControlFlowNode for request | full_partial_test.py:101:5:101:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:103:5:103:7 | ControlFlowNode for url | full_partial_test.py:105:18:105:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:108:5:108:14 | ControlFlowNode for user_input | full_partial_test.py:110:5:110:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:108:18:108:24 | ControlFlowNode for request | full_partial_test.py:108:5:108:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:110:5:110:7 | ControlFlowNode for url | full_partial_test.py:112:18:112:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:115:5:115:14 | ControlFlowNode for user_input | full_partial_test.py:117:5:117:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:115:18:115:24 | ControlFlowNode for request | full_partial_test.py:115:5:115:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:117:5:117:7 | ControlFlowNode for url | full_partial_test.py:119:18:119:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:122:5:122:14 | ControlFlowNode for user_input | full_partial_test.py:124:5:124:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:122:18:122:24 | ControlFlowNode for request | full_partial_test.py:122:5:122:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:124:5:124:7 | ControlFlowNode for url | full_partial_test.py:126:18:126:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:129:5:129:14 | ControlFlowNode for user_input | full_partial_test.py:134:5:134:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:129:18:129:24 | ControlFlowNode for request | full_partial_test.py:129:5:129:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:134:5:134:7 | ControlFlowNode for url | full_partial_test.py:136:18:136:20 | ControlFlowNode for url | provenance | | -| full_partial_test.py:139:5:139:14 | ControlFlowNode for user_input | full_partial_test.py:141:5:141:7 | ControlFlowNode for url | provenance | | -| full_partial_test.py:139:18:139:24 | ControlFlowNode for request | full_partial_test.py:139:5:139:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| full_partial_test.py:141:5:141:7 | ControlFlowNode for url | full_partial_test.py:143:18:143:20 | ControlFlowNode for url | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | test_azure_client.py:6:19:6:25 | ControlFlowNode for request | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | test_azure_client.py:9:18:9:24 | ControlFlowNode for request | provenance | | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | test_azure_client.py:10:19:10:25 | ControlFlowNode for request | provenance | | -| test_azure_client.py:9:5:9:14 | ControlFlowNode for user_input | test_azure_client.py:12:5:12:7 | ControlFlowNode for url | provenance | | -| test_azure_client.py:9:18:9:24 | ControlFlowNode for request | test_azure_client.py:9:5:9:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_azure_client.py:9:18:9:24 | ControlFlowNode for request | test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | provenance | | -| test_azure_client.py:10:19:10:25 | ControlFlowNode for request | test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | test_azure_client.py:15:28:15:30 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | test_azure_client.py:17:35:17:37 | ControlFlowNode for url | provenance | Sink:MaD:4 | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | test_azure_client.py:19:15:19:17 | ControlFlowNode for url | provenance | Sink:MaD:1 | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | test_azure_client.py:21:54:21:56 | ControlFlowNode for url | provenance | Sink:MaD:3 | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | test_azure_client.py:24:37:24:39 | ControlFlowNode for url | provenance | Sink:MaD:5 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:16:28:16:35 | ControlFlowNode for full_url | provenance | Sink:MaD:2 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:18:35:18:42 | ControlFlowNode for full_url | provenance | Sink:MaD:4 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:20:15:20:22 | ControlFlowNode for full_url | provenance | Sink:MaD:1 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:22:54:22:61 | ControlFlowNode for full_url | provenance | Sink:MaD:3 | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | test_azure_client.py:25:37:25:44 | ControlFlowNode for full_url | provenance | Sink:MaD:5 | -| test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | test_http_client.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | test_http_client.py:9:19:9:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | test_http_client.py:10:19:10:25 | ControlFlowNode for request | provenance | | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | test_http_client.py:11:18:11:24 | ControlFlowNode for request | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:13:27:13:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | test_http_client.py:28:27:28:37 | ControlFlowNode for unsafe_host | provenance | | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | provenance | AdditionalTaintStep | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | provenance | AdditionalTaintStep | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:15:25:15:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:21:25:21:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | test_http_client.py:34:25:34:35 | ControlFlowNode for unsafe_path | provenance | | -| test_http_client.py:10:19:10:25 | ControlFlowNode for request | test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | provenance | AdditionalTaintStep | -| test_http_client.py:10:19:10:25 | ControlFlowNode for request | test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | test_http_client.py:36:5:36:8 | ControlFlowNode for path | provenance | | -| test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | test_http_client.py:41:5:41:8 | ControlFlowNode for path | provenance | | -| test_http_client.py:11:18:11:24 | ControlFlowNode for request | test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_http_client.py:36:5:36:8 | ControlFlowNode for path | test_http_client.py:39:25:39:28 | ControlFlowNode for path | provenance | | -| test_http_client.py:41:5:41:8 | ControlFlowNode for path | test_http_client.py:44:25:44:28 | ControlFlowNode for path | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:5:19:5:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:8:18:8:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:9:19:9:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:24:18:24:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:25:19:25:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:40:18:40:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:41:19:41:25 | ControlFlowNode for request | provenance | | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | test_path_validation.py:57:18:57:24 | ControlFlowNode for request | provenance | | -| test_path_validation.py:8:5:8:14 | ControlFlowNode for user_input | test_path_validation.py:10:5:10:7 | ControlFlowNode for url | provenance | | -| test_path_validation.py:8:18:8:24 | ControlFlowNode for request | test_path_validation.py:8:5:8:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_path_validation.py:8:18:8:24 | ControlFlowNode for request | test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:9:19:9:25 | ControlFlowNode for request | test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:10:5:10:7 | ControlFlowNode for url | test_path_validation.py:14:32:14:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:10:5:10:7 | ControlFlowNode for url | test_path_validation.py:16:32:16:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | test_path_validation.py:19:32:19:39 | ControlFlowNode for full_url | provenance | Sink:MaD:2 | -| test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | test_path_validation.py:21:32:21:39 | ControlFlowNode for full_url | provenance | Sink:MaD:2 | -| test_path_validation.py:24:5:24:14 | ControlFlowNode for user_input | test_path_validation.py:26:5:26:7 | ControlFlowNode for url | provenance | | -| test_path_validation.py:24:18:24:24 | ControlFlowNode for request | test_path_validation.py:24:5:24:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_path_validation.py:24:18:24:24 | ControlFlowNode for request | test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:25:19:25:25 | ControlFlowNode for request | test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:26:5:26:7 | ControlFlowNode for url | test_path_validation.py:30:29:30:31 | ControlFlowNode for url | provenance | Sink:MaD:1 | -| test_path_validation.py:26:5:26:7 | ControlFlowNode for url | test_path_validation.py:32:29:32:31 | ControlFlowNode for url | provenance | Sink:MaD:1 | -| test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | test_path_validation.py:35:29:35:36 | ControlFlowNode for full_url | provenance | Sink:MaD:1 | -| test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | test_path_validation.py:37:29:37:36 | ControlFlowNode for full_url | provenance | Sink:MaD:1 | -| test_path_validation.py:40:5:40:14 | ControlFlowNode for user_input | test_path_validation.py:42:5:42:7 | ControlFlowNode for url | provenance | | -| test_path_validation.py:40:18:40:24 | ControlFlowNode for request | test_path_validation.py:40:5:40:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_path_validation.py:40:18:40:24 | ControlFlowNode for request | test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | provenance | | -| test_path_validation.py:41:19:41:25 | ControlFlowNode for request | test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | provenance | AdditionalTaintStep | -| test_path_validation.py:42:5:42:7 | ControlFlowNode for url | test_path_validation.py:46:39:46:41 | ControlFlowNode for url | provenance | Sink:MaD:4 | -| test_path_validation.py:42:5:42:7 | ControlFlowNode for url | test_path_validation.py:48:39:48:41 | ControlFlowNode for url | provenance | Sink:MaD:4 | -| test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | test_path_validation.py:51:39:51:46 | ControlFlowNode for full_url | provenance | Sink:MaD:4 | -| test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | test_path_validation.py:53:39:53:46 | ControlFlowNode for full_url | provenance | Sink:MaD:4 | -| test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | provenance | | -| test_path_validation.py:57:18:57:24 | ControlFlowNode for request | test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:64:32:64:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:66:32:66:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:69:32:69:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:71:32:71:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:74:32:74:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:76:32:76:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:79:32:79:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:81:32:81:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:85:32:85:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:87:32:87:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:90:32:90:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:92:32:92:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:95:32:95:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:97:32:97:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:100:32:100:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:102:32:102:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:105:32:105:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:107:32:107:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:110:32:110:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:112:32:112:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:115:32:115:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:117:32:117:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:120:32:120:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:127:32:127:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:130:32:130:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | -| test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:7:18:7:24 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:14:18:14:24 | ControlFlowNode for request | provenance | | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:20:18:20:24 | ControlFlowNode for request | provenance | | -| test_requests.py:7:5:7:14 | ControlFlowNode for user_input | test_requests.py:9:18:9:27 | ControlFlowNode for user_input | provenance | | -| test_requests.py:7:18:7:24 | ControlFlowNode for request | test_requests.py:7:5:7:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_requests.py:14:5:14:14 | ControlFlowNode for user_input | test_requests.py:17:17:17:26 | ControlFlowNode for user_input | provenance | | -| test_requests.py:14:18:14:24 | ControlFlowNode for request | test_requests.py:14:5:14:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | -| test_requests.py:20:5:20:14 | ControlFlowNode for user_input | test_requests.py:22:34:22:43 | ControlFlowNode for user_input | provenance | | -| test_requests.py:20:18:20:24 | ControlFlowNode for request | test_requests.py:20:5:20:14 | ControlFlowNode for user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:1:19:1:25 | After ImportMember | full_partial_test.py:1:19:1:25 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:7:18:7:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:8:17:8:23 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:41:18:41:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:42:17:42:23 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:66:18:66:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:67:17:67:23 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:83:18:83:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:84:17:84:23 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:101:18:101:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:108:18:108:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:115:18:115:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:122:18:122:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:129:18:129:24 | request | provenance | | +| full_partial_test.py:1:19:1:25 | request | full_partial_test.py:139:18:139:24 | request | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:11:18:11:27 | user_input | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:13:5:13:7 | url | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:20:5:20:7 | url | provenance | | +| full_partial_test.py:7:5:7:14 | user_input | full_partial_test.py:25:5:25:7 | url | provenance | | +| full_partial_test.py:7:18:7:24 | request | full_partial_test.py:7:5:7:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:7:18:7:24 | request | full_partial_test.py:8:5:8:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:8:5:8:13 | query_val | full_partial_test.py:25:5:25:7 | url | provenance | | +| full_partial_test.py:8:17:8:23 | request | full_partial_test.py:8:5:8:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:13:5:13:7 | url | full_partial_test.py:15:18:15:20 | url | provenance | | +| full_partial_test.py:20:5:20:7 | url | full_partial_test.py:22:18:22:20 | url | provenance | | +| full_partial_test.py:25:5:25:7 | url | full_partial_test.py:27:18:27:20 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:45:5:45:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:49:5:49:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:53:5:53:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:57:5:57:7 | url | provenance | | +| full_partial_test.py:41:5:41:14 | user_input | full_partial_test.py:61:5:61:7 | url | provenance | | +| full_partial_test.py:41:18:41:24 | request | full_partial_test.py:41:5:41:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:41:18:41:24 | request | full_partial_test.py:42:5:42:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:42:5:42:13 | query_val | full_partial_test.py:53:5:53:7 | url | provenance | | +| full_partial_test.py:42:17:42:23 | request | full_partial_test.py:42:5:42:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:45:5:45:7 | url | full_partial_test.py:47:18:47:20 | url | provenance | | +| full_partial_test.py:49:5:49:7 | url | full_partial_test.py:51:18:51:20 | url | provenance | | +| full_partial_test.py:53:5:53:7 | url | full_partial_test.py:55:18:55:20 | url | provenance | | +| full_partial_test.py:57:5:57:7 | url | full_partial_test.py:59:18:59:20 | url | provenance | | +| full_partial_test.py:61:5:61:7 | url | full_partial_test.py:63:18:63:20 | url | provenance | | +| full_partial_test.py:66:5:66:14 | user_input | full_partial_test.py:70:5:70:7 | url | provenance | | +| full_partial_test.py:66:5:66:14 | user_input | full_partial_test.py:74:5:74:7 | url | provenance | | +| full_partial_test.py:66:5:66:14 | user_input | full_partial_test.py:78:5:78:7 | url | provenance | | +| full_partial_test.py:66:18:66:24 | request | full_partial_test.py:66:5:66:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:66:18:66:24 | request | full_partial_test.py:67:5:67:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:67:5:67:13 | query_val | full_partial_test.py:78:5:78:7 | url | provenance | | +| full_partial_test.py:67:17:67:23 | request | full_partial_test.py:67:5:67:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:70:5:70:7 | url | full_partial_test.py:72:18:72:20 | url | provenance | | +| full_partial_test.py:74:5:74:7 | url | full_partial_test.py:76:18:76:20 | url | provenance | | +| full_partial_test.py:78:5:78:7 | url | full_partial_test.py:80:18:80:20 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:87:5:87:7 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:91:5:91:7 | url | provenance | | +| full_partial_test.py:83:5:83:14 | user_input | full_partial_test.py:95:5:95:7 | url | provenance | | +| full_partial_test.py:83:18:83:24 | request | full_partial_test.py:83:5:83:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:83:18:83:24 | request | full_partial_test.py:84:5:84:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:84:5:84:13 | query_val | full_partial_test.py:95:5:95:7 | url | provenance | | +| full_partial_test.py:84:17:84:23 | request | full_partial_test.py:84:5:84:13 | query_val | provenance | AdditionalTaintStep | +| full_partial_test.py:87:5:87:7 | url | full_partial_test.py:89:18:89:20 | url | provenance | | +| full_partial_test.py:91:5:91:7 | url | full_partial_test.py:93:18:93:20 | url | provenance | | +| full_partial_test.py:95:5:95:7 | url | full_partial_test.py:97:18:97:20 | url | provenance | | +| full_partial_test.py:101:5:101:14 | user_input | full_partial_test.py:103:5:103:7 | url | provenance | | +| full_partial_test.py:101:18:101:24 | request | full_partial_test.py:101:5:101:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:103:5:103:7 | url | full_partial_test.py:105:18:105:20 | url | provenance | | +| full_partial_test.py:108:5:108:14 | user_input | full_partial_test.py:110:5:110:7 | url | provenance | | +| full_partial_test.py:108:18:108:24 | request | full_partial_test.py:108:5:108:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:110:5:110:7 | url | full_partial_test.py:112:18:112:20 | url | provenance | | +| full_partial_test.py:115:5:115:14 | user_input | full_partial_test.py:117:5:117:7 | url | provenance | | +| full_partial_test.py:115:18:115:24 | request | full_partial_test.py:115:5:115:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:117:5:117:7 | url | full_partial_test.py:119:18:119:20 | url | provenance | | +| full_partial_test.py:122:5:122:14 | user_input | full_partial_test.py:124:5:124:7 | url | provenance | | +| full_partial_test.py:122:18:122:24 | request | full_partial_test.py:122:5:122:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:124:5:124:7 | url | full_partial_test.py:126:18:126:20 | url | provenance | | +| full_partial_test.py:129:5:129:14 | user_input | full_partial_test.py:134:5:134:7 | url | provenance | | +| full_partial_test.py:129:18:129:24 | request | full_partial_test.py:129:5:129:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:134:5:134:7 | url | full_partial_test.py:136:18:136:20 | url | provenance | | +| full_partial_test.py:139:5:139:14 | user_input | full_partial_test.py:141:5:141:7 | url | provenance | | +| full_partial_test.py:139:18:139:24 | request | full_partial_test.py:139:5:139:14 | user_input | provenance | AdditionalTaintStep | +| full_partial_test.py:141:5:141:7 | url | full_partial_test.py:143:18:143:20 | url | provenance | | +| test_azure_client.py:6:19:6:25 | After ImportMember | test_azure_client.py:6:19:6:25 | request | provenance | | +| test_azure_client.py:6:19:6:25 | request | test_azure_client.py:9:18:9:24 | request | provenance | | +| test_azure_client.py:6:19:6:25 | request | test_azure_client.py:10:19:10:25 | request | provenance | | +| test_azure_client.py:9:5:9:14 | user_input | test_azure_client.py:12:5:12:7 | url | provenance | | +| test_azure_client.py:9:18:9:24 | request | test_azure_client.py:9:5:9:14 | user_input | provenance | AdditionalTaintStep | +| test_azure_client.py:9:18:9:24 | request | test_azure_client.py:10:5:10:15 | user_input2 | provenance | AdditionalTaintStep | +| test_azure_client.py:10:5:10:15 | user_input2 | test_azure_client.py:13:5:13:12 | full_url | provenance | | +| test_azure_client.py:10:19:10:25 | request | test_azure_client.py:10:5:10:15 | user_input2 | provenance | AdditionalTaintStep | +| test_azure_client.py:12:5:12:7 | url | test_azure_client.py:15:28:15:30 | url | provenance | Sink:MaD:2 | +| test_azure_client.py:12:5:12:7 | url | test_azure_client.py:17:35:17:37 | url | provenance | Sink:MaD:4 | +| test_azure_client.py:12:5:12:7 | url | test_azure_client.py:19:15:19:17 | url | provenance | Sink:MaD:1 | +| test_azure_client.py:12:5:12:7 | url | test_azure_client.py:21:54:21:56 | url | provenance | Sink:MaD:3 | +| test_azure_client.py:12:5:12:7 | url | test_azure_client.py:24:37:24:39 | url | provenance | Sink:MaD:5 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:16:28:16:35 | full_url | provenance | Sink:MaD:2 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:18:35:18:42 | full_url | provenance | Sink:MaD:4 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:20:15:20:22 | full_url | provenance | Sink:MaD:1 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:22:54:22:61 | full_url | provenance | Sink:MaD:3 | +| test_azure_client.py:13:5:13:12 | full_url | test_azure_client.py:25:37:25:44 | full_url | provenance | Sink:MaD:5 | +| test_http_client.py:1:19:1:25 | After ImportMember | test_http_client.py:1:19:1:25 | request | provenance | | +| test_http_client.py:1:19:1:25 | request | test_http_client.py:9:19:9:25 | request | provenance | | +| test_http_client.py:1:19:1:25 | request | test_http_client.py:10:19:10:25 | request | provenance | | +| test_http_client.py:1:19:1:25 | request | test_http_client.py:11:18:11:24 | request | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:13:27:13:37 | unsafe_host | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:19:27:19:37 | unsafe_host | provenance | | +| test_http_client.py:9:5:9:15 | unsafe_host | test_http_client.py:28:27:28:37 | unsafe_host | provenance | | +| test_http_client.py:9:19:9:25 | request | test_http_client.py:9:5:9:15 | unsafe_host | provenance | AdditionalTaintStep | +| test_http_client.py:9:19:9:25 | request | test_http_client.py:10:5:10:15 | unsafe_path | provenance | AdditionalTaintStep | +| test_http_client.py:9:19:9:25 | request | test_http_client.py:11:5:11:14 | user_input | provenance | AdditionalTaintStep | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:15:25:15:35 | unsafe_path | provenance | | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:21:25:21:35 | unsafe_path | provenance | | +| test_http_client.py:10:5:10:15 | unsafe_path | test_http_client.py:34:25:34:35 | unsafe_path | provenance | | +| test_http_client.py:10:19:10:25 | request | test_http_client.py:10:5:10:15 | unsafe_path | provenance | AdditionalTaintStep | +| test_http_client.py:10:19:10:25 | request | test_http_client.py:11:5:11:14 | user_input | provenance | AdditionalTaintStep | +| test_http_client.py:11:5:11:14 | user_input | test_http_client.py:36:5:36:8 | path | provenance | | +| test_http_client.py:11:5:11:14 | user_input | test_http_client.py:41:5:41:8 | path | provenance | | +| test_http_client.py:11:18:11:24 | request | test_http_client.py:11:5:11:14 | user_input | provenance | AdditionalTaintStep | +| test_http_client.py:36:5:36:8 | path | test_http_client.py:39:25:39:28 | path | provenance | | +| test_http_client.py:41:5:41:8 | path | test_http_client.py:44:25:44:28 | path | provenance | | +| test_path_validation.py:5:19:5:25 | After ImportMember | test_path_validation.py:5:19:5:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:8:18:8:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:9:19:9:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:24:18:24:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:25:19:25:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:40:18:40:24 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:41:19:41:25 | request | provenance | | +| test_path_validation.py:5:19:5:25 | request | test_path_validation.py:57:18:57:24 | request | provenance | | +| test_path_validation.py:8:5:8:14 | user_input | test_path_validation.py:10:5:10:7 | url | provenance | | +| test_path_validation.py:8:18:8:24 | request | test_path_validation.py:8:5:8:14 | user_input | provenance | AdditionalTaintStep | +| test_path_validation.py:8:18:8:24 | request | test_path_validation.py:9:5:9:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:9:5:9:15 | user_input2 | test_path_validation.py:11:5:11:12 | full_url | provenance | | +| test_path_validation.py:9:19:9:25 | request | test_path_validation.py:9:5:9:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:10:5:10:7 | url | test_path_validation.py:14:32:14:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:10:5:10:7 | url | test_path_validation.py:16:32:16:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:11:5:11:12 | full_url | test_path_validation.py:19:32:19:39 | full_url | provenance | Sink:MaD:2 | +| test_path_validation.py:11:5:11:12 | full_url | test_path_validation.py:21:32:21:39 | full_url | provenance | Sink:MaD:2 | +| test_path_validation.py:24:5:24:14 | user_input | test_path_validation.py:26:5:26:7 | url | provenance | | +| test_path_validation.py:24:18:24:24 | request | test_path_validation.py:24:5:24:14 | user_input | provenance | AdditionalTaintStep | +| test_path_validation.py:24:18:24:24 | request | test_path_validation.py:25:5:25:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:25:5:25:15 | user_input2 | test_path_validation.py:27:5:27:12 | full_url | provenance | | +| test_path_validation.py:25:19:25:25 | request | test_path_validation.py:25:5:25:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:26:5:26:7 | url | test_path_validation.py:30:29:30:31 | url | provenance | Sink:MaD:1 | +| test_path_validation.py:26:5:26:7 | url | test_path_validation.py:32:29:32:31 | url | provenance | Sink:MaD:1 | +| test_path_validation.py:27:5:27:12 | full_url | test_path_validation.py:35:29:35:36 | full_url | provenance | Sink:MaD:1 | +| test_path_validation.py:27:5:27:12 | full_url | test_path_validation.py:37:29:37:36 | full_url | provenance | Sink:MaD:1 | +| test_path_validation.py:40:5:40:14 | user_input | test_path_validation.py:42:5:42:7 | url | provenance | | +| test_path_validation.py:40:18:40:24 | request | test_path_validation.py:40:5:40:14 | user_input | provenance | AdditionalTaintStep | +| test_path_validation.py:40:18:40:24 | request | test_path_validation.py:41:5:41:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:41:5:41:15 | user_input2 | test_path_validation.py:43:5:43:12 | full_url | provenance | | +| test_path_validation.py:41:19:41:25 | request | test_path_validation.py:41:5:41:15 | user_input2 | provenance | AdditionalTaintStep | +| test_path_validation.py:42:5:42:7 | url | test_path_validation.py:46:39:46:41 | url | provenance | Sink:MaD:4 | +| test_path_validation.py:42:5:42:7 | url | test_path_validation.py:48:39:48:41 | url | provenance | Sink:MaD:4 | +| test_path_validation.py:43:5:43:12 | full_url | test_path_validation.py:51:39:51:46 | full_url | provenance | Sink:MaD:4 | +| test_path_validation.py:43:5:43:12 | full_url | test_path_validation.py:53:39:53:46 | full_url | provenance | Sink:MaD:4 | +| test_path_validation.py:57:5:57:14 | user_input | test_path_validation.py:61:5:61:7 | url | provenance | | +| test_path_validation.py:57:18:57:24 | request | test_path_validation.py:57:5:57:14 | user_input | provenance | AdditionalTaintStep | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:64:32:64:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:66:32:66:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:69:32:69:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:71:32:71:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:74:32:74:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:76:32:76:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:79:32:79:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:81:32:81:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:85:32:85:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:87:32:87:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:90:32:90:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:92:32:92:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:95:32:95:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:97:32:97:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:100:32:100:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:102:32:102:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:105:32:105:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:107:32:107:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:110:32:110:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:112:32:112:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:115:32:115:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:117:32:117:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:120:32:120:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:122:32:122:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:125:32:125:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:127:32:127:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:130:32:130:34 | url | provenance | Sink:MaD:2 | +| test_path_validation.py:61:5:61:7 | url | test_path_validation.py:132:32:132:34 | url | provenance | Sink:MaD:2 | +| test_requests.py:1:19:1:25 | After ImportMember | test_requests.py:1:19:1:25 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:7:18:7:24 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:14:18:14:24 | request | provenance | | +| test_requests.py:1:19:1:25 | request | test_requests.py:20:18:20:24 | request | provenance | | +| test_requests.py:7:5:7:14 | user_input | test_requests.py:9:18:9:27 | user_input | provenance | | +| test_requests.py:7:18:7:24 | request | test_requests.py:7:5:7:14 | user_input | provenance | AdditionalTaintStep | +| test_requests.py:14:5:14:14 | user_input | test_requests.py:17:17:17:26 | user_input | provenance | | +| test_requests.py:14:18:14:24 | request | test_requests.py:14:5:14:14 | user_input | provenance | AdditionalTaintStep | +| test_requests.py:20:5:20:14 | user_input | test_requests.py:22:34:22:43 | user_input | provenance | | +| test_requests.py:20:18:20:24 | request | test_requests.py:20:5:20:14 | user_input | provenance | AdditionalTaintStep | models | 1 | Sink: azure.keyvault.keys.KeyClient!; Call.Argument[0,vault_url:]; request-forgery | | 2 | Sink: azure.keyvault.secrets.SecretClient!; Call.Argument[0,vault_url:]; request-forgery | @@ -238,185 +238,185 @@ models | 4 | Sink: azure.storage.fileshare.ShareFileClient!; Member[from_file_url].Argument[0,file_url:]; request-forgery | | 5 | Sink: azure; Member[storage].Member[blob].Member[download_blob_from_url].Argument[0,blob_url:]; request-forgery | nodes -| full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| full_partial_test.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:7:18:7:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:8:5:8:13 | ControlFlowNode for query_val | semmle.label | ControlFlowNode for query_val | -| full_partial_test.py:8:17:8:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:11:18:11:27 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:13:5:13:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:15:18:15:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:20:5:20:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:22:18:22:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:25:5:25:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:27:18:27:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:41:5:41:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:41:18:41:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:42:5:42:13 | ControlFlowNode for query_val | semmle.label | ControlFlowNode for query_val | -| full_partial_test.py:42:17:42:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:45:5:45:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:47:18:47:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:49:5:49:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:51:18:51:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:53:5:53:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:55:18:55:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:57:5:57:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:59:18:59:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:61:5:61:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:63:18:63:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:66:5:66:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:66:18:66:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:67:5:67:13 | ControlFlowNode for query_val | semmle.label | ControlFlowNode for query_val | -| full_partial_test.py:67:17:67:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:70:5:70:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:72:18:72:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:74:5:74:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:76:18:76:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:78:5:78:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:80:18:80:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:83:5:83:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:83:18:83:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:84:5:84:13 | ControlFlowNode for query_val | semmle.label | ControlFlowNode for query_val | -| full_partial_test.py:84:17:84:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:87:5:87:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:89:18:89:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:91:5:91:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:93:18:93:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:95:5:95:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:97:18:97:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:101:5:101:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:101:18:101:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:103:5:103:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:105:18:105:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:108:5:108:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:108:18:108:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:110:5:110:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:112:18:112:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:115:5:115:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:115:18:115:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:117:5:117:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:119:18:119:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:122:5:122:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:122:18:122:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:124:5:124:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:126:18:126:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:129:5:129:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:129:18:129:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:134:5:134:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:136:18:136:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:139:5:139:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| full_partial_test.py:139:18:139:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| full_partial_test.py:141:5:141:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| full_partial_test.py:143:18:143:20 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_azure_client.py:6:19:6:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:9:5:9:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_azure_client.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:10:5:10:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_azure_client.py:10:19:10:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_azure_client.py:12:5:12:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:13:5:13:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:15:28:15:30 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:16:28:16:35 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:17:35:17:37 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:18:35:18:42 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:19:15:19:17 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:20:15:20:22 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:21:54:21:56 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:22:54:22:61 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_azure_client.py:24:37:24:39 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_azure_client.py:25:37:25:44 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_http_client.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_http_client.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:9:5:9:15 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:9:19:9:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:10:5:10:15 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:10:19:10:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:11:5:11:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_http_client.py:11:18:11:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_http_client.py:13:27:13:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:15:25:15:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:19:27:19:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:21:25:21:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:28:27:28:37 | ControlFlowNode for unsafe_host | semmle.label | ControlFlowNode for unsafe_host | -| test_http_client.py:34:25:34:35 | ControlFlowNode for unsafe_path | semmle.label | ControlFlowNode for unsafe_path | -| test_http_client.py:36:5:36:8 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | -| test_http_client.py:39:25:39:28 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | -| test_http_client.py:41:5:41:8 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | -| test_http_client.py:44:25:44:28 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_path_validation.py:5:19:5:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:8:5:8:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_path_validation.py:8:18:8:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:9:5:9:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:9:19:9:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:10:5:10:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:11:5:11:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:14:32:14:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:16:32:16:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:19:32:19:39 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:21:32:21:39 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:24:5:24:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_path_validation.py:24:18:24:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:25:5:25:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:25:19:25:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:26:5:26:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:27:5:27:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:30:29:30:31 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:32:29:32:31 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:35:29:35:36 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:37:29:37:36 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:40:5:40:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_path_validation.py:40:18:40:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:41:5:41:15 | ControlFlowNode for user_input2 | semmle.label | ControlFlowNode for user_input2 | -| test_path_validation.py:41:19:41:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:42:5:42:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:43:5:43:12 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:46:39:46:41 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:48:39:48:41 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:51:39:51:46 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:53:39:53:46 | ControlFlowNode for full_url | semmle.label | ControlFlowNode for full_url | -| test_path_validation.py:57:5:57:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_path_validation.py:57:18:57:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_path_validation.py:61:5:61:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:64:32:64:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:66:32:66:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:69:32:69:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:71:32:71:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:74:32:74:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:76:32:76:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:79:32:79:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:81:32:81:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:85:32:85:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:87:32:87:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:90:32:90:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:92:32:92:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:95:32:95:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:97:32:97:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:100:32:100:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:102:32:102:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:105:32:105:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:107:32:107:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:110:32:110:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:112:32:112:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:115:32:115:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:117:32:117:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:120:32:120:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:122:32:122:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:125:32:125:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:127:32:127:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:130:32:130:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_path_validation.py:132:32:132:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | -| test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | -| test_requests.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:7:18:7:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:9:18:9:27 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:14:5:14:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:14:18:14:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:17:17:17:26 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:20:5:20:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | -| test_requests.py:20:18:20:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| test_requests.py:22:34:22:43 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | +| full_partial_test.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| full_partial_test.py:1:19:1:25 | request | semmle.label | request | +| full_partial_test.py:7:5:7:14 | user_input | semmle.label | user_input | +| full_partial_test.py:7:18:7:24 | request | semmle.label | request | +| full_partial_test.py:8:5:8:13 | query_val | semmle.label | query_val | +| full_partial_test.py:8:17:8:23 | request | semmle.label | request | +| full_partial_test.py:11:18:11:27 | user_input | semmle.label | user_input | +| full_partial_test.py:13:5:13:7 | url | semmle.label | url | +| full_partial_test.py:15:18:15:20 | url | semmle.label | url | +| full_partial_test.py:20:5:20:7 | url | semmle.label | url | +| full_partial_test.py:22:18:22:20 | url | semmle.label | url | +| full_partial_test.py:25:5:25:7 | url | semmle.label | url | +| full_partial_test.py:27:18:27:20 | url | semmle.label | url | +| full_partial_test.py:41:5:41:14 | user_input | semmle.label | user_input | +| full_partial_test.py:41:18:41:24 | request | semmle.label | request | +| full_partial_test.py:42:5:42:13 | query_val | semmle.label | query_val | +| full_partial_test.py:42:17:42:23 | request | semmle.label | request | +| full_partial_test.py:45:5:45:7 | url | semmle.label | url | +| full_partial_test.py:47:18:47:20 | url | semmle.label | url | +| full_partial_test.py:49:5:49:7 | url | semmle.label | url | +| full_partial_test.py:51:18:51:20 | url | semmle.label | url | +| full_partial_test.py:53:5:53:7 | url | semmle.label | url | +| full_partial_test.py:55:18:55:20 | url | semmle.label | url | +| full_partial_test.py:57:5:57:7 | url | semmle.label | url | +| full_partial_test.py:59:18:59:20 | url | semmle.label | url | +| full_partial_test.py:61:5:61:7 | url | semmle.label | url | +| full_partial_test.py:63:18:63:20 | url | semmle.label | url | +| full_partial_test.py:66:5:66:14 | user_input | semmle.label | user_input | +| full_partial_test.py:66:18:66:24 | request | semmle.label | request | +| full_partial_test.py:67:5:67:13 | query_val | semmle.label | query_val | +| full_partial_test.py:67:17:67:23 | request | semmle.label | request | +| full_partial_test.py:70:5:70:7 | url | semmle.label | url | +| full_partial_test.py:72:18:72:20 | url | semmle.label | url | +| full_partial_test.py:74:5:74:7 | url | semmle.label | url | +| full_partial_test.py:76:18:76:20 | url | semmle.label | url | +| full_partial_test.py:78:5:78:7 | url | semmle.label | url | +| full_partial_test.py:80:18:80:20 | url | semmle.label | url | +| full_partial_test.py:83:5:83:14 | user_input | semmle.label | user_input | +| full_partial_test.py:83:18:83:24 | request | semmle.label | request | +| full_partial_test.py:84:5:84:13 | query_val | semmle.label | query_val | +| full_partial_test.py:84:17:84:23 | request | semmle.label | request | +| full_partial_test.py:87:5:87:7 | url | semmle.label | url | +| full_partial_test.py:89:18:89:20 | url | semmle.label | url | +| full_partial_test.py:91:5:91:7 | url | semmle.label | url | +| full_partial_test.py:93:18:93:20 | url | semmle.label | url | +| full_partial_test.py:95:5:95:7 | url | semmle.label | url | +| full_partial_test.py:97:18:97:20 | url | semmle.label | url | +| full_partial_test.py:101:5:101:14 | user_input | semmle.label | user_input | +| full_partial_test.py:101:18:101:24 | request | semmle.label | request | +| full_partial_test.py:103:5:103:7 | url | semmle.label | url | +| full_partial_test.py:105:18:105:20 | url | semmle.label | url | +| full_partial_test.py:108:5:108:14 | user_input | semmle.label | user_input | +| full_partial_test.py:108:18:108:24 | request | semmle.label | request | +| full_partial_test.py:110:5:110:7 | url | semmle.label | url | +| full_partial_test.py:112:18:112:20 | url | semmle.label | url | +| full_partial_test.py:115:5:115:14 | user_input | semmle.label | user_input | +| full_partial_test.py:115:18:115:24 | request | semmle.label | request | +| full_partial_test.py:117:5:117:7 | url | semmle.label | url | +| full_partial_test.py:119:18:119:20 | url | semmle.label | url | +| full_partial_test.py:122:5:122:14 | user_input | semmle.label | user_input | +| full_partial_test.py:122:18:122:24 | request | semmle.label | request | +| full_partial_test.py:124:5:124:7 | url | semmle.label | url | +| full_partial_test.py:126:18:126:20 | url | semmle.label | url | +| full_partial_test.py:129:5:129:14 | user_input | semmle.label | user_input | +| full_partial_test.py:129:18:129:24 | request | semmle.label | request | +| full_partial_test.py:134:5:134:7 | url | semmle.label | url | +| full_partial_test.py:136:18:136:20 | url | semmle.label | url | +| full_partial_test.py:139:5:139:14 | user_input | semmle.label | user_input | +| full_partial_test.py:139:18:139:24 | request | semmle.label | request | +| full_partial_test.py:141:5:141:7 | url | semmle.label | url | +| full_partial_test.py:143:18:143:20 | url | semmle.label | url | +| test_azure_client.py:6:19:6:25 | After ImportMember | semmle.label | After ImportMember | +| test_azure_client.py:6:19:6:25 | request | semmle.label | request | +| test_azure_client.py:9:5:9:14 | user_input | semmle.label | user_input | +| test_azure_client.py:9:18:9:24 | request | semmle.label | request | +| test_azure_client.py:10:5:10:15 | user_input2 | semmle.label | user_input2 | +| test_azure_client.py:10:19:10:25 | request | semmle.label | request | +| test_azure_client.py:12:5:12:7 | url | semmle.label | url | +| test_azure_client.py:13:5:13:12 | full_url | semmle.label | full_url | +| test_azure_client.py:15:28:15:30 | url | semmle.label | url | +| test_azure_client.py:16:28:16:35 | full_url | semmle.label | full_url | +| test_azure_client.py:17:35:17:37 | url | semmle.label | url | +| test_azure_client.py:18:35:18:42 | full_url | semmle.label | full_url | +| test_azure_client.py:19:15:19:17 | url | semmle.label | url | +| test_azure_client.py:20:15:20:22 | full_url | semmle.label | full_url | +| test_azure_client.py:21:54:21:56 | url | semmle.label | url | +| test_azure_client.py:22:54:22:61 | full_url | semmle.label | full_url | +| test_azure_client.py:24:37:24:39 | url | semmle.label | url | +| test_azure_client.py:25:37:25:44 | full_url | semmle.label | full_url | +| test_http_client.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| test_http_client.py:1:19:1:25 | request | semmle.label | request | +| test_http_client.py:9:5:9:15 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:9:19:9:25 | request | semmle.label | request | +| test_http_client.py:10:5:10:15 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:10:19:10:25 | request | semmle.label | request | +| test_http_client.py:11:5:11:14 | user_input | semmle.label | user_input | +| test_http_client.py:11:18:11:24 | request | semmle.label | request | +| test_http_client.py:13:27:13:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:15:25:15:35 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:19:27:19:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:21:25:21:35 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:28:27:28:37 | unsafe_host | semmle.label | unsafe_host | +| test_http_client.py:34:25:34:35 | unsafe_path | semmle.label | unsafe_path | +| test_http_client.py:36:5:36:8 | path | semmle.label | path | +| test_http_client.py:39:25:39:28 | path | semmle.label | path | +| test_http_client.py:41:5:41:8 | path | semmle.label | path | +| test_http_client.py:44:25:44:28 | path | semmle.label | path | +| test_path_validation.py:5:19:5:25 | After ImportMember | semmle.label | After ImportMember | +| test_path_validation.py:5:19:5:25 | request | semmle.label | request | +| test_path_validation.py:8:5:8:14 | user_input | semmle.label | user_input | +| test_path_validation.py:8:18:8:24 | request | semmle.label | request | +| test_path_validation.py:9:5:9:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:9:19:9:25 | request | semmle.label | request | +| test_path_validation.py:10:5:10:7 | url | semmle.label | url | +| test_path_validation.py:11:5:11:12 | full_url | semmle.label | full_url | +| test_path_validation.py:14:32:14:34 | url | semmle.label | url | +| test_path_validation.py:16:32:16:34 | url | semmle.label | url | +| test_path_validation.py:19:32:19:39 | full_url | semmle.label | full_url | +| test_path_validation.py:21:32:21:39 | full_url | semmle.label | full_url | +| test_path_validation.py:24:5:24:14 | user_input | semmle.label | user_input | +| test_path_validation.py:24:18:24:24 | request | semmle.label | request | +| test_path_validation.py:25:5:25:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:25:19:25:25 | request | semmle.label | request | +| test_path_validation.py:26:5:26:7 | url | semmle.label | url | +| test_path_validation.py:27:5:27:12 | full_url | semmle.label | full_url | +| test_path_validation.py:30:29:30:31 | url | semmle.label | url | +| test_path_validation.py:32:29:32:31 | url | semmle.label | url | +| test_path_validation.py:35:29:35:36 | full_url | semmle.label | full_url | +| test_path_validation.py:37:29:37:36 | full_url | semmle.label | full_url | +| test_path_validation.py:40:5:40:14 | user_input | semmle.label | user_input | +| test_path_validation.py:40:18:40:24 | request | semmle.label | request | +| test_path_validation.py:41:5:41:15 | user_input2 | semmle.label | user_input2 | +| test_path_validation.py:41:19:41:25 | request | semmle.label | request | +| test_path_validation.py:42:5:42:7 | url | semmle.label | url | +| test_path_validation.py:43:5:43:12 | full_url | semmle.label | full_url | +| test_path_validation.py:46:39:46:41 | url | semmle.label | url | +| test_path_validation.py:48:39:48:41 | url | semmle.label | url | +| test_path_validation.py:51:39:51:46 | full_url | semmle.label | full_url | +| test_path_validation.py:53:39:53:46 | full_url | semmle.label | full_url | +| test_path_validation.py:57:5:57:14 | user_input | semmle.label | user_input | +| test_path_validation.py:57:18:57:24 | request | semmle.label | request | +| test_path_validation.py:61:5:61:7 | url | semmle.label | url | +| test_path_validation.py:64:32:64:34 | url | semmle.label | url | +| test_path_validation.py:66:32:66:34 | url | semmle.label | url | +| test_path_validation.py:69:32:69:34 | url | semmle.label | url | +| test_path_validation.py:71:32:71:34 | url | semmle.label | url | +| test_path_validation.py:74:32:74:34 | url | semmle.label | url | +| test_path_validation.py:76:32:76:34 | url | semmle.label | url | +| test_path_validation.py:79:32:79:34 | url | semmle.label | url | +| test_path_validation.py:81:32:81:34 | url | semmle.label | url | +| test_path_validation.py:85:32:85:34 | url | semmle.label | url | +| test_path_validation.py:87:32:87:34 | url | semmle.label | url | +| test_path_validation.py:90:32:90:34 | url | semmle.label | url | +| test_path_validation.py:92:32:92:34 | url | semmle.label | url | +| test_path_validation.py:95:32:95:34 | url | semmle.label | url | +| test_path_validation.py:97:32:97:34 | url | semmle.label | url | +| test_path_validation.py:100:32:100:34 | url | semmle.label | url | +| test_path_validation.py:102:32:102:34 | url | semmle.label | url | +| test_path_validation.py:105:32:105:34 | url | semmle.label | url | +| test_path_validation.py:107:32:107:34 | url | semmle.label | url | +| test_path_validation.py:110:32:110:34 | url | semmle.label | url | +| test_path_validation.py:112:32:112:34 | url | semmle.label | url | +| test_path_validation.py:115:32:115:34 | url | semmle.label | url | +| test_path_validation.py:117:32:117:34 | url | semmle.label | url | +| test_path_validation.py:120:32:120:34 | url | semmle.label | url | +| test_path_validation.py:122:32:122:34 | url | semmle.label | url | +| test_path_validation.py:125:32:125:34 | url | semmle.label | url | +| test_path_validation.py:127:32:127:34 | url | semmle.label | url | +| test_path_validation.py:130:32:130:34 | url | semmle.label | url | +| test_path_validation.py:132:32:132:34 | url | semmle.label | url | +| test_requests.py:1:19:1:25 | After ImportMember | semmle.label | After ImportMember | +| test_requests.py:1:19:1:25 | request | semmle.label | request | +| test_requests.py:7:5:7:14 | user_input | semmle.label | user_input | +| test_requests.py:7:18:7:24 | request | semmle.label | request | +| test_requests.py:9:18:9:27 | user_input | semmle.label | user_input | +| test_requests.py:14:5:14:14 | user_input | semmle.label | user_input | +| test_requests.py:14:18:14:24 | request | semmle.label | request | +| test_requests.py:17:17:17:26 | user_input | semmle.label | user_input | +| test_requests.py:20:5:20:14 | user_input | semmle.label | user_input | +| test_requests.py:20:18:20:24 | request | semmle.label | request | +| test_requests.py:22:34:22:43 | user_input | semmle.label | user_input | subpaths From cd703dcf0c67c1a1dc27edabecc4d43309f62ea4 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 17:09:09 +0000 Subject: [PATCH 71/72] Python: omit PEP 695 type-param names from FunctionDefExpr/ClassDefExpr children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PEP 695 type-param names (e.g. `T` in `def func[T]:` or `class Box[T]:`) bind in an annotation scope that nests the function/class body, so their AST scope is the inner function/class — not the enclosing scope where the FunctionDefExpr/ClassDefExpr CFG node lives. Visiting them as children created scope-crossing CFG edges (nonLocalStep violations: 96 across CPython). Drop them from the children list; the legacy CFG omitted them too. TypeAliasStmt is unaffected (its type-params share scope with the alias's enclosing scope). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 49 ++++++------------- .../ControlFlow/bindings/type_params.py | 9 ++-- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 9c6e9322c283..72f3a2e58fd7 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -1423,49 +1423,34 @@ module Ast implements AstSig { override AstNode getChild(int index) { index = 0 and result = this.getValue() } } - /** A class definition expression (has base classes evaluated at definition time). */ + /** + * A class definition expression (visits bases, but NOT PEP 695 type + * parameters — those bind in an annotation scope that nests the class + * body, so they belong to the inner scope's CFG, not the enclosing + * scope's; the legacy CFG also omitted them). + */ additional class ClassDefExpr extends Expr { private Py::ClassExpr classExpr; ClassDefExpr() { this = TPyExpr(classExpr) } - /** - * Gets the `n`th PEP 695 type-parameter name (a `Name` in store - * context), in declaration order. These bind in the enclosing scope - * at class-definition time, so the CFG must visit them. - */ - Expr getTypeParamName(int n) { - result.asExpr() = typeParameterName(classExpr.getTypeParameter(n)) - } - - int getNumberOfTypeParams() { result = count(classExpr.getATypeParameter()) } - Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) } - override AstNode getChild(int index) { - result = this.getTypeParamName(index) - or - result = this.getBase(index - this.getNumberOfTypeParams()) - } + override AstNode getChild(int index) { result = this.getBase(index) } } - /** A function definition expression (has default args evaluated at definition time). */ + /** + * A function definition expression (visits positional and keyword + * defaults, but NOT PEP 695 type parameters — those bind in an + * annotation scope that nests the function body, so they belong to + * the inner scope's CFG, not the enclosing scope's; the legacy CFG + * also omitted them). + */ additional class FunctionDefExpr extends Expr { private Py::FunctionExpr funcExpr; FunctionDefExpr() { this = TPyExpr(funcExpr) } - /** - * Gets the `n`th PEP 695 type-parameter name (a `Name` in store - * context), in declaration order. These bind in the enclosing scope - * at function-definition time, so the CFG must visit them. - */ - Expr getTypeParamName(int n) { - result.asExpr() = typeParameterName(funcExpr.getInnerScope().getTypeParameter(n)) - } - - int getNumberOfTypeParams() { result = count(funcExpr.getInnerScope().getATypeParameter()) } - /** * Gets the `n`th default for a positional argument, in evaluation * order. Note that `Args.getDefault(int)` is indexed by argument @@ -1486,11 +1471,9 @@ module Ast implements AstSig { int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } override AstNode getChild(int index) { - result = this.getTypeParamName(index) - or - result = this.getDefault(index - this.getNumberOfTypeParams()) + result = this.getDefault(index) or - result = this.getKwDefault(index - this.getNumberOfTypeParams() - this.getNumberOfDefaults()) + result = this.getKwDefault(index - this.getNumberOfDefaults()) } } diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py index 3e5aaf9d042a..2bd34dc3f0ee 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -1,15 +1,18 @@ # PEP 695 type parameters (Python 3.12+). -def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x cfgdefines=T +# PEP 695 type-param names on `def`/`class` bind in an annotation scope +# that nests the function/class body — they have no CFG node in the +# enclosing scope (matching the legacy CFG). +def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x return x -class Box[T]: # $ cfgdefines=Box cfgdefines=T +class Box[T]: # $ cfgdefines=Box item: T # $ cfgdefines=item # Multi-parameter, with bound and variadics. -def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs cfgdefines=T cfgdefines=Ts cfgdefines=P +def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs return x From 0a9946121b273c46174270d73f7bf12e6b8c021c Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 26 May 2026 21:35:39 +0000 Subject: [PATCH 72/72] Python: migrate src queries to new shared CFG types + reformat Migrate 27 queries under python/ql/src/ from legacy CFG types (CallNode/AttrNode/NameNode/etc.) to the shared-CFG-based 'Cfg::' namespace, matching the dataflow API surface introduced earlier on this branch. ModificationOfParameterWithDefaultCustomizations.qll is rewritten on top of BarrierGuard, removing the last legacy ESSA dependency in that file. UnguardedNextInGenerator.ql still uses ESSA and bridges to the new CFG via Cfg::CallNode.getNode(). Also reformat 14 library and query files that had drifted from the formatter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/ql/lib/semmle/python/Exprs.qll | 10 +-- python/ql/lib/semmle/python/Flow.qll | 8 ++- python/ql/lib/semmle/python/Import.qll | 1 - .../new/internal/DataFlowDispatch.qll | 18 +++-- .../dataflow/new/internal/DataFlowPublic.qll | 24 +++++-- .../new/internal/IterableUnpacking.qll | 3 +- .../new/internal/TypeTrackingImpl.qll | 7 +- .../python/dataflow/old/Implementation.qll | 6 +- .../Exceptions/UnguardedNextInGenerator.ql | 16 +++-- python/ql/src/Expressions/CallArgs.qll | 4 +- .../DuplicateKeyInDictionaryLiteral.ql | 4 +- .../Formatting/AdvancedFormatting.qll | 4 +- python/ql/src/Expressions/UseofApply.ql | 3 +- .../Functions/SignatureOverriddenMethod.ql | 3 +- .../Resources/FileNotAlwaysClosedQuery.qll | 9 ++- .../CWE-020-ExternalAPIs/ExternalAPIs.qll | 3 +- .../Security/CWE-079/Jinja2WithoutEscaping.ql | 5 +- python/ql/src/Security/CWE-327/PyOpenSSL.qll | 5 +- python/ql/src/Security/CWE-327/Ssl.qll | 5 +- .../Security/CWE-798/HardcodedCredentials.ql | 7 +- .../ql/src/Statements/ModificationOfLocals.ql | 13 ++-- .../ql/src/Statements/SideEffectInAssert.ql | 3 +- python/ql/src/Statements/UseOfExit.ql | 3 +- .../src/Variables/LeakingListComprehension.ql | 3 +- .../Security/CWE-022bis/TarSlipImprov.ql | 5 +- .../Security/CWE-340/TokenBuiltFromUUID.ql | 5 +- .../Security/CWE-346/CorsBypass.ql | 18 ++--- .../Security/CWE-770/UnicodeDoS.ql | 5 +- .../Security/UnsafeUnpackQuery.qll | 5 +- .../security/injection/CsvInjection.qll | 3 +- .../analysis-quality/CallGraphQuality.qll | 5 +- .../src/meta/analysis-quality/TTCallGraph.ql | 3 +- .../analysis-quality/TTCallGraphMissing.ql | 3 +- .../meta/analysis-quality/TTCallGraphNew.ql | 3 +- .../TTCallGraphNewAmbiguous.ql | 3 +- .../analysis-quality/TTCallGraphOverview.ql | 7 +- .../analysis-quality/TTCallGraphShared.ql | 3 +- ...onOfParameterWithDefaultCustomizations.qll | 66 +++++++------------ .../dataflow/regression/custom_dataflow.ql | 4 +- .../typetracking_imports/highlight_problem.ql | 5 +- 40 files changed, 172 insertions(+), 138 deletions(-) diff --git a/python/ql/lib/semmle/python/Exprs.qll b/python/ql/lib/semmle/python/Exprs.qll index 6f462f714eb6..9ce6f9e6680a 100644 --- a/python/ql/lib/semmle/python/Exprs.qll +++ b/python/ql/lib/semmle/python/Exprs.qll @@ -28,7 +28,9 @@ class Expr extends Expr_, AstNode { /** Whether this expression may have a side effect (as determined purely from its syntax) */ predicate hasSideEffects() { /* If an exception raised by this expression handled, count that as a side effect */ - exists(ControlFlowNode n | n.getNode() = this | n.getASuccessor().getNode() instanceof ExceptStmt) + exists(ControlFlowNode n | n.getNode() = this | + n.getASuccessor().getNode() instanceof ExceptStmt + ) or this.getASubExpression().hasSideEffects() } @@ -94,7 +96,6 @@ class Subscript extends Subscript_ { } Expr getObject() { result = Subscript_.super.getValue() } - } /** A call expression, such as `func(...)` */ @@ -110,7 +111,6 @@ class Call extends Call_ { override string toString() { result = this.getFunc().toString() + "()" } - /** Gets a tuple (*) argument of this call. */ Expr getStarargs() { result = this.getAPositionalArg().(Starred).getValue() } @@ -196,7 +196,6 @@ class IfExp extends IfExp_ { override Expr getASubExpression() { result = this.getTest() or result = this.getBody() or result = this.getOrelse() } - } /** A starred expression, such as the `*rest` in the assignment `first, *rest = seq` */ @@ -405,7 +404,6 @@ class PlaceHolder extends PlaceHolder_ { override Expr getASubExpression() { none() } override string toString() { result = "$" + this.getId() } - } /** A tuple expression such as `( 1, 3, 5, 7, 9 )` */ @@ -472,7 +470,6 @@ class Name extends Name_ { override string toString() { result = this.getId() } - override predicate isArtificial() { /* Artificial variable names in comprehensions all start with "." */ this.getId().charAt(0) = "." @@ -578,7 +575,6 @@ abstract class NameConstant extends Name, ImmutableLiteral { override predicate isConstant() { any() } - override predicate isArtificial() { none() } } diff --git a/python/ql/lib/semmle/python/Flow.qll b/python/ql/lib/semmle/python/Flow.qll index a48fcf7c3e26..0f84f3f367ad 100644 --- a/python/ql/lib/semmle/python/Flow.qll +++ b/python/ql/lib/semmle/python/Flow.qll @@ -726,7 +726,9 @@ private Py::AstNode assigned_value(Py::Expr lhs) { exists(Py::Alias a | a.getAsname() = lhs and result = a.getValue()) or /* lhs += x => result = (lhs + x) */ - exists(Py::AugAssign a, Py::BinaryExpr b | b = a.getOperation() and result = b and lhs = b.getLeft()) + exists(Py::AugAssign a, Py::BinaryExpr b | + b = a.getOperation() and result = b and lhs = b.getLeft() + ) or /* * ..., lhs, ... = ..., result, ... @@ -868,7 +870,9 @@ class NameNode extends ControlFlowNode { /** Whether this is a use of a global (including builtin) variable. */ predicate isGlobal() { Scopes::use_of_global_variable(this, _, _) } - predicate isSelf() { exists(Py::SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this) } + predicate isSelf() { + exists(Py::SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this) + } } /** A control flow node corresponding to a named constant, one of `None`, `True` or `False`. */ diff --git a/python/ql/lib/semmle/python/Import.qll b/python/ql/lib/semmle/python/Import.qll index 5256403c8b90..d4f5109ed47c 100644 --- a/python/ql/lib/semmle/python/Import.qll +++ b/python/ql/lib/semmle/python/Import.qll @@ -162,7 +162,6 @@ class ImportMember extends ImportMember_ { string getImportedModuleName() { result = this.getModule().(ImportExpr).getImportedModuleName() + "." + this.getName() } - } /** An import statement */ diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll index ba6d134f83bb..06cada5a6690 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll @@ -287,9 +287,7 @@ predicate isClassmethod(Function func) { /** Holds if the function `func` has a `property` decorator. */ overlay[local] -predicate hasPropertyDecorator(Function func) { - func.getADecorator().(Name).getId() = "property" -} +predicate hasPropertyDecorator(Function func) { func.getADecorator().(Name).getId() = "property" } /** * Holds if the function `func` has a `contextlib.contextmanager`. @@ -297,9 +295,11 @@ predicate hasPropertyDecorator(Function func) { overlay[local] predicate hasContextmanagerDecorator(Function func) { exists(Cfg::ControlFlowNode contextmanager | - contextmanager.(Cfg::NameNode).getId() = "contextmanager" and contextmanager.(Cfg::NameNode).isGlobal() + contextmanager.(Cfg::NameNode).getId() = "contextmanager" and + contextmanager.(Cfg::NameNode).isGlobal() or - contextmanager.(Cfg::AttrNode).getObject("contextmanager").(Cfg::NameNode).getId() = "contextlib" + contextmanager.(Cfg::AttrNode).getObject("contextmanager").(Cfg::NameNode).getId() = + "contextlib" | func.getADecorator() = contextmanager.getNode() ) @@ -1348,7 +1348,9 @@ predicate normalCallArg(Cfg::CallNode call, Node arg, ArgumentPosition apos) { * translated into `l.clear()`, and we can still have use-use flow. */ cached -predicate getCallArg(Cfg::CallNode call, Function target, CallType type, Node arg, ArgumentPosition apos) { +predicate getCallArg( + Cfg::CallNode call, Function target, CallType type, Node arg, ArgumentPosition apos +) { Stages::DataFlow::ref() and resolveCall(call, target, type) and ( @@ -1441,7 +1443,9 @@ private predicate sameEnclosingCallable(Node node1, Node node2) { // DataFlowCall // ============================================================================= newtype TDataFlowCall = - TNormalCall(Cfg::CallNode call, Function target, CallType type) { resolveCall(call, target, type) } or + TNormalCall(Cfg::CallNode call, Function target, CallType type) { + resolveCall(call, target, type) + } or /** A call to the generated function inside a comprehension */ TComprehensionCall(Comp c) or TPotentialLibraryCall(Cfg::CallNode call) or diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll index 8ed76588004a..0a59b66f07fe 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll @@ -37,7 +37,9 @@ newtype TNode = * A node corresponding to a scope entry definition. That is, the value of a variable * as it enters a scope. */ - TScopeEntryDefinitionNode(SsaImpl::ScopeEntryDefinition def) { not def.getScope() instanceof Module } or + TScopeEntryDefinitionNode(SsaImpl::ScopeEntryDefinition def) { + not def.getScope() instanceof Module + } or /** * A synthetic node representing the value of an object before a state change. * @@ -656,11 +658,15 @@ private predicate outcomeOfGuard( ) or // Recursive: comparisons against a boolean literal. - exists(Cfg::CompareNode cmpNode, Cmpop op, Cfg::ControlFlowNode otherOperand, - Cfg::ControlFlowNode guardOperand, boolean polarity, boolean cmpBranch + exists( + Cfg::CompareNode cmpNode, Cmpop op, Cfg::ControlFlowNode otherOperand, + Cfg::ControlFlowNode guardOperand, boolean polarity, boolean cmpBranch | guardOperand.getNode() = guard.getNode() and - (cmpNode.operands(guardOperand, op, otherOperand) or cmpNode.operands(otherOperand, op, guardOperand)) and + ( + cmpNode.operands(guardOperand, op, otherOperand) or + cmpNode.operands(otherOperand, op, guardOperand) + ) and not guard.getNode() instanceof BooleanLiteral and ( (op instanceof Eq or op instanceof Is) and @@ -692,7 +698,9 @@ module BarrierGuard { result = ParameterizedBarrierGuard::getABarrierNode(_) } - private predicate extendedGuardChecks(GuardNode g, Cfg::ControlFlowNode node, boolean branch, Unit u) { + private predicate extendedGuardChecks( + GuardNode g, Cfg::ControlFlowNode node, boolean branch, Unit u + ) { guardChecks(g, node, branch) and u = u } @@ -790,7 +798,11 @@ newtype TContent = or // d["key"] = ... key = - any(Cfg::SubscriptNode sub | sub.isStore() | sub.getIndex().getNode().(StringLiteral).getText()) + any(Cfg::SubscriptNode sub | + sub.isStore() + | + sub.getIndex().getNode().(StringLiteral).getText() + ) or // d.setdefault("key", ...) exists(Cfg::CallNode call | call.getFunction().(Cfg::AttrNode).getName() = "setdefault" | diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll b/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll index 0704763bd890..ac7200115ce2 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/IterableUnpacking.qll @@ -233,7 +233,8 @@ class UnpackingAssignmentTarget extends Cfg::ControlFlowNode { } /** A (possibly recursive) target of an unpacking assignment which is also a sequence. */ -class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof Cfg::SequenceNode { +class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof Cfg::SequenceNode +{ Cfg::ControlFlowNode getElement(int i) { result = super.getElement(i) } Cfg::ControlFlowNode getAnElement() { result = this.getElement(_) } diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/TypeTrackingImpl.qll b/python/ql/lib/semmle/python/dataflow/new/internal/TypeTrackingImpl.qll index 195d0c4da05f..7fa1d4e573aa 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/TypeTrackingImpl.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/TypeTrackingImpl.qll @@ -97,8 +97,7 @@ private module SummaryTypeTrackerInput implements SummaryTypeTracker::Input { return = FlowSummaryImpl::Private::SummaryComponent::return() and // `result` should be the return value of a callable expression (lambda or function) referenced by `callable` exists(Return ret | - ret.getScope() = - callable.getALocalSource().asExpr().(CallableExpr).getInnerScope() and + ret.getScope() = callable.getALocalSource().asExpr().(CallableExpr).getInnerScope() and result.asCfgNode().getNode() = ret.getValue() ) } @@ -311,7 +310,9 @@ module TypeTrackingInput implements Shared::TypeTrackingInput { // // nodeFrom is `expr` // nodeTo is entry node for `f` - exists(SsaImpl::ScopeEntryDefinition e, SsaImpl::SsaSourceVariable var, Cfg::DefinitionNode def | + exists( + SsaImpl::ScopeEntryDefinition e, SsaImpl::SsaSourceVariable var, Cfg::DefinitionNode def + | e.getSourceVariable() = var and def.getNode() = var.getVariable().getAStore() | diff --git a/python/ql/lib/semmle/python/dataflow/old/Implementation.qll b/python/ql/lib/semmle/python/dataflow/old/Implementation.qll index 00f446563726..6e1314ff9e9a 100644 --- a/python/ql/lib/semmle/python/dataflow/old/Implementation.qll +++ b/python/ql/lib/semmle/python/dataflow/old/Implementation.qll @@ -448,8 +448,7 @@ class TaintTrackingImplementation extends string instanceof TaintTracking::Confi context = TNoParam() and src = TTaintTrackingNode_(retval, TNoParam(), path, kind, this) and node.asCfgNode() = call and - retval.asCfgNode().getNode() = - any(Return ret | ret.getScope() = pyfunc.getScope()).getValue() + retval.asCfgNode().getNode() = any(Return ret | ret.getScope() = pyfunc.getScope()).getValue() ) and edgeLabel = "return" } @@ -471,8 +470,7 @@ class TaintTrackingImplementation extends string instanceof TaintTracking::Confi this.callContexts(call, src, pyfunc, context, callee) and retnode = TTaintTrackingNode_(retval, callee, path, kind, this) and node.asCfgNode() = call and - retval.asCfgNode().getNode() = - any(Return ret | ret.getScope() = pyfunc.getScope()).getValue() + retval.asCfgNode().getNode() = any(Return ret | ret.getScope() = pyfunc.getScope()).getValue() ) and edgeLabel = "call" } diff --git a/python/ql/src/Exceptions/UnguardedNextInGenerator.ql b/python/ql/src/Exceptions/UnguardedNextInGenerator.ql index a6969218fddc..437a3f208df7 100644 --- a/python/ql/src/Exceptions/UnguardedNextInGenerator.ql +++ b/python/ql/src/Exceptions/UnguardedNextInGenerator.ql @@ -12,6 +12,8 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg +private import semmle.python.Flow as Flow API::Node iter() { result = API::builtin("iter") } @@ -19,17 +21,17 @@ API::Node next() { result = API::builtin("next") } API::Node stopIteration() { result = API::builtin("StopIteration") } -predicate call_to_iter(CallNode call, EssaVariable sequence) { - call = iter().getACall().asCfgNode() and +predicate call_to_iter(Flow::CallNode call, EssaVariable sequence) { + call.getNode() = iter().getACall().asCfgNode().(Cfg::CallNode).getNode() and call.getArg(0) = sequence.getAUse() } -predicate call_to_next(CallNode call, ControlFlowNode iter) { - call = next().getACall().asCfgNode() and +predicate call_to_next(Flow::CallNode call, Flow::ControlFlowNode iter) { + call.getNode() = next().getACall().asCfgNode().(Cfg::CallNode).getNode() and call.getArg(0) = iter } -predicate call_to_next_has_default(CallNode call) { +predicate call_to_next_has_default(Flow::CallNode call) { exists(call.getArg(1)) or exists(call.getArgByName("default")) } @@ -49,14 +51,14 @@ predicate iter_not_exhausted(EssaVariable iterator) { ) } -predicate stop_iteration_handled(CallNode call) { +predicate stop_iteration_handled(Flow::CallNode call) { exists(Try t | t.containsInScope(call.getNode()) and t.getAHandler().getType() = stopIteration().getAValueReachableFromSource().asExpr() ) } -from CallNode call +from Flow::CallNode call where call_to_next(call, _) and not call_to_next_has_default(call) and diff --git a/python/ql/src/Expressions/CallArgs.qll b/python/ql/src/Expressions/CallArgs.qll index 1c354ad9f941..43782a903dcb 100644 --- a/python/ql/src/Expressions/CallArgs.qll +++ b/python/ql/src/Expressions/CallArgs.qll @@ -116,9 +116,7 @@ FunctionValue get_function_or_initializer(Value func_or_cls) { predicate illegally_named_parameter_objectapi(Call call, Object func, string name) { not func.isC() and name = call.getANamedArgumentName() and - exists(ControlFlowNode callCfg | callCfg.getNode() = call | - callCfg = get_a_call_objectapi(func) - ) and + exists(ControlFlowNode callCfg | callCfg.getNode() = call | callCfg = get_a_call_objectapi(func)) and not get_function_or_initializer_objectapi(func).isLegalArgumentName(name) } diff --git a/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql b/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql index bc9fb968dbb0..0c55a2ece587 100644 --- a/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql +++ b/python/ql/src/Expressions/DuplicateKeyInDictionaryLiteral.ql @@ -41,7 +41,9 @@ where i1 < i2 ) or - exists(ControlFlowNode k1Cfg, ControlFlowNode k2Cfg | k1Cfg.getNode() = k1 and k2Cfg.getNode() = k2 | + exists(ControlFlowNode k1Cfg, ControlFlowNode k2Cfg | + k1Cfg.getNode() = k1 and k2Cfg.getNode() = k2 + | k1Cfg.getBasicBlock().strictlyDominates(k2Cfg.getBasicBlock()) ) ) diff --git a/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll b/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll index a860f96061f4..a5e0379685a3 100644 --- a/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll +++ b/python/ql/src/Expressions/Formatting/AdvancedFormatting.qll @@ -98,7 +98,9 @@ private predicate brace_pair(PossibleAdvancedFormatString fmt, int start, int en } private predicate advanced_format_call(Call format_expr, PossibleAdvancedFormatString fmt, int args) { - exists(CallNode call, ControlFlowNode fmtCfg | call.getNode() = format_expr and fmtCfg.getNode() = fmt | + exists(CallNode call, ControlFlowNode fmtCfg | + call.getNode() = format_expr and fmtCfg.getNode() = fmt + | call.getFunction().(ControlFlowNodeWithPointsTo).pointsTo(Value::named("format")) and call.getArg(0).(ControlFlowNodeWithPointsTo).pointsTo(_, fmtCfg) and args = count(format_expr.getAnArg()) - 1 diff --git a/python/ql/src/Expressions/UseofApply.ql b/python/ql/src/Expressions/UseofApply.ql index f1068eca837c..6fa5c9817224 100644 --- a/python/ql/src/Expressions/UseofApply.ql +++ b/python/ql/src/Expressions/UseofApply.ql @@ -11,8 +11,9 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call +from Cfg::CallNode call where major_version() = 2 and call = API::builtin("apply").getACall().asCfgNode() diff --git a/python/ql/src/Functions/SignatureOverriddenMethod.ql b/python/ql/src/Functions/SignatureOverriddenMethod.ql index 15b3fa706401..7ecced29c60f 100644 --- a/python/ql/src/Functions/SignatureOverriddenMethod.ql +++ b/python/ql/src/Functions/SignatureOverriddenMethod.ql @@ -15,6 +15,7 @@ import python import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.internal.DataFlowDispatch +private import semmle.python.controlflow.internal.Cfg as Cfg import codeql.util.Option /** Holds if `base` is overridden by `sub` */ @@ -143,7 +144,7 @@ predicate ignore(Function f) { /** Gets a function that `call` may resolve to. */ Function resolveCall(Call call) { - exists(DataFlowCall dfc | call = dfc.getNode().(CallNode).getNode() | + exists(DataFlowCall dfc | call = dfc.getNode().(Cfg::CallNode).getNode() | result = viableCallable(dfc).(DataFlowFunction).getScope() ) } diff --git a/python/ql/src/Resources/FileNotAlwaysClosedQuery.qll b/python/ql/src/Resources/FileNotAlwaysClosedQuery.qll index 9d91e4f523c2..c5c4795eeccf 100644 --- a/python/ql/src/Resources/FileNotAlwaysClosedQuery.qll +++ b/python/ql/src/Resources/FileNotAlwaysClosedQuery.qll @@ -3,6 +3,7 @@ import python import semmle.python.dataflow.new.internal.DataFlowDispatch import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg /** A CFG node where a file is opened. */ abstract class FileOpenSource extends DataFlow::CfgNode { } @@ -64,12 +65,14 @@ abstract class FileClose extends DataFlow::CfgNode { } } -private predicate bbSuccessor(BasicBlock src, BasicBlock sink) { sink = src.getASuccessor() } +private predicate bbSuccessor(Cfg::BasicBlock src, Cfg::BasicBlock sink) { + sink = src.getASuccessor() +} -private predicate bbReachableStrict(BasicBlock src, BasicBlock sink) = +private predicate bbReachableStrict(Cfg::BasicBlock src, Cfg::BasicBlock sink) = fastTC(bbSuccessor/2)(src, sink) -private predicate bbReachableRefl(BasicBlock src, BasicBlock sink) { +private predicate bbReachableRefl(Cfg::BasicBlock src, Cfg::BasicBlock sink) { bbReachableStrict(src, sink) or src = sink } diff --git a/python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIs.qll b/python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIs.qll index 9c12845a0cad..1aa1e74cf132 100644 --- a/python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIs.qll +++ b/python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIs.qll @@ -10,6 +10,7 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.ApiGraphs private import semmle.python.dataflow.new.internal.DataFlowPrivate as DataFlowPrivate private import semmle.python.dataflow.new.internal.TaintTrackingPrivate as TaintTrackingPrivate +private import semmle.python.controlflow.internal.Cfg as Cfg /** * An external API that is considered "safe" from a security perspective. @@ -71,7 +72,7 @@ string apiNodeToStringRepr(API::Node node) { ) } -predicate resolvedCall(CallNode call) { +predicate resolvedCall(Cfg::CallNode call) { DataFlowPrivate::resolveCall(call, _, _) or DataFlowPrivate::resolveClassCall(call, _) } diff --git a/python/ql/src/Security/CWE-079/Jinja2WithoutEscaping.ql b/python/ql/src/Security/CWE-079/Jinja2WithoutEscaping.ql index fd03ba433a10..fb5ef283e65a 100644 --- a/python/ql/src/Security/CWE-079/Jinja2WithoutEscaping.ql +++ b/python/ql/src/Security/CWE-079/Jinja2WithoutEscaping.ql @@ -14,6 +14,7 @@ import python import semmle.python.dataflow.new.DataFlow import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg /* * Jinja 2 Docs: @@ -36,8 +37,8 @@ private API::Node jinja2EnvironmentOrTemplate() { from API::CallNode call where call = jinja2EnvironmentOrTemplate().getACall() and - not exists(call.asCfgNode().(CallNode).getNode().getStarargs()) and - not exists(call.asCfgNode().(CallNode).getNode().getKwargs()) and + not exists(call.asCfgNode().(Cfg::CallNode).getNode().getStarargs()) and + not exists(call.asCfgNode().(Cfg::CallNode).getNode().getKwargs()) and ( not exists(call.getArgByName("autoescape")) or diff --git a/python/ql/src/Security/CWE-327/PyOpenSSL.qll b/python/ql/src/Security/CWE-327/PyOpenSSL.qll index e2571195bfa0..7ce3276598c8 100644 --- a/python/ql/src/Security/CWE-327/PyOpenSSL.qll +++ b/python/ql/src/Security/CWE-327/PyOpenSSL.qll @@ -5,6 +5,7 @@ private import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg import TlsLibraryModel class PyOpenSslContextCreation extends ContextCreation, DataFlow::CallCfgNode { @@ -37,10 +38,10 @@ class ConnectionCall extends ConnectionCreation, DataFlow::CallCfgNode { // This cannot be used to unrestrict, // see https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_options class SetOptionsCall extends ProtocolRestriction, DataFlow::CallCfgNode { - SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } + SetOptionsCall() { node.getFunction().(Cfg::AttrNode).getName() = "set_options" } override DataFlow::CfgNode getContext() { - result.getNode() = node.getFunction().(AttrNode).getObject() + result.getNode() = node.getFunction().(Cfg::AttrNode).getObject() } override ProtocolVersion getRestriction() { diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index c3fd0366436d..5f35590bacf5 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -5,6 +5,7 @@ private import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg import TlsLibraryModel class SslContextCreation extends ContextCreation, DataFlow::CallCfgNode { @@ -53,7 +54,7 @@ class OptionsAugOr extends ProtocolRestriction, DataFlow::CfgNode { ProtocolVersion restriction; OptionsAugOr() { - exists(AugAssign aa, AttrNode attr, Expr flag | + exists(AugAssign aa, Cfg::AttrNode attr, Expr flag | aa.getOperation().getOp() instanceof BitOr and aa.getTarget() = attr.getNode() and attr.getName() = "options" and @@ -80,7 +81,7 @@ class OptionsAugAndNot extends ProtocolUnrestriction, DataFlow::CfgNode { ProtocolVersion restriction; OptionsAugAndNot() { - exists(AugAssign aa, AttrNode attr, Expr flag, UnaryExpr notFlag | + exists(AugAssign aa, Cfg::AttrNode attr, Expr flag, UnaryExpr notFlag | aa.getOperation().getOp() instanceof BitAnd and aa.getTarget() = attr.getNode() and attr.getName() = "options" and diff --git a/python/ql/src/Security/CWE-798/HardcodedCredentials.ql b/python/ql/src/Security/CWE-798/HardcodedCredentials.ql index ab21c106348f..28b0e5da8c54 100644 --- a/python/ql/src/Security/CWE-798/HardcodedCredentials.ql +++ b/python/ql/src/Security/CWE-798/HardcodedCredentials.ql @@ -19,6 +19,7 @@ import semmle.python.filters.Tests private import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch private import semmle.python.dataflow.new.internal.Builtins::Builtins as Builtins private import semmle.python.frameworks.data.ModelsAsData +private import semmle.python.controlflow.internal.Cfg as Cfg bindingset[char, fraction] predicate fewer_characters_than(StringLiteral str, string char, float fraction) { @@ -48,7 +49,7 @@ predicate capitalized_word(StringLiteral str) { str.getText().regexpMatch("[A-Z] predicate format_string(StringLiteral str) { str.getText().matches("%{%}%") } -predicate maybeCredential(ControlFlowNode f) { +predicate maybeCredential(Cfg::ControlFlowNode f) { /* A string that is not too short and unlikely to be text or an identifier. */ exists(StringLiteral str | str = f.getNode() | /* At least 10 characters */ @@ -94,9 +95,9 @@ class CredentialSink extends DataFlow::Node { this.(DataFlow::ArgumentNode).argumentOf(_, pos) ) or - exists(Keyword k | k.getArg() = name and this.getNode() = k.getValue().asCfgNode()) + exists(Keyword k | k.getArg() = name and this.asCfgNode().getNode() = k.getValue()) or - exists(CompareNode cmp, NameNode n | n.getId() = name | + exists(Cfg::CompareNode cmp, Cfg::NameNode n | n.getId() = name | cmp.operands(this.asCfgNode(), any(Eq eq), n) or cmp.operands(n, any(Eq eq), this.asCfgNode()) diff --git a/python/ql/src/Statements/ModificationOfLocals.ql b/python/ql/src/Statements/ModificationOfLocals.ql index f32ddcf78849..29e08f80776c 100644 --- a/python/ql/src/Statements/ModificationOfLocals.ql +++ b/python/ql/src/Statements/ModificationOfLocals.ql @@ -13,24 +13,25 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg -predicate originIsLocals(ControlFlowNode n) { +predicate originIsLocals(Cfg::ControlFlowNode n) { API::builtin("locals").getReturn().getAValueReachableFromSource().asCfgNode() = n } -predicate modification_of_locals(ControlFlowNode f) { - originIsLocals(f.(SubscriptNode).getObject()) and +predicate modification_of_locals(Cfg::ControlFlowNode f) { + originIsLocals(f.(Cfg::SubscriptNode).getObject()) and (f.isStore() or f.isDelete()) or - exists(string mname, AttrNode attr | - attr = f.(CallNode).getFunction() and + exists(string mname, Cfg::AttrNode attr | + attr = f.(Cfg::CallNode).getFunction() and originIsLocals(attr.getObject(mname)) | mname in ["pop", "popitem", "update", "clear"] ) } -from AstNode a, ControlFlowNode f +from AstNode a, Cfg::ControlFlowNode f where modification_of_locals(f) and a = f.getNode() and diff --git a/python/ql/src/Statements/SideEffectInAssert.ql b/python/ql/src/Statements/SideEffectInAssert.ql index 55c34144dced..c58d3947424e 100644 --- a/python/ql/src/Statements/SideEffectInAssert.ql +++ b/python/ql/src/Statements/SideEffectInAssert.ql @@ -14,6 +14,7 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg predicate func_with_side_effects(Expr e) { exists(string name | name = e.(Attribute).getName() or name = e.(Name).getId() | @@ -24,7 +25,7 @@ predicate func_with_side_effects(Expr e) { } predicate call_with_side_effect(Call e) { - exists(ControlFlowNode eCfg | eCfg.getNode() = e | + exists(Cfg::ControlFlowNode eCfg | eCfg.getNode() = e | eCfg = API::moduleImport("subprocess") .getMember(["call", "check_call", "check_output"]) diff --git a/python/ql/src/Statements/UseOfExit.ql b/python/ql/src/Statements/UseOfExit.ql index 2310a839f67b..88a4f1ff7774 100644 --- a/python/ql/src/Statements/UseOfExit.ql +++ b/python/ql/src/Statements/UseOfExit.ql @@ -13,8 +13,9 @@ import python private import semmle.python.ApiGraphs +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, string name +from Cfg::CallNode call, string name where name = ["exit", "quit"] and call = API::builtin(name).getACall().asCfgNode() diff --git a/python/ql/src/Variables/LeakingListComprehension.ql b/python/ql/src/Variables/LeakingListComprehension.ql index 34bf26a35559..a9baa21661da 100644 --- a/python/ql/src/Variables/LeakingListComprehension.ql +++ b/python/ql/src/Variables/LeakingListComprehension.ql @@ -13,7 +13,8 @@ import python import Definition -from ListComprehensionDeclaration l, Name use, Name defn, ControlFlowNode lCfg, ControlFlowNode useCfg +from + ListComprehensionDeclaration l, Name use, Name defn, ControlFlowNode lCfg, ControlFlowNode useCfg where use = l.getALeakedVariableUse() and defn = l.getDefinition() and diff --git a/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql b/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql index 42c0bc170fd9..5f49cb218805 100755 --- a/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql +++ b/python/ql/src/experimental/Security/CWE-022bis/TarSlipImprov.ql @@ -21,6 +21,7 @@ import semmle.python.ApiGraphs import semmle.python.dataflow.new.internal.Attributes import semmle.python.dataflow.new.BarrierGuards import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Handle those three cases of Tarfile opens: @@ -75,8 +76,8 @@ private module TarSlipImprovConfig implements DataFlow::ConfigSig { call = atfo.getReturn().getMember("extractall").getACall() and arg = call.getArgByName("members") and if - arg.asCfgNode() instanceof NameConstantNode or - arg.asCfgNode() instanceof ListNode + arg.asCfgNode() instanceof Cfg::NameConstantNode or + arg.asCfgNode() instanceof Cfg::ListNode then sink = call.getObject() else if arg.(MethodCallNode).getMethodName() = "getmembers" diff --git a/python/ql/src/experimental/Security/CWE-340/TokenBuiltFromUUID.ql b/python/ql/src/experimental/Security/CWE-340/TokenBuiltFromUUID.ql index ab5a4243a746..f43b718289de 100644 --- a/python/ql/src/experimental/Security/CWE-340/TokenBuiltFromUUID.ql +++ b/python/ql/src/experimental/Security/CWE-340/TokenBuiltFromUUID.ql @@ -16,6 +16,7 @@ import python import semmle.python.dataflow.new.DataFlow import semmle.python.ApiGraphs import semmle.python.dataflow.new.TaintTracking +private import semmle.python.controlflow.internal.Cfg as Cfg class PredictableResultSource extends DataFlow::Node { PredictableResultSource() { @@ -32,7 +33,9 @@ class PredictableResultSource extends DataFlow::Node { class TokenAssignmentValueSink extends DataFlow::Node { TokenAssignmentValueSink() { exists(string name | name.toLowerCase().matches(["%token", "%code"]) | - exists(DefinitionNode n | n.getValue() = this.asCfgNode() | name = n.(NameNode).getId()) + exists(Cfg::DefinitionNode n | n.getValue() = this.asCfgNode() | + name = n.(Cfg::NameNode).getId() + ) or exists(DataFlow::AttrWrite aw | aw.getValue() = this | name = aw.getAttributeName()) ) diff --git a/python/ql/src/experimental/Security/CWE-346/CorsBypass.ql b/python/ql/src/experimental/Security/CWE-346/CorsBypass.ql index 01e661cb0bbf..5ce58869a890 100644 --- a/python/ql/src/experimental/Security/CWE-346/CorsBypass.ql +++ b/python/ql/src/experimental/Security/CWE-346/CorsBypass.ql @@ -11,25 +11,25 @@ import python import semmle.python.ApiGraphs import semmle.python.dataflow.new.TaintTracking -import semmle.python.Flow import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Returns true if the control flow node may be useful in the current context. * * Ideally for more completeness, we should alert on every `startswith` call and every remote flow source which gets partailly checked. But, as this can lead to lots of FPs, we apply heuristics to filter some calls. This predicate provides logic for this filteration. */ -private predicate maybeInteresting(ControlFlowNode c) { +private predicate maybeInteresting(Cfg::ControlFlowNode c) { // Check if the name of the variable which calls the function matches the heuristic. // This would typically occur at the sink. // This should deal with cases like // `origin.startswith("bla")` - heuristics(c.(CallNode).getFunction().(AttrNode).getObject().(NameNode).getId()) + heuristics(c.(Cfg::CallNode).getFunction().(Cfg::AttrNode).getObject().(Cfg::NameNode).getId()) or // Check if the name of the variable passed as an argument to the functions matches the heuristic. This would typically occur at the sink. // This should deal with cases like // `bla.startswith(origin)` - heuristics(c.(CallNode).getArg(0).(NameNode).getId()) + heuristics(c.(Cfg::CallNode).getArg(0).(Cfg::NameNode).getId()) or // Check if the value gets written to any interesting variable. This would typically occur at the source. // This should deal with cases like @@ -37,8 +37,10 @@ private predicate maybeInteresting(ControlFlowNode c) { exists(Variable v | heuristics(v.getId()) | c.getASuccessor*().getNode() = v.getAStore()) } -private class StringStartswithCall extends ControlFlowNode { - StringStartswithCall() { this.(CallNode).getFunction().(AttrNode).getName() = "startswith" } +private class StringStartswithCall extends Cfg::ControlFlowNode { + StringStartswithCall() { + this.(Cfg::CallNode).getFunction().(Cfg::AttrNode).getName() = "startswith" + } } bindingset[s] @@ -66,8 +68,8 @@ module CorsBypassConfig implements DataFlow::ConfigSig { predicate isSink(DataFlow::Node node) { exists(StringStartswithCall s | - node.asCfgNode() = s.(CallNode).getArg(0) or - node.asCfgNode() = s.(CallNode).getFunction().(AttrNode).getObject() + node.asCfgNode() = s.(Cfg::CallNode).getArg(0) or + node.asCfgNode() = s.(Cfg::CallNode).getFunction().(Cfg::AttrNode).getObject() ) } diff --git a/python/ql/src/experimental/Security/CWE-770/UnicodeDoS.ql b/python/ql/src/experimental/Security/CWE-770/UnicodeDoS.ql index 61cdd34920de..84627456310b 100644 --- a/python/ql/src/experimental/Security/CWE-770/UnicodeDoS.ql +++ b/python/ql/src/experimental/Security/CWE-770/UnicodeDoS.ql @@ -16,6 +16,7 @@ import semmle.python.Concepts import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.internal.DataFlowPublic import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.controlflow.internal.Cfg as Cfg // The Unicode compatibility normalization calls from unicodedata, unidecode, pyunormalize // and textnorm modules. The use of argIdx is to constraint the argument being normalized. @@ -52,8 +53,8 @@ class UnicodeCompatibilityNormalize extends API::CallNode { DataFlow::Node getPathArg() { result = this.getArg(argIdx) } } -predicate underAValue(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { - exists(CompareNode cn | cn = g | +predicate underAValue(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { + exists(Cfg::CompareNode cn | cn = g | exists(API::CallNode lenCall, Cmpop op, Node n | lenCall = n.getALocalSource() and ( diff --git a/python/ql/src/experimental/Security/UnsafeUnpackQuery.qll b/python/ql/src/experimental/Security/UnsafeUnpackQuery.qll index 64da6b8d799a..ecc029ca8a78 100644 --- a/python/ql/src/experimental/Security/UnsafeUnpackQuery.qll +++ b/python/ql/src/experimental/Security/UnsafeUnpackQuery.qll @@ -9,6 +9,7 @@ import semmle.python.ApiGraphs import semmle.python.dataflow.new.TaintTracking import semmle.python.frameworks.Stdlib import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Handle those three cases of Tarfile opens: @@ -111,8 +112,8 @@ module UnsafeUnpackConfig implements DataFlow::ConfigSig { call = atfo.getReturn().getMember("extractall").getACall() and arg = call.getArgByName("members") and if - arg.asCfgNode() instanceof NameConstantNode or - arg.asCfgNode() instanceof ListNode + arg.asCfgNode() instanceof Cfg::NameConstantNode or + arg.asCfgNode() instanceof Cfg::ListNode then sink = call.getObject() else if arg.(MethodCallNode).getMethodName() = "getmembers" diff --git a/python/ql/src/experimental/semmle/python/security/injection/CsvInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/CsvInjection.qll index 859f6d1e5e80..d789007b8d50 100644 --- a/python/ql/src/experimental/semmle/python/security/injection/CsvInjection.qll +++ b/python/ql/src/experimental/semmle/python/security/injection/CsvInjection.qll @@ -4,6 +4,7 @@ import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.BarrierGuards import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.controlflow.internal.Cfg as Cfg /** * A taint-tracking configuration for tracking untrusted user input used in file read. @@ -21,7 +22,7 @@ private module CsvInjectionConfig implements DataFlow::ConfigSig { predicate observeDiffInformedIncrementalMode() { any() } } -private predicate startsWithCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) { +private predicate startsWithCheck(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { exists(DataFlow::MethodCallNode mc | g = mc.asCfgNode() and mc.calls(_, "startswith") and diff --git a/python/ql/src/meta/analysis-quality/CallGraphQuality.qll b/python/ql/src/meta/analysis-quality/CallGraphQuality.qll index 679b39ddc8d4..1ac0896064b5 100644 --- a/python/ql/src/meta/analysis-quality/CallGraphQuality.qll +++ b/python/ql/src/meta/analysis-quality/CallGraphQuality.qll @@ -6,6 +6,7 @@ import python import meta.MetaMetrics private import LegacyPointsTo +private import semmle.python.controlflow.internal.Cfg as Cfg newtype TTarget = TFunction(Function func) or @@ -50,7 +51,7 @@ class TargetClass extends Target, TClass { * A call that is (possibly) relevant for analysis quality. * See `IgnoredFile` for details on what is excluded. */ -class RelevantCall extends CallNode { +class RelevantCall extends Cfg::CallNode { RelevantCall() { not this.getLocation().getFile() instanceof IgnoredFile } } @@ -60,7 +61,7 @@ module PointsToBasedCallGraph { class ResolvableCall extends RelevantCall { Value targetValue; - ResolvableCall() { targetValue.getACall() = this } + ResolvableCall() { targetValue.getACall().getNode() = this.getNode() } /** Gets a resolved target of this call. */ Target getTarget() { diff --git a/python/ql/src/meta/analysis-quality/TTCallGraph.ql b/python/ql/src/meta/analysis-quality/TTCallGraph.ql index bdd634951919..4487095a6fb6 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraph.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraph.ql @@ -8,8 +8,9 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, Target target +from Cfg::CallNode call, Target target where target.isRelevant() and call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget() = target diff --git a/python/ql/src/meta/analysis-quality/TTCallGraphMissing.ql b/python/ql/src/meta/analysis-quality/TTCallGraphMissing.ql index bb28c5bf804f..188f483689f5 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraphMissing.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraphMissing.ql @@ -8,8 +8,9 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, Target target +from Cfg::CallNode call, Target target where target.isRelevant() and call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and diff --git a/python/ql/src/meta/analysis-quality/TTCallGraphNew.ql b/python/ql/src/meta/analysis-quality/TTCallGraphNew.ql index b9f1df54b3b2..198007d07759 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraphNew.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraphNew.ql @@ -8,8 +8,9 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, Target target +from Cfg::CallNode call, Target target where target.isRelevant() and not call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and diff --git a/python/ql/src/meta/analysis-quality/TTCallGraphNewAmbiguous.ql b/python/ql/src/meta/analysis-quality/TTCallGraphNewAmbiguous.ql index 702541ca16d4..b14644f6585a 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraphNewAmbiguous.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraphNewAmbiguous.ql @@ -8,8 +8,9 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, Target target +from Cfg::CallNode call, Target target where target.isRelevant() and not call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and diff --git a/python/ql/src/meta/analysis-quality/TTCallGraphOverview.ql b/python/ql/src/meta/analysis-quality/TTCallGraphOverview.ql index 5a789d1be90b..19660f92ec55 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraphOverview.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraphOverview.ql @@ -6,12 +6,13 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg from string tag, int c where tag = "SHARED" and c = - count(CallNode call, Target target | + count(Cfg::CallNode call, Target target | target.isRelevant() and call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget() = target @@ -19,7 +20,7 @@ where or tag = "NEW" and c = - count(CallNode call, Target target | + count(Cfg::CallNode call, Target target | target.isRelevant() and not call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget() = target @@ -27,7 +28,7 @@ where or tag = "MISSING" and c = - count(CallNode call, Target target | + count(Cfg::CallNode call, Target target | target.isRelevant() and call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and not call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget() = target diff --git a/python/ql/src/meta/analysis-quality/TTCallGraphShared.ql b/python/ql/src/meta/analysis-quality/TTCallGraphShared.ql index d44d1ac497ff..f8275b5a965b 100644 --- a/python/ql/src/meta/analysis-quality/TTCallGraphShared.ql +++ b/python/ql/src/meta/analysis-quality/TTCallGraphShared.ql @@ -8,8 +8,9 @@ import python import CallGraphQuality +private import semmle.python.controlflow.internal.Cfg as Cfg -from CallNode call, Target target +from Cfg::CallNode call, Target target where target.isRelevant() and call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and diff --git a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll index 83ba4df4e298..d4cdc388b14f 100644 --- a/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll +++ b/python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll @@ -6,6 +6,7 @@ private import python private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.BarrierGuards +private import semmle.python.controlflow.internal.Cfg as Cfg /** * Provides default sources, sinks and sanitizers for detecting @@ -76,7 +77,7 @@ module ModificationOfParameterWithDefault { boolean nonEmpty; MutableDefaultValue() { - nonEmpty = mutableDefaultValue(this.asCfgNode().(NameNode).getNode()) and + nonEmpty = mutableDefaultValue(this.asCfgNode().(Cfg::NameNode).getNode()) and // Ignore sources inside the standard library. These are unlikely to be true positives. exists(this.getLocation().getFile().getRelativePath()) } @@ -125,10 +126,10 @@ module ModificationOfParameterWithDefault { class Mutation extends Sink { Mutation() { // assignment to a subscript (includes slices) - exists(DefinitionNode d | d.(SubscriptNode).getObject() = this.asCfgNode()) + exists(Cfg::DefinitionNode d | d.(Cfg::SubscriptNode).getObject() = this.asCfgNode()) or // deletion of a subscript - exists(DeletionNode d | d.getTarget().(SubscriptNode).getObject() = this.asCfgNode()) + exists(Cfg::DeletionNode d | d.(Cfg::SubscriptNode).getObject() = this.asCfgNode()) or // augmented assignment to the value exists(AugAssign a | this.asCfgNode().getNode() = a.getTarget()) @@ -141,54 +142,33 @@ module ModificationOfParameterWithDefault { } } - // This to reimplement some of the functionality of the DataFlow::BarrierGuard - private import semmle.python.essa.SsaCompute - /** - * A data-flow node that is known to be either truthy or falsey. - * - * It handles the cases `if x` and `if not x`. + * Holds if `g` validates `node` as truthy when evaluating to `branch`. * - * For example, in the following code, `this` will be the `x` that is printed, - * which we will know is truthy: - * - * ```py - * if x: - * print(x) - * ``` + * The new shared CFG's `GuardNode`/`outcomeOfGuard` already unwraps + * `not x` wrappers, so we only need the direct case: a guard `g` + * controls a block where the guarded value (also `g`) is known to + * have the matching truthiness for the taken branch. */ - private class MustBe extends DataFlow::Node { - boolean truthy; - - MustBe() { - exists(DataFlow::GuardNode guard, NameNode guarded, boolean branch | - // case: if x - guard = guarded and - branch = truthy - or - // case: if not x - guard.(UnaryExprNode).getNode().getOp() instanceof Not and - guarded = guard.(UnaryExprNode).getOperand() and - branch = truthy.booleanNot() - | - // guard controls this - guard.controlsBlock(this.asCfgNode().getBasicBlock(), branch) and - // there is a definition tying the guarded value to this - exists(EssaDefinition def | - AdjacentUses::useOfDef(def, this.asCfgNode()) and - AdjacentUses::useOfDef(def, guarded) - ) - ) - } + private predicate truthinessGuard(DataFlow::GuardNode g, Cfg::ControlFlowNode node, boolean branch) { + node = g and branch in [true, false] } /** Simple guard detecting truthy values. */ - private class MustBeTruthy extends MustBe, MustBeNonEmpty { - MustBeTruthy() { truthy = true } + private class MustBeTruthy extends MustBeNonEmpty { + MustBeTruthy() { + this = DataFlow::BarrierGuard::getABarrierNode() and + // truthy = true branch + exists(DataFlow::GuardNode g | g.controlsBlock(this.asCfgNode().getBasicBlock(), true)) + } } /** Simple guard detecting falsey values. */ - private class MustBeFalsey extends MustBe, MustBeEmpty { - MustBeFalsey() { truthy = false } + private class MustBeFalsey extends MustBeEmpty { + MustBeFalsey() { + this = DataFlow::BarrierGuard::getABarrierNode() and + // truthy = false branch + exists(DataFlow::GuardNode g | g.controlsBlock(this.asCfgNode().getBasicBlock(), false)) + } } } diff --git a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql index 44350378b75c..b89d2ddad230 100644 --- a/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql +++ b/python/ql/test/library-tests/dataflow/regression/custom_dataflow.ql @@ -12,7 +12,9 @@ private import semmle.python.controlflow.internal.Cfg as Cfg import semmle.python.dataflow.new.DataFlow module CustomTestConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node node) { node.asCfgNode().(Cfg::NameNode).getId() = "CUSTOM_SOURCE" } + predicate isSource(DataFlow::Node node) { + node.asCfgNode().(Cfg::NameNode).getId() = "CUSTOM_SOURCE" + } predicate isSink(DataFlow::Node node) { exists(Cfg::CallNode call | diff --git a/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql b/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql index 68cc84998c2b..cc8049e00d08 100644 --- a/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql +++ b/python/ql/test/library-tests/dataflow/typetracking_imports/highlight_problem.ql @@ -11,7 +11,10 @@ where m = v.getScope().getEnclosingModule() and not m.getName() in ["pkg.use", "pkg.foo_def"] and v.getName() = "foo" and - if exists(Cfg::ControlFlowNode exit | exit.isNormalExit() and exit.getScope() = m and v.getAUse() = exit) + if + exists(Cfg::ControlFlowNode exit | + exit.isNormalExit() and exit.getScope() = m and v.getAUse() = exit + ) then useToNormalExit = "use to normal exit" else useToNormalExit = "no use to normal exit" select m, v, useToNormalExit