|
// 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()));
}
}
}
|