File: Language\Extensions\DefaultTagHelperTargetExtension.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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
 
namespace Microsoft.AspNetCore.Razor.Language.Extensions;
 
internal sealed class DefaultTagHelperTargetExtension : IDefaultTagHelperTargetExtension
{
    private static readonly ImmutableArray<string> s_fieldUninitializedModifiers = ["0649"];
    private static readonly ImmutableArray<string> s_fieldUnusedModifiers = ["0169"];
    private static readonly ImmutableArray<string> s_privateModifiers = ["private"];
 
    public string RunnerVariableName { get; set; } = "__tagHelperRunner";
 
    public string StringValueBufferVariableName { get; set; } = "__tagHelperStringValueBuffer";
 
    public string CreateTagHelperMethodName { get; set; } = "CreateTagHelper";
 
    public string ExecutionContextTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext";
 
    public string ExecutionContextVariableName { get; set; } = "__tagHelperExecutionContext";
 
    public string ExecutionContextAddMethodName { get; set; } = "Add";
 
    public string TagHelperRunnerTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner";
 
    public string ExecutionContextOutputPropertyName { get; set; } = "Output";
 
    public string ExecutionContextSetOutputContentAsyncMethodName { get; set; } = "SetOutputContentAsync";
 
    public string ExecutionContextAddHtmlAttributeMethodName { get; set; } = "AddHtmlAttribute";
 
