File: Language\Legacy\TagHelperBlockRewriter.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.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
 
internal static class TagHelperBlockRewriter
{
    public static TagMode GetTagMode(
        MarkupStartTagSyntax startTag,
        MarkupEndTagSyntax endTag,
        TagHelperBinding bindingResult)
    {
        var childSpan = startTag.GetLastToken().Parent;
 
        // Self-closing tags are always valid despite descriptors[X].TagStructure.
        if (childSpan?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false)
        {
            return TagMode.SelfClosing;
        }
 
        var hasDirectiveAttribute = false;
        foreach (var boundRulesInfo in bindingResult.AllBoundRules)
        {
            var nonDefaultRule = boundRulesInfo.Rules.FirstOrDefault(static rule => rule.TagStructure != TagStructure.Unspecified);
 
            if (nonDefaultRule?.TagStructure == TagStructure.WithoutEndTag)
            {
                return TagMode.StartTagOnly;
            }
 
            // Directive attribute will tolerate forms that don't work for tag helpers. For instance:
            //
            // <input @onclick="..."> vs <input onclick="..." />
            //
            // We don't want this to become an error just because you added a directive attribute.
            var descriptor = boundRulesInfo.Descriptor;
            if (descriptor.IsAnyComponentDocumentTagHelper() && !descriptor.IsComponentOrChildContentTagHelper())
            {
                hasDirectiveAttribute = true;
            }
        }
 
        if (hasDirectiveAttribute && startTag.IsVoidElement() && endTag == null)
        {
            return TagMode.StartTagOnly;
        }
 
        return TagMode.StartTagAndEndTag;
    }
 
    public static MarkupTagHelperStartTagSyntax Rewrite(
        string tagName,
        RazorParserOptions options,
        MarkupStartTagSyntax startTag,
        TagHelperBinding binding,
        ErrorSink errorSink,
        RazorSourceDocument source)
    {
        var processedBoundAttributeNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        using PooledArrayBuilder<RazorSyntaxNode> attributeBuilder = [];
 
        var attributes = startTag.Attributes;
 
        for (var i = 0; i < startTag.Attributes.Count; i++)
        {
            var isMinimized = false;
            var attributeNameLocation = SourceLocation.Undefined;
            var child = startTag.Attributes[i];
            TryParseResult result;
            if (child is MarkupAttributeBlockSyntax attributeBlock)
            {
                attributeNameLocation = attributeBlock.Name.GetSourceLocation(source);
                result = TryParseAttribute(
                    tagName,
                    attributeBlock,
                    binding.TagHelpers,
                    errorSink,
                    processedBoundAttributeNames,
                    options);
                attributeBuilder.Add(result.RewrittenAttribute);
            }
            else if (child is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock)
            {
                isMinimized = true;
                attributeNameLocation = minimizedAttributeBlock.Name.GetSourceLocation(source);
                result = TryParseMinimizedAttribute(
                    tagName,
                    minimizedAttributeBlock,
                    binding.TagHelpers,
                    errorSink,
                    processedBoundAttributeNames);
                attributeBuilder.Add(result.RewrittenAttribute);
            }
            else if (child is MarkupMiscAttributeContentSyntax miscContent)
            {
                foreach (var contentChild in miscContent.Children)
                {
                    if (contentChild is CSharpCodeBlockSyntax codeBlock)
                    {
                        // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96.
                        // Something like:
                        // <input @checked />
                        var location = new SourceSpan(codeBlock.GetSourceLocation(source), codeBlock.Width);
                        var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
                        errorSink.OnError(diagnostic);
                        break;
                    }
                    else
                    {
                        // If the original span content was whitespace it ultimately means the tag
                        // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
                        // ex: <myTH class="btn"| |
                        var literalContent = contentChild.GetContent();
                        if (!string.IsNullOrWhiteSpace(literalContent))
                        {
                            var location = contentChild.GetSourceSpan(source);
                            var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperAttributeListMustBeWellFormed(location);
                            errorSink.OnError(diagnostic);
                            break;
                        }
                    }
                }
 
                result = null;
            }
            else if (child is RazorCommentBlockSyntax razorComment)
            {
                // Razor comments in attribute lists should be preserved but not treated as attributes.
                // Continue processing subsequent attributes.
                attributeBuilder.Add(razorComment);
                continue;
            }
            else if (child is MarkupTextLiteralSyntax textLiteral)
            {
                // Whitespace between attributes should be preserved but not treated as attributes.
                // Continue processing subsequent attributes.
                var content = textLiteral.GetContent();
                if (string.IsNullOrWhiteSpace(content))
                {
                    attributeBuilder.Add(textLiteral);
                    continue;
                }
 
                result = null;
            }
            else if (child is MarkupEphemeralTextLiteralSyntax ephemeralLiteral)
            {
                // Ephemeral literals (like escaped @@ in attribute names) should be preserved.
                // Continue processing subsequent attributes.
                attributeBuilder.Add(ephemeralLiteral);
                continue;
            }
            else
            {
                result = null;
            }
 
            // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
            if (result == null)
            {
                // Error occurred while parsing the attribute. Don't try parsing the rest to avoid misleading errors.
                for (var j = i; j < startTag.Attributes.Count; j++)
                {
                    attributeBuilder.Add(startTag.Attributes[j]);
                }
 
                break;
            }
 
            // Check if it's a non-boolean bound attribute that is minimized or if it's a bound
            // non-string attribute that has null or whitespace content.
            var isValidMinimizedAttribute = options.AllowMinimizedBooleanTagHelperAttributes && result.IsBoundBooleanAttribute;
            if ((isMinimized &&
                result.IsBoundAttribute &&
                !isValidMinimizedAttribute) ||
                (!isMinimized &&
                result.IsBoundNonStringAttribute &&
                 string.IsNullOrWhiteSpace(GetAttributeValueContent(result.RewrittenAttribute))))
            {
                var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                var propertyTypeName = GetPropertyType(result.AttributeName, binding.TagHelpers);
                var diagnostic = RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute(errorLocation, result.AttributeName, tagName, propertyTypeName);
                errorSink.OnError(diagnostic);
            }
 
            // Check if the attribute was a prefix match for a tag helper dictionary property but the
            // dictionary key would be the empty string.
            if (result.IsMissingDictionaryKey)
            {
                var errorLocation = new SourceSpan(attributeNameLocation, result.AttributeName.Length);
                var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey(errorLocation, result.AttributeName, tagName);
                errorSink.OnError(diagnostic);
            }
        }
 
        if (attributeBuilder.Count > 0)
        {
            // This means we rewrote something. Use the new set of attributes.
            attributes = attributeBuilder.ToList();
        }
 
        return SyntaxFactory.MarkupTagHelperStartTag(
            startTag.OpenAngle,
            startTag.Bang,
            startTag.Name,
            attributes,
            startTag.ForwardSlash,
            startTag.CloseAngle,
            startTag.ChunkGenerator,
            startTag.EditHandler);
    }
 
