Skip to content
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

Detection of PLINQ nops analyzer #4126

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions src/Microsoft.NetCore.Analyzers/Microsoft.NetCore.Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,18 @@ Calls to 'string.IndexOf' where the result is used to check for the presence/abs
|CodeFix|True|
---

## [CA2250](https://docs.microsoft.com/visualstudio/code-quality/ca2250): 'AsParallel' at the end of a LINQ query does not have an effect
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After merging the current master into your branch, you will need to re-run msbuild pack.

The documentation links have moved from VS docs to .NET docs.
In current master you'll see these links are from .NET docs (while your branch is still on the old VS docs links).


'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.

|Item|Value|
|-|-|
|Category|Usage|
|Enabled|True|
|Severity|Warning|
|CodeFix|True|
---

## [CA2300](https://docs.microsoft.com/visualstudio/code-quality/ca2300): Do not use insecure deserializer BinaryFormatter

The method '{0}' is insecure when deserializing untrusted data. If you need to instead detect BinaryFormatter deserialization without a SerializationBinder set, then disable rule CA2300, and enable rules CA2301 and CA2302.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,25 @@
]
}
},
"CA2250": {
"id": "CA2250",
"shortDescription": "'AsParallel' at the end of a LINQ query does not have an effect",
"fullDescription": "'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.",
"defaultLevel": "warning",
"helpUri": "https://docs.microsoft.com/visualstudio/code-quality/ca2250",
"properties": {
"category": "Usage",
"isEnabledByDefault": true,
"typeName": "DetectPLINQNopsAnalyzer",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry"
]
}
},
"CA2300": {
"id": "CA2300",
"shortDescription": "Do not use insecure deserializer BinaryFormatter",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

Rule ID | Missing Help Link | Title |
--------|-------------------|-------|
CA1416 | https://docs.microsoft.com/visualstudio/code-quality/ca1416 | Validate platform compatibility |
CA2250 | https://docs.microsoft.com/visualstudio/code-quality/ca2250 | 'AsParallel' at the end of a LINQ query does not have an effect |
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.NetAnalyzers.UnitTests" />
</ItemGroup>
<ItemGroup>
<Folder Include="Microsoft.NetCore.Analyzers\Usage\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.NetCore.Analyzers;
using Microsoft.NetCore.Analyzers.Performance;

namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = DetectPLINQNopsAnalyzer.RuleId), Shared]
public sealed class DetectPLINQNopsFixer : CodeFixProvider
{
private static readonly string[] s_removableEnds = new string[] { "ToList", "ToArray", "AsParallel", "ToDictionary", "ToHashSet" };

private static readonly string[] s_requiredAppendableEnds = new string[] { "ToDictionary", "ToHashSet" };

public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DetectPLINQNopsAnalyzer.RuleId);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

var node = root.FindNode(context.Span);
if (node is not InvocationExpressionSyntax declaration)
{
return;
}

context.RegisterCodeFix(
new AsParallelCodeAction(
title: MicrosoftNetCoreAnalyzersResources.RemoveRedundantCall,
createChangedSolution: c => RemoveAsParallelCall(context.Document, declaration, c)),
context.Diagnostics);
}

private static async Task<Solution> RemoveAsParallelCall(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken)
{
var originalSolution = document.Project.Solution;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
ExpressionSyntax possibleInvocation = invocationExpression;

do
{
var newExpression = ((possibleInvocation as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Expression;
possibleInvocation = newExpression;
} while (possibleInvocation is InvocationExpressionSyntax nestedInvocation && nestedInvocation.Expression is MemberAccessExpressionSyntax member && s_removableEnds.Contains(member.Name.Identifier.ValueText));

if (invocationExpression.Expression is MemberAccessExpressionSyntax directMember && s_requiredAppendableEnds.Contains(directMember.Name.Identifier.ValueText))
{
possibleInvocation = SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, possibleInvocation, directMember.Name), invocationExpression.ArgumentList);
}

return originalSolution.WithDocumentSyntaxRoot(document.Id, root.ReplaceNode(invocationExpression, possibleInvocation));
}

