File: RazorCodeDocumentExtensionsTest.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.AspNetCore.Razor.Language\test\Microsoft.AspNetCore.Razor.Language.UnitTests.csproj (Microsoft.AspNetCore.Razor.Language.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Xunit;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
public class RazorCodeDocumentExtensionsTest
{
    [Fact]
    public void GetAndSetImportSyntaxTrees_ReturnsSyntaxTrees()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.CreateEmpty();
 
        var importSyntaxTree = RazorSyntaxTree.Parse(codeDocument.Source);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        var actual = codeDocument.GetImportSyntaxTrees();
 
        // Assert
        Assert.False(actual.IsEmpty);
        Assert.Equal<RazorSyntaxTree>([importSyntaxTree], actual);
    }
 
    [Fact]
    public void GetAndSetTagHelpers_ReturnsTagHelpers()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.CreateEmpty();
 
        TagHelperCollection expected =
        [
            TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "TestAssembly").Build()
        ];
 
        codeDocument = codeDocument.WithTagHelpers(expected);
 
        // Act
        var actual = codeDocument.GetTagHelpers();
 
        // Assert
        Assert.Same(expected, actual);
    }
 
    [Fact]
    public void GetAndSetTagHelperContext_ReturnsTagHelperContext()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.CreateEmpty();
 
        var expected = TagHelperDocumentContext.GetOrCreate(tagHelpers: []);
        codeDocument = codeDocument.WithTagHelperContext(expected);
 
        // Act
        var actual = codeDocument.GetTagHelperContext();
 
        // Assert
        Assert.Same(expected, actual);
    }
 
    [Fact]
    public void GetAndSetDirectiveTagHelperContributions_ReturnsContributions()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.Create("@using A");
        var usingDirective = GetUsingDirectives(codeDocument).Single();
        var contribution = new DirectiveTagHelperContribution(usingDirective.SpanStart, TagHelperCollection.Empty);
 
        // Act
        codeDocument = codeDocument.WithDirectiveTagHelperContributions([contribution]);
        var actual = codeDocument.GetDirectiveTagHelperContributions();
 
        // Assert
        var stored = Assert.Single(actual);
        Assert.Equal(usingDirective.SpanStart, stored.DirectiveSpanStart);
    }
 
    [Fact]
    public void IsDirectiveUsed_NoReferencedTagHelpers_ReturnsFalse()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.Create("@using A\r\n@using B");
        var directives = GetUsingDirectives(codeDocument);
        codeDocument = codeDocument.WithDirectiveTagHelperContributions(
        [
            new(directives[0].SpanStart, TagHelperCollection.Empty),
            new(directives[1].SpanStart, TagHelperCollection.Empty),
        ]);
 
        // Act
        var isFirstUsed = codeDocument.IsDirectiveUsed(directives[0]);
        var isSecondUsed = codeDocument.IsDirectiveUsed(directives[1]);
 
        // Assert
        Assert.False(isFirstUsed);
        Assert.False(isSecondUsed);
    }
 
    [Fact]
    public void IsDirectiveUsed_MixOfUsedAndUnused_ReturnsExpectedValues()
    {
        // Arrange
        var codeDocument = TestRazorCodeDocument.Create("@using A\r\n@using B");
        var directives = GetUsingDirectives(codeDocument);
        var usedTagHelper = TagHelperDescriptorBuilder.CreateTagHelper("T", "A").Build();
 
        codeDocument = codeDocument
            .WithDirectiveTagHelperContributions(
            [
                new(directives[0].SpanStart, TagHelperCollection.Create([usedTagHelper])),
                new(directives[1].SpanStart, TagHelperCollection.Empty),
            ])
            .WithReferencedTagHelpers(TagHelperCollection.Create([usedTagHelper]));
 
        // Act
        var isFirstUsed = codeDocument.IsDirectiveUsed(directives[0]);
        var isSecondUsed = codeDocument.IsDirectiveUsed(directives[1]);
 
        // Assert
        Assert.True(isFirstUsed);
        Assert.False(isSecondUsed);
    }
 
    [Theory]
    [InlineData("_Imports.razor")]
    [InlineData("_ViewImports.cshtml")]
    public void IsDirectiveUsed_ImportDocument_ReturnsTrue(string filePath)
    {
        // Arrange
        var source = TestRazorSourceDocument.Create("@using A", filePath: filePath, relativePath: filePath);
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, FileKinds.GetFileKindFromPath(filePath)));
 
        var directive = GetUsingDirectives(codeDocument).Single();
        codeDocument = codeDocument.WithDirectiveTagHelperContributions([new(directive.SpanStart, TagHelperCollection.Empty)]);
 
        // Act
        var isDirectiveUsed = codeDocument.IsDirectiveUsed(directive);
 
        // Assert
        Assert.True(isDirectiveUsed);
    }
 
    [Fact]
    public void TryGetNamespace_RootNamespaceNotSet_ReturnsNull()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Test.cshtml", relativePath: "Test.cshtml");
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Null(@namespace);
    }
 
    [Fact]
    public void TryGetNamespace_RelativePathNull_ReturnsNull()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Test.cshtml", relativePath: null);
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Null(@namespace);
    }
 
    [Fact]
    public void TryGetNamespace_FilePathNull_ReturnsNull()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(filePath: null, relativePath: "Test.cshtml");
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Null(@namespace);
    }
 
    [Fact]
    public void TryGetNamespace_RelativePathLongerThanFilePath_ReturnsNull()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Test.cshtml",
            relativePath: "Some\\invalid\\relative\\path\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Null(@namespace);
    }
 
    [Fact]
    public void TryGetNamespace_ComputesNamespace()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("Hello.Components", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_NoRootNamespaceFallback_ReturnsNull()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: false, out var @namespace);
 
        // Assert
        Assert.Null(@namespace);
    }
 
    [Fact]
    public void TryGetNamespace_SanitizesNamespaceName()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components with space\\Test$name.cshtml",
            relativePath: "\\Components with space\\Test$name.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hel?o.World"));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("Hel_o.World.Components_with_space", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_RespectsNamespaceDirective()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.NS",
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Component, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }),
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello.World"));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("My.Custom.NS", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_RespectsImportsNamespaceDirective()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Component, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }),
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello.World"));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.NS",
            filePath: "C:\\Hello\\_Imports.razor",
            relativePath: "\\_Imports.razor");
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("My.Custom.NS.Components", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_IgnoresImportsNamespaceDirectiveWhenAsked()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Component, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }),
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello.World"));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.NS",
            filePath: "C:\\Hello\\_Imports.razor",
            relativePath: "\\_Imports.razor");
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, considerImports: false, out var @namespace, out _);
 
        // Assert
        Assert.Equal("Hello.World.Components", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_RespectsImportsNamespaceDirective_SameFolder()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Component, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }),
            codeGenerationOptions: RazorCodeGenerationOptions.Default.WithRootNamespace("Hello.World"));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.NS",
            filePath: "C:\\Hello\\Components\\_Imports.razor",
            relativePath: "\\Components\\_Imports.razor");
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("My.Custom.NS", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_OverrideImportsNamespaceDirective()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.OverrideNS",
            filePath: "C:\\Hello\\Components\\Test.cshtml",
            relativePath: "\\Components\\Test.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Component, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace My.Custom.NS",
            filePath: "C:\\Hello\\_Imports.razor",
            relativePath: "\\_Imports.razor");
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("My.Custom.OverrideNS", @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_PicksNearestImportsNamespaceDirective()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "C:\\RazorPagesWebPage\\Pages\\Namespace\\Nested\\Folder\\Index.cshtml",
            relativePath: "\\Pages\\Namespace\\Nested\\Folder\\Index.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Create(RazorLanguageVersion.Latest, RazorFileKind.Legacy, builder =>
            {
                builder.Directives = [NamespaceDirective.Directive];
            }));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource1 = TestRazorSourceDocument.Create(
            content: "@namespace RazorPagesWebSite.Pages",
            filePath: "C:\\RazorPagesWebPage\\Pages\\_ViewImports.cshtml",
            relativePath: "\\Pages\\_ViewImports.cshtml");
 
        var importSyntaxTree1 = RazorSyntaxTree.Parse(importSource1, codeDocument.ParserOptions);
 
        var importSource2 = TestRazorSourceDocument.Create(
            content: "@namespace CustomNamespace",
            filePath: "C:\\RazorPagesWebPage\\Pages\\Namespace\\_ViewImports.cshtml",
            relativePath: "\\Pages\\Namespace\\_ViewImports.cshtml");
 
        var importSyntaxTree2 = RazorSyntaxTree.Parse(importSource2, codeDocument.ParserOptions);
 
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree1, importSyntaxTree2]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("CustomNamespace.Nested.Folder", @namespace);
    }
 
    [Theory]
    [InlineData("/", "foo.cshtml", "Base")]
    [InlineData("/", "foo/bar.cshtml", "Base.foo")]
    [InlineData("/", "foo/bar/baz.cshtml", "Base.foo.bar")]
    [InlineData("/foo/", "bar/baz.cshtml", "Base.bar")]
    [InlineData("/Foo/", "bar/baz.cshtml", "Base.bar")]
    [InlineData("c:\\", "foo.cshtml", "Base")]
    [InlineData("c:\\", "foo\\bar.cshtml", "Base.foo")]
    [InlineData("c:\\", "foo\\bar\\baz.cshtml", "Base.foo.bar")]
    [InlineData("c:\\foo\\", "bar\\baz.cshtml", "Base.bar")]
    [InlineData("c:\\Foo\\", "bar\\baz.cshtml", "Base.bar")]
    public void TryGetNamespace_ComputesNamespaceWithSuffix(string basePath, string relativePath, string expectedNamespace)
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: Path.Combine(basePath, relativePath),
            relativePath: relativePath);
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Default.WithDirectives(NamespaceDirective.Directive));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importRelativePath = "_ViewImports.cshtml";
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace Base",
            filePath: Path.Combine(basePath, importRelativePath),
            relativePath: importRelativePath);
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal(expectedNamespace, @namespace);
    }
 
    [Fact]
    public void TryGetNamespace_ForNonRelatedFiles_UsesNamespaceVerbatim()
    {
        // Arrange
        var source = TestRazorSourceDocument.Create(
            filePath: "c:\\foo\\bar\\bleh.cshtml",
            relativePath: "bar\\bleh.cshtml");
 
        var codeDocument = RazorCodeDocument.Create(
            source,
            parserOptions: RazorParserOptions.Default.WithDirectives(NamespaceDirective.Directive));
 
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(source, codeDocument.ParserOptions));
 
        var importSource = TestRazorSourceDocument.Create(
            content: "@namespace Base",
            filePath: "c:\\foo\\baz\\bleh.cshtml",
            relativePath: "baz\\bleh.cshtml");
 
        var importSyntaxTree = RazorSyntaxTree.Parse(importSource, codeDocument.ParserOptions);
        codeDocument = codeDocument.WithImportSyntaxTrees([importSyntaxTree]);
 
        // Act
        codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace);
 
        // Assert
        Assert.Equal("Base", @namespace);
    }
 
    private static RazorUsingDirectiveSyntax[] GetUsingDirectives(RazorCodeDocument codeDocument)
    {
        var syntaxTree = RazorSyntaxTree.Parse(codeDocument.Source);
        return [.. syntaxTree.Root.DescendantNodes().OfType<RazorUsingDirectiveSyntax>()];
    }
}