    private static TryParseResult TryParseMinimizedAttribute(
        string tagName,
        MarkupMinimizedAttributeBlockSyntax attributeBlock,
        TagHelperCollection tagHelpers,
        ErrorSink errorSink,
        HashSet<string> processedBoundAttributeNames)
    {
        // Have a name now. Able to determine correct isBoundNonStringAttribute value.
        var result = CreateTryParseResult(attributeBlock.Name.GetContent(), tagHelpers, processedBoundAttributeNames);
 
        result.AttributeStructure = AttributeStructure.Minimized;
 
        if (result.IsDirectiveAttribute)
        {
            // Directive attributes have a different syntax.
            result.RewrittenAttribute = RewriteToMinimizedDirectiveAttribute(attributeBlock, result);
 
            return result;
        }
        else
        {
            var rewritten = SyntaxFactory.MarkupMinimizedTagHelperAttribute(
                attributeBlock.NamePrefix,
                attributeBlock.Name,
                new TagHelperAttributeInfo(
                    result.AttributeName,
                    parameterName: null,
                    result.AttributeStructure,
                    result.IsBoundAttribute,
                    isDirectiveAttribute: false));
 
            result.RewrittenAttribute = rewritten;
 
            return result;
        }
    }
 
    private static TryParseResult TryParseAttribute(
        string tagName,
        MarkupAttributeBlockSyntax attributeBlock,
        TagHelperCollection tagHelpers,
        ErrorSink errorSink,
        HashSet<string> processedBoundAttributeNames,
        RazorParserOptions options)
    {
        // Have a name now. Able to determine correct isBoundNonStringAttribute value.
        var result = CreateTryParseResult(attributeBlock.Name.GetContent(), tagHelpers, processedBoundAttributeNames);
 
        if (attributeBlock.ValuePrefix == null)
        {
            // We are purposefully not persisting NoQuotes even for unbound attributes because it is still possible to
            // rewrite the values that introduces a space like in UrlResolutionTagHelper.
            // The other case is it could be an expression, treat NoQuotes and DoubleQuotes equivalently. We purposefully do not persist NoQuotes
            // ValueStyles at code generation time to protect users from rendering dynamic content with spaces
            // that can break attributes.
            // Ex: <tag my-attribute=@value /> where @value results in the test "hello world".
            // This way, the above code would render <tag my-attribute="hello world" />.
            result.AttributeStructure = AttributeStructure.DoubleQuotes;
        }
        else
        {
            var lastToken = attributeBlock.ValuePrefix.GetLastToken();
            switch (lastToken.Kind)
            {
                case SyntaxKind.DoubleQuote:
                    result.AttributeStructure = AttributeStructure.DoubleQuotes;
                    break;
                case SyntaxKind.SingleQuote:
                    result.AttributeStructure = AttributeStructure.SingleQuotes;
                    break;
                default:
                    result.AttributeStructure = AttributeStructure.Minimized;
                    break;
            }
        }
 
        var attributeValue = attributeBlock.Value;
        if (attributeValue == null)
        {
            using PooledArrayBuilder<RazorSyntaxNode> builder = [];
 
            // Add a marker for attribute value when there are no quotes like, <p class= >
            builder.Add(SyntaxFactory.MarkupTextLiteral(literalTokens: default));
 
            attributeValue = SyntaxFactory.GenericBlock(builder.ToList());
        }
        var rewrittenValue = RewriteAttributeValue(result, attributeValue, options);
 
        if (result.IsDirectiveAttribute)
        {
            // Directive attributes have a different syntax.
            result.RewrittenAttribute = RewriteToDirectiveAttribute(attributeBlock, result, rewrittenValue);
 
            return result;
        }
        else
        {
            var rewritten = SyntaxFactory.MarkupTagHelperAttribute(
                attributeBlock.NamePrefix,
                attributeBlock.Name,
                attributeBlock.NameSuffix,
                attributeBlock.EqualsToken,
                attributeBlock.ValuePrefix,
                rewrittenValue,
                attributeBlock.ValueSuffix,
                new TagHelperAttributeInfo(
                    result.AttributeName,
                    parameterName: null,
                    result.AttributeStructure,
                    result.IsBoundAttribute,
                    isDirectiveAttribute: false));
 
            result.RewrittenAttribute = rewritten;
 
            return result;
        }
    }
 
