File: Language\Components\ComponentNodeWriter.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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.AspNetCore.Razor.Language.Components;

internal sealed class ComponentNodeWriter : IntermediateNodeWriter, ITemplateTargetExtension
{
    private readonly RazorLanguageVersion _version;
    private readonly ScopeStack ScopeStack = new();

    public ComponentNodeWriter(RazorLanguageVersion version)
    {
        _version = version;
    }

    public BuilderVariableName BuilderVariableName => ScopeStack.BuilderVariableName;
    public RenderModeVariableName RenderModeVariableName => ScopeStack.RenderModeVariableName;
    public FormNameVariableName FormNameVariableName => ScopeStack.FormNameVariableName;

    private bool CanUseAddComponentParameter(CodeRenderingContext context)
    {
        return !context.Options.SuppressAddComponentParameter && _version >= RazorLanguageVersion.Version_8_0;
    }

    private string GetAddComponentParameterMethodName(CodeRenderingContext context)
    {
        return CanUseAddComponentParameter(context)
            ? ComponentsApi.RenderTreeBuilder.AddComponentParameter
            : ComponentsApi.RenderTreeBuilder.AddAttribute;
    }





    public sealed override void BeginWriterScope(CodeRenderingContext context, string writer)
    {
        throw new NotImplementedException(nameof(BeginWriterScope));
    }

    public sealed override void EndWriterScope(CodeRenderingContext context)
    {
        throw new NotImplementedException(nameof(EndWriterScope));
    }

