-
Notifications
You must be signed in to change notification settings - Fork 466
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
Add WhenAll and WaitAll analyzer for single task argument #4841
Changes from 19 commits
d8512c8
accfc0e
19c9121
a99dc38
c37cfa4
92a6d9f
29858e3
79b0c78
50da58c
5f0762b
6e63890
59e68f9
ba0c6ce
c9a5de5
11ddf1c
604a1f0
719e4c5
7285360
0ae0ff3
81ceec1
02947da
dd00427
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// 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.Composition; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Operations; | ||
using Microsoft.NetCore.Analyzers.Tasks; | ||
|
||
namespace Microsoft.NetCore.CSharp.Analyzers.Tasks | ||
{ | ||
[ExportCodeFixProvider(LanguageNames.CSharp), Shared] | ||
public sealed class CSharpDoNotUseWhenAllOrWaitAllWithSingleArgumentFixer : DoNotUseWhenAllOrWaitAllWithSingleArgumentFixer | ||
{ | ||
protected override SyntaxNode GetSingleArgumentSyntax(IInvocationOperation operation) | ||
{ | ||
var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; | ||
return invocationSyntax.ArgumentList.Arguments.Single().Expression; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// 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.Linq; | ||
using System.Threading.Tasks; | ||
using Analyzer.Utilities; | ||
using Analyzer.Utilities.Extensions; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Operations; | ||
|
||
namespace Microsoft.NetCore.Analyzers.Tasks | ||
{ | ||
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] | ||
public class DoNotUseWhenAllOrWaitAllWithSingleArgument : DiagnosticAnalyzer | ||
{ | ||
internal const string WhenAllRuleId = "CA1842"; | ||
internal const string WaitAllRuleId = "CA1843"; | ||
|
||
internal static readonly DiagnosticDescriptor WhenAllRule = DiagnosticDescriptorHelper.Create(WhenAllRuleId, | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWhenAllWithSingleTaskTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWhenAllWithSingleTaskTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
Comment on lines
+22
to
+23
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. Was it intentional to pass the title as the message too? 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. In this case they are the same, so I didn't add a separate message. |
||
DiagnosticCategory.Performance, | ||
RuleLevel.IdeSuggestion, | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWhenAllWithSingleTaskDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
isPortedFxCopRule: false, | ||
isDataflowRule: false); | ||
|
||
internal static readonly DiagnosticDescriptor WaitAllRule = DiagnosticDescriptorHelper.Create(WaitAllRuleId, | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWaitAllWithSingleTaskTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWaitAllWithSingleTaskTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
DiagnosticCategory.Performance, | ||
RuleLevel.IdeSuggestion, | ||
new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.DoNotUseWaitAllWithSingleTaskDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)), | ||
isPortedFxCopRule: false, | ||
isDataflowRule: false); | ||
|
||
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(WhenAllRule, WaitAllRule); | ||
|
||
public sealed override void Initialize(AnalysisContext context) | ||
{ | ||
context.EnableConcurrentExecution(); | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.RegisterCompilationStartAction(compilationContext => | ||
{ | ||
_ = compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask, out var taskType); | ||
_ = compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1, out var genericTaskType); | ||
|
||
if (taskType is null || genericTaskType is null) | ||
{ | ||
return; | ||
} | ||
ryzngard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
compilationContext.RegisterOperationAction(operationContext => | ||
{ | ||
ryzngard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var invocation = (IInvocationOperation)operationContext.Operation; | ||
if (IsWhenOrWaitAllMethod(invocation.TargetMethod, taskType) && | ||
IsSingleTaskArgument(invocation, taskType, genericTaskType)) | ||
{ | ||
switch (invocation.TargetMethod.Name) | ||
{ | ||
case nameof(Task.WhenAll): | ||
operationContext.ReportDiagnostic(invocation.CreateDiagnostic(WhenAllRule)); | ||
break; | ||
|
||
case nameof(Task.WaitAll): | ||
operationContext.ReportDiagnostic(invocation.CreateDiagnostic(WaitAllRule)); | ||
break; | ||
|
||
default: | ||
throw new InvalidOperationException($"Unexpected method name: {invocation.TargetMethod.Name}"); | ||
} | ||
} | ||
}, OperationKind.Invocation); | ||
}); | ||
} | ||
|
||
private static bool IsWhenOrWaitAllMethod(IMethodSymbol targetMethod, INamedTypeSymbol taskType) | ||
{ | ||
var nameMatches = targetMethod.Name is (nameof(Task.WhenAll)) or (nameof(Task.WaitAll)); | ||
var parameters = targetMethod.Parameters; | ||
|
||
return nameMatches && | ||
targetMethod.IsStatic && | ||
SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, taskType) && | ||
parameters.Length == 1 && | ||
parameters[0].IsParams; | ||
} | ||
Comment on lines
+82
to
+87
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. Not critical, but I think a bit more performant implementation would be to compute and store the method symbols for |
||
|
||
private static bool IsSingleTaskArgument(IInvocationOperation invocation, INamedTypeSymbol taskType, INamedTypeSymbol genericTaskType) | ||
{ | ||
if (invocation.Arguments.Length != 1) | ||
{ | ||
return false; | ||
} | ||
|
||
var argument = invocation.Arguments.Single(); | ||
|
||
// Task.WhenAll and Task.WaitAll have params arguments, which are implicit | ||
// array creation for cases where params were passed in without explicitly | ||
// being an array already. | ||
if (argument.Value is not IArrayCreationOperation | ||
{ | ||
IsImplicit: true, | ||
Initializer: { ElementValues: { Length: 1 } initializerValues } | ||
}) | ||
{ | ||
return false; | ||
} | ||
|
||
if (initializerValues.Single().Type is not INamedTypeSymbol namedTypeSymbol) | ||
{ | ||
return false; | ||
} | ||
|
||
return namedTypeSymbol.Equals(taskType) || namedTypeSymbol.ConstructedFrom.Equals(genericTaskType); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// 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.Threading; | ||
using System.Threading.Tasks; | ||
using Analyzer.Utilities; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.Editing; | ||
using Microsoft.CodeAnalysis.Operations; | ||
|
||
namespace Microsoft.NetCore.Analyzers.Tasks | ||
{ | ||
public abstract class DoNotUseWhenAllOrWaitAllWithSingleArgumentFixer : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create( | ||
DoNotUseWhenAllOrWaitAllWithSingleArgument.WaitAllRule.Id, | ||
DoNotUseWhenAllOrWaitAllWithSingleArgument.WhenAllRule.Id); | ||
|
||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var cancellationToken = context.CancellationToken; | ||
var root = await context.Document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||
var nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: true); | ||
ryzngard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (nodeToFix is null) | ||
{ | ||
return; | ||
} | ||
|
||
var semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); | ||
if (semanticModel.GetOperation(nodeToFix, cancellationToken) is not IInvocationOperation operation) | ||
{ | ||
return; | ||
} | ||
|
||
if (operation.TargetMethod.Name == nameof(Task.WaitAll)) | ||
{ | ||
var title = MicrosoftNetCoreAnalyzersResources.DoNotUseWaitAllWithSingleTaskFix; | ||
context.RegisterCodeFix(new MyCodeAction(title, | ||
async ct => | ||
{ | ||
var editor = await DocumentEditor.CreateAsync(context.Document, ct).ConfigureAwait(false); | ||
editor.ReplaceNode(nodeToFix, | ||
editor.Generator.InvocationExpression( | ||
editor.Generator.MemberAccessExpression( | ||
GetSingleArgumentSyntax(operation), | ||
nameof(Task.Wait)) | ||
).WithTriviaFrom(nodeToFix) | ||
); | ||
|
||
return editor.GetChangedDocument(); | ||
}, | ||
equivalenceKey: title), | ||
context.Diagnostics); | ||
} | ||
else if (!IsValueStored(operation) && operation.TargetMethod.Name == nameof(Task.WhenAll)) | ||
{ | ||
var title = MicrosoftNetCoreAnalyzersResources.DoNotUseWhenAllWithSingleTaskFix; | ||
context.RegisterCodeFix(new MyCodeAction(title, | ||
async ct => | ||
{ | ||
var editor = await DocumentEditor.CreateAsync(context.Document, ct).ConfigureAwait(false); | ||
var newNode = GetSingleArgumentSyntax(operation).WithTriviaFrom(nodeToFix); | ||
|
||
// The original invocation already returns a Task, | ||
// so we can just replace directly with the argument | ||
editor.ReplaceNode(nodeToFix, newNode); | ||
|
||
return editor.GetChangedDocument(); | ||
}, | ||
equivalenceKey: title), | ||
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. IIRC, there was a comment from @sharwell somewhere that equivalence key shouldn't be localized and use nameof(Resources.FixTitle) instead. I don't know the reason though, but just pointing this out. 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. |
||
context.Diagnostics); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Returns true if the invocation is part of an assignment or variable declaration | ||
/// </summary> | ||
private static bool IsValueStored(IInvocationOperation operation) | ||
{ | ||
var currentOperation = operation.Parent; | ||
while (currentOperation is not null) | ||
{ | ||
if (currentOperation is IAssignmentOperation or | ||
IVariableDeclarationOperation) | ||
{ | ||
return true; | ||
} | ||
|
||
currentOperation = currentOperation.Parent; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
protected abstract SyntaxNode GetSingleArgumentSyntax(IInvocationOperation operation); | ||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
// Needed for Telemetry (https://github.com/dotnet/roslyn-analyzers/issues/192) | ||
private sealed class MyCodeAction : DocumentChangeAction | ||
{ | ||
public MyCodeAction(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string equivalenceKey) : | ||
base(title, createChangedDocument, equivalenceKey) | ||
{ | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also include the
why
part in the description?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it enough to say something like
"Using 'WaitAll' may result in performance loss, await or return the task instead" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds fine to me.