diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d6c63c6027c..eb21cf32b1d 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 fcc9a17aa74..89a93f41a01 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 514a68cba47..617d21b23a5 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. */ @@ -1539,19 +1553,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 @@ -1564,10 +1591,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 |