File: src\Analyzers\CSharp\Tests\MatchFolderAndNamespace\CSharpMatchFolderAndNamespaceTests.cs
Web Access
Project: src\src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Features.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Test.Utilities;
using Xunit;
using VerifyCS = Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions.CSharpCodeFixVerifier<
    Microsoft.CodeAnalysis.CSharp.Analyzers.MatchFolderAndNamespace.CSharpMatchFolderAndNamespaceDiagnosticAnalyzer,
    Microsoft.CodeAnalysis.CSharp.CodeFixes.MatchFolderAndNamespace.CSharpChangeNamespaceToMatchFolderCodeFixProvider>;
 
namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UnitTests.MatchFolderAndNamespace;
 
public class CSharpMatchFolderAndNamespaceTests
{
    private static readonly string Directory = "/0/";
 
    // DefaultNamespace gets exposed as RootNamespace in the build properties
    private const string DefaultNamespace = "Test.Root.Namespace";
 
    private static readonly string EditorConfig = @$"
is_global=true
build_property.ProjectDir = {Directory}
build_property.RootNamespace = {DefaultNamespace}
";
 
    private static string CreateFolderPath(params string[] folders)
        => Path.Combine(Directory, Path.Combine(folders));
 
    private static Task RunTestAsync(string fileName, string fileContents, string? directory = null, string? editorConfig = null, string? fixedCode = null, string? defaultNamespace = null)
    {
        var filePath = Path.Combine(directory ?? Directory, fileName);
        fixedCode ??= fileContents;
 
        return RunTestAsync(
            new[] { (filePath, fileContents) },
            new[] { (filePath, fixedCode) },
            editorConfig,
            defaultNamespace);
    }
 
    private static Task RunTestAsync(IEnumerable<(string, string)> originalSources, IEnumerable<(string, string)>? fixedSources = null, string? editorconfig = null, string? defaultNamespace = null)
    {
        // When a namespace isn't provided we will fallback on our default
        defaultNamespace ??= DefaultNamespace;
 
        var testState = new VerifyCS.Test
        {
            EditorConfig = editorconfig ?? EditorConfig,
            CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck,
            LanguageVersion = LanguageVersion.CSharp10,
        };
 
        foreach (var (fileName, content) in originalSources)
            testState.TestState.Sources.Add((fileName, content));
 
        fixedSources ??= [];
        foreach (var (fileName, content) in fixedSources)
            testState.FixedState.Sources.Add((fileName, content));
 
        // If empty string was provided as the namespace, then we will not set a default
        if (defaultNamespace.Length > 0)
        {
            testState.SolutionTransforms.Add((solution, projectId) =>
            {
                var project = solution.GetRequiredProject(projectId);
                return project.WithDefaultNamespace(defaultNamespace).Solution;
            });
        }
 
        return testState.RunAsync();
    }
 
