File: ExtractInterface\AbstractExtractInterfaceService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractInterface;
 
internal abstract class AbstractExtractInterfaceService : ILanguageService
{
    protected abstract Task<SyntaxNode> GetTypeDeclarationAsync(
        Document document,
        int position,
        TypeDiscoveryRule typeDiscoveryRule,
        CancellationToken cancellationToken);
 
    protected abstract Task<Solution> UpdateMembersWithExplicitImplementationsAsync(
        Solution unformattedSolution,
        IReadOnlyList<DocumentId> documentId,
        INamedTypeSymbol extractedInterfaceSymbol,
        INamedTypeSymbol typeToExtractFrom,
        IEnumerable<ISymbol> includedMembers,
        ImmutableDictionary<ISymbol, SyntaxAnnotation> symbolToDeclarationAnnotationMap,
        CancellationToken cancellationToken);
 
    internal abstract string GetContainingNamespaceDisplay(INamedTypeSymbol typeSymbol, CompilationOptions compilationOptions);
 
    internal abstract bool ShouldIncludeAccessibilityModifier(SyntaxNode typeNode);
 
    public async Task<ImmutableArray<ExtractInterfaceCodeAction>> GetExtractInterfaceCodeActionAsync(Document document, TextSpan span, CancellationToken cancellationToken)
    {
        var typeAnalysisResult = await AnalyzeTypeAtPositionAsync(document, span.Start, TypeDiscoveryRule.TypeNameOnly, cancellationToken).ConfigureAwait(false);
 
        return typeAnalysisResult.CanExtractInterface
            ? [new ExtractInterfaceCodeAction(this, typeAnalysisResult)]
            : [];
    }
 
    public async Task<ExtractInterfaceResult> ExtractInterfaceAsync(
        Document documentWithTypeToExtractFrom,
        int position,
        Action<string, NotificationSeverity> errorHandler,
        CancellationToken cancellationToken)
    {
        var typeAnalysisResult = await AnalyzeTypeAtPositionAsync(
            documentWithTypeToExtractFrom,
            position,
            TypeDiscoveryRule.TypeDeclaration,
            cancellationToken).ConfigureAwait(false);
 
        if (!typeAnalysisResult.CanExtractInterface)
        {
            errorHandler(typeAnalysisResult.ErrorMessage, NotificationSeverity.Error);
            return new ExtractInterfaceResult(succeeded: false);
        }
 
        return await ExtractInterfaceFromAnalyzedTypeAsync(typeAnalysisResult, cancellationToken).ConfigureAwait(false);
    }
 
    public async Task<ExtractInterfaceTypeAnalysisResult> AnalyzeTypeAtPositionAsync(
        Document document,
        int position,
        TypeDiscoveryRule typeDiscoveryRule,
        CancellationToken cancellationToken)
    {
        var typeNode = await GetTypeDeclarationAsync(document, position, typeDiscoveryRule, cancellationToken).ConfigureAwait(false);
        if (typeNode == null)
        {
            var errorMessage = FeaturesResources.Could_not_extract_interface_colon_The_selection_is_not_inside_a_class_interface_struct;
            return new ExtractInterfaceTypeAnalysisResult(errorMessage);
        }
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        if (semanticModel.GetDeclaredSymbol(typeNode, cancellationToken) is not INamedTypeSymbol typeToExtractFrom)
        {
            var errorMessage = FeaturesResources.Could_not_extract_interface_colon_The_selection_is_not_inside_a_class_interface_struct;
            return new ExtractInterfaceTypeAnalysisResult(errorMessage);
        }
 
        var extractableMembers = typeToExtractFrom.GetMembers().Where(IsExtractableMember);
        if (!extractableMembers.Any())
        {
            var errorMessage = FeaturesResources.Could_not_extract_interface_colon_The_type_does_not_contain_any_member_that_can_be_extracted_to_an_interface;
            return new ExtractInterfaceTypeAnalysisResult(errorMessage);
        }
 
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        return new ExtractInterfaceTypeAnalysisResult(document, typeNode, typeToExtractFrom, extractableMembers, formattingOptions);
    }
 