    private static MarkupTagHelperDirectiveAttributeSyntax RewriteToDirectiveAttribute(
        MarkupAttributeBlockSyntax attributeBlock,
        TryParseResult result,
        MarkupTagHelperAttributeValueSyntax rewrittenValue)
    {
        //
        // Consider, <Foo @bind:param="..." />
        // We're now going to rewrite @bind:param from a regular MarkupAttributeBlock to a MarkupTagHelperDirectiveAttribute.
        // We need to split the name "@bind:param" into four parts,
        // @ - Transition (MetaCode)
        // bind - Name (Text)
        // : - Colon (MetaCode)
        // param - ParameterName (Text)
        //
        var attributeName = result.AttributeName;
        var attributeNameSyntax = attributeBlock.Name;
        var transition = SyntaxFactory.RazorMetaCode(SyntaxFactory.MissingToken(SyntaxKind.Transition));
        RazorMetaCodeSyntax colon = null;
        MarkupTextLiteralSyntax parameterName = null;
        if (attributeName.StartsWith("@", StringComparison.Ordinal))
        {
            attributeName = attributeName.Substring(1);
            var attributeNameToken = SyntaxFactory.Token(SyntaxKind.Text, attributeName);
            attributeNameSyntax = SyntaxFactory.MarkupTextLiteral(attributeNameToken);
 
            var transitionToken = SyntaxFactory.Token(SyntaxKind.Transition, "@");
            transition = SyntaxFactory.RazorMetaCode(transitionToken);
        }
 
        if (attributeName.IndexOf(':') != -1)
        {
            var segments = attributeName.Split(new[] { ':' }, 2);
 
            var attributeNameToken = SyntaxFactory.Token(SyntaxKind.Text, segments[0]);
            attributeNameSyntax = SyntaxFactory.MarkupTextLiteral(attributeNameToken);
 
            var colonToken = SyntaxFactory.Token(SyntaxKind.Colon, ":");
            colon = SyntaxFactory.RazorMetaCode(colonToken);
 
            var parameterNameToken = SyntaxFactory.Token(SyntaxKind.Text, segments[1]);
            parameterName = SyntaxFactory.MarkupTextLiteral(parameterNameToken);
        }
 
        return SyntaxFactory.MarkupTagHelperDirectiveAttribute(
            attributeBlock.NamePrefix,
            transition,
            attributeNameSyntax,
            colon,
            parameterName,
            attributeBlock.NameSuffix,
            attributeBlock.EqualsToken,
            attributeBlock.ValuePrefix,
            rewrittenValue,
            attributeBlock.ValueSuffix,
            new TagHelperAttributeInfo(
                result.AttributeName,
                parameterName?.GetContent(),
                result.AttributeStructure,
                result.IsBoundAttribute,
                isDirectiveAttribute: true));
    }
 
