File: ExtractMethod\SelectionResult.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
internal abstract partial class AbstractExtractMethodService<
    TStatementSyntax,
    TExecutableStatementSyntax,
    TExpressionSyntax>
{
    internal abstract class SelectionResult(
        SemanticDocument document,
        SelectionType selectionType,
        TextSpan finalSpan)
    {
        protected static readonly SyntaxAnnotation s_firstTokenAnnotation = new();
        protected static readonly SyntaxAnnotation s_lastTokenAnnotation = new();
 
        private bool? _containsAwaitExpression;
        private bool? _containsConfigureAwaitFalse;
 
        public SemanticDocument SemanticDocument { get; private set; } = document;
        public TextSpan FinalSpan { get; } = finalSpan;
        public SelectionType SelectionType { get; } = selectionType;
 
        /// <summary>
        /// Cached data flow analysis result for the selected code.  Valid for both expressions and statements.
        /// </summary>
        private DataFlowAnalysis? _dataFlowAnalysis;
 
        /// <summary>
        /// Cached information about the control flow of the selected code.  Only valid if the selection covers one or
        /// more statements.
        /// </summary>
        private ControlFlowAnalysis? _statementControlFlowAnalysis;
 
        public abstract TExecutableStatementSyntax GetFirstStatementUnderContainer();
        public abstract TExecutableStatementSyntax GetLastStatementUnderContainer();
 
        public abstract bool ContainingScopeHasAsyncKeyword();
 
        public abstract SyntaxNode GetContainingScope();
        public abstract SyntaxNode GetOutermostCallSiteContainerToProcess(CancellationToken cancellationToken);
 
        protected abstract (ITypeSymbol? returnType, bool returnsByRef) GetReturnTypeInfoWorker(CancellationToken cancellationToken);
 
        public abstract ImmutableArray<TExecutableStatementSyntax> GetOuterReturnStatements(SyntaxNode commonRoot, ImmutableArray<SyntaxNode> jumpsOutOfRegion);
        public abstract bool IsFinalSpanSemanticallyValidSpan(ImmutableArray<TExecutableStatementSyntax> returnStatements, CancellationToken cancellationToken);
        public abstract bool ContainsUnsupportedExitPointsStatements(ImmutableArray<SyntaxNode> exitPoints);
 
        protected abstract OperationStatus ValidateLanguageSpecificRules(CancellationToken cancellationToken);
 
        public (ITypeSymbol returnType, bool returnsByRef) GetReturnTypeInfo(CancellationToken cancellationToken)
        {
            var (returnType, returnsByRef) = GetReturnTypeInfoWorker(cancellationToken);
            return (returnType ?? this.SemanticDocument.SemanticModel.Compilation.GetSpecialType(SpecialType.System_Object), returnsByRef);
        }
 
        public ITypeSymbol GetReturnType(CancellationToken cancellationToken)
            => GetReturnTypeInfo(cancellationToken).returnType;
 
        public bool IsExtractMethodOnExpression => this.SelectionType == SelectionType.Expression;
        public bool IsExtractMethodOnSingleStatement => this.SelectionType == SelectionType.SingleStatement;
        public bool IsExtractMethodOnMultipleStatements => this.SelectionType == SelectionType.MultipleStatements;
 
        protected virtual SyntaxNode GetNodeForDataFlowAnalysis() => GetContainingScope();
 
        public SelectionResult With(SemanticDocument document)
        {
            if (SemanticDocument == document)
            {
                return this;
            }
 
            var clone = (SelectionResult)MemberwiseClone();
            clone.SemanticDocument = document;
 
            return clone;
        }
 
        public SyntaxToken GetFirstTokenInSelection()
            => SemanticDocument.GetTokenWithAnnotation(s_firstTokenAnnotation);
 
        public SyntaxToken GetLastTokenInSelection()
            => SemanticDocument.GetTokenWithAnnotation(s_lastTokenAnnotation);
 
        public TNode? GetContainingScopeOf<TNode>() where TNode : SyntaxNode
        {
            var containingScope = GetContainingScope();
            return containingScope.GetAncestorOrThis<TNode>();
        }
 
        public TExecutableStatementSyntax GetFirstStatement()
        {
            Contract.ThrowIfTrue(IsExtractMethodOnExpression);
 
            var token = GetFirstTokenInSelection();
            return token.GetRequiredAncestor<TExecutableStatementSyntax>();
        }
 
        public TExecutableStatementSyntax GetLastStatement()
        {
            Contract.ThrowIfTrue(IsExtractMethodOnExpression);
 
            var token = GetLastTokenInSelection();
            return token.GetRequiredAncestor<TExecutableStatementSyntax>();
        }
 
        /// <summary>
        /// Checks all of the nodes within the user's selection to see if any of them satisfy the supplied <paramref
        /// name="predicate"/>. Will not descend into local functions or lambdas.
        /// </summary>
        /// <param name="predicate"></param>
        private bool CheckNodesInSelection(Func<ISyntaxFacts, SyntaxNode, bool> predicate)
        {
            var firstToken = this.GetFirstTokenInSelection();
            var lastToken = this.GetLastTokenInSelection();
            var span = TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End);
 
            using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var stack);
            stack.Push(this.GetContainingScope());
 
            var syntaxFacts = this.SemanticDocument.GetRequiredLanguageService<ISyntaxFactsService>();
 
            while (stack.TryPop(out var current))
            {
                // Don't dive into lambdas and local functions.  They reset the async/await context.
                if (syntaxFacts.IsAnonymousOrLocalFunction(current))
                    continue;
 
                if (predicate(syntaxFacts, current))
                    return true;
 
                // Only dive into child nodes within the span being extracted.
                foreach (var childNode in current.ChildNodes())
                {
                    if (childNode.Span.OverlapsWith(span))
                        stack.Push(childNode);
                }
            }
 
            return false;
        }
 
        public bool ContainsAwaitExpression()
        {
            return _containsAwaitExpression ??= CheckNodesInSelection(
                static (syntaxFacts, node) => syntaxFacts.IsAwaitExpression(node));
        }
 
        public bool ContainsConfigureAwaitFalse()
        {
            return _containsConfigureAwaitFalse ??= CheckNodesInSelection(
                static (syntaxFacts, node) => IsConfigureAwaitFalse(syntaxFacts, node));
 
            static bool IsConfigureAwaitFalse(ISyntaxFacts syntaxFacts, SyntaxNode node)
            {
                if (!syntaxFacts.IsInvocationExpression(node))
                    return false;
 
                var invokedExpression = syntaxFacts.GetExpressionOfInvocationExpression(node);
                if (!syntaxFacts.IsSimpleMemberAccessExpression(invokedExpression))
                    return false;
 
                var name = syntaxFacts.GetNameOfMemberAccessExpression(invokedExpression);
                var identifier = syntaxFacts.GetIdentifierOfSimpleName(name);
                if (!syntaxFacts.StringComparer.Equals(identifier.ValueText, nameof(Task.ConfigureAwait)))
                    return false;
 
                var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(node);
                if (arguments.Count != 1)
                    return false;
 
                var expression = syntaxFacts.GetExpressionOfArgument(arguments[0]);
                return syntaxFacts.IsFalseLiteralExpression(expression);
            }
        }
 
        public DataFlowAnalysis GetDataFlowAnalysis()
        {
            return _dataFlowAnalysis ??= ComputeDataFlowAnalysis();
 
            DataFlowAnalysis ComputeDataFlowAnalysis()
            {
                var semanticModel = this.SemanticDocument.SemanticModel;
                if (this.IsExtractMethodOnExpression)
                    return semanticModel.AnalyzeDataFlow(this.GetNodeForDataFlowAnalysis());
 
                var (firstStatement, lastStatement) = this.GetFlowAnalysisNodeRange();
                return semanticModel.AnalyzeDataFlow(firstStatement, lastStatement);
            }
        }
 
        public ControlFlowAnalysis GetStatementControlFlowAnalysis()
        {
            Contract.ThrowIfTrue(IsExtractMethodOnExpression);
            return _statementControlFlowAnalysis ??= ComputeControlFlowAnalysis();
 
            ControlFlowAnalysis ComputeControlFlowAnalysis()
            {
                var (firstStatement, lastStatement) = this.GetFlowAnalysisNodeRange();
                return this.SemanticDocument.SemanticModel.AnalyzeControlFlow(firstStatement, lastStatement);
            }
        }
 
        /// <summary>f
        /// convert text span to node range for the flow analysis API
        /// </summary>
        private (TExecutableStatementSyntax firstStatement, TExecutableStatementSyntax lastStatement) GetFlowAnalysisNodeRange()
        {
            if (this.IsExtractMethodOnSingleStatement)
            {
                var first = this.GetFirstStatement();
                return (first, first);
            }
            else
            {
                // multiple statement case
                return (this.GetFirstStatementUnderContainer(), this.GetLastStatementUnderContainer());
            }
        }
 
        /// <summary>
        /// create a new root node from the given root after adding annotations to the tokens
        /// 
        /// tokens should belong to the given root
        /// </summary>
        protected static SyntaxNode AddAnnotations(SyntaxNode root, IEnumerable<(SyntaxToken, SyntaxAnnotation)> pairs)
        {
            Contract.ThrowIfNull(root);
 
            var tokenMap = pairs.GroupBy(p => p.Item1, p => p.Item2).ToDictionary(g => g.Key, g => g.ToArray());
            return root.ReplaceTokens(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations(tokenMap[o]));
        }
 
        /// <summary>
        /// create a new root node from the given root after adding annotations to the nodes
        /// 
        /// nodes should belong to the given root
        /// </summary>
        protected static SyntaxNode AddAnnotations(SyntaxNode root, IEnumerable<(SyntaxNode, SyntaxAnnotation)> pairs)
        {
            Contract.ThrowIfNull(root);
 
            var tokenMap = pairs.GroupBy(p => p.Item1, p => p.Item2).ToDictionary(g => g.Key, g => g.ToArray());
            return root.ReplaceNodes(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations(tokenMap[o]));
        }
 
        public OperationStatus ValidateSelectionResult(CancellationToken cancellationToken)
        {
            if (!this.IsExtractMethodOnExpression)
            {
                if (!IsFinalSpanSemanticallyValidSpan(cancellationToken))
                    return new(succeeded: true, FeaturesResources.Not_all_code_paths_return);
 
                return ValidateLanguageSpecificRules(cancellationToken);
            }
 
            return OperationStatus.SucceededStatus;
        }
 
        protected bool IsFinalSpanSemanticallyValidSpan(CancellationToken cancellationToken)
        {
            var controlFlowAnalysisData = this.GetStatementControlFlowAnalysis();
 
            // 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 (ContainsUnsupportedExitPointsStatements(controlFlowAnalysisData.ExitPoints))
                return false;
 
            // okay, there is no branch out, check whether next statement can be executed normally
            var (firstStatement, lastStatement) = this.GetFlowAnalysisNodeRange();
            var returnStatements = GetOuterReturnStatements(firstStatement.GetCommonRoot(lastStatement), controlFlowAnalysisData.ExitPoints);
            if (!returnStatements.Any())
                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(returnStatements, cancellationToken);
        }
    }
}