    public async Task<ExtractInterfaceResult> ExtractInterfaceFromAnalyzedTypeAsync(
        ExtractInterfaceTypeAnalysisResult refactoringResult,
        CancellationToken cancellationToken)
    {
        var containingNamespaceDisplay = refactoringResult.TypeToExtractFrom.ContainingNamespace.IsGlobalNamespace
            ? string.Empty
            : refactoringResult.TypeToExtractFrom.ContainingNamespace.ToDisplayString();
 
        var extractInterfaceOptions = GetExtractInterfaceOptions(
            refactoringResult.DocumentToExtractFrom,
            refactoringResult.TypeToExtractFrom,
            refactoringResult.ExtractableMembers,
            containingNamespaceDisplay,
            refactoringResult.FormattingOptions,
            cancellationToken);
 
        if (extractInterfaceOptions.IsCancelled)
        {
            return new ExtractInterfaceResult(succeeded: false);
        }
 
        return await ExtractInterfaceFromAnalyzedTypeAsync(refactoringResult, extractInterfaceOptions, cancellationToken).ConfigureAwait(false);
    }
 
    public async Task<ExtractInterfaceResult> ExtractInterfaceFromAnalyzedTypeAsync(
        ExtractInterfaceTypeAnalysisResult refactoringResult,
        ExtractInterfaceOptionsResult extractInterfaceOptions,
        CancellationToken cancellationToken)
    {
        var solution = refactoringResult.DocumentToExtractFrom.Project.Solution;
 
        var extractedInterfaceSymbol = CodeGenerationSymbolFactory.CreateNamedTypeSymbol(
            attributes: default,
            accessibility: ShouldIncludeAccessibilityModifier(refactoringResult.TypeNode) ? refactoringResult.TypeToExtractFrom.DeclaredAccessibility : Accessibility.NotApplicable,
            modifiers: new DeclarationModifiers(),
            typeKind: TypeKind.Interface,
            name: extractInterfaceOptions.InterfaceName,
            typeParameters: ExtractTypeHelpers.GetRequiredTypeParametersForMembers(refactoringResult.TypeToExtractFrom, extractInterfaceOptions.IncludedMembers),
            members: CreateInterfaceMembers(extractInterfaceOptions.IncludedMembers));
 
        switch (extractInterfaceOptions.Location)
        {
            case ExtractInterfaceOptionsResult.ExtractLocation.NewFile:
                var containingNamespaceDisplay = GetContainingNamespaceDisplay(refactoringResult.TypeToExtractFrom, refactoringResult.DocumentToExtractFrom.Project.CompilationOptions!);
                return await ExtractInterfaceToNewFileAsync(
                    solution,
                    containingNamespaceDisplay,
                    extractedInterfaceSymbol,
                    refactoringResult,
                    extractInterfaceOptions,
                    cancellationToken).ConfigureAwait(false);
 
            case ExtractInterfaceOptionsResult.ExtractLocation.SameFile:
                return await ExtractInterfaceToSameFileAsync(
                    solution,
                    refactoringResult,
                    extractedInterfaceSymbol,
                    extractInterfaceOptions,
                    cancellationToken).ConfigureAwait(false);
 
            default: throw new InvalidOperationException($"Unable to extract interface for operation of type {extractInterfaceOptions.GetType()}");
        }
    }
 
