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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
internal abstract partial class SelectionValidator<
    TSelectionResult,
    TStatementSyntax>(
        SemanticDocument document,
        TextSpan textSpan)
    where TSelectionResult : SelectionResult<TStatementSyntax>
    where TStatementSyntax : SyntaxNode
{
    protected readonly SemanticDocument SemanticDocument = document;
    protected readonly TextSpan OriginalSpan = textSpan;
 
    public bool ContainsValidSelection => !OriginalSpan.IsEmpty;
 
    public abstract Task<(TSelectionResult, OperationStatus)> GetValidSelectionAsync(CancellationToken cancellationToken);
    public abstract IEnumerable<SyntaxNode> GetOuterReturnStatements(SyntaxNode commonRoot, IEnumerable<SyntaxNode> jumpsOutOfRegion);
    public abstract bool IsFinalSpanSemanticallyValidSpan(SyntaxNode node, TextSpan textSpan, IEnumerable<SyntaxNode> returnStatements, CancellationToken cancellationToken);
    public abstract bool ContainsNonReturnExitPointsStatements(IEnumerable<SyntaxNode> jumpsOutOfRegion);
 
    protected bool IsFinalSpanSemanticallyValidSpan(
        SemanticModel semanticModel, TextSpan textSpan, (SyntaxNode, SyntaxNode) range, CancellationToken cancellationToken)
    {
        var controlFlowAnalysisData = semanticModel.AnalyzeControlFlow(range.Item1, range.Item2);
 
        // there must be no control in and out of given span
        if (controlFlowAnalysisData.EntryPoints.Any())
        {
            return false;
        }
 
        // check something like continue, break, yield break, yield return, and etc
        if (ContainsNonReturnExitPointsStatements(controlFlowAnalysisData.ExitPoints))
        {
            return false;
        }
 
        // okay, there is no branch out, check whether next statement can be executed normally
        var returnStatements = GetOuterReturnStatements(range.Item1.GetCommonRoot(range.Item2), controlFlowAnalysisData.ExitPoints);
        if (!returnStatements.Any())
        {
            if (!controlFlowAnalysisData.EndPointIsReachable)
            {
                // REVIEW: should we just do extract method regardless or show some warning to user?
                // in dev10, looks like we went ahead and did the extract method even if selection contains
                // unreachable code.
            }
 
            return true;
        }
 
        // okay, only branch was return. make sure we have all return in the selection.
 
        // check for special case, if end point is not reachable, we don't care the selection
        // actually contains all return statements. we just let extract method go through
        // and work like we did in dev10
        if (!controlFlowAnalysisData.EndPointIsReachable)
        {
            return true;
        }
 
        // there is a return statement, and current position is reachable. let's check whether this is a case where that is okay
        return IsFinalSpanSemanticallyValidSpan(semanticModel.SyntaxTree.GetRoot(cancellationToken), textSpan, returnStatements, cancellationToken);
    }
 
    protected static (T, T)? GetStatementRangeContainingSpan<T>(
        ISyntaxFacts syntaxFacts,
        SyntaxNode root, TextSpan textSpan, CancellationToken cancellationToken) where T : SyntaxNode
    {
        // 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 token1 = root.FindToken(textSpan.Start);
        var token2 = root.FindTokenFromEnd(textSpan.End);
 
        var commonRoot = token1.GetCommonRoot(token2).GetAncestorOrThis<T>() ?? root;
 
        var firstStatement = (T)null;
        var lastStatement = (T)null;
 
        var spine = new List<T>();
 
        foreach (var stmt in commonRoot.DescendantNodesAndSelf().OfType<T>())
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            // quick skip check.
            // - not containing at all
            if (stmt.Span.End < textSpan.Start)
            {
                continue;
            }
 
            // quick exit check
            // - passed candidate statements
            if (textSpan.End < stmt.SpanStart)
            {
                break;
            }
 
            if (stmt.SpanStart <= textSpan.Start)
            {
                // keep track spine
                spine.Add(stmt);
            }
 
            if (textSpan.End <= stmt.Span.End && spine.Any(s => CanMergeExistingSpineWithCurrent(syntaxFacts, s, stmt)))
            {
                // malformed code or selection can make spine to have more than an elements
                firstStatement = spine.First(s => CanMergeExistingSpineWithCurrent(syntaxFacts, s, stmt));
                lastStatement = stmt;
 
                spine.Clear();
            }
        }
 
        if (firstStatement == null || lastStatement == null)
        {
            return null;
        }
 
        return (firstStatement, lastStatement);
 
        static bool CanMergeExistingSpineWithCurrent(ISyntaxFacts syntaxFacts, T existing, T current)
            => syntaxFacts.AreStatementsInSameContainer(existing, current);
    }
 
    protected static (T, T)? GetStatementRangeContainedInSpan<T>(
        SyntaxNode root, TextSpan textSpan, CancellationToken cancellationToken) where T : SyntaxNode
    {
        // 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<T>() ?? root;
 
        T firstStatement = null;
        T lastStatement = null;
 
        foreach (var stmt in commonRoot.DescendantNodesAndSelf().OfType<T>())
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            if (firstStatement == null && stmt.SpanStart >= textSpan.Start)
            {
                firstStatement = stmt;
            }
 
            if (firstStatement != null && stmt.Span.End <= textSpan.End && stmt.Parent == firstStatement.Parent)
            {
                lastStatement = stmt;
            }
        }
 
        if (firstStatement == null || lastStatement == null)
        {
            return null;
        }
 
        return (firstStatement, lastStatement);
    }
 
    protected sealed class SelectionInfo
    {
        public OperationStatus Status { get; set; }
 
        public TextSpan OriginalSpan { get; set; }
        public TextSpan FinalSpan { get; set; }
 
        public SyntaxNode CommonRootFromOriginalSpan { get; set; }
 
        public SyntaxToken FirstTokenInOriginalSpan { get; set; }
        public SyntaxToken LastTokenInOriginalSpan { get; set; }
 
        public SyntaxToken FirstTokenInFinalSpan { get; set; }
        public SyntaxToken LastTokenInFinalSpan { get; set; }
 
        public bool SelectionInExpression { get; set; }
        public bool SelectionInSingleStatement { get; set; }
 
        public SelectionInfo WithStatus(Func<OperationStatus, OperationStatus> statusGetter)
            => With(s => s.Status = statusGetter(s.Status));
 
        public SelectionInfo With(Action<SelectionInfo> valueSetter)
        {
            var newInfo = Clone();
            valueSetter(newInfo);
            return newInfo;
        }
 
        public SelectionInfo Clone()
            => (SelectionInfo)MemberwiseClone();
    }
}