File: IntroduceVariable\CSharpIntroduceVariableService_IntroduceQueryLocal.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.IntroduceVariable;
 
internal sealed partial class CSharpIntroduceVariableService
{
    private static bool IsAnyQueryClause(SyntaxNode node)
        => node is QueryClauseSyntax or SelectOrGroupClauseSyntax;
 
    protected override Task<Document> IntroduceQueryLocalAsync(
        SemanticDocument document, ExpressionSyntax expression, bool allOccurrences, CancellationToken cancellationToken)
    {
        var oldOutermostQuery = expression.GetAncestorsOrThis<QueryExpressionSyntax>().LastOrDefault();
 
        var newLocalNameToken = GenerateUniqueLocalName(
            document, expression, isConstant: false,
            containerOpt: oldOutermostQuery, cancellationToken: cancellationToken);
        var newLocalName = SyntaxFactory.IdentifierName(newLocalNameToken);
 
        var letClause = SyntaxFactory.LetClause(
            newLocalNameToken.WithAdditionalAnnotations(RenameAnnotation.Create()),
            expression).WithAdditionalAnnotations(Formatter.Annotation);
 
        var matches = FindMatches(document, expression, document, oldOutermostQuery, allOccurrences, cancellationToken);
        var innermostClauses = new HashSet<SyntaxNode>(
            matches.Select(expr => expr.GetAncestorsOrThis<SyntaxNode>().First(IsAnyQueryClause)));
 
        if (innermostClauses.Count == 1)
        {
            // If there was only one match, or all the matches came from the same
            // statement, then we want to place the declaration right above that
            // statement. Note: we special case this because the statement we are going
            // to go above might not be in a block and we may have to generate it
            return Task.FromResult(IntroduceQueryLocalForSingleOccurrence(
                document, expression, newLocalName, letClause, allOccurrences, cancellationToken));
        }
 
        var oldInnerMostCommonQuery = matches.FindInnermostCommonNode<QueryExpressionSyntax>();
        var newInnerMostQuery = Rewrite(
            document, expression, newLocalName, document, oldInnerMostCommonQuery, allOccurrences, cancellationToken);
 
        var allAffectedClauses = new HashSet<SyntaxNode>(matches.SelectMany(expr => expr.GetAncestorsOrThis<SyntaxNode>().Where(IsAnyQueryClause)));
 
        var oldClauses = oldInnerMostCommonQuery.GetAllClauses();
        var newClauses = newInnerMostQuery.GetAllClauses();
 
        var firstClauseAffectedInQuery = oldClauses.First(allAffectedClauses.Contains);
        var firstClauseAffectedIndex = oldClauses.IndexOf(firstClauseAffectedInQuery);
 
        var finalClauses = newClauses.Take(firstClauseAffectedIndex)
                                     .Concat(letClause)
                                     .Concat(newClauses.Skip(firstClauseAffectedIndex)).ToList();
 
        var finalQuery = newInnerMostQuery.WithAllClauses(finalClauses);
        var newRoot = document.Root.ReplaceNode(oldInnerMostCommonQuery, finalQuery);
 
        return Task.FromResult(document.Document.WithSyntaxRoot(newRoot));
    }
 
    private Document IntroduceQueryLocalForSingleOccurrence(
        SemanticDocument document,
        ExpressionSyntax expression,
        NameSyntax newLocalName,
        LetClauseSyntax letClause,
        bool allOccurrences,
        CancellationToken cancellationToken)
    {
        var oldClause = expression.GetAncestors<SyntaxNode>().First(IsAnyQueryClause);
        var newClause = Rewrite(
            document, expression, newLocalName, document, oldClause, allOccurrences, cancellationToken);
 
        var oldQuery = (QueryBodySyntax)oldClause.Parent;
        var newQuery = GetNewQuery(oldQuery, oldClause, newClause, letClause);
 
        var newRoot = document.Root.ReplaceNode(oldQuery, newQuery);
        return document.Document.WithSyntaxRoot(newRoot);
    }
 
    private static QueryBodySyntax GetNewQuery(
        QueryBodySyntax oldQuery,
        SyntaxNode oldClause,
        SyntaxNode newClause,
        LetClauseSyntax letClause)
    {
        var oldClauses = oldQuery.GetAllClauses();
        var oldClauseIndex = oldClauses.IndexOf(oldClause);
 
        var newClauses = oldClauses.Take(oldClauseIndex)
                                   .Concat(letClause)
                                   .Concat(newClause)
                                   .Concat(oldClauses.Skip(oldClauseIndex + 1)).ToList();
        return oldQuery.WithAllClauses(newClauses);
    }
}