Skip to content

Commit cf26b00

Browse files
authored
Merge pull request #22484 from baywet/fix/csharp-where-opportunity-false
fix: missed where opporunity false positive
2 parents c970d43 + dd84d7f commit cf26b00

6 files changed

Lines changed: 156 additions & 10 deletions

File tree

csharp/ql/lib/Linq/Helpers.qll

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ private int numStmts(ForeachStmt fes) {
2020
else result = 1
2121
}
2222

23+
private predicate terminatesCallable(Stmt s) {
24+
exists(Stmt stripped | stripped = s.stripSingletonBlocks() |
25+
stripped instanceof ReturnStmt
26+
or
27+
stripped instanceof YieldBreakStmt
28+
or
29+
stripped instanceof ThrowStmt
30+
or
31+
stripped instanceof BreakStmt
32+
or
33+
stripped = any(BlockStmt b | terminatesCallable(b.getLastStmt()))
34+
or
35+
stripped =
36+
any(IfStmt nested |
37+
terminatesCallable(nested.getThen()) and
38+
terminatesCallable(nested.getElse())
39+
)
40+
)
41+
}
42+
2343
/** Holds if the type's qualified name is "System.Linq.Enumerable" */
2444
predicate isEnumerableType(ValueOrRefType t) {
2545
t.hasFullyQualifiedName("System.Linq", "Enumerable")
@@ -152,7 +172,8 @@ predicate missedWhereOpportunity(ForeachStmtGenericEnumerable fes, IfStmt is) {
152172
is.getThen() instanceof ContinueStmt
153173
or
154174
not exists(is.getElse()) and
155-
numStmts(fes) = 1
175+
numStmts(fes) = 1 and
176+
not terminatesCallable(is.getThen())
156177
)
157178
}
158179

csharp/ql/src/Linq/MissedWhereOpportunity.qhelp

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,38 @@
33
"qhelp.dtd">
44
<qhelp>
55
<overview>
6-
<p>Programmers sometimes need to iterative over a filtered version of a sequence, rather than the
7-
sequence itself. For example, you might want to print out only the numbers in the range [1,10] that
8-
are even. One standard way of doing this is to write a loop that iterates over the whole sequence,
9-
testing the variable each iteration to determine whether or not it is even. This is often written
10-
using either <code>if(!condition(var)) continue;</code> as the initial statement in the loop, or by
6+
<p>Programmers sometimes need to iterate over a filtered version of a sequence, rather than the
7+
sequence itself. For example, you might want to print out only the numbers in the range [1,10] that
8+
are even. One standard way of doing this is to write a loop that iterates over the whole sequence,
9+
testing the variable each iteration to determine whether or not it is even. This is often written
10+
using either <code>if(!condition(var)) continue;</code> as the initial statement in the loop, or by
1111
enclosing the entire loop body with <code>if(condition(var))</code>.</p>
1212

13+
<p>This recommendation does not apply when the matching branch exits the loop without continuing to
14+
later iterations, such as with <code>return</code>, <code>yield break</code>, or <code>throw</code>.
15+
In those cases the loop is searching for a terminal condition rather than filtering the remaining
16+
loop body.</p>
17+
1318
</overview>
1419
<recommendation>
15-
<p>This pattern works well and is also available as the <code>Where</code> method in LINQ in C# 3.5
16-
and above. It is better to use a library method in preference to writing your own pattern unless you
17-
have a specific need for a custom version. In particular, this makes the code easier to read by
20+
<p>This pattern works well and is also available as the <code>Where</code> method in LINQ in C# 3.5
21+
and above. It is better to use a library method in preference to writing your own pattern unless you
22+
have a specific need for a custom version. In particular, this makes the code easier to read by
1823
expressing the intent better and by reducing the nesting depth of the code.</p>
1924

2025
</recommendation>
2126
<example>
22-
<p>This example shows two ways of iterating over a series of integers and only performing an action
27+
<p>This example shows two ways of iterating over a series of integers and only performing an action
2328
on the even ones.</p>
2429
<sample src="MissedWhereOpportunity.cs" />
2530

2631
<p>This is far better expressed using the <code>Where</code> method.</p>
2732
<sample src="MissedWhereOpportunityFix.cs" />
2833

