File: Mvc.Version2_X\NamespaceDirective.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// 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);
        }
    }
}