Skip to content

Commit 6ae6778

Browse files
Do not make unnecessarily simplification changes in sync-namespace. (#78969)
2 parents 1d0d8a5 + fd69b21 commit 6ae6778

File tree

11 files changed

+220
-87
lines changed

11 files changed

+220
-87
lines changed

src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_ChangeNamespace.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,72 @@ class Class1
14481448
await TestChangeNamespaceAsync(code, expectedSourceOriginal);
14491449
}
14501450

1451+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/34707")]
1452+
public async Task ChangeFromGlobalNamespace_DoNotSimplifyUnrelatedCode()
1453+
{
1454+
var defaultNamespace = "A";
1455+
var (folder, filePath) = CreateDocumentFilePath(["B", "C"], "File1.cs");
1456+
var documentPath2 = CreateDocumentFilePath([], "File2.cs");
1457+
var code =
1458+
$$"""
1459+
<Workspace>
1460+
<Project Language="C#" AssemblyName="Assembly1" FilePath="{{ProjectFilePath}}" RootNamespace="{{defaultNamespace}}" CommonReferences="true">
1461+
<Document Folders="{{folder}}" FilePath="{{filePath}}">
1462+
class [||]Class1
1463+
{
1464+
private A.Class2 c2;
1465+
private A.B.Class3 c3;
1466+
private A.B.C.Class4 c4;
1467+
1468+
void M1()
1469+
{
1470+
int i = 0;
1471+
// This cast should not be touched.
1472+
int j = (int)i;
1473+
}
1474+
}</Document>
1475+
1476+
<Document Folders="{{documentPath2.folder}}" FilePath="{{documentPath2.filePath}}">
1477+
namespace A
1478+
{
1479+
class Class2{}
1480+
1481+
namespace B
1482+
{
1483+
class Class3 {}
1484+
1485+
namespace C
1486+
{
1487+
class Class4 {}
1488+
}
1489+
}
1490+
}</Document>
1491+
</Project>
1492+
</Workspace>
1493+
""";
1494+
1495+
var expectedSourceOriginal =
1496+
"""
1497+
namespace A.B.C
1498+
{
1499+
class Class1
1500+
{
1501+
private Class2 c2;
1502+
private Class3 c3;
1503+
private Class4 c4;
1504+
1505+
void M1()
1506+
{
1507+
int i = 0;
1508+
// This cast should not be touched.
1509+
int j = (int)i;
1510+
}
1511+
}
1512+
}
1513+
""";
1514+
await TestChangeNamespaceAsync(code, expectedSourceOriginal);
1515+
}
1516+
14511517
[Fact]
14521518
public async Task ChangeFromGlobalNamespace_ChangeUsingsInMultipleContainers()
14531519
{
@@ -2169,7 +2235,7 @@ namespace {{defaultNamespace}}
21692235
{
21702236
public static class Extensions
21712237
{
2172-
public static bool Foo(this string s) => true;
2238+
public static bool Foo(this String s) => true;
21732239
}
21742240
}
21752241
""";

src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpChangeNamespaceService.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
using Microsoft.CodeAnalysis.ChangeNamespace;
1414
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
1515
using Microsoft.CodeAnalysis.CSharp.Extensions;
16+
using Microsoft.CodeAnalysis.CSharp.Simplification;
1617
using Microsoft.CodeAnalysis.CSharp.Syntax;
1718
using Microsoft.CodeAnalysis.Host.Mef;
1819
using Microsoft.CodeAnalysis.LanguageService;
1920
using Microsoft.CodeAnalysis.PooledObjects;
2021
using Microsoft.CodeAnalysis.Shared.Extensions;
22+
using Microsoft.CodeAnalysis.Simplification;
2123
using Microsoft.CodeAnalysis.Text;
2224
using Roslyn.Utilities;
2325

@@ -27,14 +29,18 @@ namespace Microsoft.CodeAnalysis.CSharp.ChangeNamespace;
2729
using static SyntaxFactory;
2830

2931
[ExportLanguageService(typeof(IChangeNamespaceService), LanguageNames.CSharp), Shared]
30-
internal sealed class CSharpChangeNamespaceService :
31-
AbstractChangeNamespaceService<BaseNamespaceDeclarationSyntax, CompilationUnitSyntax, MemberDeclarationSyntax>
32+
[method: ImportingConstructor]
33+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
34+
internal sealed class CSharpChangeNamespaceService() :
35+
AbstractChangeNamespaceService<
36+
CompilationUnitSyntax,
37+
MemberDeclarationSyntax,
38+
BaseNamespaceDeclarationSyntax,
39+
NameSyntax,
40+
SimpleNameSyntax,
41+
QualifiedCrefSyntax>
3242
{
33-
[ImportingConstructor]
34-
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
35-
public CSharpChangeNamespaceService()
36-
{
37-
}
43+
public override AbstractReducer NameReducer { get; } = new CSharpNameReducer();
3844

3945
protected override async Task<ImmutableArray<(DocumentId, SyntaxNode)>> GetValidContainersFromAllLinkedDocumentsAsync(
4046
Document document,

src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceCodeRefactoringProvider.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@
1717
namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.SyncNamespace;
1818

1919
[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SyncNamespace), Shared]
20-
internal sealed class CSharpSyncNamespaceCodeRefactoringProvider
21-
: AbstractSyncNamespaceCodeRefactoringProvider<BaseNamespaceDeclarationSyntax, CompilationUnitSyntax, MemberDeclarationSyntax>
20+
[method: ImportingConstructor]
21+
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
22+
internal sealed class CSharpSyncNamespaceCodeRefactoringProvider()
23+
: AbstractSyncNamespaceCodeRefactoringProvider<BaseNamespaceDeclarationSyntax, CompilationUnitSyntax, MemberDeclarationSyntax>
2224
{
23-
[ImportingConstructor]
24-
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
25-
public CSharpSyncNamespaceCodeRefactoringProvider()
26-
{
27-
}
28-
2925
protected override async Task<SyntaxNode?> TryGetApplicableInvocationNodeAsync(Document document, TextSpan span, CancellationToken cancellationToken)
3026
{
3127
if (!span.IsEmpty)
@@ -58,7 +54,4 @@ public CSharpSyncNamespaceCodeRefactoringProvider()
5854

5955
return null;
6056
}
61-
62-
protected override string EscapeIdentifier(string identifier)
63-
=> identifier.EscapeIdentifier();
6457
}

src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ namespace Microsoft.CodeAnalysis.ChangeNamespace;
3434
/// <summary>
3535
/// This intermediate class is used to hide method `TryGetReplacementReferenceSyntax` from <see cref="IChangeNamespaceService" />.
3636
/// </summary>
37-
internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService
37+
internal abstract partial class AbstractChangeNamespaceService : IChangeNamespaceService
3838
{
3939
public abstract Task<bool> CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken);
4040

4141
public abstract Task<Solution> ChangeNamespaceAsync(Document document, SyntaxNode container, string targetNamespace, CancellationToken cancellationToken);
4242

4343
public abstract Task<Solution?> TryChangeTopLevelNamespacesAsync(Document document, string targetNamespace, CancellationToken cancellationToken);
4444

45+
public abstract AbstractReducer NameReducer { get; }
46+
4547
/// <summary>
4648
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the
4749
/// namespace to be changed. If this reference is the right side of a qualified name, the new node returned would
@@ -57,11 +59,20 @@ internal abstract class AbstractChangeNamespaceService : IChangeNamespaceService
5759
public abstract bool TryGetReplacementReferenceSyntax(SyntaxNode reference, ImmutableArray<string> newNamespaceParts, ISyntaxFactsService syntaxFacts, [NotNullWhen(returnValue: true)] out SyntaxNode? old, [NotNullWhen(returnValue: true)] out SyntaxNode? @new);
5860
}
5961

60-
internal abstract class AbstractChangeNamespaceService<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax>
62+
internal abstract partial class AbstractChangeNamespaceService<
63+
TCompilationUnitSyntax,
64+
TMemberDeclarationSyntax,
65+
TNamespaceDeclarationSyntax,
66+
TNameSyntax,
67+
TSimpleNameSyntax,
68+
TCrefSyntax>
6169
: AbstractChangeNamespaceService
62-
where TNamespaceDeclarationSyntax : SyntaxNode
6370
where TCompilationUnitSyntax : SyntaxNode
6471
where TMemberDeclarationSyntax : SyntaxNode
72+
where TNamespaceDeclarationSyntax : TMemberDeclarationSyntax
73+
where TNameSyntax : SyntaxNode
74+
where TSimpleNameSyntax : TNameSyntax
75+
where TCrefSyntax : SyntaxNode
6576
{
6677
private static readonly char[] s_dotSeparator = ['.'];
6778

@@ -125,9 +136,7 @@ public override async Task<bool> CanChangeNamespaceAsync(Document document, Synt
125136
var originalNamespaceDeclarations = await GetTopLevelNamespacesAsync(document, cancellationToken).ConfigureAwait(false);
126137

127138
if (originalNamespaceDeclarations.Length == 0)
128-
{
129139
return null;
130-
}
131140

132141
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
133142
var originalNamespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations.First(), cancellationToken).ToDisplayString();
@@ -139,12 +148,10 @@ public override async Task<bool> CanChangeNamespaceAsync(Document document, Synt
139148
for (var i = 0; i < originalNamespaceDeclarations.Length; i++)
140149
{
141150
var namespaceName = semanticModel.GetRequiredDeclaredSymbol(originalNamespaceDeclarations[i], cancellationToken).ToDisplayString();
151+
152+
// Skip all namespaces that didn't match the original namespace name that we were syncing.
142153
if (namespaceName != originalNamespaceName)
143-
{
144-
// Skip all namespaces that didn't match the original namespace name that
145-
// we were syncing.
146154
continue;
147-
}
148155

149156
syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
150157

@@ -194,16 +201,12 @@ public override async Task<Solution> ChangeNamespaceAsync(
194201

195202
var containersFromAllDocuments = await GetValidContainersFromAllLinkedDocumentsAsync(document, container, cancellationToken).ConfigureAwait(false);
196203
if (containersFromAllDocuments.IsDefault)
197-
{
198204
return solution;
199-
}
200205

201206
// No action required if declared namespace already matches target.
202207
var declaredNamespace = GetDeclaredNamespace(container);
203208
if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace))
204-
{
205209
return solution;
206-
}
207210

208211
// Annotate the container nodes so we can still find and modify them after syntax tree has changed.
209212
var annotatedSolution = await AnnotateContainersAsync(solution, containersFromAllDocuments, cancellationToken).ConfigureAwait(false);
@@ -222,9 +225,8 @@ public override async Task<Solution> ChangeNamespaceAsync(
222225

223226
foreach (var documentId in documentIds)
224227
{
225-
var (newSolution, refDocumentIds) =
226-
await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documentId, declaredNamespace, targetNamespace, cancellationToken)
227-
.ConfigureAwait(false);
228+
var (newSolution, refDocumentIds) = await ChangeNamespaceInSingleDocumentAsync(
229+
solutionAfterNamespaceChange, documentId, declaredNamespace, targetNamespace, cancellationToken).ConfigureAwait(false);
228230
solutionAfterNamespaceChange = newSolution;
229231
referenceDocuments.AddRange(refDocumentIds);
230232
}
@@ -463,8 +465,8 @@ private static SyntaxNode CreateImport(SyntaxGenerator syntaxGenerator, string n
463465
}
464466
}
465467

466-
var documentWithNewNamespace = await FixDeclarationDocumentAsync(document, refLocationsInCurrentDocument, oldNamespace, newNamespace, cancellationToken)
467-
.ConfigureAwait(false);
468+
var documentWithNewNamespace = await FixDeclarationDocumentAsync(
469+
document, refLocationsInCurrentDocument, oldNamespace, newNamespace, cancellationToken).ConfigureAwait(false);
468470
var solutionWithChangedNamespace = documentWithNewNamespace.Project.Solution;
469471

470472
var refLocationsInSolution = refLocationsInOtherDocuments
@@ -501,15 +503,6 @@ private static SyntaxNode CreateImport(SyntaxGenerator syntaxGenerator, string n
501503
return (solutionWithFixedReferences, refLocationGroups.SelectAsArray(g => g.Key));
502504
}
503505

504-
private readonly struct LocationForAffectedSymbol(ReferenceLocation location, bool isReferenceToExtensionMethod)
505-
{
506-
public ReferenceLocation ReferenceLocation { get; } = location;
507-
508-
public bool IsReferenceToExtensionMethod { get; } = isReferenceToExtensionMethod;
509-
510-
public Document Document => ReferenceLocation.Document;
511-
}
512-
513506
private static async Task<ImmutableArray<LocationForAffectedSymbol>> FindReferenceLocationsForSymbolAsync(
514507
Document document, ISymbol symbol, CancellationToken cancellationToken)
515508
{
@@ -631,9 +624,49 @@ private async Task<Document> FixDeclarationDocumentAsync(
631624
var services = documentWithAddedImports.Project.Solution.Services;
632625
root = Formatter.Format(root, Formatter.Annotation, services, documentOptions.FormattingOptions, cancellationToken);
633626

634-
root = root.WithAdditionalAnnotations(Simplifier.Annotation);
627+
using var _ = PooledHashSet<string>.GetInstance(out var allNamespaceNameParts);
628+
allNamespaceNameParts.AddRange(oldNamespaceParts);
629+
allNamespaceNameParts.AddRange(newNamespaceParts);
630+
631+
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
632+
root = AddSimplifierAnnotationToPotentialReferences(syntaxFacts, root, allNamespaceNameParts);
633+
635634
var formattedDocument = documentWithAddedImports.WithSyntaxRoot(root);
636-
return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
635+
return await SimplifyTypeNamesAsync(formattedDocument, documentOptions, cancellationToken).ConfigureAwait(false);
636+
}
637+
638+
private static SyntaxNode AddSimplifierAnnotationToPotentialReferences(
639+
ISyntaxFactsService syntaxFacts, SyntaxNode root, HashSet<string> allNamespaceNameParts)
640+
{
641+
// Find all identifiers in this tree that use at least one of the namespace names of either the old or new
642+
// namespace. Mark those as needing potential complexification/simplification to preserve meaning.
643+
//
644+
// Note: we could go further here and actually bind these nodes to make sure they are actually references
645+
// to one of the namespaces in question. But that doesn't seem super necessary as the chance that these names
646+
// are actually to something else *and* they would reduce without issue seems very low. This can be revisited
647+
// if we get feedback on this.
648+
649+
using var _ = PooledHashSet<SyntaxNode>.GetInstance(out var namesToUpdate);
650+
foreach (var descendent in root.DescendantNodes(descendIntoTrivia: true))
651+
{
652+
if (descendent is TSimpleNameSyntax simpleName &&
653+
allNamespaceNameParts.Contains(syntaxFacts.GetIdentifierOfSimpleName(simpleName).ValueText))
654+
{
655+
namesToUpdate.Add(GetHighestNameOrCref(simpleName));
656+
}
657+
}
658+
659+
return root.ReplaceNodes(
660+
namesToUpdate,
661+
(_, current) => current.WithAdditionalAnnotations(Simplifier.Annotation));
662+
663+
static SyntaxNode GetHighestNameOrCref(TNameSyntax name)
664+
{
665+
while (name.Parent is TNameSyntax parentName)
666+
name = parentName;
667+
668+
return name.Parent is TCrefSyntax ? name.Parent : name;
669+
}
637670
}
638671

639672
private static async Task<Document> FixReferencingDocumentAsync(
@@ -651,9 +684,8 @@ private static async Task<Document> FixReferencingDocumentAsync(
651684

652685
var newNamespaceParts = GetNamespaceParts(newNamespace);
653686

654-
var (documentWithRefFixed, containers) =
655-
await FixReferencesAsync(document, changeNamespaceService, addImportService, refLocations, newNamespaceParts, cancellationToken)
656-
.ConfigureAwait(false);
687+
var (documentWithRefFixed, containers) = await FixReferencesAsync(
688+
document, changeNamespaceService, addImportService, refLocations, newNamespaceParts, cancellationToken).ConfigureAwait(false);
657689

658690
var documentOptions = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
659691

@@ -666,10 +698,24 @@ await FixReferencesAsync(document, changeNamespaceService, addImportService, ref
666698
cancellationToken).ConfigureAwait(false);
667699

668700
// Need to invoke formatter explicitly since we are doing the diff merge ourselves.
669-
var formattedDocument = await Formatter.FormatAsync(documentWithAdditionalImports, Formatter.Annotation, documentOptions.FormattingOptions, cancellationToken)
670-
.ConfigureAwait(false);
701+
var formattedDocument = await Formatter.FormatAsync(
702+
documentWithAdditionalImports, Formatter.Annotation, documentOptions.FormattingOptions, cancellationToken).ConfigureAwait(false);
671703

672-
return await Simplifier.ReduceAsync(formattedDocument, documentOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
704+
return await SimplifyTypeNamesAsync(formattedDocument, documentOptions, cancellationToken).ConfigureAwait(false);
705+
}
706+
707+
private static async Task<Document> SimplifyTypeNamesAsync(
708+
Document document, CodeCleanupOptions documentOptions, CancellationToken cancellationToken)
709+
{
710+
var changeNamespaceService = document.GetRequiredLanguageService<IChangeNamespaceService>();
711+
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
712+
var service = document.GetRequiredLanguageService<ISimplificationService>();
713+
return await service.ReduceAsync(
714+
document,
715+
[new TextSpan(0, text.Length)],
716+
documentOptions.SimplifierOptions,
717+
[changeNamespaceService.NameReducer],
718+
cancellationToken).ConfigureAwait(false);
673719
}
674720

675721
/// <summary>
@@ -708,9 +754,7 @@ await FixReferencesAsync(document, changeNamespaceService, addImportService, ref
708754
// it will be handled properly because it is one of the reference to the type symbol. Otherwise, we don't
709755
// attempt to make a potential fix, and user might end up with errors as a result.
710756
if (refLoc.ReferenceLocation.Alias != null)
711-
{
712757
continue;
713-
}
714758

715759
// Other documents in the solution might have changed after we calculated those ReferenceLocation,
716760
// so we can't trust anything to be still up-to-date except their spans.
@@ -743,9 +787,7 @@ await FixReferencesAsync(document, changeNamespaceService, addImportService, ref
743787
}
744788

745789
foreach (var container in containers)
746-
{
747790
editor.TrackNode(container);
748-
}
749791

750792
var fixedDocument = editor.GetChangedDocument();
751793
root = await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
@@ -858,7 +900,7 @@ private SyntaxNodeSpanStartComparer()
858900
{
859901
}
860902

861-
public static SyntaxNodeSpanStartComparer Instance { get; } = new SyntaxNodeSpanStartComparer();
903+
public static SyntaxNodeSpanStartComparer Instance { get; } = new();
862904

863905
public int Compare(SyntaxNode? x, SyntaxNode? y)
864906
{

0 commit comments

Comments
 (0)