File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\LanguageServices\MoveDeclarationNearReference\AbstractMoveDeclarationNearReferenceService.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Utilities;
 
namespace Microsoft.CodeAnalysis.MoveDeclarationNearReference;
 
internal abstract partial class AbstractMoveDeclarationNearReferenceService<
    TService,
    TStatementSyntax,
    TLocalDeclarationStatementSyntax,
    TVariableDeclaratorSyntax> : IMoveDeclarationNearReferenceService
    where TService : AbstractMoveDeclarationNearReferenceService<TService, TStatementSyntax, TLocalDeclarationStatementSyntax, TVariableDeclaratorSyntax>
    where TStatementSyntax : SyntaxNode
    where TLocalDeclarationStatementSyntax : TStatementSyntax
    where TVariableDeclaratorSyntax : SyntaxNode
{
    protected abstract bool IsMeaningfulBlock(SyntaxNode node);
    protected abstract bool CanMoveToBlock(ILocalSymbol localSymbol, SyntaxNode currentBlock, SyntaxNode destinationBlock);
    protected abstract SyntaxNode GetVariableDeclaratorSymbolNode(TVariableDeclaratorSyntax variableDeclarator);
    protected abstract bool IsValidVariableDeclarator(TVariableDeclaratorSyntax variableDeclarator);
    protected abstract SyntaxToken GetIdentifierOfVariableDeclarator(TVariableDeclaratorSyntax variableDeclarator);
    protected abstract Task<bool> TypesAreCompatibleAsync(Document document, ILocalSymbol localSymbol, TLocalDeclarationStatementSyntax declarationStatement, SyntaxNode right, CancellationToken cancellationToken);
 
    public async Task<(bool canMove, bool mayChangeSemantics)> CanMoveDeclarationNearReferenceAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
    {
        var state = await ComputeStateAsync(document, node, cancellationToken).ConfigureAwait(false);
        if (state is null)
            return default;
 
        return (canMove: true, CrossesMeaningfulBlock(state));
    }
 
    private async Task<State> ComputeStateAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
    {
        if (node is not TLocalDeclarationStatementSyntax statement)
        {
            return null;
        }
 
        var state = await State.GenerateAsync((TService)this, document, statement, cancellationToken).ConfigureAwait(false);
        if (state == null)
        {
            return null;
        }
 
        if (state.IndexOfDeclarationStatementInInnermostBlock >= 0 &&
            state.IndexOfDeclarationStatementInInnermostBlock == state.IndexOfFirstStatementAffectedInInnermostBlock - 1 &&
            !await CanMergeDeclarationAndAssignmentAsync(document, state, cancellationToken).ConfigureAwait(false))
        {
            // Declaration statement is already closest to the first reference
            // and they both cannot be merged into a single statement, so bail out.
            return null;
        }
 
        if (!CanMoveToBlock(state.LocalSymbol, state.OutermostBlock, state.InnermostBlock))
        {
            return null;
        }
 
        return state;
    }
 
    public async Task<Document> MoveDeclarationNearReferenceAsync(
        Document document, SyntaxNode localDeclarationStatement, CancellationToken cancellationToken)
    {
        var state = await ComputeStateAsync(document, localDeclarationStatement, cancellationToken).ConfigureAwait(false);
        if (state == null)
            return document;
 
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var editor = new SyntaxEditor(root, document.Project.Solution.Services);
 
        var crossesMeaningfulBlock = CrossesMeaningfulBlock(state);
        var warningAnnotation = crossesMeaningfulBlock
            ? WarningAnnotation.Create(WorkspaceExtensionsResources.Warning_colon_Declaration_changes_scope_and_may_change_meaning)
            : null;
 
        var canMergeDeclarationAndAssignment = await CanMergeDeclarationAndAssignmentAsync(document, state, cancellationToken).ConfigureAwait(false);
        if (canMergeDeclarationAndAssignment)
        {
            editor.RemoveNode(state.DeclarationStatement);
            MergeDeclarationAndAssignment(
                document, state, editor, warningAnnotation);
        }
        else
        {
            var statementIndex = state.OutermostBlockStatements.IndexOf(state.DeclarationStatement);
            if (statementIndex + 1 < state.OutermostBlockStatements.Count &&
                state.OutermostBlockStatements[statementIndex + 1] == state.FirstStatementAffectedInInnermostBlock)
            {
                // Already at the correct location.
                return document;
            }
 
            editor.RemoveNode(state.DeclarationStatement);
            await MoveDeclarationToFirstReferenceAsync(
                document, state, editor, warningAnnotation, cancellationToken).ConfigureAwait(false);
        }
 
        var newRoot = editor.GetChangedRoot();
        return document.WithSyntaxRoot(newRoot);
    }
 
    private static async Task MoveDeclarationToFirstReferenceAsync(
        Document document, State state, SyntaxEditor editor, SyntaxAnnotation warningAnnotation, CancellationToken cancellationToken)
    {
        // If we're not merging with an existing declaration, make the declaration semantically
        // explicit to improve the chances that it won't break code.
        var explicitDeclarationStatement = await Simplifier.ExpandAsync(
            state.DeclarationStatement, document, cancellationToken: cancellationToken).ConfigureAwait(false);
 
        // place the declaration above the first statement that references it.
        var declarationStatement = warningAnnotation == null
            ? explicitDeclarationStatement
            : explicitDeclarationStatement.WithAdditionalAnnotations(warningAnnotation);
        declarationStatement = declarationStatement.WithAdditionalAnnotations(Formatter.Annotation);
 
        var bannerService = document.GetRequiredLanguageService<IFileBannerFactsService>();
 
        var newNextStatement = state.FirstStatementAffectedInInnermostBlock;
        declarationStatement = declarationStatement.WithPrependedLeadingTrivia(
            bannerService.GetLeadingBlankLines(newNextStatement));
 
        editor.InsertBefore(
            state.FirstStatementAffectedInInnermostBlock,
            declarationStatement);
 
        editor.ReplaceNode(
            newNextStatement,
            newNextStatement.WithAdditionalAnnotations(Formatter.Annotation).WithLeadingTrivia(
                bannerService.GetTriviaAfterLeadingBlankLines(newNextStatement)));
 
        // Move leading whitespace from the declaration statement to the next statement.
        var statementIndex = state.OutermostBlockStatements.IndexOf(state.DeclarationStatement);
        if (statementIndex + 1 < state.OutermostBlockStatements.Count)
        {
            var originalNextStatement = state.OutermostBlockStatements[statementIndex + 1];
            editor.ReplaceNode(
                originalNextStatement,
                (current, generator) => current.WithAdditionalAnnotations(Formatter.Annotation).WithPrependedLeadingTrivia(
                    bannerService.GetLeadingBlankLines(state.DeclarationStatement)));
        }
    }
 
    private static void MergeDeclarationAndAssignment(
        Document document, State state, SyntaxEditor editor, SyntaxAnnotation warningAnnotation)
    {
        // Replace the first reference with a new declaration.
        var declarationStatement = CreateMergedDeclarationStatement(document, state);
        declarationStatement = warningAnnotation == null
            ? declarationStatement
            : declarationStatement.WithAdditionalAnnotations(warningAnnotation);
 
        var bannerService = document.GetRequiredLanguageService<IFileBannerFactsService>();
        declarationStatement = declarationStatement.WithLeadingTrivia(
            GetMergedTrivia(bannerService, state.DeclarationStatement, state.FirstStatementAffectedInInnermostBlock));
 
        editor.ReplaceNode(
            state.FirstStatementAffectedInInnermostBlock,
            declarationStatement.WithAdditionalAnnotations(Formatter.Annotation));
    }
 
    private static ImmutableArray<SyntaxTrivia> GetMergedTrivia(
        IFileBannerFactsService bannerService, TStatementSyntax statement1, TStatementSyntax statement2)
    {
        return bannerService.GetLeadingBlankLines(statement2).Concat(
               bannerService.GetTriviaAfterLeadingBlankLines(statement1)).Concat(
               bannerService.GetTriviaAfterLeadingBlankLines(statement2));
    }
 
    private bool CrossesMeaningfulBlock(State state)
    {
        var blocks = state.InnermostBlock.GetAncestorsOrThis<SyntaxNode>();
        foreach (var block in blocks)
        {
            if (block == state.OutermostBlock)
            {
                break;
            }
 
            if (IsMeaningfulBlock(block))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private async Task<bool> CanMergeDeclarationAndAssignmentAsync(
        Document document,
        State state,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
 
        var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(state.VariableDeclarator);
        if (initializer == null ||
            syntaxFacts.IsLiteralExpression(syntaxFacts.GetValueOfEqualsValueClause(initializer)))
        {
            var firstStatement = state.FirstStatementAffectedInInnermostBlock;
            if (syntaxFacts.IsSimpleAssignmentStatement(firstStatement))
            {
                syntaxFacts.GetPartsOfAssignmentStatement(firstStatement, out var left, out var right);
                if (syntaxFacts.IsIdentifierName(left))
                {
                    var localSymbol = state.LocalSymbol;
                    var name = syntaxFacts.GetIdentifierOfSimpleName(left).ValueText;
                    if (syntaxFacts.StringComparer.Equals(name, localSymbol.Name))
                    {
                        return await TypesAreCompatibleAsync(
                            document, localSymbol, state.DeclarationStatement, right, cancellationToken).ConfigureAwait(false);
                    }
                }
            }
        }
 
        return false;
    }
 
    private static TLocalDeclarationStatementSyntax CreateMergedDeclarationStatement(
        Document document, State state)
    {
        var generator = document.GetLanguageService<SyntaxGeneratorInternal>();
 
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        syntaxFacts.GetPartsOfAssignmentStatement(
            state.FirstStatementAffectedInInnermostBlock,
            out var _, out var operatorToken, out var right);
 
        return state.DeclarationStatement.ReplaceNode(
            state.VariableDeclarator,
            generator.WithInitializer(
                state.VariableDeclarator.WithoutTrailingTrivia(),
                generator.EqualsValueClause(operatorToken, right))
                .WithTrailingTrivia(state.VariableDeclarator.GetTrailingTrivia()));
    }
}