/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs
481 строка
21 KB
Cyrus Najmabadi
Only add annotations to potential references
14 июн 2025, 00:48
14 июн 2025, 00:48
4276652
Код
Авторство
О чём код?
// 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.Simplification; 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.Simplification; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.ChangeNamespace; using static CSharpSyntaxTokens; using static SyntaxFactory; [ExportLanguageService(typeof(IChangeNamespaceService), LanguageNames.CSharp), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CSharpChangeNamespaceService() : AbstractChangeNamespaceService< CompilationUnitSyntax, MemberDeclarationSyntax, BaseNamespaceDeclarationSyntax, NameSyntax, SimpleNameSyntax, QualifiedCrefSyntax> { public override AbstractReducer NameReducer { get; } = new CSharpNameReducer(); 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()); } }