    [Fact]
    public Task InvalidFolderName1_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["3B", "C"]);
        var code =
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """;
 
        return RunTestAsync(
            "File1.cs",
            code,
            directory: folder);
    }
 
    [Fact]
    public Task InvalidFolderName1_NoDiagnostic_FileScopedNamespace()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["3B", "C"]);
        var code =
            """
            namespace A.B;
 
            class Class1
            {
            }
            """;
 
        return RunTestAsync(
            "File1.cs",
            code,
            directory: folder);
    }
 
    [Fact]
    public Task InvalidFolderName2_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath(["B.3C", "D"]);
        var code =
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """;
 
        return RunTestAsync(
            "File1.cs",
            code,
            directory: folder);
    }
 
    [Fact]
    public Task InvalidFolderName3_NoDiagnostic()
    {
        // No change namespace action because the folder name is not valid identifier
        var folder = CreateFolderPath([".folder", "..subfolder", "name"]);
        var code =
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """;
 
        return RunTestAsync(
            "File1.cs",
            code,
            directory: folder);
    }
 
    [Fact]
    public Task CaseInsensitiveMatch_NoDiagnostic()
    {
        var folder = CreateFolderPath(["A", "B"]);
        var code =
@$"
namespace {DefaultNamespace}.a.b
{{
    class Class1
    {{
    }}
}}";
 
        return RunTestAsync(
            "File1.cs",
            code,
            directory: folder);
    }
 
    [Fact]
    public async Task CodeStyleOptionIsFalse()
    {
        var folder = CreateFolderPath("B", "C");
        var code =
            """
            namespace A.B
            {
                class Class1
                {
                }
            }
            """;
 
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: code,
            directory: folder,
            editorConfig: EditorConfig + """
            dotnet_style_namespace_match_folder = false
            """
);
    }
 
    [Fact]
    public async Task SingleDocumentNoReference()
    {
        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_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()
    {
        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,
            // passing empty string means that a default namespace isn't set on the test Project
            defaultNamespace: string.Empty);
    }
 
    [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,
            // passing empty string means that a default namespace isn't set on the test Project
            defaultNamespace: string.Empty);
    }
 
    [Fact]
    public async Task NamespaceWithSpaces_NoDiagnostic()
    {
        var folder = CreateFolderPath("A", "B");
        var code =
@$"namespace {DefaultNamespace}.A    .     B
{{
    class Class1
    {{
    }}
}}";
 
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: code,
            directory: folder);
    }
 
    [Fact]
    public async Task NestedNamespaces_NoDiagnostic()
    {
        // The code fix doesn't currently support nested namespaces for sync, so
        // diagnostic does not report.
 
        var folder = CreateFolderPath("B", "C");
        var code =
            """
            namespace A.B
            {
                namespace C.D
                {
                    class CDClass
                    {
                    }
                }
 
                class ABClass
                {
                }
            }
            """;
 
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: code,
            directory: folder);
    }
 
    [Fact]
    public async Task PartialTypeWithMultipleDeclarations_NoDiagnostic()
    {
        // The code fix doesn't currently support nested namespaces for sync, so
        // diagnostic does not report.
 
        var folder = CreateFolderPath("B", "C");
        var code1 =
            """
            namespace A.B
            {
                partial class ABClass
                {
                    void M1() {}
                }
            }
            """;
 
        var code2 =
            """
            namespace A.B
            {
                partial class ABClass
                {
                    void M2() {}
                }
            }
            """;
 
        var sources = new[]
        {
            (Path.Combine(folder, "ABClass1.cs"), code1),
            (Path.Combine(folder, "ABClass2.cs"), code2),
        };
 
        await RunTestAsync(sources);
    }
 
    [Fact]
    public async Task FileNotInProjectFolder_NoDiagnostic()
    {
        // Default directory is Test\Directory for the project,
        // putting the file outside the directory should have no
        // diagnostic shown.
 
        var folder = Path.Combine("B", "C");
        var code =
$@"namespace A.B
{{
    class ABClass
    {{
    }}
}}";
 
        await RunTestAsync(
            fileName: "Class1.cs",
            fileContents: code,
            directory: folder);
    }
 
    [Fact]
    public async Task SingleDocumentLocalReference()
    {
        var @namespace = "Bar.Baz";
 
        var folder = CreateFolderPath("A", "B", "C");
        var code =
$@"
namespace [|{@namespace}|]
{{
    delegate void D1();
 
    interface Class1
    {{
        void M1();
    }}
 
    class Class2 : {@namespace}.Class1
    {{
        {@namespace}.D1 d;
 
        void {@namespace}.Class1.M1(){{}}
    }}
}}";
 
        var expected =
@$"namespace {DefaultNamespace}.A.B.C
{{
    delegate void D1();
 
    interface Class1
    {{
        void M1();
    }}
 
    class Class2 : Class1
    {{
        D1 d;
 
        void Class1.M1() {{ }}
    }}
}}";
 
        await RunTestAsync(
            "Class1.cs",
            code,
            folder,
            fixedCode: expected);
    }
 
    [Fact]
    public async Task ChangeUsingsInMultipleContainers()
    {
        var declaredNamespace = "Bar.Baz";
 
        var folder = CreateFolderPath("A", "B", "C");
        var code1 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class1
    {{
    }}
}}";
 
        var code2 =
$@"namespace NS1
{{
    using {declaredNamespace};
 
    class Class2
    {{
        Class1 c2;
    }}
 
    namespace NS2
    {{
        using {declaredNamespace};
 
        class Class2
        {{
            Class1 c1;
        }}
    }}
}}";
 
        var fixed1 =
@$"namespace {DefaultNamespace}.A.B.C
{{
    class Class1
    {{
    }}
}}";
 
        var fixed2 =
@$"namespace NS1
{{
    using {DefaultNamespace}.A.B.C;
 
    class Class2
    {{
        Class1 c2;
    }}
 
    namespace NS2
    {{
        class Class2
        {{
            Class1 c1;
        }}
    }}
}}";
 
        var originalSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), code1),
            ("Class2.cs", code2)
        };
 
        var fixedSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), fixed1),
            ("Class2.cs", fixed2)
        };
 
        await RunTestAsync(originalSources, fixedSources);
    }
 
    [Fact]
    public async Task DocumentAtRoot_NoDiagnostic()
    {
        var folder = CreateFolderPath();
 
        var code = $@"
namespace {DefaultNamespace}
{{
    class C {{ }}
}}";
 
        await RunTestAsync(
            "File1.cs",
            code,
            folder);
    }
 
    [Fact]
    public async Task DocumentAtRoot_ChangeNamespace()
    {
        var folder = CreateFolderPath();
 
        var code =
$@"namespace [|{DefaultNamespace}.Test|]
{{
    class C {{ }}
}}";
 
        var fixedCode =
$@"namespace {DefaultNamespace}
{{
    class C {{ }}
}}";
 
        await RunTestAsync(
            "File1.cs",
            code,
            folder,
            fixedCode: fixedCode);
    }
 
    [Fact]
    public async Task ChangeNamespace_WithAliasReferencesInOtherDocument()
    {
        var declaredNamespace = $"Bar.Baz";
 
        var folder = CreateFolderPath("A", "B", "C");
        var code1 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class1
    {{
    }}
}}";
 
        var code2 = $@"
using System;
using {declaredNamespace};
using Class1Alias = {declaredNamespace}.Class1;
 
namespace Foo
{{
    class RefClass
    {{
        private Class1Alias c1;
    }}
}}";
 
        var fixed1 =
@$"namespace {DefaultNamespace}.A.B.C
{{
    class Class1
    {{
    }}
}}";
 
        var fixed2 =
@$"
using System;
using Class1Alias = {DefaultNamespace}.A.B.C.Class1;
 
namespace Foo
{{
    class RefClass
    {{
        private Class1Alias c1;
    }}
}}";
 
        var originalSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), code1),
            ("Class2.cs", code2)
        };
 
        var fixedSources = new[]
        {
            (Path.Combine(folder, "Class1.cs"), fixed1),
            ("Class2.cs", fixed2)
        };
 
        await RunTestAsync(originalSources, fixedSources);
    }
 
    [Fact]
    public async Task FixAll()
    {
        var declaredNamespace = "Bar.Baz";
 
        var folder1 = CreateFolderPath("A", "B", "C");
        var fixedNamespace1 = $"{DefaultNamespace}.A.B.C";
 
        var folder2 = CreateFolderPath("Second", "Folder", "Path");
        var fixedNamespace2 = $"{DefaultNamespace}.Second.Folder.Path";
 
        var folder3 = CreateFolderPath("Third", "Folder", "Path");
        var fixedNamespace3 = $"{DefaultNamespace}.Third.Folder.Path";
 
        var code1 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class1
    {{
        Class2 C2 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var fixed1 =
$@"using {fixedNamespace2};
using {fixedNamespace3};
 
namespace {fixedNamespace1}
{{
    class Class1
    {{
        Class2 C2 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var code2 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class2
    {{
        Class1 C1 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var fixed2 =
$@"using {fixedNamespace1};
using {fixedNamespace3};
 
namespace {fixedNamespace2}
{{
    class Class2
    {{
        Class1 C1 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var code3 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class3
    {{
        Class1 C1 {{ get; }}
        Class2 C2 {{ get; }}
    }}
}}";
 
        var fixed3 =
$@"using {fixedNamespace1};
using {fixedNamespace2};
 
namespace {fixedNamespace3}
{{
    class Class3
    {{
        Class1 C1 {{ get; }}
        Class2 C2 {{ get; }}
    }}
}}";
 
        var sources = new[]
        {
            (Path.Combine(folder1, "Class1.cs"), code1),
            (Path.Combine(folder2, "Class2.cs"), code2),
            (Path.Combine(folder3, "Class3.cs"), code3),
        };
 
        var fixedSources = new[]
        {
            (Path.Combine(folder1, "Class1.cs"), fixed1),
            (Path.Combine(folder2, "Class2.cs"), fixed2),
            (Path.Combine(folder3, "Class3.cs"), fixed3),
        };
 
        await RunTestAsync(sources, fixedSources);
    }
 
    [Fact]
    public async Task FixAll_MultipleProjects()
    {
        var declaredNamespace = "Bar.Baz";
 
        var folder1 = CreateFolderPath("A", "B", "C");
        var fixedNamespace1 = $"{DefaultNamespace}.A.B.C";
 
        var folder2 = CreateFolderPath("Second", "Folder", "Path");
        var fixedNamespace2 = $"{DefaultNamespace}.Second.Folder.Path";
 
        var folder3 = CreateFolderPath("Third", "Folder", "Path");
        var fixedNamespace3 = $"{DefaultNamespace}.Third.Folder.Path";
 
        var code1 =
$@"namespace [|{declaredNamespace}|]
{{
    public class Class1
    {{
        Class2 C2 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var fixed1 =
$@"using {fixedNamespace2};
using {fixedNamespace3};
 
namespace {fixedNamespace1}
{{
    public class Class1
    {{
        Class2 C2 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var code2 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class2
    {{
        Class1 C1 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var fixed2 =
$@"using {fixedNamespace1};
using {fixedNamespace3};
 
namespace {fixedNamespace2}
{{
    class Class2
    {{
        Class1 C1 {{ get; }}
        Class3 C3 {{ get; }}
    }}
}}";
 
        var code3 =
$@"namespace [|{declaredNamespace}|]
{{
    class Class3
    {{
        Class1 C1 {{ get; }}
        Class2 C2 {{ get; }}
    }}
}}";
 
        var fixed3 =
$@"using {fixedNamespace1};
using {fixedNamespace2};
 
namespace {fixedNamespace3}
{{
    class Class3
    {{
        Class1 C1 {{ get; }}
        Class2 C2 {{ get; }}
    }}
}}";
 
        var project2Directory = "/Project2/";
        var project2folder = Path.Combine(project2Directory, "A", "B", "C");
        var project2EditorConfig = @$"
is_global=true
build_property.ProjectDir = {project2Directory}
build_property.RootNamespace = {DefaultNamespace}
";
 
        var project2Source =
@$"using {declaredNamespace};
 
namespace [|Project2.Test|]
{{
    class P
    {{
        Class1 _c1;
    }}
}}";
 
        var project2FixedSource =
$@"namespace {fixedNamespace1}
{{
    class P
    {{
        Class1 _c1;
    }}
}}";
 
        var testState = new VerifyCS.Test
        {
            EditorConfig = EditorConfig,
            CodeFixTestBehaviors = CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInDocumentCheck | CodeAnalysis.Testing.CodeFixTestBehaviors.SkipFixAllInProjectCheck,
            LanguageVersion = LanguageVersion.CSharp10,
            TestState =
            {
                Sources =
                {
                    (Path.Combine(folder1, "Class1.cs"), code1),
                    (Path.Combine(folder2, "Class2.cs"), code2),
                    (Path.Combine(folder3, "Class3.cs"), code3),
                },
                AdditionalProjects =
                {
                    ["Project2"] =
                    {
                        AdditionalProjectReferences = { "TestProject" },
                        Sources = { (Path.Combine(project2folder, "P.cs"), project2Source) },
                        AnalyzerConfigFiles = { (Path.Combine(project2Directory, ".editorconfig"), project2EditorConfig) },
                    },
                },
            },
            FixedState =
            {
                Sources =
                {
                    (Path.Combine(folder1, "Class1.cs"), fixed1),
                    (Path.Combine(folder2, "Class2.cs"), fixed2),
                    (Path.Combine(folder3, "Class3.cs"), fixed3),
                },
                AdditionalProjects =
                {
                    ["Project2"] =
                    {
                        AdditionalProjectReferences = { "TestProject" },
                        Sources = { (Path.Combine(project2folder, "P.cs"), project2FixedSource) },
                        AnalyzerConfigFiles = { (Path.Combine(project2Directory, ".editorconfig"), project2EditorConfig) },
                    }
                }
            }
        };
 
        testState.SolutionTransforms.Add((solution, projectId) =>
        {
            foreach (var id in solution.ProjectIds)
            {
                var project = solution.GetRequiredProject(id);
                solution = project.WithDefaultNamespace(DefaultNamespace).Solution;
            }
            return solution;
        });
 
        await testState.RunAsync();
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/58372")]
    public async Task InvalidProjectName_ChangeNamespace()
    {
        var defaultNamespace = "Invalid-Namespace";
        var editorConfig = @$"
is_global=true
build_property.ProjectDir = {Directory}
build_property.RootNamespace = {defaultNamespace}
";
 
        var folder = CreateFolderPath(["B", "C"]);
        var code =
            """
            namespace [|A.B|]
            {
                class Class1
                {
                }
            }
            """;
 
        // The project name is invalid so the default namespace is not prepended
        var fixedCode =
@$"namespace B.C
{{
    class Class1
    {{
    }}
}}";
 
        await RunTestAsync(
            "Class1.cs",
            fileContents: code,
            fixedCode: fixedCode,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: defaultNamespace);
    }
 
    [Fact]
    public async Task InvalidProjectName_DocumentAtRoot_ChangeNamespace()
    {
        var defaultNamespace = "Invalid-Namespace";
        var editorConfig = @$"
is_global=true
build_property.ProjectDir = {Directory}
build_property.RootNamespace = {defaultNamespace}
";
 
        var folder = CreateFolderPath();
 
        var code =
$@"namespace Test.Code
{{
    class C {{ }}
}}";
 
        await RunTestAsync(
            "Class1.cs",
            fileContents: code,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: defaultNamespace);
    }
 
    [Fact]
    public async Task InvalidRootNamespace_DocumentAtRoot_ChangeNamespace()
    {
        var editorConfig = @$"
is_global=true
build_property.ProjectDir = {Directory}
build_property.RootNamespace = Test.Code # not an editorconfig comment even though it looks like one
";
 
        var folder = CreateFolderPath();
 
        var code =
$@"namespace Test.Code
{{
    class C {{ }}
}}";
 
        await RunTestAsync(
            "Class1.cs",
            fileContents: code,
            directory: folder,
            editorConfig: editorConfig,
            defaultNamespace: "Invalid-Namespace");
    }
}