    private async Task<ExtractInterfaceResult> ExtractInterfaceToNewFileAsync(
        Solution solution, string containingNamespaceDisplay, INamedTypeSymbol extractedInterfaceSymbol,
        ExtractInterfaceTypeAnalysisResult refactoringResult, ExtractInterfaceOptionsResult extractInterfaceOptions,
        CancellationToken cancellationToken)
    {
        var symbolMapping = await AnnotatedSymbolMapping.CreateAsync(
            extractInterfaceOptions.IncludedMembers,
            solution,
            refactoringResult.TypeNode,
            cancellationToken).ConfigureAwait(false);
 
        var (unformattedInterfaceDocument, _) = await ExtractTypeHelpers.AddTypeToNewFileAsync(
            symbolMapping.AnnotatedSolution,
            containingNamespaceDisplay,
            extractInterfaceOptions.FileName,
            refactoringResult.DocumentToExtractFrom.Project.Id,
            refactoringResult.DocumentToExtractFrom.Folders,
            extractedInterfaceSymbol,
            refactoringResult.DocumentToExtractFrom,
            cancellationToken).ConfigureAwait(false);
 
        var completedUnformattedSolution = await GetSolutionWithOriginalTypeUpdatedAsync(
            unformattedInterfaceDocument.Project.Solution,
            symbolMapping.DocumentIdsToSymbolMap.Keys.ToImmutableArray(),
            symbolMapping.TypeNodeAnnotation,
            refactoringResult.TypeToExtractFrom,
            extractedInterfaceSymbol,
            extractInterfaceOptions.IncludedMembers,
            symbolMapping.SymbolToDeclarationAnnotationMap,
            cancellationToken).ConfigureAwait(false);
 
        var completedSolution = await GetFormattedSolutionAsync(
            completedUnformattedSolution,
            symbolMapping.DocumentIdsToSymbolMap.Keys.Concat(unformattedInterfaceDocument.Id),
            cancellationToken).ConfigureAwait(false);
 
        return new ExtractInterfaceResult(
            succeeded: true,
            updatedSolution: completedSolution,
            navigationDocumentId: unformattedInterfaceDocument.Id);
    }
 
    private async Task<ExtractInterfaceResult> ExtractInterfaceToSameFileAsync(
        Solution solution, ExtractInterfaceTypeAnalysisResult refactoringResult, INamedTypeSymbol extractedInterfaceSymbol,
        ExtractInterfaceOptionsResult extractInterfaceOptions, CancellationToken cancellationToken)
    {
        // Track all of the symbols we need to modify, which includes the original type declaration being modified
        var symbolMapping = await AnnotatedSymbolMapping.CreateAsync(
            extractInterfaceOptions.IncludedMembers,
            solution,
            refactoringResult.TypeNode,
            cancellationToken).ConfigureAwait(false);
 
        var document = symbolMapping.AnnotatedSolution.GetDocument(refactoringResult.DocumentToExtractFrom.Id);
 
        var (documentWithInterface, _) = await ExtractTypeHelpers.AddTypeToExistingFileAsync(
            document,
            extractedInterfaceSymbol,
            symbolMapping,
            cancellationToken).ConfigureAwait(false);
 
        var unformattedSolution = documentWithInterface.Project.Solution;
 
        // After the interface is inserted, update the original type to show it implements the new interface
        var unformattedSolutionWithUpdatedType = await GetSolutionWithOriginalTypeUpdatedAsync(
            unformattedSolution, symbolMapping.DocumentIdsToSymbolMap.Keys.ToImmutableArray(),
            symbolMapping.TypeNodeAnnotation,
            refactoringResult.TypeToExtractFrom, extractedInterfaceSymbol,
            extractInterfaceOptions.IncludedMembers, symbolMapping.SymbolToDeclarationAnnotationMap, cancellationToken).ConfigureAwait(false);
 
        var completedSolution = await GetFormattedSolutionAsync(
            unformattedSolutionWithUpdatedType,
            symbolMapping.DocumentIdsToSymbolMap.Keys.Concat(refactoringResult.DocumentToExtractFrom.Id),
            cancellationToken).ConfigureAwait(false);
 
        return new ExtractInterfaceResult(
            succeeded: true,
            updatedSolution: completedSolution,
            navigationDocumentId: refactoringResult.DocumentToExtractFrom.Id);
    }
 
