-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(csharp): adds a missed first or default opprtunity rule #22485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
baywet
wants to merge
12
commits into
github:main
Choose a base branch
from
baywet:feat/csharp-missed-firstordefault-opprtunity
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ed15edf
feat(csharp): adds a missed first or default opprtunity rule
baywet 41a9a16
fix: handle non null default values in the rule
baywet baab759
chore: applies review suggestion
baywet a52c2cb
chore: applies review suggestion
baywet 6744d56
chore: reverts addition to ruleset
baywet c97f0f4
chore: applies review suggestions
baywet 6525503
chore: linting
baywet 543ead4
C#: Add change-note.
michaelnebel 2eb16da
C#: Update integration test expected output.
michaelnebel 6d867e1
C#: Address some review comments.
michaelnebel 1617ace
chore: applies review suggestions
baywet 63a5e8a
chore: applies review suggestions
baywet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| class MissedFirstOrDefaultOpportunity | ||
| { | ||
| public static Operation FindOperation(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| class Operation | ||
| { | ||
| public string OperationId { get; set; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <!DOCTYPE qhelp PUBLIC | ||
| "-//Semmle//qhelp//EN" | ||
| "qhelp.dtd"> | ||
| <qhelp> | ||
| <overview> | ||
| <p>Programmers sometimes search a sequence by iterating over each element, testing it, and returning | ||
| the first element that satisfies the test. If the loop completes without finding a match, the method | ||
| then returns a default value such as <code>null</code> or <code>default</code>.</p> | ||
|
|
||
| </overview> | ||
| <recommendation> | ||
| <p>This pattern is directly available as the <code>FirstOrDefault</code> method in LINQ. Using the | ||
| library method makes the search intent explicit and avoids manually spelling out the loop and | ||
| fallback return.</p> | ||
|
|
||
| </recommendation> | ||
| <example> | ||
| <p>In this example the method searches a list of operations for the first operation with a matching | ||
| identifier, returning <code>null</code> if no match is found.</p> | ||
| <sample src="MissedFirstOrDefaultOpportunity.cs" /> | ||
|
|
||
| <p>The LINQ <code>FirstOrDefault</code> method can express this search more directly.</p> | ||
| <sample src="MissedFirstOrDefaultOpportunityFix.cs" /> | ||
|
|
||
| <p>The following examples should not use <code>FirstOrDefault</code>, because they do more than | ||
| return the matching element or because the fallback value is not the default value.</p> | ||
| <sample src="MissedFirstOrDefaultOpportunityGood.cs" /> | ||
|
|
||
| </example> | ||
| <references> | ||
|
|
||
| <li>MSDN: <a href="https://learn.microsoft.com/dotnet/api/system.linq.enumerable.firstordefault">Enumerable.FirstOrDefault Method</a>.</li> | ||
|
|
||
|
|
||
| </references> | ||
| </qhelp> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * @name Missed opportunity to use FirstOrDefault | ||
| * @description The intent of a foreach loop that returns the first sequence element satisfying a predicate, or a default value otherwise, | ||
| * can often be better expressed using LINQ's 'FirstOrDefault' method. | ||
| * @kind problem | ||
| * @problem.severity recommendation | ||
| * @precision high | ||
| * @id cs/linq/missed-firstordefault | ||
| * @tags quality | ||
| * maintainability | ||
| * readability | ||
| * language-features | ||
| */ | ||
|
|
||
| import csharp | ||
| import Linq.Helpers | ||
|
|
||
| from ForeachStmtGenericEnumerable fes, IfStmt is | ||
| where missedFirstOrDefaultOpportunity(fes, is) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @michaelnebel it seems that the changes you suggested re-activated this finding. What would be the best path forward here? |
||
| select fes, | ||
| "This foreach loop returns the first sequence element satisfying a $@ - consider finding the element explicitly using '.FirstOrDefault(...)'.", | ||
| is.getCondition(), "predicate" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
|
|
||
| class MissedFirstOrDefaultOpportunityFix | ||
| { | ||
| public static Operation FindOperation(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| return operations.FirstOrDefault(operation => | ||
| string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| class MissedFirstOrDefaultOpportunityGood | ||
| { | ||
| public static Operation FindOperationOrThrow(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| throw new InvalidOperationException("Unexpected operation."); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public static Operation FindReplacementOperation(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation; | ||
| } | ||
|
|
||
| return new Operation(); | ||
| } | ||
|
|
||
| public static string FindOperationId(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation.OperationId; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } |
4 changes: 4 additions & 0 deletions
4
csharp/ql/src/change-notes/2026-09-03-missed-firstordefault.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| --- | ||
| category: newQuery | ||
| --- | ||
| * Added a new query, `cs/linq/missed-firstordefault`, that detects `foreach` loops that can be expressed more clearly using LINQ's `FirstOrDefault` method. |
177 changes: 177 additions & 0 deletions
177
.../test/query-tests/Linq/MissedFirstOrDefaultOpportunity/MissedFirstOrDefaultOpportunity.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
|
|
||
| class MissedFirstOrDefaultOpportunity | ||
| { | ||
| public Operation M1(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // BAD: Can be replaced with operations.FirstOrDefault(operation => ...). | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation; | ||
| } // $ Alert | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public int M2(IEnumerable<int> values) | ||
| { | ||
| // BAD: Can be replaced with values.FirstOrDefault(value => ...). | ||
| foreach (var value in values) | ||
| { | ||
| if (value > 0) | ||
| { | ||
| return value; | ||
| } | ||
| } // $ Alert | ||
|
|
||
| return default; | ||
| } | ||
|
|
||
| public int? M3(List<int> values) | ||
| { | ||
| // BAD: Can be replaced with values.FirstOrDefault(value => ...). | ||
| foreach (var value in values) | ||
| { | ||
| if (value > 0) | ||
| return value; | ||
| } // $ Alert | ||
|
|
||
| return default(int); | ||
| } | ||
|
|
||
| public Operation M4(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: FirstOrDefault does not throw when a match is found. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| throw new InvalidOperationException(); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public Operation M5(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: FirstOrDefault would return null/default when no match is found. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation; | ||
| } | ||
|
|
||
| return new Operation(); | ||
| } | ||
|
|
||
| public string M6(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: FirstOrDefault would return the matching operation, not one of its properties. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation.OperationId; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public Operation M7(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: The matched case has an additional side effect. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| { | ||
| Console.WriteLine(operation.OperationId); | ||
| return operation; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public async Task<Operation> M8(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: FirstOrDefault does not support an async predicate. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (await IsMatch(operation, operationId)) | ||
| return operation; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public Operation M9(IEnumerable<Operation> operations, string operationId) | ||
| { | ||
| // GOOD: FirstOrDefault does not have an equivalent for an else branch in the loop. | ||
| foreach (var operation in operations) | ||
| { | ||
| if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)) | ||
| return operation; | ||
| else | ||
| return null; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public object M10(IEnumerable<int> values) | ||
| { | ||
| // GOOD: FirstOrDefault would return boxed 0 when no match is found, not null. | ||
| foreach (var value in values) | ||
| { | ||
| if (value > 0) | ||
| return value; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public object M11(IEnumerable<int> values) | ||
| { | ||
| // GOOD: FirstOrDefault would return boxed 0 when no match is found, not default(object). | ||
| foreach (var value in values) | ||
| { | ||
| if (value > 0) | ||
| return value; | ||
| } | ||
|
|
||
| return default(object); | ||
| } | ||
|
|
||
| public object M12(IEnumerable<string> values) | ||
| { | ||
| // BAD: FirstOrDefault returns null for missing reference-type elements, matching the fallback. | ||
| foreach (var value in values) | ||
| { | ||
| if (value.Length > 0) | ||
| return value; | ||
| } // $ Alert | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| public object M13(IEnumerable<int> values) | ||
| { | ||
| // BAD: FirstOrDefault returns 0 for missing int elements, matching the fallback before boxing. | ||
| foreach (var value in values) | ||
| { | ||
| if (value > 0) | ||
| return value; | ||
| } // $ Alert | ||
|
|
||
| return default(int); | ||
| } | ||
|
|
||
| private static Task<bool> IsMatch(Operation operation, string operationId) => | ||
| Task.FromResult(string.Equals(operation.OperationId, operationId, StringComparison.Ordinal)); | ||
| } | ||
|
|
||
| class Operation | ||
| { | ||
| public string OperationId { get; set; } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.