// 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.FindSymbols; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.MoveDeclarationNearReference; internal abstract partial class AbstractMoveDeclarationNearReferenceService< TService, TStatementSyntax, TLocalDeclarationStatementSyntax, TVariableDeclaratorSyntax> { private sealed class State { public TLocalDeclarationStatementSyntax DeclarationStatement { get; private set; } public TVariableDeclaratorSyntax VariableDeclarator { get; private set; } public ILocalSymbol LocalSymbol { get; private set; } public SyntaxNode OutermostBlock { get; private set; } public SyntaxNode InnermostBlock { get; private set; } public IReadOnlyList<SyntaxNode> OutermostBlockStatements { get; private set; } public IReadOnlyList<SyntaxNode> InnermostBlockStatements { get; private set; } public TStatementSyntax FirstStatementAffectedInInnermostBlock { get; private set; } public int IndexOfDeclarationStatementInInnermostBlock { get; private set; } public int IndexOfFirstStatementAffectedInInnermostBlock { get; private set; } internal static async Task<State> GenerateAsync( TService service, Document document, TLocalDeclarationStatementSyntax statement, CancellationToken cancellationToken) { var state = new State(); if (!await state.TryInitializeAsync(service, document, statement, cancellationToken).ConfigureAwait(false)) { return null; } return state; } private async Task<bool> TryInitializeAsync( TService service, Document document, TLocalDeclarationStatementSyntax node, CancellationToken cancellationToken) { var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>(); var blockFacts = document.GetRequiredLanguageService<IBlockFactsService>(); DeclarationStatement = node; var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(DeclarationStatement); if (variables.Count != 1) { return false; } VariableDeclarator = (TVariableDeclaratorSyntax)variables[0]; if (!service.IsValidVariableDeclarator(VariableDeclarator)) { return false; } OutermostBlock = blockFacts.GetStatementContainer(DeclarationStatement); if (!blockFacts.IsExecutableBlock(OutermostBlock)) { return false; } var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); LocalSymbol = (ILocalSymbol)semanticModel.GetDeclaredSymbol( service.GetVariableDeclaratorSymbolNode(VariableDeclarator), cancellationToken); if (LocalSymbol == null) { // This can happen in broken code, for example: "{ object x; object }" return false; } var findReferencesResult = await SymbolFinder.FindReferencesAsync(LocalSymbol, document.Project.Solution, cancellationToken).ConfigureAwait(false); var findReferencesList = findReferencesResult.ToList(); if (findReferencesList.Count != 1) { return false; } var references = findReferencesList[0].Locations.ToList(); if (references.Count == 0) { return false; } var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var referencingStatements = (from r in references let token = syntaxRoot.FindToken(r.Location.SourceSpan.Start) let statement = token.GetAncestor<TStatementSyntax>() where statement != null select statement).ToSet(); if (referencingStatements.Count == 0) { return false; } InnermostBlock = blockFacts.FindInnermostCommonExecutableBlock(referencingStatements); if (InnermostBlock == null) { return false; } InnermostBlockStatements = blockFacts.GetExecutableBlockStatements(InnermostBlock); OutermostBlockStatements = blockFacts.GetExecutableBlockStatements(OutermostBlock); var allAffectedStatements = new HashSet<TStatementSyntax>(referencingStatements.SelectMany( expr => expr.GetAncestorsOrThis<TStatementSyntax>())); FirstStatementAffectedInInnermostBlock = InnermostBlockStatements.Cast<TStatementSyntax>().FirstOrDefault(allAffectedStatements.Contains); if (FirstStatementAffectedInInnermostBlock == null) { return false; } if (FirstStatementAffectedInInnermostBlock == DeclarationStatement) { return false; } IndexOfDeclarationStatementInInnermostBlock = InnermostBlockStatements.IndexOf(DeclarationStatement); IndexOfFirstStatementAffectedInInnermostBlock = InnermostBlockStatements.IndexOf(FirstStatementAffectedInInnermostBlock); if (IndexOfDeclarationStatementInInnermostBlock >= 0 && IndexOfDeclarationStatementInInnermostBlock < IndexOfFirstStatementAffectedInInnermostBlock) { // Don't want to move a decl with initializer past other decls in order to move it to the first // affected statement. If we do we can end up in the following situation: #if false int x = 0; int y = 0; Console.WriteLine(x + y); #endif // Each of these declarations will want to 'move' down to the WriteLine // statement and we don't want to keep offering the refactoring. Note: this // solution is overly aggressive. Technically if 'y' weren't referenced in // Console.Writeline, then it might be a good idea to move the 'x'. But this // gives good enough behavior most of the time. // Note that if the variable declaration has no initializer, then we still want to offer // the move as the closest reference will be an assignment to the variable // and we should be able to merge the declaration and assignment into a single // statement. // So, we also check if the variable declaration has an initializer below. if (syntaxFacts.GetInitializerOfVariableDeclarator(VariableDeclarator) != null && InDeclarationStatementGroup(IndexOfDeclarationStatementInInnermostBlock, IndexOfFirstStatementAffectedInInnermostBlock)) { return false; } } var previousToken = FirstStatementAffectedInInnermostBlock.GetFirstToken().GetPreviousToken(); var affectedSpan = TextSpan.FromBounds(previousToken.SpanStart, FirstStatementAffectedInInnermostBlock.Span.End); if (semanticModel.SyntaxTree.OverlapsHiddenPosition(affectedSpan, cancellationToken)) { return false; } return true; } private bool InDeclarationStatementGroup( int originalIndexInBlock, int firstStatementIndexAffectedInBlock) { for (var i = originalIndexInBlock; i < firstStatementIndexAffectedInBlock; i++) { if (InnermostBlockStatements[i] is not TLocalDeclarationStatementSyntax) { return false; } } return true; } } } |