未验证 提交 891fc9b3 编写于 作者: D dotnet-automerge-bot 提交者: GitHub

Merge pull request #31215 from dotnet/merges/dev16.0-preview2-to-master

Merge dev16.0-preview2 to master
// 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<string[]> 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<string>();
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<List<Tuple<Solution, Solution>>> TestOperationAsync(
TestParameters parameters,
TestWorkspace workspace,
string expectedCode)
{
var results = new List<Tuple<Solution, Solution>>();
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<TextSpan>.Empty,
renameSpans: ImmutableArray<TextSpan>.Empty,
warningSpans: ImmutableArray<TextSpan>.Empty,
navigationSpans: ImmutableArray<TextSpan>.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<SyntaxAnnotation> 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<Tuple<Solution, Solution>> 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);
}
}
}
}
// 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<string[]>();
var documentPath = CreateDocumentFilePath(Array.Empty<string>(), "File1.cs");
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_SingleAction1()
{
// current path is <root>\
// expected new path is <root>\B\C\
var defaultNamespace = "A";
var declaredNamespace = "A.B.C";
var expectedFolders = new List<string[]>();
expectedFolders.Add(new[] { "B", "C" });
var documentPath = CreateDocumentFilePath(Array.Empty<string>());
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_SingleAction2()
{
// current path is <root>\
// expected new path is <root>\B\C\D\E\
var defaultNamespace = "A";
var declaredNamespace = "A.B.C.D.E";
var expectedFolders = new List<string[]>();
expectedFolders.Add(new[] { "B", "C", "D", "E" });
var documentPath1 = CreateDocumentFilePath(Array.Empty<string>(), "File1.cs");
var documentPath2 = CreateDocumentFilePath(new[] { "B", "C" }, "File2.cs"); // file2 is in <root>\B\C\
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath1.folder}"" FilePath=""{documentPath1.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_MoveToRoot()
{
// current path is <root>\A\B\C\
// expected new path is <root>
var defaultNamespace = "";
var expectedFolders = new List<string[]>();
expectedFolders.Add(Array.Empty<string>());
var documentPath = CreateDocumentFilePath(new[] { "A", "B", "C" });
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
class [||]Class1
{{
}}
class Class2
{{
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_MultipleAction1()
{
// current path is <root>\
// expected new paths are"
// 1. <root>\B\C\D\E\
// 2. <root>\B.C\D\E\
var defaultNamespace = "A";
var declaredNamespace = "A.B.C.D.E";
var expectedFolders = new List<string[]>();
expectedFolders.Add(new[] { "B", "C", "D", "E" });
expectedFolders.Add(new[] { "B.C", "D", "E" });
var documentPath1 = CreateDocumentFilePath(Array.Empty<string>(), "File1.cs");
var documentPath2 = CreateDocumentFilePath(new[] { "B.C" }, "File2.cs"); // file2 is in <root>\B.C\
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath1.folder}"" FilePath=""{documentPath1.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_MultipleAction2()
{
// current path is <root>\
// expected new paths are:
// 1. <root>\B\C\D\E\
// 2. <root>\B.C\D\E\
// 3. <root>\B\C.D\E\
var defaultNamespace = "A";
var declaredNamespace = "A.B.C.D.E";
var expectedFolders = new List<string[]>();
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<string>(), "File1.cs");
var documentPath2 = CreateDocumentFilePath(new[] { "B", "C.D" }, "File2.cs"); // file2 is in <root>\B\C.D\
var documentPath3 = CreateDocumentFilePath(new[] { "B.C" }, "File3.cs"); // file3 is in <root>\B.C\
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath1.folder}"" FilePath=""{documentPath1.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
<Document Folders=""{documentPath3.folder}"" FilePath=""{documentPath3.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
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<string[]>();
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 <root>\B.C\
var documentPath2 = CreateDocumentFilePath(new[] { "B", "Foo" }, "File2.cs"); // file2 is in <root>\B\Foo\
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath1.folder}"" FilePath=""{documentPath1.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
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<string[]>();
expectedFolders.Add(new[] { "B", "C", "D", "E" });
var documentPath1 = CreateDocumentFilePath(new[] { "Foo.Bar", "Baz" }, "File1.cs"); // file1 is in <root>\Foo.Bar\Baz\
var documentPath2 = CreateDocumentFilePath(new[] { "B", "Foo" }, "File2.cs"); // file2 is in <root>\B\Foo\
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{documentPath1.folder}"" FilePath=""{documentPath1.filePath}"">
namespace [||]{declaredNamespace}
{{
class Class1
{{
}}
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}
}
}
// 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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace NS
{{
class [||]Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace="""" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
class Class1
{{
}}
class [||]Class2
{{
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]NS1
{{
class Class1
{{
}}
}}
namespace NS2
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]NS1
{{
class Class1
{{
}}
}}
class Class2
{{
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
class [||]Class1
{{
}}
namespace NS1
{{
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]NS1
{{
namespace NS2
{{
class Class1
{{
}}
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace="""" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMissingInRegularAndScriptAsync(code);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task NoAction_MatchingNamespace_InGlobalNamespace()
{
var folders = Array.Empty<string>();
var documentPath = CreateDocumentFilePath(folders);
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace="""" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
class [||]Class1
{{
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace="""" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]A.B.C
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" DefaultNamespace=""A"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
namespace [||]A.B.C
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document FilePath=""{filePath}"">
namespace [||]NS
{{
class Class1
{{
}}
}}
</Document>
</Project>
</Workspace>";
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 =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{documentPath.folder}"" FilePath=""{documentPath.filePath}"">
using System;
[||]
</Document>
</Project>
</Workspace>";
await TestMissingInRegularAndScriptAsync(code);
}
}
}
......@@ -462,7 +462,7 @@ void TestAnnotations(ImmutableArray<TextSpan> 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
......
......@@ -19,7 +19,6 @@ public class TestHostProject
private readonly ProjectId _id;
private readonly string _name;
private readonly IEnumerable<ProjectReference> _projectReferences;
private readonly IEnumerable<MetadataReference> _metadataReferences;
private readonly IEnumerable<AnalyzerReference> _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<TestHostDocument> Documents;
public IEnumerable<TestHostDocument> AdditionalDocuments;
public IEnumerable<ProjectReference> ProjectReferences;
public string Name
{
......@@ -42,14 +43,6 @@ public string Name
}
}
public IEnumerable<ProjectReference> ProjectReferences
{
get
{
return _projectReferences;
}
}
public IEnumerable<MetadataReference> 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<AnalyzerReference> analyzerReferences = null)
IList<AnalyzerReference> analyzerReferences = null,
string defaultNamespace = null)
{
_assemblyName = assemblyName;
_name = projectName;
......@@ -183,12 +182,13 @@ public string OutputFilePath
_analyzerReferences = analyzerReferences ?? SpecializedCollections.EmptyEnumerable<AnalyzerReference>();
this.Documents = documents;
this.AdditionalDocuments = additionalDocuments ?? SpecializedCollections.EmptyEnumerable<TestHostDocument>();
_projectReferences = SpecializedCollections.EmptyEnumerable<ProjectReference>();
ProjectReferences = SpecializedCollections.EmptyEnumerable<ProjectReference>();
_isSubmission = isSubmission;
_hostObjectType = hostObjectType;
_version = VersionStamp.Create();
_filePath = filePath;
_outputFilePath = GetTestOutputFilePath(filePath);
_defaultNamespace = defaultNamespace;
}
public TestHostProject(
......@@ -201,8 +201,9 @@ public string OutputFilePath
IEnumerable<TestHostProject> projectReferences = null,
IEnumerable<MetadataReference> metadataReferences = null,
IEnumerable<AnalyzerReference> analyzerReferences = null,
string assemblyName = null)
: this(workspace, name, language, compilationOptions, parseOptions, SpecializedCollections.SingletonEnumerable(document), SpecializedCollections.EmptyEnumerable<TestHostDocument>(), projectReferences, metadataReferences, analyzerReferences, assemblyName)
string assemblyName = null,
string defaultNamespace = null)
: this(workspace, name, language, compilationOptions, parseOptions, SpecializedCollections.SingletonEnumerable(document), SpecializedCollections.EmptyEnumerable<TestHostDocument>(), projectReferences, metadataReferences, analyzerReferences, assemblyName, defaultNamespace)
{
}
......@@ -217,7 +218,8 @@ public string OutputFilePath
IEnumerable<TestHostProject> projectReferences = null,
IEnumerable<MetadataReference> metadataReferences = null,
IEnumerable<AnalyzerReference> 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<ISyntaxTreeFactoryService>().GetDefaultParseOptions();
this.Documents = documents ?? SpecializedCollections.EmptyEnumerable<TestHostDocument>();
this.AdditionalDocuments = additionalDocuments ?? SpecializedCollections.EmptyEnumerable<TestHostDocument>();
_projectReferences = projectReferences != null ? projectReferences.Select(p => new ProjectReference(p.Id)) : SpecializedCollections.EmptyEnumerable<ProjectReference>();
ProjectReferences = projectReferences != null ? projectReferences.Select(p => new ProjectReference(p.Id)) : SpecializedCollections.EmptyEnumerable<ProjectReference>();
_metadataReferences = metadataReferences ?? new MetadataReference[] { TestReferences.NetFx.v4_0_30319.mscorlib };
_analyzerReferences = analyzerReferences ?? SpecializedCollections.EmptyEnumerable<AnalyzerReference>();
_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.
......
......@@ -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";
/// <summary>
/// Creates a single buffer in a workspace.
......
......@@ -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<string> 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<string>(folderContainers.ToList());
}
......
// 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<NamespaceDeclarationSyntax, CompilationUnitSyntax, MemberDeclarationSyntax>
{
protected override SyntaxList<MemberDeclarationSyntax> GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl)
{
if (compilationUnitOrNamespaceDecl is NamespaceDeclarationSyntax namespaceDecl)
{
return namespaceDecl.Members;
}
else if (compilationUnitOrNamespaceDecl is CompilationUnitSyntax compilationUnit)
{
return compilationUnit.Members;
}
throw ExceptionUtilities.Unreachable;
}
/// <summary>
/// Try to get a new node to replace given node, which is a reference to a top-level type declared inside the 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 <paramref name="newNamespaceParts"/> is provided, the name in the new node might be qualified with this new namespace instead.
/// </summary>
/// <param name="reference">A reference to a type declared inside the namespce to be changed, which is calculated based on results from
/// `SymbolFinder.FindReferencesAsync`.</param>
/// <param name="newNamespaceParts">If specified, and the reference is qualified with namespace, the namespace part of original reference
/// will be replaced with given namespace in the new node.</param>
/// <param name="old">The node to be replaced. This might be an ancestor of original reference.</param>
/// <param name="new">The replacement node.</param>
public override bool TryGetReplacementReferenceSyntax(
SyntaxNode reference,
ImmutableArray<string> 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, `<see cref="Foo.Baz.Bar"/>` and `<see cref="SomeAlias::Foo.Baz.Bar"/>`.
// This is the form of `cref` we need to handle as a spacial case when changing namespace name or
// changing namespace from non-global to global, other cases in these 2 scenarios can be handled in the
// same way we handle non cref references, for example, `<see cref="SomeAlias::Foo"/>` and `<see cref="Foo"/>`.
var container = qualifiedCref.Container;
var aliasQualifier = 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<SyntaxNode> 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<NamespaceDeclarationSyntax>().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;
}
}
/// <summary>
/// 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).
/// </summary>
protected override CompilationUnitSyntax ChangeNamespaceDeclaration(
CompilationUnitSyntax compilationUnit,
ImmutableArray<string> declaredNamespaceParts,
ImmutableArray<string> 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<MemberDeclarationSyntax>(targetNamespaceDecl));
}
// We should have a single member which is a namespace declaration in this compilation unit.
var namespaceDeclaration = compilationUnit.DescendantNodes().OfType<NamespaceDeclarationSyntax>().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<string> 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<string> 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);
}
}
/// <summary>
/// return trivia attached to namespace declaration.
/// Leading trivia of the node and trivia around opening brace, as well as
/// trivia around closing brace are concatenated together respectively.
/// </summary>
private static (ImmutableArray<SyntaxTrivia> openingTrivia, ImmutableArray<SyntaxTrivia> closingTrivia)
GetOpeningAndClosingTriviaOfNamespaceDeclaration(NamespaceDeclarationSyntax namespaceDeclaration)
{
var openingBuilder = ArrayBuilder<SyntaxTrivia>.GetInstance();
openingBuilder.AddRange(namespaceDeclaration.GetLeadingTrivia());
openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.LeadingTrivia);
openingBuilder.AddRange(namespaceDeclaration.OpenBraceToken.TrailingTrivia);
var closingBuilder = ArrayBuilder<SyntaxTrivia>.GetInstance();
closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.LeadingTrivia);
closingBuilder.AddRange(namespaceDeclaration.CloseBraceToken.TrailingTrivia);
return (openingBuilder.ToImmutableAndFree(), closingBuilder.ToImmutableAndFree());
}
}
}
......@@ -20,7 +20,7 @@ internal partial class AbstractCSharpRemoveUnnecessaryImportsService :
AbstractRemoveUnnecessaryImportsService<UsingDirectiveSyntax>
{
public override async Task<Document> RemoveUnnecessaryImportsAsync(
Document document,
Document document,
Func<SyntaxNode, bool> predicate,
CancellationToken cancellationToken)
{
......
......@@ -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";
}
}
// 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<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax>
where TNamespaceDeclarationSyntax : SyntaxNode
where TCompilationUnitSyntax : SyntaxNode
where TMemberDeclarationSyntax : SyntaxNode
{
internal sealed class MoveFileCodeAction : CodeAction
{
private readonly State _state;
private readonly ImmutableArray<string> _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<string> 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<IEnumerable<CodeActionOperation>> 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<CodeActionOperation>(
new ApplyChangesOperation(solution),
new OpenDocumentOperation(newDocumentId, activateIfAlreadyOpen: true));
}
public static ImmutableArray<MoveFileCodeAction> 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<string>.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<string>.Empty);
return candidateFolders.SelectAsArray(folders => new MoveFileCodeAction(state, folders));
}
/// <summary>
/// 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\".
/// </summary>
private static ImmutableArray<ImmutableArray<string>> FindCandidateFolders(
FolderInfo currentFolderInfo,
ImmutableArray<string> parts,
ImmutableArray<string> 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<ImmutableArray<string>>.GetInstance();
foreach (var (folderName, index) in candidates)
{
if (subFolders.TryGetValue(folderName, out var matchingFolderInfo))
{
var newParts = index >= parts.Length
? ImmutableArray<string>.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 <ROOT>\A.B\,
// the search above would only return "<ROOT>\A.B\C\D". We'd want to provide
// "<ROOT>\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<string, FolderInfo> _childFolders;
public string Name { get; }
public IReadOnlyDictionary<string, FolderInfo> ChildFolders => _childFolders;
private FolderInfo(string name)
{
Name = name;
_childFolders = new Dictionary<string, FolderInfo>(StringComparer.Ordinal);
}
private void AddFolder(IEnumerable<string> 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<string>(StringComparer.Ordinal);
var rootFolderInfo = new FolderInfo("<ROOT>");
foreach (var document in project.Documents)
{
var folders = document.Folders;
if (handledFolders.Add(string.Join(PathUtilities.DirectorySeparatorStr, folders)))
{
rootFolderInfo.AddFolder(folders);
}
}
return rootFolderInfo;
}
}
}
}
}
// 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<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax>
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; }
/// <summary>
/// The document in which the refactoring is triggered.
/// </summary>
public DocumentId OriginalDocumentId { get; }
/// <summary>
/// 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.
/// </summary>
public ImmutableArray<DocumentId> DocumentIds { get; }
/// <summary>
/// This is the default namespace defined in the project file.
/// </summary>
public string DefaultNamespace { get; }
/// <summary>
/// This is the name of the namespace declaration that trigger the refactoring.
/// </summary>
public string DeclaredNamespace { get; }
/// <summary>
/// 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.
/// </summary>
public string TargetNamespace { get; }
/// <summary>
/// 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`.
/// </summary>
public string RelativeDeclaredNamespace { get; }
private State(
Solution solution,
DocumentId originalDocumentId,
ImmutableArray<DocumentId> documentIds,
string rootNamespce,
string targetNamespace,
string declaredNamespace,
string relativeDeclaredNamespace)
{
Solution = solution;
OriginalDocumentId = originalDocumentId;
DocumentIds = documentIds;
DefaultNamespace = rootNamespce;
TargetNamespace = targetNamespace;
DeclaredNamespace = declaredNamespace;
RelativeDeclaredNamespace = relativeDeclaredNamespace;
}
/// <summary>
/// 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.
/// </summary>
private static bool IsSupportedLinkedDocument(Document document, out ImmutableArray<DocumentId> 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 <project root path>\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<Document> 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<string>(
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<Document> documents,
AbstractSyncNamespaceService<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> 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<TextSpan, TNamespaceDeclarationSyntax>.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<State> CreateAsync(
AbstractSyncNamespaceService<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> 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<ISyntaxFactsService>();
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);
}
/// <summary>
/// Create a qualified identifier as the suffix of namespace based on a list of folder names.
/// </summary>
private static string TryBuildNamespaceFromFolders(
AbstractSyncNamespaceService<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> service,
IEnumerable<string> 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;
}
}
}
}
}
// 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<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> :
ISyncNamespaceService
where TNamespaceDeclarationSyntax : SyntaxNode
where TCompilationUnitSyntax : SyntaxNode
where TMemberDeclarationSyntax : SyntaxNode
{
public async Task<ImmutableArray<CodeAction>> 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<string> newNamespaceParts, ISyntaxFactsService syntaxFacts, out SyntaxNode old, out SyntaxNode @new);
protected abstract string EscapeIdentifier(string identifier);
protected abstract TCompilationUnitSyntax ChangeNamespaceDeclaration(
TCompilationUnitSyntax root, ImmutableArray<string> declaredNamespaceParts, ImmutableArray<string> targetNamespaceParts);
protected abstract SyntaxList<TMemberDeclarationSyntax> GetMemberDeclarationsInContainer(SyntaxNode compilationUnitOrNamespaceDecl);
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// If the refactoring should be triggered, then returns the only namespace declaration node in the document (or type
/// <typeparamref name="TNamespaceDeclarationSyntax"/>) or the compilation unit node (of type <typeparamref name="TCompilationUnitSyntax"/>)
/// if no namespace declaration in the document. Otherwise, return null.
/// </returns>
protected abstract Task<SyntaxNode> 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<bool> ContainsPartialTypeWithMultipleDeclarationsAsync(
Document document, SyntaxNode compilationUnitOrNamespaceDecl, CancellationToken cancellationToken)
{
var memberDecls = GetMemberDeclarationsInContainer(compilationUnitOrNamespaceDecl);
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var semanticFacts = document.GetLanguageService<ISemanticFactsService>();
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;
}
/// <summary>
/// Try get the relative namespace for <paramref name="namespace"/> based on <paramref name="relativeTo"/>,
/// if <paramref name="relativeTo"/> is the containing namespace of <paramref name="namespace"/>.
/// For example:
/// - If <paramref name="relativeTo"/> is "A.B" and <paramref name="namespace"/> is "A.B.C.D", then
/// the relative namespace is "C.D".
/// - If <paramref name="relativeTo"/> is "A.B" and <paramref name="namespace"/> is also "A.B", then
/// the relative namespace is "".
/// - If <paramref name="relativeTo"/> is "" then the relative namespace us <paramref name="namespace"/>.
/// </summary>
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<CodeAction> CreateCodeActions(
AbstractSyncNamespaceService<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> service, State state)
{
var builder = ArrayBuilder<CodeAction>.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();
}
}
}
// 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<ImmutableArray<CodeAction>> GetRefactoringsAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken);
/// <summary>
/// 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 <paramref name="newNamespaceParts"/> is provided, the name
/// in the new node might be qualified with this new namespace instead.
/// </summary>
/// <param name="reference">A reference to a type declared inside the namespce to be changed, which is calculated
/// based on results from `SymbolFinder.FindReferencesAsync`.</param>
/// <param name="newNamespaceParts">If specified, the namespace of original reference will be replaced with given
/// namespace in the replacement node.</param>
/// <param name="old">The node to be replaced. This might be an ancestor of original </param>
/// <param name="new">The replacement node.</param>
bool TryGetReplacementReferenceSyntax(
SyntaxNode reference,
ImmutableArray<string> newNamespaceParts,
ISyntaxFactsService syntaxFacts,
out SyntaxNode old,
out SyntaxNode @new);
}
}
// 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<ISyncNamespaceService>();
var actions = await service.GetRefactoringsAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
context.RegisterRefactorings(actions);
}
}
}
......@@ -723,6 +723,15 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Change namespace to &apos;{0}&apos;.
/// </summary>
internal static string Change_namespace_to_0 {
get {
return ResourceManager.GetString("Change_namespace_to_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Change signature....
/// </summary>
......@@ -732,6 +741,15 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Change to global namespace.
/// </summary>
internal static string Change_to_global_namespace {
get {
return ResourceManager.GetString("Change_to_global_namespace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Changes to expression trees may result in behavior changes at runtime.
/// </summary>
......@@ -2454,6 +2472,24 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Move file to &apos;{0}&apos;.
/// </summary>
internal static string Move_file_to_0 {
get {
return ResourceManager.GetString("Move_file_to_0", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Move file to project root folder.
/// </summary>
internal static string Move_file_to_project_root_folder {
get {
return ResourceManager.GetString("Move_file_to_project_root_folder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Move type to {0}.
/// </summary>
......@@ -4105,6 +4141,16 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Warning: Changing namespace may produce invalid code and change code meaning..
/// </summary>
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);
}
}
/// <summary>
/// Looks up a localized string similar to Warning: Collection may be modified during iteration..
/// </summary>
......
......@@ -1442,6 +1442,21 @@ This version used in: {2}</value>
<data name="Modifying_source_file_will_prevent_the_debug_session_from_continuing_due_to_internal_error" xml:space="preserve">
<value>Modifying source file {0} will prevent the debug session from continuing due to internal error: {1}.</value>
</data>
<data name="Change_namespace_to_0" xml:space="preserve">
<value>Change namespace to '{0}'</value>
</data>
<data name="Move_file_to_0" xml:space="preserve">
<value>Move file to '{0}'</value>
</data>
<data name="Move_file_to_project_root_folder" xml:space="preserve">
<value>Move file to project root folder</value>
</data>
<data name="Change_to_global_namespace" xml:space="preserve">
<value>Change to global namespace</value>
</data>
<data name="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning" xml:space="preserve">
<value>Warning: Changing namespace may produce invalid code and change code meaning.</value>
</data>
<data name="Use_compound_assignment" xml:space="preserve">
<value>Use compound assignment</value>
</data>
......
......@@ -12,9 +12,9 @@
namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports
{
internal abstract class AbstractRemoveUnnecessaryImportsService<T> :
IRemoveUnnecessaryImportsService,
IUnnecessaryImportsService,
internal abstract class AbstractRemoveUnnecessaryImportsService<T> :
IRemoveUnnecessaryImportsService,
IUnnecessaryImportsService,
IEqualityComparer<T> where T : SyntaxNode
{
public Task<Document> RemoveUnnecessaryImportsAsync(Document document, CancellationToken cancellationToken)
......@@ -48,7 +48,7 @@ protected SyntaxToken StripNewLines(Document document, SyntaxToken token)
}
protected abstract ImmutableArray<T> GetUnnecessaryImports(
SemanticModel model, SyntaxNode root,
SemanticModel model, SyntaxNode root,
Func<SyntaxNode, bool> predicate, CancellationToken cancellationToken);
protected async Task<HashSet<T>> GetCommonUnnecessaryImportsOfAllContextAsync(
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Formátuje se dokument.</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Dokument wird formatiert</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Aplicando formato al documento</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Mise en forme du document</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Formattazione del documento</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">ドキュメントの書式設定</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">문서 서식 지정</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Formatowanie dokumentu</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -32,6 +32,16 @@
<target state="new">Adding a method with an explicit interface specifier will prevent the debug session from continuing.</target>
<note />
</trans-unit>
<trans-unit id="Change_namespace_to_0">
<source>Change namespace to '{0}'</source>
<target state="new">Change namespace to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Change_to_global_namespace">
<source>Change to global namespace</source>
<target state="new">Change to global namespace</target>
<note />
</trans-unit>
<trans-unit id="Code_Quality">
<source>Code Quality</source>
<target state="new">Code Quality</target>
......@@ -87,6 +97,16 @@
<target state="translated">Formatando documento</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_0">
<source>Move file to '{0}'</source>
<target state="new">Move file to '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Move_file_to_project_root_folder">
<source>Move file to project root folder</source>
<target state="new">Move file to project root folder</target>
<note />
</trans-unit>
<trans-unit id="Indexing_can_be_simplified">
<source>Indexing can be simplified</source>
<target state="new">Indexing can be simplified</target>
......@@ -182,6 +202,11 @@
<target state="new">Use interpolated verbatim string</target>
<note />
</trans-unit>
<trans-unit id="Warning_colon_changing_namespace_may_produce_invalid_code_and_change_code_meaning">
<source>Warning: Changing namespace may produce invalid code and change code meaning.</source>
<target state="new">Warning: Changing namespace may produce invalid code and change code meaning.</target>
<note />
</trans-unit>
<trans-unit id="Use_range_operator">
<source>Use range operator</source>
<target state="new">Use range operator</target>
......
......@@ -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";
......
......@@ -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;
......
......@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册