File: Language\Intermediate\ComponentTypeArgumentIntermediateNode.cs
Web Access
Project: src\src\roslyn\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.Text;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.AspNetCore.Razor.Language.Intermediate;

public sealed class ComponentTypeArgumentIntermediateNode(
    BoundAttributeDescriptor boundAttribute, CSharpIntermediateToken value) : IntermediateNode
{
    public BoundAttributeDescriptor BoundAttribute { get; } = boundAttribute;
    public TagHelperDescriptor TagHelper => BoundAttribute.Parent;

    public string TypeParameterName => BoundAttribute.Name;

    public CSharpIntermediateToken Value { get; } = value;

    public override IntermediateNodeCollection Children { get; } = [value];

    public ComponentTypeArgumentIntermediateNode(TagHelperPropertyIntermediateNode node)
        : this(node.BoundAttribute, GetValue(node))
    {
        Source = node.Source;
        AddDiagnosticsFromNode(node);
    }

    private static CSharpIntermediateToken GetValue(TagHelperPropertyIntermediateNode node)
        => node.Children switch
        {
            [CSharpIntermediateToken t] => t,
            [CSharpExpressionIntermediateNode { Children: [CSharpIntermediateToken t] }] => t,
            // Handle the case where the value was lowered as HTML content (from the unresolved tag helper pipeline).
            [HtmlContentIntermediateNode { Children: [HtmlIntermediateToken t] }] => t.IsLazy
                ? IntermediateNodeFactory.CSharpToken(
                    arg: t,
                    contentFactory: static token => token.Content,
                    source: t.Source)
                : new CSharpIntermediateToken(t.Content, t.Source),
            // Fallback: the value lowered to a more complex shape, e.g. literal text mixed
            // with one or more Razor expressions ("leading @x", "@x trailing", multiple
            // expressions, etc.). Concatenate the token content from all descendants into a
            // single CSharp token so downstream code generation can emit the user-authored
            // text as a type argument. The C# compiler will then produce a normal diagnostic
            // if the result is not a valid type, instead of the Razor compiler crashing.
            _ => MergeChildTokens(node),
        };

    private static CSharpIntermediateToken MergeChildTokens(TagHelperPropertyIntermediateNode node)
    {
        using var _sb = StringBuilderPool.GetPooledObject(out var sb);
        SourceSpan? firstSpan = null;
        SourceSpan? lastSpan = null;

        AppendTokens(node, sb, ref firstSpan, ref lastSpan);

        SourceSpan? mergedSpan = null;
        if (firstSpan is { } first && lastSpan is { } last && first.AbsoluteIndex <= last.AbsoluteIndex)
        {
            mergedSpan = DefaultTagHelperResolutionPhase.MergeSourceSpans(first, last);
        }
        else
        {
            mergedSpan = firstSpan ?? lastSpan ?? node.Source;
        }

        return new CSharpIntermediateToken(sb.ToString(), mergedSpan);
    }

    private static void AppendTokens(IntermediateNode node, StringBuilder sb, ref SourceSpan? firstSpan, ref SourceSpan? lastSpan)
    {
        foreach (var child in node.Children)
        {
            if (child is IntermediateToken token)
            {
                sb.Append(token.Content);
                if (token.Source is { } s)
                {
                    firstSpan ??= s;
                    lastSpan = s;
                }
            }
            else
            {
                // Unwrap container nodes (CSharpExpressionIntermediateNode, HtmlContentIntermediateNode, etc.).
                AppendTokens(child, sb, ref firstSpan, ref lastSpan);
            }
        }
    }

    public override void Accept(IntermediateNodeVisitor visitor)
        => visitor.VisitComponentTypeArgument(this);

    public override void FormatNode(IntermediateNodeFormatter formatter)
    {
        formatter.WriteContent(TypeParameterName);

        formatter.WriteProperty(nameof(BoundAttribute), BoundAttribute.DisplayName);
        formatter.WriteProperty(nameof(TagHelper), TagHelper.DisplayName);
    }
}