-
Notifications
You must be signed in to change notification settings - Fork 470
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4841 from ryzngard/feature/task_when_wait_all_ana…
…lyzer Add WhenAll and WaitAll analyzer for single task argument
- Loading branch information
Showing
24 changed files
with
1,179 additions
and
2 deletions.
There are no files selected for viewing
22 changes: 22 additions & 0 deletions
22
...icrosoft.NetCore.Analyzers/Tasks/CSharpDoNotUseWhenAllOrWaitAllWithSingleArgumentFixer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
...zers/Core/Microsoft.NetCore.Analyzers/Tasks/DoNotUseWhenAllOrWaitAllWithSingleArgument.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// 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)), | ||
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 => | ||
{ | ||
if (!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask, out var taskType) || | ||
!compilationContext.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1, out var genericTaskType)) | ||
{ | ||
return; | ||
} | ||
|
||
compilationContext.RegisterOperationAction(operationContext => | ||
{ | ||
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; | ||
} | ||
|
||
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); | ||
} | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
...Core/Microsoft.NetCore.Analyzers/Tasks/DoNotUseWhenAllOrWaitAllWithSingleArgumentFixer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
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), | ||
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) | ||
{ | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.