File: AbstractMoveDeclarationNearReferenceService.cs
Web Access
Project: ..\..\..\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> CanMoveDeclarationNearReferenceAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
        {
            var state = await ComputeStateAsync(document, node, cancellationToken).ConfigureAwait(false);
            return state != null;
        }
 
        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()));
        }
    }
}