Added support for @namespace directive in Blazor (dotnet/aspnetcore-tooling#504)

* Added support for @namespace directive in Blazor
\n\nCommit migrated from 7f6c1422dd
This commit is contained in:
Ajay Bhargav Baaskaran 2019-04-30 17:58:13 -07:00 committed by GitHub
parent ed04f3ebb8
commit 5f462346c6
31 changed files with 1116 additions and 662 deletions

View File

@ -23,10 +23,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method);
@namespace.Content = "AspNetCore";
if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var namespaceName))
{
@namespace.Content = "AspNetCore";
}
else
{
@namespace.Content = namespaceName;
}
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
if (string.IsNullOrEmpty(filePath))
if (!TryComputeClassName(codeDocument, out var className))
{
// It's possible for a Razor document to not have a file path.
// Eg. When we try to generate code for an in memory document like default imports.
@ -35,8 +41,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
}
else
{
@class.ClassName = GetClassNameFromPath(filePath);
@class.ClassName = className;
}
@class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>";
@class.Modifiers.Clear();
@ -50,6 +57,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}";
}
private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className)
{
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
if (string.IsNullOrEmpty(filePath))
{
className = null;
return false;
}
className = GetClassNameFromPath(filePath);
return true;
}
private static string GetClassNameFromPath(string path)
{
const string cshtmlExtension = ".cshtml";

View File

@ -1,204 +0,0 @@
// 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 System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
public static class NamespaceDirective
{
private static readonly char[] Separators = new char[] { '\\', '/' };
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"namespace",
DirectiveKind.SingleLine,
builder =>
{
builder.AddNamespaceToken(
Resources.NamespaceDirective_NamespaceToken_Name,
Resources.NamespaceDirective_NamespaceToken_Description);
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
builder.Description = Resources.NamespaceDirective_Description;
});
public static void Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException();
}
builder.AddDirective(Directive);
builder.Features.Add(new Pass());
}
// internal for testing
internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind &&
documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind)
{
// Not a page. Skip.
return;
}
var visitor = new Visitor();
visitor.Visit(documentNode);
var directive = visitor.LastNamespaceDirective;
if (directive == null)
{
// No namespace set. Skip.
return;
}
var @namespace = visitor.FirstNamespace;
if (@namespace == null)
{
// No namespace node. Skip.
return;
}
@namespace.Content = GetNamespace(codeDocument.Source.FilePath, directive);
}
}
// internal for testing.
//
// This code does a best-effort attempt to compute a namespace 'suffix' - the path difference between
// where the @namespace directive appears and where the current document is on disk.
//
// In the event that these two source either don't have FileNames set or don't follow a coherent hierarchy,
// we will just use the namespace verbatim.
internal static string GetNamespace(string source, DirectiveIntermediateNode directive)
{
var directiveSource = NormalizeDirectory(directive.Source?.FilePath);
var baseNamespace = directive.Tokens.FirstOrDefault()?.Content;
if (string.IsNullOrEmpty(baseNamespace))
{
// The namespace directive was incomplete.
return string.Empty;
}
if (string.IsNullOrEmpty(source) || directiveSource == null)
{
// No sources, can't compute a suffix.
return baseNamespace;
}
// We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive.
if (!source.StartsWith(directiveSource, StringComparison.OrdinalIgnoreCase) ||
source.Length <= directiveSource.Length)
{
// The imports are not from the directory hierarchy, can't compute a suffix.
return baseNamespace;
}
// OK so that this point we know that the 'imports' file containing this directive is in the directory
// hierarchy of this soure file. This is the case where we can append a suffix to the baseNamespace.
//
// Everything so far has just been defensiveness on our part.
var builder = new StringBuilder(baseNamespace);
var segments = source.Substring(directiveSource.Length).Split(Separators);
// Skip the last segment because it's the FileName.
for (var i = 0; i < segments.Length - 1; i++)
{
builder.Append('.');
builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i]));
}
return builder.ToString();
}
// We want to normalize the path of the file containing the '@namespace' directive to just the containing
// directory with a trailing separator.
//
// Not using Path.GetDirectoryName here because it doesn't meet these requirements, and we want to handle
// both 'view engine' style paths and absolute paths.
//
// We also don't normalize the separators here. We expect that all documents are using a consistent style of path.
//
// If we can't normalize the path, we just return null so it will be ignored.
private static string NormalizeDirectory(string path)
{
if (string.IsNullOrEmpty(path))
{
return null;
}
var lastSeparator = path.LastIndexOfAny(Separators);
if (lastSeparator == -1)
{
return null;
}
// Includes the separator
return path.Substring(0, lastSeparator + 1);
}
private class Visitor : IntermediateNodeWalker
{
public ClassDeclarationIntermediateNode FirstClass { get; private set; }
public NamespaceDeclarationIntermediateNode FirstNamespace { get; private set; }
// We want the last one, so get them all and then .
public DirectiveIntermediateNode LastNamespaceDirective { get; private set; }
public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
{
if (FirstNamespace == null)
{
FirstNamespace = node;
}
base.VisitNamespaceDeclaration(node);
}
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
{
if (FirstClass == null)
{
FirstClass = node;
}
base.VisitClassDeclaration(node);
}
public override void VisitDirective(DirectiveIntermediateNode node)
{
if (node.Directive == Directive)
{
LastNamespaceDirective = node;
}
base.VisitDirective(node);
}
}
#region Obsolete
[Obsolete("This method is obsolete and will be removed in a future version.")]
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException();
}
builder.AddDirective(Directive);
builder.Features.Add(new Pass());
}
#endregion
}
}

