From d5583712cb297252f8fd258d79c6d9c26190a615 Mon Sep 17 00:00:00 2001 From: yoff <51428+yoff@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:30:51 +0000 Subject: [PATCH] Shared CFG: add defaulted getWhileElse/getForeachElse to AstSig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the shared-controlflow changes out of the larger Python new-CFG PR so they can be reviewed independently. No behavioural change for existing languages (Java, C#, Ruby, Swift, Go, ...) — both new predicates default to `none()` and the Make0 loop-edge case extensions only fire when a language overrides them. Python's upcoming AstNodeImpl wiring uses these predicates to model `while-else` and `for-else`, where the `else` block runs when the loop condition becomes false (rather than via a `break`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../change-notes/2026-05-19-loop-else.md | 4 ++ .../codeql/controlflow/ControlFlowGraph.qll | 40 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 shared/controlflow/change-notes/2026-05-19-loop-else.md diff --git a/shared/controlflow/change-notes/2026-05-19-loop-else.md b/shared/controlflow/change-notes/2026-05-19-loop-else.md new file mode 100644 index 00000000000..37ba48bc02b --- /dev/null +++ b/shared/controlflow/change-notes/2026-05-19-loop-else.md @@ -0,0 +1,4 @@ +--- +category: feature +--- +* The `AstSig` signature gains two new defaulted predicates `getWhileElse` and `getForeachElse`, allowing languages (like Python) to model `while-else` / `for-else` constructs where the `else` branch is taken when the loop condition becomes false (rather than via a `break`). Existing languages that do not provide these predicates retain the previous behaviour. diff --git a/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll b/shared/controlflow/codeql/controlflow/ControlFlowGraph.qll index fff877b9fcd..dd71b5f98c7 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 |