Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 'sync namespace' with file scoped namespaces #55022

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.LanguageServices;
using System.Collections.Immutable;

namespace Microsoft.CodeAnalysis.CSharp.Analyzers.MatchFolderAndNamespace
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class CSharpMatchFolderAndNamespaceDiagnosticAnalyzer : AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<NamespaceDeclarationSyntax>
internal class CSharpMatchFolderAndNamespaceDiagnosticAnalyzer
: AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<SyntaxKind, BaseNamespaceDeclarationSyntax>
{
protected override ISyntaxFacts GetSyntaxFacts() => CSharpSyntaxFacts.Instance;

protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, SyntaxKind.NamespaceDeclaration);
}
protected override ImmutableArray<SyntaxKind> GetSyntaxKindsToAnalyze()
=> ImmutableArray.Create(SyntaxKind.NamespaceDeclaration, SyntaxKind.FileScopedNamespaceDeclaration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,16 @@ private static Task RunTestAsync(IEnumerable<(string, string)> originalSources,
var testState = new VerifyCS.Test
{
EditorConfig = editorconfig ?? EditorConfig,
CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck
CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck,
LanguageVersion = LanguageVersion.CSharp10,
};

foreach (var (fileName, content) in originalSources)
{
testState.TestState.Sources.Add((fileName, content));
}

fixedSources ??= Array.Empty<(string, string)>();
foreach (var (fileName, content) in fixedSources)
{
testState.FixedState.Sources.Add((fileName, content));
}

return testState.RunAsync();
}
Expand All @@ -82,6 +79,26 @@ class Class1
directory: folder);
}

[Fact]
public Task InvalidFolderName1_NoDiagnostic_FileScopedNamespace()
{
// No change namespace action because the folder name is not valid identifier
var folder = CreateFolderPath(new[] { "3B", "C" });
var code =
@"
namespace A.B;

class Class1
{
}
";

return RunTestAsync(
"File1.cs",
code,
directory: folder);
}

[Fact]
public Task InvalidFolderName2_NoDiagnostic()
{
Expand Down Expand Up @@ -167,6 +184,32 @@ await RunTestAsync(
fixedCode: fixedCode);
}

[Fact]
public async Task SingleDocumentNoReference_FileScopedNamespace()
{
var folder = CreateFolderPath("B", "C");
var code =
@"namespace [|A.B|];

class Class1
{
}
";

var fixedCode =
@$"namespace {DefaultNamespace}.B.C;

class Class1
{{
}}
";
await RunTestAsync(
fileName: "Class1.cs",
fileContents: code,
directory: folder,
fixedCode: fixedCode);
}

[Fact]
public async Task SingleDocumentNoReference_NoDefaultNamespace()
{
Expand Down Expand Up @@ -199,6 +242,38 @@ await RunTestAsync(
editorConfig: editorConfig);
}

[Fact]
public async Task SingleDocumentNoReference_NoDefaultNamespace_FileScopedNamespace()
{
var editorConfig = @$"
is_global=true
build_property.ProjectDir = {Directory}
";

var folder = CreateFolderPath("B", "C");
var code =
@"namespace [|A.B|];

class Class1
{
}
";

var fixedCode =
@$"namespace B.C;

class Class1
{{
}}
";
await RunTestAsync(
fileName: "Class1.cs",
fileContents: code,
directory: folder,
fixedCode: fixedCode,
editorConfig: editorConfig);
}

[Fact]
public async Task NamespaceWithSpaces_NoDiagnostic()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