    public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node)
    {
        // We used to support syntaxes like <elem onsomeevent=@{ /* some C# code */ } /> but this is no longer the
        // case.
        //
        // We provide an error for this case just to be friendly.
        var content = string.Join("", node.Children.OfType<IntermediateToken>().Select(t => t.Content));
        context.AddDiagnostic(ComponentDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content));
        return;
    }

    private bool ShouldSuppressTypeInferenceCall(ComponentIntermediateNode node)
    {
        // When RZ10001 (type of component cannot be inferred) is reported, we want to suppress the equivalent CS0411 errors,
        // so we don't generate the call to TypeInference.CreateComponent.
        return node.Diagnostics.Any(d => d.Id == ComponentDiagnosticFactory.GenericComponentTypeInferenceUnderspecified.Id);
    }

    private void WriteComponentTypeInferenceMethod(CodeRenderingContext context, ComponentTypeInferenceMethodIntermediateNode node, bool returnComponentType, bool allowNameof, bool mapComponentStartTag)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        var parameters = GetTypeInferenceMethodParameters(node);

        // This is really similar to the code in WriteComponentAttribute and WriteComponentChildContent - except simpler because
        // attributes and child contents look like variables.
        //
        // Looks like:
        //
        //  public static void CreateFoo_0<T1, T2>(RenderTreeBuilder __builder, int seq, int __seq0, T1 __arg0, int __seq1, global::System.Collections.Generic.List<T2> __arg1, int __seq2, string __arg2)
        //  {
        //      builder.OpenComponent<Foo<T1, T2>>();
        //      builder.AddComponentParameter(__seq0, nameof(Foo<T1, T2>.Attr0), __arg0);
        //      builder.AddComponentParameter(__seq1, nameof(Foo<T1, T2>.Attr1), __arg1);
        //      builder.AddComponentParameter(__seq2, nameof(Foo<T1, T2>.Attr2), __arg2);
        //      builder.CloseComponent();
        //  }
        //
        // As a special case, we need to generate a thunk for captures in this block instead of using
        // them verbatim.
        //
        // The problem is that RenderTreeBuilder wants an Action<object>. The caller can't write the type
        // name if it contains generics, and we can't write the variable they want to assign to.
        var writer = context.CodeWriter;

        writer.Write("public static ");
        if (returnComponentType)
        {
            writer.Write(node.Component.TypeName);
        }
        else
        {
            writer.Write("void");
        }
        writer.Write(" ");
        writer.Write(node.MethodName);
        writer.Write("<");
        writer.Write(string.Join(", ", node.Component.Component.GetTypeParameters().Select(serializeTypeParameter)));
        writer.Write(">");

        writer.Write("(");
        writer.Write("global::");
        writer.Write(ComponentsApi.RenderTreeBuilder.FullTypeName);
        writer.Write(" ");
        writer.Write(ComponentsApi.RenderTreeBuilder.BuilderParameter);
        writer.Write(", ");
        writer.Write("int seq");

        if (parameters.Count > 0)
        {
            writer.Write(", ");
        }

        for (var i = 0; i < parameters.Count; i++)
        {
            if (parameters[i].SeqName is SeqName seqName)
            {
                writer.Write($"int {seqName}, ");
            }

            writer.Write(parameters[i].TypeName);
            writer.Write(" ");
            writer.Write(parameters[i].ParameterName);

            if (i < parameters.Count - 1)
            {
                writer.Write(", ");
            }
        }

        writer.Write(")");

        writeConstraints(writer, node);

        writer.WriteLine("{");

        // _builder.OpenComponent<TComponent>(42);
        context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.BuilderParameter);
        context.CodeWriter.Write(".");
        context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent);
        context.CodeWriter.Write("<");

        if (mapComponentStartTag)
        {
            var nonGenericTypeName = TypeNameHelper.GetNonGenericTypeName(node.Component.TypeName, out var genericTypeParameterList);
            WriteComponentTypeName(context, node.Component, nonGenericTypeName);
            context.CodeWriter.Write(genericTypeParameterList);
        }
        else
        {
            context.CodeWriter.Write(node.Component.TypeName);
        }

        context.CodeWriter.Write(">(");
        context.CodeWriter.Write("seq");
        context.CodeWriter.Write(");");
        context.CodeWriter.WriteLine();

        ParameterName? renderModeParameterName = null;

        foreach (var parameter in parameters)
        {
            switch (parameter.Source)
            {
                case ComponentAttributeIntermediateNode attribute:
                    context.CodeWriter.WriteStartInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, GetAddComponentParameterMethodName(context));
                    context.CodeWriter.Write(parameter.SeqName.AssumeNotNull());
                    context.CodeWriter.Write(", ");
                    WriteComponentAttributeName(context, attribute, allowNameof);
                    context.CodeWriter.Write(", ");

                    if (!CanUseAddComponentParameter(context))
                    {
                        context.CodeWriter.Write("(object)");
                    }

                    context.CodeWriter.Write(parameter.ParameterName);
                    context.CodeWriter.WriteEndMethodInvocation();
                    break;

                case SplatIntermediateNode:
                    context.CodeWriter.WriteStartInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, ComponentsApi.RenderTreeBuilder.AddMultipleAttributes);
                    context.CodeWriter.Write(parameter.SeqName.AssumeNotNull());
                    context.CodeWriter.Write(", ");

                    context.CodeWriter.Write(parameter.ParameterName);
                    context.CodeWriter.WriteEndMethodInvocation();
                    break;

                case ComponentChildContentIntermediateNode childContent:
                    context.CodeWriter.WriteStartInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, GetAddComponentParameterMethodName(context));
                    context.CodeWriter.Write(parameter.SeqName.AssumeNotNull());
                    context.CodeWriter.Write(", ");

                    context.CodeWriter.Write($"\"{childContent.AttributeName}\"");
                    context.CodeWriter.Write(", ");

                    if (!CanUseAddComponentParameter(context))
                    {
                        context.CodeWriter.Write("(object)");
                    }

                    context.CodeWriter.Write(parameter.ParameterName);
                    context.CodeWriter.WriteEndMethodInvocation();
                    break;

                case SetKeyIntermediateNode:
                    context.CodeWriter.WriteStartInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, ComponentsApi.RenderTreeBuilder.SetKey);
                    context.CodeWriter.Write(parameter.ParameterName);
                    context.CodeWriter.WriteEndMethodInvocation();
                    break;

                case ReferenceCaptureIntermediateNode capture:
                    var methodName = capture.IsComponentCapture
                        ? ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture
                        : ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture;

                    context.CodeWriter.WriteStartInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, methodName);
                    context.CodeWriter.Write(parameter.SeqName.AssumeNotNull());
                    context.CodeWriter.Write(", ");

                    var cast = capture.IsComponentCapture ? $"({capture.FieldTypeName})" : string.Empty;
                    context.CodeWriter.Write($"(__value) => {{ {parameter.ParameterName}({cast}__value); }}");
                    context.CodeWriter.WriteEndMethodInvocation();
                    break;

                case CascadingGenericTypeParameter:
                    // We only use the synthetic cascading parameters for type inference
                    break;

                case RenderModeIntermediateNode:
                    renderModeParameterName = parameter.ParameterName;
                    break;

                default:
                    throw new InvalidOperationException($"Not implemented: type inference method parameter from source {parameter.Source}");
            }
        }

        if (renderModeParameterName is ParameterName parameterName)
        {
            WriteAddComponentRenderMode(context, BuilderVariableName.Default, parameterName);
        }

        context.CodeWriter.WriteInstanceMethodInvocation(ComponentsApi.RenderTreeBuilder.BuilderParameter, ComponentsApi.RenderTreeBuilder.CloseComponent);

        if (returnComponentType)
        {
            writer.WriteLine("return default;");
        }

        writer.WriteLine("}");

        if (node.Component.Component.SuppliesCascadingGenericParameters())
        {
            // If this component cascades any generic parameters, we'll need to be able to capture its type inference
            // args at the call site. The point of this is to ensure that:
            //
            // [1] We only evaluate each expression once
            // [2] We evaluate them in the correct order matching the developer's source
            // [3] We can even make variables for lambdas or other expressions that can't just be assigned to implicitly-typed vars.
            //
            // We do that by emitting a method like the following. It has exactly the same generic type inference
            // characteristics as the regular CreateFoo_0 method emitted earlier
            //
            //  public static void CreateFoo_0_CaptureParameters<T1, T2>(T1 __arg0, out T1 __arg0_out, global::System.Collections.Generic.List<T2> __arg1, out global::System.Collections.Generic.List<T2> __arg1_out, int __seq2, string __arg2, out string __arg2_out)
            //  {
            //      __arg0_out = __arg0;
            //      __arg1_out = __arg1;
            //      __arg2_out = __arg2;
            //  }
            //
            writer.WriteLine();
            writer.Write("public static void ");
            writer.Write(node.MethodName);
            writer.Write("_CaptureParameters<");
            writer.Write(string.Join(", ", node.Component.Component.GetTypeParameters().Select(a => a.Name)));
            writer.Write(">");

            writer.Write("(");
            var isFirst = true;
            foreach (var parameter in parameters.Where(p => p.UsedForTypeInference))
            {
                if (isFirst)
                {
                    isFirst = false;
                }
                else
                {
                    writer.Write(", ");
                }

                writer.Write(parameter.TypeName);
                writer.Write(" ");
                writer.Write(parameter.ParameterName);
                writer.Write(", out ");
                writer.Write(parameter.TypeName);
                writer.Write(" ");
                writer.Write(parameter.ParameterName);
                writer.Write("_out");
            }

            writer.Write(")");

            writeConstraints(writer, node);

            writer.WriteLine("{");
            foreach (var parameter in parameters.Where(p => p.UsedForTypeInference))
            {
                writer.Write("    ");
                writer.Write(parameter.ParameterName);
                writer.Write("_out = ");
                writer.Write(parameter.ParameterName);
                writer.WriteLine(";");
            }
            writer.WriteLine("}");
        }

        static void writeConstraints(CodeWriter writer, ComponentTypeInferenceMethodIntermediateNode node)
        {
            // Writes out a list of generic type constraints with indentation
            // public void Foo<T, U>(T t, U u)
            //      where T: new()
            //      where U: Foo, notnull
            foreach (var constraint in node.GenericTypeConstraints)
            {
                writer.WriteLine();
                writer.Indent(writer.CurrentIndent + writer.TabSize);
                writer.Write(constraint);
            }

            writer.WriteLine();
        }

        static string serializeTypeParameter(BoundAttributeDescriptor attribute)
        {
            if (attribute.Metadata is TypeParameterMetadata { NameWithAttributes: string withAttributes })
            {
                return withAttributes;
            }

            return attribute.Name;
        }
    }

    private static void WriteComponentAttributeName(CodeRenderingContext context, ComponentAttributeIntermediateNode attribute, bool allowNameof = true)
    {
        if (allowNameof && attribute.BoundAttribute?.ContainingType is string containingType)
        {
            containingType = attribute.ConcreteContainingType ?? containingType;

            // nameof(containingType.PropertyName)
            // This allows things like Find All References to work in the IDE as we have an actual reference to the parameter
            context.CodeWriter.Write("nameof(");
            TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, containingType);
            context.CodeWriter.Write(".");

            if (!attribute.IsSynthesized)
            {
                var attributeSourceSpan = (SourceSpan)(attribute.PropertySpan ?? attribute.OriginalAttributeSpan);
                var requiresEscaping = attribute.PropertyName.IdentifierRequiresEscaping();
                using (context.BuildEnhancedLinePragma(attributeSourceSpan, characterOffset: requiresEscaping ? 1 : 0))
                {
                    context.CodeWriter.WriteIdentifierEscapeIfNeeded(attribute.PropertyName);
                    context.CodeWriter.WriteLine(attribute.PropertyName);
                }
            }
            else
            {
                context.CodeWriter.Write(attribute.PropertyName);
            }
            context.CodeWriter.Write(")");
        }
        else
        {
            context.CodeWriter.WriteStringLiteral(attribute.AttributeName);
        }
    }

    private List<TypeInferenceMethodParameter> GetTypeInferenceMethodParameters(ComponentTypeInferenceMethodIntermediateNode node)
    {
        var p = new List<TypeInferenceMethodParameter>();

        // Preserve order between attributes and splats
        foreach (var child in node.Component.Children)
        {
            if (child is ComponentAttributeIntermediateNode attribute)
            {
                // Some nodes just exist to help with property access at design time, and don't need anything else written
                if (attribute.IsDesignTimePropertyAccessHelper)
                {
                    continue;
                }

                string typeName;
                if (attribute.GloballyQualifiedTypeName != null)
                {
                    typeName = attribute.GloballyQualifiedTypeName;
                }
                else
                {
                    typeName = attribute.TypeName;
                    if (attribute.BoundAttribute != null && !attribute.BoundAttribute.IsGenericTypedProperty())
                    {
                        typeName = typeName.StartsWith("global::", StringComparison.Ordinal) ? typeName : $"global::{typeName}";
                    }
                }

                p.Add(new TypeInferenceMethodParameter(new(p.Count), typeName, new(p.Count), usedForTypeInference: true, attribute));
            }
            else if (child is SplatIntermediateNode splat)
            {
                var typeName = ComponentsApi.AddMultipleAttributesTypeFullName;
                p.Add(new TypeInferenceMethodParameter(new(p.Count), typeName, new(p.Count), usedForTypeInference: false, splat));
            }
            else if (child is RenderModeIntermediateNode renderMode)
            {
                var typeName = ComponentsApi.IComponentRenderMode.FullTypeName;
                p.Add(new TypeInferenceMethodParameter(new(p.Count), typeName, new(p.Count), usedForTypeInference: false, renderMode));
            }
        }

        foreach (var childContent in node.Component.ChildContents)
        {
            var typeName = childContent.TypeName;
            if (childContent.BoundAttribute != null && !childContent.BoundAttribute.IsGenericTypedProperty())
            {
                typeName = childContent.BoundAttribute.GetGloballyQualifiedTypeName();
            }
            p.Add(new TypeInferenceMethodParameter(new(p.Count), typeName, new(p.Count), usedForTypeInference: false, childContent));
        }

        foreach (var capture in node.Component.SetKeys)
        {
            p.Add(new TypeInferenceMethodParameter(new(p.Count), "object", new(p.Count), usedForTypeInference: false, capture));
        }

        foreach (var capture in node.Component.Captures)
        {
            // The capture type name should already contain the global:: prefix.
            p.Add(new TypeInferenceMethodParameter(new(p.Count), capture.TypeName, new(p.Count), usedForTypeInference: false, capture));
        }

        // Insert synthetic args for cascaded type inference at the start of the list
        // We do this last so that the indices above aren't affected
        if (node.ReceivesCascadingGenericTypes != null)
        {
            var i = 0;
            foreach (var cascadingGenericType in node.ReceivesCascadingGenericTypes)
            {
                p.Insert(i, new TypeInferenceMethodParameter(null, cascadingGenericType.ValueType, new(i, isSynthetic: true), usedForTypeInference: true, cascadingGenericType));
                i++;
            }
        }

        return p;
    }

    private static void UseCapturedCascadingGenericParameterVariable(ComponentIntermediateNode node, TypeInferenceMethodParameter parameter, TypeInferenceArgName variableName)
    {
        // If this captured variable corresponds to a generic type we want to cascade to
        // descendants, supply that info to descendants
        if (node.ProvidesCascadingGenericTypes != null)
        {
            foreach (var cascadeGeneric in node.ProvidesCascadingGenericTypes.Values)
            {
                if (cascadeGeneric.ValueSourceNode == parameter.Source)
                {
                    cascadeGeneric.ValueExpression = variableName;
                }
            }
        }

        // Since we've now evaluated and captured this expression, use the variable
        // instead of the expression from now on
        parameter.ReplaceSourceWithCapturedVariable(variableName);
    }

    private static bool IsDefaultExpression(string expression)
    {
        return expression == "default" || expression.StartsWith("default(", StringComparison.Ordinal);
    }

    private static void WriteAddComponentRenderMode<T>(CodeRenderingContext context, BuilderVariableName builderName, T renderModeName)
        where T : IWriteableValue
        => context.CodeWriter.WriteLine($"{builderName}.{ComponentsApi.RenderTreeBuilder.AddComponentRenderMode}({renderModeName});");

    private static void WriteGloballyQualifiedTypeName(CodeRenderingContext context, ComponentAttributeIntermediateNode node)
    {
        if (node.HasExplicitTypeName)
        {
            context.CodeWriter.Write(node.TypeName);
        }
        else if (node.BoundAttribute?.GetGloballyQualifiedTypeName() is string typeName)
        {
            context.CodeWriter.Write(typeName);
        }
        else
        {
            TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName);
        }
    }

    private static void WriteGloballyQualifiedTypeName(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
    {
        if (node.BoundAttribute?.GetGloballyQualifiedTypeName() is string typeName &&
            !node.BoundAttribute.IsGenericTypedProperty())
        {
            context.CodeWriter.Write(typeName);
        }
        else
        {
            TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeName);
        }
    }

    private static void WriteComponentTypeName(CodeRenderingContext context, ComponentIntermediateNode node, ReadOnlyMemory<char> nonGenericTypeName)
    {
        // The type name we are given may or may not be globally qualified, and we want to map it to the component start
        // tag, which may or may not be fully qualified. ie "global::My.Fun.Component" could map to just "Component"

        // Write out "global::" if it's present, and trim it off
        var lastColon = nonGenericTypeName.Span.LastIndexOf(':');
        if (lastColon > -1)
        {
            lastColon++;
            context.CodeWriter.Write(nonGenericTypeName[0..lastColon]);
            nonGenericTypeName = nonGenericTypeName.Slice(lastColon);
        }

        // If the start tag is shorter than the type name, then it must not be a fully qualified tag, so write out
        // the namespace parts and trim. Razor components don't support nested types, so this logic doesn't either.
        if (node.StartTagSpan.Length < nonGenericTypeName.Length)
        {
            var lastDot = nonGenericTypeName.Span.LastIndexOf('.');
            if (lastDot > -1)
            {
                lastDot++;
                context.CodeWriter.Write(nonGenericTypeName[0..lastDot]);
                nonGenericTypeName = nonGenericTypeName.Slice(lastDot);
            }
        }

        var offset = nonGenericTypeName.Span.StartsWith('@')
            ? 1
            : 0;
        context.AddSourceMappingFor(node.StartTagSpan, offset);
        context.CodeWriter.Write(nonGenericTypeName);
    }

    [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
    internal readonly struct SeqName(int index) : IWriteableValue
    {
        public void WriteTo(CodeWriter writer)
        {
            writer.Write("__seq");
            writer.WriteIntegerLiteral(index);
        }

        internal string GetDebuggerDisplay()
            => $"__seq{index}";
    }

    [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
    internal readonly struct ParameterName(int index, bool isSynthetic = false) : IWriteableValue
    {
        public void WriteTo(CodeWriter writer)
        {
            if (isSynthetic)
            {
                writer.Write("__syntheticArg");
            }
            else
            {
                writer.Write("__arg");
            }

            writer.WriteIntegerLiteral(index);
        }

        internal string GetDebuggerDisplay()
            => isSynthetic ? $"__syntheticArg{index}" : $"__arg{index}";
    }

    [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
    internal readonly struct TypeInferenceArgName(int depth, ParameterName parameterName) : IWriteableValue
    {
        public void WriteTo(CodeWriter writer)
        {
            writer.Write("__typeInferenceArg_");
            writer.WriteIntegerLiteral(depth);
            writer.Write($"_{parameterName}");
        }

        internal string GetDebuggerDisplay()
            => $"__typeInferenceArg_{depth}_{parameterName.GetDebuggerDisplay()}";
    }

    private class TypeInferenceMethodParameter
    {
        public SeqName? SeqName { get; }
        public string TypeName { get; }
        public ParameterName ParameterName { get; }
        public bool UsedForTypeInference { get; }
        public object Source { get; private set; }

        public TypeInferenceMethodParameter(SeqName? seqName, string typeName, ParameterName parameterName, bool usedForTypeInference, object source)
        {
            SeqName = seqName;
            TypeName = typeName;
            ParameterName = parameterName;
            UsedForTypeInference = usedForTypeInference;
            Source = source;
        }

        public void ReplaceSourceWithCapturedVariable(TypeInferenceArgName variableName)
        {
            Source = new TypeInferenceCapturedVariable(variableName);
        }
    }

    private sealed class TypeInferenceCapturedVariable(TypeInferenceArgName variableName)
    {
        public TypeInferenceArgName VariableName { get; } = variableName;
    }

    private readonly ImmutableArray<IntermediateToken>.Builder _currentAttributeValues = ImmutableArray.CreateBuilder<IntermediateToken>();

    private int _sourceSequence;

    public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
    {
        var isWhitespaceStatement = true;
        foreach (var child in node.Children)
        {
            if (child is not IntermediateToken token || !string.IsNullOrWhiteSpace(token.Content))
            {
                isWhitespaceStatement = false;
                break;
            }
        }

        if (node.Source is null && isWhitespaceStatement)
        {
            // If source is null, we won't create source mappings, and if we're not creating source mappings,
            // there is no point emitting whitespace
            return;
        }

        foreach (var child in node.Children)
        {
            if (child is CSharpIntermediateToken token)
            {
                WriteCSharpToken(context, token);
            }
            else
            {
                // There may be something else inside the statement like an extension node.
                context.RenderNode(child);
            }
        }

        context.CodeWriter.WriteLine();
    }

    public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        var characterOffset = BuilderVariableName.Length // for "__builder"
            + 1 // for '.'
            + ComponentsApi.RenderTreeBuilder.AddContent.Length
            + 1 // for '('
            + _sourceSequence.CountDigits() // for the sequence number
            + 2; // for ', '

        // Sequence points can only be emitted when the eval stack is empty. That means we can't arbitrarily map everything that could be in
        // the node. Instead we map just the first C# child node by putting the pragma before we start the method invocation and offset it.
        // This is not a perfect mapping, but generally this works for most cases:
        // - Common case: there is only a single node and it is C#, so it maps correctly
        // - There is some C# followed by a render template: the C# gets mapped, and the render template issues a lambda call which conceptually
        //   is another method so a sequence point can be emitted. Unfortunately any trailing C# is not mapped, although in many cases it's uninteresting
        //   such as closing parenthesis.
        // - Error cases: there are no nodes, so we do nothing
        var firstCSharpChild = node.Children.OfType<CSharpIntermediateToken>().FirstOrDefault();
        using (context.BuildEnhancedLinePragma(firstCSharpChild?.Source, characterOffset))
        {
            context.CodeWriter
                .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddContent}")
                .WriteIntegerLiteral(_sourceSequence++)
                .WriteParameterSeparator();

            if (firstCSharpChild is not null)
            {
                context.CodeWriter.Write(firstCSharpChild.Content);
            }
        }

        // render the remaining children. We still emit the #line pragmas for the remaining csharp tokens but
        // these wont actually generate any sequence points for debugging.

        foreach (var child in node.Children)
        {
            if (child == firstCSharpChild)
            {
                continue;
            }

            if (child is CSharpIntermediateToken csharpToken)
            {
                WriteCSharpToken(context, csharpToken);
            }
            else
            {
                // There may be something else inside the expression like a Template or another extension node.
                context.RenderNode(child);
            }
        }

        context.CodeWriter.WriteEndMethodInvocation();
    }

    public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
    {
        // In cases like "somestring @variable", Razor tokenizes it as:
        //  [0] HtmlContent="somestring"
        //  [1] CsharpContent="variable" Prefix=" "
        // ... so to avoid losing whitespace, convert the prefix to a further token in the list
        if (!string.IsNullOrEmpty(node.Prefix))
        {
            _currentAttributeValues.Add(IntermediateNodeFactory.HtmlToken(node.Prefix));
        }

        foreach (var child in node.Children)
        {
            _currentAttributeValues.Add((IntermediateToken)child);
        }
    }

    public override void WriteMarkupBlock(CodeRenderingContext context, MarkupBlockIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        context.CodeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddMarkupContent}")
            .WriteIntegerLiteral(_sourceSequence++)
            .WriteParameterSeparator()
            .WriteStringLiteral(node.Content)
            .WriteEndMethodInvocation();
    }

    public override void WriteMarkupElement(CodeRenderingContext context, MarkupElementIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        context.CodeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.OpenElement}")
            .WriteIntegerLiteral(_sourceSequence++)
            .WriteParameterSeparator()
            .WriteStringLiteral(node.TagName)
            .WriteEndMethodInvocation();

        bool hasFormName = false;

        // Render attributes and splats (in order) before creating the scope.
        foreach (var child in node.Children)
        {
            if (child is HtmlAttributeIntermediateNode attribute)
            {
                context.RenderNode(attribute);
            }
            else if (child is ComponentAttributeIntermediateNode componentAttribute)
            {
                context.RenderNode(componentAttribute);
            }
            else if (child is SplatIntermediateNode splat)
            {
                context.RenderNode(splat);
            }
            else if (child is FormNameIntermediateNode formName)
            {
                Debug.Assert(!hasFormName);
                context.RenderNode(formName);
                hasFormName = true;
            }
        }

        foreach (var setKey in node.SetKeys)
        {
            context.RenderNode(setKey);
        }

        foreach (var capture in node.Captures)
        {
            context.RenderNode(capture);
        }

        // AddNamedEvent must be called after all attributes (but before child content).
        if (hasFormName)
        {
            // _builder.AddNamedEvent("onsubmit", __formName);
            context.CodeWriter.WriteLine($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddNamedEvent}(\"onsubmit\", {FormNameVariableName});");
            ScopeStack.IncrementFormName();
        }

        // Render body of the tag inside the scope
        foreach (var child in node.Body)
        {
            context.RenderNode(child);
        }

        context.CodeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.CloseElement}")
            .WriteEndMethodInvocation();
    }

    public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
    {
        Debug.Assert(_currentAttributeValues.Count == 0);
        context.RenderChildren(node);

        if (node.AttributeNameExpression == null)
        {
            WriteAttribute(context, node.AttributeName, _currentAttributeValues.ToImmutableAndClear());
        }
        else
        {
            WriteAttribute(context, node.AttributeNameExpression, _currentAttributeValues.ToImmutableAndClear());
        }

        if (!string.IsNullOrEmpty(node.EventUpdatesAttributeName))
        {
            context.CodeWriter
                .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.SetUpdatesAttributeName}")
                .WriteStringLiteral(node.EventUpdatesAttributeName)
                .WriteEndMethodInvocation();
        }
    }

    public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        var stringContent = ((IntermediateToken)node.Children.Single()).Content;
        _currentAttributeValues.Add(IntermediateNodeFactory.HtmlToken(node.Prefix + stringContent));
    }

    public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        // Text node
        var content = GetHtmlContent(node);
        var renderApi = ComponentsApi.RenderTreeBuilder.AddContent;
        if (node.HasEncodedContent)
        {
            // This content is already encoded.
            renderApi = ComponentsApi.RenderTreeBuilder.AddMarkupContent;
        }

        context.CodeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{renderApi}")
            .WriteIntegerLiteral(_sourceSequence++)
            .WriteParameterSeparator()
            .WriteStringLiteral(content)
            .WriteEndMethodInvocation();
    }

    public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        if (node.Source is { FilePath: not null } sourceSpan)
        {
            using (context.BuildEnhancedLinePragma(sourceSpan, suppressLineDefaultAndHidden: true))
            {
                context.CodeWriter.WriteUsing(node.Content, endLine: node.HasExplicitSemicolon);
            }
            if (!node.HasExplicitSemicolon)
            {
                context.CodeWriter.WriteLine(";");
            }
            if (node.AppendLineDefaultAndHidden)
            {
                context.CodeWriter.WriteLine("#line default");
                context.CodeWriter.WriteLine("#line hidden");
            }
        }
        else
        {
            context.CodeWriter.WriteUsing(node.Content, endLine: true);

            if (node.AppendLineDefaultAndHidden)
            {
                context.CodeWriter.WriteLine("#line default");
                context.CodeWriter.WriteLine("#line hidden");
            }
        }
    }

    public override void WriteComponent(CodeRenderingContext context, ComponentIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        if (ShouldSuppressTypeInferenceCall(node))
        {
        }
        else if (node.TypeInferenceNode == null)
        {
            // If the component is not using type inference then we just write an open/close with a series
            // of add attribute calls in between.
            //
            // Writes something like:
            //
            // _builder.OpenComponent<MyComponent>(0);
            // _builder.AddComponentParameter(1, "Foo", ...);
            // _builder.AddComponentParameter(2, "ChildContent", ...);
            // _builder.SetKey(someValue);
            // _builder.AddElementCapture(3, (__value) => _field = __value);
            // _builder.CloseComponent();

            // _builder.OpenComponent<TComponent>(42);
            context.CodeWriter.Write(BuilderVariableName);
            context.CodeWriter.Write(".");
            context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.OpenComponent);
            context.CodeWriter.Write("<");

            var nonGenericTypeName = TypeNameHelper.GetNonGenericTypeName(node.TypeName, out _);
            TypeNameHelper.WriteGlobalPrefixIfNeeded(context.CodeWriter, nonGenericTypeName);
            WriteComponentTypeName(context, node, nonGenericTypeName);

            if (!node.OrderedTypeArguments.IsDefaultOrEmpty)
            {
                context.CodeWriter.Write("<");
                for (var i = 0; i < node.OrderedTypeArguments.Length; i++)
                {
                    var typeArg = node.OrderedTypeArguments[i];
                    WriteComponentTypeArgument(context, typeArg);
                    if (i != node.OrderedTypeArguments.Length - 1)
                    {
                        context.CodeWriter.Write(", ");
                    }
                }
                context.CodeWriter.Write(">");
            }

            context.CodeWriter.Write(">(");
            context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);
            context.CodeWriter.Write(");");
            context.CodeWriter.WriteLine();

            // We can skip type arguments during runtime codegen, they are handled in the
            // type/parameter declarations.

            bool hasRenderMode = false;

            // Preserve order of attributes and splats
            foreach (var child in node.Children)
            {
                if (child is ComponentAttributeIntermediateNode attribute)
                {
                    context.RenderNode(attribute);
                }
                else if (child is SplatIntermediateNode splat)
                {
                    context.RenderNode(splat);
                }
                else if (child is RenderModeIntermediateNode renderMode)
                {
                    Debug.Assert(!hasRenderMode);
                    context.RenderNode(renderMode);
                    hasRenderMode = true;
                }
            }

            foreach (var childContent in node.ChildContents)
            {
                context.RenderNode(childContent);
            }

            foreach (var setKey in node.SetKeys)
            {
                context.RenderNode(setKey);
            }

            foreach (var capture in node.Captures)
            {
                context.RenderNode(capture);
            }

            if (hasRenderMode)
            {
                // _builder.AddComponentRenderMode(__renderMode_0);
                WriteAddComponentRenderMode(context, BuilderVariableName, RenderModeVariableName);
                ScopeStack.IncrementRenderMode();
            }

            // _builder.CloseComponent();
            context.CodeWriter.Write(BuilderVariableName);
            context.CodeWriter.Write(".");
            context.CodeWriter.Write(ComponentsApi.RenderTreeBuilder.CloseComponent);
            context.CodeWriter.Write("();");
            context.CodeWriter.WriteLine();
        }
        else
        {
            var parameters = GetTypeInferenceMethodParameters(node.TypeInferenceNode);

            // If this component is going to cascade any of its generic types, we have to split its type inference
            // into two parts. First we call an inference method that captures all the parameters in local variables,
            // then we use those to call the real type inference method that emits the component. The reason for this
            // is so the captured variables can be used by descendants without re-evaluating the expressions.
            CodeWriterExtensions.CSharpCodeWritingScope? typeInferenceCaptureScope = null;
            if (node.Component.SuppliesCascadingGenericParameters())
            {
                typeInferenceCaptureScope = context.CodeWriter.BuildScope();
                TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeInferenceNode.FullTypeName);
                context.CodeWriter.Write(".");
                context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
                context.CodeWriter.Write("_CaptureParameters(");
                var isFirst = true;
                foreach (var parameter in parameters.Where(p => p.UsedForTypeInference))
                {
                    if (isFirst)
                    {
                        isFirst = false;
                    }
                    else
                    {
                        context.CodeWriter.Write(", ");
                    }

                    WriteTypeInferenceMethodParameterInnards(context, parameter);
                    context.CodeWriter.Write(", out var ");

                    var variableName = new TypeInferenceArgName(ScopeStack.Depth, parameter.ParameterName);
                    context.CodeWriter.Write(variableName);

                    UseCapturedCascadingGenericParameterVariable(node, parameter, variableName);
                }
                context.CodeWriter.WriteLine(");");
            }

            // When we're doing type inference, we can't write all of the code inline to initialize
            // the component on the builder. We generate a method elsewhere, and then pass all of the information
            // to that method. We pass in all of the attribute values + the sequence numbers.
            //
            // __Blazor.MyComponent.TypeInference.CreateMyComponent_0(builder, 0, 1, ..., 2, ..., 3, ...);

            TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, node.TypeInferenceNode.FullTypeName);
            context.CodeWriter.Write(".");
            context.CodeWriter.Write(node.TypeInferenceNode.MethodName);
            context.CodeWriter.Write("(");

            context.CodeWriter.Write(BuilderVariableName);
            context.CodeWriter.Write(", ");

            context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);

            foreach (var parameter in parameters)
            {
                context.CodeWriter.Write(", ");

                if (parameter.SeqName != null)
                {
                    context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);
                    context.CodeWriter.Write(", ");
                }

                WriteTypeInferenceMethodParameterInnards(context, parameter);
            }

            context.CodeWriter.Write(");");
            context.CodeWriter.WriteLine();

            if (typeInferenceCaptureScope.HasValue)
            {
                foreach (var localToClear in parameters.Select(p => p.Source).OfType<TypeInferenceCapturedVariable>())
                {
                    // Ensure we're not interfering with the GC lifetime of these captured values
                    // We don't need the values any longer (code in closures only uses its types for compile-time inference)
                    context.CodeWriter.Write(localToClear.VariableName);
                    context.CodeWriter.WriteLine(" = default;");
                }

                typeInferenceCaptureScope.Value.Dispose();
            }
        }
    }

    public override void WriteComponentTypeInferenceMethod(CodeRenderingContext context, ComponentTypeInferenceMethodIntermediateNode node)
    {
        WriteComponentTypeInferenceMethod(context, node, returnComponentType: false, allowNameof: true, mapComponentStartTag: true);
    }

    private void WriteTypeInferenceMethodParameterInnards(CodeRenderingContext context, TypeInferenceMethodParameter parameter)
    {
        switch (parameter.Source)
        {
            case ComponentAttributeIntermediateNode attribute:
                // Don't type check generics, since we can't actually write the type name.
                // The type checking will happen anyway since we defined a method and we're generating
                // a call to it.
                WriteComponentAttributeInnards(context, attribute, canTypeCheck: false);
                break;
            case SplatIntermediateNode splat:
                WriteSplatInnards(context, splat, canTypeCheck: false);
                break;
            case ComponentChildContentIntermediateNode childNode:
                WriteComponentChildContentInnards(context, childNode);
                break;
            case SetKeyIntermediateNode setKey:
                WriteSetKeyInnards(context, setKey);
                break;
            case ReferenceCaptureIntermediateNode capture:
                WriteReferenceCaptureInnards(context, capture, shouldTypeCheck: false);
                break;
            case CascadingGenericTypeParameter syntheticArg:
                // The value should be populated before we use it, because we emit code for creating ancestors
                // first, and that's where it's populated. However if this goes wrong somehow, we don't want to
                // throw, so use a fallback
                if (syntheticArg.ValueExpression is IWriteableValue writeableValue)
                {
                    writeableValue.WriteTo(context.CodeWriter);
                }
                else
                {
                    var valueExpression = syntheticArg.ValueExpression as string ?? "default";
                    context.CodeWriter.Write(valueExpression);

                    if (!context.Options.SuppressNullabilityEnforcement && IsDefaultExpression(valueExpression))
                    {
                        context.CodeWriter.Write("!");
                    }
                }

                break;

            case TypeInferenceCapturedVariable capturedVariable:
                context.CodeWriter.Write(capturedVariable.VariableName);
                break;
            case RenderModeIntermediateNode renderMode:
                WriteCSharpCode(context, new CSharpCodeIntermediateNode() { Source = renderMode.Source, Children = { renderMode.Children[0] } });
                break;
            default:
                throw new InvalidOperationException($"Not implemented: type inference method parameter from source {parameter.Source}");
        }
    }

    public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        if (node.IsDesignTimePropertyAccessHelper)
        {
            WriteDesignTimePropertyAccessor(context, node);
            return;
        }

        var addAttributeMethod = node.AddAttributeMethodName ?? GetAddComponentParameterMethodName(context);

        // _builder.AddComponentParameter(1, nameof(Component.Property), 42);
        context.CodeWriter.Write(BuilderVariableName);
        context.CodeWriter.Write(".");
        context.CodeWriter.Write(addAttributeMethod);
        context.CodeWriter.Write("(");
        context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);
        context.CodeWriter.Write(", ");

        WriteComponentAttributeName(context, node);
        context.CodeWriter.Write(", ");

        if (addAttributeMethod == ComponentsApi.RenderTreeBuilder.AddAttribute)
        {
            context.CodeWriter.Write("(object)(");
        }

        WriteComponentAttributeInnards(context, node, canTypeCheck: true);

        if (addAttributeMethod == ComponentsApi.RenderTreeBuilder.AddAttribute)
        {
            context.CodeWriter.Write(")");
        }

        context.CodeWriter.Write(");");
        context.CodeWriter.WriteLine();
    }

    private static void WriteDesignTimePropertyAccessor(CodeRenderingContext context, ComponentAttributeIntermediateNode attribute)
    {
        // These attributes don't really exist in the emitted code, but have a representation in the razor document.
        // We emit a small piece of empty code that is elided by the compiler, so that the IDE has something to reference
        // for Find All References etc.
        //
        // We use `var (_, _) = (nameof(...), 0);` rather than `_ = nameof(...);` so that `_` is unambiguously a
        // discard pattern. A bare `_ = ...` would bind to any outer `_` lambda parameter (introduced e.g. by
        // `Context="_"` on a templated parent component), turning the discard into a typed assignment that fails
        // with CS0029 / cascading CS1662 errors.
        Debug.Assert(attribute.BoundAttribute?.ContainingType is not null);
        context.CodeWriter.Write(" var (_, _) = (");
        WriteComponentAttributeName(context, attribute);
        context.CodeWriter.WriteLine(", 0);");
    }

    private void WriteComponentAttributeInnards(CodeRenderingContext context, ComponentAttributeIntermediateNode node, bool canTypeCheck)
    {
        if (node.Children.Count > 1)
        {
            Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
            // We render the children anyway, so tooling works.
        }

        if (node.AttributeStructure == AttributeStructure.Minimized)
        {
            // Minimized attributes always map to 'true'
            context.CodeWriter.Write("true");
        }
        else if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlNode)
        {
            // This is how string attributes are lowered by default, a single HTML node with a single HTML token.
            var content = string.Join(string.Empty, GetHtmlTokens(htmlNode).Select(t => t.Content));
            context.CodeWriter.WriteStringLiteral(content);
        }
        else
        {
            // Handle delegate or child-content attributes specially.
            var tokens = GetCSharpTokens(node);
            if ((node.BoundAttribute?.IsDelegateProperty() ?? false) ||
                (node.BoundAttribute?.IsChildContentProperty() ?? false))
            {
                if (canTypeCheck)
                {
                    context.CodeWriter.Write("(");
                    WriteGloballyQualifiedTypeName(context, node);
                    context.CodeWriter.Write(")");
                    context.CodeWriter.Write("(");
                }

                WriteCSharpTokens(context, tokens);

                if (canTypeCheck)
                {
                    context.CodeWriter.Write(")");
                }
            }
            else if (node.BoundAttribute?.IsEventCallbackProperty() ?? false)
            {
                var explicitType = node.HasExplicitTypeName;
                var isInferred = node.IsOpenGeneric;
                if (canTypeCheck && NeedsTypeCheck(node))
                {
                    context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
                    context.CodeWriter.Write("<");
                    QualifyEventCallback(context.CodeWriter, node.TypeName, explicitType);
                    context.CodeWriter.Write(">");
                    context.CodeWriter.Write("(");
                }

                // Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, ...) OR
                // Microsoft.AspNetCore.Components.EventCallback.Factory.Create<T>(this, ...)

                context.CodeWriter.Write("global::");
                context.CodeWriter.Write(ComponentsApi.EventCallback.FactoryAccessor);
                context.CodeWriter.Write(".");
                context.CodeWriter.Write(ComponentsApi.EventCallbackFactory.CreateMethod);

                if (isInferred != true && node.TryParseEventCallbackTypeArgument(out ReadOnlyMemory<char> argument))
                {
                    context.CodeWriter.Write("<");
                    if (explicitType)
                    {
                        context.CodeWriter.Write(argument);
                    }
                    else
                    {
                        TypeNameHelper.WriteGloballyQualifiedName(context.CodeWriter, argument);
                    }
                    context.CodeWriter.Write(">");
                }

                context.CodeWriter.Write("(");
                context.CodeWriter.Write("this");
                context.CodeWriter.Write(", ");

                WriteCSharpTokens(context, tokens);

                context.CodeWriter.Write(")");

                if (canTypeCheck && NeedsTypeCheck(node))
                {
                    context.CodeWriter.Write(")");
                }
            }
            else
            {
                if (canTypeCheck && NeedsTypeCheck(node))
                {
                    context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
                    context.CodeWriter.Write("<");
                    WriteGloballyQualifiedTypeName(context, node);
                    context.CodeWriter.Write(">");
                    context.CodeWriter.Write("(");
                }

                WriteCSharpTokens(context, tokens);

                if (canTypeCheck && NeedsTypeCheck(node))
                {
                    context.CodeWriter.Write(")");
                }

            }

            static void QualifyEventCallback(CodeWriter codeWriter, string typeName, bool? explicitType)
            {
                if (ComponentAttributeIntermediateNode.TryGetEventCallbackArgument(typeName.AsMemory(), out var argument))
                {
                    codeWriter.Write("global::");
                    codeWriter.Write(ComponentsApi.EventCallback.FullTypeName);
                    codeWriter.Write("<");
                    if (explicitType == true)
                    {
                        codeWriter.Write(argument);
                    }
                    else
                    {
                        TypeNameHelper.WriteGloballyQualifiedName(codeWriter, argument);
                    }
                    codeWriter.Write(">");
                }
                else
                {
                    TypeNameHelper.WriteGloballyQualifiedName(codeWriter, typeName);
                }
            }
        }

        static bool NeedsTypeCheck(ComponentAttributeIntermediateNode n)
        {
            return n.BoundAttribute != null && !n.BoundAttribute.IsWeaklyTyped;
        }
    }

    private static ImmutableArray<HtmlIntermediateToken> GetHtmlTokens(IntermediateNode node)
    {
        // We generally expect all children to be HTML, this is here just in case.
        return node.FindDescendantNodes<HtmlIntermediateToken>();
    }

    private static ImmutableArray<CSharpIntermediateToken> GetCSharpTokens(IntermediateNode node)
    {
        // We generally expect all children to be CSharp, this is here just in case.
        return node.FindDescendantNodes<CSharpIntermediateToken>();
    }

    public override void WriteComponentChildContent(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        // Writes something like:
        //
        // _builder.AddComponentParameter(1, "ChildContent", (RenderFragment)((__builder73) => { ... }));
        // OR
        // _builder.AddComponentParameter(1, "ChildContent", (RenderFragment<Person>)((person) => (__builder73) => { ... }));
        BeginWriteAttribute(context, node.AttributeName);
        context.CodeWriter.WriteParameterSeparator();
        context.CodeWriter.Write("(");
        WriteGloballyQualifiedTypeName(context, node);
        context.CodeWriter.Write(")(");

        WriteComponentChildContentInnards(context, node);

        context.CodeWriter.Write(")");
        context.CodeWriter.WriteEndMethodInvocation();
    }

    private void WriteComponentChildContentInnards(CodeRenderingContext context, ComponentChildContentIntermediateNode node)
    {
        // Writes something like:
        //
        // ((__builder73) => { ... })
        // OR
        // ((person) => (__builder73) => { })
        var parameterName = node.IsParameterized ? node.ParameterName : null;

        using (ScopeStack.OpenComponentScope(context, parameterName))
        {
            foreach (var child in node.Children)
            {
                context.RenderNode(child);
            }
        }
    }

    public override void WriteComponentTypeArgument(CodeRenderingContext context, ComponentTypeArgumentIntermediateNode node)
    {
        WriteCSharpToken(context, node.Value);
    }

    public void WriteTemplate(CodeRenderingContext context, TemplateIntermediateNode node)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (node == null)
        {
            throw new ArgumentNullException(nameof(node));
        }

        // Looks like:
        //
        // (__builder73) => { ... }
        using (ScopeStack.OpenTemplateScope(context))
        {
            context.RenderChildren(node);
        }
    }

    public override void WriteSetKey(CodeRenderingContext context, SetKeyIntermediateNode node)
    {
        // Looks like:
        //
        // _builder.SetKey(_keyValue);

        var codeWriter = context.CodeWriter;

        codeWriter.WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.SetKey}");
        WriteSetKeyInnards(context, node);
        codeWriter.WriteEndMethodInvocation();
    }

    private void WriteSetKeyInnards(CodeRenderingContext context, SetKeyIntermediateNode node)
    {
        WriteCSharpCode(context, new CSharpCodeIntermediateNode
        {
            Source = node.Source,
            Children = { node.KeyValueToken }
        });
    }

    public override void WriteSplat(CodeRenderingContext context, SplatIntermediateNode node)
    {
        // Looks like:
        //
        // _builder.AddMultipleAttributes(2, ...);
        context.CodeWriter.WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddMultipleAttributes}");
        context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);
        context.CodeWriter.WriteParameterSeparator();

        WriteSplatInnards(context, node, canTypeCheck: true);

        context.CodeWriter.WriteEndMethodInvocation();
    }

    private static void WriteSplatInnards(CodeRenderingContext context, SplatIntermediateNode node, bool canTypeCheck)
    {
        var writer = context.CodeWriter;

        if (canTypeCheck)
        {
            writer.Write($"{ComponentsApi.RuntimeHelpers.TypeCheck}<{ComponentsApi.AddMultipleAttributesTypeFullName}>(");
        }

        using var tokens = new PooledArrayBuilder<CSharpIntermediateToken>();
        node.CollectDescendantNodes(ref tokens.AsRef());

        WriteCSharpTokens(context, in tokens);

        if (canTypeCheck)
        {
            writer.Write(")");
        }
    }

    public sealed override void WriteFormName(CodeRenderingContext context, FormNameIntermediateNode node)
    {
        if (node.Children.Count > 1)
        {
            Debug.Assert(node.HasDiagnostics, "We should have reported an error for mixed content.");
        }

        // string __formName = expression;
        context.CodeWriter.Write($"string {FormNameVariableName} = {ComponentsApi.RuntimeHelpers.TypeCheck}<string>(");
        WriteAttributeValue(context, node.FindDescendantNodes<IntermediateToken>());
        context.CodeWriter.WriteLine(");");
    }

    public override void WriteReferenceCapture(CodeRenderingContext context, ReferenceCaptureIntermediateNode node)
    {
        // Looks like:
        //
        // _builder.AddComponentReferenceCapture(2, (__value) = { _field = (MyComponent)__value; });
        // OR
        // _builder.AddElementReferenceCapture(2, (__value) = { _field = (ElementReference)__value; });
        var codeWriter = context.CodeWriter;

        var methodName = node.IsComponentCapture
            ? ComponentsApi.RenderTreeBuilder.AddComponentReferenceCapture
            : ComponentsApi.RenderTreeBuilder.AddElementReferenceCapture;
        codeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{methodName}")
            .WriteIntegerLiteral(_sourceSequence++)
            .WriteParameterSeparator();

        WriteReferenceCaptureInnards(context, node, shouldTypeCheck: true);

        codeWriter.WriteEndMethodInvocation();
    }

    private void WriteReferenceCaptureInnards(CodeRenderingContext context, ReferenceCaptureIntermediateNode node, bool shouldTypeCheck)
    {
        // Looks like:
        //
        // (__value) = { _field = (MyComponent)__value; }
        // OR
        // (__value) = { _field = (ElementRef)__value; }
        const string RefCaptureParamName = "__value";
        const string DefaultAssignment = $" = {RefCaptureParamName};";

        using (context.CodeWriter.BuildLambda(RefCaptureParamName))
        {
            shouldTypeCheck = shouldTypeCheck && node.IsComponentCapture;

            var assignmentToken = shouldTypeCheck
                ? IntermediateNodeFactory.CSharpToken($" = ({node.FieldTypeName}){RefCaptureParamName};")
                : IntermediateNodeFactory.CSharpToken(DefaultAssignment);

            WriteCSharpCode(context, new CSharpCodeIntermediateNode
            {
                Source = node.Source,
                Children = { node.IdentifierToken, assignmentToken }
            });
        }
    }

    public override void WriteRenderMode(CodeRenderingContext context, RenderModeIntermediateNode node)
    {
        // Looks like:
        // global::Microsoft.AspNetCore.Components.IComponentRenderMode __renderMode0 = expression;
        context.CodeWriter.Write($"global::{ComponentsApi.IComponentRenderMode.FullTypeName} {RenderModeVariableName} = ");

        WriteCSharpCode(context, new CSharpCodeIntermediateNode
        {
            Source = node.Source,
            Children = { node.Children[0] }
        });

        context.CodeWriter.WriteLine(";");
    }

    private void WriteAttribute(CodeRenderingContext context, string key, ImmutableArray<IntermediateToken> value)
    {
        BeginWriteAttribute(context, key);

        if (value.Length > 0)
        {
            context.CodeWriter.WriteParameterSeparator();
            WriteAttributeValue(context, value);
        }
        else if (!context.Options.OmitMinimizedComponentAttributeValues)
        {
            // In version 5+, there's no need to supply a value for a minimized attribute.
            // But for older language versions, minimized attributes were represented as "true".
            context.CodeWriter.WriteParameterSeparator();
            context.CodeWriter.WriteBooleanLiteral(true);
        }

        context.CodeWriter.WriteEndMethodInvocation();
    }

    private void WriteAttribute(CodeRenderingContext context, IntermediateNode nameExpression, ImmutableArray<IntermediateToken> value)
    {
        BeginWriteAttribute(context, nameExpression);

        if (value.Length > 0)
        {
            context.CodeWriter.WriteParameterSeparator();
            WriteAttributeValue(context, value);
        }

        context.CodeWriter.WriteEndMethodInvocation();
    }

    private void BeginWriteAttribute(CodeRenderingContext context, string key)
    {
        context.CodeWriter
            .WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddAttribute}")
            .WriteIntegerLiteral(_sourceSequence++)
            .WriteParameterSeparator()
            .WriteStringLiteral(key);
    }

    private void BeginWriteAttribute(CodeRenderingContext context, IntermediateNode nameExpression)
    {
        context.CodeWriter.WriteStartMethodInvocation($"{BuilderVariableName}.{ComponentsApi.RenderTreeBuilder.AddAttribute}");
        context.CodeWriter.WriteIntegerLiteral(_sourceSequence++);
        context.CodeWriter.WriteParameterSeparator();

        var tokens = GetCSharpTokens(nameExpression);
        for (var i = 0; i < tokens.Length; i++)
        {
            WriteCSharpToken(context, tokens[i]);
        }
    }

    private static string GetHtmlContent(HtmlContentIntermediateNode node)
    {
        using var _ = StringBuilderPool.GetPooledObject(out var builder);

        var htmlTokens = node.Children.OfType<HtmlIntermediateToken>();

        foreach (var child in node.Children)
        {
            if (child is HtmlIntermediateToken htmlToken)
            {
                builder.Append(htmlToken.Content);
            }
        }

        return builder.ToString();
    }

    // There are a few cases here, we need to handle:
    // - Pure HTML
    // - Pure CSharp
    // - Mixed HTML and CSharp
    //
    // Only the mixed case is complicated, we want to turn it into code that will concatenate
    // the values into a string at runtime.

    private static void WriteAttributeValue(CodeRenderingContext context, ImmutableArray<IntermediateToken> tokens)
    {
        if (tokens.Length == 0)
        {
            return;
        }

        var writer = context.CodeWriter;
        var hasHtml = false;
        var hasCSharp = false;

        foreach (var token in tokens)
        {
            if (token is CSharpIntermediateToken)
            {
                hasCSharp |= true;
            }
            else
            {
                Debug.Assert(token is HtmlIntermediateToken);
                hasHtml |= true;
            }
        }

        if (!hasCSharp && !hasHtml)
        {
            Assumed.Unreachable("Found attribute whose value is neither HTML nor CSharp");
        }

        // If we only have C# tokens, we write them out directly.
        if (hasCSharp && !hasHtml)
        {
            foreach (var token in tokens)
            {
                WriteCSharpToken(context, (CSharpIntermediateToken)token);
            }

            return;
        }

        // If we only have HTML tokens, we write out a single string literal.
        if (hasHtml && !hasCSharp)
        {
            using var _ = StringBuilderPool.GetPooledObject(out var builder);

            foreach (var token in tokens)
            {
                Debug.Assert(token is HtmlIntermediateToken);
                builder.Append(token.Content);
            }

            writer.WriteStringLiteral(builder.ToString());
            return;
        }

        // If it's a C# expression, we have to wrap it in parentheses, otherwise things like ternary
        // expressions don't compose with concatenation. However, this is a little complicated
        // because C# tokens themselves aren't guaranteed to be distinct expressions. We want
        // to treat all contiguous C# tokens as a single expression.
        var insideCSharp = false;
        var first = true;
        foreach (var token in tokens)
        {
            if (token is CSharpIntermediateToken csharpToken)
            {
                if (!insideCSharp)
                {
                    // Transition to a new C# expression
                    if (!first)
                    {
                        writer.Write(" + ");
                    }

                    writer.Write("(");
                    insideCSharp = true;
                }

                WriteCSharpToken(context, csharpToken);
            }
            else
            {
                if (insideCSharp)
                {
                    // Transition to HTML, close out the C# expression
                    writer.Write(")");
                    insideCSharp = false;
                }

                if (!first)
                {
                    writer.Write(" + ");
                }

                writer.WriteStringLiteral(token.Content);
            }

            if (first)
            {
                first = false;
            }
        }

        if (insideCSharp)
        {
            writer.Write(")");
        }
    }

    private static void WriteCSharpTokens(CodeRenderingContext context, ImmutableArray<CSharpIntermediateToken> tokens)
    {
        foreach (var token in tokens)
        {
            WriteCSharpToken(context, token);
        }
    }

    private static void WriteCSharpTokens(CodeRenderingContext context, ref readonly PooledArrayBuilder<CSharpIntermediateToken> tokens)
    {
        foreach (var token in tokens)
        {
            WriteCSharpToken(context, token);
        }
    }

    private static void WriteCSharpToken(CodeRenderingContext context, CSharpIntermediateToken token)
    {
        if (token.Source?.FilePath == null)
        {
            context.CodeWriter.Write(token.Content);
            return;
        }

        using (context.BuildEnhancedLinePragma(token.Source))
        {
            context.CodeWriter.Write(token.Content);
        }
    }
}