// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using System.Composition; using System.Threading; using System.Threading.Tasks; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.NetAnalyzers; using Microsoft.CodeAnalysis.Operations; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; namespace Microsoft.NetCore.Analyzers.Runtime { /// <summary> /// CA2208: Instantiate argument exceptions correctly /// </summary> [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] public sealed class InstantiateArgumentExceptionsCorrectlyFixer : CodeFixProvider { private const string AddNullMessageKey = nameof(MicrosoftNetCoreAnalyzersResources.InstantiateArgumentExceptionsCorrectlyChangeToTwoArgumentCodeFixTitle); private const string SwapArgumentsKey = nameof(MicrosoftNetCoreAnalyzersResources.InstantiateArgumentExceptionsCorrectlyFlipArgumentOrderCodeFixTitle); public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(InstantiateArgumentExceptionsCorrectlyAnalyzer.RuleId); public sealed override FixAllProvider GetFixAllProvider() => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync); public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { Diagnostic diagnostic = context.Diagnostics[0]; SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); SemanticModel model = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); if (!TryGetCreation(model, root, diagnostic, context.CancellationToken, out IObjectCreationOperation? creation, out _)) { return; } (string title, string equivalenceKey) = creation.Arguments.Length == 1 ? (MicrosoftNetCoreAnalyzersResources.InstantiateArgumentExceptionsCorrectlyChangeToTwoArgumentCodeFixTitle, AddNullMessageKey) : (MicrosoftNetCoreAnalyzersResources.InstantiateArgumentExceptionsCorrectlyFlipArgumentOrderCodeFixTitle, SwapArgumentsKey); context.RegisterCodeFix( CodeAction.Create( title, ct => SyntaxEditorFixAllProvider.ApplyFixesAsync(context.Document, context.Diagnostics, (document, diag, editor, token) => ApplyFixAsync(document, diag, editor, equivalenceKey, token), ct), equivalenceKey), diagnostic); } private static async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken) { SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (!TryGetCreation(model, editor.OriginalRoot, diagnostic, cancellationToken, out IObjectCreationOperation? creation, out int paramPosition)) { return; } SyntaxGenerator generator = editor.Generator; int argumentCount = creation.Arguments.Length; if (argumentCount == 1) { if (equivalenceKey is not null && equivalenceKey != AddNullMessageKey) { return; } // Add a null message ahead of the parameter name. FixArgument nullMessage = FixArgument.Generated(generator.Argument(generator.NullLiteralExpression())); ReplaceCreation(editor, creation, nullMessage, GetArgument(creation, generator, 0, nameOf: true)); return; } if (equivalenceKey is not null && equivalenceKey != SwapArgumentsKey) { return; } // Swap the message and the parameter name. FixArgument parameter = GetArgument(creation, generator, paramPosition, nameOf: true); if (argumentCount == 2) { if (paramPosition == 0) { ReplaceCreation(editor, creation, GetArgument(creation, generator, 1), parameter); } else { ReplaceCreation(editor, creation, parameter, GetArgument(creation, generator, 0)); } } else { Debug.Assert(argumentCount == 3); if (paramPosition == 0) { ReplaceCreation(editor, creation, GetArgument(creation, generator, 1), parameter, GetArgument(creation, generator, 2)); } else { ReplaceCreation(editor, creation, parameter, GetArgument(creation, generator, 1), GetArgument(creation, generator, 0)); } } } private static bool TryGetCreation(SemanticModel model, SyntaxNode root, Diagnostic diagnostic, CancellationToken cancellationToken, [NotNullWhen(true)] out IObjectCreationOperation? creation, out int paramPosition) { creation = null; paramPosition = 0; if (diagnostic.Properties.GetValueOrDefault(InstantiateArgumentExceptionsCorrectlyAnalyzer.MessagePosition) is not string paramPositionString || !int.TryParse(paramPositionString, out paramPosition)) { return false; } if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not SyntaxNode node || model.GetOperation(node, cancellationToken) is not IObjectCreationOperation objectCreation) { return false; } creation = objectCreation; return true; } /// <remarks> /// The rewritten argument list is positional, so the value is taken without the enclosing /// argument: carrying a named argument's syntax into a different position would either name /// the wrong parameter or fail to compile. /// </remarks> private static FixArgument GetArgument(IObjectCreationOperation creation, SyntaxGenerator generator, int parameterIndex, bool nameOf = false) { IOperation value = creation.Arguments.GetArgumentForParameterAtIndex(parameterIndex).Value; if (nameOf && value is ILiteralOperation literal && literal.ConstantValue.Value is object constant) { return FixArgument.Generated(generator.NameOfExpression(generator.IdentifierName(constant.ToString()))); } return FixArgument.CarriedOver(value.Syntax); } private static void ReplaceCreation(SyntaxEditor editor, IObjectCreationOperation creation, params FixArgument[] arguments) { if (creation.Type is not ITypeSymbol creationType) { return; } foreach (FixArgument argument in arguments) { if (argument.Original is SyntaxNode original) { editor.TrackNode(original); } } // The carried-over arguments can themselves contain a diagnosed creation, so they are read // from the creation as the inner fixes left it rather than from the original tree. editor.ReplaceNode(creation.Syntax, (currentNode, generator) => { SyntaxNode[] newArguments = new SyntaxNode[arguments.Length]; for (int i = 0; i < arguments.Length; i++) { FixArgument argument = arguments[i]; newArguments[i] = argument.Original is SyntaxNode original ? currentNode.GetCurrentNode(original) ?? original : argument.Node; } return generator.ObjectCreationExpression(creationType, newArguments); }); } /// <summary> /// An argument of the rewritten creation: either a node generated by the fix, or one carried /// over from the original creation and therefore tracked across the other fixes in the document. /// </summary> private readonly struct FixArgument { private FixArgument(SyntaxNode node, bool carriedOver) { Node = node; Original = carriedOver ? node : null; } public static FixArgument Generated(SyntaxNode node) => new FixArgument(node, carriedOver: false); public static FixArgument CarriedOver(SyntaxNode node) => new FixArgument(node, carriedOver: true); public SyntaxNode Node { get; } public SyntaxNode? Original { get; } } } }