View File

@ -206,48 +206,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
internal static string FormatMvcRazorParser_InvalidPropertyType(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorParser_InvalidPropertyType"), p0, p1, p2);
/// <summary>
/// Specify the base namespace for the page.
/// </summary>
internal static string NamespaceDirective_Description
{
get => GetString("NamespaceDirective_Description");
}
/// <summary>
/// Specify the base namespace for the page.
/// </summary>
internal static string FormatNamespaceDirective_Description()
=> GetString("NamespaceDirective_Description");
/// <summary>
/// The namespace for the page.
/// </summary>
internal static string NamespaceDirective_NamespaceToken_Description
{
get => GetString("NamespaceDirective_NamespaceToken_Description");
}
/// <summary>
/// The namespace for the page.
/// </summary>
internal static string FormatNamespaceDirective_NamespaceToken_Description()
=> GetString("NamespaceDirective_NamespaceToken_Description");
/// <summary>
/// Namespace
/// </summary>
internal static string NamespaceDirective_NamespaceToken_Name
{
get => GetString("NamespaceDirective_NamespaceToken_Name");
}
/// <summary>
/// Namespace
/// </summary>
internal static string FormatNamespaceDirective_NamespaceToken_Name()
=> GetString("NamespaceDirective_NamespaceToken_Name");
/// <summary>
/// The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.
/// </summary>

View File

@ -19,7 +19,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
InjectDirective.Register(builder);
ModelDirective.Register(builder);
NamespaceDirective.Register(builder);
PageDirective.Register(builder);
SectionDirective.Register(builder);

View File

@ -48,12 +48,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method);
@namespace.Content = "AspNetCore";
if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var namespaceName))
{
@namespace.Content = "AspNetCore";
}
else
{
@namespace.Content = namespaceName;
}
@class.BaseType = "global::Microsoft.AspNetCore.Mvc.RazorPages.Page";
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
if (string.IsNullOrEmpty(filePath))
if (!TryComputeClassName(codeDocument, out var className))
{
// It's possible for a Razor document to not have a file path.
// Eg. When we try to generate code for an in memory document like default imports.
@ -62,9 +66,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
}
else
{
@class.ClassName = GetClassNameFromPath(filePath);
@class.ClassName = className;
}
@class.BaseType = "global::Microsoft.AspNetCore.Mvc.RazorPages.Page";
@class.Modifiers.Clear();
@class.Modifiers.Add("public");
@ -143,6 +148,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
}
}
private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className)
{
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
if (string.IsNullOrEmpty(filePath))
{
className = null;
return false;
}
className = GetClassNameFromPath(filePath);
return true;
}
private static string GetClassNameFromPath(string path)
{
const string cshtmlExtension = ".cshtml";

View File

@ -159,15 +159,6 @@
<data name="MvcRazorParser_InvalidPropertyType" xml:space="preserve">
<value>Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'.</value>
</data>
<data name="NamespaceDirective_Description" xml:space="preserve">
<value>Specify the base namespace for the page.</value>
</data>
<data name="NamespaceDirective_NamespaceToken_Description" xml:space="preserve">
<value>The namespace for the page.</value>
</data>
<data name="NamespaceDirective_NamespaceToken_Name" xml:space="preserve">
<value>Namespace</value>
</data>
<data name="PageDirectiveCannotBeImported" xml:space="preserve">
<value>The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.</value>
</data>

View File

@ -1,352 +0,0 @@
// 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 Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
public class NamespaceDirectiveTest
{
[Fact]
public void GetNamespace_IncompleteDirective_UsesEmptyNamespace()
{
// Arrange
var source = "c:\\foo\\bar\\bleh.cshtml";
var imports = "c:\\foo\\baz\\bleh.cshtml";
var node = new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(imports, 0, 0, 0, 0),
};
// Act
var @namespace = NamespaceDirective.GetNamespace(source, node);
// Assert
Assert.Equal(string.Empty, @namespace);
}
[Fact]
public void GetNamespace_EmptyDirective_UsesEmptyNamespace()
{
// Arrange
var source = "c:\\foo\\bar\\bleh.cshtml";
var imports = "c:\\foo\\baz\\bleh.cshtml";
var node = new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(imports, 0, 0, 0, 0),
};
node.Children.Add(new DirectiveTokenIntermediateNode() { Content = string.Empty });
// Act
var @namespace = NamespaceDirective.GetNamespace(source, node);
// Assert
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 GetNamespace_ForNonRelatedFiles_UsesNamespaceVerbatim(string source, string imports)
{
// Arrange
var node = new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(imports, 0, 0, 0, 0),
};
node.Children.Add(new DirectiveTokenIntermediateNode() { Content = "Base" });
// Act
var @namespace = NamespaceDirective.GetNamespace(source, node);
// Assert
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 GetNamespace_ForRelatedFiles_ComputesNamespaceWithSuffix(string source, string imports, string expected)
{
// Arrange
var node = new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(imports, 0, 0, 0, 0),
};
node.Children.Add(new DirectiveTokenIntermediateNode() { Content = "Base" });
// Act
var @namespace = NamespaceDirective.GetNamespace(source, node);
// Assert
Assert.Equal(expected, @namespace);
}
// This is the case where a _ViewImports sets the namespace.
[Fact]
public void Pass_SetsNamespace_ComputedFromImports()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "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 = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("WebApplication.Account.Manage", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
// This is the case where the source file sets the namespace.
[Fact]
public void Pass_SetsNamespace_ComputedFromSource()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
// This will be ignored.
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "ignored" });
builder.Pop();
// This will be used.
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/Manage/AddUser.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account.Manage" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "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 = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("WebApplication.Account.Manage", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
// Handles cases where invalid characters appears in FileNames. Note that we don't sanitize the part of
// the namespace that you put in an import, just the file-based-suffix. Garbage in, garbage out.
[Fact]
public void Pass_SetsNamespace_SanitizesClassAndNamespace()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "default" };
builder.Add(@class);
document.DocumentKind = RazorPageDocumentClassifierPass.RazorPageDocumentKind;
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("ignored", "/Account/Manage-Info/Add+User.cshtml"));
var pass = new NamespaceDirective.Pass();
pass.Engine = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("WebApplication.Account.Manage_Info", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
// This is the case where the source file sets the namespace.
[Fact]
public void Pass_SetsNamespace_ComputedFromSource_ForView()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
// This will be ignored.
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/_ViewImports.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "ignored" });
builder.Pop();
// This will be used.
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan("/Account/Manage/AddUser.cshtml", 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account.Manage" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "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 = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("WebApplication.Account.Manage", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
// This handles an error case where we can't determine the relationship between the
// imports and the source.
[Fact]
public void Pass_SetsNamespace_VerbatimFromImports()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(null, 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "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 = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("WebApplication.Account", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
[Fact]
public void Pass_DoesNothing_ForUnknownDocumentKind()
{
// Arrange
var document = new DocumentIntermediateNode();
var builder = IntermediateNodeBuilder.Create(document);
builder.Push(new DirectiveIntermediateNode()
{
Directive = NamespaceDirective.Directive,
Source = new SourceSpan(null, 0, 0, 0, 0),
});
builder.Add(new DirectiveTokenIntermediateNode() { Content = "WebApplication.Account" });
builder.Pop();
var @namespace = new NamespaceDeclarationIntermediateNode() { Content = "default" };
builder.Push(@namespace);
var @class = new ClassDeclarationIntermediateNode() { ClassName = "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 = Mock.Of<RazorEngine>();
// Act
pass.Execute(codeDocument, document);
// Assert
Assert.Equal("default", @namespace.Content);
Assert.Equal("default", @class.ClassName);
}
}
}

View File

@ -54,7 +54,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
ClassDeclarationIntermediateNode @class,
MethodDeclarationIntermediateNode method)
{
if (!codeDocument.TryComputeNamespaceAndClass(out var computedNamespace, out var computedClass))
if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var computedNamespace) ||
!TryComputeClassName(codeDocument, out var computedClass))
{
// If we can't compute a nice namespace (no relative path) then just generate something
// mangled.
@ -117,5 +118,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
});
}
}
private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className)
{
className = null;
if (codeDocument.Source.FilePath == null || codeDocument.Source.RelativePath == null)
{
return false;
}
var relativePath = NormalizePath(codeDocument.Source.RelativePath);
className = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(relativePath));
return true;
}
private static string NormalizePath(string path)
{
path = path.Replace('\\', '/');
return path;
}
}
}

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Razor.Language
if (FileKinds.IsComponent(codeDocument.GetFileKind()) &&
(parserOptions == null || parserOptions.FeatureFlags.AllowComponentFileKind))
{
codeDocument.TryComputeNamespaceAndClass(out var currentNamespace, out var _);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var currentNamespace);
visitor = new ComponentDirectiveVisitor(codeDocument.Source.FilePath, descriptors, currentNamespace);
}
else

