// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { public class NamespaceDirectiveTest { [Fact] public void TryComputeNamespace_IncompleteDirective_UsesEmptyNamespace() { // Arrange var source = "c:\\foo\\bar\\bleh.cshtml"; var imports = "c:\\foo\\baz\\bleh.cshtml"; var node = new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(imports, 0, 0, 0, 0), }; // Act var computed = NamespaceDirective.TryComputeNamespace(source, node, out var @namespace); // Assert Assert.False(computed); Assert.Equal(string.Empty, @namespace); } [Fact] public void TryComputeNamespace_EmptyDirective_UsesEmptyNamespace() { // Arrange var source = "c:\\foo\\bar\\bleh.cshtml"; var imports = "c:\\foo\\baz\\bleh.cshtml"; var node = new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(imports, 0, 0, 0, 0), }; node.Children.Add(new DirectiveTokenIRNode() { Content = string.Empty }); node.Children[0].Parent = node; // Act var computed = NamespaceDirective.TryComputeNamespace(source, node, out var @namespace); // Assert Assert.False(computed); Assert.Equal(string.Empty, @namespace); } // When we don't have a relationship between the source file and the imports file // we will just use the namespace on the node directly. [Theory] [InlineData((string)null, (string)null)] [InlineData("", "")] [InlineData(null, "/foo/bar")] [InlineData("/foo/baz", "/foo/bar/bleh")] [InlineData("/foo.cshtml", "/foo/bar.cshtml")] [InlineData("c:\\foo.cshtml", "d:\\foo\\bar.cshtml")] [InlineData("c:\\foo\\bar\\bleh.cshtml", "c:\\foo\\baz\\bleh.cshtml")] public void TryComputeNamespace_ForNonRelatedFiles_UsesNamespaceVerbatim(string source, string imports) { // Arrange var node = new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(imports, 0, 0, 0, 0), }; node.Children.Add(new DirectiveTokenIRNode() { Content = "Base" }); node.Children[0].Parent = node; // Act var computed = NamespaceDirective.TryComputeNamespace(source, node, out var @namespace); // Assert Assert.False(computed); Assert.Equal("Base", @namespace); } [Theory] [InlineData("/foo.cshtml", "/_ViewImports.cshtml", "Base")] [InlineData("/foo/bar.cshtml", "/_ViewImports.cshtml", "Base.foo")] [InlineData("/foo/bar/baz.cshtml", "/_ViewImports.cshtml", "Base.foo.bar")] [InlineData("/foo/bar/baz.cshtml", "/foo/_ViewImports.cshtml", "Base.bar")] [InlineData("/Foo/bar/baz.cshtml", "/foo/_ViewImports.cshtml", "Base.bar")] [InlineData("c:\\foo.cshtml", "c:\\_ViewImports.cshtml", "Base")] [InlineData("c:\\foo\\bar.cshtml", "c:\\_ViewImports.cshtml", "Base.foo")] [InlineData("c:\\foo\\bar\\baz.cshtml", "c:\\_ViewImports.cshtml", "Base.foo.bar")] [InlineData("c:\\foo\\bar\\baz.cshtml", "c:\\foo\\_ViewImports.cshtml", "Base.bar")] [InlineData("c:\\Foo\\bar\\baz.cshtml", "c:\\foo\\_ViewImports.cshtml", "Base.bar")] public void TryComputeNamespace_ForRelatedFiles_ComputesNamespaceWithSuffix(string source, string imports, string expected) { // Arrange var node = new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(imports, 0, 0, 0, 0), }; node.Children.Add(new DirectiveTokenIRNode() { Content = "Base" }); node.Children[0].Parent = node; // Act var computed = NamespaceDirective.TryComputeNamespace(source, node, out var @namespace); // Assert Assert.True(computed); Assert.Equal(expected, @namespace); } // This is the case where a _ViewImports sets the namespace. [Fact] public void Pass_SetsNamespaceAndClassName_ComputedFromImports() { // Arrange var document = new DocumentIRNode(); var builder = RazorIRBuilder.Create(document); builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "WebApplication.Account" }); builder.Pop(); var @namespace = new NamespaceDeclarationIRNode() { Content = "default" }; builder.Push(@namespace); var @class = new ClassDeclarationIRNode() { Name = "default" }; builder.Add(@class); document.DocumentKind = RazorPageDocumentClassifierPass.RazorPageDocumentKind; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage/AddUser.cshtml")); var pass = new NamespaceDirective.Pass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act pass.Execute(codeDocument, document); // Assert Assert.Equal("WebApplication.Account.Manage", @namespace.Content); Assert.Equal("AddUser_Page", @class.Name); } // This is the case where the source file sets the namespace. [Fact] public void Pass_SetsNamespaceAndClassName_ComputedFromSource() { // Arrange var document = new DocumentIRNode(); var builder = RazorIRBuilder.Create(document); // This will be ignored. builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "ignored" }); builder.Pop(); // This will be used. builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan("/Account/Manage/AddUser.cshtml", 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "WebApplication.Account.Manage" }); builder.Pop(); var @namespace = new NamespaceDeclarationIRNode() { Content = "default" }; builder.Push(@namespace); var @class = new ClassDeclarationIRNode() { Name = "default" }; builder.Add(@class); document.DocumentKind = RazorPageDocumentClassifierPass.RazorPageDocumentKind; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage/AddUser.cshtml")); var pass = new NamespaceDirective.Pass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act pass.Execute(codeDocument, document); // Assert Assert.Equal("WebApplication.Account.Manage", @namespace.Content); Assert.Equal("AddUser_Page", @class.Name); } // This is the case where the source file sets the namespace. [Fact] public void Pass_SetsNamespaceAndClassName_ComputedFromSource_ForView() { // Arrange var document = new DocumentIRNode(); var builder = RazorIRBuilder.Create(document); // This will be ignored. builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "ignored" }); builder.Pop(); // This will be used. builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan("/Account/Manage/AddUser.cshtml", 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "WebApplication.Account.Manage" }); builder.Pop(); var @namespace = new NamespaceDeclarationIRNode() { Content = "default" }; builder.Push(@namespace); var @class = new ClassDeclarationIRNode() { Name = "default" }; builder.Add(@class); document.DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage/AddUser.cshtml")); var pass = new NamespaceDirective.Pass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act pass.Execute(codeDocument, document); // Assert Assert.Equal("WebApplication.Account.Manage", @namespace.Content); Assert.Equal("AddUser_View", @class.Name); } // This handles an error case where we can't determine the relationship between the // imports and the source. [Fact] public void Pass_SetsNamespaceButNotClassName_VerbatimFromImports() { // Arrange var document = new DocumentIRNode(); var builder = RazorIRBuilder.Create(document); builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(null, 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "WebApplication.Account" }); builder.Pop(); var @namespace = new NamespaceDeclarationIRNode() { Content = "default" }; builder.Push(@namespace); var @class = new ClassDeclarationIRNode() { Name = "default" }; builder.Add(@class); document.DocumentKind = RazorPageDocumentClassifierPass.RazorPageDocumentKind; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage/AddUser.cshtml")); var pass = new NamespaceDirective.Pass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act pass.Execute(codeDocument, document); // Assert Assert.Equal("WebApplication.Account", @namespace.Content); Assert.Equal("default", @class.Name); } [Fact] public void Pass_DoesNothing_ForUnknownDocumentKind() { // Arrange var document = new DocumentIRNode(); var builder = RazorIRBuilder.Create(document); builder.Push(new DirectiveIRNode() { Descriptor = NamespaceDirective.Directive, Source = new SourceSpan(null, 0, 0, 0, 0), }); builder.Add(new DirectiveTokenIRNode() { Content = "WebApplication.Account" }); builder.Pop(); var @namespace = new NamespaceDeclarationIRNode() { Content = "default" }; builder.Push(@namespace); var @class = new ClassDeclarationIRNode() { Name = "default" }; builder.Add(@class); document.DocumentKind = null; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage/AddUser.cshtml")); var pass = new NamespaceDirective.Pass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act pass.Execute(codeDocument, document); // Assert Assert.Equal("default", @namespace.Content); Assert.Equal("default", @class.Name); } } }