34+
<p>The following example should not use <code>Where</code>, because the matching branch exits the
35+
method or iterator instead of continuing with filtered loop work.</p>
36+
<sample src="MissedWhereOpportunityGood.cs" />
37+
2938
</example>
3039
<references>
3140

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class MissedWhereOpportunityGood
2+
{
3+
public int? FindFirstEven(System.Collections.Generic.IEnumerable<int> values)
4+
{
5+
foreach (int value in values)
6+
{
7+
if (value % 2 == 0)
8+
return value;
9+
}
10+
11+
return null;
12+
}
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* The `cs/linq/missed-where` query no longer flags `foreach` loops where the matching branch terminates the method, iterator, or loop instead of continuing with filtered loop work.

csharp/ql/test/query-tests/Linq/MissedWhereOpportunity/MissedWhereOpportunity.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,104 @@ public void M5(IEnumerable<int> elements)
7676
} // $ Alert
7777
}
7878

79+
public int M6(IEnumerable<int> elements)
80+
{
81+
// GOOD: The filtered case returns from the method instead of continuing the loop.
82+
foreach (var element in elements)
83+
{
84+
if (element.GetHashCode() % 2 == 0)
85+
{
86+
return element;
87+
}
88+
}
89+
90+
return 0;
91+
}
92+
93+
public IEnumerable<int> M7(IEnumerable<int> elements)
94+
{
95+
// GOOD: The filtered case exits the iterator instead of continuing the loop.
96+
foreach (var element in elements)
97+
{
98+
if (element.GetHashCode() % 2 == 0)
99+
{
100+
yield break;
101+
}
102+
}
103+
}
104+
105+
public void M8(IEnumerable<int> elements)
106+
{
107+
// GOOD: The filtered case throws instead of continuing the loop.
108+
foreach (var element in elements)
109+
{
110+
if (element.GetHashCode() % 2 == 0)
111+
{
112+
throw new InvalidOperationException();
113+
}
114+
}
115+
}
116+
117+
public IEnumerable<int> M9(IEnumerable<int> elements)
118+
{
119+
// BAD: A yield return does not exit the iterator, so the loop still filters yielded values.
120+
foreach (var element in elements)
121+
{
122+
if (element.GetHashCode() % 2 == 0)
123+
{
124+
yield return element;
125+
}
126+
} // $ Alert
127+
}
128+
129+
public int M10(IEnumerable<int> elements)
130+
{
131+
// GOOD: The filtered case ends with a return from the method instead of continuing the loop.
132+
foreach (var element in elements)
133+
{
134+
if (element.GetHashCode() % 2 == 0)
135+
{
136+
Console.WriteLine(element);
137+
return element;
138+
}
139+
}
140+
141+
return 0;
142+
}
143+
144+
public int M11(IEnumerable<int> elements)
145+
{
146+
// GOOD: Both nested filtered cases return from the method instead of continuing the loop.
147+
foreach (var element in elements)
148+
{
149+
if (element.GetHashCode() % 2 == 0)
150+
{
151+
if (element > 10)
152+
{
153+
return element;
154+
}
155+
else
156+
{
157+
return 10;
158+
}
159+
}
160+
}
161+
162+
return 0;
163+
}
164+
165+
public void M12(IEnumerable<int> elements)
166+
{
167+
// GOOD: The filtered case exits the loop instead of continuing with filtered loop work.
168+
foreach (var element in elements)
169+
{
170+
if (element.GetHashCode() % 2 == 0)
171+
{
172+
break;
173+
}
174+
}
175+
}
176+
79177
public class NonEnumerableClass
80178
{
81179
public IEnumerator<int> GetEnumerator() => throw null;

csharp/ql/test/query-tests/Linq/MissedWhereOpportunity/MissedWhereOpportunity.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
| MissedWhereOpportunity.cs:19:9:26:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider filtering the sequence explicitly using '.Where(...)'. | MissedWhereOpportunity.cs:21:17:21:26 | ... == ... | implicitly filters its target sequence |
33
| MissedWhereOpportunity.cs:45:9:52:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider filtering the sequence explicitly using '.Where(...)'. | MissedWhereOpportunity.cs:47:17:47:26 | ... == ... | implicitly filters its target sequence |
44
| MissedWhereOpportunity.cs:70:9:76:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider filtering the sequence explicitly using '.Where(...)'. | MissedWhereOpportunity.cs:72:17:72:46 | ... == ... | implicitly filters its target sequence |
5+
| MissedWhereOpportunity.cs:120:9:126:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider filtering the sequence explicitly using '.Where(...)'. | MissedWhereOpportunity.cs:122:17:122:46 | ... == ... | implicitly filters its target sequence |

0 commit comments

Comments
 (0)