Skip to content

Commit 41a9a16

Browse files
committed
fix: handle non null default values in the rule
1 parent ed15edf commit 41a9a16

3 files changed

Lines changed: 69 additions & 5 deletions

File tree

csharp/ql/lib/Linq/Helpers.qll

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,24 @@ private predicate returnsLoopVariable(ForeachStmt fes, Stmt s, ReturnStmt ret) {
2525
ret.getExpr().stripCasts().(VariableAccess).getTarget() = fes.getVariable()
2626
}
2727

28-
private predicate returnsDefaultValue(ReturnStmt ret) {
29-
ret.getExpr().stripCasts() instanceof NullLiteral
30-
or
31-
ret.getExpr().stripCasts() instanceof DefaultValueExpr
28+
private predicate hasNullDefault(Type t) { t.isRefType() or t instanceof NullableType }
29+
30+
private predicate returnsDefaultValue(ForeachStmt fes, ReturnStmt ret) {
31+
exists(Type elementType |
32+
elementType = fes.getVariable().getType() |
33+
ret.getExpr().stripCasts() instanceof NullLiteral and
34+
hasNullDefault(elementType)
35+
or
36+
exists(DefaultValueExpr defaultValue |
37+
defaultValue = ret.getExpr().stripCasts() and
38+
(
39+
defaultValue.getType() = elementType
40+
or
41+
hasNullDefault(elementType) and
42+
hasNullDefault(defaultValue.getType())
43+
)
44+
)
45+
)
3246
}
3347

3448
/** Holds if the type's qualified name is "System.Linq.Enumerable" */
@@ -186,7 +200,7 @@ predicate missedFirstOrDefaultOpportunity(
186200
not is.getCondition().getAChildExpr*() instanceof AwaitExpr and
187201
returnsLoopVariable(fes, is.getThen(), ret) and
188202
// If no element matches, the method returns the same value that FirstOrDefault would.
189-
returnsDefaultValue(defaultRet) and
203+
returnsDefaultValue(fes, defaultRet) and
190204
exists(BlockStmt enclosingBlock, int i |
191205
enclosingBlock.getStmt(i) = fes and
192206
enclosingBlock.getStmt(i + 1) = defaultRet

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,54 @@ public Operation M9(IEnumerable<Operation> operations, string operationId)
119119
return null;
120120
}
121121

122+
public object M10(IEnumerable<int> values)
123+
{
124+
// GOOD: FirstOrDefault would return boxed 0 when no match is found, not null.
125+
foreach (var value in values)
126+
{
127+
if (value > 0)
128+
return value;
129+
}
130+
131+
return null;
132+
}
133+
134+
public object M11(IEnumerable<int> values)
135+
{
136+
// GOOD: FirstOrDefault would return boxed 0 when no match is found, not default(object).
137+
foreach (var value in values)
138+
{
139+
if (value > 0)
140+
return value;
141+
}
142+
143+
return default(object);
144+
}
145+
146+
public object M12(IEnumerable<string> values)
147+
{
148+
// BAD: FirstOrDefault returns null for missing reference-type elements, matching the fallback.
149+
foreach (var value in values)
150+
{
151+
if (value.Length > 0)
152+
return value;
153+
} // $ Alert
154+
155+
return null;
156+
}
157+
158+
public object M13(IEnumerable<int> values)
159+
{
160+
// BAD: FirstOrDefault returns 0 for missing int elements, matching the fallback before boxing.
161+
foreach (var value in values)
162+
{
163+
if (value > 0)
164+
return value;
165+
} // $ Alert
166+
167+
return default(int);
168+
}
169+
122170
private static Task<bool> IsMatch(Operation operation, string operationId) =>
123171
Task.FromResult(string.Equals(operation.OperationId, operationId, StringComparison.Ordinal));
124172
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
| MissedFirstOrDefaultOpportunity.cs:10:9:14:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider finding the element explicitly using '.FirstOrDefault(...)'. | MissedFirstOrDefaultOpportunity.cs:12:17:12:91 | call to method Equals | returns the first sequence element satisfying a predicate |
22
| MissedFirstOrDefaultOpportunity.cs:22:9:28:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider finding the element explicitly using '.FirstOrDefault(...)'. | MissedFirstOrDefaultOpportunity.cs:24:17:24:25 | ... > ... | returns the first sequence element satisfying a predicate |
33
| MissedFirstOrDefaultOpportunity.cs:36:9:40:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider finding the element explicitly using '.FirstOrDefault(...)'. | MissedFirstOrDefaultOpportunity.cs:38:17:38:25 | ... > ... | returns the first sequence element satisfying a predicate |
4+
| MissedFirstOrDefaultOpportunity.cs:149:9:153:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider finding the element explicitly using '.FirstOrDefault(...)'. | MissedFirstOrDefaultOpportunity.cs:151:17:151:32 | ... > ... | returns the first sequence element satisfying a predicate |
5+
| MissedFirstOrDefaultOpportunity.cs:161:9:165:9 | foreach (... ... in ...) ... | This foreach loop $@ - consider finding the element explicitly using '.FirstOrDefault(...)'. | MissedFirstOrDefaultOpportunity.cs:163:17:163:25 | ... > ... | returns the first sequence element satisfying a predicate |

0 commit comments

Comments
 (0)