File: RemoveInvalidUnsafeCodeFixProvider.cs
Web Access
Project: src\runtime\src\tools\illink\src\ILLink.CodeFix\ILLink.CodeFixProvider.csproj (ILLink.CodeFixProvider)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if DEBUG
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ILLink.CodeFixProvider;
using ILLink.RoslynAnalyzer;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace ILLink.CodeFix
{
    /// <summary>
    /// Fixes <c>CS9377</c> and unsafe-specific <c>CS0106</c> diagnostics by removing the invalid <c>unsafe</c> modifier.
    /// The shared <c>CS0106</c> ID is filtered so modifiers unrelated to unsafe are left to their own fixes.
    /// </summary>
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveInvalidUnsafeCodeFixProvider)), Shared]
    public sealed class RemoveInvalidUnsafeCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
    {
        public const string UnsafeModifierHasNoEffectDiagnosticId = "CS9377";
        public const string InvalidModifierDiagnosticId = "CS0106";

        private static LocalizableString CodeFixTitle =>
            new LocalizableResourceString(
                nameof(Resources.RemoveInvalidUnsafeCodeFixTitle),
                Resources.ResourceManager,
                typeof(Resources));

        public override ImmutableArray<string> FixableDiagnosticIds =>
            [UnsafeModifierHasNoEffectDiagnosticId, InvalidModifierDiagnosticId];

        public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            Diagnostic diagnostic = context.Diagnostics[0];
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            if (root is null)
                return;

            SyntaxNode targetNode = root.FindNode(
                diagnostic.Location.SourceSpan,
                getInnermostNodeForTie: true);
            if (UnsafeModifierCodeFixHelpers.FindDeclaration(targetNode) is not { } declaration)
                return;

            // CS0106 is shared by every invalid modifier. Restrict this fixer to declaration shapes where
            // Roslyn specifically reports unsafe as invalid, rather than inspecting localized message text.
            if (diagnostic.Id == InvalidModifierDiagnosticId && !IsInvalidUnsafeDeclaration(declaration))
                return;

            string title = CodeFixTitle.ToString();
            if (UnsafeMigrationSyntaxHelpers.HasModifier(declaration, SyntaxKind.UnsafeKeyword))
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        title,
                        cancellationToken => UnsafeModifierCodeFixHelpers.RemoveUnsafeModifierAsync(
                            context.Document,
                            declaration,
                            cancellationToken),
                        title),
                    diagnostic);
                return;
            }

            // A partial type is reported once for the merged symbol, at a declaration that need not be the one
            // carrying the modifier, so the other parts have to be visited to find it.
            if (declaration is not BaseTypeDeclarationSyntax
                || await GetPartsWithUnsafeModifierAsync(context.Document, declaration, context.CancellationToken).ConfigureAwait(false) is not { Length: > 0 } parts)
            {
                return;
            }

            context.RegisterCodeFix(
                CodeAction.Create(
                    title,
                    cancellationToken => RemoveUnsafeModifierFromPartsAsync(
                        context.Document.Project.Solution,
                        parts,
                        cancellationToken),
                    title),
                diagnostic);
        }

        /// <summary>
        /// Returns the document IDs containing partial type declarations that carry the <c>unsafe</c> modifier in source.
        /// </summary>
        private static async Task<ImmutableArray<DocumentId>> GetPartsWithUnsafeModifierAsync(
            Document document,
            SyntaxNode declaration,
            CancellationToken cancellationToken)
        {
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            if (semanticModel?.GetDeclaredSymbol(declaration, cancellationToken) is not { } symbol)
                return [];

            Solution solution = document.Project.Solution;
            var parts = ImmutableArray.CreateBuilder<DocumentId>();
            foreach (SyntaxReference reference in symbol.DeclaringSyntaxReferences)
            {
                SyntaxNode part = reference.GetSyntax(cancellationToken);
                if (!UnsafeMigrationSyntaxHelpers.HasModifier(part, SyntaxKind.UnsafeKeyword))
                    continue;

                // A modifier in a generated part cannot be edited, and for the interop generators it is also
                // what keeps the generated stub legal under the legacy rules, so it has to stay.
                if (solution.GetDocumentId(part.SyntaxTree) is { } partId && !parts.Contains(partId))
                    parts.Add(partId);
            }

            return parts.ToImmutable();
        }

        /// <summary>
        /// Removes the <c>unsafe</c> modifier from every listed partial declaration.
        /// </summary>
        private static async Task<Solution> RemoveUnsafeModifierFromPartsAsync(
            Solution solution,
            ImmutableArray<DocumentId> parts,
            CancellationToken cancellationToken)
        {
            foreach (DocumentId partId in parts)
            {
                if (solution.GetDocument(partId) is not { } part
                    || await part.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) is not { } partRoot)
                {
                    continue;
                }

                foreach (BaseTypeDeclarationSyntax type in partRoot.DescendantNodes().OfType<BaseTypeDeclarationSyntax>())
                {
                    if (!UnsafeMigrationSyntaxHelpers.HasModifier(type, SyntaxKind.UnsafeKeyword))
                        continue;

                    Document changed = await UnsafeModifierCodeFixHelpers.RemoveUnsafeModifierAsync(
                        part,
                        type,
                        cancellationToken).ConfigureAwait(false);
                    solution = changed.Project.Solution;
                    break;
                }
            }

            return solution;
        }

        /// <summary>
        /// Identifies the well-formed declarations for which Roslyn reports unsafe-specific <c>CS0106</c>.
        /// </summary>
        private static bool IsInvalidUnsafeDeclaration(SyntaxNode declaration) =>
            declaration switch
            {
                EnumDeclarationSyntax => true,
                FieldDeclarationSyntax { Modifiers: var modifiers } => modifiers.Any(SyntaxKind.ConstKeyword),
                _ => false,
            };
    }
}
#endif