From 0e66555e377ad6207d705e2b67bbf6b3821701ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:10:53 +0000 Subject: [PATCH 1/3] Initial plan From 3483050526e70c4c92f30746f4696e41eb1bd076 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:18:02 +0000 Subject: [PATCH 2/3] Fix false positive in MissedSelectOpportunity for async/await loops Agent-Logs-Url: https://github.com/github/codeql/sessions/3e8f4320-2bf4-45f5-b9ea-dad41d522d84 Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- csharp/ql/lib/Linq/Helpers.qll | 8 +++-- .../MissedSelectOpportunity.cs | 32 +++++++++++++++++++ .../MissedSelectOpportunity.expected | 1 + .../MissedSelectOpportunity.qlref | 1 + .../Linq/MissedSelectOpportunity/options | 2 ++ 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs create mode 100644 csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.expected create mode 100644 csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref create mode 100644 csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/options diff --git a/csharp/ql/lib/Linq/Helpers.qll b/csharp/ql/lib/Linq/Helpers.qll index 912b12b9457b..2a4d5c8c27a2 100644 --- a/csharp/ql/lib/Linq/Helpers.qll +++ b/csharp/ql/lib/Linq/Helpers.qll @@ -121,15 +121,17 @@ predicate missedOfTypeOpportunity(ForeachStmtEnumerable fes, LocalVariableDeclSt /** * Holds if `foreach` statement `fes` can be converted to a `.Select()` call. * That is, the loop variable is accessed only in the first statement of the - * block, the access is not a cast, and the first statement is a - * local variable declaration statement `s`. + * block, the access is not a cast, the first statement is a + * local variable declaration statement `s`, and the initializer does not + * contain an `await` expression (since `Select` does not support async lambdas). */ predicate missedSelectOpportunity(ForeachStmtGenericEnumerable fes, LocalVariableDeclStmt s) { s = firstStmt(fes) and forex(VariableAccess va | va = fes.getVariable().getAnAccess() | va = s.getAVariableDeclExpr().getAChildExpr*() ) and - not s.getAVariableDeclExpr().getInitializer() instanceof Cast + not s.getAVariableDeclExpr().getInitializer() instanceof Cast and + not s.getAVariableDeclExpr().getInitializer().getAChildExpr*() instanceof AwaitExpr } /** diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs new file mode 100644 index 000000000000..a61eba16f149 --- /dev/null +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; + +class MissedSelectOpportunity +{ + public void M1(List lst) + { + // BAD: Can be replaced with lst.Select(i => i * i) + foreach (int i in lst) + { + int j = i * i; + Console.WriteLine(j); + } + } + + public async Task M2(IEnumerable counters) + { + // GOOD: Cannot use Select because the initializer contains an await expression + foreach (var counter in counters) + { + var count = await counter.CountAsync(); + Console.WriteLine(count); + } + } + + public interface ICounter + { + Task CountAsync(); + } +} diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.expected b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.expected new file mode 100644 index 000000000000..bc6d464fa3b9 --- /dev/null +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.expected @@ -0,0 +1 @@ +| MissedSelectOpportunity.cs:11:9:15:9 | foreach (... ... in ...) ... | This foreach loop immediately $@ - consider mapping the sequence explicitly using '.Select(...)'. | MissedSelectOpportunity.cs:13:13:13:26 | ... ...; | maps its iteration variable to another variable | diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref new file mode 100644 index 000000000000..8056fd74c47c --- /dev/null +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref @@ -0,0 +1 @@ +Linq/MissedSelectOpportunity.ql diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/options b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/options new file mode 100644 index 000000000000..75c39b4541ba --- /dev/null +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/options @@ -0,0 +1,2 @@ +semmle-extractor-options: /nostdlib /noconfig +semmle-extractor-options: --load-sources-from-project:${testdir}/../../../resources/stubs/_frameworks/Microsoft.NETCore.App/Microsoft.NETCore.App.csproj From 878cfd720ca909b35c25b5caecf09b32deb811f2 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Tue, 14 Apr 2026 14:41:28 +0200 Subject: [PATCH 3/3] C#: Use inline test expectations --- .../Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs | 2 +- .../Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs index a61eba16f149..9655a5a0fa9c 100644 --- a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.cs @@ -12,7 +12,7 @@ public void M1(List lst) { int j = i * i; Console.WriteLine(j); - } + } // $ Alert } public async Task M2(IEnumerable counters) diff --git a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref index 8056fd74c47c..722d84896800 100644 --- a/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref +++ b/csharp/ql/test/query-tests/Linq/MissedSelectOpportunity/MissedSelectOpportunity.qlref @@ -1 +1,2 @@ -Linq/MissedSelectOpportunity.ql +query: Linq/MissedSelectOpportunity.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql