File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\MarkAttributesWithAttributeUsage.Fixer.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// 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.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public sealed class MarkAttributesWithAttributeUsageFixer : CodeFixProvider
    {
        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(MarkAttributesWithAttributeUsageAnalyzer.RuleId);

        // Each nested action applies a different AttributeTargets value, so the fix-all pass has to be
        // told which one the user picked - DocumentBasedFixAllProvider hands over every diagnostic it
        // collected without filtering by the equivalence key.
        public override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(
                static fixAllContext => GetAttributeTargetValue(fixAllContext.CodeActionEquivalenceKey),
                static (document, diagnostic, editor, attributeTargetValue, cancellationToken) =>
                    attributeTargetValue is null
                        ? Task.CompletedTask
                        : AddAttributeUsageAttributeAsync(document, diagnostic, editor, attributeTargetValue, cancellationToken));

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
            if (!semanticModel.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttributeUsageAttribute, out _) ||
                !semanticModel.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttributeTargets, out _))
            {
                return;
            }

            var document = context.Document;
            var diagnostics = context.Diagnostics;

            var applyAttributeTargetValues = Enum.GetValues(typeof(AttributeTargets))
                .Cast<AttributeTargets>()
                .Select(attributeTarget =>
                {
                    var attributeTargetValue = attributeTarget.ToString();
                    var title = $"{nameof(AttributeTargets)}.{attributeTargetValue}";

                    return CodeAction.Create(
                        title,
                        cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                            document,
                            diagnostics,
                            (doc, diagnostic, editor, token) => AddAttributeUsageAttributeAsync(doc, diagnostic, editor, attributeTargetValue, token),
                            cancellationToken),
                        equivalenceKey: title);
                })
                .OrderBy(a => a.Title)
                .ToImmutableArray();

#pragma warning disable RS1010 // Provide an explicit value for EquivalenceKey - false positive
            context.RegisterCodeFix(
                CodeAction.Create(MicrosoftCodeQualityAnalyzersResources.MarkAttributesWithAttributeUsageCodeFix, applyAttributeTargetValues, isInlinable: false),
                diagnostics);
#pragma warning restore RS1010
        }

        /// <summary>
        /// Recovers the <see cref="AttributeTargets"/> value a nested action was registered for from its
        /// equivalence key, or <see langword="null"/> if the key names no such value.
        /// </summary>
        private static string? GetAttributeTargetValue(string? equivalenceKey)
        {
            const string Prefix = nameof(AttributeTargets) + ".";

            if (equivalenceKey is null || !equivalenceKey.StartsWith(Prefix, StringComparison.Ordinal))
            {
                return null;
            }

            string value = equivalenceKey[Prefix.Length..];
            return Enum.TryParse(value, out AttributeTargets _) ? value : null;
        }

        private static async Task AddAttributeUsageAttributeAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor,
            string attributeTargetValue, CancellationToken cancellationToken)
        {
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            if (!semanticModel.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttributeUsageAttribute, out var attributeUsageAttributeType) ||
                !semanticModel.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttributeTargets, out var attributeTargetsType))
            {
                return;
            }

            var nodeToFix = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);
            var attribute = editor.Generator.Attribute(editor.Generator.TypeExpression(attributeUsageAttributeType),
                new[] { editor.Generator.MemberAccessExpression(editor.Generator.TypeExpression(attributeTargetsType), attributeTargetValue) });
            editor.AddAttribute(nodeToFix, attribute);
        }
    }
}