Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,18 @@ In many situations, logging is disabled or set to a log level that results in an
|CodeFix|True|
---

## [CA1876](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876): Do not use 'AsParallel' in 'foreach'

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.

|Item|Value|
|-|-|
|Category|Performance|
|Enabled|True|
|Severity|Info|
|CodeFix|False|
---

## [CA2000](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000): Dispose objects before losing scope

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.
Expand Down Expand Up @@ -2166,6 +2178,18 @@ JsonDocument implements IDisposable and needs to be properly disposed. When only
|CodeFix|True|
---

## [CA2027](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027): Cancel Task.Delay after Task.WhenAny completes

When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.

|Item|Value|
|-|-|
|Category|Reliability|
|Enabled|True|
|Severity|Info|
|CodeFix|False|
---

## [CA2100](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100): Review SQL queries for security vulnerabilities

SQL queries that directly use user input can be vulnerable to SQL injection attacks. Review this SQL query for potential vulnerabilities, and consider using a parameterized SQL query.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3507,6 +3507,26 @@
]
}
},
"CA1876": {
"id": "CA1876",
"shortDescription": "Do not use 'AsParallel' in 'foreach'",
"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.",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876",
"properties": {
"category": "Performance",
"isEnabledByDefault": true,
"typeName": "DoNotUseAsParallelInForEachLoopAnalyzer",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2000": {
"id": "CA2000",
"shortDescription": "Dispose objects before losing scope",
Expand Down Expand Up @@ -3870,6 +3890,26 @@
]
}
},
"CA2027": {
"id": "CA2027",
"shortDescription": "Cancel Task.Delay after Task.WhenAny completes",
"fullDescription": "When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.",
"defaultLevel": "note",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027",
"properties": {
"category": "Reliability",
"isEnabledByDefault": true,
"typeName": "DoNotUseNonCancelableTaskDelayWithWhenAny",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2100": {
"id": "CA2100",
"shortDescription": "Review SQL queries for security vulnerabilities",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Rule ID | Category | Severity | Notes
CA1873 | Performance | Info | AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
CA1874 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1874)
CA1875 | Performance | Info | UseRegexMembers, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)
CA1876 | Performance | Info | DoNotUseAsParallelInForEachLoopAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876)
CA2023 | Reliability | Warning | LoggerMessageDefineAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2023)
CA2024 | Reliability | Warning | DoNotUseEndOfStreamInAsyncMethods, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2024)
CA2025 | Reliability | Disabled | DoNotPassDisposablesIntoUnawaitedTasksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2025)
CA2026 | Reliability | Info | PreferJsonElementParse, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026)
CA2027 | Reliability | Info | DoNotUseNonCancelableTaskDelayWithWhenAny, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027)
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@
<data name="DoNotUseCountWhenAnyCanBeUsedTitle" xml:space="preserve">
<value>Do not use Count() or LongCount() when Any() can be used</value>
</data>
<data name="DoNotUseAsParallelInForEachLoopTitle" xml:space="preserve">
<value>Do not use 'AsParallel' in 'foreach'</value>
</data>
<data name="DoNotUseAsParallelInForEachLoopMessage" xml:space="preserve">
<value>Using 'AsParallel()' directly in a 'foreach' loop has no effect and the loop is not parallelized</value>
</data>
<data name="DoNotUseAsParallelInForEachLoopDescription" xml:space="preserve">
<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>
</data>
<data name="PreferConvertToHexStringOverBitConverterTitle" xml:space="preserve">
<value>Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'</value>
</data>
Expand Down Expand Up @@ -1637,6 +1646,15 @@
<data name="DoNotUseWhenAllWithSingleTaskFix" xml:space="preserve">
<value>Replace 'WhenAll' call with argument</value>
</data>
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyTitle" xml:space="preserve">
<value>Cancel Task.Delay after Task.WhenAny completes</value>
</data>
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyMessage" xml:space="preserve">
<value>Using Task.WhenAny with Task.Delay may result in a timer continuing to run after the operation completes, wasting resources</value>
</data>
<data name="DoNotUseNonCancelableTaskDelayWithWhenAnyDescription" xml:space="preserve">
<value>When Task.Delay is used with Task.WhenAny to implement a timeout, the timer created by Task.Delay continues to run even after WhenAny completes, wasting resources. If your target framework supports Task.WaitAsync, use that instead as it has built-in timeout support without leaving timers running. Otherwise, pass a CancellationToken to Task.Delay that can be canceled when the operation completes.</value>
</data>
<data name="UseStringEqualsOverStringCompareCodeFixTitle" xml:space="preserve">
<value>Use 'string.Equals'</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Performance
{
using static MicrosoftNetCoreAnalyzersResources;

/// <summary>
/// CA1876: <inheritdoc cref="DoNotUseAsParallelInForEachLoopTitle"/>
/// Analyzer to detect misuse of AsParallel() when used directly in a foreach loop.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DoNotUseAsParallelInForEachLoopAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1876";

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopTitle)),
CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
description: CreateLocalizableResourceString(nameof(DoNotUseAsParallelInForEachLoopDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
var typeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);

// Get the ParallelEnumerable type
var parallelEnumerableType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqParallelEnumerable);

