// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;
using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.NetCore.Analyzers.Runtime
{
public abstract class ForwardCancellationTokenToInvocationsFixer<TArgumentSyntax> : SyntaxEditorBasedCodeFixProvider
where TArgumentSyntax : SyntaxNode
{
// Attempts to retrieve the invocation from the current operation.
protected abstract bool TryGetInvocation(
SemanticModel model,
SyntaxNode node,
CancellationToken ct,
[NotNullWhen(returnValue: true)] out IInvocationOperation? invocation);
// Retrieves the invocation expression node and the invocation argument list
protected abstract bool TryGetExpressionAndArguments(
SyntaxNode invocationNode,
[NotNullWhen(returnValue: true)] out SyntaxNode? expression,
out ImmutableArray<TArgumentSyntax> arguments);
// Verifies if the specified argument was passed with an explicit name.
protected abstract bool IsArgumentNamed(IArgumentOperation argumentOperation);
// Retrieves the invocation expression for a conditional operation, which consists of the dot and the method name.
protected abstract SyntaxNode GetConditionalOperationInvocationExpression(SyntaxNode invocationNode);
protected abstract SyntaxNode GetTypeSyntaxForArray(IArrayTypeSymbol type);
protected abstract IEnumerable<SyntaxNode> GetExpressions(ImmutableArray<TArgumentSyntax> newArguments);
protected abstract SyntaxNode GetArrayCreationExpression(SyntaxGenerator generator, SyntaxNode typeSyntax, IEnumerable<SyntaxNode> expressions);
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(ForwardCancellationTokenToInvocationsAnalyzer.RuleId);
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Document doc = context.Document;
CancellationToken ct = context.CancellationToken;
SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(ct).ConfigureAwait(false);
SemanticModel model = await doc.GetRequiredSemanticModelAsync(ct).ConfigureAwait(false);
if (!TryGetFix(model, root, context.Diagnostics[0], ct, out _))
{
return;
}
RegisterCodeFix(context,
MicrosoftNetCoreAnalyzersResources.ForwardCancellationTokenToInvocationsTitle,
nameof(MicrosoftNetCoreAnalyzersResources.ForwardCancellationTokenToInvocationsTitle));
}
protected sealed override async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, CancellationToken cancellationToken)
{
SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (!TryGetFix(model, editor.OriginalRoot, diagnostic, cancellationToken, out Fix fix))
{
return;
}
editor.TrackNode(fix.Expression);
foreach (TArgumentSyntax argument in fix.Arguments)
{
editor.TrackNode(argument);
}
// An argument can itself be a diagnosed invocation, so the invocation is rebuilt from the
// arguments as the inner fixes left them rather than from the original tree.
editor.ReplaceNode(fix.Invocation.Syntax, (currentNode, generator) =>
{
SyntaxNode expression = currentNode.GetCurrentNode(fix.Expression) ?? fix.Expression;
ImmutableArray<TArgumentSyntax>.Builder currentArguments = ImmutableArray.CreateBuilder<TArgumentSyntax>(fix.Arguments.Length);
foreach (TArgumentSyntax argument in fix.Arguments)
{
currentArguments.Add((TArgumentSyntax)(currentNode.GetCurrentNode(argument) ?? argument));
}
return GenerateInvocation(generator, fix, expression, currentArguments.MoveToImmutable()).WithTriviaFrom(currentNode);
});
}
private bool TryGetFix(SemanticModel model, SyntaxNode root, Diagnostic diagnostic, CancellationToken cancellationToken, out Fix fix)
{
fix = default;
if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not SyntaxNode node)
{
return false;
}
// The analyzer created the diagnostic on the IdentifierNameSyntax, and the parent is the actual invocation
if (!TryGetInvocation(model, node, cancellationToken, out IInvocationOperation? invocation))
{
return false;
}
ImmutableDictionary<string, string?> properties = diagnostic.Properties;
if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ShouldFix, out var shouldFix) ||
string.IsNullOrEmpty(shouldFix) ||
shouldFix!.Equals("0", StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
// The name that identifies the object that is to be passed
if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ArgumentName, out var argumentName) ||
string.IsNullOrEmpty(argumentName))
{
return false;
}
// If the invocation requires the token to be passed with a name, use this
if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ParameterName, out var parameterName))
{
return false;
}
if (!TryGetExpressionAndArguments(invocation.Syntax, out SyntaxNode? expression, out ImmutableArray<TArgumentSyntax> arguments))
{
return false;
}
var paramsArrayType = invocation.Arguments.SingleOrDefault(a => a.ArgumentKind == ArgumentKind.ParamArray)?.Value.Type as IArrayTypeSymbol;
fix = new Fix(invocation, expression, arguments, argumentName!, parameterName!, paramsArrayType);
return true;
}
private SyntaxNode GenerateInvocation(SyntaxGenerator generator, in Fix fix, SyntaxNode expression, ImmutableArray<TArgumentSyntax> currentArguments)
{
ImmutableArray<SyntaxNode> newArguments;
if (fix.ParamsArrayType is not null)
{
// current callsite is a params array, we need to wrap all these arguments to preserve semantics
var typeSyntax = GetTypeSyntaxForArray(fix.ParamsArrayType);
var expressions = GetExpressions(currentArguments);
newArguments = ImmutableArray.Create(GetArrayCreationExpression(generator, typeSyntax, expressions));
}
else
{
// not a params array just pass the existing arguments along
newArguments = currentArguments.CastArray<SyntaxNode>();
}
SyntaxNode identifier = generator.IdentifierName(fix.ArgumentName);
SyntaxNode cancellationTokenArgument;
if (!string.IsNullOrEmpty(fix.ParameterName))
{
cancellationTokenArgument = generator.Argument(fix.ParameterName, RefKind.None, identifier);
}
else
{
cancellationTokenArgument = generator.Argument(identifier);
}
newArguments = newArguments.Add(cancellationTokenArgument);
return generator.InvocationExpression(expression, newArguments);
}
private readonly struct Fix
{
public Fix(IInvocationOperation invocation, SyntaxNode expression, ImmutableArray<TArgumentSyntax> arguments,
string argumentName, string parameterName, IArrayTypeSymbol? paramsArrayType)
{
Invocation = invocation;
Expression = expression;
Arguments = arguments;
ArgumentName = argumentName;
ParameterName = parameterName;
ParamsArrayType = paramsArrayType;
}
public IInvocationOperation Invocation { get; }
public SyntaxNode Expression { get; }
public ImmutableArray<TArgumentSyntax> Arguments { get; }
/// <summary>The name of the token to forward.</summary>
public string ArgumentName { get; }
/// <summary>The parameter to name the forwarded token after, or empty to pass it positionally.</summary>
public string ParameterName { get; }
public IArrayTypeSymbol? ParamsArrayType { get; }
}
}
}