Skip to content

Commit 448facf

Browse files
Copilotstephentoub
andauthored
Add CA1876 analyzer to detect misuse of AsParallel() in foreach loops (#51287)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]>
1 parent ba0db14 commit 448facf

21 files changed

+583
-1
lines changed

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,6 +1908,18 @@ In many situations, logging is disabled or set to a log level that results in an
19081908
|CodeFix|True|
19091909
---
19101910

1911+
## [CA1876](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876): Do not use 'AsParallel' in 'foreach'
1912+
1913+
Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.
1914+
1915+
|Item|Value|
1916+
|-|-|
1917+
|Category|Performance|
1918+
|Enabled|True|
1919+
|Severity|Info|
1920+
|CodeFix|False|
1921+
---
1922+
19111923
## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope
19121924

19131925
If a disposable object is not explicitly disposed before all references to it are out of scope, the object will be disposed at some indeterminate time when the garbage collector runs the finalizer of the object. Because an exceptional event might occur that will prevent the finalizer of the object from running, the object should be explicitly disposed instead.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers.sarif

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,6 +3507,26 @@
35073507
]
35083508
}
35093509
},
3510+
"CA1876": {
3511+
"id": "CA1876",
3512+
"shortDescription": "Do not use 'AsParallel' in 'foreach'",
3513+
"fullDescription": "Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.",
3514+
"defaultLevel": "note",
3515+
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876",
3516+
"properties": {
3517+
"category": "Performance",
3518+
"isEnabledByDefault": true,
3519+
"typeName": "DoNotUseAsParallelInForEachLoopAnalyzer",
3520+
"languages": [
3521+
"C#",
3522+
"Visual Basic"
3523+
],
3524+
"tags": [
3525+
"Telemetry",
3526+
"EnabledRuleInAggressiveMode"
3527+
]
3528+
}
3529+
},
35103530
"CA2000": {
35113531
"id": "CA2000",
35123532
"shortDescription": "Dispose objects before losing scope",

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Rule ID | Category | Severity | Notes
77
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
88
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
99
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
10+
CA1876 | Performance | Info | DoNotUseAsParallelInForEachLoopAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
1011
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
1112
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
1213
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@
195195
<data name="DoNotUseCountWhenAnyCanBeUsedTitle" xml:space="preserve">
196196
<value>Do not use Count() or LongCount() when Any() can be used</value>
197197
</data>
198+
<data name="DoNotUseAsParallelInForEachLoopTitle" xml:space="preserve">
199+
<value>Do not use 'AsParallel' in 'foreach'</value>
200+
</data>
201+
<data name="DoNotUseAsParallelInForEachLoopMessage" xml:space="preserve">
202+
<value>Using 'AsParallel()' directly in a 'foreach' loop has no effect and the loop is not parallelized</value>
203+
</data>
204+
<data name="DoNotUseAsParallelInForEachLoopDescription" xml:space="preserve">
205+
<value>Using 'AsParallel()' directly in a 'foreach' loop has no effect. The 'foreach' statement iterates serially through the collection regardless. To parallelize LINQ operations, call 'AsParallel()' earlier in the query chain before other LINQ operators. To parallelize the loop itself, use 'Parallel.ForEach' instead.</value>
206+
</data>
198207
<data name="PreferConvertToHexStringOverBitConverterTitle" xml:space="preserve">
199208
<value>Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'</value>
200209
</data>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using Analyzer.Utilities;
6+
using Analyzer.Utilities.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Operations;
10+
11+
namespace Microsoft.NetCore.Analyzers.Performance
12+
{
13+
using static MicrosoftNetCoreAnalyzersResources;
14+
15+
/// <summary>
16+
/// CA1876: <inheritdoc cref="DoNotUseAsParallelInForEachLoopTitle"/>
17+
/// Analyzer to detect misuse of AsParallel() when used directly in a foreach loop.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
20+
public sealed class DoNotUseAsParallelInForEachLoopAnalyzer : DiagnosticAnalyzer
21+
{
22+
internal const string RuleId = "CA1876";
23+
24+
internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
25+
RuleId,
26+
CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopTitle)),
27+
CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopMessage)),
28+
DiagnosticCategory.Performance,
29+
RuleLevel.IdeSuggestion,
30+
description: CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopDescription)),
31+
isPortedFxCopRule: false,
32+
isDataflowRule: false);
33+
34+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
35+
36+
public override void Initialize(AnalysisContext context)
37+
{
38+
context.EnableConcurrentExecution();
39+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
40+
context.RegisterCompilationStartAction(OnCompilationStart);
41+
}
42+
43+
private static void OnCompilationStart(CompilationStartAnalysisContext context)
44+
{
45+
var typeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);
46+
47+
// Get the ParallelEnumerable type
48+
var parallelEnumerableType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqParallelEnumerable);
49+
50+
if (parallelEnumerableType == null)
51+
{
52+
return;
53+
}
54+
55+
// Get all AsParallel methods - use SymbolEqualityComparer for proper comparison
56+
var asParallelMethods = ImmutableHashSet.CreateRange<IMethodSymbol>(
57+
SymbolEqualityComparer.Default,
58+
parallelEnumerableType.GetMembers("AsParallel").OfType<IMethodSymbol>());
59+
60+
if (asParallelMethods.IsEmpty)
61+
{
62+
return;
63+
}
64+
65+
context.RegisterOperationAction(ctx => AnalyzeForEachLoop(ctx, asParallelMethods), OperationKind.Loop);
66+
}
67+
68+
private static void AnalyzeForEachLoop(OperationAnalysisContext context, ImmutableHashSet<IMethodSymbol> asParallelMethods)
69+
{
70+
if (context.Operation is not IForEachLoopOperation forEachLoop)
71+
{
72+
return;
73+
}
74+
75+
// Check if the collection is a direct result of AsParallel()
76+
var collection = forEachLoop.Collection;
77+
78+
// Walk up conversions to find the actual operation
79+
while (collection is IConversionOperation conversion)
80+
{
81+
collection = conversion.Operand;
82+
}
83+
84+
// Check if this is an invocation of AsParallel
85+
if (collection is IInvocationOperation invocation)
86+
{
87+
var targetMethod = invocation.TargetMethod;
88+
89+
// For extension methods, we need to check the ReducedFrom or the original method
90+
var methodToCheck = targetMethod.ReducedFrom ?? targetMethod;
91+
92+
if (asParallelMethods.Contains(methodToCheck.OriginalDefinition))
93+
{
94+
// Report diagnostic on the AsParallel call
95+
context.ReportDiagnostic(invocation.CreateDiagnostic(Rule));
96+
}
97+
}
98+
}
99+
}
100+
}

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.cs.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.de.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.es.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.fr.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/xlf/MicrosoftNetCoreAnalyzersResources.it.xlf

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)