View File

@ -0,0 +1,33 @@
// 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 System;
namespace Microsoft.AspNetCore.Razor.Language.Extensions
{
public static class NamespaceDirective
{
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"namespace",
DirectiveKind.SingleLine,
builder =>
{
builder.AddNamespaceToken(
Resources.NamespaceDirective_NamespaceToken_Name,
Resources.NamespaceDirective_NamespaceToken_Description);
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
builder.Description = Resources.NamespaceDirective_Description;
});
public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive, FileKinds.Legacy, FileKinds.Component, FileKinds.ComponentImport);
return builder;
}
}
}

View File

@ -1898,6 +1898,48 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatRewriter_InsufficientStack()
=> GetString("Rewriter_InsufficientStack");
/// <summary>
/// Specify the base namespace for the document.
/// </summary>
internal static string NamespaceDirective_Description
{
get => GetString("NamespaceDirective_Description");
}
/// <summary>
/// Specify the base namespace for the document.
/// </summary>
internal static string FormatNamespaceDirective_Description()
=> GetString("NamespaceDirective_Description");
/// <summary>
/// The namespace for the document.
/// </summary>
internal static string NamespaceDirective_NamespaceToken_Description
{
get => GetString("NamespaceDirective_NamespaceToken_Description");
}
/// <summary>
/// The namespace for the document.
/// </summary>
internal static string FormatNamespaceDirective_NamespaceToken_Description()
=> GetString("NamespaceDirective_NamespaceToken_Description");
/// <summary>
/// Namespace
/// </summary>
internal static string NamespaceDirective_NamespaceToken_Name
{
get => GetString("NamespaceDirective_NamespaceToken_Name");
}
/// <summary>
/// Namespace
/// </summary>
internal static string FormatNamespaceDirective_NamespaceToken_Name()
=> GetString("NamespaceDirective_NamespaceToken_Name");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -3,9 +3,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Syntax;
namespace Microsoft.AspNetCore.Razor.Language
{
@ -195,11 +199,11 @@ namespace Microsoft.AspNetCore.Razor.Language
}
// In general documents will have a relative path (relative to the project root).
// We can only really compute a nice class/namespace when we know a relative path.
// We can only really compute a nice namespace when we know a relative path.
//
// However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't
// set up correctly.
internal static bool TryComputeNamespaceAndClass(this RazorCodeDocument document, out string @namespace, out string @class)
public static bool TryComputeNamespace(this RazorCodeDocument document, bool fallbackToRootNamespace, out string @namespace)
{
if (document == null)
{
@ -208,28 +212,78 @@ namespace Microsoft.AspNetCore.Razor.Language
var filePath = document.Source.FilePath;
var relativePath = document.Source.RelativePath;
if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length)
if (filePath == null || relativePath == null || filePath.Length < relativePath.Length)
{
@namespace = null;
@class = null;
return false;
}
filePath = NormalizePath(filePath);
relativePath = NormalizePath(relativePath);
var options = document.GetCodeGenerationOptions() ?? document.GetDocumentIntermediateNode()?.Options;
var rootNamespace = options?.RootNamespace;
if (string.IsNullOrEmpty(rootNamespace))
// If the document or it's imports contains a @namespace directive, we want to use that over the root namespace.
var baseNamespace = string.Empty;
var appendSuffix = true;
var lastNamespaceContent = string.Empty;
var lastNamespaceLocation = SourceSpan.Undefined;
var importSyntaxTrees = document.GetImportSyntaxTrees();
if (importSyntaxTrees != null)
{
// ImportSyntaxTrees is usually set. Just being defensive.
foreach (var importSyntaxTree in importSyntaxTrees)
{
if (importSyntaxTree != null && NamespaceVisitor.TryGetLastNamespaceDirective(importSyntaxTree, out var importNamespaceContent, out var importNamespaceLocation))
{
lastNamespaceContent = importNamespaceContent;
lastNamespaceLocation = importNamespaceLocation;
}
}
}
var syntaxTree = document.GetSyntaxTree();
if (syntaxTree != null && NamespaceVisitor.TryGetLastNamespaceDirective(syntaxTree, out var namespaceContent, out var namespaceLocation))
{
lastNamespaceContent = namespaceContent;
lastNamespaceLocation = namespaceLocation;
}
// If there are multiple @namespace directives in the heirarchy,
// we want to pick the closest one to the current document.
if (!string.IsNullOrEmpty(lastNamespaceContent))
{
baseNamespace = lastNamespaceContent;
var directiveLocationDirectory = NormalizeDirectory(lastNamespaceLocation.FilePath);
// We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive.
if (!document.Source.FilePath.StartsWith(directiveLocationDirectory, StringComparison.OrdinalIgnoreCase) ||
document.Source.FilePath.Length <= directiveLocationDirectory.Length)
{
// The most relevant directive is not from the directory hierarchy, can't compute a suffix.
appendSuffix = false;
}
else
{
// We know that the document containing the namespace directive is in the current document's heirarchy.
// Let's compute the actual relative path that we'll use to compute the namespace suffix.
relativePath = document.Source.FilePath.Substring(directiveLocationDirectory.Length);
}
}
else if (fallbackToRootNamespace)
{
var options = document.GetCodeGenerationOptions() ?? document.GetDocumentIntermediateNode()?.Options;
baseNamespace = options?.RootNamespace;
appendSuffix = true;
}
if (string.IsNullOrEmpty(baseNamespace))
{
// There was no valid @namespace directive and we couldn't compute the RootNamespace.
@namespace = null;
@class = null;
return false;
}
var builder = new StringBuilder();
// Sanitize the base namespace, but leave the dots.
var segments = rootNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries);
var segments = baseNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries);
builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[0]));
for (var i = 1; i < segments.Length; i++)
{
@ -237,19 +291,49 @@ namespace Microsoft.AspNetCore.Razor.Language
builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i]));
}
segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
// Skip the last segment because it's the FileName.
for (var i = 0; i < segments.Length - 1; i++)
if (appendSuffix)
{
builder.Append('.');
builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i]));
// If we get here, we already have a base namespace and the relative path that should be used as the namespace suffix.
segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
// Skip the last segment because it's the FileName.
for (var i = 0; i < segments.Length - 1; i++)
{
builder.Append('.');
builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i]));
}
}
@namespace = builder.ToString();
@class = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(relativePath));
return true;
// We want to normalize the path of the file containing the '@namespace' directive to just the containing
// directory with a trailing separator.
//
// Not using Path.GetDirectoryName here because it doesn't meet these requirements, and we want to handle
// both 'view engine' style paths and absolute paths.
//
// We also don't normalize the separators here. We expect that all documents are using a consistent style of path.
//
// If we can't normalize the path, we just return null so it will be ignored.
string NormalizeDirectory(string path)
{
char[] Separators = new char[] { '\\', '/' };
if (string.IsNullOrEmpty(path))
{
return null;
}
var lastSeparator = path.LastIndexOfAny(Separators);
if (lastSeparator == -1)
{
return null;
}
// Includes the separator
return path.Substring(0, lastSeparator + 1);
}
}
private static string NormalizePath(string path)
@ -288,5 +372,55 @@ namespace Microsoft.AspNetCore.Razor.Language
public IReadOnlyList<TagHelperDescriptor> TagHelpers { get; }
}
private class NamespaceVisitor : SyntaxWalker
{
private readonly RazorSourceDocument _source;
private NamespaceVisitor(RazorSourceDocument source)
{
_source = source;
}
public string LastNamespaceContent { get; set; }
public SourceSpan LastNamespaceLocation { get; set; }
public static bool TryGetLastNamespaceDirective(
RazorSyntaxTree syntaxTree,
out string namespaceDirectiveContent,
out SourceSpan namespaceDirectiveSpan)
{
var visitor = new NamespaceVisitor(syntaxTree.Source);
visitor.Visit(syntaxTree.Root);
if (string.IsNullOrEmpty(visitor.LastNamespaceContent))
{
namespaceDirectiveContent = null;
namespaceDirectiveSpan = SourceSpan.Undefined;
return false;
}
namespaceDirectiveContent = visitor.LastNamespaceContent;
namespaceDirectiveSpan = visitor.LastNamespaceLocation;
return true;
}
public override void VisitRazorDirective(RazorDirectiveSyntax node)
{
if (node != null && node.DirectiveDescriptor == NamespaceDirective.Directive)
{
var directiveContent = node.Body?.GetContent();
// In practice, this should never be null and always start with 'namespace'. Just being defensive here.
if (directiveContent != null && directiveContent.StartsWith(NamespaceDirective.Directive.Directive))
{
LastNamespaceContent = directiveContent.Substring(NamespaceDirective.Directive.Directive.Length).Trim();
LastNamespaceLocation = node.GetSourceSpan(_source);
}
}
base.VisitRazorDirective(node);
}
}
}
}

