File: CodeRefactorings\SyncNamespace\CSharpChangeNamespaceService.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.
 
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ChangeNamespace;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.ChangeNamespace;
 
using static CSharpSyntaxTokens;
using static SyntaxFactory;
 
[ExportLanguageService(typeof(IChangeNamespaceService), LanguageNames.CSharp), Shared]
internal sealed class CSharpChangeNamespaceService :
    AbstractChangeNamespaceService<BaseNamespaceDeclarationSyntax, CompilationUnitSyntax, MemberDeclarationSyntax>
{
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public CSharpChangeNamespaceService()
    {
    }
 
    protected override async Task<ImmutableArray<(DocumentId, SyntaxNode)>> GetValidContainersFromAllLinkedDocumentsAsync(
        Document document,
        SyntaxNode container,
        CancellationToken cancellationToken)
    {
        if (document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles
            || document.IsGeneratedCode(cancellationToken))
        {
            return default;
        }
 
        TextSpan containerSpan;
        if (container is BaseNamespaceDeclarationSyntax)
        {
            containerSpan = container.Span;
        }
        else if (container is CompilationUnitSyntax)
        {
            // A compilation unit as container means user want to move all its members from global to some namespace.
            // We use an empty span to indicate this case.
            containerSpan = default;
        }
        else
        {
            throw ExceptionUtilities.Unreachable();
        }
 
        if (!IsSupportedLinkedDocument(document, out var allDocumentIds))
            return default;
 
        return await TryGetApplicableContainersFromAllDocumentsAsync(
            document.Project.Solution, allDocumentIds, containerSpan, cancellationToken).ConfigureAwait(false);
    }
 
    protected override string GetDeclaredNamespace(SyntaxNode container)
    {
        if (container is CompilationUnitSyntax)
            return string.Empty;
 
        if (container is BaseNamespaceDeclarationSyntax namespaceDecl)
            return CSharpSyntaxGenerator.Instance.GetName(namespaceDecl);
 
        throw ExceptionUtilities.Unreachable();
    }
 
    protected override SyntaxList<MemberDeclarationSyntax> GetMemberDeclarationsInContainer(SyntaxNode container)
    {
        if (container is BaseNamespaceDeclarationSyntax namespaceDecl)
            return namespaceDecl.Members;
 
        if (container is CompilationUnitSyntax compilationUnit)
            return compilationUnit.Members;
 
        throw ExceptionUtilities.Unreachable();
    }
 
    /// <summary>
    /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namespace to be changed.
    /// If this reference is the right side of a qualified name, the new node returned would be the entire qualified name. Depends on 
    /// whether <paramref name="newNamespaceParts"/> is provided, the name in the new node might be qualified with this new namespace instead.
    /// </summary>
    /// <param name="reference">A reference to a type declared inside the namespace to be changed, which is calculated based on results from 
    /// `SymbolFinder.FindReferencesAsync`.</param>
    /// <param name="newNamespaceParts">If specified, and the reference is qualified with namespace, the namespace part of original reference 
    /// will be replaced with given namespace in the new node.</param>
    /// <param name="oldNode">The node to be replaced. This might be an ancestor of original reference.</param>
    /// <param name="newNode">The replacement node.</param>
    public override bool TryGetReplacementReferenceSyntax(
        SyntaxNode reference,
        ImmutableArray<string> newNamespaceParts,
        ISyntaxFactsService syntaxFacts,
        [NotNullWhen(returnValue: true)] out SyntaxNode? oldNode,
        [NotNullWhen(returnValue: true)] out SyntaxNode? newNode)
    {
        if (reference is not SimpleNameSyntax nameRef)
        {
            oldNode = newNode = null;
            return false;
        }
 
        // A few different cases are handled here:
        //
        // 1. When the reference is not qualified (i.e. just a simple name), then there's nothing need to be done.
        //    And both old and new will point to the original reference.
        //
        // 2. When the new namespace is not specified, we don't need to change the qualified part of reference.
        //    Both old and new will point to the qualified reference.
        //
        // 3. When the new namespace is "", i.e. we are moving type referenced by name here to global namespace.
        //    As a result, we need replace qualified reference with the simple name.
        //
        // 4. When the namespace is specified and not "", i.e. we are moving referenced type to a different non-global 
        //    namespace. We need to replace the qualified reference with a new qualified reference (which is qualified 
        //    with new namespace.)
        //
        // Note that qualified type name can appear in QualifiedNameSyntax or MemberAccessSyntax, so we need to handle both cases.
 
        if (syntaxFacts.IsRightOfQualifiedName(nameRef))
        {
            RoslynDebug.Assert(nameRef.Parent is object);
            oldNode = nameRef.Parent;
            var aliasQualifier = GetAliasQualifier(oldNode);
 
            if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode))
            {
                var qualifiedNamespaceName = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1);
                newNode = QualifiedName(qualifiedNamespaceName, nameRef.WithoutTrivia());
            }
 
            // We might lose some trivia associated with children of `oldNode`.  
            newNode = newNode.WithTriviaFrom(oldNode);
            return true;
        }
        else if (syntaxFacts.IsNameOfSimpleMemberAccessExpression(nameRef) ||
                 syntaxFacts.IsNameOfMemberBindingExpression(nameRef))
        {
            RoslynDebug.Assert(nameRef.Parent is object);
            oldNode = nameRef.Parent;
            var aliasQualifier = GetAliasQualifier(oldNode);
 
            if (!TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode))
            {
                var memberAccessNamespaceName = CreateNamespaceAsMemberAccess(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1);
                newNode = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, memberAccessNamespaceName, nameRef.WithoutTrivia());
            }
 
            // We might lose some trivia associated with children of `oldNode`.  
            newNode = newNode.WithTriviaFrom(oldNode);
            return true;
        }
        else if (nameRef.Parent is NameMemberCrefSyntax crefName && crefName.Parent is QualifiedCrefSyntax qualifiedCref)
        {
            // This is the case where the reference is the right most part of a qualified name in `cref`.
            // for example, `<see cref="Foo.Baz.Bar"/>` and `<see cref="SomeAlias::Foo.Baz.Bar"/>`. 
            // This is the form of `cref` we need to handle as a spacial case when changing namespace name or
            // changing namespace from non-global to global, other cases in these 2 scenarios can be handled in the 
            // same way we handle non cref references, for example, `<see cref="SomeAlias::Foo"/>` and `<see cref="Foo"/>`.
 
            var container = qualifiedCref.Container;
            var aliasQualifier = GetAliasQualifier(container);
 
            if (TryGetGlobalQualifiedName(newNamespaceParts, nameRef, aliasQualifier, out newNode))
            {
                // We will replace entire `QualifiedCrefSyntax` with a `TypeCrefSyntax`, 
                // which is a alias qualified simple name, similar to the regular case above.
                oldNode = qualifiedCref;
                newNode = TypeCref((AliasQualifiedNameSyntax)newNode!);
            }
            else
            {
                // if the new namespace is not global, then we just need to change the container in `QualifiedCrefSyntax`,
                // which is just a regular namespace node, no cref node involve here.
                oldNode = container;
                newNode = CreateNamespaceAsQualifiedName(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1);
            }
 
            return true;
        }
 
        // Simple name reference, nothing to be done. 
        // The name will be resolved by adding proper import.
        oldNode = newNode = nameRef;
        return false;
    }
 
    private static bool TryGetGlobalQualifiedName(
        ImmutableArray<string> newNamespaceParts,
        SimpleNameSyntax nameNode,
        string? aliasQualifier,
        [NotNullWhen(returnValue: true)] out SyntaxNode? newNode)
    {
        if (IsGlobalNamespace(newNamespaceParts))
        {
            // If new namespace is "", then name will be declared in global namespace.
            // We will replace qualified reference with simple name qualified with alias (global if it's not alias qualified)
            var aliasNode = aliasQualifier?.ToIdentifierName() ?? IdentifierName(GlobalKeyword);
            newNode = AliasQualifiedName(aliasNode, nameNode.WithoutTrivia());
            return true;
        }
 
        newNode = null;
        return false;
    }
 
    /// <summary>
    /// Try to change the namespace declaration based on the following rules:
    ///     - if neither declared nor target namespace are "" (i.e. global namespace),
    ///     then we try to change the name of the namespace.
    ///     - if declared namespace is "", then we try to move all types declared 
    ///     in global namespace in the document into a new namespace declaration.
    ///     - if target namespace is "", then we try to move all members in declared 
    ///     namespace to global namespace (i.e. remove the namespace declaration).    
    /// </summary>
    protected override CompilationUnitSyntax ChangeNamespaceDeclaration(
        CompilationUnitSyntax root,
        ImmutableArray<string> declaredNamespaceParts,
        ImmutableArray<string> targetNamespaceParts)
    {
        Debug.Assert(!declaredNamespaceParts.IsDefault && !targetNamespaceParts.IsDefault);
        var container = root.GetAnnotatedNodes(ContainerAnnotation).Single();
 
        if (container is CompilationUnitSyntax compilationUnit)
        {
            // Move everything from global namespace to a namespace declaration
            Debug.Assert(IsGlobalNamespace(declaredNamespaceParts));
            return MoveMembersFromGlobalToNamespace(compilationUnit, targetNamespaceParts);
        }
 
        if (container is BaseNamespaceDeclarationSyntax namespaceDecl)
        {
            // Move everything to global namespace
            if (IsGlobalNamespace(targetNamespaceParts))
                return MoveMembersFromNamespaceToGlobal(root, namespaceDecl);
 
            // Change namespace name
            return root.ReplaceNode(
                namespaceDecl,
                namespaceDecl.WithName(
                    CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1)
                    .WithTriviaFrom(namespaceDecl.Name).WithAdditionalAnnotations(WarningAnnotation))
                    .WithoutAnnotations(ContainerAnnotation));      // Make sure to remove the annotation we added
        }
 
        throw ExceptionUtilities.Unreachable();
    }
 
    private static CompilationUnitSyntax MoveMembersFromNamespaceToGlobal(
        CompilationUnitSyntax root, BaseNamespaceDeclarationSyntax namespaceDecl)
    {
        var (namespaceOpeningTrivia, namespaceClosingTrivia) =
            GetOpeningAndClosingTriviaOfNamespaceDeclaration(namespaceDecl);
        var members = namespaceDecl.Members;
        var eofToken = root.EndOfFileToken
            .WithAdditionalAnnotations(WarningAnnotation);
 
        // Try to preserve trivia from original namespace declaration.
        // If there's any member inside the declaration, we attach them to the 
        // first and last member, otherwise, simply attach all to the EOF token.
        if (members.Count > 0)
        {
            var first = members.First();
            var firstWithTrivia = first.WithPrependedLeadingTrivia(namespaceOpeningTrivia);
            members = members.Replace(first, firstWithTrivia);
 
            var last = members.Last();
            var lastWithTrivia = last.WithAppendedTrailingTrivia(namespaceClosingTrivia);
            members = members.Replace(last, lastWithTrivia);
        }
        else
        {
            eofToken = eofToken.WithPrependedLeadingTrivia(
                namespaceOpeningTrivia.Concat(namespaceClosingTrivia));
        }
 
        // Moving inner imports out of the namespace declaration can lead to a break in semantics.
        // For example:
        //
        //  namespace A.B.C
        //  {
        //    using D.E.F;
        //  }
        //
        //  The using of D.E.F is looked up with in the context of A.B.C first. If it's moved outside,
        //  it may fail to resolve.
 
        return root.Update(
            root.Externs.AddRange(namespaceDecl.Externs),
            root.Usings.AddRange(namespaceDecl.Usings),
            root.AttributeLists,
            root.Members.ReplaceRange(namespaceDecl, members),
            eofToken);
    }
 
    private static CompilationUnitSyntax MoveMembersFromGlobalToNamespace(CompilationUnitSyntax compilationUnit, ImmutableArray<string> targetNamespaceParts)
    {
        Debug.Assert(!compilationUnit.Members.Any(m => m is BaseNamespaceDeclarationSyntax));
 
        var targetNamespaceDecl = NamespaceDeclaration(
            name: CreateNamespaceAsQualifiedName(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1)
                    .WithAdditionalAnnotations(WarningAnnotation),
            externs: default,
            usings: default,
            members: compilationUnit.Members);
        return compilationUnit.WithMembers(new SyntaxList<MemberDeclarationSyntax>(targetNamespaceDecl))
            .WithoutAnnotations(ContainerAnnotation);   // Make sure to remove the annotation we added
    }
 
    /// <summary>
    /// For the node specified by <paramref name="span"/> to be applicable container, it must be a namespace 
    /// declaration or a compilation unit, contain no partial declarations and meet the following additional
    /// requirements:
    /// 
    /// - If a namespace declaration:
    ///    1. It doesn't contain or is nested in other namespace declarations
    ///    2. The name of the namespace is valid (i.e. no errors)
    ///
    /// - If a compilation unit (i.e. <paramref name="span"/> is empty), there must be no namespace declaration
    ///   inside (i.e. all members are declared in global namespace)
    /// </summary>
    protected override async Task<SyntaxNode?> TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken)
    {
        var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        Contract.ThrowIfNull(syntaxRoot);
        var compilationUnit = (CompilationUnitSyntax)syntaxRoot;
        SyntaxNode? container = null;
 
        // Empty span means that user wants to move all types declared in the document to a new namespace.
        // This action is only supported when everything in the document is declared in global namespace,
        // which we use the number of namespace declaration nodes to decide.
        if (span.IsEmpty)
        {
            if (ContainsNamespaceDeclaration(compilationUnit))
                return null;
 
            container = compilationUnit;
        }
        else
        {
            // Otherwise, the span should contain a namespace declaration node, which must be the only one
            // in the entire syntax spine to enable the change namespace operation.
            if (!compilationUnit.Span.Contains(span))
                return null;
 
            var node = compilationUnit.FindNode(span, getInnermostNodeForTie: true);
 
            var namespaceDecls = node.AncestorsAndSelf().OfType<BaseNamespaceDeclarationSyntax>().ToImmutableArray();
            if (namespaceDecls.Length != 1)
                return null;
 
            var namespaceDecl = namespaceDecls[0];
            if (namespaceDecl == null)
                return null;
 
            if (namespaceDecl.Name.GetDiagnostics().Any(diag => diag.DefaultSeverity == DiagnosticSeverity.Error))
                return null;
 
            if (ContainsNamespaceDeclaration(node))
                return null;
 
            container = namespaceDecl;
        }
 
        var containsPartial =
            await ContainsPartialTypeWithMultipleDeclarationsAsync(document, container, cancellationToken).ConfigureAwait(false);
 
        if (containsPartial)
            return null;
 
        return container;
 
        static bool ContainsNamespaceDeclaration(SyntaxNode node)
            => node.DescendantNodes(n => n is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax)
                   .OfType<BaseNamespaceDeclarationSyntax>().Any();
    }
 
    private static string? GetAliasQualifier(SyntaxNode? name)
    {
        while (true)
        {
            switch (name)
            {
                case QualifiedNameSyntax qualifiedNameNode:
                    name = qualifiedNameNode.Left;
                    continue;
                case MemberAccessExpressionSyntax memberAccessNode:
                    name = memberAccessNode.Expression;
                    continue;
                case AliasQualifiedNameSyntax aliasQualifiedNameNode:
                    return aliasQualifiedNameNode.Alias.Identifier.ValueText;
            }
 
            return null;
        }
    }
 
    private static NameSyntax CreateNamespaceAsQualifiedName(ImmutableArray<string> namespaceParts, string? aliasQualifier, int index)
    {
        var part = namespaceParts[index].EscapeIdentifier();
        Debug.Assert(part.Length > 0);
 
        var namePiece = IdentifierName(part);
 
        if (index == 0)
            return aliasQualifier == null ? namePiece : AliasQualifiedName(aliasQualifier, namePiece);
 
        return QualifiedName(CreateNamespaceAsQualifiedName(namespaceParts, aliasQualifier, index - 1), namePiece);
    }
 
    private static ExpressionSyntax CreateNamespaceAsMemberAccess(ImmutableArray<string> namespaceParts, string? aliasQualifier, int index)
    {
        var part = namespaceParts[index].EscapeIdentifier();
        Debug.Assert(part.Length > 0);
 
        var namePiece = IdentifierName(part);
 
        if (index == 0)
        {
            return aliasQualifier == null
                 ? namePiece
                 : AliasQualifiedName(aliasQualifier, namePiece);
        }
 
        return MemberAccessExpression(
            SyntaxKind.SimpleMemberAccessExpression,
            CreateNamespaceAsMemberAccess(namespaceParts, aliasQualifier, index - 1),
            namePiece);
    }
 
    /// <summary>
    /// return trivia attached to namespace declaration. 
    /// Leading trivia of the node and trivia around opening brace, as well as
    /// trivia around closing brace are concatenated together respectively.
    /// </summary>
    private static (ImmutableArray<SyntaxTrivia> openingTrivia, ImmutableArray<SyntaxTrivia> closingTrivia)
        GetOpeningAndClosingTriviaOfNamespaceDeclaration(BaseNamespaceDeclarationSyntax baseNamespace)
    {
        var openingBuilder = ArrayBuilder<SyntaxTrivia>.GetInstance();
        var closingBuilder = ArrayBuilder<SyntaxTrivia>.GetInstance();
 
        openingBuilder.AddRange(baseNamespace.GetLeadingTrivia());
 
        if (baseNamespace is NamespaceDeclarationSyntax namespaceDeclaration)
        {
            openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.LeadingTrivia);
            openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.TrailingTrivia);
 
            closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.LeadingTrivia);
            closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.TrailingTrivia);
        }
        else if (baseNamespace is FileScopedNamespaceDeclarationSyntax fileScopedNamespace)
        {
            openingBuilder.AddRange(fileScopedNamespace.SemicolonToken.TrailingTrivia);
        }
 
        return (openingBuilder.ToImmutableAndFree(), closingBuilder.ToImmutableAndFree());
    }
}