From 1b2548479772b4fd51e5a2c30cc229531d904ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:19:22 +0200 Subject: [PATCH 1/9] await has side-effects --- python/ql/src/Statements/StatementNoEffect.ql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index 222907f24e33..bf902d6afb7b 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -126,6 +126,8 @@ predicate python2_print(Expr e) { predicate no_effect(Expr e) { // strings can be used as comments not e instanceof StringLiteral and + // await triggers actions and switches coroutines + not e instanceof Await and not e.hasSideEffects() and forall(Expr sub | sub = e.getASubExpression*() | not side_effecting_binary(sub) and From 26b67136c46ac7213cc50ab43d08b7f4e2daff53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:31:53 +0200 Subject: [PATCH 2/9] helper to find Protocol ClassDefs --- python/ql/src/Statements/StatementNoEffect.ql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index bf902d6afb7b..ebae4b58f933 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -14,6 +14,7 @@ import python private import LegacyPointsTo +import semmle.python.ApiGraphs predicate understood_attribute(Attribute attr, ClassValue cls, ClassValue attr_cls) { exists(string name | attr.getName() = name | @@ -94,6 +95,20 @@ private string special_method() { result = any(BinaryExpr b).getOp().getSpecialMethodName() } +ClassDef getAProtocolDef() { + exists(Expr e | + e = + API::moduleImport("typing") + .getMember("Protocol") + .getASubclass*() + .getAValueReachableFromSource() + .asExpr() + | + e instanceof ClassExpr and + e = result.getValue() + ) +} + predicate is_notebook(File f) { exists(Comment c | c.getLocation().getFile() = f | c.getText().regexpMatch("#\\s*.+\\s*") From df478947e64f54a5d8ab867685c5f9739521bf30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:35:43 +0200 Subject: [PATCH 3/9] helper to check statements are only scope content --- python/ql/src/Statements/StatementNoEffect.ql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index ebae4b58f933..08dbad6876f1 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -95,6 +95,10 @@ private string special_method() { result = any(BinaryExpr b).getOp().getSpecialMethodName() } +private predicate is_only_scope_statement(Stmt s) { + forex(Stmt scope_stmt | scope_stmt = s.getScope().getAStmt() | scope_stmt = s) +} + ClassDef getAProtocolDef() { exists(Expr e | e = From d733b3789a307163289bb851648ead62643a1424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:36:03 +0200 Subject: [PATCH 4/9] minor motivation docs for typing helpers --- python/ql/src/Statements/StatementNoEffect.ql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index 08dbad6876f1..a76ec831edef 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -95,6 +95,13 @@ private string special_method() { result = any(BinaryExpr b).getOp().getSpecialMethodName() } +/* + * utilities for detection `...` typing ellipsis expression statements + * + * Various Python typing constructs merely define class/function signatures. + * In this case, the body is commonly a single `...` expression statement. + */ + private predicate is_only_scope_statement(Stmt s) { forex(Stmt scope_stmt | scope_stmt = s.getScope().getAStmt() | scope_stmt = s) } From d54d07526a6732dcfee2e56d8caf7382669b25df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:50:00 +0200 Subject: [PATCH 5/9] helper to find overload decorated functions --- python/ql/src/Statements/StatementNoEffect.ql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index a76ec831edef..9f4f583ffaf5 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -106,6 +106,14 @@ private predicate is_only_scope_statement(Stmt s) { forex(Stmt scope_stmt | scope_stmt = s.getScope().getAStmt() | scope_stmt = s) } +private Function getAnOverload() { + exists(Expr e | + e = API::moduleImport("typing").getMember("overload").getAValueReachableFromSource().asExpr() + | + e = result.getADecorator() + ) +} + ClassDef getAProtocolDef() { exists(Expr e | e = From 2a23663c5280cd5ff71ef7f5c199dc53d0dfb599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 13:53:44 +0200 Subject: [PATCH 6/9] limit visibility --- python/ql/src/Statements/StatementNoEffect.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index 9f4f583ffaf5..5d7ae063574e 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -114,7 +114,7 @@ private Function getAnOverload() { ) } -ClassDef getAProtocolDef() { +private ClassDef getAProtocolDef() { exists(Expr e | e = API::moduleImport("typing") From 40da1097f40a00776ee9a48a53419b4457c27d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 14:05:29 +0200 Subject: [PATCH 7/9] ignore typing ellipses --- python/ql/src/Statements/StatementNoEffect.ql | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index 5d7ae063574e..dc189b844588 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -128,6 +128,18 @@ private ClassDef getAProtocolDef() { ) } +predicate is_typing_ellipsis(ExprStmt s) { + s.getValue() instanceof Ellipsis and + s.getScope() instanceof Function and + is_only_scope_statement(s) and + ( + s.getScope() = getAnOverload() + or + s.getScope().getScope() instanceof Class and + s.getScope().getScope() = getAProtocolDef().getDefinedClass() + ) +} + predicate is_notebook(File f) { exists(Comment c | c.getLocation().getFile() = f | c.getText().regexpMatch("#\\s*.+\\s*") @@ -173,5 +185,5 @@ predicate no_effect(Expr e) { } from ExprStmt stmt -where no_effect(stmt.getValue()) +where no_effect(stmt.getValue()) and not is_typing_ellipsis(stmt) select stmt, "This statement has no effect." From 012e13833e279c77bb3579db91faa5d6d5b94436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Thu, 6 Aug 2026 20:44:12 +0200 Subject: [PATCH 8/9] helper for typed subclassing --- python/ql/src/Statements/StatementNoEffect.ql | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/python/ql/src/Statements/StatementNoEffect.ql b/python/ql/src/Statements/StatementNoEffect.ql index dc189b844588..b11cec4097e0 100644 --- a/python/ql/src/Statements/StatementNoEffect.ql +++ b/python/ql/src/Statements/StatementNoEffect.ql @@ -114,12 +114,18 @@ private Function getAnOverload() { ) } +private API::Node getATypedSubclass(API::Node base) { + // class Result(base, ...) + result = base.getASubclass() + or + // Result = base[...] + result = base.getASubscript() +} + private ClassDef getAProtocolDef() { exists(Expr e | e = - API::moduleImport("typing") - .getMember("Protocol") - .getASubclass*() + getATypedSubclass*(API::moduleImport("typing").getMember("Protocol")) .getAValueReachableFromSource() .asExpr() | From 32a698da8071aad0126f7789656470c9a62849e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Fri, 7 Aug 2026 12:53:22 +0200 Subject: [PATCH 9/9] add change notes --- python/ql/src/change-notes/2026-08-07-stmt-effects.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 python/ql/src/change-notes/2026-08-07-stmt-effects.md diff --git a/python/ql/src/change-notes/2026-08-07-stmt-effects.md b/python/ql/src/change-notes/2026-08-07-stmt-effects.md new file mode 100644 index 000000000000..b9cca78dd43a --- /dev/null +++ b/python/ql/src/change-notes/2026-08-07-stmt-effects.md @@ -0,0 +1,5 @@ +--- +category: majorAnalysis +--- +* Ignore `await` expression statements for `StatementNoEffect` check. +* Ignore `...` expression statements for `StatementNoEffect` check in various typing contexts.