View File

@ -117,6 +117,7 @@ namespace Microsoft.AspNetCore.Razor.Language
FunctionsDirective.Register(builder);
ImplementsDirective.Register(builder);
InheritsDirective.Register(builder);
NamespaceDirective.Register(builder);
AddComponentFeatures(builder);
}

View File

@ -542,4 +542,13 @@ Instead, wrap the contents of the block in "{{}}":
<data name="Rewriter_InsufficientStack" xml:space="preserve">
<value>Not enough stack space to continue parsing this document. Razor doesn't support deeply nested elements.</value>
</data>
<data name="NamespaceDirective_Description" xml:space="preserve">
<value>Specify the base namespace for the document.</value>
</data>
<data name="NamespaceDirective_NamespaceToken_Description" xml:space="preserve">
<value>The namespace for the document.</value>
</data>
<data name="NamespaceDirective_NamespaceToken_Name" xml:space="preserve">
<value>Namespace</value>
</data>
</root>

View File

@ -1570,6 +1570,50 @@ namespace AnotherTest
CompileToAssembly(generated);
}
[Fact]
public void Component_WithNamespaceDirective()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using System;
using Microsoft.AspNetCore.Components;
namespace Test
{
public class HeaderComponent : ComponentBase
{
[Parameter]
string Header { get; set; }
}
}
namespace AnotherTest
{
public class FooterComponent : ComponentBase
{
[Parameter]
string Footer { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@using Test
@namespace AnotherTest
<HeaderComponent Header='head'>
</HeaderComponent>
<FooterComponent Footer='feet'>
</FooterComponent>
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
#endregion
#region EventCallback
@ -3582,6 +3626,75 @@ namespace Test
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated, throwOnFailure: false);
}
[Fact]
public void Component_NamespaceDirective_InImports()
{
// Arrange
var importContent = @"
@using System.Text
@using System.Reflection
@namespace New.Test
";
var importItem = CreateProjectItem("_Imports.razor", importContent, FileKinds.ComponentImport);
ImportItems.Add(importItem);
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Components;
namespace New.Test
{
public class Counter : ComponentBase
{
public int Count { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
<Counter />
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Component_NamespaceDirective_OverrideImports()
{
// Arrange
var importContent = @"
@using System.Text
@using System.Reflection
@namespace Import.Test
";
var importItem = CreateProjectItem("_Imports.razor", importContent, FileKinds.ComponentImport);
ImportItems.Add(importItem);
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Components;
namespace New.Test
{
public class Counter2 : ComponentBase
{
public int Count { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp("Pages/Counter.razor", @"
@namespace New.Test
<Counter2 />
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
#endregion
#region Misc

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Xunit;
@ -228,67 +230,63 @@ namespace Microsoft.AspNetCore.Razor.Language
}
[Fact]
public void TryComputeNamespaceAndClass_RootNamespaceNotSet_ReturnsNull()
public void TryComputeNamespace_RootNamespaceNotSet_ReturnsNull()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Test.cshtml", relativePath: "Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Null(@namespace);
Assert.Null(@class);
}
[Fact]
public void TryComputeNamespaceAndClass_RelativePathNull_ReturnsNull()
public void TryComputeNamespace_RelativePathNull_ReturnsNull()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Test.cshtml", relativePath: null);
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Null(@namespace);
Assert.Null(@class);
}
[Fact]
public void TryComputeNamespaceAndClass_FilePathNull_ReturnsNull()
public void TryComputeNamespace_FilePathNull_ReturnsNull()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: null, relativePath: "Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Null(@namespace);
Assert.Null(@class);
}
[Fact]
public void TryComputeNamespaceAndClass_RelativePathLongerThanFilePath_ReturnsNull()
public void TryComputeNamespace_RelativePathLongerThanFilePath_ReturnsNull()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Test.cshtml", relativePath: "Some\\invalid\\relative\\path\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Null(@namespace);
Assert.Null(@class);
}
[Fact]
public void TryComputeNamespaceAndClass_ComputesNamespaceAndClass()
public void TryComputeNamespace_ComputesNamespace()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Components\\Test.cshtml", relativePath: "\\Components\\Test.cshtml");
@ -299,15 +297,14 @@ namespace Microsoft.AspNetCore.Razor.Language
}));
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("Hello.Components", @namespace);
Assert.Equal("Test", @class);
}
[Fact]
public void TryComputeNamespaceAndClass_UsesIROptions_ComputesNamespaceAndClass()
public void TryComputeNamespace_UsesIROptions_ComputesNamespace()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Components\\Test.cshtml", relativePath: "\\Components\\Test.cshtml");
@ -322,15 +319,36 @@ namespace Microsoft.AspNetCore.Razor.Language
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("Hello.Components", @namespace);
Assert.Equal("Test", @class);
}
[Fact]
public void TryComputeNamespaceAndClass_PrefersOptionsFromCodeDocument_ComputesNamespaceAndClass()
public void TryComputeNamespace_NoRootNamespaceFallback_ReturnsNull()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Components\\Test.cshtml", relativePath: "\\Components\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
var documentNode = new DocumentIntermediateNode()
{
Options = RazorCodeGenerationOptions.Create(c =>
{
c.RootNamespace = "Hello";
})
};
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var @namespace);
// Assert
Assert.Null(@namespace);
}
[Fact]
public void TryComputeNamespace_PrefersOptionsFromCodeDocument_ComputesNamespace()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Components\\Test.cshtml", relativePath: "\\Components\\Test.cshtml");
@ -349,15 +367,14 @@ namespace Microsoft.AspNetCore.Razor.Language
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("World.Components", @namespace);
Assert.Equal("Test", @class);
}
[Fact]
public void TryComputeNamespaceAndClass_SanitizesNamespaceAndClassName()
public void TryComputeNamespace_SanitizesNamespaceName()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(filePath: "C:\\Hello\\Components with space\\Test$name.cshtml", relativePath: "\\Components with space\\Test$name.cshtml");
@ -372,11 +389,243 @@ namespace Microsoft.AspNetCore.Razor.Language
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespaceAndClass(out var @namespace, out var @class);
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("Hel_o.World.Components_with_space", @namespace);
Assert.Equal("Test_name", @class);
}
[Fact]
public void TryComputeNamespace_RespectsNamespaceDirective()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
content: "@namespace My.Custom.NS",
filePath: "C:\\Hello\\Components\\Test.cshtml",
relativePath: "\\Components\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetFileKind(FileKinds.Component);
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var documentNode = new DocumentIntermediateNode()
{
Options = RazorCodeGenerationOptions.Create(c =>
{
c.RootNamespace = "Hello.World";
})
};
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("My.Custom.NS", @namespace);
}
[Fact]
public void TryComputeNamespace_RespectsImportsNamespaceDirective()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
filePath: "C:\\Hello\\Components\\Test.cshtml",
relativePath: "\\Components\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetFileKind(FileKinds.Component);
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var importSourceDocument = TestRazorSourceDocument.Create(
content: "@namespace My.Custom.NS",
filePath: "C:\\Hello\\_Imports.razor",
relativePath: "\\_Imports.razor");
codeDocument.SetImportSyntaxTrees(new[]
{
RazorSyntaxTree.Parse(importSourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
}))
});
var documentNode = new DocumentIntermediateNode()
{
Options = RazorCodeGenerationOptions.Create(c =>
{
c.RootNamespace = "Hello.World";
})
};
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("My.Custom.NS.Components", @namespace);
}
[Fact]
public void TryComputeNamespace_RespectsImportsNamespaceDirective_SameFolder()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
filePath: "C:\\Hello\\Components\\Test.cshtml",
relativePath: "\\Components\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetFileKind(FileKinds.Component);
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var importSourceDocument = TestRazorSourceDocument.Create(
content: "@namespace My.Custom.NS",
filePath: "C:\\Hello\\Components\\_Imports.razor",
relativePath: "\\Components\\_Imports.razor");
codeDocument.SetImportSyntaxTrees(new[]
{
RazorSyntaxTree.Parse(importSourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
}))
});
var documentNode = new DocumentIntermediateNode()
{
Options = RazorCodeGenerationOptions.Create(c =>
{
c.RootNamespace = "Hello.World";
})
};
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("My.Custom.NS", @namespace);
}
[Fact]
public void TryComputeNamespace_OverrideImportsNamespaceDirective()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
content: "@namespace My.Custom.OverrideNS",
filePath: "C:\\Hello\\Components\\Test.cshtml",
relativePath: "\\Components\\Test.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetFileKind(FileKinds.Component);
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var importSourceDocument = TestRazorSourceDocument.Create(
content: "@namespace My.Custom.NS",
filePath: "C:\\Hello\\_Imports.razor",
relativePath: "\\_Imports.razor");
codeDocument.SetImportSyntaxTrees(new[]
{
RazorSyntaxTree.Parse(importSourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
}))
});
var documentNode = new DocumentIntermediateNode()
{
Options = RazorCodeGenerationOptions.Create(c =>
{
c.RootNamespace = "Hello.World";
})
};
codeDocument.SetDocumentIntermediateNode(documentNode);
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("My.Custom.OverrideNS", @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 TryComputeNamespace_ComputesNamespaceWithSuffix(string basePath, string relativePath, string expectedNamespace)
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
filePath: Path.Combine(basePath, relativePath),
relativePath: relativePath);
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var importRelativePath = "_ViewImports.cshtml";
var importSourceDocument = TestRazorSourceDocument.Create(
content: "@namespace Base",
filePath: Path.Combine(basePath, importRelativePath),
relativePath: importRelativePath);
codeDocument.SetImportSyntaxTrees(new[]
{
RazorSyntaxTree.Parse(importSourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
}))
});
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal(expectedNamespace, @namespace);
}
[Fact]
public void TryComputeNamespace_ForNonRelatedFiles_UsesNamespaceVerbatim()
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(
filePath: "c:\\foo\\bar\\bleh.cshtml",
relativePath: "bar\\bleh.cshtml");
var codeDocument = TestRazorCodeDocument.Create(sourceDocument, Array.Empty<RazorSourceDocument>());
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(sourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
})));
var importSourceDocument = TestRazorSourceDocument.Create(
content: "@namespace Base",
filePath: "c:\\foo\\baz\\bleh.cshtml",
relativePath: "baz\\bleh.cshtml");
codeDocument.SetImportSyntaxTrees(new[]
{
RazorSyntaxTree.Parse(importSourceDocument, RazorParserOptions.Create(options =>
{
options.Directives.Add(NamespaceDirective.Directive);
}))
});
// Act
codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace);
// Assert
Assert.Equal("Base", @namespace);
}
}
}