private class AsParallelCodeAction : SolutionChangeAction
{
public AsParallelCodeAction(string title, Func<CancellationToken, Task<Solution>> createChangedSolution)
: base(title, createChangedSolution, title)
{
}
}
}
}
4 changes: 4 additions & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
; Please do not edit this file manually, it should only be updated through code fix application.
### New Rules
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA1839 | Performance | Warning | DetectPLINQNopsAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca2250)
Original file line number Diff line number Diff line change
Expand Up @@ -1482,4 +1482,13 @@
<data name="PreferStringContainsOverIndexOfCodeFixTitle" xml:space="preserve">
<value>Replace with 'string.Contains'</value>
</data>
<data name="DetectPLINQNopsDescription" xml:space="preserve">
<value>'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</value>
</data>
<data name="DetectPLINQNopsMessage" xml:space="preserve">
<value>'AsParallel' has no effect within a foreach statement</value>
</data>
<data name="DetectPLINQNopsTitle" xml:space="preserve">
<value>'AsParallel' at the end of a LINQ query does not have an effect</value>
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
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
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DetectPLINQNopsAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA1839";
internal static readonly LocalizableString localizableTitle = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DetectPLINQNopsTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));

private static readonly LocalizableString s_localizableMessageDefault = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DetectPLINQNopsMessage), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DetectPLINQNopsDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));

internal static readonly DiagnosticDescriptor DefaultRule = DiagnosticDescriptorHelper.Create(RuleId,
localizableTitle,
s_localizableMessageDefault,
DiagnosticCategory.Performance,
RuleLevel.BuildWarning,
description: s_localizableDescription,
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DefaultRule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(ctx =>
{
if (!ctx.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqParallelEnumerable, out var parallelEnumerable) || !ctx.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqEnumerable, out var linqEnumerable))
{
return;
}

var asParallelSymbols = parallelEnumerable.GetMembers("AsParallel").ToImmutableHashSet();
var collectionSymbols = parallelEnumerable.GetMembers("ToArray")
.Concat(parallelEnumerable.GetMembers("ToList"))
.Concat(parallelEnumerable.GetMembers("ToDictionary"))
.Concat(linqEnumerable.GetMembers("ToHashSet"))
.ToImmutableHashSet();

ctx.RegisterOperationAction(x => AnalyzeOperation(x, asParallelSymbols, collectionSymbols), OperationKind.Invocation);
});
}

public static bool ParentIsForEachStatement(IInvocationOperation operation) => operation.Parent is IForEachLoopOperation || operation.Parent?.Parent is IForEachLoopOperation;

private static bool FindFirstInvocationParent(IOperation operation, [NotNullWhen(true)] out IInvocationOperation? invocationOperation)
{
invocationOperation = null;
do
{
operation = operation.Parent;
if (operation is IInvocationOperation invocation)
{
invocationOperation = invocation;
return true;
}

if (operation is ILocalFunctionOperation or IAnonymousFunctionOperation)
return false;
} while (operation != null);

return false;
}

public static bool TryGetParentIsToCollection(IInvocationOperation operation, ImmutableHashSet<ISymbol> collectionSymbols, out IInvocationOperation parentInvocation)
{
parentInvocation = operation;
var hasParentInvocation = FindFirstInvocationParent(operation, out var invocationParent);
if (!hasParentInvocation)
{
return false;
}

var targetMethod = (invocationParent!.TargetMethod.ReducedFrom ?? invocationParent.TargetMethod).OriginalDefinition;
if (collectionSymbols.Contains(targetMethod))
{
parentInvocation = invocationParent;
return true;
}

return false;
}