    internal static ExtractInterfaceOptionsResult GetExtractInterfaceOptions(
        Document document,
        INamedTypeSymbol type,
        IEnumerable<ISymbol> extractableMembers,
        string containingNamespace,
        SyntaxFormattingOptions formattingOptions,
        CancellationToken cancellationToken)
    {
        var conflictingTypeNames = type.ContainingNamespace.GetAllTypes(cancellationToken).Select(t => t.Name);
        var candidateInterfaceName = type.TypeKind == TypeKind.Interface ? type.Name : "I" + type.Name;
        var defaultInterfaceName = NameGenerator.GenerateUniqueName(candidateInterfaceName, name => !conflictingTypeNames.Contains(name));
        var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var notificationService = document.Project.Solution.Services.GetRequiredService<INotificationService>();
        var generatedNameTypeParameterSuffix = ExtractTypeHelpers.GetTypeParameterSuffix(document, formattingOptions, type, extractableMembers, cancellationToken);
 
        var service = document.Project.Solution.Services.GetRequiredService<IExtractInterfaceOptionsService>();
        return service.GetExtractInterfaceOptions(
            syntaxFactsService,
            notificationService,
            extractableMembers.ToList(),
            defaultInterfaceName,
            conflictingTypeNames.ToList(),
            containingNamespace,
            generatedNameTypeParameterSuffix,
            document.Project.Language,
            cancellationToken);
    }
 
    private static async Task<Solution> GetFormattedSolutionAsync(Solution unformattedSolution, IEnumerable<DocumentId> documentIds, CancellationToken cancellationToken)
    {
        // Since code action performs formatting and simplification on a single document, 
        // this ensures that anything marked with formatter or simplifier annotations gets 
        // correctly handled as long as it it's in the listed documents
        var formattedSolution = unformattedSolution;
        foreach (var documentId in documentIds)
        {
            var document = formattedSolution.GetRequiredDocument(documentId);
 
            var cleanupOptions = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
 
            var formattedDocument = await Formatter.FormatAsync(
                document,
                Formatter.Annotation,
                cleanupOptions.FormattingOptions,
                cancellationToken).ConfigureAwait(false);
 
            var simplifiedDocument = await Simplifier.ReduceAsync(
                formattedDocument,
                Simplifier.Annotation,
                cleanupOptions.SimplifierOptions,
                cancellationToken).ConfigureAwait(false);
 
            formattedSolution = simplifiedDocument.Project.Solution;
        }
 
        return formattedSolution;
    }
 
    private async Task<Solution> GetSolutionWithOriginalTypeUpdatedAsync(
        Solution solution,
        ImmutableArray<DocumentId> documentIds,
        SyntaxAnnotation typeNodeAnnotation,
        INamedTypeSymbol typeToExtractFrom,
        INamedTypeSymbol extractedInterfaceSymbol,
        IEnumerable<ISymbol> includedMembers,
        ImmutableDictionary<ISymbol, SyntaxAnnotation> symbolToDeclarationAnnotationMap,
        CancellationToken cancellationToken)
    {
        // If an interface "INewInterface" is extracted from an interface "IExistingInterface",
        // then "INewInterface" is not marked as implementing "IExistingInterface" and its 
        // extracted members are also not updated.
        if (typeToExtractFrom.TypeKind == TypeKind.Interface)
        {
            return solution;
        }
 
        var unformattedSolution = solution;
        foreach (var documentId in documentIds)
        {
            var document = solution.GetRequiredDocument(documentId);
            var currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var editor = new SyntaxEditor(currentRoot, solution.Services);
 
            var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
            var typeReference = syntaxGenerator.TypeExpression(extractedInterfaceSymbol);
 
            var typeDeclaration = currentRoot.GetAnnotatedNodes(typeNodeAnnotation).SingleOrDefault();
 
            if (typeDeclaration == null)
            {
                continue;
            }
 
            var unformattedTypeDeclaration = syntaxGenerator.AddInterfaceType(typeDeclaration, typeReference).WithAdditionalAnnotations(Formatter.Annotation);
            editor.ReplaceNode(typeDeclaration, unformattedTypeDeclaration);
 
            unformattedSolution = document.WithSyntaxRoot(editor.GetChangedRoot()).Project.Solution;
 
            // Only update the first instance of the typedeclaration,
            // since it's not needed in all declarations
            break;
        }
 
        var updatedUnformattedSolution = await UpdateMembersWithExplicitImplementationsAsync(
            unformattedSolution,
            documentIds,
            extractedInterfaceSymbol,
            typeToExtractFrom,
            includedMembers,
            symbolToDeclarationAnnotationMap,
            cancellationToken).ConfigureAwait(false);
 
        return updatedUnformattedSolution;
    }
 