if (parallelEnumerableType == null)
{
return;
}

// Get all AsParallel methods - use SymbolEqualityComparer for proper comparison
var asParallelMethods = ImmutableHashSet.CreateRange<IMethodSymbol>(
SymbolEqualityComparer.Default,
parallelEnumerableType.GetMembers("AsParallel").OfType<IMethodSymbol>());

if (asParallelMethods.IsEmpty)
{
return;
}

context.RegisterOperationAction(ctx => AnalyzeForEachLoop(ctx, asParallelMethods), OperationKind.Loop);
}

private static void AnalyzeForEachLoop(OperationAnalysisContext context, ImmutableHashSet<IMethodSymbol> asParallelMethods)
{
if (context.Operation is not IForEachLoopOperation forEachLoop)
{
return;
}

// Check if the collection is a direct result of AsParallel()
var collection = forEachLoop.Collection;

// Walk up conversions to find the actual operation
while (collection is IConversionOperation conversion)
{
collection = conversion.Operand;
}

// Check if this is an invocation of AsParallel
if (collection is IInvocationOperation invocation)
{
var targetMethod = invocation.TargetMethod;

// For extension methods, we need to check the ReducedFrom or the original method
var methodToCheck = targetMethod.ReducedFrom ?? targetMethod;

if (asParallelMethods.Contains(methodToCheck.OriginalDefinition))
{
// Report diagnostic on the AsParallel call
context.ReportDiagnostic(invocation.CreateDiagnostic(Rule));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.Lightup;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources;

namespace Microsoft.NetCore.Analyzers.Tasks
{
/// <summary>
/// CA2027: <inheritdoc cref="DoNotUseNonCancelableTaskDelayWithWhenAnyTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DoNotUseNonCancelableTaskDelayWithWhenAny : DiagnosticAnalyzer
{
internal const string RuleId = "CA2027";

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyTitle)),
CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyMessage)),
DiagnosticCategory.Reliability,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyDescription)),
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterCompilationStartAction(context =>
{
var compilation = context.Compilation;

if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask, out var taskType) ||
!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingCancellationToken, out var cancellationTokenType))
{
return;
}

context.RegisterOperationAction(context =>
{
var invocation = (IInvocationOperation)context.Operation;

// Check if this is a call to Task.WhenAny
var method = invocation.TargetMethod;
if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, taskType) ||
!method.IsStatic ||
method.Name != nameof(Task.WhenAny))
{
return;
}

// Count the total number of tasks passed to WhenAny
int taskCount = 0;
List<IOperation>? taskDelayOperations = null;

// Task.WhenAny has params parameters, so arguments are often implicitly wrapped in an array
// We need to check inside the array initializer or collection expression
for (int i = 0; i < invocation.Arguments.Length; i++)
{
var argument = invocation.Arguments[i].Value.WalkDownConversion();

// Check if this is an array creation
if (argument is IArrayCreationOperation { Initializer: not null } arrayCreation)
{
// Check each element in the array
foreach (var element in arrayCreation.Initializer.ElementValues)
{
taskCount++;
if (IsNonCancelableTaskDelay(element, taskType, cancellationTokenType))
{
(taskDelayOperations ??= []).Add(element);
}
}
}
else if (ICollectionExpressionOperationWrapper.IsInstance(argument))
{
// Check each element in the collection expression
var collectionExpression = ICollectionExpressionOperationWrapper.FromOperation(argument);
foreach (var element in collectionExpression.Elements)
{
taskCount++;
if (IsNonCancelableTaskDelay(element, taskType, cancellationTokenType))
{
(taskDelayOperations ??= []).Add(element);
}
}
}
else
{
// Direct argument (not params or array)
taskCount++;
if (IsNonCancelableTaskDelay(argument, taskType, cancellationTokenType))
{
(taskDelayOperations ??= []).Add(argument);
}
}
}

// Only report diagnostics if there are at least 2 tasks total
// (avoid flagging Task.WhenAny(Task.Delay(...)) which may be used to avoid exceptions)
if (taskCount >= 2 && taskDelayOperations is not null)
{
foreach (var operation in taskDelayOperations)
{
context.ReportDiagnostic(operation.CreateDiagnostic(Rule));
}
}
}, OperationKind.Invocation);
});
}

private static bool IsNonCancelableTaskDelay(IOperation operation, INamedTypeSymbol taskType, INamedTypeSymbol cancellationTokenType)
{
operation = operation.WalkDownConversion();

if (operation is not IInvocationOperation invocation)
{
return false;
}

// Check if this is Task.Delay
var method = invocation.TargetMethod;
if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, taskType) ||
!method.IsStatic ||
method.Name != nameof(Task.Delay))
{
return false;
}

// Check if any parameter is a CancellationToken, in which case we consider it cancelable
foreach (var parameter in method.Parameters)
{
if (SymbolEqualityComparer.Default.Equals(parameter.Type, cancellationTokenType))
{
return false;
}
}

return true; // Task.Delay without CancellationToken
}
}
}
Loading
Loading