private static void AnalyzeOperation(OperationAnalysisContext context, ImmutableHashSet<ISymbol> asParallelSymbols, ImmutableHashSet<ISymbol> collectionSymbols)
{
var invocation = (IInvocationOperation)context.Operation;
var reducedMethod = invocation.TargetMethod.OriginalDefinition;
if (reducedMethod is null)
{
return;
}

if (!(asParallelSymbols.Contains(reducedMethod) || asParallelSymbols.Contains(reducedMethod.ReducedFrom)))
{
return;
}

IInvocationOperation? diagnosticInvocation = null;
if (!ParentIsForEachStatement(invocation))
{
if (!TryGetParentIsToCollection(invocation, collectionSymbols, out var parentInvocation) || !ParentIsForEachStatement(parentInvocation))
{
return;
}

diagnosticInvocation = parentInvocation;
}

diagnosticInvocation ??= invocation;
var diagnostic = diagnosticInvocation.CreateDiagnostic(DefaultRule);
context.ReportDiagnostic(diagnostic);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
<target state="translated">Nepoužívat zastaralé hodnoty SslProtocols</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsDescription">
<source>'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsMessage">
<source>'AsParallel' has no effect within a foreach statement</source>
<target state="new">'AsParallel' has no effect within a foreach statement</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query does not have an effect</source>
<target state="new">'AsParallel' at the end of a LINQ query does not have an effect</target>
<note />
</trans-unit>
<trans-unit id="DisposableFieldsShouldBeDisposedDescription">
<source>A type that implements System.IDisposable declares fields that are of types that also implement IDisposable. The Dispose method of the field is not called by the Dispose method of the declaring type. To fix a violation of this rule, call Dispose on fields that are of types that implement IDisposable if you are responsible for allocating and releasing the unmanaged resources held by the field.</source>
<target state="translated">Typ, který implementuje System.IDisposable, deklaruje pole, která mají typ, který IDisposable implementuje taky. Metoda Dispose deklarujícího typu nevolá metodu Dispose daného pole. Pokud chcete porušení tohoto pravidla opravit a je vaší odpovědností přidělovat a uvolňovat nespravované prostředky, které pole uchovává, zavolejte Dispose pro pole s typy, které implementují IDisposable.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
<target state="translated">Keine veralteten SslProtocols-Werte verwenden</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsDescription">
<source>'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsMessage">
<source>'AsParallel' has no effect within a foreach statement</source>
<target state="new">'AsParallel' has no effect within a foreach statement</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query does not have an effect</source>
<target state="new">'AsParallel' at the end of a LINQ query does not have an effect</target>
<note />
</trans-unit>
<trans-unit id="DisposableFieldsShouldBeDisposedDescription">
<source>A type that implements System.IDisposable declares fields that are of types that also implement IDisposable. The Dispose method of the field is not called by the Dispose method of the declaring type. To fix a violation of this rule, call Dispose on fields that are of types that implement IDisposable if you are responsible for allocating and releasing the unmanaged resources held by the field.</source>
<target state="translated">Ein Typ, der System.IDisposable implementiert, deklariert Felder, die Typen aufweisen, die ebenfalls IDisposable implementieren. Die Dispose-Methode des Felds wird nicht von der Dispose-Methode des deklarierenden Typs aufgerufen. Wenn Sie für das Zuordnen und Freigeben nicht verwalteter Ressourcen zuständig sind, die von diesem Feld belegt werden, rufen Sie zum Beheben eines Verstoßes gegen diese Regel Dispose für Felder auf, die Typen aufweisen, welche IDisposable implementieren.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
<target state="translated">No usar valores de SslProtocols en desuso</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsDescription">
<source>'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' does not have an effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsMessage">
<source>'AsParallel' has no effect within a foreach statement</source>
<target state="new">'AsParallel' has no effect within a foreach statement</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query does not have an effect</source>
<target state="new">'AsParallel' at the end of a LINQ query does not have an effect</target>
<note />
</trans-unit>
<trans-unit id="DisposableFieldsShouldBeDisposedDescription">
<source>A type that implements System.IDisposable declares fields that are of types that also implement IDisposable. The Dispose method of the field is not called by the Dispose method of the declaring type. To fix a violation of this rule, call Dispose on fields that are of types that implement IDisposable if you are responsible for allocating and releasing the unmanaged resources held by the field.</source>
<target state="translated">Un tipo que implementa System.IDisposable declara campos de tipos que también implementan IDisposable. El método Dispose del tipo declarativo no llama al método Dispose del campo. Para corregir una infracción de esta regla, llame a Dispose en campos que sean de tipos que implementan IDisposable si usted es el responsable de asignar y liberar los recursos no administrados que contiene el campo.</target>
Expand Down
Loading