File: Language\Components\ComponentDocumentClassifierPass.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.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language.Components;
 
internal sealed class ComponentDocumentClassifierPass : DocumentClassifierPassBase
{
    public const string ComponentDocumentKind = "component.1.0";
 
    /// <summary>
    /// The fallback value of the root namespace. Only used if the fallback root namespace
    /// was not passed in.
    /// </summary>
    public string FallbackRootNamespace { get; set; } = "__GeneratedComponent";
 
    /// <summary>
    /// Gets or sets whether to mangle class names.
    ///
    /// Set to true in the IDE so we can generated mangled class names. This is needed
    /// to avoid conflicts between generated design-time code and the code in the editor.
    ///
    /// A better workaround for this would be to create a singlefilegenerator that overrides
    /// the codegen process when a document is open, but this is more involved, so hacking
    /// it for now.
    /// </summary>
    public bool MangleClassNames { get; set; }
 
    protected override string DocumentKind => ComponentDocumentKind;
 
    // Ensure this runs before the MVC classifiers which have Order = 0
    public override int Order => -100;
 
    protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
    {
        return codeDocument.FileKind.IsComponent();
    }
 
    protected override CodeTarget CreateTarget(RazorCodeDocument codeDocument)
        => new ComponentCodeTarget(codeDocument, TargetExtensions);
 
    /// <inheritdoc />
    protected override void OnDocumentStructureCreated(
        RazorCodeDocument codeDocument,
        NamespaceDeclarationIntermediateNode @namespace,
        ClassDeclarationIntermediateNode @class,
        MethodDeclarationIntermediateNode method)
    {
        if (!codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var computedNamespace, out var computedNamespaceSpan))
        {
            computedNamespace = FallbackRootNamespace;
        }
 
        if (!TryComputeClassName(codeDocument, out var computedClass))
        {
            var checksum = ChecksumUtilities.BytesToString(codeDocument.Source.Text.GetChecksum());
            computedClass = $"AspNetCore_{checksum}";
        }
 
        var documentNode = codeDocument.GetRequiredDocumentNode();
        if (char.IsLower(computedClass, 0))
        {
            // We don't allow component names to start with a lowercase character.
            documentNode.AddDiagnostic(
                ComponentDiagnosticFactory.Create_ComponentNamesCannotStartWithLowerCase(computedClass, documentNode.Source));
        }
 
        if (MangleClassNames)
        {
            computedClass = ComponentHelpers.MangleClassName(computedClass);
        }
 
        @class.NullableContext = true;
 
        @namespace.Name = computedNamespace;
        @namespace.Source = computedNamespaceSpan;
        @class.Name = computedClass;
        @class.Modifiers = CommonModifiers.PublicPartial;
 
        if (codeDocument.FileKind.IsComponentImport())
        {
            // We don't want component imports to be considered as real component.
            // But we still want to generate code for it so we can get diagnostics.
            @class.BaseType = new BaseTypeWithModel("object");
 
            method.ReturnType = "void";
            method.Name = "Execute";
            method.Modifiers = CommonModifiers.Protected;
            method.Parameters = [];
        }
        else
        {
            @class.BaseType = new BaseTypeWithModel("global::" + ComponentsApi.ComponentBase.FullTypeName);
 
            // Constrained type parameters are only supported in Razor language versions v6.0
            var razorLanguageVersion = codeDocument.ParserOptions.LanguageVersion;
            var directiveType = razorLanguageVersion >= RazorLanguageVersion.Version_6_0
                ? ComponentConstrainedTypeParamDirective.Directive
                : ComponentTypeParamDirective.Directive;
 
            using var typeParameters = new PooledArrayBuilder<TypeParameter>();
 
            foreach (var typeParamReference in documentNode.FindDirectiveReferences(directiveType))
            {
                var typeParamNode = typeParamReference.Node;
                if (typeParamNode.HasDiagnostics)
                {
                    continue;
                }
 
                // The first token is the type parameter's name, the rest are its constraints, if any.
                var name = typeParamNode.Tokens.First();
                var constraints = typeParamNode.Tokens.Skip(1).FirstOrDefault();
 
                typeParameters.Add(new(name.Content, name.Source, constraints?.Content, constraints?.Source));
            }
 
            @class.TypeParameters = typeParameters.ToImmutableAndClear();
 
            method.ReturnType = "void";
            method.Name = ComponentsApi.ComponentBase.BuildRenderTree;
            method.Modifiers = CommonModifiers.ProtectedOverride;
 
            method.Parameters = [new(
                name: ComponentsApi.RenderTreeBuilder.BuilderParameter,
                type: $"global::{ComponentsApi.RenderTreeBuilder.FullTypeName}")];
        }
    }
 
    private static bool TryComputeClassName(RazorCodeDocument codeDocument, [NotNullWhen(true)] 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).AsSpanOrDefault());
        return true;
    }
 
    private static string NormalizePath(string path)
    {
        path = path.Replace('\\', '/');
 
        return path;
    }
}