Skip to content

Commit 41ad940

Browse files
committed
fix: missed where opporunity false positive
1 parent 7b2695b commit 41ad940

5 files changed

Lines changed: 121 additions & 10 deletions

File tree

csharp/ql/lib/Linq/Helpers.qll

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

23+
private predicate terminatesCallable(Stmt s) {
24+
s.stripSingletonBlocks() instanceof ReturnStmt
25+
or
26+
s.stripSingletonBlocks() instanceof YieldBreakStmt
27+
or
28+
s.stripSingletonBlocks() instanceof ThrowStmt
29+
or
30+
exists(BlockStmt b | b = s.stripSingletonBlocks() | terminatesCallable(b.getLastStmt()))
31+
or
32+
exists(IfStmt nested |
33+
nested = s.stripSingletonBlocks() and
34+
exists(nested.getElse()) and
35+
terminatesCallable(nested.getThen()) and
36+
terminatesCallable(nested.getElse())
37+
)
38+
}
39+
2340
/** Holds if the type's qualified name is "System.Linq.Enumerable" */
2441
predicate isEnumerableType(ValueOrRefType t) {
2542
t.hasFullyQualifiedName("System.Linq", "Enumerable")
@@ -152,7 +169,8 @@ predicate missedWhereOpportunity(ForeachStmtGenericEnumerable fes, IfStmt is) {
152169
is.getThen() instanceof ContinueStmt
153170
or
154171
not exists(is.getElse()) and
155-
numStmts(fes) = 1
172+
numStmts(fes) = 1 and
173+
not terminatesCallable(is.getThen())
156174
)
157175
}
158176

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 by exiting the current
14+
method or iterator, such as with a full <code>return</code>, <code>yield break</code>, or
15+
<code>throw</code>. In those cases the loop is searching for a terminal condition rather than
16+
filtering the remaining 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 examples 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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
14+
public System.Collections.Generic.IEnumerable<int> ValuesUntilFirstEven(System.Collections.Generic.IEnumerable<int> values)
15+
{
16+
foreach (int value in values)
17+
{
18+
if (value % 2 == 0)
19+
yield break;
20+
21+
yield return value;
22+
}
23+
}
24+
25+
public void ThrowOnFirstEven(System.Collections.Generic.IEnumerable<int> values)
26+
{
27+
foreach (int value in values)
28+
{
29+
if (value % 2 == 0)
30+
throw new System.InvalidOperationException("Unexpected even value.");
31+
}
32+
}
33+
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,56 @@ 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+
79129
public class NonEnumerableClass
80130
{
81131
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)