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 17 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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.Syntax;
using Microsoft.NetCore.Analyzers;
using Microsoft.NetCore.Analyzers.Usage;

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

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,
Copy link
Member

Choose a reason for hiding this comment

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

Not sure we do want to reuse this message or create a new specific one.

Copy link
Author

Choose a reason for hiding this comment

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

I have no idea, I'll adapt this to whatever is decided. It seemed to accurately reflect what the code fix does so I just chose that

createChangedSolution: c => RemoveAsParallelCall(context.Document, declaration, c),
equivalenceKey: MicrosoftNetCoreAnalyzersResources.RemoveRedundantCall),
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
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 = ((MemberAccessExpressionSyntax)((InvocationExpressionSyntax)possibleInvocation).Expression)!.Expression;
possibleInvocation = newExpression;
} while (possibleInvocation is InvocationExpressionSyntax nestedInvocation && nestedInvocation.Expression is MemberAccessExpressionSyntax member && removableEnds.Contains(member.Name.Identifier.ValueText));

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

private class AsParallelCodeAction : SolutionChangeAction
{
public AsParallelCodeAction(string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string equivalenceKey)
: base(title, createChangedSolution, equivalenceKey)
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
{
}
}
}
}
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
--------|----------|----------|-------
CA2250 | Usage | Info | 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' in expression '{0}' has no effect</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,99 @@
// 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.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Usage
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DetectPLINQNopsAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA2250";
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.Usage,
RuleLevel.IdeSuggestion,
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))
{
return;
}

var asParallelSymbols = parallelEnumerable.GetMembers("AsParallel").ToImmutableHashSet();
var collectionSymbols = parallelEnumerable.GetMembers("ToArray").Concat(parallelEnumerable.GetMembers("ToList")).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;

public static bool TryGetParentIsToArrayOrToList(IInvocationOperation operation, ImmutableHashSet<ISymbol> collectionSymbols, out IInvocationOperation parentInvocation)
{
parentInvocation = operation;
if (operation.Parent?.Parent is not IInvocationOperation invocation)
{
return false;
}
if (collectionSymbols.Contains(invocation.TargetMethod.OriginalDefinition))
{
parentInvocation = invocation;
return true;
}

return false;
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
}

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))
{
return;
}

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

diagnosticInvocation = parentInvocation;
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
}

diagnosticInvocation ??= invocation;
var diagnostic = diagnosticInvocation.CreateDiagnostic(DefaultRule, diagnosticInvocation.Syntax);
Mrnikbobjeff marked this conversation as resolved.
Show resolved Hide resolved
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' has no effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' has no 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' in expression '{0}' has no effect</source>
<target state="new">'AsParallel' in expression '{0}' has no effect</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query has no effect</source>
<target state="new">'AsParallel' at the end of a LINQ query has no 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' has no effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' has no 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' in expression '{0}' has no effect</source>
<target state="new">'AsParallel' in expression '{0}' has no effect</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query has no effect</source>
<target state="new">'AsParallel' at the end of a LINQ query has no 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' has no effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' has no 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' in expression '{0}' has no effect</source>
<target state="new">'AsParallel' in expression '{0}' has no effect</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query has no effect</source>
<target state="new">'AsParallel' at the end of a LINQ query has no 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
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
<target state="translated">N'utilisez pas de valeurs dépréciées pour SslProtocols</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsDescription">
<source>'AsParallel' has no effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' has no 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' in expression '{0}' has no effect</source>
<target state="new">'AsParallel' in expression '{0}' has no effect</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query has no effect</source>
<target state="new">'AsParallel' at the end of a LINQ query has no 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 type qui implémente System.IDisposable déclare des champs dont les types implémentent également IDisposable. La méthode Dispose du champ n'est pas appelée par la méthode Dispose du type déclarant. Pour corriger toute violation de cette règle, appelez Dispose sur les champs dont les types implémentent IDisposable si vous êtes responsable de l'allocation et de la libération des ressources non managées détenues par le champ.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@
<target state="translated">Non usare valori SslProtocols deprecati</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsDescription">
<source>'AsParallel' has no effect if it is the last expression in chain or if the only remaining expressions are 'ToList' or 'ToArray'.</source>
<target state="new">'AsParallel' has no 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' in expression '{0}' has no effect</source>
<target state="new">'AsParallel' in expression '{0}' has no effect</target>
<note />
</trans-unit>
<trans-unit id="DetectPLINQNopsTitle">
<source>'AsParallel' at the end of a LINQ query has no effect</source>
<target state="new">'AsParallel' at the end of a LINQ query has no 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 che implementa System.IDisposable dichiara i campi di tipi che implementano anch'essi IDisposable. Il metodo Dispose del campo non viene chiamato dal metodo Dispose del tipo dichiarante. Per correggere una violazione di questa regola, chiamare Dispose su campi di tipi che implementano IDisposable se si è responsabile dell'allocazione e del rilascio delle risorse non gestite utilizzate dal campo.</target>
Expand Down
Loading