View File

@ -90,7 +90,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Test
feature.Directives,
directive => Assert.Same(FunctionsDirective.Directive, directive),
directive => Assert.Same(ImplementsDirective.Directive, directive),
directive => Assert.Same(InheritsDirective.Directive, directive));
directive => Assert.Same(InheritsDirective.Directive, directive),
directive => Assert.Same(NamespaceDirective.Directive, directive));
}
private static void AssertDefaultTargetExtensions(RazorProjectEngine engine)

View File

@ -0,0 +1,51 @@
// <auto-generated/>
#pragma warning disable 1591
namespace New.Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\_Imports.razor"
using System.Text;
#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "x:\dir\subdir\Test\_Imports.razor"
using System.Reflection;
#line default
#line hidden
#nullable disable
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
__o = typeof(Counter);
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,22 @@
Document -
NamespaceDeclaration - - New.Test
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [37] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [17] x:\dir\subdir\Test\_Imports.razor) - System.Text
UsingDirective - (21:1,1 [23] x:\dir\subdir\Test\_Imports.razor) - System.Reflection
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
DesignTimeDirective -
DirectiveToken - (57:2,11 [8] x:\dir\subdir\Test\_Imports.razor) - New.Test
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (0:0,0 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Counter
HtmlContent - (11:0,11 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (11:0,11 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n

View File

@ -0,0 +1,61 @@
// <auto-generated/>
#pragma warning disable 1591
namespace New.Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\_Imports.razor"
using System.Text;
#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "x:\dir\subdir\Test\_Imports.razor"
using System.Reflection;
#line default
#line hidden
#nullable disable
public class Counter : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((System.Action)(() => {
#nullable restore
#line 1 "x:\dir\subdir\Test\Pages/Counter.razor"
global::System.Object __typeHelper = nameof(New.Test);
#line default
#line hidden
#nullable disable
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
#nullable restore
#line 2 "x:\dir\subdir\Test\Pages/Counter.razor"
__o = typeof(Counter2);
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,22 @@
Document -
NamespaceDeclaration - - New.Test
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [37] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [17] x:\dir\subdir\Test\_Imports.razor) - System.Text
UsingDirective - (21:1,1 [23] x:\dir\subdir\Test\_Imports.razor) - System.Reflection
ClassDeclaration - - public - Counter - Microsoft.AspNetCore.Components.ComponentBase -
DesignTimeDirective -
DirectiveToken - (11:0,11 [8] Counter.razor) - New.Test
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (21:1,0 [12] Counter.razor) - Counter2
HtmlContent - (33:1,12 [2] Counter.razor)
IntermediateToken - (33:1,12 [2] Counter.razor) - Html - \n

View File

@ -0,0 +1,5 @@
Source Location: (11:0,11 [8] x:\dir\subdir\Test\Pages/Counter.razor)
|New.Test|
Generated Location: (850:31,44 [8] )
|New.Test|

View File

@ -0,0 +1,66 @@
// <auto-generated/>
#pragma warning disable 1591
namespace AnotherTest
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
using Test;
#line default
#line hidden
#nullable disable
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((System.Action)(() => {
#nullable restore
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
global::System.Object __typeHelper = nameof(AnotherTest);
#line default
#line hidden
#nullable disable
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
__o = "";
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
#nullable restore
#line 4 "x:\dir\subdir\Test\TestComponent.cshtml"
__o = typeof(HeaderComponent);
#line default
#line hidden
#nullable disable
__o = "";
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
#nullable restore
#line 6 "x:\dir\subdir\Test\TestComponent.cshtml"
__o = typeof(FooterComponent);
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,34 @@
Document -
NamespaceDeclaration - - AnotherTest
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [37] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [10] x:\dir\subdir\Test\TestComponent.cshtml) - Test
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
DesignTimeDirective -
DirectiveToken - (24:1,11 [11] x:\dir\subdir\Test\TestComponent.cshtml) - AnotherTest
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
HtmlContent - (11:0,11 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (11:0,11 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
HtmlContent - (37:2,0 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (37:2,0 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
Component - (39:3,0 [51] x:\dir\subdir\Test\TestComponent.cshtml) - HeaderComponent
ComponentAttribute - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Header - AttributeStructure.SingleQuotes
HtmlContent - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - head
HtmlContent - (90:4,18 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (90:4,18 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
Component - (92:5,0 [51] x:\dir\subdir\Test\TestComponent.cshtml) - FooterComponent
ComponentAttribute - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Footer - AttributeStructure.SingleQuotes
HtmlContent - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - feet
HtmlContent - (143:6,18 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (143:6,18 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n

View File

@ -0,0 +1,10 @@
Source Location: (1:0,1 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|using Test|
Generated Location: (327:12,0 [10] )
|using Test|
Source Location: (24:1,11 [11] x:\dir\subdir\Test\TestComponent.cshtml)
|AnotherTest|
Generated Location: (719:24,44 [11] )
|AnotherTest|

View File

@ -0,0 +1,36 @@
// <auto-generated/>
#pragma warning disable 1591
namespace New.Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\_Imports.razor"
using System.Text;
#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "x:\dir\subdir\Test\_Imports.razor"
using System.Reflection;
#line default
#line hidden
#nullable disable
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
builder.OpenComponent<New.Test.Counter>(0);
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,12 @@
Document -
NamespaceDeclaration - - New.Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [39] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [19] x:\dir\subdir\Test\_Imports.razor) - System.Text
UsingDirective - (21:1,1 [25] x:\dir\subdir\Test\_Imports.razor) - System.Reflection
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (0:0,0 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Counter

View File

@ -0,0 +1,36 @@
// <auto-generated/>
#pragma warning disable 1591
namespace New.Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\_Imports.razor"
using System.Text;
#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "x:\dir\subdir\Test\_Imports.razor"
using System.Reflection;
#line default
#line hidden
#nullable disable
public class Counter : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
builder.OpenComponent<New.Test.Counter2>(0);
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,12 @@
Document -
NamespaceDeclaration - - New.Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [39] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [19] x:\dir\subdir\Test\_Imports.razor) - System.Text
UsingDirective - (21:1,1 [25] x:\dir\subdir\Test\_Imports.razor) - System.Reflection
ClassDeclaration - - public - Counter - Microsoft.AspNetCore.Components.ComponentBase -
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (21:1,0 [12] Counter.razor) - Counter2

View File

@ -0,0 +1,34 @@
// <auto-generated/>
#pragma warning disable 1591
namespace AnotherTest
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
using Test;
#line default
#line hidden
#nullable disable
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder)
{
builder.OpenComponent<Test.HeaderComponent>(0);
builder.AddAttribute(1, "Header", "head");
builder.CloseComponent();
builder.AddMarkupContent(2, "\r\n");
builder.OpenComponent<AnotherTest.FooterComponent>(3);
builder.AddAttribute(4, "Footer", "feet");
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,20 @@
Document -
NamespaceDeclaration - - AnotherTest
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [39] ) - Microsoft.AspNetCore.Components
UsingDirective - (1:0,1 [12] x:\dir\subdir\Test\TestComponent.cshtml) - Test
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (39:3,0 [51] x:\dir\subdir\Test\TestComponent.cshtml) - HeaderComponent
ComponentAttribute - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Header - AttributeStructure.SingleQuotes
HtmlContent - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (64:3,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - head
HtmlContent - (90:4,18 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (90:4,18 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
Component - (92:5,0 [51] x:\dir\subdir\Test\TestComponent.cshtml) - FooterComponent
ComponentAttribute - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Footer - AttributeStructure.SingleQuotes
HtmlContent - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (117:5,25 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - feet