File: Language\NamespaceComputer.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
internal static class NamespaceComputer
{
    private static ReadOnlySpan<char> PathSeparators => ['/', '\\'];
    private static ReadOnlySpan<char> NamespaceSeparators => ['.'];
 
    public static bool TryComputeNamespace(
        RazorCodeDocument codeDocument,
        bool fallbackToRootNamespace,
        bool considerImports,
        [NotNullWhen(true)] out string? @namespace,
        out SourceSpan? namespaceSpan)
    {
        var filePath = codeDocument.Source.FilePath;
        var relativePath = codeDocument.Source.RelativePath;
 
        if (filePath == null || relativePath == null || filePath.Length < relativePath.Length)
        {
            @namespace = null;
            namespaceSpan = null;
            return false;
        }
 
        // If the document or its imports contains a @namespace directive, we want to use that over the root namespace.
        if (TryGetNamespaceFromDirective(codeDocument, considerImports, out var directiveNamespaceName, out var directiveNamespaceSpan))
        {
            using var builder = new NamespaceBuilder();
 
            builder.AppendNamespace(directiveNamespaceName);
 
            var sourceFilePath = filePath.AsSpan();
            var directiveDirectorySpan = NormalizeDirectory(directiveNamespaceSpan.FilePath);
 
            // We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive.
            if (sourceFilePath.Length > directiveDirectorySpan.Length &&
                sourceFilePath.StartsWith(directiveDirectorySpan, StringComparison.OrdinalIgnoreCase))
            {
                // We know that the document containing the namespace directive is in the current document's hierarchy.
                // Compute the actual relative path and use that as the namespace suffix.
                var suffix = sourceFilePath[directiveDirectorySpan.Length..];
                builder.AppendRelativePath(suffix);
            }
 
            @namespace = builder.ToString();
            namespaceSpan = directiveNamespaceSpan;
            return true;
        }
 
        if (fallbackToRootNamespace)
        {
            var rootNamespace = codeDocument.CodeGenerationOptions.RootNamespace;
 
            if (!rootNamespace.IsNullOrEmpty() || codeDocument.FileKind.IsComponent())
            {
                using var builder = new NamespaceBuilder();
 
                builder.AppendNamespace(rootNamespace);
                builder.AppendRelativePath(relativePath.AsSpan());
 
                @namespace = builder.ToString();
                namespaceSpan = null;
                return true;
            }
        }
 
        // There was no valid @namespace directive.
        @namespace = null;
        namespaceSpan = null;
        return false;
    }
 
    // 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 ReadOnlySpan<char> NormalizeDirectory(string path)
    {
        var span = path.AsSpanOrDefault();
 
        if (span.IsEmpty)
        {
            return default;
        }
 
        var lastSeparator = span.LastIndexOfAny(PathSeparators);
        if (lastSeparator < 0)
        {
            return default;
        }
 
        // Includes the separator
        return span[..(lastSeparator + 1)];
    }
 
    private readonly ref struct NamespaceBuilder
    {
        private readonly PooledObject<StringBuilder> _pooledBuilder;
 
        public NamespaceBuilder()
        {
            _pooledBuilder = StringBuilderPool.GetPooledObject();
        }
 
        public void Dispose()
        {
            _pooledBuilder.Dispose();
        }
 
        public override string ToString()
            => _pooledBuilder.Object.ToString();
 
        public void AppendNamespace(string? namespaceName)
        {
            var builder = _pooledBuilder.Object;
            var tokenizer = new StringTokenizer(namespaceName, NamespaceSeparators);
 
            foreach (var token in tokenizer)
            {
                if (token.IsEmpty)
                {
                    continue;
                }
 
                if (builder.Length > 0)
                {
                    builder.Append('.');
                }
 
                CSharpIdentifier.AppendSanitized(builder, token);
            }
        }
 
        public void AppendRelativePath(ReadOnlySpan<char> relativePath)
        {
            var lastSeparatorIndex = relativePath.LastIndexOfAny(PathSeparators);
            if (lastSeparatorIndex < 0)
            {
                return;
            }
 
            relativePath = relativePath[..lastSeparatorIndex];
 
            var builder = _pooledBuilder.Object;
            var tokenizer = new StringTokenizer(relativePath, PathSeparators);
 
            foreach (var token in tokenizer)
            {
                if (token.IsEmpty)
                {
                    continue;
                }
 
                if (builder.Length > 0)
                {
                    builder.Append('.');
                }
 
                CSharpIdentifier.AppendSanitized(builder, token);
            }
        }
    }
 
    private static bool TryGetNamespaceFromDirective(
        RazorCodeDocument codeDocument,
        bool considerImports,
        [NotNullWhen(true)] out string? namespaceName,
        out SourceSpan namespaceSpan)
    {
        // If there are multiple @namespace directives in the hierarchy,
        // we want to pick the closest one to the current document.
        // So, we start with the current document before looking at imports.
 
        var visitor = new NamespaceDirectiveVisitor();
 
        if (codeDocument.TryGetSyntaxTree(out var syntaxTree) &&
            visitor.TryGetLastNamespaceDirective(syntaxTree, out namespaceName, out namespaceSpan))
        {
            return true;
        }
 
        if (considerImports &&
            codeDocument.TryGetImportSyntaxTrees(out var importSyntaxTrees))
        {
            // Be sure to walk the imports in reverse order since the last one is the closest to the document.
            for (var i = importSyntaxTrees.Length - 1; i >= 0; i--)
            {
                var importSyntaxTree = importSyntaxTrees[i];
                if (visitor.TryGetLastNamespaceDirective(importSyntaxTree, out namespaceName, out namespaceSpan))
                {
                    return true;
                }
            }
        }
 
        namespaceName = null;
        namespaceSpan = default;
        return false;
    }
 
    private sealed class NamespaceDirectiveVisitor : SyntaxWalker
    {
        private RazorSourceDocument? _source;
        private string? _lastNamespaceName;
        private SourceSpan _lastNamespaceSpan;
 
        public bool TryGetLastNamespaceDirective(
            RazorSyntaxTree syntaxTree,
            [NotNullWhen(true)] out string? namespaceName,
            out SourceSpan namespaceSpan)
        {
            _source = syntaxTree.Source;
            _lastNamespaceName = null;
            _lastNamespaceSpan = default;
 
            Visit(syntaxTree.Root);
 
            if (_lastNamespaceName.IsNullOrEmpty())
            {
                namespaceName = null;
                namespaceSpan = SourceSpan.Undefined;
                return false;
            }
 
            namespaceName = _lastNamespaceName;
            namespaceSpan = _lastNamespaceSpan;
            return true;
        }
 
        public override void VisitRazorDirective(RazorDirectiveSyntax node)
        {
            Debug.Assert(_source != null);
 
            if (node.IsDirective(NamespaceDirective.Directive) &&
                node.DirectiveBody.CSharpCode.Children is [_, CSharpSyntaxNode @namespace, ..])
            {
                _lastNamespaceName = @namespace.GetContent();
                _lastNamespaceSpan = @namespace.GetSourceSpan(_source);
            }
 
            base.VisitRazorDirective(node);
        }
    }
}