|
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ChangeNamespace;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeRefactorings.MoveType;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.MoveToNamespace;
internal interface IMoveToNamespaceService : ILanguageService
{
Task<ImmutableArray<AbstractMoveToNamespaceCodeAction>> GetCodeActionsAsync(Document document, TextSpan span, CancellationToken cancellationToken);
Task<MoveToNamespaceAnalysisResult> AnalyzeTypeAtPositionAsync(Document document, int position, CancellationToken cancellationToken);
Task<MoveToNamespaceResult> MoveToNamespaceAsync(MoveToNamespaceAnalysisResult analysisResult, string targetNamespace, CancellationToken cancellationToken);
MoveToNamespaceOptionsResult GetChangeNamespaceOptions(Document document, string defaultNamespace, ImmutableArray<string> namespaces);
IMoveToNamespaceOptionsService OptionsService { get; }
}
internal abstract class AbstractMoveToNamespaceService<TCompilationUnitSyntax, TNamespaceDeclarationSyntax, TNamedTypeDeclarationSyntax>
: IMoveToNamespaceService
where TCompilationUnitSyntax : SyntaxNode
where TNamespaceDeclarationSyntax : SyntaxNode
where TNamedTypeDeclarationSyntax : SyntaxNode
{
protected abstract string GetNamespaceName(SyntaxNode namespaceSyntax);
protected abstract bool IsContainedInNamespaceDeclaration(TNamespaceDeclarationSyntax namespaceSyntax, int position);
public IMoveToNamespaceOptionsService OptionsService { get; }
protected AbstractMoveToNamespaceService(IMoveToNamespaceOptionsService moveToNamespaceOptionsService)
=> OptionsService = moveToNamespaceOptionsService;
public async Task<ImmutableArray<AbstractMoveToNamespaceCodeAction>> GetCodeActionsAsync(
Document document,
TextSpan span,
CancellationToken cancellationToken)
{
// Code actions cannot be completed without the options needed
// to fill in missing information.
if (OptionsService != null)
{
var typeAnalysisResult = await AnalyzeTypeAtPositionAsync(document, span.Start, cancellationToken).ConfigureAwait(false);
if (typeAnalysisResult.CanPerform)
{
return [AbstractMoveToNamespaceCodeAction.Generate(this, typeAnalysisResult)];
}
}
return [];
}
public async Task<MoveToNamespaceAnalysisResult> AnalyzeTypeAtPositionAsync(
Document document,
int position,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(position);
var node = token.Parent;
var moveToNamespaceAnalysisResult = await TryAnalyzeNamespaceAsync(document, node, position, cancellationToken).ConfigureAwait(false);
if (moveToNamespaceAnalysisResult != null)
{
return moveToNamespaceAnalysisResult;
}
moveToNamespaceAnalysisResult = await TryAnalyzeNamedTypeAsync(document, node, cancellationToken).ConfigureAwait(false);
return moveToNamespaceAnalysisResult ?? MoveToNamespaceAnalysisResult.Invalid;
}
private async Task<MoveToNamespaceAnalysisResult> TryAnalyzeNamespaceAsync(
Document document, SyntaxNode node, int position, CancellationToken cancellationToken)
{
var declarationSyntax = node.FirstAncestorOrSelf<TNamespaceDeclarationSyntax>();
if (declarationSyntax == null || !IsContainedInNamespaceDeclaration(declarationSyntax, position))
{
return null;
}
// The underlying ChangeNamespace service doesn't support nested namespace declaration.
if (GetNamespaceInSpineCount(declarationSyntax) == 1)
{
var changeNamespaceService = document.GetLanguageService<IChangeNamespaceService>();
if (await changeNamespaceService.CanChangeNamespaceAsync(document, declarationSyntax, cancellationToken).ConfigureAwait(false))
{
var namespaceName = GetNamespaceName(declarationSyntax);
var namespaces = await GetNamespacesAsync(document, cancellationToken).ConfigureAwait(false);
return new MoveToNamespaceAnalysisResult(document, declarationSyntax, namespaceName, namespaces.ToImmutableArray(), MoveToNamespaceAnalysisResult.ContainerType.Namespace);
}
}
return MoveToNamespaceAnalysisResult.Invalid;
}
private async Task<MoveToNamespaceAnalysisResult> TryAnalyzeNamedTypeAsync(
Document document, SyntaxNode node, CancellationToken cancellationToken)
{
var namespaceInSpineCount = GetNamespaceInSpineCount(node);
// Nested namespaces are currently not supported by the underlying ChangeNamespace service
if (namespaceInSpineCount > 1 || ContainsMultipleTypesInSpine(node))
{
return MoveToNamespaceAnalysisResult.Invalid;
}
SyntaxNode container = null;
// Moving one of the many members declared in global namespace is not currently supported,
// but if it's the only member declared, then that's fine.
if (namespaceInSpineCount == 0)
{
container = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
if (syntaxFacts.GetMembersOfCompilationUnit(container).Count > 1)
{
return MoveToNamespaceAnalysisResult.Invalid;
}
}
if (node is TNamedTypeDeclarationSyntax namedTypeDeclarationSyntax)
{
// If we are inside a namespace declaration, then find it as the container.
container ??= GetContainingNamespace(namedTypeDeclarationSyntax);
var changeNamespaceService = document.GetLanguageService<IChangeNamespaceService>();
if (await changeNamespaceService.CanChangeNamespaceAsync(document, container, cancellationToken).ConfigureAwait(false))
{
var namespaces = await GetNamespacesAsync(document, cancellationToken).ConfigureAwait(false);
return new MoveToNamespaceAnalysisResult(document, namedTypeDeclarationSyntax, GetNamespaceName(container), namespaces.ToImmutableArray(), MoveToNamespaceAnalysisResult.ContainerType.NamedType);
}
}
return MoveToNamespaceAnalysisResult.Invalid;
}
private static TNamespaceDeclarationSyntax GetContainingNamespace(TNamedTypeDeclarationSyntax namedTypeSyntax)
=> namedTypeSyntax.FirstAncestorOrSelf<TNamespaceDeclarationSyntax>();
private static int GetNamespaceInSpineCount(SyntaxNode node)
=> node.AncestorsAndSelf().OfType<TNamespaceDeclarationSyntax>().Count() + node.DescendantNodes().OfType<TNamespaceDeclarationSyntax>().Count();
private static bool ContainsMultipleTypesInSpine(SyntaxNode node)
=> node.AncestorsAndSelf().OfType<TNamedTypeDeclarationSyntax>().Count() > 1;
public Task<MoveToNamespaceResult> MoveToNamespaceAsync(
MoveToNamespaceAnalysisResult analysisResult,
string targetNamespace,
CancellationToken cancellationToken)
{
if (!analysisResult.CanPerform)
{
return Task.FromResult(MoveToNamespaceResult.Failed);
}
return analysisResult.Container switch
{
MoveToNamespaceAnalysisResult.ContainerType.Namespace => MoveItemsInNamespaceAsync(analysisResult.Document, analysisResult.SyntaxNode, targetNamespace, cancellationToken),
MoveToNamespaceAnalysisResult.ContainerType.NamedType => MoveTypeToNamespaceAsync(analysisResult.Document, analysisResult.SyntaxNode, targetNamespace, cancellationToken),
_ => throw new InvalidOperationException(),
};
}
private static async Task<ImmutableArray<ISymbol>> GetMemberSymbolsAsync(Document document, SyntaxNode container, CancellationToken cancellationToken)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
switch (container)
{
case TNamespaceDeclarationSyntax namespaceNode:
var namespaceMembers = syntaxFacts.GetMembersOfBaseNamespaceDeclaration(namespaceNode);
return namespaceMembers.SelectAsArray(member => semanticModel.GetDeclaredSymbol(member, cancellationToken));
case TCompilationUnitSyntax compilationUnit:
var compilationUnitMembers = syntaxFacts.GetMembersOfCompilationUnit(compilationUnit);
// We are trying to move a selected type from global namespace to the target namespace.
// This is supported if the selected type is the only member declared in the global namespace in this document.
// (See `TryAnalyzeNamedTypeAsync`)
Debug.Assert(compilationUnitMembers.Count == 1);
return compilationUnitMembers.SelectAsArray(member => semanticModel.GetDeclaredSymbol(member, cancellationToken));
default:
throw ExceptionUtilities.UnexpectedValue(container);
}
}
private static async Task<MoveToNamespaceResult> MoveItemsInNamespaceAsync(
Document document,
SyntaxNode container,
string targetNamespace,
CancellationToken cancellationToken)
{
var memberSymbols = await GetMemberSymbolsAsync(document, container, cancellationToken).ConfigureAwait(false);
var newNameOriginalSymbolMapping = memberSymbols
.ToImmutableDictionary(symbol => GetNewSymbolName(symbol, targetNamespace), symbol => symbol);
var changeNamespaceService = document.GetLanguageService<IChangeNamespaceService>();
if (changeNamespaceService == null)
{
return MoveToNamespaceResult.Failed;
}
var originalSolution = document.Project.Solution;
var changedSolution = await changeNamespaceService.ChangeNamespaceAsync(
document,
container,
targetNamespace,
cancellationToken).ConfigureAwait(false);
return new MoveToNamespaceResult(originalSolution, changedSolution, document.Id, newNameOriginalSymbolMapping);
}
private static async Task<MoveToNamespaceResult> MoveTypeToNamespaceAsync(
Document document,
SyntaxNode container,
string targetNamespace,
CancellationToken cancellationToken)
{
var moveTypeService = document.GetLanguageService<IMoveTypeService>();
if (moveTypeService == null)
{
return MoveToNamespaceResult.Failed;
}
// The move service expects a single position, not a full selection
// See https://github.com/dotnet/roslyn/issues/34643
var moveSpan = new TextSpan(container.FullSpan.Start, 0);
var modifiedSolution = await moveTypeService.GetModifiedSolutionAsync(
document,
moveSpan,
MoveTypeOperationKind.MoveTypeNamespaceScope,
cancellationToken).ConfigureAwait(false);
var modifiedDocument = modifiedSolution.GetDocument(document.Id);
// Since MoveTypeService doesn't handle linked files, we need to merge the diff ourselves,
// otherwise, we will end up with multiple linked documents with different content.
var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
var mergedSolution = await PropagateChangeToLinkedDocumentsAsync(modifiedDocument, formattingOptions, cancellationToken).ConfigureAwait(false);
var mergedDocument = mergedSolution.GetDocument(document.Id);
var syntaxRoot = await mergedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var syntaxNode = syntaxRoot.GetAnnotatedNodes(AbstractMoveTypeService.NamespaceScopeMovedAnnotation).SingleOrDefault();
// The type might be declared in global namespace
syntaxNode ??= container.FirstAncestorOrSelf<TNamespaceDeclarationSyntax>() ?? syntaxRoot;
return await MoveItemsInNamespaceAsync(
mergedDocument,
syntaxNode,
targetNamespace,
cancellationToken).ConfigureAwait(false);
}
private static async Task<Solution> PropagateChangeToLinkedDocumentsAsync(Document document, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken)
{
// Need to make sure elastic trivia is formatted properly before pushing the text to other documents.
var formattedDocument = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, formattingOptions, cancellationToken).ConfigureAwait(false);
var formattedText = await formattedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
var solution = formattedDocument.Project.Solution;
var finalSolution = solution.WithDocumentTexts(
formattedDocument.GetLinkedDocumentIds().SelectAsArray(id => (id, formattedText)));
return finalSolution;
}
private static string GetNewSymbolName(ISymbol symbol, string targetNamespace)
{
Debug.Assert(symbol != null && !string.IsNullOrEmpty(targetNamespace));
var offset = symbol.ContainingNamespace.IsGlobalNamespace
? 0
: symbol.ContainingNamespace.ToDisplayString().Length + 1;
return $"{targetNamespace}.{symbol.ToDisplayString()[offset..]}";
}
private static readonly SymbolDisplayFormat QualifiedNamespaceFormat = new(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
protected static string GetQualifiedName(INamespaceSymbol namespaceSymbol)
=> namespaceSymbol.ToDisplayString(QualifiedNamespaceFormat);
private static async Task<IEnumerable<string>> GetNamespacesAsync(Document document, CancellationToken cancellationToken)
{
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
return compilation.GlobalNamespace.GetAllNamespaces(cancellationToken)
.Where(n => n.NamespaceKind == NamespaceKind.Module && n.ContainingAssembly == compilation.Assembly)
.Select(GetQualifiedName);
}
public MoveToNamespaceOptionsResult GetChangeNamespaceOptions(
Document document,
string defaultNamespace,
ImmutableArray<string> namespaces)
{
var syntaxFactsService = document.GetLanguageService<ISyntaxFactsService>();
return OptionsService.GetChangeNamespaceOptions(
defaultNamespace,
namespaces,
syntaxFactsService);
}
}
|