File: src\Analyzers\Core\CodeFixes\ImplementInterface\AbstractImplementInterfaceService.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.ImplementType;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.ImplementInterface;
 
using static ImplementHelpers;
 
internal abstract partial class AbstractImplementInterfaceService<TTypeDeclarationSyntax> : IImplementInterfaceService
    where TTypeDeclarationSyntax : SyntaxNode
{
    protected const string DisposingName = "disposing";
 
    protected abstract SyntaxGeneratorInternal SyntaxGeneratorInternal { get; }
 
    protected abstract string ToDisplayString(IMethodSymbol disposeImplMethod, SymbolDisplayFormat format);
 
    protected abstract bool CanImplementImplicitly { get; }
    protected abstract bool HasHiddenExplicitImplementation { get; }
    protected abstract bool TryInitializeState(Document document, SemanticModel model, SyntaxNode interfaceNode, CancellationToken cancellationToken,
        [NotNullWhen(true)] out SyntaxNode? classOrStructDecl,
        [NotNullWhen(true)] out INamedTypeSymbol? classOrStructType,
        out ImmutableArray<INamedTypeSymbol> interfaceTypes);
    protected abstract bool AllowDelegateAndEnumConstraints(ParseOptions options);
 
    protected abstract SyntaxNode AddCommentInsideIfStatement(SyntaxNode ifDisposingStatement, SyntaxTriviaList trivia);
    protected abstract SyntaxNode CreateFinalizer(SyntaxGenerator generator, INamedTypeSymbol classType, string disposeMethodDisplayString);
 
    protected abstract bool IsTypeInInterfaceBaseList([NotNullWhen(true)] SyntaxNode? type);
    protected abstract void AddInterfaceTypes(TTypeDeclarationSyntax typeDeclaration, ArrayBuilder<SyntaxNode> result);
 
    public ImmutableArray<SyntaxNode> GetInterfaceTypes(SyntaxNode typeDeclaration)
    {
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var result);
        if (typeDeclaration is TTypeDeclarationSyntax typeSyntax)
            AddInterfaceTypes(typeSyntax, result);
 
        return result.ToImmutableAndClear();
    }
 
    public async Task<Document> ImplementInterfaceAsync(
        Document document, ImplementTypeOptions options, SyntaxNode node, CancellationToken cancellationToken)
    {
        using (Logger.LogBlock(FunctionId.Refactoring_ImplementInterface, cancellationToken))
        {
            var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var state = State.Generate(this, document, model, node, cancellationToken);
            if (state == null)
                return document;
 
            // While implementing just one default action, like in the case of pressing enter after interface name in VB,
            // choose to implement with the dispose pattern as that's the Dev12 behavior.
            var implementDisposePattern = ShouldImplementDisposePattern(model.Compilation, state.Info, explicitly: false);
            var generator = new ImplementInterfaceGenerator(
                this, document, state.Info, options, new() { OnlyRemaining = true, ImplementDisposePattern = implementDisposePattern });
 
            return await generator.ImplementInterfaceAsync(cancellationToken).ConfigureAwait(false);
        }
    }
 
    private async Task<ImplementInterfaceInfo?> AnalyzeAsync(Document document, SyntaxNode interfaceType, CancellationToken cancellationToken)
    {
        var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        return State.Generate(this, document, model, interfaceType, cancellationToken)?.Info;
    }
 
    protected TNode AddComment<TNode>(string comment, TNode node) where TNode : SyntaxNode
        => AddComments([comment], node);
 
    protected TNode AddComments<TNode>(string comment1, string comment2, TNode node) where TNode : SyntaxNode
        => AddComments([comment1, comment2], node);
 
    protected TNode AddComments<TNode>(string[] comments, TNode node) where TNode : SyntaxNode
        => node.WithPrependedLeadingTrivia(CreateCommentTrivia(comments));
 
    protected SyntaxTriviaList CreateCommentTrivia(
        params ReadOnlySpan<string> comments)
    {
        using var _ = ArrayBuilder<SyntaxTrivia>.GetInstance(out var trivia);
 
        foreach (var comment in comments)
        {
            trivia.Add(this.SyntaxGeneratorInternal.SingleLineComment(" " + comment));
            trivia.Add(this.SyntaxGeneratorInternal.ElasticCarriageReturnLineFeed);
        }
 
        return [.. trivia];
    }
 
    private async Task<Document> ImplementInterfaceAsync(
        Document document,
        ImplementInterfaceInfo info,
        ImplementTypeOptions options,
        ImplementInterfaceConfiguration configuration,
        CancellationToken cancellationToken)
    {
        var generator = new ImplementInterfaceGenerator(
            this, document, info, options, configuration);
        return await generator.ImplementInterfaceAsync(cancellationToken).ConfigureAwait(false);
    }
 
    public ImmutableArray<ISymbol> ImplementInterfaceMember(
        Document document,
        ImplementInterfaceInfo info,
        ImplementTypeOptions options,
        ImplementInterfaceConfiguration configuration,
        Compilation compilation,
        ISymbol interfaceMember)
    {
        var generator = new ImplementInterfaceGenerator(
            this, document, info, options, configuration);
 
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var supportsImplementingLessAccessibleMember = syntaxFacts.SupportsImplicitImplementationOfNonPublicInterfaceMembers(document.Project.ParseOptions!);
        var implementedMembers = generator.GenerateMembers(
            compilation,
            interfaceMember,
            conflictingMember: null,
            memberName: interfaceMember.Name,
            generateInvisibly: generator.ShouldGenerateInvisibleMember(document.Project.ParseOptions!, interfaceMember, interfaceMember.Name, supportsImplementingLessAccessibleMember),
            generateAbstractly: configuration.Abstractly,
            addNew: false,
            interfaceMember.RequiresUnsafeModifier() && !syntaxFacts.IsUnsafeContext(info.ContextNode),
            options.PropertyGenerationBehavior);
 
        return implementedMembers;
    }
 
    public async Task<ImmutableArray<CodeAction>> GetCodeActionsAsync(Document document, SyntaxNode? interfaceType, CancellationToken cancellationToken)
    {
        var options = await document.GetImplementTypeOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        if (!this.IsTypeInInterfaceBaseList(interfaceType))
            return [];
 
        var info = await this.AnalyzeAsync(
            document, interfaceType, cancellationToken).ConfigureAwait(false);
        if (info is null)
            return [];
 
        using var _ = ArrayBuilder<CodeAction>.GetInstance(out var codeActions);
        await foreach (var implementOptions in GetImplementOptionsAsync(document, info, cancellationToken).ConfigureAwait(false))
        {
            var title = GetTitle(implementOptions);
            var equivalenceKey = GetEquivalenceKey(info, implementOptions);
            codeActions.Add(CodeAction.Create(
                title,
                cancellationToken => this.ImplementInterfaceAsync(
                    document, info, options, implementOptions, cancellationToken),
                equivalenceKey));
        }
 
        return codeActions.ToImmutableAndClear();
    }
 
    private static async IAsyncEnumerable<ImplementInterfaceConfiguration> GetImplementOptionsAsync(
        Document document, ImplementInterfaceInfo state, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var supportsImplicitImplementationOfNonPublicInterfaceMembers = syntaxFacts.SupportsImplicitImplementationOfNonPublicInterfaceMembers(document.Project.ParseOptions!);
        if (state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented.Length > 0)
        {
            var totalMemberCount = 0;
            var inaccessibleMemberCount = 0;
 
            foreach (var (_, members) in state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented)
            {
                foreach (var member in members)
                {
                    totalMemberCount++;
 
                    if (ContainsTypeLessAccessibleThan(member, state.ClassOrStructType, supportsImplicitImplementationOfNonPublicInterfaceMembers))
                        inaccessibleMemberCount++;
                }
            }
 
            // If all members to implement are inaccessible, then "Implement interface" codeaction
            // will be the same as "Implement interface explicitly", so there is no point in having both of them
            if (totalMemberCount != inaccessibleMemberCount)
                yield return new() { OnlyRemaining = true };
 
            if (ShouldImplementDisposePattern(compilation, state, explicitly: false))
                yield return new() { OnlyRemaining = true, ImplementDisposePattern = true, };
 
            var delegatableMembers = GetDelegatableMembers(document, state, cancellationToken);
            foreach (var member in delegatableMembers)
                yield return new() { ThroughMember = member };
 
            if (state.ClassOrStructType.IsAbstract)
                yield return new() { OnlyRemaining = true, Abstractly = true };
        }
 
        if (state.MembersWithoutExplicitImplementation.Length > 0)
        {
            yield return new() { Explicitly = true };
 
            if (ShouldImplementDisposePattern(compilation, state, explicitly: true))
                yield return new() { ImplementDisposePattern = true, Explicitly = true };
        }
 
        if (AnyImplementedImplicitly(state))
            yield return new() { OnlyRemaining = true, Explicitly = true };
    }
 
    private static string GetTitle(ImplementInterfaceConfiguration options)
    {
        if (options.ImplementDisposePattern)
        {
            return options.Explicitly
                ? CodeFixesResources.Implement_interface_explicitly_with_Dispose_pattern
                : CodeFixesResources.Implement_interface_with_Dispose_pattern;
        }
        else if (options.Explicitly)
        {
            return options.OnlyRemaining
                ? CodeFixesResources.Implement_remaining_members_explicitly
                : CodeFixesResources.Implement_all_members_explicitly;
        }
        else if (options.Abstractly)
        {
            return CodeFixesResources.Implement_interface_abstractly;
        }
        else if (options.ThroughMember != null)
        {
            return string.Format(CodeFixesResources.Implement_interface_through_0, options.ThroughMember.Name);
        }
        else
        {
            return CodeFixesResources.Implement_interface;
        }
    }
 
    private static string GetEquivalenceKey(
        ImplementInterfaceInfo state,
        ImplementInterfaceConfiguration options)
    {
        var interfaceType = state.InterfaceTypes.First();
        var typeName = interfaceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 
        // Legacy part of the equivalence key.  Kept the same to avoid test churn.
        var codeActionTypeName = options.ImplementDisposePattern
            ? "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceWithDisposePatternCodeAction"
            : "Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction";
 
        // Consider code actions equivalent if they correspond to the same interface being implemented elsewhere
        // in the same manner.  Note: 'implement through member' means implementing the same interface through
        // an applicable member with the same name in the destination.
        return options.Explicitly.ToString() + ";" +
           options.Abstractly.ToString() + ";" +
           options.OnlyRemaining.ToString() + ":" +
           typeName + ";" +
           codeActionTypeName + ";" +
           options.ThroughMember?.Name;
    }
 
    private static bool AnyImplementedImplicitly(ImplementInterfaceInfo state)
    {
        if (state.MembersWithoutExplicitOrImplicitImplementation.Length != state.MembersWithoutExplicitImplementation.Length)
        {
            return true;
        }
 
        for (var i = 0; i < state.MembersWithoutExplicitOrImplicitImplementation.Length; i++)
        {
            var (typeA, membersA) = state.MembersWithoutExplicitOrImplicitImplementation[i];
            var (typeB, membersB) = state.MembersWithoutExplicitImplementation[i];
            if (!typeA.Equals(typeB))
            {
                return true;
            }
 
            if (!membersA.SequenceEqual(membersB))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static ImmutableArray<ISymbol> GetDelegatableMembers(
        Document document, ImplementInterfaceInfo state, CancellationToken cancellationToken)
    {
        var firstInterfaceType = state.InterfaceTypes.First();
 
        return ImplementHelpers.GetDelegatableMembers(
            document,
            state.ClassOrStructType,
            t => t.GetAllInterfacesIncludingThis().Contains(firstInterfaceType),
            cancellationToken);
    }
}