    private static MarkupMinimizedTagHelperDirectiveAttributeSyntax RewriteToMinimizedDirectiveAttribute(
        MarkupMinimizedAttributeBlockSyntax attributeBlock,
        TryParseResult result)
    {
        //
        // Consider, <Foo @bind:param />
        // We're now going to rewrite @bind:param from a regular MarkupAttributeBlock to a MarkupTagHelperDirectiveAttribute.
        // We need to split the name "@bind:param" into four parts,
        // @ - Transition (MetaCode)
        // bind - Name (Text)
        // : - Colon (MetaCode)
        // param - ParameterName (Text)
        //
        var attributeName = result.AttributeName;
        var attributeNameSyntax = attributeBlock.Name;
        var transition = SyntaxFactory.RazorMetaCode(SyntaxFactory.MissingToken(SyntaxKind.Transition));
        RazorMetaCodeSyntax colon = null;
        MarkupTextLiteralSyntax parameterName = null;
        if (attributeName.StartsWith("@", StringComparison.Ordinal))
        {
            attributeName = attributeName.Substring(1);
            var attributeNameToken = SyntaxFactory.Token(SyntaxKind.Text, attributeName);
            attributeNameSyntax = SyntaxFactory.MarkupTextLiteral(attributeNameToken);
 
            var transitionToken = SyntaxFactory.Token(SyntaxKind.Transition, "@");
            transition = SyntaxFactory.RazorMetaCode(transitionToken);
        }
 
        if (attributeName.IndexOf(':') != -1)
        {
            var segments = attributeName.Split(new[] { ':' }, 2);
 
            var attributeNameToken = SyntaxFactory.Token(SyntaxKind.Text, segments[0]);
            attributeNameSyntax = SyntaxFactory.MarkupTextLiteral(attributeNameToken);
 
            var colonToken = SyntaxFactory.Token(SyntaxKind.Colon, ":");
            colon = SyntaxFactory.RazorMetaCode(colonToken);
 
            var parameterNameToken = SyntaxFactory.Token(SyntaxKind.Text, segments[1]);
            parameterName = SyntaxFactory.MarkupTextLiteral(parameterNameToken);
        }
 
        return SyntaxFactory.MarkupMinimizedTagHelperDirectiveAttribute(
            attributeBlock.NamePrefix,
            transition,
            attributeNameSyntax,
            colon,
            parameterName,
            new TagHelperAttributeInfo(
                result.AttributeName,
                parameterName?.GetContent(),
                result.AttributeStructure,
                result.IsBoundAttribute,
                isDirectiveAttribute: true));
    }
 
    private static MarkupTagHelperAttributeValueSyntax RewriteAttributeValue(TryParseResult result, RazorBlockSyntax attributeValue, RazorParserOptions options)
    {
        var rewriter = new AttributeValueRewriter(result, options);
        var rewrittenValue = attributeValue;
        if (result.IsBoundAttribute)
        {
            // If the attribute was requested by a tag helper but the corresponding property was not a
            // string, then treat its value as code. A non-string value can be any C# value so we need
            // to ensure the tree reflects that.
            rewrittenValue = (RazorBlockSyntax)rewriter.Visit(attributeValue);
        }
 
        return SyntaxFactory.MarkupTagHelperAttributeValue(rewrittenValue.Children);
    }
 
    // Determines the full name of the Type of the property corresponding to an attribute with the given name.
    private static string GetPropertyType(string name, TagHelperCollection tagHelpers)
    {
        foreach (var tagHelper in tagHelpers)
        {
            if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(tagHelper, name, out var match))
            {
                return match.IsIndexerMatch
                    ? match.Attribute.IndexerTypeName
                    : match.Attribute.TypeName;
            }
        }
 
