File: ExtractClass\AbstractExtractClassRefactoringProvider.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.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PullMemberUp;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractClass;
 
internal abstract class AbstractExtractClassRefactoringProvider(IExtractClassOptionsService? service) : CodeRefactoringProvider
{
    private readonly IExtractClassOptionsService? _optionsService = service;
 
    protected abstract Task<ImmutableArray<SyntaxNode>> GetSelectedNodesAsync(CodeRefactoringContext context);
    protected abstract Task<SyntaxNode?> GetSelectedClassDeclarationAsync(CodeRefactoringContext context);
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        // For simplicity if we can't add a document the don't supply this refactoring. Not checking this results in known
        // cases that won't work because the refactoring may try to add a document. There's non-trivial
        // work to support a user interaction that makes sense for those cases. 
        // See: https://github.com/dotnet/roslyn/issues/50868
        var solution = context.Document.Project.Solution;
        if (!solution.CanApplyChange(ApplyChangesKind.AddDocument))
        {
            return;
        }
 
        var optionsService = _optionsService ?? solution.Services.GetService<IExtractClassOptionsService>();
        if (optionsService is null)
        {
            return;
        }
 
        var (action, hasBaseType) = await TryGetMemberActionAsync(context, optionsService).ConfigureAwait(false);
 
        // If the action was not offered because we know the containing type
        // already has a base class, no need to do extra work to see if just a class is selected
        if (action is null && !hasBaseType)
        {
            action = await TryGetClassActionAsync(context, optionsService).ConfigureAwait(false);
        }
 
        if (action != null)
        {
            context.RegisterRefactoring(action, action.Span);
        }
    }
 
    private async Task<(ExtractClassWithDialogCodeAction? action, bool hasBaseType)> TryGetMemberActionAsync(CodeRefactoringContext context, IExtractClassOptionsService optionsService)
    {
        var selectedMemberNodes = await GetSelectedNodesAsync(context).ConfigureAwait(false);
        if (selectedMemberNodes.IsEmpty)
        {
            return (null, false);
        }
 
        var (document, span, cancellationToken) = context;
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var memberNodeSymbolPairs = selectedMemberNodes
            .SelectAsArray(m => (node: m, symbol: semanticModel.GetRequiredDeclaredSymbol(m, cancellationToken)))
            // Use same logic as pull members up for determining if a selected member
            // is valid to be moved into a base
            .WhereAsArray(pair => MemberAndDestinationValidator.IsMemberValid(pair.symbol));
 
        if (memberNodeSymbolPairs.IsEmpty)
        {
            return (null, false);
        }
 
        var selectedMembers = memberNodeSymbolPairs.SelectAsArray(pair => pair.symbol);
 
        var containingType = selectedMembers.First().ContainingType;
        Contract.ThrowIfNull(containingType);
 
        // Treat the entire nodes' span as the span of interest here.  That way if the user's location is closer to
        // a refactoring with a narrower span (for example, a span just on the name/parameters of a member, then it
        // will take precedence over us).
        var memberSpan = TextSpan.FromBounds(
            memberNodeSymbolPairs.First().node.FullSpan.Start,
            memberNodeSymbolPairs.Last().node.FullSpan.End);
 
        if (HasBaseType(containingType))
        {
            return (null, true);
        }
 
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var containingTypeDeclarationNode = selectedMemberNodes.First().FirstAncestorOrSelf<SyntaxNode>(syntaxFacts.IsTypeDeclaration);
        if (containingTypeDeclarationNode is null)
        {
            // If the containing type node isn't found exit. This could be malformed code that we don't know
            // how to correctly handle
            return (null, false);
        }
 
        if (selectedMemberNodes.Any(m => m.FirstAncestorOrSelf<SyntaxNode>(syntaxFacts.IsTypeDeclaration) != containingTypeDeclarationNode))
        {
            return (null, false);
        }
 
        var action = new ExtractClassWithDialogCodeAction(
            document, memberSpan, optionsService, containingType, containingTypeDeclarationNode, selectedMembers);
 
        return (action, false);
    }
 
    private async Task<ExtractClassWithDialogCodeAction?> TryGetClassActionAsync(CodeRefactoringContext context, IExtractClassOptionsService optionsService)
    {
        var selectedClassNode = await GetSelectedClassDeclarationAsync(context).ConfigureAwait(false);
        if (selectedClassNode is null)
        {
            return null;
        }
 
        var (document, span, cancellationToken) = context;
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        if (semanticModel.GetDeclaredSymbol(selectedClassNode, cancellationToken) is not INamedTypeSymbol selectedType)
        {
            return null;
        }
 
        if (HasBaseType(selectedType))
        {
            return null;
        }
 
        return new ExtractClassWithDialogCodeAction(
            document, span, optionsService, selectedType, selectedClassNode, selectedMembers: []);
    }
 
    private static bool HasBaseType(INamedTypeSymbol containingType) => containingType.BaseType?.SpecialType != SpecialType.System_Object;
}