    public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } = "AddTagHelperAttribute";
 
    public string RunnerRunAsyncMethodName { get; set; } = "RunAsync";
 
    public string ScopeManagerTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager";
 
    public string ScopeManagerVariableName { get; set; } = "__tagHelperScopeManager";
 
    public string ScopeManagerBeginMethodName { get; set; } = "Begin";
 
    public string ScopeManagerEndMethodName { get; set; } = "End";
 
    public string StartTagHelperWritingScopeMethodName { get; set; } = "StartTagHelperWritingScope";
 
    public string EndTagHelperWritingScopeMethodName { get; set; } = "EndTagHelperWritingScope";
 
    public string TagModeTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode";
 
    public string HtmlAttributeValueStyleTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle";
 
    public string TagHelperOutputIsContentModifiedPropertyName { get; set; } = "IsContentModified";
 
    public string BeginAddHtmlAttributeValuesMethodName { get; set; } = "BeginAddHtmlAttributeValues";
 
    public string EndAddHtmlAttributeValuesMethodName { get; set; } = "EndAddHtmlAttributeValues";
 
    public string BeginWriteTagHelperAttributeMethodName { get; set; } = "BeginWriteTagHelperAttribute";
 
    public string EndWriteTagHelperAttributeMethodName { get; set; } = "EndWriteTagHelperAttribute";
 
    public string MarkAsHtmlEncodedMethodName { get; set; } = "Html.Raw";
 
    public string FormatInvalidIndexerAssignmentMethodName { get; set; } = "InvalidTagHelperIndexerAssignment";
 
    public string WriteTagHelperOutputMethod { get; set; } = "Write";
 
    public void WriteTagHelperBody(CodeRenderingContext context, DefaultTagHelperBodyIntermediateNode node)
    {
        if (context.Parent as TagHelperIntermediateNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }
 
        if (context.Options.DesignTime)
        {
            context.RenderChildren(node);
        }
        else
        {
            // Call into the tag helper scope manager to start a new tag helper scope.
            // Also capture the value as the current execution context.
            context.CodeWriter
                .WriteStartAssignment(ExecutionContextVariableName)
                .WriteStartInstanceMethodInvocation(
                    ScopeManagerVariableName,
                    ScopeManagerBeginMethodName);
 
            // Assign a unique ID for this instance of the source HTML tag. This must be unique
            // per call site, e.g. if the tag is on the view twice, there should be two IDs.
            var uniqueId = GetDeterministicId(context);
            
            context.CodeWriter.WriteStringLiteral(node.TagName)
                .WriteParameterSeparator()
                .Write($"{TagModeTypeName}.{node.TagMode}")
                .WriteParameterSeparator()
                .WriteStringLiteral(uniqueId)
                .WriteParameterSeparator();
 
            using (context.CodeWriter.BuildAsyncLambda())
            {
                // We remove and redirect writers so TagHelper authors can retrieve content.
                context.RenderChildren(node, RuntimeNodeWriter.Instance);
            }
 
            context.CodeWriter.WriteEndMethodInvocation();
        }
    }
 
    public void WriteTagHelperCreate(CodeRenderingContext context, DefaultTagHelperCreateIntermediateNode node)
    {
        if (context.Parent as TagHelperIntermediateNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }
 
        context.CodeWriter
            .WriteStartAssignment(node.FieldName)
            .Write(CreateTagHelperMethodName)
            .WriteLine($"<global::{node.TypeName}>();");
 
        if (!context.Options.DesignTime)
        {
            context.CodeWriter.WriteInstanceMethodInvocation(
                ExecutionContextVariableName,
                ExecutionContextAddMethodName,
                node.FieldName);
        }
    }
 
    public void WriteTagHelperExecute(CodeRenderingContext context, DefaultTagHelperExecuteIntermediateNode node)
    {
        if (context.Parent as TagHelperIntermediateNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }
 
        // We always render `await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);` to notify users of the requirement for a method
        // to be asynchronous.
 
        context.CodeWriter
            .Write("await ")
            .WriteStartInstanceMethodInvocation(
                RunnerVariableName,
                RunnerRunAsyncMethodName)
            .Write(ExecutionContextVariableName)
            .WriteEndMethodInvocation();
 
        if (!context.Options.DesignTime)
        {
            var tagHelperOutputAccessor = $"{ExecutionContextVariableName}.{ExecutionContextOutputPropertyName}";
 
            context.CodeWriter
                .WriteLine($"if (!{tagHelperOutputAccessor}.{TagHelperOutputIsContentModifiedPropertyName})");
 
            using (context.CodeWriter.BuildScope())
            {
                context.CodeWriter
                    .Write("await ")
                    .WriteInstanceMethodInvocation(
                        ExecutionContextVariableName,
                        ExecutionContextSetOutputContentAsyncMethodName);
            }
 
            context.CodeWriter
                .WriteStartMethodInvocation(WriteTagHelperOutputMethod)
                .Write(tagHelperOutputAccessor)
                .WriteEndMethodInvocation()
                .WriteStartAssignment(ExecutionContextVariableName)
                .WriteInstanceMethodInvocation(
                    ScopeManagerVariableName,
                    ScopeManagerEndMethodName);
        }
    }
 
    public void WriteTagHelperHtmlAttribute(CodeRenderingContext context, DefaultTagHelperHtmlAttributeIntermediateNode node)
    {
        if (context.Parent as TagHelperIntermediateNode == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }
 
        if (context.Options.DesignTime)
        {
            context.RenderChildren(node);
        }
        else
        {
            var attributeValueStyleParameter = $"{HtmlAttributeValueStyleTypeName}.{node.AttributeStructure}";
            var isConditionalAttributeValue = node.Children.Any(
                child => child is CSharpExpressionAttributeValueIntermediateNode || child is CSharpCodeAttributeValueIntermediateNode);
 
            // All simple text and minimized attributes will be pre-allocated.
            if (isConditionalAttributeValue)
            {
                // Dynamic attribute value should be run through the conditional attribute removal system. It's
                // unbound and contains C#.
 
                // TagHelper attribute rendering is buffered by default. We do not want to write to the current
                // writer.
                var valuePieceCount = node.Children.Count(
                    child =>
                        child is HtmlAttributeValueIntermediateNode ||
                        child is CSharpExpressionAttributeValueIntermediateNode ||
                        child is CSharpCodeAttributeValueIntermediateNode ||
                        child is ExtensionIntermediateNode);
 
                context.CodeWriter
                    .WriteStartMethodInvocation(BeginAddHtmlAttributeValuesMethodName)
                    .Write(ExecutionContextVariableName)
                    .WriteParameterSeparator()
                    .WriteStringLiteral(node.AttributeName)
                    .WriteParameterSeparator()
                    .WriteIntegerLiteral(valuePieceCount)
                    .WriteParameterSeparator()
                    .Write(attributeValueStyleParameter)
                    .WriteEndMethodInvocation();
 
                context.RenderChildren(node, TagHelperHtmlAttributeRuntimeNodeWriter.Instance);
 
                context.CodeWriter
                    .WriteMethodInvocation(
                        EndAddHtmlAttributeValuesMethodName,
                        ExecutionContextVariableName);
            }
            else
            {
                // This is a data-* attribute which includes C#. Do not perform the conditional attribute removal or
                // other special cases used when IsDynamicAttributeValue(). But the attribute must still be buffered to
                // determine its final value.
 
                // Attribute value is not plain text, must be buffered to determine its final value.
                context.CodeWriter.WriteMethodInvocation(BeginWriteTagHelperAttributeMethodName);
 
                // We're building a writing scope around the provided chunks which captures everything written from the
                // page. Therefore, we do not want to write to any other buffer since we're using the pages buffer to
                // ensure we capture all content that's written, directly or indirectly.
                context.RenderChildren(node, RuntimeNodeWriter.Instance);
 
                context.CodeWriter
                    .WriteStartAssignment(StringValueBufferVariableName)
                    .WriteMethodInvocation(EndWriteTagHelperAttributeMethodName)
                    .WriteStartInstanceMethodInvocation(
                        ExecutionContextVariableName,
                        ExecutionContextAddHtmlAttributeMethodName)
                    .WriteStringLiteral(node.AttributeName)
                    .WriteParameterSeparator()
                    .WriteStartMethodInvocation(MarkAsHtmlEncodedMethodName)
                    .Write(StringValueBufferVariableName)
                    .WriteEndMethodInvocation(endLine: false)
                    .WriteParameterSeparator()
                    .Write(attributeValueStyleParameter)
                    .WriteEndMethodInvocation();
            }
        }
    }
 
    public void WriteTagHelperProperty(CodeRenderingContext context, DefaultTagHelperPropertyIntermediateNode node)
    {
        var tagHelperNode = context.Parent as TagHelperIntermediateNode;
        if (context.Parent == null)
        {
            var message = Resources.FormatIntermediateNodes_InvalidParentNode(node.GetType(), typeof(TagHelperIntermediateNode));
            throw new InvalidOperationException(message);
        }
 
        if (!context.Options.DesignTime)
        {
            // Ensure that the property we're trying to set has initialized its dictionary bound properties.
            if (node.IsIndexerNameMatch &&
                object.ReferenceEquals(FindFirstUseOfIndexer(tagHelperNode, node), node))
            {
                // Throw a reasonable Exception at runtime if the dictionary property is null.
                context.CodeWriter
                    .WriteLine($"if ({node.FieldName}.{node.PropertyName} == null)");
                using (context.CodeWriter.BuildScope())
                {
                    // System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName
                    // of InvalidOperationException type.
                    context.CodeWriter
                        .Write("throw ")
                        .WriteStartNewObject(nameof(InvalidOperationException))
                        .WriteStartMethodInvocation(FormatInvalidIndexerAssignmentMethodName)
                        .WriteStringLiteral(node.AttributeName)
                        .WriteParameterSeparator()
                        .WriteStringLiteral(node.TagHelper.TypeName)
                        .WriteParameterSeparator()
                        .WriteStringLiteral(node.PropertyName)
                        .WriteEndMethodInvocation(endLine: false)   // End of method call
                        .WriteEndMethodInvocation();   // End of new expression / throw statement
                }
            }
        }
 
        // If this is not the first use of the attribute value, we need to evaluate the expression and assign it to
        // the tag helper property.
        //
        // Otherwise, the value has already been computed and assigned to another tag helper. We just need to
        // copy from that tag helper to this one.
        //
        // This is important because we can't evaluate the expression twice due to side-effects.
        var firstUseOfAttribute = FindFirstUseOfAttribute(tagHelperNode, node);
        if (!object.ReferenceEquals(firstUseOfAttribute, node))
        {
            // If we get here, this value has already been used. We just need to copy the value.
            WritePropertyAccessorStartAssignment(context.CodeWriter, node);
 
            WritePropertyAccessor(context.CodeWriter, firstUseOfAttribute)
                .WriteLine(";");
 
            return;
        }
 
        // If we get there, this is the first time seeing this property so we need to evaluate the expression.
        if (node.BoundAttribute.ExpectsStringValue(node.AttributeName))
        {
            if (context.Options.DesignTime)
            {
                context.RenderChildren(node);
 
                WritePropertyAccessorStartAssignment(context.CodeWriter, node);
                if (node.Children.Count == 1 && node.Children.First() is HtmlContentIntermediateNode htmlNode)
                {
                    var content = GetContent(htmlNode);
                    context.CodeWriter.WriteStringLiteral(content);
                }
                else
                {
                    context.CodeWriter.Write("string.Empty");
                }
                context.CodeWriter.WriteLine(";");
            }
            else
            {
                context.CodeWriter.WriteMethodInvocation(BeginWriteTagHelperAttributeMethodName);
 
                context.RenderChildren(node, LiteralRuntimeNodeWriter.Instance);
 
                context.CodeWriter
                    .WriteStartAssignment(StringValueBufferVariableName)
                    .WriteMethodInvocation(EndWriteTagHelperAttributeMethodName);
 
                WritePropertyAccessorStartAssignment(context.CodeWriter, node)
                    .WriteLine($"{StringValueBufferVariableName};");
            }
        }
        else
        {
            if (context.Options.DesignTime)
            {
                var firstMappedChild = node.Children.FirstOrDefault(child => child.Source != null) as IntermediateNode;
                var valueStart = firstMappedChild?.Source;
 
                using (context.BuildLinePragma(node.Source))
                {
                    var accessorLength = GetPropertyAccessorLength(node);
                    var assignmentPrefixLength = accessorLength + " = ".Length;
                    if (node.BoundAttribute.IsEnum &&
                        node.Children is [CSharpIntermediateToken token])
                    {
                        assignmentPrefixLength += $"global::{node.BoundAttribute.TypeName}.".Length;
 
                        if (valueStart != null)
                        {
                            context.CodeWriter.WritePadding(assignmentPrefixLength, node.Source, context);
                        }
 
                        WritePropertyAccessorStartAssignment(context.CodeWriter, node)
                            .Write($"global::{node.BoundAttribute.TypeName}.");
                    }
                    else
                    {
                        if (valueStart != null)
                        {
                            context.CodeWriter.WritePadding(assignmentPrefixLength, node.Source, context);
                        }
 
                        WritePropertyAccessorStartAssignment(context.CodeWriter, node);
                    }
 
                    if (node.Children.Count == 0 &&
                        node.AttributeStructure == AttributeStructure.Minimized &&
                        node.BoundAttribute.ExpectsBooleanValue(node.AttributeName))
                    {
                        // If this is a minimized boolean attribute, set the value to true.
                        context.CodeWriter.Write("true");
                    }
                    else
                    {
                        RenderTagHelperAttributeInline(context, node, node.Source);
                    }
 
                    context.CodeWriter.WriteLine(";");
                }
            }
            else
            {
                WritePropertyAccessorStartAssignment(context.CodeWriter, node);
 
                if (node.BoundAttribute.IsEnum &&
                    node.Children is [CSharpIntermediateToken token])
                {
                    context.CodeWriter
                        .Write($"global::{node.BoundAttribute.TypeName}.");
                }
 
                if (node.Children.Count == 0 &&
                    node.AttributeStructure == AttributeStructure.Minimized &&
                    node.BoundAttribute.ExpectsBooleanValue(node.AttributeName))
                {
                    // If this is a minimized boolean attribute, set the value to true.
                    context.CodeWriter.Write("true");
                }
                else
                {
                    RenderTagHelperAttributeInline(context, node, node.Source);
                }
 
                context.CodeWriter.WriteLine(";");
            }
        }
 
        if (!context.Options.DesignTime)
        {
            // We need to inform the context of the attribute value.
            context.CodeWriter
                .WriteStartInstanceMethodInvocation(
                    ExecutionContextVariableName,
                    ExecutionContextAddTagHelperAttributeMethodName)
                .WriteStringLiteral(node.AttributeName)
                .WriteParameterSeparator();
 
            WritePropertyAccessor(context.CodeWriter, node)
                .WriteParameterSeparator()
                .Write($"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}")
                .WriteEndMethodInvocation();
        }
    }
 
    public void WriteTagHelperRuntime(CodeRenderingContext context, DefaultTagHelperRuntimeIntermediateNode node)
    {
        context.CodeWriter.WriteLine("#line hidden");
        context.CodeWriter.WriteField(s_fieldUninitializedModifiers, s_privateModifiers, ExecutionContextTypeName, ExecutionContextVariableName);
 
        context.CodeWriter
            .WriteLine($"private {TagHelperRunnerTypeName} {RunnerVariableName} = new {TagHelperRunnerTypeName}();");
 
        if (!context.Options.DesignTime)
        {
            context.CodeWriter.WriteField(s_fieldUnusedModifiers, s_privateModifiers, "string", StringValueBufferVariableName);
 
            var backedScopeManageVariableName = "__backed" + ScopeManagerVariableName;
            context.CodeWriter
                .Write("private ")
                .WriteVariableDeclaration(
                    ScopeManagerTypeName,
                    backedScopeManageVariableName,
                    value: null);
 
            context.CodeWriter
                .WriteLine($"private {ScopeManagerTypeName} {ScopeManagerVariableName}");
 
            using (context.CodeWriter.BuildScope())
            {
                context.CodeWriter.WriteLine("get");
                using (context.CodeWriter.BuildScope())
                {
                    context.CodeWriter
                        .WriteLine($"if ({backedScopeManageVariableName} == null)");
 
                    using (context.CodeWriter.BuildScope())
                    {
                        context.CodeWriter
                            .WriteStartAssignment(backedScopeManageVariableName)
                            .WriteStartNewObject(ScopeManagerTypeName)
                            .Write(StartTagHelperWritingScopeMethodName)
                            .WriteParameterSeparator()
                            .Write(EndTagHelperWritingScopeMethodName)
                            .WriteEndMethodInvocation();
                    }
 
                    context.CodeWriter
                        .WriteLine($"return {backedScopeManageVariableName};");
                }
            }
        }
    }
 
    private void RenderTagHelperAttributeInline(
        CodeRenderingContext context,
        DefaultTagHelperPropertyIntermediateNode property,
        SourceSpan? span)
    {
        for (var i = 0; i < property.Children.Count; i++)
        {
            RenderTagHelperAttributeInline(context, property, property.Children[i], span);
        }
    }
 
    // Internal for testing
    internal void RenderTagHelperAttributeInline(
        CodeRenderingContext context,
        DefaultTagHelperPropertyIntermediateNode property,
        IntermediateNode node,
        SourceSpan? span)
    {
        if (node is CSharpExpressionIntermediateNode || node is HtmlContentIntermediateNode)
        {
            for (var i = 0; i < node.Children.Count; i++)
            {
                RenderTagHelperAttributeInline(context, property, node.Children[i], span);
            }
        }
        else if (node is IntermediateToken token)
        {
            if (context.Options.DesignTime)
            {
                if (node.Source != null)
                {
                    context.AddSourceMappingFor(node);
                }
 
                context.CodeWriter.Write(token.Content);
            }
            else
            {
                using (context.BuildEnhancedLinePragma(token.Source))
                {
                    context.CodeWriter.Write(token.Content);
                }
            }
        }
        else if (node is CSharpCodeIntermediateNode)
        {
            var diagnostic = RazorDiagnosticFactory.CreateTagHelper_CodeBlocksNotSupportedInAttributes(span);
            context.AddDiagnostic(diagnostic);
        }
        else if (node is TemplateIntermediateNode)
        {
            var expectedTypeName = property.IsIndexerNameMatch ? property.BoundAttribute.IndexerTypeName : property.BoundAttribute.TypeName;
            var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InlineMarkupBlocksNotSupportedInAttributes(span, expectedTypeName);
            context.AddDiagnostic(diagnostic);
        }
    }
 
    private static DefaultTagHelperPropertyIntermediateNode FindFirstUseOfIndexer(
        TagHelperIntermediateNode tagHelperNode,
        DefaultTagHelperPropertyIntermediateNode propertyNode)
    {
        Debug.Assert(tagHelperNode.Children.Contains(propertyNode));
        Debug.Assert(propertyNode.IsIndexerNameMatch);
 
        for (var i = 0; i < tagHelperNode.Children.Count; i++)
        {
            if (tagHelperNode.Children[i] is DefaultTagHelperPropertyIntermediateNode otherPropertyNode &&
                otherPropertyNode.TagHelper.Equals(propertyNode.TagHelper) &&
                otherPropertyNode.BoundAttribute.Equals(propertyNode.BoundAttribute) &&
                otherPropertyNode.IsIndexerNameMatch)
            {
                return otherPropertyNode;
            }
        }
 
        // This is unreachable, we should find 'propertyNode' in the list of children.
        throw new InvalidOperationException();
    }
 
    private static DefaultTagHelperPropertyIntermediateNode FindFirstUseOfAttribute(
        TagHelperIntermediateNode tagHelperNode,
        DefaultTagHelperPropertyIntermediateNode propertyNode)
    {
        for (var i = 0; i < tagHelperNode.Children.Count; i++)
        {
            if (tagHelperNode.Children[i] is DefaultTagHelperPropertyIntermediateNode otherPropertyNode &&
                string.Equals(otherPropertyNode.AttributeName, propertyNode.AttributeName, StringComparison.Ordinal))
            {
                return otherPropertyNode;
            }
        }
 
        // This is unreachable, we should find 'propertyNode' in the list of children.
        throw new InvalidOperationException();
    }
 
    private string GetContent(HtmlContentIntermediateNode node)
    {
        var builder = new StringBuilder();
        for (var i = 0; i < node.Children.Count; i++)
        {
            if (node.Children[i] is HtmlIntermediateToken token)
            {
                builder.Append(token.Content);
            }
        }
 
        return builder.ToString();
    }
 
    // Internal for testing
    internal static string GetDeterministicId(CodeRenderingContext context)
    {
        var uniqueId = context.Options.SuppressUniqueIds;
        if (uniqueId is null)
        {
            // Use the file checksum along with the absolute position in the generated code to create a unique id for each tag helper call site.
            var checksum = ChecksumUtilities.BytesToString(context.SourceDocument.Text.GetChecksum());
            uniqueId = checksum + context.CodeWriter.Location.AbsoluteIndex;
        }
        return uniqueId;
    }
 
    private static int GetPropertyAccessorLength(DefaultTagHelperPropertyIntermediateNode node)
    {
        var propertyAccessorLength =
            node.FieldName.Length
            + ".".Length
            + node.PropertyName.Length;
 
        if (node.IsIndexerNameMatch)
        {
            propertyAccessorLength +=
                "[\"".Length
                + (node.AttributeName.Length - node.BoundAttribute.IndexerNamePrefix.Length)
                + "\"]".Length;
        }
 
        return propertyAccessorLength;
    }
 
    private static CodeWriter WritePropertyAccessor(CodeWriter writer, DefaultTagHelperPropertyIntermediateNode node)
    {
        writer
            .Write($"{node.FieldName}.{node.PropertyName}");
 
        if (node.IsIndexerNameMatch)
        {
            var dictionaryKey = node.AttributeName.AsMemory()[node.BoundAttribute.IndexerNamePrefix.Length..];
 
            writer
                .Write($"[\"{dictionaryKey}\"]");
        }
 
        return writer;
    }
 
    private static CodeWriter WritePropertyAccessorStartAssignment(CodeWriter writer, DefaultTagHelperPropertyIntermediateNode node)
    {
        return WritePropertyAccessor(writer, node)
            .Write(" = ");
    }
}