        return null;
    }
 
    // Create a TryParseResult for given name, filling in binding details.
    private static TryParseResult CreateTryParseResult(
        string name,
        TagHelperCollection tagHelpers,
        HashSet<string> processedBoundAttributeNames)
    {
        var isBoundAttribute = false;
        var isBoundNonStringAttribute = false;
        var isBoundBooleanAttribute = false;
        var isMissingDictionaryKey = false;
        var isDirectiveAttribute = false;
        var isDuplicateAttribute = false;
 
        foreach (var tagHelper in tagHelpers)
        {
            if (TagHelperMatchingConventions.TryGetFirstBoundAttributeMatch(tagHelper, name, out var match))
            {
                isBoundAttribute = true;
                isBoundNonStringAttribute = !match.ExpectsStringValue;
                isBoundBooleanAttribute = match.ExpectsBooleanValue;
 
                if (!match.IsParameterMatch)
                {
                    isMissingDictionaryKey = match.Attribute.IndexerNamePrefix?.Length == name.Length;
                }
 
                isDirectiveAttribute = match.Attribute.IsDirectiveAttribute;
 
                if (!processedBoundAttributeNames.Add(name))
                {
                    // A bound attribute with the same name has already been processed.
                    isDuplicateAttribute = true;
                }
 
                break;
            }
        }
 
        return new TryParseResult
        {
            AttributeName = name,
            IsBoundAttribute = isBoundAttribute,
            IsBoundNonStringAttribute = isBoundNonStringAttribute,
            IsBoundBooleanAttribute = isBoundBooleanAttribute,
            IsMissingDictionaryKey = isMissingDictionaryKey,
            IsDuplicateAttribute = isDuplicateAttribute,
            IsDirectiveAttribute = isDirectiveAttribute
        };
    }
 
    private static string GetAttributeValueContent(RazorSyntaxNode attributeBlock)
    {
        if (attributeBlock is MarkupTagHelperAttributeSyntax tagHelperAttribute)
        {
            return tagHelperAttribute.Value?.GetContent();
        }
        else if (attributeBlock is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute)
        {
            return directiveAttribute.Value?.GetContent();
        }
        else if (attributeBlock is MarkupAttributeBlockSyntax attribute)
        {
            return attribute.Value?.GetContent();
        }
 
        return null;
    }
 
    private class AttributeValueRewriter : SyntaxRewriter
    {
        private readonly TryParseResult _tryParseResult;
        private bool _rewriteAsMarkup;
        private readonly RazorParserOptions _options;
 
        public AttributeValueRewriter(TryParseResult result, RazorParserOptions options)
        {
            _tryParseResult = result;
            _options = options;
        }
 
        public override SyntaxNode VisitGenericBlock(GenericBlockSyntax node)
        {
            if (_tryParseResult.IsBoundNonStringAttribute && CanBeCollapsed(node))
            {
                using var builder = new PooledArrayBuilder<SyntaxToken>();
                builder.AddRange(node.DescendantTokens());
 
                var tokens = builder.ToList();
 
                if (tokens is [{ Kind: SyntaxKind.Transition } transition, ..])
                {
                    // Lift the transition token so it is not part of the generated output.
                    tokens = tokens.RemoveAt(0);
                    var children = createExpressionLiteral(tokens);
                    return SyntaxFactory.MarkupBlock(
                        [SyntaxFactory.CSharpCodeBlock(
                            [SyntaxFactory.CSharpImplicitExpression(
                                SyntaxFactory.CSharpTransition(transition),
                                SyntaxFactory.CSharpImplicitExpressionBody(
                                    SyntaxFactory.CSharpCodeBlock(children)))])]);
                }
 
                return node.Update(createExpressionLiteral(tokens));
            }
 
            return base.VisitGenericBlock(node);
 
            SyntaxList<RazorSyntaxNode> createExpressionLiteral(SyntaxTokenList tokens)
            {
                var expression = SyntaxFactory.CSharpExpressionLiteral(tokens);
                var rewrittenExpression = (CSharpExpressionLiteralSyntax)VisitCSharpExpressionLiteral(expression);
                return [rewrittenExpression];
            }
        }
 
        public override SyntaxNode VisitCSharpTransition(CSharpTransitionSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitCSharpTransition(node);
            }
 
            // For bound non-string attributes, we'll only allow a transition span to appear at the very
            // beginning of the attribute expression. All later transitions would appear as code so that
            // they are part of the generated output. E.g.
            // key="@value" -> MyTagHelper.key = value
            // key=" @value" -> MyTagHelper.key =  @value
            // key="1 + @case" -> MyTagHelper.key = 1 + @case
            // key="@int + @case" -> MyTagHelper.key = int + @case
            // key="@(a + b) -> MyTagHelper.key = a + b
            // key="4 + @(a + b)" -> MyTagHelper.key = 4 + @(a + b)
            if (_rewriteAsMarkup)
            {
                // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output.
                var expression = SyntaxFactory.CSharpExpressionLiteral(node.Transition, MarkupChunkGenerator.Instance, node.EditHandler);
 
                return base.VisitCSharpExpressionLiteral(expression);
            }
 
            _rewriteAsMarkup = true;
            return base.VisitCSharpTransition(node);
        }
 
        public override SyntaxNode VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
        {
            if (_rewriteAsMarkup)
            {
                using PooledArrayBuilder<RazorSyntaxNode> builder = [];
 
                var rewrittenBody = (CSharpCodeBlockSyntax)VisitCSharpCodeBlock(((CSharpImplicitExpressionBodySyntax)node.Body).CSharpCode);
 
                // The transition needs to be considered part of the first token in the rewritten body, so we update the first child to include it.
                // This ensures that, when we create tracking spans and write out the final C# code, the `@` is not treated as a separate, separable
                // token from the content that follows it.
                var firstChild = rewrittenBody.Children[0];
                var firstToken = firstChild.GetFirstToken();
                var newFirstToken = SyntaxFactory.Token(firstToken.Kind, node.Transition.Transition.Content + firstToken.Content);
 
                var newFirstChild = firstChild.ReplaceToken(firstToken, newFirstToken);
                builder.AddRange(rewrittenBody.Children.Replace(firstChild, newFirstChild));
 
                // Since the original transition is part of the body, we need something to take it's place.
                var transition = SyntaxFactory.CSharpTransition(SyntaxFactory.MissingToken(SyntaxKind.Transition));
 
                var rewrittenCodeBlock = SyntaxFactory.CSharpCodeBlock(builder.ToList());
                return SyntaxFactory.CSharpImplicitExpression(transition, SyntaxFactory.CSharpImplicitExpressionBody(rewrittenCodeBlock));
            }
 
            return base.VisitCSharpImplicitExpression(node);
        }
 
        public override SyntaxNode VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
        {
            CSharpTransitionSyntax transition;
            using PooledArrayBuilder<RazorSyntaxNode> builder = [];
 
            if (_rewriteAsMarkup)
            {
                // Convert transition.
                // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output.
                // This is bad code, since @( is never valid C#, so we don't worry about trying to stitch the @ and the ( together.
                var editHandler = _options.EnableSpanEditHandlers
                    ? SpanEditHandler.GetDefault(AcceptedCharactersInternal.Any)
                    : null;
 
                var expression = SyntaxFactory.CSharpExpressionLiteral(node.Transition.Transition, MarkupChunkGenerator.Instance, editHandler);
                expression = (CSharpExpressionLiteralSyntax)VisitCSharpExpressionLiteral(expression);
                builder.Add(expression);
 
                // Since the original transition is part of the body, we need something to take it's place.
                transition = SyntaxFactory.CSharpTransition(SyntaxFactory.MissingToken(SyntaxKind.Transition));
 
                var body = (CSharpExplicitExpressionBodySyntax)node.Body;
                var rewrittenOpenParen = (RazorSyntaxNode)VisitRazorMetaCode(body.OpenParen);
                var rewrittenBody = (CSharpCodeBlockSyntax)VisitCSharpCodeBlock(body.CSharpCode);
                var rewrittenCloseParen = (RazorSyntaxNode)VisitRazorMetaCode(body.CloseParen);
                builder.Add(rewrittenOpenParen);
                builder.AddRange(rewrittenBody.Children);
                builder.Add(rewrittenCloseParen);
            }
            else
            {
                // This is the first expression of a non-string attribute like attr=@(a + b)
                // Below code converts this to an implicit expression to make the parens
                // part of the expression so that it is rendered.
                transition = (CSharpTransitionSyntax)Visit(node.Transition);
                var body = (CSharpExplicitExpressionBodySyntax)node.Body;
                var rewrittenOpenParen = (RazorSyntaxNode)VisitRazorMetaCode(body.OpenParen);
                var rewrittenBody = (CSharpCodeBlockSyntax)VisitCSharpCodeBlock(body.CSharpCode);
                var rewrittenCloseParen = (RazorSyntaxNode)VisitRazorMetaCode(body.CloseParen);
                builder.Add(rewrittenOpenParen);
                builder.AddRange(rewrittenBody.Children);
                builder.Add(rewrittenCloseParen);
            }
 
            var rewrittenCodeBlock = SyntaxFactory.CSharpCodeBlock(builder.ToList());
            return SyntaxFactory.CSharpImplicitExpression(transition, SyntaxFactory.CSharpImplicitExpressionBody(rewrittenCodeBlock));
        }
 
        public override SyntaxNode VisitRazorMetaCode(RazorMetaCodeSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitRazorMetaCode(node);
            }
 
            if (_rewriteAsMarkup)
            {
                // Change to a MarkupChunkGenerator so that the '@' \ parenthesis is generated as part of the output.
                var expression = SyntaxFactory.CSharpExpressionLiteral(node.MetaCode, MarkupChunkGenerator.Instance, node.EditHandler);
 
                return VisitCSharpExpressionLiteral(expression);
            }
 
            _rewriteAsMarkup = true;
            return base.VisitRazorMetaCode(node);
        }
 
        public override SyntaxNode VisitCSharpStatement(CSharpStatementSyntax node)
        {
            // We don't support code blocks inside tag helper attributes. Don't rewrite anything inside a code block.
            // E.g, <p age="@{1 + 2}"> is not supported.
            return node;
        }
 
        public override SyntaxNode VisitRazorUsingDirective(RazorUsingDirectiveSyntax node)
        {
            // We don't support directives inside tag helper attributes. Don't rewrite anything inside a directive.
            return node;
        }
 
        public override SyntaxNode VisitRazorDirective(RazorDirectiveSyntax node)
        {
            // We don't support directives inside tag helper attributes. Don't rewrite anything inside a directive.
            // E.g, <p age="@functions { }"> is not supported.
            return node;
        }
 
        public override SyntaxNode VisitMarkupElement(MarkupElementSyntax node)
        {
            // We're visiting an attribute value. If we encounter a MarkupElement this means the attribute value is invalid.
            // We don't want to rewrite anything here.
            // E.g, <my age="@if (true) { <my4 age=... }"></my4>
            return node;
        }
 
        public override SyntaxNode VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitCSharpExpressionLiteral(node);
            }
 
            node = (CSharpExpressionLiteralSyntax)ConfigureNonStringAttribute(node);
 
            _rewriteAsMarkup = true;
            return base.VisitCSharpExpressionLiteral(node);
        }
 
        public override SyntaxNode VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
        {
            using PooledArrayBuilder<SyntaxToken> builder = [];
 
            if (node.Prefix != null)
            {
                builder.AddRange(node.Prefix.LiteralTokens);
            }
 
            if (node.Value != null)
            {
                builder.AddRange(node.Value.LiteralTokens);
            }
 
            if (_tryParseResult.IsBoundNonStringAttribute)
            {
                _rewriteAsMarkup = true;
                // Since this is a bound non-string attribute, we want to convert LiteralAttributeValue to just be a CSharp Expression literal.
                var expression = SyntaxFactory.CSharpExpressionLiteral(builder.ToList());
                return VisitCSharpExpressionLiteral(expression);
            }
            else
            {
                var literal = SyntaxFactory.MarkupTextLiteral(builder.ToList(), node.Value?.ChunkGenerator, node.Value?.EditHandler);
 
                return Visit(literal);
            }
        }
 
        public override SyntaxNode VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node)
        {
            // Move the prefix to be part of the actual value.
            using PooledArrayBuilder<RazorSyntaxNode> builder = [];
 
            if (node.Prefix != null)
            {
                builder.Add(node.Prefix);
            }
 
            if (node.Value?.Children != null)
            {
                builder.AddRange(node.Value.Children);
            }
 
            var rewrittenValue = SyntaxFactory.MarkupBlock(builder.ToList());
 
            return base.VisitMarkupBlock(rewrittenValue);
        }
 
        public override SyntaxNode VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitCSharpStatementLiteral(node);
            }
 
            _rewriteAsMarkup = true;
            return base.VisitCSharpStatementLiteral(node);
        }
 
        public override SyntaxNode VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitMarkupTextLiteral(node);
            }
 
            _rewriteAsMarkup = true;
            node = (MarkupTextLiteralSyntax)ConfigureNonStringAttribute(node);
            return SyntaxFactory.CSharpExpressionLiteral(node.LiteralTokens, node.ChunkGenerator, node.EditHandler);
        }
 
        public override SyntaxNode VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node)
        {
            if (!_tryParseResult.IsBoundNonStringAttribute)
            {
                return base.VisitMarkupEphemeralTextLiteral(node);
            }
 
            // Since this is a non-string attribute we need to rewrite this as code.
            // Rewriting it to CSharpEphemeralTextLiteral so that it is not rendered to output.
            _rewriteAsMarkup = true;
            node = (MarkupEphemeralTextLiteralSyntax)ConfigureNonStringAttribute(node);
            return SyntaxFactory.CSharpEphemeralTextLiteral(node.LiteralTokens, node.ChunkGenerator, node.EditHandler);
        }
 
        // Being collapsed represents that a block contains several identical looking markup literal attribute values. This can be the case
        // when a user has written something like: @onclick="() => SomeMethod()"
        // In that case there would be 3 children:
        //   - ()
        //   -  =>
        //   -  SomeMethod()
        // There are 3 children because the Razor parser separates attribute values based on whitespace.
        private bool CanBeCollapsed(GenericBlockSyntax node)
        {
            if (node.Children.Count <= 1)
            {
                // The node is either already collapsed or has no children.
                return false;
            }
 
            for (var i = 0; i < node.Children.Count; i++)
            {
                var kind = node.Children[i].Kind;
                if (kind != SyntaxKind.MarkupLiteralAttributeValue &&
                    // We only want to collapse dynamic values if we're in a legacy file.
                    // Mixed C#/HTML content is not allowed in components.
                    (kind != SyntaxKind.MarkupDynamicAttributeValue || !_options.FileKind.IsLegacy()))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private SyntaxNode ConfigureNonStringAttribute(SyntaxNode node)
        {
            var context = node.GetEditHandler();
            var builder = _options.EnableSpanEditHandlers
                ? new SpanEditHandlerBuilder(defaultLanguageTokenizer: null)
                {
                    Tokenizer = context?.Tokenizer,
                    Factory = (acceptedCharacters, tokenizer) => new ImplicitExpressionEditHandler
                    {
                        Tokenizer = tokenizer,
                        AcceptedCharacters = acceptedCharacters,
                        AcceptTrailingDot = true,
                        Keywords = CSharpCodeParser.DefaultKeywords
                    }
                }
                : null;
 
            var originalGenerator = node.GetChunkGenerator();
            var newGenerator = originalGenerator ?? SpanChunkGenerator.Null;
            if (!_tryParseResult.IsDuplicateAttribute && originalGenerator != null && originalGenerator != SpanChunkGenerator.Null)
            {
                // We want to mark the value of non-string bound attributes to be CSharp.
                // Except in two cases,
                // 1. Cases when we don't want to render the span. Eg: Transition span '@'.
                // 2. Cases when it is a duplicate of a bound attribute. This should just be rendered as html.
 
                newGenerator = new ExpressionChunkGenerator();
            }
 
            context = builder?.Build(AcceptedCharactersInternal.AnyExceptNewline);
 
            if (originalGenerator != newGenerator)
            {
                return (node as ILegacySyntax)?.Update(newGenerator, context)
                    ?? Assumed.Unreachable<SyntaxNode>($"Unexpected node type {node.Kind}");
            }
            else
            {
                return (node as ILegacySyntax)?.WithEditHandler(context)
                    ?? Assumed.Unreachable<SyntaxNode>($"Unexpected node type {node.Kind}");
            }
        }
    }
 
    private class TryParseResult
    {
        public string AttributeName { get; set; }
 
        public RazorSyntaxNode RewrittenAttribute { get; set; }
 
        public AttributeStructure AttributeStructure { get; set; }
 
        public bool IsBoundAttribute { get; set; }
 
        public bool IsBoundNonStringAttribute { get; set; }
 
        public bool IsBoundBooleanAttribute { get; set; }
 
        public bool IsMissingDictionaryKey { get; set; }
 
        public bool IsDuplicateAttribute { get; set; }
 
        public bool IsDirectiveAttribute { get; set; }
    }
}