File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\InterfaceMethodsShouldBeCallableByChildTypes.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.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Globalization;
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;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    /// <summary>
    /// CA1033: Interface methods should be callable by child types
    /// </summary>
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public sealed class InterfaceMethodsShouldBeCallableByChildTypesFixer : CodeFixProvider
    {
        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(InterfaceMethodsShouldBeCallableByChildTypesAnalyzer.RuleId);

        public override FixAllProvider GetFixAllProvider()
        {
            return WellKnownFixAllProviders.BatchFixer;
        }

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            SemanticModel semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
            SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            SyntaxNode nodeToFix = root.FindNode(context.Span);
            if (nodeToFix == null)
            {
                return;
            }

            if (semanticModel.GetDeclaredSymbol(nodeToFix, context.CancellationToken) is not IMethodSymbol methodSymbol)
            {
                return;
            }

            SyntaxGenerator generator = SyntaxGenerator.GetGenerator(context.Document);
            SyntaxNode declaration = generator.GetDeclaration(nodeToFix);
            if (declaration == null)
            {
                return;
            }

            IMethodSymbol? candidateToIncreaseVisibility = GetExistingNonVisibleAlternate(methodSymbol);
            if (candidateToIncreaseVisibility != null)
            {
                ISymbol? symbolToChange;
                bool checkSetter = false;
                if (candidateToIncreaseVisibility.IsAccessorMethod())
                {
                    symbolToChange = candidateToIncreaseVisibility.AssociatedSymbol;
                    if (methodSymbol.AssociatedSymbol?.Kind == SymbolKind.Property)
                    {
                        var originalProperty = (IPropertySymbol)methodSymbol.AssociatedSymbol;
                        checkSetter = originalProperty.SetMethod != null;
                    }
                }
                else
                {
                    symbolToChange = candidateToIncreaseVisibility;
                }

                if (symbolToChange != null)
                {
                    string title = string.Format(CultureInfo.CurrentCulture, MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix1, symbolToChange.Name);

                    context.RegisterCodeFix(CodeAction.Create(title,
                         async ct => await MakeProtectedAsync(context.Document, symbolToChange, checkSetter, ct).ConfigureAwait(false),
                         equivalenceKey: MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix1),
                    context.Diagnostics);
                }
            }
            else
            {
                ISymbol? symbolToChange = methodSymbol.IsAccessorMethod() ? methodSymbol.AssociatedSymbol : methodSymbol;
                if (symbolToChange != null)
                {
                    string title = string.Format(CultureInfo.CurrentCulture, MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix2, symbolToChange.Name);

                    context.RegisterCodeFix(CodeAction.Create(title,
                         async ct => await ChangeToPublicInterfaceImplementationAsync(context.Document, symbolToChange, ct).ConfigureAwait(false),
                         equivalenceKey: MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix2),
                    context.Diagnostics);
                }
            }

            context.RegisterCodeFix(CodeAction.Create(string.Format(CultureInfo.CurrentCulture, MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix3, methodSymbol.ContainingType.Name),
                     async ct => await MakeContainingTypeSealedAsync(context.Document, methodSymbol, ct).ConfigureAwait(false),
                         equivalenceKey: MicrosoftCodeQualityAnalyzersResources.InterfaceMethodsShouldBeCallableByChildTypesFix3),
                context.Diagnostics);
        }

        private static IMethodSymbol? GetExistingNonVisibleAlternate(IMethodSymbol methodSymbol)
        {
            foreach (IMethodSymbol interfaceMethod in methodSymbol.ExplicitInterfaceImplementations)
            {
                foreach (INamedTypeSymbol type in methodSymbol.ContainingType.GetBaseTypesAndThis())
                {
                    IMethodSymbol candidate = type.GetMembers(interfaceMethod.Name).OfType<IMethodSymbol>().FirstOrDefault(m => !m.Equals(methodSymbol));
                    if (candidate != null)
                    {
                        return candidate;
                    }
                }
            }

            return null;
        }

        private static async Task<Document> MakeProtectedAsync(Document document, ISymbol symbolToChange, bool checkSetter, CancellationToken cancellationToken)
        {
            SymbolEditor editor = SymbolEditor.Create(document);

            ISymbol? getter = null;
            ISymbol? setter = null;
            if (symbolToChange.Kind == SymbolKind.Property)
            {
                var propertySymbol = (IPropertySymbol)symbolToChange;
                getter = propertySymbol.GetMethod;
                setter = propertySymbol.SetMethod;
            }

            await editor.EditAllDeclarationsAsync(symbolToChange, (docEditor, declaration) =>
            {
                docEditor.SetAccessibility(declaration, Accessibility.Protected);
            }, cancellationToken).ConfigureAwait(false);

            if (getter != null && getter.DeclaredAccessibility == Accessibility.Private)
            {
                await editor.EditAllDeclarationsAsync(getter, (docEditor, declaration) =>
                {
                    docEditor.SetAccessibility(declaration, Accessibility.NotApplicable);
                }, cancellationToken).ConfigureAwait(false);
            }

            if (checkSetter && setter != null && setter.DeclaredAccessibility == Accessibility.Private)
            {
                await editor.EditAllDeclarationsAsync(setter, (docEditor, declaration) =>
                {
                    docEditor.SetAccessibility(declaration, Accessibility.NotApplicable);
                }, cancellationToken).ConfigureAwait(false);
            }

            return editor.GetChangedDocuments().First();
        }

        private static async Task<Document> ChangeToPublicInterfaceImplementationAsync(Document document, ISymbol symbolToChange, CancellationToken cancellationToken)
        {
            SymbolEditor editor = SymbolEditor.Create(document);

            IEnumerable<ISymbol>? explicitImplementations = GetExplicitImplementations(symbolToChange);
            if (explicitImplementations == null)
            {
                return document;
            }

            await editor.EditAllDeclarationsAsync(symbolToChange, (docEditor, declaration) =>
            {
                SyntaxNode newDeclaration = declaration;
                foreach (ISymbol implementedMember in explicitImplementations)
                {
                    SyntaxNode interfaceTypeNode = docEditor.Generator.TypeExpression(implementedMember.ContainingType);
                    newDeclaration = docEditor.Generator.AsPublicInterfaceImplementation(newDeclaration, interfaceTypeNode);
                }

                docEditor.ReplaceNode(declaration, newDeclaration);
            }, cancellationToken).ConfigureAwait(false);

            return editor.GetChangedDocuments().First();
        }

        private static IEnumerable<ISymbol>? GetExplicitImplementations(ISymbol? symbol)
        {
            if (symbol == null)
            {
                return null;
            }

            return symbol.Kind switch
            {
                SymbolKind.Method => ((IMethodSymbol)symbol).ExplicitInterfaceImplementations,

                SymbolKind.Event => ((IEventSymbol)symbol).ExplicitInterfaceImplementations,

                SymbolKind.Property => ((IPropertySymbol)symbol).ExplicitInterfaceImplementations,

                _ => null,
            };
        }

        private static async Task<Document> MakeContainingTypeSealedAsync(Document document, IMethodSymbol methodSymbol, CancellationToken cancellationToken)
        {
            SymbolEditor editor = SymbolEditor.Create(document);

            await editor.EditAllDeclarationsAsync(methodSymbol.ContainingType, (docEditor, declaration) =>
            {
                DeclarationModifiers modifiers = docEditor.Generator.GetModifiers(declaration);
                docEditor.SetModifiers(declaration, modifiers + DeclarationModifiers.Sealed);
            }, cancellationToken).ConfigureAwait(false);

            return editor.GetChangedDocuments().First();
        }
    }
}