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.
 
#nullable disable
 
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;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExtractMethod;
 
/// <summary>
/// clean up this code when we do selection validator work.
/// </summary>
internal abstract class SelectionResult<TStatementSyntax>(
    TextSpan originalSpan,
    TextSpan finalSpan,
    bool selectionInExpression,
    SemanticDocument document,
    SyntaxAnnotation firstTokenAnnotation,
    SyntaxAnnotation lastTokenAnnotation,
    bool selectionChanged)
    where TStatementSyntax : SyntaxNode
{
    private bool? _createAsyncMethod;
 
    public TextSpan OriginalSpan { get; } = originalSpan;
    public TextSpan FinalSpan { get; } = finalSpan;
    public bool SelectionInExpression { get; } = selectionInExpression;
    public SemanticDocument SemanticDocument { get; private set; } = document;
    public SyntaxAnnotation FirstTokenAnnotation { get; } = firstTokenAnnotation;
    public SyntaxAnnotation LastTokenAnnotation { get; } = lastTokenAnnotation;
    public bool SelectionChanged { get; } = selectionChanged;
 
    protected abstract ISyntaxFacts SyntaxFacts { get; }
    protected abstract bool UnderAnonymousOrLocalMethod(SyntaxToken token, SyntaxToken firstToken, SyntaxToken lastToken);
 
    public abstract TStatementSyntax GetFirstStatementUnderContainer();
    public abstract TStatementSyntax GetLastStatementUnderContainer();
 
    public abstract bool ContainingScopeHasAsyncKeyword();
 
    public abstract SyntaxNode GetContainingScope();
    public abstract SyntaxNode GetOutermostCallSiteContainerToProcess(CancellationToken cancellationToken);
 
    public abstract (ITypeSymbol returnType, bool returnsByRef) GetReturnType();
 
    public ITypeSymbol GetContainingScopeType()
    {
        var (typeSymbol, _) = GetReturnType();
        return typeSymbol;
    }
 
    public virtual SyntaxNode GetNodeForDataFlowAnalysis() => GetContainingScope();
 
    public SelectionResult<TStatementSyntax> With(SemanticDocument document)
    {
        if (SemanticDocument == document)
        {
            return this;
        }
 
        var clone = (SelectionResult<TStatementSyntax>)MemberwiseClone();
        clone.SemanticDocument = document;
 
        return clone;
    }
 
    public SyntaxToken GetFirstTokenInSelection()
        => SemanticDocument.GetTokenWithAnnotation(FirstTokenAnnotation);
 
    public SyntaxToken GetLastTokenInSelection()
        => SemanticDocument.GetTokenWithAnnotation(LastTokenAnnotation);
 
    public TNode GetContainingScopeOf<TNode>() where TNode : SyntaxNode
    {
        var containingScope = GetContainingScope();
        return containingScope.GetAncestorOrThis<TNode>();
    }
 
    public bool IsExtractMethodOnSingleStatement()
    {
        var firstStatement = this.GetFirstStatement();
        var lastStatement = this.GetLastStatement();
 
        return firstStatement == lastStatement || firstStatement.Span.Contains(lastStatement.Span);
    }
 
    public bool IsExtractMethodOnMultipleStatements()
    {
        var first = this.GetFirstStatement();
        var last = this.GetLastStatement();
 
        if (first != last)
        {
            var firstUnderContainer = this.GetFirstStatementUnderContainer();
            var lastUnderContainer = this.GetLastStatementUnderContainer();
            Contract.ThrowIfFalse(this.SyntaxFacts.AreStatementsInSameContainer(firstUnderContainer, lastUnderContainer));
            return true;
        }
 
        return false;
    }
 
    public TStatementSyntax GetFirstStatement()
    {
        Contract.ThrowIfTrue(SelectionInExpression);
 
        var token = GetFirstTokenInSelection();
        return token.GetAncestor<TStatementSyntax>();
    }
 
    public TStatementSyntax GetLastStatement()
    {
        Contract.ThrowIfTrue(SelectionInExpression);
 
        var token = GetLastTokenInSelection();
        return token.GetAncestor<TStatementSyntax>();
    }
 
    public bool CreateAsyncMethod()
    {
        _createAsyncMethod ??= CreateAsyncMethodWorker();
        return _createAsyncMethod.Value;
 
        bool CreateAsyncMethodWorker()
        {
            var firstToken = GetFirstTokenInSelection();
            var lastToken = GetLastTokenInSelection();
            var syntaxFacts = SemanticDocument.Project.Services.GetService<ISyntaxFactsService>();
 
            for (var currentToken = firstToken;
                currentToken.Span.End < lastToken.SpanStart;
                currentToken = currentToken.GetNextToken())
            {
                // [|
                //     async () => await ....
                // |]
                //
                // for the case above, even if the selection contains "await", it doesn't belong to the enclosing block
                // which extract method is applied to
                if (syntaxFacts.IsAwaitKeyword(currentToken)
                    && !UnderAnonymousOrLocalMethod(currentToken, firstToken, lastToken))
                {
                    return true;
                }
            }
 
            return false;
        }
    }
 
    public bool ShouldCallConfigureAwaitFalse()
    {
        var syntaxFacts = SemanticDocument.Project.Services.GetService<ISyntaxFactsService>();
 
        var firstToken = GetFirstTokenInSelection();
        var lastToken = GetLastTokenInSelection();
 
        var span = TextSpan.FromBounds(firstToken.SpanStart, lastToken.Span.End);
 
        foreach (var node in SemanticDocument.Root.DescendantNodesAndSelf())
        {
            if (!node.Span.OverlapsWith(span))
                continue;
 
            if (IsConfigureAwaitFalse(node) && !UnderAnonymousOrLocalMethod(node.GetFirstToken(), firstToken, lastToken))
                return true;
        }
 
        return false;
 
        bool IsConfigureAwaitFalse(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);
        }
    }
 
    /// <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]));
    }
}