    private static ImmutableArray<ISymbol> CreateInterfaceMembers(IEnumerable<ISymbol> includedMembers)
    {
        using var _ = ArrayBuilder<ISymbol>.GetInstance(out var interfaceMembers);
 
        foreach (var member in includedMembers)
        {
            switch (member)
            {
                case IEventSymbol @event:
                    interfaceMembers.Add(CodeGenerationSymbolFactory.CreateEventSymbol(
                        attributes: [],
                        accessibility: Accessibility.Public,
                        modifiers: new DeclarationModifiers(isAbstract: true, isStatic: member.IsStatic),
                        type: @event.Type,
                        explicitInterfaceImplementations: default,
                        name: @event.Name));
                    break;
                case IMethodSymbol method:
                    interfaceMembers.Add(CodeGenerationSymbolFactory.CreateMethodSymbol(
                        attributes: [],
                        accessibility: Accessibility.Public,
                        modifiers: new DeclarationModifiers(isAbstract: true, isStatic: member.IsStatic, isUnsafe: method.RequiresUnsafeModifier()),
                        returnType: method.ReturnType,
                        refKind: method.RefKind,
                        explicitInterfaceImplementations: default,
                        name: method.Name,
                        typeParameters: method.TypeParameters,
                        parameters: method.Parameters,
                        isInitOnly: method.IsInitOnly));
                    break;
                case IPropertySymbol property:
                    // We recreate the get accessor because it is possible it has the readonly modifier due
                    // to being an auto property on a struct which is invalid for an interface member
 
                    var getMethod = property.GetMethod != null && property.GetMethod.DeclaredAccessibility == Accessibility.Public
                        ? CodeGenerationSymbolFactory.CreateAccessorSymbol(property.GetMethod, property.GetMethod.GetAttributes())
                        : null;
 
                    interfaceMembers.Add(CodeGenerationSymbolFactory.CreatePropertySymbol(
                        attributes: [],
                        accessibility: Accessibility.Public,
                        modifiers: new DeclarationModifiers(isAbstract: true, isStatic: member.IsStatic, isUnsafe: property.RequiresUnsafeModifier()),
                        type: property.Type,
                        refKind: property.RefKind,
                        explicitInterfaceImplementations: default,
                        name: property.Name,
                        parameters: property.Parameters,
                        getMethod: getMethod,
                        setMethod: property is { SetMethod.DeclaredAccessibility: Accessibility.Public } ? property.SetMethod : null,
                        isIndexer: property.IsIndexer));
                    break;
                default:
                    Debug.Assert(false, string.Format(FeaturesResources.Unexpected_interface_member_kind_colon_0, member.Kind.ToString()));
                    break;
            }
        }
 
        return interfaceMembers.ToImmutableAndClear();
    }
 
    internal virtual bool IsExtractableMember(ISymbol m)
    {
        if (m.DeclaredAccessibility != Accessibility.Public ||
            m.Name == "<Clone>$") // TODO: Use WellKnownMemberNames.CloneMethodName when it's public.
        {
            return false;
        }
 
        if (m.Kind == SymbolKind.Event || m.IsOrdinaryMethod())
            return true;
 
        if (m is IPropertySymbol prop)
        {
            return !prop.IsWithEvents &&
                (prop is { GetMethod.DeclaredAccessibility: Accessibility.Public } ||
                 prop is { SetMethod.DeclaredAccessibility: Accessibility.Public });
        }
 
        return false;
    }
}