|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X;
public static class NamespaceDirective
{
private static readonly char[] Separators = ['\\', '/'];
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(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new Pass());
}
// internal for testing
internal sealed class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(
RazorCodeDocument codeDocument,
DocumentIntermediateNode documentNode,
CancellationToken cancellationToken)
{
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.Name = 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 source 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].AsSpan()));
}
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)
{
FirstNamespace ??= node;
base.VisitNamespaceDeclaration(node);
}
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
{
FirstClass ??= node;
base.VisitClassDeclaration(node);
}
public override void VisitDirective(DirectiveIntermediateNode node)
{
if (node.Directive == Directive)
{
LastNamespaceDirective = node;
}
base.VisitDirective(node);
}
}
}
|