namespace Microsoft.CodeAnalysis.Analyzers.MatchFolderAndNamespace
{
internal abstract class AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<TNamespaceSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer
internal abstract class AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<TSyntaxKind, TNamespaceSyntax>
: AbstractBuiltInCodeStyleDiagnosticAnalyzer
where TSyntaxKind : struct
where TNamespaceSyntax : SyntaxNode
{
private static readonly LocalizableResourceString s_localizableTitle = new(
Expand All @@ -39,11 +41,15 @@ protected AbstractMatchFolderAndNamespaceDiagnosticAnalyzer()
}

protected abstract ISyntaxFacts GetSyntaxFacts();
protected abstract ImmutableArray<TSyntaxKind> GetSyntaxKindsToAnalyze();

protected sealed override void InitializeWorker(AnalysisContext context)
=> context.RegisterSyntaxNodeAction(AnalyzeNamespaceNode, GetSyntaxKindsToAnalyze());

public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

protected void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context)
private void AnalyzeNamespaceNode(SyntaxNodeAnalysisContext context)
{
// It's ok to not have a rootnamespace property, but if it's there we want to use it correctly
context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(MatchFolderAndNamespaceConstants.RootNamespaceOption, out var rootNamespace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,31 @@ class Class1 { }
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveTypeWithWithFileScopedNamespace()
{
var code =
@"namespace N1;

[||]class Class1 { }
class Class2 { }
";

var codeAfterMove =
@"namespace N1;
class Class2 { }
";

var expectedDocumentName = "Class1.cs";

var destinationDocumentText =
@"namespace N1;

class Class1 { }
";
await TestMoveTypeToNewFileAsync(code, codeAfterMove, expectedDocumentName, destinationDocumentText);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveType)]
public async Task MoveNestedTypeToNewFile_Simple()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,37 @@ class Class1
await TestChangeNamespaceAsync(code, expectedSourceOriginal);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task ChangeNamespace_SingleDocumentNoReference_FileScopedNamespace()
{
var defaultNamespace = "A";
var declaredNamespace = "Foo.Bar";

var (folder, filePath) = CreateDocumentFilePath(new[] { "B", "C" }, "File1.cs");
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" RootNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{folder}"" FilePath=""{filePath}"">
namespace [||]{declaredNamespace};

class Class1
{{
}}
</Document>
</Project>
</Workspace>";

var expectedSourceOriginal =
@"namespace A.B.C;

class Class1
{
}
";
await TestChangeNamespaceAsync(code, expectedSourceOriginal);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task ChangeNamespace_SingleDocumentLocalReference()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ class Class1
await TestMoveFileToMatchNamespace(code, expectedFolders);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_DeclarationNotContainedInDefaultNamespace_FileScopedNamespace()
{
// 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 (folder, filePath) = CreateDocumentFilePath(Array.Empty<string>(), "File1.cs");
var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" RootNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{folder}"" FilePath=""{filePath}"">
namespace [||]{declaredNamespace};

class Class1
{{
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_SingleAction1()
{
Expand Down Expand Up @@ -310,6 +336,41 @@ namespace Foo
class Class2
{{
}}
}}
</Document>
</Project>
</Workspace>";
await TestMoveFileToMatchNamespace(code, expectedFolders);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task MoveFile_FromOneFolderToAnother2_FileScopedNamespace()
{
var defaultNamespace = "A";
var declaredNamespace = "A.B.C.D.E";

var expectedFolders = new List<string[]>();
expectedFolders.Add(new[] { "B", "C", "D", "E" });

var (folder, filePath) = 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}"" RootNamespace=""{defaultNamespace}"" CommonReferences=""true"">
<Document Folders=""{folder}"" FilePath=""{filePath}"">
namespace [||]{declaredNamespace};

class Class1
{{
}}
</Document>
<Document Folders=""{documentPath2.folder}"" FilePath=""{documentPath2.filePath}"">
namespace Foo;

class Class2
{{
}}
</Document>
</Project>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ class [||]Class1

await TestMissingInRegularAndScriptAsync(code);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task NoAction_NotOnNamespaceDeclaration_FileScopedNamespace()
{
var folders = new[] { "A", "B" };
var (folder, filePath) = CreateDocumentFilePath(folders);

var code =
$@"
<Workspace>
<Project Language=""C#"" AssemblyName=""Assembly1"" FilePath=""{ProjectFilePath}"" CommonReferences=""true"">
<Document Folders=""{folder}"" FilePath=""{filePath}"">
namespace NS;

class [||]Class1
{{
}}
</Document>
</Project>
</Workspace>";

await TestMissingInRegularAndScriptAsync(code);
}

[WpfFact, Trait(Traits.Feature, Traits.Features.CodeActionsSyncNamespace)]
public async Task NoAction_NotOnFirstMemberInGlobal()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void Method()
{
}$$
}
", "Class.Method()", 2);
", "Namespace.Class.Method()", 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. The namepace name shoudl be appended. but it wasn't because the walker wasn't seeing the file-scoped-namespace and was only checking for normal namespaces.

}

[Fact, Trait(Traits.Feature, Traits.Features.DebuggingLocationName)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.MoveType
{
[ExportLanguageService(typeof(IMoveTypeService), LanguageNames.CSharp), Shared]
internal class CSharpMoveTypeService :
AbstractMoveTypeService<CSharpMoveTypeService, BaseTypeDeclarationSyntax, NamespaceDeclarationSyntax, MemberDeclarationSyntax, CompilationUnitSyntax>
AbstractMoveTypeService<CSharpMoveTypeService, BaseTypeDeclarationSyntax, BaseNamespaceDeclarationSyntax, MemberDeclarationSyntax, CompilationUnitSyntax>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
Expand Down
Loading