diff --git a/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/CSharpSyncNamespaceTestsBase.cs b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/CSharpSyncNamespaceTestsBase.cs new file mode 100644 index 0000000000000000000000000000000000000000..2574c8c3cead85795e4d7f946bf8aff1e94cb8b1 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/CSharpSyncNamespaceTestsBase.cs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; +using Microsoft.CodeAnalysis.CSharp.CodeRefactorings.SyncNamespace; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.UnitTests; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.SyncNamespace +{ + public abstract class CSharpSyncNamespaceTestsBase : AbstractCodeActionTest + { + protected override ParseOptions GetScriptOptions() => Options.Script; + + protected override string GetLanguage() => LanguageNames.CSharp; + + protected override CodeRefactoringProvider CreateCodeRefactoringProvider(Workspace workspace, TestParameters parameters) + => new SyncNamespaceCodeRefactoringProvider(); + + protected override TestWorkspace CreateWorkspaceFromFile(string initialMarkup, TestParameters parameters) + { + return TestWorkspace.IsWorkspaceElement(initialMarkup) + ? TestWorkspace.Create(initialMarkup) + : TestWorkspace.CreateCSharp(initialMarkup, parameters.parseOptions, parameters.compilationOptions); + } + + protected string ProjectRootPath + => PathUtilities.IsUnixLikePlatform + ? @"/ProjectA/" + : @"C:\ProjectA\"; + + protected string ProjectFilePath + => PathUtilities.CombineAbsoluteAndRelativePaths(ProjectRootPath, "ProjectA.csproj"); + + protected (string folder, string filePath) CreateDocumentFilePath(string[] folder, string fileName = "DocumentA.cs") + { + if (folder == null || folder.Length == 0) + { + return (string.Empty, PathUtilities.CombineAbsoluteAndRelativePaths(ProjectRootPath, fileName)); + } + else + { + var folderPath = CreateFolderPath(folder); + var relativePath = PathUtilities.CombinePossiblyRelativeAndRelativePaths(folderPath, fileName); + return (folderPath, PathUtilities.CombineAbsoluteAndRelativePaths(ProjectRootPath, relativePath)); + } + } + + protected string CreateFolderPath(params string[] folders) + { + return string.Join(PathUtilities.DirectorySeparatorStr, folders); + } + + protected async Task TestMoveFileToMatchNamespace(string initialMarkup, List expectedFolders = null) + { + var testOptions = new TestParameters(); + using (var workspace = CreateWorkspaceFromOptions(initialMarkup, testOptions)) + { + if (expectedFolders?.Count > 0) + { + var expectedFolderPaths = expectedFolders.Select(f => string.Join(PathUtilities.DirectorySeparatorStr, f)); + + var oldDocument = workspace.Documents[0]; + var oldDocumentId = oldDocument.Id; + var expectedText = workspace.Documents[0].TextBuffer.CurrentSnapshot.GetText(); + + // a new document with the same text as old document is added. + var allResults = await TestOperationAsync(testOptions, workspace, expectedText); + + var actualFolderPaths = new HashSet(); + foreach (var result in allResults) + { + // the original source document does not exist in the new solution. + var oldSolution = result.Item1; + var newSolution = result.Item2; + + Assert.Null(newSolution.GetDocument(oldDocumentId)); + + var newDocument = GetDocumentToVerify(expectedChangedDocumentId: null, oldSolution, newSolution); + actualFolderPaths.Add(string.Join(PathUtilities.DirectorySeparatorStr, newDocument.Folders)); + } + + Assert.True(expectedFolderPaths.Count() == actualFolderPaths.Count, "Number of available \"Move file\" actions are not equal."); + foreach (var expected in expectedFolderPaths) + { + Assert.True(actualFolderPaths.Contains(expected)); + } + } + else + { + var (actions, _) = await GetCodeActionsAsync(workspace, testOptions); + if (actions.Length > 0) + { + var renameFileAction = actions.Any(action => action is CSharpSyncNamespaceService.MoveFileCodeAction); + Assert.False(renameFileAction, "Rename File to match type code action was not expected, but shows up."); + } + } + } + + async Task>> TestOperationAsync( + TestParameters parameters, + TestWorkspace workspace, + string expectedCode) + { + var results = new List>(); + + var (actions, _) = await GetCodeActionsAsync(workspace, parameters); + var moveFileActions = actions.Where(a => a is CSharpSyncNamespaceService.MoveFileCodeAction); + + foreach (var action in moveFileActions) + { + var operations = await action.GetOperationsAsync(CancellationToken.None); + + results.Add( + await TestOperationsAsync(workspace, + expectedText: expectedCode, + operations: operations, + conflictSpans: ImmutableArray.Empty, + renameSpans: ImmutableArray.Empty, + warningSpans: ImmutableArray.Empty, + navigationSpans: ImmutableArray.Empty, + expectedChangedDocumentId: null)); + } + + return results; + } + } + + protected async Task TestChangeNamespaceAsync( + string initialMarkUp, + string expectedSourceOriginal, + string expectedSourceReference = null) + { + var testOptions = new TestParameters(); + using (var workspace = CreateWorkspaceFromOptions(initialMarkUp, testOptions)) + { + if (workspace.Projects.Count == 2) + { + var project = workspace.Documents.Single(doc => !doc.SelectedSpans.IsEmpty()).Project; + var dependentProject = workspace.Projects.Single(proj => proj.Id != project.Id); + var references = dependentProject.ProjectReferences.ToList(); + references.Add(new ProjectReference(project.Id)); + dependentProject.ProjectReferences = references; + workspace.OnProjectReferenceAdded(dependentProject.Id, new ProjectReference(project.Id)); + } + + if (expectedSourceOriginal != null) + { + var originalDocument = workspace.Documents.Single(doc => !doc.SelectedSpans.IsEmpty()); + var originalDocumentId = originalDocument.Id; + + var refDocument = workspace.Documents.Where(doc => doc.Id != originalDocumentId).SingleOrDefault(); + var refDocumentId = refDocument?.Id; + + var oldAndNewSolution = await TestOperationAsync(testOptions, workspace); + var oldSolution = oldAndNewSolution.Item1; + var newSolution = oldAndNewSolution.Item2; + + var changedDocumentIds = SolutionUtilities.GetChangedDocuments(oldSolution, newSolution); + + Assert.True(changedDocumentIds.Contains(originalDocumentId), "original document was not changed."); + Assert.True(expectedSourceReference == null || changedDocumentIds.Contains(refDocumentId), "reference document was not changed."); + + var modifiedOriginalDocument = newSolution.GetDocument(originalDocumentId); + var modifiedOringinalRoot = await modifiedOriginalDocument.GetSyntaxRootAsync(); + + // One node/token will contain the warning we attached for change namespace action. + Assert.Single(modifiedOringinalRoot.DescendantNodesAndTokensAndSelf().Where(n => + { + IEnumerable annotations; + if (n.IsNode) + { + annotations = n.AsNode().GetAnnotations(WarningAnnotation.Kind); + } + else + { + annotations = n.AsToken().GetAnnotations(WarningAnnotation.Kind); + } + + return annotations.Any(annotation => + WarningAnnotation.GetDescription(annotation) == FeaturesResources.Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning); + })); + + + var actualText = (await modifiedOriginalDocument.GetTextAsync()).ToString(); + Assert.Equal(expectedSourceOriginal, actualText); + + if (expectedSourceReference != null) + { + var actualRefText = (await newSolution.GetDocument(refDocumentId).GetTextAsync()).ToString(); + Assert.Equal(expectedSourceReference, actualRefText); + } + } + else + { + var (actions, _) = await GetCodeActionsAsync(workspace, testOptions); + if (actions.Length > 0) + { + var hasChangeNamespaceAction = actions.Any(action => action is CSharpSyncNamespaceService.ChangeNamespaceCodeAction); + Assert.False(hasChangeNamespaceAction, "Change namespace to match folder action was not expected, but shows up."); + } + } + } + + async Task> TestOperationAsync(TestParameters parameters, TestWorkspace workspace) + { + var (actions, _) = await GetCodeActionsAsync(workspace, parameters); + var changeNamespaceAction = actions.Single(a => a is CSharpSyncNamespaceService.ChangeNamespaceCodeAction); + var operations = await changeNamespaceAction.GetOperationsAsync(CancellationToken.None); + + return ApplyOperationsAndGetSolution(workspace, operations); + } + } + } +} diff --git a/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_ChangeNamespace.cs b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_ChangeNamespace.cs new file mode 100644 index 0000000000000000000000000000000000000000..5bd5e594fd00b8431a0b07e74eda8ff5dfbbbe20 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_ChangeNamespace.cs @@ -0,0 +1,1846 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.SyncNamespace +{ + public partial class SyncNamespaceTests : CSharpSyncNamespaceTestsBase + { + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_InvalidFolderName1() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar"; + + // No change namespace action because the folder name is not valid identifier + var documentPath = CreateDocumentFilePath(new[] { "3B", "C" }, "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal: null); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_InvalidFolderName2() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar"; + + // No change namespace action because the folder name is not valid identifier + var documentPath = CreateDocumentFilePath(new[] { "B.3C", "D" }, "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal: null); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_SingleDocumentNoReference() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar"; + + var documentPath = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_SingleDocumentLocalReference() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar"; + + var documentPath = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + delegate void D1; + + interface Class1 + {{ + void M1(); + }} + + class Class2 : {declaredNamespace}.Class1 + {{ + {declaredNamespace}.D1 d; + + void {declaredNamespace}.Class1.M1(){{}} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + delegate void D1; + + interface Class1 + { + void M1(); + } + + class Class2 : Class1 + { + D1 d; + + void Class1.M1() { } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithCrefReference() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + /// <summary> + /// See <see cref=""Class1""/> + /// See <see cref=""{declaredNamespace}.Class1""/> + /// See <see cref=""global::{declaredNamespace}.Class1""/> + /// See <see cref=""global::{declaredNamespace}.Class1.M1""/> + /// </summary> + public class Class1 + {{ + public void M1() {{ }} + }} +}} + +namespace Foo +{{ + using {declaredNamespace}; + + /// <summary> + /// See <see cref=""Class1""/> + /// See <see cref=""{declaredNamespace}.Class1""/> + /// See <see cref=""global::{declaredNamespace}.Class1""/> + /// See <see cref=""global::{declaredNamespace}.Class1.M1""/> + /// </summary> + class RefClass + {{ + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + /// + /// See + /// See + /// See + /// See + /// + public class Class1 + { + public void M1() { } + } +}"; + var expectedSourceReference = +@" +namespace Foo +{ + using A.B.C; + + /// + /// See + /// See + /// See + /// See + /// + class RefClass + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithCrefReferencesInVB() + { + var defaultNamespace = "A.B.C"; + var declaredNamespace = "A.B.C.D"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + /// <summary> + /// See <see cref=""Class1""/> + /// See <see cref=""{declaredNamespace}.Class1""/> + /// </summary> + public class Class1 + {{ + }} +}} + + + +Imports {declaredNamespace} + +''' <summary> +''' See <see cref=""Class1""/> +''' See <see cref=""{declaredNamespace}.Class1""/> +''' </summary> +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + /// + /// See + /// See + /// + public class Class1 + { + } +}"; + var expectedSourceReference = +@" +Imports A.B.C + +''' +''' See +''' See +''' +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_ReferencingTypesDeclaredInOtherDocument() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + private Class2 c2; + private Class3 c3; + private Class4 c4; + }} +}} + +namespace Foo +{{ + class Class2 {{}} + + namespace Bar + {{ + class Class3 {{}} + + namespace Baz + {{ + class Class4 {{}} + }} + }} +}} + +"; + + var expectedSourceOriginal = +@" +using Foo; +using Foo.Bar; +using Foo.Bar.Baz; + +namespace A.B.C +{ + class Class1 + { + private Class2 c2; + private Class3 c3; + private Class4 c4; + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_ReferencingQualifiedTypesDeclaredInOtherDocument() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + private Foo.Class2 c2; + private Foo.Bar.Class3 c3; + private Foo.Bar.Baz.Class4 c4; + }} +}} + +namespace Foo +{{ + class Class2 {{}} + + namespace Bar + {{ + class Class3 {{}} + + namespace Baz + {{ + class Class4 {{}} + }} + }} +}} + +"; + + var expectedSourceOriginal = +@" +using Foo; +using Foo.Bar; +using Foo.Bar.Baz; + +namespace A.B.C +{ + class Class1 + { + private Class2 c2; + private Class3 c3; + private Class4 c4; + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithReferencesInOtherDocument() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} + + class Class2 + {{ + }} +}} + +using Foo.Bar.Baz; + +namespace Foo +{{ + class RefClass + {{ + private Class1 c1; + + void M1() + {{ + Bar.Baz.Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } + + class Class2 + { + } +}"; + var expectedSourceReference = +@" +using A.B.C; + +namespace Foo +{ + class RefClass + { + private Class1 c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithQualifiedReferencesInOtherDocument() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + interface Interface1 + {{ + void M1(Interface1 c1); + }} +}} + +namespace Foo +{{ + using {declaredNamespace}; + + class RefClass : Interface1 + {{ + void {declaredNamespace}.Interface1.M1(Interface1 c1){{}} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + interface Interface1 + { + void M1(Interface1 c1); + } +}"; + var expectedSourceReference = +@" +namespace Foo +{ + using A.B.C; + + class RefClass : Interface1 + { + void Interface1.M1(Interface1 c1){} + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_ChangeUsingsInMultipleContainers() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + +namespace NS1 +{{ + using Foo.Bar.Baz; + + class Class2 + {{ + Class1 c2; + }} + + namespace NS2 + {{ + using Foo.Bar.Baz; + + class Class2 + {{ + Class1 c1; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } +}"; + var expectedSourceReference = +@" +namespace NS1 +{ + using A.B.C; + + class Class2 + { + Class1 c2; + } + + namespace NS2 + { + class Class2 + { + Class1 c1; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithAliasReferencesInOtherDocument() + { + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} + + class Class2 + {{ + }} +}} + +using System; +using Class1Alias = Foo.Bar.Baz.Class1; + +namespace Foo +{{ + class RefClass + {{ + private Class1Alias c1; + + void M1() + {{ + Bar.Baz.Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } + + class Class2 + { + } +}"; + var expectedSourceReference = +@" +using System; +using A.B.C; +using Class1Alias = A.B.C.Class1; + +namespace Foo +{ + class RefClass + { + private Class1Alias c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_SingleDocumentNoRef() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar"; + + var documentPath = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +using System; + +// Comments before declaration. +namespace [||]{declaredNamespace} +{{ // Comments after opening brace + class Class1 + {{ + }} + // Comments before closing brace +}} // Comments after declaration. + + +"; + + var expectedSourceOriginal = +@" +using System; + +// Comments before declaration. +// Comments after opening brace +class Class1 +{ +} +// Comments before closing brace +// Comments after declaration. +"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_SingleDocumentLocalRef() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar"; + + var documentPath = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + delegate void D1; + + interface Class1 + {{ + void M1(); + }} + + class Class2 : {declaredNamespace}.Class1 + {{ + global::{declaredNamespace}.D1 d; + + void {declaredNamespace}.Class1.M1() {{ }} + }} +}} + +"; + + var expectedSourceOriginal = +@"delegate void D1; + +interface Class1 +{ + void M1(); +} + +class Class2 : Class1 +{ + global::D1 d; + + void Class1.M1() { } +} +"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithReferencesInOtherDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} + + class Class2 + {{ + }} +}} + +using Foo.Bar.Baz; + +namespace Foo +{{ + class RefClass + {{ + private Class1 c1; + + void M1() + {{ + Bar.Baz.Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"class Class1 +{ +} + +class Class2 +{ +} +"; + var expectedSourceReference = +@"namespace Foo +{ + class RefClass + { + private Class1 c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithQualifiedReferencesInOtherDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + interface Interface1 + {{ + void M1(Interface1 c1); + }} +}} + +namespace Foo +{{ + using {declaredNamespace}; + + class RefClass : Interface1 + {{ + void {declaredNamespace}.Interface1.M1(Interface1 c1){{}} + }} +}} + +"; + + var expectedSourceOriginal = +@"interface Interface1 +{ + void M1(Interface1 c1); +} +"; + var expectedSourceReference = +@" +namespace Foo +{ + class RefClass : Interface1 + { + void Interface1.M1(Interface1 c1){} + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithReferenceAndConflictDeclarationInOtherDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class MyClass + {{ + }} +}} + +namespace Foo +{{ + using {declaredNamespace}; + + class RefClass + {{ + Foo.Bar.Baz.MyClass c; + }} + + class MyClass + {{ + }} +}} + +"; + + var expectedSourceOriginal = +@"class MyClass +{ +} +"; + var expectedSourceReference = +@" +namespace Foo +{ + class RefClass + { + global::MyClass c; + } + + class MyClass + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_ReferencingTypesDeclaredInOtherDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + private Class2 c2; + private Class3 c3; + private Class4 c4; + }} +}} + +namespace Foo +{{ + class Class2 {{}} + + namespace Bar + {{ + class Class3 {{}} + + namespace Baz + {{ + class Class4 {{}} + }} + }} +}} + +"; + + var expectedSourceOriginal = +@" +using Foo; +using Foo.Bar; +using Foo.Bar.Baz; + +class Class1 +{ + private Class2 c2; + private Class3 c3; + private Class4 c4; +} +"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_ChangeUsingsInMultipleContainers() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + +namespace NS1 +{{ + using Foo.Bar.Baz; + + class Class2 + {{ + Class1 c2; + }} + + namespace NS2 + {{ + using Foo.Bar.Baz; + + class Class2 + {{ + Class1 c1; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"class Class1 +{ +} +"; + var expectedSourceReference = +@" +namespace NS1 +{ + class Class2 + { + Class1 c2; + } + + namespace NS2 + { + class Class2 + { + Class1 c1; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithAliasReferencesInOtherDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} + + class Class2 + {{ + }} +}} + +using System; +using Class1Alias = Foo.Bar.Baz.Class1; + +namespace Foo +{{ + class RefClass + {{ + private Class1Alias c1; + + void M1() + {{ + Bar.Baz.Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"class Class1 +{ +} + +class Class2 +{ +} +"; + var expectedSourceReference = +@"using System; +using Class1Alias = Class1; + +namespace Foo +{ + class RefClass + { + private Class1Alias c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_SingleDocumentNoRef() + { + var defaultNamespace = "A"; + var documentPath = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var code = +$@" + + + +using System; + +class [||]Class1 +{{ +}} + + +"; + + var expectedSourceOriginal = +@"using System; + +namespace A.B.C +{ + class Class1 + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_SingleDocumentLocalRef() + { + var defaultNamespace = "A"; + var documentPath = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var code = +$@" + + + +delegate void [||]D1; + +interface Class1 +{{ + void M1(); +}} + +class Class2 : Class1 +{{ + D1 d; + + void Class1.M1() {{ }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + delegate void D1; + + interface Class1 + { + void M1(); + } + + class Class2 : Class1 + { + D1 d; + + void Class1.M1() { } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_WithReferencesInOtherDocument() + { + var defaultNamespace = "A"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +class [||]Class1 +{{ +}} + +class Class2 +{{ +}} + +namespace Foo +{{ + class RefClass + {{ + private Class1 c1; + + void M1() + {{ + Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } + + class Class2 + { + } +}"; + var expectedSourceReference = +@" +using A.B.C; + +namespace Foo +{ + class RefClass + { + private Class1 c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_WithQualifiedReferencesInOtherDocument() + { + var defaultNamespace = "A"; + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +interface [||]Interface1 +{{ + void M1(Interface1 c1); +}} + +namespace Foo +{{ + class RefClass : Interface1 + {{ + void Interface1.M1(Interface1 c1){{}} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + interface Interface1 + { + void M1(Interface1 c1); + } +}"; + var expectedSourceReference = +@" +using A.B.C; + +namespace Foo +{ + class RefClass : Interface1 + { + void Interface1.M1(Interface1 c1){} + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_ReferencingQualifiedTypesDeclaredInOtherDocument() + { + var defaultNamespace = "A"; + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +class [||]Class1 +{{ + private A.Class2 c2; + private A.B.Class3 c3; + private A.B.C.Class4 c4; +}} + + +namespace A +{{ + class Class2 {{}} + + namespace B + {{ + class Class3 {{}} + + namespace C + {{ + class Class4 {{}} + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + private Class2 c2; + private Class3 c3; + private Class4 c4; + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_ChangeUsingsInMultipleContainers() + { + var defaultNamespace = "A"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +class [||]Class1 +{{ +}} + +namespace NS1 +{{ + using System; + + class Class2 + {{ + Class1 c2; + }} + + namespace NS2 + {{ + using System; + + class Class2 + {{ + Class1 c1; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } +}"; + var expectedSourceReference = +@" +namespace NS1 +{ + using System; + using A.B.C; + + class Class2 + { + Class1 c2; + } + + namespace NS2 + { + using System; + + class Class2 + { + Class1 c1; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_WithAliasReferencesInOtherDocument() + { + var defaultNamespace = "A"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +class [||]Class1 +{{ +}} + +class Class2 +{{ +}} + +using Class1Alias = Class1; + +namespace Foo +{{ + using System; + + class RefClass + {{ + private Class1Alias c1; + + void M1() + {{ + Class2 c2 = null; + }} + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + class Class1 + { + } + + class Class2 + { + } +}"; + var expectedSourceReference = +@" +using A.B.C; +using Class1Alias = Class1; + +namespace Foo +{ + using System; + + class RefClass + { + private Class1Alias c1; + + void M1() + { + Class2 c2 = null; + } + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithReferencesInVBDocument() + { + var defaultNamespace = "A.B.C"; + var declaredNamespace = "A.B.C.D"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + public class Class1 + {{ + }} +}} + + + +Imports {declaredNamespace} + +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + public class Class1 + { + } +}"; + var expectedSourceReference = +@" +Imports A.B.C + +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeNamespace_WithQualifiedReferencesInVBDocument() + { + var defaultNamespace = "A.B.C"; + var declaredNamespace = "A.B.C.D"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + public class Class1 + {{ + }} +}} + + + +Public Class VBClass + Public ReadOnly Property C1 As A.B.C.D.Class1 +End Class + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + public class Class1 + { + } +}"; + var expectedSourceReference = +@"Public Class VBClass + Public ReadOnly Property C1 As A.B.C.Class1 +End Class"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_WithReferencesInVBDocument() + { + var defaultNamespace = "A"; + + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var code = +$@" + + + +public class [||]Class1 +{{ +}} + + + + +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + public class Class1 + { + } +}"; + var expectedSourceReference = +@" +Imports A.B.C + +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeFromGlobalNamespace_WithCredReferences() + { + var defaultNamespace = "A"; + var documentPath1 = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +/// <summary> +/// See <see cref=""Class1""/> +/// </summary> +class [||]Class1 +{{ +}} + +namespace Foo +{{ + /// <summary> + /// See <see cref=""Class1""/> + /// </summary> + class Bar + {{ + }} +}} + +"; + + var expectedSourceOriginal = +@"namespace A.B.C +{ + /// + /// See + /// + class Class1 + { + } +}"; + var expectedSourceReference = +@" +using A.B.C; + +namespace Foo +{ + /// + /// See + /// + class Bar + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithReferencesInVBDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + public class Class1 + {{ + }} +}} + + + +Imports {declaredNamespace} + +Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class + +"; + + var expectedSourceOriginal = +@"public class Class1 +{ +} +"; + var expectedSourceReference = +@"Public Class VBClass + Public ReadOnly Property C1 As Class1 +End Class"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithReferenceAndConflictDeclarationInVBDocument() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + public class MyClass + {{ + }} +}} + + + +Namespace Foo + Public Class VBClass + Public ReadOnly Property C1 As Foo.Bar.Baz.MyClass + End Class + + Public Class MyClass + End Class +End Namespace + +"; + + var expectedSourceOriginal = +@"public class MyClass +{ +} +"; + var expectedSourceReference = +@"Namespace Foo + Public Class VBClass + Public ReadOnly Property C1 As Global.MyClass + End Class + + Public Class MyClass + End Class +End Namespace"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task ChangeToGlobalNamespace_WithCredReferences() + { + var defaultNamespace = ""; + var declaredNamespace = "Foo.Bar.Baz"; + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(Array.Empty(), "File2.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + /// <summary> + /// See <see cref=""Class1""/> + /// See <see cref=""{declaredNamespace}.Class1""/> + /// </summary> + public class Class1 + {{ + }} +}} + +namespace Foo +{{ + using {declaredNamespace}; + + /// <summary> + /// See <see cref=""Class1""/> + /// See <see cref=""{declaredNamespace}.Class1""/> + /// </summary> + class RefClass + {{ + }} +}} + +"; + + var expectedSourceOriginal = +@"/// +/// See +/// See +/// +public class Class1 +{ +} +"; + var expectedSourceReference = +@" +namespace Foo +{ + /// + /// See + /// See + /// + class RefClass + { + } +}"; + await TestChangeNamespaceAsync(code, expectedSourceOriginal, expectedSourceReference); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_MoveFile.cs b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_MoveFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..7568b1108c16f03857aa970cd6ebed08c27136dc --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_MoveFile.cs @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.SyncNamespace +{ + public partial class SyncNamespaceTests : CSharpSyncNamespaceTestsBase + { + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_DeclarationNotContainedInDefaultNamespace() + { + // No "move file" action because default namespace is not container of declared namespace + var defaultNamespace = "A"; + var declaredNamespace = "Foo.Bar"; + + var expectedFolders = new List(); + + var documentPath = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_SingleAction1() + { + // current path is \ + // expected new path is \B\C\ + + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C" }); + + var documentPath = CreateDocumentFilePath(Array.Empty()); + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_SingleAction2() + { + // current path is \ + // expected new path is \B\C\D\E\ + + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C.D.E"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C", "D", "E" }); + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(new[] { "B", "C" }, "File2.cs"); // file2 is in \B\C\ + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_MoveToRoot() + { + // current path is \A\B\C\ + // expected new path is + + var defaultNamespace = ""; + + var expectedFolders = new List(); + expectedFolders.Add(Array.Empty()); + + var documentPath = CreateDocumentFilePath(new[] { "A", "B", "C" }); + var code = +$@" + + + +class [||]Class1 +{{ +}} + +class Class2 +{{ +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_MultipleAction1() + { + // current path is \ + // expected new paths are" + // 1. \B\C\D\E\ + // 2. \B.C\D\E\ + + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C.D.E"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C", "D", "E" }); + expectedFolders.Add(new[] { "B.C", "D", "E" }); + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(new[] { "B.C" }, "File2.cs"); // file2 is in \B.C\ + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_MultipleAction2() + { + // current path is \ + // expected new paths are: + // 1. \B\C\D\E\ + // 2. \B.C\D\E\ + // 3. \B\C.D\E\ + + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C.D.E"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C", "D", "E" }); + expectedFolders.Add(new[] { "B.C", "D", "E" }); + expectedFolders.Add(new[] { "B", "C.D", "E" }); + + var documentPath1 = CreateDocumentFilePath(Array.Empty(), "File1.cs"); + var documentPath2 = CreateDocumentFilePath(new[] { "B", "C.D" }, "File2.cs"); // file2 is in \B\C.D\ + var documentPath3 = CreateDocumentFilePath(new[] { "B.C" }, "File3.cs"); // file3 is in \B.C\ + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_FromOneFolderToAnother1() + { + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C.D.E"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C", "D", "E" }); + expectedFolders.Add(new[] { "B.C", "D", "E" }); + + var documentPath1 = CreateDocumentFilePath(new[] { "B.C" }, "File1.cs"); // file1 is in \B.C\ + var documentPath2 = CreateDocumentFilePath(new[] { "B", "Foo" }, "File2.cs"); // file2 is in \B\Foo\ + + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task MoveFile_FromOneFolderToAnother2() + { + var defaultNamespace = "A"; + var declaredNamespace = "A.B.C.D.E"; + + var expectedFolders = new List(); + expectedFolders.Add(new[] { "B", "C", "D", "E" }); + + var documentPath1 = CreateDocumentFilePath(new[] { "Foo.Bar", "Baz" }, "File1.cs"); // file1 is in \Foo.Bar\Baz\ + var documentPath2 = CreateDocumentFilePath(new[] { "B", "Foo" }, "File2.cs"); // file2 is in \B\Foo\ + + var code = +$@" + + + +namespace [||]{declaredNamespace} +{{ + class Class1 + {{ + }} +}} + + +namespace Foo +{{ + class Class2 + {{ + }} +}} + + +"; + await TestMoveFileToMatchNamespace(code, expectedFolders); + } + } +} diff --git a/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_NoAction.cs b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_NoAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..9c223c6bf5224373186e0ec886dadfe5d0abb646 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/CodeActions/SyncNamespace/SyncNamespaceTests_NoAction.cs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.CodeActions.SyncNamespace +{ + public partial class SyncNamespaceTests : CSharpSyncNamespaceTestsBase + { + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_NotOnNamespaceDeclaration() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace NS +{{ + class [||]Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_NotOnFirstMemberInGlobal() + { + var folders = new[] { "A" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +class Class1 +{{ +}} + +class [||]Class2 +{{ +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MultipleNamespaceDeclarations() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||]NS1 +{{ + class Class1 + {{ + }} +}} + +namespace NS2 +{{ + class Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MembersInBothGlobalAndNamespaceDeclaration_CursorOnNamespace() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||]NS1 +{{ + class Class1 + {{ + }} +}} + +class Class2 +{{ +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MembersInBothGlobalAndNamespaceDeclaration_CursorOnFirstGlobalMember() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +class [||]Class1 +{{ +}} + +namespace NS1 +{{ + class Class2 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_NestedNamespaceDeclarations() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||]NS1 +{{ + namespace NS2 + {{ + class Class1 + {{ + }} + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_InvalidNamespaceIdentifier() + { + var folders = new[] { "A", "B" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||] +{{ + class Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MatchingNamespace_InGlobalNamespace() + { + var folders = Array.Empty(); + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +class [||]Class1 +{{ +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MatchingNamespace_DefaultGlobalNamespace() + { + var folders = new[] { "A", "B", "C" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||]A.B.C +{{ + class Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_MatchingNamespace_InNamespaceDeclaration() + { + var folders = new[] { "B", "C" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +namespace [||]A.B.C +{{ + class Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_FileNotRooted() + { + var filePath = PathUtilities.CombineAbsoluteAndRelativePaths(PathUtilities.GetPathRoot(ProjectFilePath), "Foo.cs"); + + var code = +$@" + + + +namespace [||]NS +{{ + class Class1 + {{ + }} +}} + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + + [WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)] + public async Task NoAction_NoDeclaration() + { + var folders = new[] { "A" }; + var documentPath = CreateDocumentFilePath(folders); + + var code = +$@" + + + +using System; +[||] + + +"; + + await TestMissingInRegularAndScriptAsync(code); + } + } +} diff --git a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs index f24dd333a1c3c3786532469a2d88de46bb52d742..52ea2a34ea47c5180dcf1adadea43415fe2b3291 100644 --- a/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs +++ b/src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs @@ -462,7 +462,7 @@ void TestAnnotations(ImmutableArray expectedSpans, string annotationKi } } - private static Document GetDocumentToVerify(DocumentId expectedChangedDocumentId, Solution oldSolution, Solution newSolution) + protected static Document GetDocumentToVerify(DocumentId expectedChangedDocumentId, Solution oldSolution, Solution newSolution) { Document document; // If the expectedChangedDocumentId is not mentioned then we expect only single document to be changed diff --git a/src/EditorFeatures/TestUtilities/Workspaces/TestHostProject.cs b/src/EditorFeatures/TestUtilities/Workspaces/TestHostProject.cs index 3b24a0c4f1f3e3f900f5efde2ff78c37ea211947..cfe3d471f4c7f0f37227d96175a9e0150748684d 100644 --- a/src/EditorFeatures/TestUtilities/Workspaces/TestHostProject.cs +++ b/src/EditorFeatures/TestUtilities/Workspaces/TestHostProject.cs @@ -19,7 +19,6 @@ public class TestHostProject private readonly ProjectId _id; private readonly string _name; - private readonly IEnumerable _projectReferences; private readonly IEnumerable _metadataReferences; private readonly IEnumerable _analyzerReferences; private readonly CompilationOptions _compilationOptions; @@ -30,9 +29,11 @@ public class TestHostProject private readonly VersionStamp _version; private readonly string _filePath; private readonly string _outputFilePath; + private readonly string _defaultNamespace; public IEnumerable Documents; public IEnumerable AdditionalDocuments; + public IEnumerable ProjectReferences; public string Name { @@ -42,14 +43,6 @@ public string Name } } - public IEnumerable ProjectReferences - { - get - { - return _projectReferences; - } - } - public IEnumerable MetadataReferences { get @@ -135,6 +128,11 @@ public string OutputFilePath get { return _outputFilePath; } } + public string DefaultNamespace + { + get { return _defaultNamespace; } + } + internal TestHostProject( HostLanguageServices languageServices, CompilationOptions compilationOptions, @@ -171,7 +169,8 @@ public string OutputFilePath Type hostObjectType = null, bool isSubmission = false, string filePath = null, - IList analyzerReferences = null) + IList analyzerReferences = null, + string defaultNamespace = null) { _assemblyName = assemblyName; _name = projectName; @@ -183,12 +182,13 @@ public string OutputFilePath _analyzerReferences = analyzerReferences ?? SpecializedCollections.EmptyEnumerable(); this.Documents = documents; this.AdditionalDocuments = additionalDocuments ?? SpecializedCollections.EmptyEnumerable(); - _projectReferences = SpecializedCollections.EmptyEnumerable(); + ProjectReferences = SpecializedCollections.EmptyEnumerable(); _isSubmission = isSubmission; _hostObjectType = hostObjectType; _version = VersionStamp.Create(); _filePath = filePath; _outputFilePath = GetTestOutputFilePath(filePath); + _defaultNamespace = defaultNamespace; } public TestHostProject( @@ -201,8 +201,9 @@ public string OutputFilePath IEnumerable projectReferences = null, IEnumerable metadataReferences = null, IEnumerable analyzerReferences = null, - string assemblyName = null) - : this(workspace, name, language, compilationOptions, parseOptions, SpecializedCollections.SingletonEnumerable(document), SpecializedCollections.EmptyEnumerable(), projectReferences, metadataReferences, analyzerReferences, assemblyName) + string assemblyName = null, + string defaultNamespace = null) + : this(workspace, name, language, compilationOptions, parseOptions, SpecializedCollections.SingletonEnumerable(document), SpecializedCollections.EmptyEnumerable(), projectReferences, metadataReferences, analyzerReferences, assemblyName, defaultNamespace) { } @@ -217,7 +218,8 @@ public string OutputFilePath IEnumerable projectReferences = null, IEnumerable metadataReferences = null, IEnumerable analyzerReferences = null, - string assemblyName = null) + string assemblyName = null, + string defaultNamespace = null) { _name = name ?? "TestProject"; @@ -230,12 +232,13 @@ public string OutputFilePath _parseOptions = parseOptions ?? this.LanguageServiceProvider.GetService().GetDefaultParseOptions(); this.Documents = documents ?? SpecializedCollections.EmptyEnumerable(); this.AdditionalDocuments = additionalDocuments ?? SpecializedCollections.EmptyEnumerable(); - _projectReferences = projectReferences != null ? projectReferences.Select(p => new ProjectReference(p.Id)) : SpecializedCollections.EmptyEnumerable(); + ProjectReferences = projectReferences != null ? projectReferences.Select(p => new ProjectReference(p.Id)) : SpecializedCollections.EmptyEnumerable(); _metadataReferences = metadataReferences ?? new MetadataReference[] { TestReferences.NetFx.v4_0_30319.mscorlib }; _analyzerReferences = analyzerReferences ?? SpecializedCollections.EmptyEnumerable(); _assemblyName = assemblyName ?? "TestProject"; _version = VersionStamp.Create(); _outputFilePath = GetTestOutputFilePath(_filePath); + _defaultNamespace = defaultNamespace; if (documents != null) { @@ -327,7 +330,8 @@ public ProjectInfo ToProjectInfo() this.AnalyzerReferences, this.AdditionalDocuments.Select(d => d.ToDocumentInfo()), this.IsSubmission, - this.HostObjectType); + this.HostObjectType) + .WithDefaultNamespace(this.DefaultNamespace); } // It is identical with the internal extension method 'GetDefaultExtension' defined in OutputKind.cs. diff --git a/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_Create.cs b/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_Create.cs index 53244e166da6caef40c95866e9ee9c12d7a0321b..c069994f88eaa773ade29cc8cdeef1d4aa4ad63e 100644 --- a/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_Create.cs +++ b/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_Create.cs @@ -61,6 +61,7 @@ public partial class TestWorkspace private const string ProjectNameAttribute = "Name"; private const string CheckOverflowAttributeName = "CheckOverflow"; private const string OutputKindName = "OutputKind"; + private const string DefaultNamespaceAttributeName = "DefaultNamespace"; /// /// Creates a single buffer in a workspace. diff --git a/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs b/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs index 43ed0981aa756437ee08031e75fd05343b65394f..9e0f2e31c1204cc830c09f16c1d984b8b2a56cef 100644 --- a/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs +++ b/src/EditorFeatures/TestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs @@ -261,6 +261,7 @@ public static TestWorkspace Create(string xmlDefinition, bool completed = true, var language = GetLanguage(workspace, projectElement); var assemblyName = GetAssemblyName(workspace, projectElement, ref projectId); + var defaultNamespace = GetDefaultNamespace(workspace, projectElement); string filePath; @@ -309,7 +310,7 @@ public static TestWorkspace Create(string xmlDefinition, bool completed = true, documentElementToFilePath.Add(documentElement, document.FilePath); } - return new TestHostProject(languageServices, compilationOptions, parseOptions, assemblyName, projectName, references, documents, filePath: filePath, analyzerReferences: analyzers); + return new TestHostProject(languageServices, compilationOptions, parseOptions, assemblyName, projectName, references, documents, filePath: filePath, analyzerReferences: analyzers, defaultNamespace: defaultNamespace); } private static ParseOptions GetParseOptions(XElement projectElement, string language, HostLanguageServices languageServices) @@ -445,6 +446,20 @@ private static string GetLanguage(TestWorkspace workspace, XElement projectEleme return languageName; } + private static string GetDefaultNamespace(TestWorkspace workspace, XElement projectElement) + { + // Default namespace is a C# only concept, for all other language, the value is set to null. + // The empty string returned in case no such property is define means global namespace. + var language = GetLanguage(workspace, projectElement); + if (language != LanguageNames.CSharp) + { + return null; + } + + var defaultNamespaceAttribute = projectElement.Attribute(DefaultNamespaceAttributeName); + return defaultNamespaceAttribute?.Value ?? string.Empty; + } + private static CompilationOptions CreateCompilationOptions( TestWorkspace workspace, XElement projectElement, @@ -702,7 +717,7 @@ private static IReadOnlyList GetFolders(XElement documentElement) return null; } - var folderContainers = folderAttribute.Value.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + var folderContainers = folderAttribute.Value.Split(new[] { PathUtilities.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); return new ReadOnlyCollection(folderContainers.ToList()); } diff --git a/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceService.cs b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceService.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed16ebd74e224016d50959c1306e97f0e74f7cc2 --- /dev/null +++ b/src/Features/CSharp/Portable/CodeRefactorings/SyncNamespace/CSharpSyncNamespaceService.cs @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; +using Microsoft.CodeAnalysis.CSharp.CodeGeneration; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.SyncNamespace +{ + [ExportLanguageService(typeof(ISyncNamespaceService), LanguageNames.CSharp), Shared] + internal sealed class CSharpSyncNamespaceService : + AbstractSyncNamespaceService + { + protected override SyntaxList GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl) + { + if (compilationUnitOrNamespaceDecl is NamespaceDeclarationSyntax namespaceDecl) + { + return namespaceDecl.Members; + } + else if (compilationUnitOrNamespaceDecl is CompilationUnitSyntax compilationUnit) + { + return compilationUnit.Members; + } + + throw ExceptionUtilities.Unreachable; + } + + /// + /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the namespce 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 is provided, the name in the new node might be qualified with this new namespace instead. + /// + /// A reference to a type declared inside the namespce to be changed, which is calculated based on results from + /// `SymbolFinder.FindReferencesAsync`. + /// 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. + /// The node to be replaced. This might be an ancestor of original reference. + /// The replacement node. + public override bool TryGetReplacementReferenceSyntax( + SyntaxNode reference, + ImmutableArray newNamespaceParts, + ISyntaxFactsService syntaxFacts, + out SyntaxNode old, + out SyntaxNode @new) + { + if (!(reference is SimpleNameSyntax nameRef)) + { + old = @new = 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.) + + if (syntaxFacts.IsRightSideOfQualifiedName(nameRef)) + { + old = nameRef.Parent; + var aliasQualifier = GetAliasQualifierOpt(old); + + 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() ?? SyntaxFactory.IdentifierName(SyntaxFactory.Token(SyntaxKind.GlobalKeyword)); + @new = SyntaxFactory.AliasQualifiedName(aliasNode, nameRef.WithoutTrivia()); + } + else + { + var qualifiedNamespaceName = CreateNameSyntax(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); + @new = SyntaxFactory.QualifiedName(qualifiedNamespaceName, nameRef.WithoutTrivia()); + } + + // We might lose some trivia associated with children of `outerMostNode`. + @new = @new.WithTriviaFrom(old); + return true; + } + + 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, `` and ``. + // 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, `` and ``. + + var container = qualifiedCref.Container; + var aliasQualifier = GetAliasQualifierOpt(container); + + if (IsGlobalNamespace(newNamespaceParts)) + { + // If new namespace is "", then name will be declared in global namespace. + // We will replace entire `QualifiedCrefSyntax` with a `TypeCrefSyntax`, + // which is a alias qualified simple name, similar to the regular case above. + + old = qualifiedCref; + var aliasNode = aliasQualifier?.ToIdentifierName() ?? SyntaxFactory.IdentifierName(SyntaxFactory.Token(SyntaxKind.GlobalKeyword)); + var aliasQualifiedNode = SyntaxFactory.AliasQualifiedName(aliasNode, nameRef.WithoutTrivia()); + @new = SyntaxFactory.TypeCref(aliasQualifiedNode); + } + 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. + old = container; + @new = CreateNameSyntax(newNamespaceParts, aliasQualifier, newNamespaceParts.Length - 1); + } + + return true; + } + + // Simple name reference, nothing to be done. + // The name will be resolved by adding proper import. + old = @new = nameRef; + return false; + } + + protected override string EscapeIdentifier(string identifier) + => identifier.EscapeIdentifier(); + + protected override async Task ShouldPositionTriggerRefactoringAsync(Document document, int position, CancellationToken cancellationToken) + { + var compilationUnit = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax; + + // Here's conditions that trigger the refactoring (all have to be true in each scenario): + // + // - There's only one namespace declaration in the document and all types are declared in it: + // 1. No nested namespace declaration (even it's empty). + // 2. The cursor is on the name of the namespace declaration. + // 3. The name of the namespace is valid (i.e. no errors). + // 4. No partial type declared in the namespace. Otherwise its multiple declaration will + // end up in different namespace. + // + // - There's no namespace declaration and all types in the document are declared in global namespace: + // 1. The cursor is on the name of first declared type. + // 2. No partial type declared in the document. Otherwise its multiple declaration will + // end up in different namespace. + + var triggeringNode = GetTriggeringNode(compilationUnit, position); + if (triggeringNode != null) + { + var containsPartial = await ContainsPartialTypeWithMultipleDeclarationsAsync(document, triggeringNode, cancellationToken) + .ConfigureAwait(false); + + if (!containsPartial) + { + return triggeringNode; + } + } + + return default; + + SyntaxNode GetTriggeringNode(CompilationUnitSyntax compUnit, int pos) + { + var namespaceDecls = compilationUnit.DescendantNodes(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax) + .OfType().ToImmutableArray(); + + if (namespaceDecls.Length == 1 && compilationUnit.Members.Count == 1) + { + var namespaceDeclaration = namespaceDecls[0]; + Debug.Assert(namespaceDeclaration == compilationUnit.Members[0]); + + if (namespaceDeclaration.Name.Span.IntersectsWith(position) + && namespaceDeclaration.Name.GetDiagnostics().All(diag => diag.DefaultSeverity != DiagnosticSeverity.Error)) + { + return namespaceDeclaration; + } + } + else if (namespaceDecls.Length == 0) + { + var firstMemberDeclarationName = compilationUnit.Members.FirstOrDefault().GetNameToken(); + + if (firstMemberDeclarationName != default + && firstMemberDeclarationName.Span.IntersectsWith(position)) + { + return compilationUnit; + } + } + + return null; + } + } + + /// + /// Try to change the namespace declaration based on the refacoring rules: + /// - if neither declared and 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). + /// + protected override CompilationUnitSyntax ChangeNamespaceDeclaration( + CompilationUnitSyntax compilationUnit, + ImmutableArray declaredNamespaceParts, + ImmutableArray targetNamespaceParts) + { + Debug.Assert(!declaredNamespaceParts.IsDefault && !targetNamespaceParts.IsDefault); + + // Move everything from global namespace to a namespace declaration + if (IsGlobalNamespace(declaredNamespaceParts)) + { + var targetNamespaceDecl = SyntaxFactory.NamespaceDeclaration( + name: CreateNameSyntax(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) + .WithAdditionalAnnotations(WarningAnnotation), + externs: default, + usings: default, + members: compilationUnit.Members); + return compilationUnit.WithMembers(new SyntaxList(targetNamespaceDecl)); + } + + // We should have a single member which is a namespace declaration in this compilation unit. + var namespaceDeclaration = compilationUnit.DescendantNodes().OfType().Single(); + + // Move everything to global namespace + if (IsGlobalNamespace(targetNamespaceParts)) + { + var (namespaceOpeningTrivia, namespaceClosingTrivia) = + GetOpeningAndClosingTriviaOfNamespaceDeclaration(namespaceDeclaration); + var members = namespaceDeclaration.Members; + var eofToken = compilationUnit.EndOfFileToken + .WithAdditionalAnnotations(WarningAnnotation); + + // Try to preserve trivia from original namesapce 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 iwith in the context of A.B.C first. If it's moved outside, + // it may fail to resolve. + + return compilationUnit.Update( + compilationUnit.Externs.AddRange(namespaceDeclaration.Externs), + compilationUnit.Usings.AddRange(namespaceDeclaration.Usings), + compilationUnit.AttributeLists, + members, + eofToken); + } + + // Change namespace name + return compilationUnit.ReplaceNode(namespaceDeclaration, + namespaceDeclaration.WithName( + CreateNameSyntax(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) + .WithTriviaFrom(namespaceDeclaration.Name) + .WithAdditionalAnnotations(WarningAnnotation))); + } + + private static bool IsGlobalNamespace(ImmutableArray parts) + => parts.Length == 1 && parts[0].Length == 0; + + private static string GetAliasQualifierOpt(SyntaxNode name) + { + while (true) + { + switch (name.Kind()) + { + case SyntaxKind.QualifiedName: + name = ((QualifiedNameSyntax)name).Left; + continue; + case SyntaxKind.AliasQualifiedName: + return ((AliasQualifiedNameSyntax)name).Alias.Identifier.ValueText; + } + + return null; + } + } + + private NameSyntax CreateNameSyntax(ImmutableArray namespaceParts, string aliasQualifier, int index) + { + var part = namespaceParts[index].EscapeIdentifier(); + Debug.Assert(part.Length > 0); + + var namePiece = SyntaxFactory.IdentifierName(part); + + if (index == 0) + { + return aliasQualifier == null ? (NameSyntax)namePiece : SyntaxFactory.AliasQualifiedName(aliasQualifier, namePiece); + } + else + { + return SyntaxFactory.QualifiedName(CreateNameSyntax(namespaceParts, aliasQualifier, index - 1), namePiece); + } + } + + /// + /// 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. + /// + private static (ImmutableArray openingTrivia, ImmutableArray closingTrivia) + GetOpeningAndClosingTriviaOfNamespaceDeclaration(NamespaceDeclarationSyntax namespaceDeclaration) + { + var openingBuilder = ArrayBuilder.GetInstance(); + openingBuilder.AddRange(namespaceDeclaration.GetLeadingTrivia()); + openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.LeadingTrivia); + openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.TrailingTrivia); + + var closingBuilder = ArrayBuilder.GetInstance(); + closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.LeadingTrivia); + closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.TrailingTrivia); + + return (openingBuilder.ToImmutableAndFree(), closingBuilder.ToImmutableAndFree()); + } + } +} diff --git a/src/Features/CSharp/Portable/RemoveUnnecessaryImports/AbstractCSharpRemoveUnnecessaryImportsService.cs b/src/Features/CSharp/Portable/RemoveUnnecessaryImports/AbstractCSharpRemoveUnnecessaryImportsService.cs index 2476313febfbdbf1a1615b1e91841755e666444a..8dfd5053716477632b07acd58123f8ceaa9f65c6 100644 --- a/src/Features/CSharp/Portable/RemoveUnnecessaryImports/AbstractCSharpRemoveUnnecessaryImportsService.cs +++ b/src/Features/CSharp/Portable/RemoveUnnecessaryImports/AbstractCSharpRemoveUnnecessaryImportsService.cs @@ -20,7 +20,7 @@ internal partial class AbstractCSharpRemoveUnnecessaryImportsService : AbstractRemoveUnnecessaryImportsService { public override async Task RemoveUnnecessaryImportsAsync( - Document document, + Document document, Func predicate, CancellationToken cancellationToken) { diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index 8184ee2aae6463ae001f508b3c67600bf33863a7..2764550f6d76ab18a0c9fbfdb6f362979b32265a 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -32,5 +32,6 @@ internal static class PredefinedCodeRefactoringProviderNames public const string UseExplicitType = "Use Explicit Type Code Action Provider"; public const string UseImplicitType = "Use Implicit Type Code Action Provider"; public const string UseExpressionBody = "Use Expression Body Code Action Provider"; + public const string SyncNamespace = "Sync Namespace and Folder Name Code Action Provider"; } } diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.ChangeNamespaceCodeAction.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.ChangeNamespaceCodeAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..f108fc728658fada49c78a675f630376dc434606 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.ChangeNamespaceCodeAction.cs @@ -0,0 +1,552 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.AddImports; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Simplification; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + internal abstract partial class AbstractSyncNamespaceService + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode + { + /// + /// This code action tries to change the name of the namespace declaration to + /// match the folder hierarchy of the document. The new namespace is constructed + /// by concatenating the default namespace of the project and all the folders in + /// the file path up to the project root. + /// + /// For example, if he default namespace is `A.B.C`, file path is + /// "[project root dir]\D\E\F\Class1.cs" and declared namespace in the file is + /// `Foo.Bar.Baz`, then this action will change the namespace declaration + /// to `A.B.C.D.E.F`. + /// + /// Note that it also handles the case where the target namespace or declared namespace + /// is global namespace, i.e. default namespace is "" and the file is located at project + /// root directory, and no namespace declaration in the document, respectively. + /// + internal sealed class ChangeNamespaceCodeAction : CodeAction + { + private readonly State _state; + private readonly AbstractSyncNamespaceService _service; + private readonly ImmutableArray _oldNamespaceParts; + private readonly ImmutableArray _newNamespaceParts; + + public override string Title => _state.TargetNamespace.Length == 0 + ? FeaturesResources.Change_to_global_namespace + : string.Format(FeaturesResources.Change_namespace_to_0, _state.TargetNamespace); + + public ChangeNamespaceCodeAction( + AbstractSyncNamespaceService service, + State state) + { + Debug.Assert(state.TargetNamespace != null); + + _service = service; + _state = state; + + var dotSeparator = new[] { '.' }; + _oldNamespaceParts = _state.DeclaredNamespace.Split(dotSeparator).ToImmutableArray(); + _newNamespaceParts = _state.TargetNamespace.Split(dotSeparator).ToImmutableArray(); + } + + private ImmutableArray GetDeclaredSymbolsInContainer( + SemanticModel semanticModel, + SyntaxNode node, + CancellationToken cancellationToken) + { + var declarations = _service.GetMemberDeclarationsInContainer(node); + var builder = ArrayBuilder.GetInstance(); + foreach (var declaration in declarations) + { + var symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken); + builder.AddIfNotNull(symbol); + } + + return builder.ToImmutableAndFree(); + } + + protected override async Task GetChangedSolutionAsync(CancellationToken cancellationToken) + { + // Here's the entire process for changing namespace: + // 1. Change the namespace declaration, fix references and add imports that might be necessary. + // 2. Explicitly merge the diff to get a new solution. + // 3. Remove added imports that are unnecessary. + // 4. Do another explicit diff merge based on last merged solution. + // + // The reason for doing explicit diff merge twice is so merging after remove unnecessaty imports can be correctly handled. + + var solutionAfterNamespaceChange = _state.Solution; + var referenceDocuments = PooledHashSet.GetInstance(); + + foreach (var id in _state.DocumentIds) + { + var (newSolution, refDocumentIds) = await ChangeNamespaceToMatchFoldersAsync(solutionAfterNamespaceChange, id, cancellationToken).ConfigureAwait(false); + solutionAfterNamespaceChange = newSolution; + referenceDocuments.AddRange(refDocumentIds); + } + + var solutionAfterFirstMerge = await MergeDiffAsync(_state.Solution, solutionAfterNamespaceChange, cancellationToken).ConfigureAwait(false); + + // After changing documents, we still need to remove unnecessary imports related to our change. + // We don't try to remove all imports that might become unnecessary/invalid after the namespace change, + // just ones that fully matche the old/new namespace. Because it's hard to get it right and will almost + // certainly cause perf issue. + // For example, if we are changing namespace `Foo.Bar` (which is the only namespace declaration with such name) + // to `A.B`, the using of name `Bar` in a different file below would remain untouched, even it's no longer valid: + // + // namespace Foo + // { + // using Bar; + // ~~~~~~~~~ + // } + // + // Also, because we may have added different imports to document that triggered the refactoring + // and the documents that reference affected types declared in changed namespace, we try to remove + // unnecessary imports separately. + + var solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( + solutionAfterFirstMerge, + _state.DocumentIds, + CreateAllContainingNamespaces(_oldNamespaceParts), + cancellationToken).ConfigureAwait(false); + + solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( + solutionAfterImportsRemoved, + referenceDocuments.ToImmutableArray(), + ImmutableArray.Create(_state.DeclaredNamespace, _state.TargetNamespace), + cancellationToken).ConfigureAwait(false); + + referenceDocuments.Free(); + return await MergeDiffAsync(solutionAfterFirstMerge, solutionAfterImportsRemoved, cancellationToken).ConfigureAwait(false); + } + + private ImmutableArray CreateAllContainingNamespaces(ImmutableArray parts) + { + var builder = ArrayBuilder.GetInstance(); + for (var i = 1; i <= _oldNamespaceParts.Length; ++i) + { + builder.Add(string.Join(".", _oldNamespaceParts.Take(i))); + } + + return builder.ToImmutableAndFree(); + } + + private ImmutableArray CreateImports(Document document, ImmutableArray names, bool withFormatterAnnotation) + { + var generator = SyntaxGenerator.GetGenerator(document); + var builder = ArrayBuilder.GetInstance(names.Length); + for (var i = 0; i < names.Length; ++i) + { + builder.Add(CreateImport(generator, names[i], withFormatterAnnotation)); + } + + return builder.ToImmutableAndFree(); + } + + private SyntaxNode CreateImport(SyntaxGenerator syntaxGenerator, string name, bool withFormatterAnnotation) + { + var import = syntaxGenerator.NamespaceImportDeclaration(name); + if (withFormatterAnnotation) + { + import = import.WithAdditionalAnnotations(Formatter.Annotation); + } + return import; + } + + /// + /// Try to change the namespace declaration in the document (specified by in ), + /// so that the namespace is in sync with project's default namespace and the folder structure where the document is located. + /// Returns a new solution after changing namespace, and a list of IDs for documents that also changed becuase they referenced + /// the types declared in the changed namespace (not include the document contains the declaration itself). + /// + private async Task<(Solution, ImmutableArray)> ChangeNamespaceToMatchFoldersAsync(Solution solution, DocumentId id, CancellationToken cancellationToken) + { + var document = solution.GetDocument(id); + var targetNamespace = _state.TargetNamespace; + + var declarationRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var container = declarationRoot.DescendantNodes().FirstOrDefault(node => node is TNamespaceDeclarationSyntax) ?? declarationRoot; + + // Get types declared in the changing namespace, because ee need to fix all references to them, + // e.g. change the namespace for qualified name, add imports to proper containers, etc. + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var declaredSymbols = GetDeclaredSymbolsInContainer(semanticModel, container, cancellationToken); + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + // Separating references to declaredSymbols into two groups based on wheter it's located in the same + // document as the namespace declaration. This is because code change required for them are different. + var refLocationsInCurrentDocument = new List(); + var refLocationsInOtherDocuments = new List(); + + var refLocations = await Task.WhenAll( + declaredSymbols.Select(declaredSymbol + => FindReferenceLocationsForSymbol(document, declaredSymbol, cancellationToken))).ConfigureAwait(false); + + foreach (var refLocation in refLocations.SelectMany(locs => locs)) + { + if (refLocation.Document.Id == document.Id) + { + refLocationsInCurrentDocument.Add(refLocation); + } + else + { + Debug.Assert(!PathUtilities.PathsEqual(refLocation.Document.FilePath, document.FilePath)); + refLocationsInOtherDocuments.Add(refLocation); + } + } + + var documentWithNewNamespace = await FixDeclarationDocumentAsync(document, refLocationsInCurrentDocument, cancellationToken) + .ConfigureAwait(false); + var solutionWithChangedNamespace = documentWithNewNamespace.Project.Solution; + + var refLocationGroups = refLocationsInOtherDocuments.GroupBy(loc => loc.Document.Id); + + var fixedDocuments = await Task.WhenAll( + refLocationGroups.Select(refInOneDocument => + FixReferencingDocumentAsync( + solutionWithChangedNamespace.GetDocument(refInOneDocument.Key), + refInOneDocument, + cancellationToken))).ConfigureAwait(false); + + var solutionWithFixedReferences = await MergeDocumentChangesAsync(solutionWithChangedNamespace, fixedDocuments, cancellationToken).ConfigureAwait(false); + + return (solutionWithFixedReferences, refLocationGroups.SelectAsArray(g => g.Key)); + } + + private static async Task MergeDocumentChangesAsync(Solution originalSolution, Document[] changedDocuments, CancellationToken cancellationToken) + { + foreach (var document in changedDocuments) + { + originalSolution = originalSolution.WithDocumentSyntaxRoot( + document.Id, + await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); + } + + return originalSolution; + } + + private static async Task> FindReferenceLocationsForSymbol( + Document document, ISymbol symbol, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var progress = new StreamingProgressCollector(StreamingFindReferencesProgress.Instance); + + await SymbolFinder.FindReferencesAsync( + symbolAndProjectId: SymbolAndProjectId.Create(symbol, document.Project.Id), + solution: document.Project.Solution, + documents: null, + progress: progress, + options: FindReferencesSearchOptions.Default, + cancellationToken: cancellationToken).ConfigureAwait(false); + + var referencedSymbols = progress.GetReferencedSymbols(); + return referencedSymbols.Where(refSymbol => refSymbol.Definition.Equals(symbol)) + .SelectMany(refSymbol => refSymbol.Locations).ToImmutableArray(); + } + + private async Task FixDeclarationDocumentAsync( + Document document, + IReadOnlyList refLocations, + CancellationToken cancellationToken) + { + Debug.Assert(!_newNamespaceParts.IsDefault); + + // 1. Fix references to the affected types in this document if necessary. + // 2. Add usings for containing namespaces, in case we have references + // relying on old namespace declaration for resolution. + // + // For example, in the code below, after we change namespace to + // "A.B.C", we will need to add "using Foo.Bar;". + // + // namespace Foo.Bar.Baz + // { + // class C1 + // { + // C2 _c2; // C2 is define in namespace "Foo.Bar" in another document. + // } + // } + // + // 3. Change namespace declaration to target namespace. + // 4. Simplify away unnecessary qualifications. + + var addImportService = document.GetLanguageService(); + ImmutableArray containers; + + if (refLocations.Count > 0) + { + (document, containers) = await FixReferencesAsync(document, addImportService, _service, refLocations, _newNamespaceParts, cancellationToken) + .ConfigureAwait(false); + } + else + { + // If there's no reference to types declared in this document, + // we will use root node as import container. + containers = ImmutableArray.Create(await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)); + } + + Debug.Assert(containers.Length > 0); + + // Need to import all containing namespaces of old namespace and add them to the document (if it's not global namespace) + var namesToImport = CreateAllContainingNamespaces(_oldNamespaceParts); + + var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + var placeSystemNamespaceFirst = optionSet.GetOption(GenerationOptions.PlaceSystemNamespaceFirst, document.Project.Language); + var documentWithAddedImports = await AddImportsInContainersAsync( + document, + addImportService, + containers, + namesToImport, + placeSystemNamespaceFirst, + cancellationToken).ConfigureAwait(false); + + var root = await documentWithAddedImports.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + root = _service.ChangeNamespaceDeclaration((TCompilationUnitSyntax)root, _oldNamespaceParts, _newNamespaceParts) + .WithAdditionalAnnotations(Formatter.Annotation); + + // Need to invoke formatter explicitly since we are doing the diff merge ourselves. + root = await Formatter.FormatAsync(root, Formatter.Annotation, documentWithAddedImports.Project.Solution.Workspace, optionSet, cancellationToken) + .ConfigureAwait(false); + + root = root.WithAdditionalAnnotations(Simplifier.Annotation); + var formattedDocument = documentWithAddedImports.WithSyntaxRoot(root); + return await Simplifier.ReduceAsync(formattedDocument, optionSet, cancellationToken).ConfigureAwait(false); + } + + private async Task FixReferencingDocumentAsync( + Document document, + IEnumerable refLocations, + CancellationToken cancellationToken) + { + // 1. Fully qualify all simple references (i.e. not via an alias) with new namespace. + // 2. Add using of new namespace (for each reference's container). + // 3. Try to simplify qualified names introduced from step(1). + + var addImportService = document.GetLanguageService(); + var syncNamespaceService = document.GetLanguageService(); + + var (documentWithRefFixed, containers) = + await FixReferencesAsync(document, addImportService, syncNamespaceService, refLocations, _newNamespaceParts, cancellationToken) + .ConfigureAwait(false); + + var optionSet = await documentWithRefFixed.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + var placeSystemNamespaceFirst = optionSet.GetOption(GenerationOptions.PlaceSystemNamespaceFirst, documentWithRefFixed.Project.Language); + + var documentWithAdditionalImports = await AddImportsInContainersAsync( + documentWithRefFixed, + addImportService, + containers, + ImmutableArray.Create(_state.TargetNamespace), + placeSystemNamespaceFirst, + cancellationToken).ConfigureAwait(false); + + // Need to invoke formatter explicitly since we are doing the diff merge ourselves. + var formattedDocument = await Formatter.FormatAsync(documentWithAdditionalImports, Formatter.Annotation, optionSet, cancellationToken) + .ConfigureAwait(false); + + return await Simplifier.ReduceAsync(formattedDocument, optionSet, cancellationToken).ConfigureAwait(false); + } + + /// + /// Fix each reference and return a collection of proper containers (innermost container + /// with imports) that new import should be added to based on reference locations. + /// If is specified (not default), the fix would be: + /// 1. qualify the reference with new namespace and mark it for simplification, or + /// 2. find and mark the qualified reference for simplification. + /// Otherwise, there would be no namespace replacement. + /// + private async Task<(Document, ImmutableArray)> FixReferencesAsync( + Document document, + IAddImportsService addImportService, + ISyncNamespaceService syncNamespaceService, + IEnumerable refLocations, + ImmutableArray namespaceParts, + CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var root = editor.OriginalRoot; + var containers = PooledHashSet.GetInstance(); + + var generator = SyntaxGenerator.GetGenerator(document); + var syntaxFacts = document.GetLanguageService(); + + // We need a dummy import to figure out the container for given reference. + var dummyImport = CreateImport(generator, "Dummy", withFormatterAnnotation: false); + + foreach (var refLoc in refLocations) + { + Debug.Assert(document.Id == refLoc.Document.Id); + + // Ignore references via alias. For simple cases where the alias is defined as the type we are interested, + // it will be handled properly because it is one of the reference to the type symbol. Otherwise, we don't + // attempt to make a potential fix, and user might end up with errors as a result. + if (refLoc.Alias != null) + { + continue; + } + + // Other documents in the solution might have changed after we calculated those ReferenceLocation, + // so we can't trust anything to be still up-to-date except their spans. + + // Get inner most node in case of type used as a base type. e.g. + // + // public class Foo {} + // public class Bar : Foo {} + // + // For the reference to Foo where it is used as a base class, the BaseTypeSyntax and the TypeSyntax + // have exact same span. + + var refNode = root.FindNode(refLoc.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + if (syncNamespaceService.TryGetReplacementReferenceSyntax( + refNode, namespaceParts, syntaxFacts, out var oldNode, out var newNode)) + { + editor.ReplaceNode(oldNode, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); + } + + // Use a dummy import node to figure out which container the new import will be added to. + var container = addImportService.GetImportContainer(root, refNode, dummyImport); + containers.Add(container); + } + + foreach(var container in containers) + { + editor.TrackNode(container); + } + + var fixedDocument = editor.GetChangedDocument(); + root = await fixedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var result = (fixedDocument, containers.SelectAsArray(c => root.GetCurrentNode(c))); + + containers.Free(); + return result; + } + + private async Task RemoveUnnecessaryImportsAsync( + Solution solution, + ImmutableArray ids, + ImmutableArray names, + CancellationToken cancellationToken) + { + var LinkedDocumentsToSkip = PooledHashSet.GetInstance(); + var documentsToProcessBuilder = ArrayBuilder.GetInstance(); + + foreach (var id in ids) + { + if (LinkedDocumentsToSkip.Contains(id)) + { + continue; + } + + var document = solution.GetDocument(id); + LinkedDocumentsToSkip.AddRange(document.GetLinkedDocumentIds()); + documentsToProcessBuilder.Add(document); + + document = await RemoveUnnecessaryImportsWorker( + document, + CreateImports(document, names, withFormatterAnnotation: false), + cancellationToken).ConfigureAwait(false); + solution = document.Project.Solution; + } + + var documentsToProcess = documentsToProcessBuilder.ToImmutableAndFree(); + LinkedDocumentsToSkip.Free(); + + var changeDocuments = await Task.WhenAll(documentsToProcess.Select( + doc => RemoveUnnecessaryImportsWorker( + doc, + CreateImports(doc, names, withFormatterAnnotation: false), + cancellationToken))).ConfigureAwait(false); + + return await MergeDocumentChangesAsync(solution, changeDocuments, cancellationToken).ConfigureAwait(false); + + Task RemoveUnnecessaryImportsWorker( + Document doc, + IEnumerable importsToRemove, + CancellationToken token) + { + var removeImportService = doc.GetLanguageService(); + var syntaxFacts = doc.GetLanguageService(); + + return removeImportService.RemoveUnnecessaryImportsAsync( + doc, + import => importsToRemove.Any(importToRemove => syntaxFacts.AreEquivalent(importToRemove, import)), + token); + } + } + + /// + /// Add imports for the namespace specified by + /// to the provided + /// + private async Task AddImportsInContainersAsync( + Document document, + IAddImportsService addImportService, + ImmutableArray containers, + ImmutableArray names, + bool placeSystemNamespaceFirst, + CancellationToken cancellationToken) + { + // Sort containers based on their span start, to make the result of + // adding imports deterministic. + if (containers.Length > 1) + { + containers = containers.Sort(SyntaxNodeSpanStartComparer.Instance); + } + + var imports = CreateImports(document, names, withFormatterAnnotation: true); + foreach (var container in containers) + { + // If the container is a namespace declaration, the context we pass to + // AddImportService must be a child of the declaration, otherwise the + // import will be added to root node instead. + var contextLocation = container is TNamespaceDeclarationSyntax + ? container.DescendantNodes().First() + : container; + + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + root = addImportService.AddImports(compilation, root, contextLocation, imports, placeSystemNamespaceFirst); + document = document.WithSyntaxRoot(root); + } + + return document; + } + + private static async Task MergeDiffAsync(Solution oldSolution, Solution newSolution, CancellationToken cancellationToken) + { + var diffMergingSession = new LinkedFileDiffMergingSession(oldSolution, newSolution, newSolution.GetChanges(oldSolution), logSessionInfo: false); + var mergeResult = await diffMergingSession.MergeDiffsAsync(mergeConflictHandler: null, cancellationToken: cancellationToken).ConfigureAwait(false); + return mergeResult.MergedSolution; + } + + private class SyntaxNodeSpanStartComparer : IComparer + { + private SyntaxNodeSpanStartComparer() + { + } + + public static SyntaxNodeSpanStartComparer Instance { get; } = new SyntaxNodeSpanStartComparer(); + + public int Compare(SyntaxNode x, SyntaxNode y) + => x.Span.Start - y.Span.Start; + } + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.MoveFileCodeAction.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.MoveFileCodeAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..b72211b2106033d381d48bae690400a1ef43effa --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.MoveFileCodeAction.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + internal abstract partial class AbstractSyncNamespaceService + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode + { + internal sealed class MoveFileCodeAction : CodeAction + { + private readonly State _state; + private readonly ImmutableArray _newfolders; + + public override string Title + => _newfolders.Length > 0 + ? string.Format(FeaturesResources.Move_file_to_0, string.Join(PathUtilities.DirectorySeparatorStr, _newfolders)) + : FeaturesResources.Move_file_to_project_root_folder; + + public MoveFileCodeAction(State state, ImmutableArray newFolders) + { + _state = state; + _newfolders = newFolders; + } + + internal override bool PerformFinalApplicabilityCheck => true; + + internal override bool IsApplicable(Workspace workspace) + { + // Due to some existing issue, move file action is not available for CPS projects. + return workspace.CanRenameFilesDuringCodeActions(workspace.CurrentSolution.GetDocument(_state.OriginalDocumentId).Project); + } + + protected override async Task> ComputeOperationsAsync(CancellationToken cancellationToken) + { + var id = _state.OriginalDocumentId; + var solution = _state.Solution; + var document = solution.GetDocument(id); + var newDocumentId = DocumentId.CreateNewId(document.Project.Id, document.Name); + + solution = solution.RemoveDocument(id); + + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + solution = solution.AddDocument(newDocumentId, document.Name, text, folders: _newfolders); + + return ImmutableArray.Create( + new ApplyChangesOperation(solution), + new OpenDocumentOperation(newDocumentId, activateIfAlreadyOpen: true)); + } + + public static ImmutableArray Create(State state) + { + Debug.Assert(state.RelativeDeclaredNamespace != null); + + // Since all documents have identical folder structure, we can do the computation on any of them. + var document = state.Solution.GetDocument(state.OriginalDocumentId); + // In case the relative namespace is "", the file should be moved to project root, + // set `parts` to empty to indicate that. + var parts = state.RelativeDeclaredNamespace.Length == 0 + ? ImmutableArray.Empty + : state.RelativeDeclaredNamespace.Split(new[] { '.' }).ToImmutableArray(); + + // Invalid char can only appear in namespace name when there's error, + // which we have checked before creating any code actions. + Debug.Assert(parts.IsEmpty || parts.Any(s => s.IndexOfAny(Path.GetInvalidPathChars()) < 0)); + + var projectRootFolder = FolderInfo.CreateFolderHierarchyForProject(document.Project); + var candidateFolders = FindCandidateFolders(projectRootFolder, parts, ImmutableArray.Empty); + return candidateFolders.SelectAsArray(folders => new MoveFileCodeAction(state, folders)); + } + + /// + /// We try to provide additional "move file" options if we can find existing folders that matches target namespace. + /// For example, if the target namespace is 'DefaultNamesapce.A.B.C', and there's a folder 'ProjectRoot\A.B\' already + /// exists, then will provide two actions, "move file to ProjectRoot\A.B\C\" and "move file to ProjectRoot\A\B\C\". + /// + private static ImmutableArray> FindCandidateFolders( + FolderInfo currentFolderInfo, + ImmutableArray parts, + ImmutableArray currentFolder) + { + if (parts.IsEmpty) + { + return ImmutableArray.Create(currentFolder); + } + + // Try to figure out all possible folder names that can match the target namespace. + // For example, if the target is "A.B.C", then the matching folder names include + // "A", "A.B" and "A.B.C". The item "index" in the result tuple is the number + // of items in namespace parts used to construct iten "foldername". + var candidates = Enumerable.Range(1, parts.Length) + .Select(i => (foldername: string.Join(".", parts.Take(i)), index: i)) + .ToImmutableDictionary(t => t.foldername, t => t.index, PathUtilities.Comparer); + + var subFolders = currentFolderInfo.ChildFolders; + + var builder = ArrayBuilder>.GetInstance(); + foreach (var (folderName, index) in candidates) + { + if (subFolders.TryGetValue(folderName, out var matchingFolderInfo)) + { + var newParts = index >= parts.Length + ? ImmutableArray.Empty + : ImmutableArray.Create(parts, index, parts.Length - index); + var newCurrentFolder = currentFolder.Add(matchingFolderInfo.Name); + builder.AddRange(FindCandidateFolders(matchingFolderInfo, newParts, newCurrentFolder)); + } + } + + // Make sure we always have the default path as an available option to the user + // (which might have been found by the search above, therefore the check here) + // For example, if the target namespace is "A.B.C.D", and there's folder \A.B\, + // the search above would only return "\A.B\C\D". We'd want to provide + // "\A\B\C\D" as the default path. + var defaultPathBasedOnCurrentFolder = currentFolder.AddRange(parts); + if (builder.All(folders => !folders.SequenceEqual(defaultPathBasedOnCurrentFolder, PathUtilities.Comparer))) + { + builder.Add(defaultPathBasedOnCurrentFolder); + } + + return builder.ToImmutableAndFree(); + } + + private class FolderInfo + { + private Dictionary _childFolders; + + public string Name { get; } + + public IReadOnlyDictionary ChildFolders => _childFolders; + + private FolderInfo(string name) + { + Name = name; + _childFolders = new Dictionary(StringComparer.Ordinal); + } + + private void AddFolder(IEnumerable folder) + { + if (!folder.Any()) + { + return; + } + + var firstFolder = folder.First(); + if (!_childFolders.TryGetValue(firstFolder, out var firstFolderInfo)) + { + firstFolderInfo = new FolderInfo(firstFolder); + _childFolders[firstFolder] = firstFolderInfo; + } + + firstFolderInfo.AddFolder(folder.Skip(1)); + } + + // TODO: + // Since we are getting folder data from documents, only non-empty folders + // in the project are discovered. It's possible to get complete folder structure + // from VS but it requires UI thread to do so. We might want to revisit this later. + public static FolderInfo CreateFolderHierarchyForProject(Project project) + { + var handledFolders = new HashSet(StringComparer.Ordinal); + + var rootFolderInfo = new FolderInfo(""); + foreach (var document in project.Documents) + { + var folders = document.Folders; + if (handledFolders.Add(string.Join(PathUtilities.DirectorySeparatorStr, folders))) + { + rootFolderInfo.AddFolder(folders); + } + } + return rootFolderInfo; + } + } + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.State.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.State.cs new file mode 100644 index 0000000000000000000000000000000000000000..bddc266f1ae3423c7633a67f58776e4e1eca6ca6 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.State.cs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + internal abstract partial class AbstractSyncNamespaceService + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode + { + internal sealed class State + { + private static readonly SymbolDisplayFormat s_qualifiedNameOnlyFormat = + new SymbolDisplayFormat(globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + public Solution Solution { get; } + + /// + /// The document in which the refactoring is triggered. + /// + public DocumentId OriginalDocumentId { get; } + + /// + /// The refactoring is also enabled for document in a multi-targeting project, + /// which is the only form of linked document allowed. This property returns IDs + /// of the original document that triggered the refactoring plus every such linked + /// documents. + /// + public ImmutableArray DocumentIds { get; } + + /// + /// This is the default namespace defined in the project file. + /// + public string DefaultNamespace { get; } + + /// + /// This is the name of the namespace declaration that trigger the refactoring. + /// + public string DeclaredNamespace { get; } + + /// + /// This is the new name we want to change the namespace to. + /// Empty string means global namespace, whereas null means change namespace action is not available. + /// + public string TargetNamespace { get; } + + /// + /// This is the part of the declared namespace that is contained in default namespace. + /// We will use this to construct target folder to move the file to. + /// For example, if default namespace is `A` and declared namespace is `A.B.C`, + /// this would be `B.C`. + /// + public string RelativeDeclaredNamespace { get; } + + private State( + Solution solution, + DocumentId originalDocumentId, + ImmutableArray documentIds, + string rootNamespce, + string targetNamespace, + string declaredNamespace, + string relativeDeclaredNamespace) + { + Solution = solution; + OriginalDocumentId = originalDocumentId; + DocumentIds = documentIds; + DefaultNamespace = rootNamespce; + TargetNamespace = targetNamespace; + DeclaredNamespace = declaredNamespace; + RelativeDeclaredNamespace = relativeDeclaredNamespace; + } + + /// + /// This refactoring only supports non-linked document and linked document in the form of + /// documents in multi-targeting project. Also for simplicity, we also don't support document + /// what has different file path and logical path in project (i.e. [ProjectRoot] + `Document.Folders`). + /// If the requirements above is met, we will return IDs of all documents linked to the specified + /// document (inclusive), an array of single element will be returned for non-linked document. + /// + private static bool IsSupportedLinkedDocument(Document document, out ImmutableArray allDocumentIds) + { + var solution = document.Project.Solution; + var linkedDocumentids = document.GetLinkedDocumentIds(); + + // TODO: figure out how to properly determine if and how a document is linked using project system. + + // If we found a linked document which is part of a project with differenct project file, + // then it's an actual linked file (i.e. not a multi-targeting project). We don't support that, because + // we don't know which default namespace and folder path we should use to construct target + // namespace. + if (linkedDocumentids.Any(id => + !PathUtilities.PathsEqual(solution.GetDocument(id).Project.FilePath, document.Project.FilePath))) + { + allDocumentIds = default; + return false; + } + + // Now determine if the actual file path matches its logical path in project + // which is constructed as \Logical\Folders\. The refactoring + // is triggered only when the two match. The reason of doing this is we don't really know + // the user's intention of keeping the file path out-of-sync with its logical path. + var projectRoot = PathUtilities.GetDirectoryName(document.Project.FilePath); + var folderPath = Path.Combine(document.Folders.ToArray()); + + var absoluteDircetoryPath = PathUtilities.GetDirectoryName(document.FilePath); + var logicalDirectoryPath = PathUtilities.CombineAbsoluteAndRelativePaths(projectRoot, folderPath); + + if (PathUtilities.PathsEqual(absoluteDircetoryPath, logicalDirectoryPath)) + { + allDocumentIds = linkedDocumentids.Add(document.Id); + return true; + } + else + { + allDocumentIds = default; + return false; + } + } + + private static string GetDefaultNamespace(ImmutableArray documents, ISyntaxFactsService syntaxFacts) + { + // For all projects containing all the linked documents, bail if + // 1. Any of them doesn't have default namespace, or + // 2. Multiple default namespace are found. (this might be possible by tweaking project file). + // The refactoring depends on a single default namespace to operate. + var defaultNamespaceFromProjects = new HashSet( + documents.Select(d => d.Project.DefaultNamespace), + syntaxFacts.StringComparer); + + if (defaultNamespaceFromProjects.Count != 1 + || defaultNamespaceFromProjects.First() == null) + { + return default; + } + + return defaultNamespaceFromProjects.Single(); + } + + private static async Task<(bool shouldTrigger, string declaredNamespace)> TryGetNamespaceDeclarationAsync( + TextSpan textSpan, + ImmutableArray documents, + AbstractSyncNamespaceService service, + CancellationToken cancellationToken) + { + // If the cursor location doesn't meet the requirement to trigger the refactoring in any of the documents + // (See `ShouldPositionTriggerRefactoringAsync`), or we are getting different namespace declarations among + // those documents, then we know we can't make a proper code change. We will return false and the refactoring + // will then bail. We use span of namespace declaration found in each document to decide if they are identical. + + var spansForNamespaceDeclaration = PooledDictionary.GetInstance(); + + try + { + foreach (var document in documents) + { + var compilationUnitOrNamespaceDeclOpt = await service.ShouldPositionTriggerRefactoringAsync(document, textSpan.Start, cancellationToken) + .ConfigureAwait(false); + + if (compilationUnitOrNamespaceDeclOpt is TNamespaceDeclarationSyntax namespaceDeclaration) + { + spansForNamespaceDeclaration[namespaceDeclaration.Span] = namespaceDeclaration; + } + else if (compilationUnitOrNamespaceDeclOpt is TCompilationUnitSyntax) + { + // In case there's no namespace declaration in the document, we used an empty span as key, + // since a valid namespace declaration node can't have zero length. + spansForNamespaceDeclaration[default] = null; + } + else + { + return default; + } + } + + if (spansForNamespaceDeclaration.Count != 1) + { + return default; + } + + var namespaceDecl = spansForNamespaceDeclaration.Values.Single(); + var declaredNamespace = namespaceDecl == null + // namespaceDecl == null means the target namespace is global namespace. + ? string.Empty + // Since the node in each document has identical type and span, + // they should have same name. + : SyntaxGenerator.GetGenerator(documents.First()).GetName(namespaceDecl); + + return (true, declaredNamespace); + } + finally + { + spansForNamespaceDeclaration.Free(); + } + } + + public static async Task CreateAsync( + AbstractSyncNamespaceService service, + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + if (document.Project.FilePath == null + || !textSpan.IsEmpty + || document.Project.Solution.Workspace.Kind == WorkspaceKind.MiscellaneousFiles + || document.IsGeneratedCode(cancellationToken)) + { + return null; + } + + if (!IsSupportedLinkedDocument(document, out var documentIds)) + { + return null; + } + + var syntaxFacts = document.GetLanguageService(); + var solution = document.Project.Solution; + var documents = documentIds.SelectAsArray(id => solution.GetDocument(id)); + + var defaultNamespace = GetDefaultNamespace(documents, syntaxFacts); + if (defaultNamespace == null) + { + return null; + } + + var (shouldTrigger, declaredNamespace) = + await TryGetNamespaceDeclarationAsync(textSpan, documents, service, cancellationToken).ConfigureAwait(false); + + if (!shouldTrigger) + { + return null; + } + + // Namespace can't be changed if we can't construct a valid qualified identifier from folder names. + // In this case, we might still be able to provide refactoring to move file to new location. + var namespaceFromFolders = TryBuildNamespaceFromFolders(service, document.Folders, syntaxFacts); + var targetNamespace = namespaceFromFolders == null + ? null + : ConcatNamespace(defaultNamespace, namespaceFromFolders); + + // No action required if namespace already matches folders. + if (syntaxFacts.StringComparer.Equals(targetNamespace, declaredNamespace)) + { + return null; + } + + // Only provide "move file" action if default namespace contains declared namespace. + // For example, if the default namespace is `Microsoft.CodeAnalysis`, and declared + // namespace is `System.Diagnostics`, it's very likely this document is an outlier + // in the project and user probably has some special rule for it. + var relativeNamespace = GetRelativeNamespace(defaultNamespace, declaredNamespace, syntaxFacts); + + return new State( + solution, + document.Id, + documentIds, + defaultNamespace, + targetNamespace, + declaredNamespace, + relativeNamespace); + } + + /// + /// Create a qualified identifier as the suffix of namespace based on a list of folder names. + /// + private static string TryBuildNamespaceFromFolders( + AbstractSyncNamespaceService service, + IEnumerable folders, + ISyntaxFactsService syntaxFacts) + { + var parts = folders.SelectMany(folder => folder.Split(new[] { '.' }).SelectAsArray(service.EscapeIdentifier)); + return parts.All(syntaxFacts.IsValidIdentifier) ? string.Join(".", parts) : null; + } + + private static string ConcatNamespace(string rootNamespace, string namespaceSuffix) + { + Debug.Assert(rootNamespace != null && namespaceSuffix != null); + if (namespaceSuffix.Length == 0) + { + return rootNamespace; + } + else if (rootNamespace.Length == 0) + { + return namespaceSuffix; + } + else + { + return rootNamespace + "." + namespaceSuffix; + } + } + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.cs new file mode 100644 index 0000000000000000000000000000000000000000..84ea0cc2b34674ca1e46bede27d46455fb7c41cc --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceService.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + internal abstract partial class AbstractSyncNamespaceService : + ISyncNamespaceService + where TNamespaceDeclarationSyntax : SyntaxNode + where TCompilationUnitSyntax : SyntaxNode + where TMemberDeclarationSyntax : SyntaxNode + { + public async Task> GetRefactoringsAsync( + Document document, TextSpan textSpan, CancellationToken cancellationToken) + { + var state = await State.CreateAsync(this, document, textSpan, cancellationToken).ConfigureAwait(false); + if (state == null) + { + return default; + } + + return CreateCodeActions(this, state); + } + + public abstract bool TryGetReplacementReferenceSyntax( + SyntaxNode reference, ImmutableArray newNamespaceParts, ISyntaxFactsService syntaxFacts, out SyntaxNode old, out SyntaxNode @new); + + protected abstract string EscapeIdentifier(string identifier); + + protected abstract TCompilationUnitSyntax ChangeNamespaceDeclaration( + TCompilationUnitSyntax root, ImmutableArray declaredNamespaceParts, ImmutableArray targetNamespaceParts); + + protected abstract SyntaxList GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl); + + /// + /// Determine if this refactoring should be triggered based on current cursor position and if there's any partial + /// type declarations. It should only be triggered if the cursor is: + /// (1) in the name of only namespace declaration + /// (2) in the name of first declaration in global namespace if there's no namespace declaration in this document. + /// + /// + /// If the refactoring should be triggered, then returns the only namespace declaration node in the document (or type + /// ) or the compilation unit node (of type ) + /// if no namespace declaration in the document. Otherwise, return null. + /// + protected abstract Task ShouldPositionTriggerRefactoringAsync(Document document, int position, CancellationToken cancellationToken); + + protected static SyntaxAnnotation WarningAnnotation { get; } + = CodeActions.WarningAnnotation.Create( + FeaturesResources.Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning); + + protected async Task ContainsPartialTypeWithMultipleDeclarationsAsync( + Document document, SyntaxNode compilationUnitOrNamespaceDecl, CancellationToken cancellationToken) + { + var memberDecls = GetMemberDeclarationsInContainer(compilationUnitOrNamespaceDecl); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticFacts = document.GetLanguageService(); + + foreach (var memberDecl in memberDecls) + { + var memberSymbol = semanticModel.GetDeclaredSymbol(memberDecl, cancellationToken); + + // Simplify the check by assuming no multiple partial declarations in one document + if (memberSymbol is ITypeSymbol typeSymbol + && typeSymbol.DeclaringSyntaxReferences.Length > 1 + && semanticFacts.IsPartial(typeSymbol, cancellationToken)) + { + return true; + } + } + return false; + } + + /// + /// Try get the relative namespace for based on , + /// if is the containing namespace of . + /// For example: + /// - If is "A.B" and is "A.B.C.D", then + /// the relative namespace is "C.D". + /// - If is "A.B" and is also "A.B", then + /// the relative namespace is "". + /// - If is "" then the relative namespace us . + /// + private static string GetRelativeNamespace(string relativeTo, string @namespace, ISyntaxFactsService syntaxFacts) + { + Debug.Assert(relativeTo != null && @namespace != null); + + if (syntaxFacts.StringComparer.Equals(@namespace, relativeTo)) + { + return string.Empty; + } + else if (relativeTo.Length == 0) + { + return @namespace; + } + else if (relativeTo.Length >= @namespace.Length) + { + return null; + } + + var containingText = relativeTo + "."; + var namespacePrefix = @namespace.Substring(0, containingText.Length); + + return syntaxFacts.StringComparer.Equals(containingText, namespacePrefix) + ? @namespace.Substring(relativeTo.Length + 1) + : null; + } + + private static ImmutableArray CreateCodeActions( + AbstractSyncNamespaceService service, State state) + { + var builder = ArrayBuilder.GetInstance(); + + // No move file action if rootnamespace isn't a prefix of current declared namespace + if (state.RelativeDeclaredNamespace != null) + { + builder.AddRange(MoveFileCodeAction.Create(state)); + } + + // No change namespace action if we can't construct a valid namespace from rootnamespace and folder names. + if (state.TargetNamespace != null) + { + builder.Add(new ChangeNamespaceCodeAction(service, state)); + } + + return builder.ToImmutableAndFree(); + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/ISyncNamespaceService.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/ISyncNamespaceService.cs new file mode 100644 index 0000000000000000000000000000000000000000..0d27fa671cf53c3e4f0541a133c789995d298de4 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/ISyncNamespaceService.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + internal interface ISyncNamespaceService : ILanguageService + { + Task> GetRefactoringsAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken); + + /// + /// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the + /// namespce 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 is provided, the name + /// in the new node might be qualified with this new namespace instead. + /// + /// A reference to a type declared inside the namespce to be changed, which is calculated + /// based on results from `SymbolFinder.FindReferencesAsync`. + /// If specified, the namespace of original reference will be replaced with given + /// namespace in the replacement node. + /// The node to be replaced. This might be an ancestor of original + /// The replacement node. + bool TryGetReplacementReferenceSyntax( + SyntaxNode reference, + ImmutableArray newNamespaceParts, + ISyntaxFactsService syntaxFacts, + out SyntaxNode old, + out SyntaxNode @new); + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..43e5a756aa31449fd1ed0c2498fa7b2bc3e9de90 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/SyncNamespaceCodeRefactoringProvider.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.SyncNamespace), Shared] + internal sealed class SyncNamespaceCodeRefactoringProvider : CodeRefactoringProvider + { + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var document = context.Document; + var textSpan = context.Span; + var cancellationToken = context.CancellationToken; + + var service = document.GetLanguageService(); + var actions = await service.GetRefactoringsAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + + context.RegisterRefactorings(actions); + } + } +} diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index ed170f4da49adcf8fa37bc3b68ee33aba19431fe..1a82783e22f0c39c5a0734e439a765eebdc390a5 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -723,6 +723,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Change namespace to '{0}'. + /// + internal static string Change_namespace_to_0 { + get { + return ResourceManager.GetString("Change_namespace_to_0", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change signature.... /// @@ -732,6 +741,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Change to global namespace. + /// + internal static string Change_to_global_namespace { + get { + return ResourceManager.GetString("Change_to_global_namespace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Changes to expression trees may result in behavior changes at runtime. /// @@ -2454,6 +2472,24 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Move file to '{0}'. + /// + internal static string Move_file_to_0 { + get { + return ResourceManager.GetString("Move_file_to_0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move file to project root folder. + /// + internal static string Move_file_to_project_root_folder { + get { + return ResourceManager.GetString("Move_file_to_project_root_folder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Move type to {0}. /// @@ -4105,6 +4141,16 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Warning: Changing namespace may produce invalid code and change code meaning.. + /// + internal static string Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning { + get { + return ResourceManager.GetString("Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning" + + "", resourceCulture); + } + } + /// /// Looks up a localized string similar to Warning: Collection may be modified during iteration.. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index a0394bf8c192f0a0c625ae8c445e43da9ef9aa75..b335c67fa656897f9f8b2b2bab96cc0d61890e27 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1442,6 +1442,21 @@ This version used in: {2} Modifying source file {0} will prevent the debug session from continuing due to internal error: {1}. + + Change namespace to '{0}' + + + Move file to '{0}' + + + Move file to project root folder + + + Change to global namespace + + + Warning: Changing namespace may produce invalid code and change code meaning. + Use compound assignment diff --git a/src/Features/Core/Portable/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsService.cs b/src/Features/Core/Portable/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsService.cs index 4bfeac9b5973e58d471ddb10207819f3e28db57d..3a16e70bdbccd92acc5d95216a161c622bf897b2 100644 --- a/src/Features/Core/Portable/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsService.cs +++ b/src/Features/Core/Portable/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsService.cs @@ -12,9 +12,9 @@ namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports { - internal abstract class AbstractRemoveUnnecessaryImportsService : - IRemoveUnnecessaryImportsService, - IUnnecessaryImportsService, + internal abstract class AbstractRemoveUnnecessaryImportsService : + IRemoveUnnecessaryImportsService, + IUnnecessaryImportsService, IEqualityComparer where T : SyntaxNode { public Task RemoveUnnecessaryImportsAsync(Document document, CancellationToken cancellationToken) @@ -48,7 +48,7 @@ protected SyntaxToken StripNewLines(Document document, SyntaxToken token) } protected abstract ImmutableArray GetUnnecessaryImports( - SemanticModel model, SyntaxNode root, + SemanticModel model, SyntaxNode root, Func predicate, CancellationToken cancellationToken); protected async Task> GetCommonUnnecessaryImportsOfAllContextAsync( diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index aeccb60a75d8b910f68b32150963f0881fb00c37..012bc1b003b61c267c21b8650533a4fa9804df65 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Formátuje se dokument. + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index 9234a60ed57684e00ae5c6b525db7dc2e8b8d363..c02403e2c27d59bc666f0c09614055b38594e671 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Dokument wird formatiert + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 98aab2b54cc917782c9cf1f4a9272ec03b6cf430..a43c5d9c88fa75ba8786282ba3e2cbc6daa59ab8 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Aplicando formato al documento + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index afe27e2983752ccdb1e7a3b4cd33ee6ba3e51497..a20b168cf6df07d43b8164be63e34344d09aa386 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Mise en forme du document + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 3f6979d907aed2d8a17b49ce484646ffb15ca653..20afc752379cd32175b0ff174f1d3a2a6559b2c7 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Formattazione del documento + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index bc8fc4c3ffdf25d81ae0e4873b7c548bcec5eea7..cb961ec0fa437a7275e18f222b9ce9ca2c14d5f1 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ ドキュメントの書式設定 + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index f9aa3fbb8ced649000310dfc5235141565ea2665..5ebcf905b429fe89ae4a20f9900f0b6375153181 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ 문서 서식 지정 + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index bf58d8e35493ad654b9ad653c597ef88ff84e387..fa038b200561cc9c81e757870b6cd4cae997a2b4 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Formatowanie dokumentu + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index 1fbae6bb2e36bcf47539ba83f1183de340e7bf54..2e8c3f03a21661692eb4607689d898b826c2ac17 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Formatando documento + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 4bb611a430124f9429912783261fc9a8558e3d90..4f66a11175bfbc2079d69bd23a73ade30800b1e4 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Форматирование документа + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index e2a95325eb5fd10eb0f0236445a4c5d42398bcca..9114cfa2241015448b1c11519c2794075e3cf08f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ Belge biçimlendiriliyor + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index 42b83b960d4634408c98147a591d7d638b7d7082..8454e25bf83c4e668814dd9886329b75748088e1 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ 设置文档格式 + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index dfedd9ca7b2dc3686de280b1408b9c62549a3e36..67975adece6256ff7ab2f8b9e3b4553093907bc2 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -32,6 +32,16 @@ Adding a method with an explicit interface specifier will prevent the debug session from continuing. + + Change namespace to '{0}' + Change namespace to '{0}' + + + + Change to global namespace + Change to global namespace + + Code Quality Code Quality @@ -87,6 +97,16 @@ 正在將文件格式化 + + Move file to '{0}' + Move file to '{0}' + + + + Move file to project root folder + Move file to project root folder + + Indexing can be simplified Indexing can be simplified @@ -182,6 +202,11 @@ Use interpolated verbatim string + + Warning: Changing namespace may produce invalid code and change code meaning. + Warning: Changing namespace may produce invalid code and change code meaning. + + Use range operator Use range operator diff --git a/src/Features/VisualBasic/Portable/CodeRefactorings/SyncNamespace/VisualBasicSyncNamespaceService.vb b/src/Features/VisualBasic/Portable/CodeRefactorings/SyncNamespace/VisualBasicSyncNamespaceService.vb new file mode 100644 index 0000000000000000000000000000000000000000..7e804e5c22ec8c8d542dd5992a5d65a012415810 --- /dev/null +++ b/src/Features/VisualBasic/Portable/CodeRefactorings/SyncNamespace/VisualBasicSyncNamespaceService.vb @@ -0,0 +1,70 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Collections.Immutable +Imports System.Composition +Imports System.Threading +Imports Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.LanguageServices +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings.SyncNamespace + + Friend Class VisualBasicSyncNamespaceService + Inherits AbstractSyncNamespaceService(Of NamespaceStatementSyntax, CompilationUnitSyntax, StatementSyntax) + + Public Overrides Function TryGetReplacementReferenceSyntax(reference As SyntaxNode, newNamespaceParts As ImmutableArray(Of String), syntaxFacts As ISyntaxFactsService, ByRef old As SyntaxNode, ByRef [new] As SyntaxNode) As Boolean + Dim nameRef = TryCast(reference, SimpleNameSyntax) + If nameRef IsNot Nothing Then + old = If(syntaxFacts.IsRightSideOfQualifiedName(nameRef), nameRef.Parent, nameRef) + + If old Is nameRef Or newNamespaceParts.IsDefaultOrEmpty Then + [new] = old + Else + If newNamespaceParts.Length = 1 And newNamespaceParts(0).Length = 0 Then + [new] = SyntaxFactory.QualifiedName(SyntaxFactory.GlobalName(), nameRef.WithoutTrivia()) + Else + Dim qualifiedNamespaceName = CreateNameSyntax(newNamespaceParts, newNamespaceParts.Length - 1) + [new] = SyntaxFactory.QualifiedName(qualifiedNamespaceName, nameRef.WithoutTrivia()) + End If + [new] = [new].WithTriviaFrom(old) + End If + Return True + Else + old = Nothing + [new] = Nothing + Return False + End If + End Function + + Protected Overrides Function EscapeIdentifier(identifier As String) As String + Return identifier.EscapeIdentifier() + End Function + + ' This is only reachable when called from a VB refacoring provider, which is not implemented yet. + Protected Overrides Function ChangeNamespaceDeclaration(root As CompilationUnitSyntax, declaredNamespaceParts As ImmutableArray(Of String), targetNamespaceParts As ImmutableArray(Of String)) As CompilationUnitSyntax + Throw ExceptionUtilities.Unreachable + End Function + + ' This is only reachable when called from a VB refacoring provider, which is not implemented yet. + Protected Overrides Function GetMemberDeclarationsInContainer(compilationUnitOrNamespaceDecl As SyntaxNode) As SyntaxList(Of StatementSyntax) + Throw ExceptionUtilities.Unreachable + End Function + + ' This is only reachable when called from a VB refacoring provider, which is not implemented yet. + Protected Overrides Function ShouldPositionTriggerRefactoringAsync(document As Document, position As Integer, cancellationToken As CancellationToken) As Task(Of SyntaxNode) + Throw ExceptionUtilities.Unreachable + End Function + + Private Function CreateNameSyntax(namespaceParts As ImmutableArray(Of String), index As Integer) As NameSyntax + Dim part = namespaceParts(index).EscapeIdentifier() + Dim namePiece = SyntaxFactory.IdentifierName(part) + + If index = 0 Then + Return namePiece + Else + Return SyntaxFactory.QualifiedName(CreateNameSyntax(namespaceParts, index - 1), namePiece) + End If + End Function + End Class +End Namespace diff --git a/src/Test/Utilities/Portable/Traits/Traits.cs b/src/Test/Utilities/Portable/Traits/Traits.cs index 1e9959d1845140af76b68bea07f80388e65d6640..34f039f72c82d944900bba017c9ac0aee078da3e 100644 --- a/src/Test/Utilities/Portable/Traits/Traits.cs +++ b/src/Test/Utilities/Portable/Traits/Traits.cs @@ -125,6 +125,7 @@ public static class Features public const string CodeActionsSimplifyTypeNames = "CodeActions.SimplifyTypeNames"; public const string CodeActionsSpellcheck = "CodeActions.Spellcheck"; public const string CodeActionsSuppression = "CodeActions.Suppression"; + public const string CodeActionsSyncNamespace = "CodeActions.SyncNamespace"; public const string CodeActionsUseInterpolatedVerbatimString = "CodeActions.UseInterpolatedVerbatimString"; public const string CodeActionsUseAutoProperty = "CodeActions.UseAutoProperty"; public const string CodeActionsUseCoalesceExpression = "CodeActions.UseCoalesceExpression"; diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 341a32cedf31960b0aae9f062dd3a7ab0d964c29..489552febd9f0f25664376faad0472778be58f65 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -88,6 +88,11 @@ public bool IsKeyword(SyntaxToken token) SyntaxFacts.IsKeywordKind(kind); // both contextual and reserved keywords } + public bool IsReservedKeyword(string text) + { + return SyntaxFacts.GetKeywordKind(text) != SyntaxKind.None; // reserved keywords only + } + public bool IsContextualKeyword(SyntaxToken token) { var kind = (SyntaxKind)token.RawKind; diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index d81ad489d4890f9264e3bf82f8dd839bb841654b..ac53e325dc9ff83e4378fbf50706045f71db1e99 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -31,7 +31,26 @@ internal interface ISyntaxFactsService : ILanguageService bool IsPredefinedType(SyntaxToken token, PredefinedType type); bool IsPredefinedOperator(SyntaxToken token); bool IsPredefinedOperator(SyntaxToken token, PredefinedOperator op); + /// + /// Determine if is a keyword. i.e. both reserved and contextual. + /// For example: + /// IsKeyword("class") == true + /// IsKeyword("async") == true + /// bool IsKeyword(SyntaxToken token); + /// + /// Determine if is a reserved keyword. i.e. not contextual. + /// For example: + /// IsReservedKeyword("class") == true + /// IsReservedKeyword("async") == false + /// + bool IsReservedKeyword(string text); + /// + /// Determine if is a contextual keyword. i.e. not reserved. + /// For example: + /// IsContextualKeyword("class") == false + /// IsContextualKeyword("async") == true + /// bool IsContextualKeyword(SyntaxToken token); bool IsPreprocessorKeyword(SyntaxToken token); bool IsHashToken(SyntaxToken token); diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index e070657d0f3ba5a6c39db535653a6f133cfdd53a..d058425e4eb18ef9580c5e2b13d586f50831337d 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -105,6 +105,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return token.IsKeyword() End Function + Public Function IsReservedKeyword(text As String) As Boolean Implements ISyntaxFactsService.IsReservedKeyword + Return GetKeywordKind(text) <> SyntaxKind.None + End Function + Public Function IsPreprocessorKeyword(token As SyntaxToken) As Boolean Implements ISyntaxFactsService.IsPreprocessorKeyword Return token.IsPreprocessorKeyword() End Function