File: ExtractMethod\SelectionValidator.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
internal abstract partial class AbstractExtractMethodService<
    TStatementSyntax,
    TExecutableStatementSyntax,
    TExpressionSyntax>
{
    public abstract partial class SelectionValidator(
        SemanticDocument document,
        TextSpan textSpan)
    {
        protected readonly SemanticDocument SemanticDocument = document;
        protected readonly TextSpan OriginalSpan = textSpan;
 
        public bool ContainsValidSelection => !OriginalSpan.IsEmpty;
 
        protected abstract TextSpan GetAdjustedSpan(TextSpan textSpan);
 
        protected abstract InitialSelectionInfo GetInitialSelectionInfo(CancellationToken cancellationToken);
 
        protected abstract FinalSelectionInfo UpdateSelectionInfo(InitialSelectionInfo selectionInfo, CancellationToken cancellationToken);
        protected abstract Task<SelectionResult> CreateSelectionResultAsync(FinalSelectionInfo selectionInfo, CancellationToken cancellationToken);
 
        public async Task<(SelectionResult?, OperationStatus)> GetValidSelectionAsync(CancellationToken cancellationToken)
        {
            if (!this.ContainsValidSelection)
                return (null, OperationStatus.FailedWithUnknownReason);
 
            var initialSelectionInfo = this.GetInitialSelectionInfo(cancellationToken);
            if (initialSelectionInfo.Status.Failed)
                return (null, initialSelectionInfo.Status);
 
            var selectionInfo = UpdateSelectionInfo(initialSelectionInfo, cancellationToken);
            if (selectionInfo.Status.Failed)
                return (null, selectionInfo.Status);
 
            if (!selectionInfo.SelectionInExpression &&
                !IsValidStatementRange(SemanticDocument.Root, selectionInfo.FinalSpan, cancellationToken))
            {
                return (null, selectionInfo.Status.With(succeeded: false, FeaturesResources.Cannot_determine_valid_range_of_statements_to_extract));
            }
 
            var selectionResult = await CreateSelectionResultAsync(selectionInfo, cancellationToken).ConfigureAwait(false);
 
            var status = selectionInfo.Status.With(
                selectionResult.ValidateSelectionResult(cancellationToken));
 
            return (selectionResult, status);
        }
 
        private static bool IsValidStatementRange(
            SyntaxNode root, TextSpan textSpan, CancellationToken cancellationToken)
        {
            // use top-down approach to find largest statement range contained in the given span
            // this method is a bit more expensive than bottom-up approach, but way more simpler than the other approach.
            var token1 = root.FindToken(textSpan.Start);
            var token2 = root.FindTokenFromEnd(textSpan.End);
 
            var commonRoot = token1.GetCommonRoot(token2).GetAncestorOrThis<TStatementSyntax>() ?? root;
 
            TStatementSyntax? firstStatement = null;
            TStatementSyntax? lastStatement = null;
 
            foreach (var statement in commonRoot.DescendantNodesAndSelf().OfType<TStatementSyntax>())
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (firstStatement == null && statement.SpanStart >= textSpan.Start)
                    firstStatement = statement;
 
                if (firstStatement != null && statement.Span.End <= textSpan.End && statement.Parent == firstStatement.Parent)
                    lastStatement = statement;
            }
 
            return firstStatement != null && lastStatement != null;
        }
 
        protected FinalSelectionInfo AssignFinalSpan(
             InitialSelectionInfo initialSelectionInfo, FinalSelectionInfo finalSelectionInfo)
        {
            if (finalSelectionInfo.Status.Failed)
                return finalSelectionInfo;
 
            var adjustedSpan = GetAdjustedSpan(OriginalSpan);
 
            // set final span
            var start = initialSelectionInfo.FirstTokenInOriginalSpan == finalSelectionInfo.FirstTokenInFinalSpan
                ? Math.Min(initialSelectionInfo.FirstTokenInOriginalSpan.SpanStart, adjustedSpan.Start)
                : finalSelectionInfo.FirstTokenInFinalSpan.FullSpan.Start;
 
            var end = initialSelectionInfo.LastTokenInOriginalSpan == finalSelectionInfo.LastTokenInFinalSpan
                ? Math.Max(initialSelectionInfo.LastTokenInOriginalSpan.Span.End, adjustedSpan.End)
                : finalSelectionInfo.LastTokenInFinalSpan.Span.End;
 
            return finalSelectionInfo with
            {
                FinalSpan = GetAdjustedSpan(TextSpan.FromBounds(start, end)),
            };
        }
 
        protected InitialSelectionInfo CreateInitialSelectionInfo(
            bool selectionInExpression,
            SyntaxToken firstTokenInOriginalSpan,
            SyntaxToken lastTokenInOriginalSpan,
            CancellationToken cancellationToken)
        {
            if (selectionInExpression)
                return new(OperationStatus.SucceededStatus, firstTokenInOriginalSpan, lastTokenInOriginalSpan, firstStatement: null, lastStatement: null, selectionInExpression: true);
 
            var statements = GetStatementRangeContainingSpan(firstTokenInOriginalSpan, lastTokenInOriginalSpan, cancellationToken);
            if (statements is not var (firstStatement, lastStatement))
                return InitialSelectionInfo.Failure(FeaturesResources.No_valid_statement_range_to_extract);
 
            return new(OperationStatus.SucceededStatus, firstTokenInOriginalSpan, lastTokenInOriginalSpan, firstStatement, lastStatement, selectionInExpression: false);
        }
 
        private (TStatementSyntax firstStatement, TStatementSyntax lastStatement)? GetStatementRangeContainingSpan(
            SyntaxToken firstTokenInOriginalSpan,
            SyntaxToken lastTokenInOriginalSpan,
            CancellationToken cancellationToken)
        {
            var document = this.SemanticDocument;
            var blockFacts = document.GetRequiredLanguageService<IBlockFactsService>();
 
            // use top-down approach to find smallest statement range that contains given span. this approach is more
            // expansive than bottom-up approach I used before but way simpler and easy to understand
            var textSpan = TextSpan.FromBounds(firstTokenInOriginalSpan.SpanStart, lastTokenInOriginalSpan.Span.End);
 
            var root = document.Root;
            var token1 = root.FindToken(textSpan.Start);
            var token2 = root.FindTokenFromEnd(textSpan.End);
 
            var commonRoot = token1.GetCommonRoot(token2).GetAncestorOrThis<TStatementSyntax>() ?? root;
 
            TStatementSyntax? firstStatement = null;
            TStatementSyntax? lastStatement = null;
 
            var spine = new List<TStatementSyntax>();
 
            foreach (var statement in commonRoot.DescendantNodesAndSelf().OfType<TStatementSyntax>())
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                // quick skip check.
                // - not containing at all
                if (statement.Span.End < textSpan.Start)
                    continue;
 
                // quick exit check
                // - passed candidate statements
                if (textSpan.End < statement.SpanStart)
                    break;
 
                if (statement.SpanStart <= textSpan.Start)
                {
                    // keep track spine
                    spine.Add(statement);
                }
 
                if (textSpan.End <= statement.Span.End && spine.Any(s => AreStatementsInSameContainer(s, statement)))
                {
                    // malformed code or selection can make spine to have more than an elements
                    firstStatement = spine.First(s => AreStatementsInSameContainer(s, statement));
                    lastStatement = statement;
 
                    spine.Clear();
                }
            }
 
            if (firstStatement == null || lastStatement == null)
                return null;
 
            return (firstStatement, lastStatement);
 
            bool AreStatementsInSameContainer(TStatementSyntax statement1, TStatementSyntax statement2)
            {
                var parent1 = blockFacts.GetImmediateParentExecutableBlockForStatement(statement1) ?? statement1.Parent;
                var parent2 = blockFacts.GetImmediateParentExecutableBlockForStatement(statement2) ?? statement2.Parent;
 
                return parent1 == parent2;
            }
        }
    }
}