File: SynchronizeUnsafeContractCodeFixProvider.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.Generic;
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;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;

namespace ILLink.CodeFix
{
    /// <summary>
    /// Fixes compiler diagnostics <c>CS9364</c>, <c>CS9365</c> and <c>CS9366</c>, reported when an
    /// <c>unsafe</c> member overrides or implements a member that is not caller-unsafe.
    /// </summary>
    /// <remarks>
    /// Two fixes are offered because the compiler cannot tell which side is wrong. Removing <c>unsafe</c> from
    /// the derived member is correct when it was an unsafe-v1 lexical scope, and is always available. Marking
    /// the base member <c>unsafe</c> is correct when the contract was genuinely under-annotated, and is only
    /// offered when that member is declared in source.
    /// </remarks>
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SynchronizeUnsafeContractCodeFixProvider)), Shared]
    public sealed class SynchronizeUnsafeContractCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider
    {
        public const string UnsafeCannotOverrideSafeDiagnosticId = "CS9364";
        public const string UnsafeCannotImplicitlyImplementSafeDiagnosticId = "CS9365";
        public const string UnsafeCannotExplicitlyImplementSafeDiagnosticId = "CS9366";

        // The keys are deliberately independent of the member being fixed, so that "fix all occurrences"
        // groups every use of the same action rather than one group per member name.
        private const string RemoveEquivalenceKey = nameof(SynchronizeUnsafeContractCodeFixProvider) + ".Remove";
        private const string AddToBaseEquivalenceKey = nameof(SynchronizeUnsafeContractCodeFixProvider) + ".AddToBase";
        private const string ReplaceWithSafeEquivalenceKey = nameof(SynchronizeUnsafeContractCodeFixProvider) + ".ReplaceWithSafe";

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

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

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

        public override ImmutableArray<string> FixableDiagnosticIds =>
            [
                UnsafeCannotOverrideSafeDiagnosticId,
                UnsafeCannotImplicitlyImplementSafeDiagnosticId,
                UnsafeCannotExplicitlyImplementSafeDiagnosticId,
            ];

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

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

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

            string removeTitle = RemoveTitle.ToString();
            bool isExtern = UnsafeMigrationSyntaxHelpers.HasModifier(derivedDeclaration, SyntaxKind.ExternKeyword);
            if (isExtern)
            {
                // An extern member must keep an explicit marker, so removing 'unsafe' would only trade
                // CS9364/CS9365/CS9366 for CS9389. Narrowing the contract to 'safe' is the equivalent edit.
                if (UnsafeMigrationSyntaxHelpers.SafeKeywordKind != SyntaxKind.None)
                {
                    context.RegisterCodeFix(
                        CodeAction.Create(
                            ReplaceWithSafeTitle.ToString(),
                            cancellationToken => UnsafeModifierCodeFixHelpers.ReplaceUnsafeWithSafeAsync(
                                context.Document,
                                derivedDeclaration,
                                cancellationToken),
                            ReplaceWithSafeEquivalenceKey),
                        diagnostic);
                }
            }
            else
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        removeTitle,
                        cancellationToken => UnsafeModifierCodeFixHelpers.RemoveUnsafeModifierAsync(
                            context.Document,
                            derivedDeclaration,
                            cancellationToken),
                        RemoveEquivalenceKey),
                    diagnostic);
            }

            if (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is not { } semanticModel
                || GetDeclaredSymbol(semanticModel, targetNode, derivedDeclaration, context.CancellationToken) is not { } derivedSymbol)
            {
                return;
            }

            // Only base members declared in source can be annotated, and only ones that are missing the modifier
            // on every one of their declarations.
            List<(DocumentId DocumentId, SyntaxNode Declaration)> baseDeclarations = [];
            foreach (ISymbol baseSymbol in GetBaseContracts(derivedSymbol))
            {
                if (GetEditableDeclarations(baseSymbol, context.Document.Project.Solution, context.CancellationToken) is not { Count: > 0 } declarations
                    || declarations.Any(static pair => UnsafeMigrationSyntaxHelpers.HasSafeModifier(pair.Declaration)))
                {
                    return;
                }

                baseDeclarations.AddRange(declarations);
            }

            if (baseDeclarations.Count == 0)
                return;

            context.RegisterCodeFix(
                CodeAction.Create(
                    AddToBaseTitle.ToString(),
                    cancellationToken => AddUnsafeToBaseAsync(context.Document.Project.Solution, baseDeclarations, cancellationToken),
                    AddToBaseEquivalenceKey),
                diagnostic);
        }

        /// <summary>
        /// Resolves the symbol whose contract the diagnostic is about.
        /// </summary>
        /// <remarks>
        /// A field or field-like event declaration declares one symbol per variable and has no symbol of its
        /// own, so the declarator the diagnostic points at is what has to be asked.
        /// </remarks>
        private static ISymbol? GetDeclaredSymbol(
            SemanticModel semanticModel,
            SyntaxNode targetNode,
            SyntaxNode declaration,
            CancellationToken cancellationToken)
        {
            if (declaration is BaseFieldDeclarationSyntax)
            {
                return targetNode.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().FirstOrDefault() is { } variable
                    ? semanticModel.GetDeclaredSymbol(variable, cancellationToken)
                    : null;
            }

            return semanticModel.GetDeclaredSymbol(declaration, cancellationToken);
        }

        private static async Task<Solution> AddUnsafeToBaseAsync(
            Solution solution,
            List<(DocumentId DocumentId, SyntaxNode Declaration)> declarations,
            CancellationToken cancellationToken)
        {
            // All declarations in a document are edited in a single pass so that no node has to be re-resolved
            // against a tree whose spans have already shifted.
            foreach (IGrouping<DocumentId, (DocumentId DocumentId, SyntaxNode Declaration)> group in declarations.GroupBy(static pair => pair.DocumentId))
            {
                if (solution.GetDocument(group.Key) is not { } document)
                    continue;

                var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
                var seenSpans = new HashSet<TextSpan>();
                bool edited = false;
                foreach ((_, SyntaxNode declaration) in group)
                {
                    // Two contracts can share one declaration, for example a member that implements both
                    // 'I<int>.M' and 'I<string>.M', and the editor cannot replace the same node twice.
                    if (!seenSpans.Add(declaration.Span)
                        || UnsafeMigrationSyntaxHelpers.HasModifier(declaration, SyntaxKind.UnsafeKeyword))
                    {
                        continue;
                    }

                    editor.ReplaceNode(
                        declaration,
                        UnsafeModifierCodeFixHelpers.AddModifier(declaration, SyntaxKind.UnsafeKeyword));
                    edited = true;
                }

                if (edited)
                    solution = editor.GetChangedDocument().Project.Solution;
            }

            return solution;
        }

        private static List<(DocumentId DocumentId, SyntaxNode Declaration)> GetEditableDeclarations(
            ISymbol symbol,
            Solution solution,
            CancellationToken cancellationToken)
        {
            var declarations = new List<(DocumentId, SyntaxNode)>();

            // A partial member is two symbols, each seeing only its own declaration, but both parts must agree
            // on the modifier or the edit trades one diagnostic for CS0764.
            foreach (ISymbol part in GetSymbolAndPartialParts(symbol))
            {
                foreach (SyntaxReference reference in part.DeclaringSyntaxReferences)
                {
                    if (solution.GetDocumentId(reference.SyntaxTree) is not { } documentId)
                        return [];

                    SyntaxNode declaration = reference.GetSyntax(cancellationToken);
                    if (declaration is VariableDeclaratorSyntax variable && variable.Parent?.Parent is BaseFieldDeclarationSyntax field)
                        declaration = field;

                    declarations.Add((documentId, declaration));
                }
            }

            return declarations;
        }

        private static IEnumerable<ISymbol> GetSymbolAndPartialParts(ISymbol symbol)
        {
            yield return symbol;

            ISymbol? otherPart = symbol switch
            {
                IMethodSymbol method => (ISymbol?)method.PartialDefinitionPart ?? method.PartialImplementationPart,
                IPropertySymbol property => (ISymbol?)property.PartialDefinitionPart ?? property.PartialImplementationPart,
                IEventSymbol @event => (ISymbol?)@event.PartialDefinitionPart ?? @event.PartialImplementationPart,
                _ => null,
            };

            if (otherPart is not null)
                yield return otherPart;
        }

        /// <summary>
        /// Finds the declaration that carries the <c>unsafe</c> modifier the compiler objected to.
        /// </summary>
        /// <remarks>
        /// For a property or event, the diagnostic is reported on the accessor while <c>unsafe</c> may be on the
        /// containing declaration, so the search continues outward until a declaration with the modifier is found.
        /// </remarks>
        private static SyntaxNode? FindUnsafeDeclaration(SyntaxNode node) =>
            node.AncestorsAndSelf()
                .Where(static ancestor => ancestor is BaseMethodDeclarationSyntax
                    or BasePropertyDeclarationSyntax
                    or BaseFieldDeclarationSyntax
                    or AccessorDeclarationSyntax)
                .FirstOrDefault(static ancestor => UnsafeMigrationSyntaxHelpers.HasModifier(ancestor, SyntaxKind.UnsafeKeyword));

        /// <summary>
        /// Collects every member whose contract the derived member has to match.
        /// </summary>
        /// <remarks>
        /// The compiler compares an override against the original definition of its chain, so annotating the
        /// root is enough and the members in between can stay as they are. A member can also override one member
        /// while implementing another, in which case both have to be annotated for the derived member to be
        /// legal, and both diagnostics are reported on it.
        /// </remarks>
        private static ImmutableArray<ISymbol> GetBaseContracts(ISymbol symbol)
        {
            var contracts = new List<ISymbol>();
            var seen = new HashSet<ISymbol>(SymbolEqualityComparer.Default) { symbol };

            var chain = new List<ISymbol> { symbol };
            for (ISymbol current = symbol; GetOverriddenMember(current) is { } overridden; current = overridden)
                chain.Add(overridden);

            if (seen.Add(chain[chain.Count - 1]))
                contracts.Add(chain[chain.Count - 1]);

            // An interface implementation is attributed to the member that declares it rather than to the most
            // derived override, so every member of the chain has to be asked about its own interfaces.
            foreach (ISymbol member in chain)
            {
                foreach (ISymbol interfaceMember in GetImplementedInterfaceMembers(member))
                {
                    if (seen.Add(interfaceMember))
                        contracts.Add(interfaceMember);
                }
            }

            return [.. contracts];
        }

        private static IEnumerable<ISymbol> GetImplementedInterfaceMembers(ISymbol symbol)
        {
            // An explicit implementation names its interface member directly; its own name is qualified, so the
            // scan below would not match it.
            foreach (ISymbol explicitImplementation in GetExplicitInterfaceImplementations(symbol))
                yield return explicitImplementation;

            if (symbol.ContainingType is not { } containingType)
                yield break;

            // An implicit implementation has no syntactic link to the interface, so the containing type's
            // interfaces are searched for members this symbol satisfies.
            foreach (INamedTypeSymbol interfaceType in containingType.AllInterfaces)
            {
                foreach (ISymbol interfaceMember in interfaceType.GetMembers())
                {
                    if (interfaceMember.Kind == symbol.Kind
                        && interfaceMember.Name == symbol.Name
                        && SymbolEqualityComparer.Default.Equals(
                            containingType.FindImplementationForInterfaceMember(interfaceMember),
                            symbol))
                    {
                        yield return interfaceMember;
                    }
                }
            }
        }

        private static ISymbol? GetOverriddenMember(ISymbol symbol) =>
            symbol switch
            {
                IMethodSymbol method => method.OverriddenMethod,
                IPropertySymbol property => property.OverriddenProperty,
                IEventSymbol @event => @event.OverriddenEvent,
                _ => null,
            };

        private static ImmutableArray<ISymbol> GetExplicitInterfaceImplementations(ISymbol symbol) =>
            symbol switch
            {
                IMethodSymbol method => ImmutableArray<ISymbol>.CastUp(method.ExplicitInterfaceImplementations),
                IPropertySymbol property => ImmutableArray<ISymbol>.CastUp(property.ExplicitInterfaceImplementations),
                IEventSymbol @event => ImmutableArray<ISymbol>.CastUp(@event.ExplicitInterfaceImplementations),
                _ => ImmutableArray<ISymbol>.Empty,
            };
    }
}
#endif