File: Language\DefaultRazorIntermediateNodeLoweringPhase.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.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
/// <summary>
/// Converts the Razor syntax tree into intermediate representation (IR) nodes. Runs before
/// <see cref="TagHelperResolutionPhase"/>, so elements that might be tag helpers are represented
/// as unresolved nodes (<see cref="UnresolvedElementIntermediateNode"/>,
/// <see cref="UnresolvedAttributeIntermediateNode"/>).
/// </summary>
/// <remarks>
/// Pre-computes fallback forms on unresolved nodes so the resolution phase can resolve them
/// without accessing the syntax tree. Three visitor subclasses handle different file kinds:
/// <see cref="LegacyFileKindVisitor"/> (.cshtml), <see cref="ComponentFileKindVisitor"/> (.razor),
/// and <see cref="ComponentImportFileKindVisitor"/> (_Imports.razor).
/// </remarks>
internal class DefaultRazorIntermediateNodeLoweringPhase : RazorEnginePhaseBase, IRazorIntermediateNodeLoweringPhase
{
    protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument, CancellationToken cancellationToken)
    {
        // The canonical syntax tree is established by DefaultRazorTagHelperContextDiscoveryPhase,
        // which always runs before this phase in the pipeline.
        var syntaxTree = codeDocument.GetSyntaxTree();
        ThrowForMissingDocumentDependency(syntaxTree);
 
        var documentNode = new DocumentIntermediateNode();
        var builder = IntermediateNodeBuilder.Create(documentNode);
 
        documentNode.Options = codeDocument.CodeGenerationOptions;
 
        // The import documents should be inserted logically before the main document.
        var imports = codeDocument.GetImportSyntaxTrees();
        var importedUsings = !imports.IsEmpty
            ? ImportDirectives(documentNode, builder, syntaxTree.Options, imports)
            : [];
 
        // Lower the main document, appending after the imported directives.
        //
        // We need to decide up front if this document is a "component" file. This will affect how
        // lowering behaves.
        LoweringVisitor visitor;
        if (codeDocument.FileKind.IsComponentImport() &&
            syntaxTree.Options.AllowComponentFileKind)
        {
            visitor = new ComponentImportFileKindVisitor(documentNode, builder, syntaxTree.Options)
            {
                SourceDocument = syntaxTree.Source,
            };
 
            visitor.Visit(syntaxTree.Root);
        }
        else if (codeDocument.FileKind.IsComponent() &&
            syntaxTree.Options.AllowComponentFileKind)
        {
            visitor = new ComponentFileKindVisitor(documentNode, builder, syntaxTree.Options)
            {
                SourceDocument = syntaxTree.Source,
            };
 
            visitor.Visit(syntaxTree.Root);
        }
        else
        {
            visitor = new LegacyFileKindVisitor(documentNode, builder, syntaxTree.Options)
            {
                SourceDocument = syntaxTree.Source,
            };
 
            visitor.Visit(syntaxTree.Root);
        }
 
        // 1. Prioritize non-imported usings over imported ones.
        // 2. Don't import usings that already exist in primary document.
        // 3. Allow duplicate usings in primary document (C# warning).
        using var _ = ListPool<UsingReference>.GetPooledObject(out var usingReferences);
        usingReferences.AddRange(visitor.Usings);
 
        for (var j = importedUsings.Count - 1; j >= 0; j--)
        {
            var importedUsing = importedUsings[j];
            if (!usingReferences.Contains(importedUsing) &&
                // If the using is from the default import, avoid adding it
                // if a user using exists which is the same except for the `global::` prefix.
                (!TryRemoveGlobalPrefixFromDefaultUsing(in importedUsing, out var trimmedUsingNamespace) ||
                !Contains(usingReferences, trimmedUsingNamespace)))
            {
                usingReferences.Insert(0, importedUsing);
            }
        }
 
        // In each lowering piece above, namespaces were tracked. We render them here to ensure every
        // lowering action has a chance to add a source location to a namespace. Ultimately, closest wins.
        var index = 0;
 
        UsingDirectiveIntermediateNode lastDirective = null;
        foreach (var reference in usingReferences)
        {
            var @using = new UsingDirectiveIntermediateNode()
            {
                Content = reference.Namespace,
                Source = reference.Source,
                HasExplicitSemicolon = reference.HasExplicitSemicolon
            };
 
            builder.Insert(index++, @using);
 
            lastDirective = @using;
        }
 
        if (lastDirective is not null)
        {
            // Using directives can be emitted without "#line hidden" regions between them, to allow Roslyn to add
            // new directives as necessary, but we want to append one on the last using, so things go back to
            // normal for whatever comes next.
            lastDirective.AppendLineDefaultAndHidden = true;
        }
 
        PostProcessImportedDirectives(documentNode);
 
        // The document should contain all errors that currently exist in the system. This involves
        // adding the errors from the primary and imported syntax trees.
        foreach (var diagnostic in syntaxTree.Diagnostics)
        {
            documentNode.AddDiagnostic(diagnostic);
        }
 
        foreach (var import in imports)
        {
            foreach (var diagnostic in import.Diagnostics)
            {
                documentNode.AddDiagnostic(diagnostic);
            }
        }
 
        return codeDocument.WithDocumentNode(documentNode);
 
        static bool TryRemoveGlobalPrefixFromDefaultUsing(in UsingReference usingReference, out ReadOnlySpan<char> trimmedNamespace)
        {
            const string globalPrefix = "global::";
            if (usingReference.Source is { FilePath: null } && // the default import has null file path
                usingReference.Namespace.StartsWith(globalPrefix, StringComparison.Ordinal))
            {
                trimmedNamespace = usingReference.Namespace.AsSpan()[globalPrefix.Length..];
                return true;
            }
            trimmedNamespace = default;
            return false;
        }
 
        static bool Contains(List<UsingReference> usingReferences, ReadOnlySpan<char> usingNamespace)
        {
            foreach (var usingReference in usingReferences)
            {
                if (usingReference.Equals(usingNamespace))
                {
                    return true;
                }
            }
            return false;
        }
    }
 
    /// <summary>
    /// Determines whether a tag name looks like it could be a component name (starts with uppercase).
    /// </summary>
    internal static bool LooksLikeAComponentName(DocumentIntermediateNode document, string startTagName)
    {
        var category = char.GetUnicodeCategory(startTagName, 0);
 
        return category is System.Globalization.UnicodeCategory.UppercaseLetter ||
            (document.Options.SupportLocalizedComponentNames &&
                (category is System.Globalization.UnicodeCategory.TitlecaseLetter or System.Globalization.UnicodeCategory.OtherLetter));
    }
 
    private static IReadOnlyList<UsingReference> ImportDirectives(
        DocumentIntermediateNode document,
        IntermediateNodeBuilder builder,
        RazorParserOptions options,
        ImmutableArray<RazorSyntaxTree> imports)
    {
        Debug.Assert(!imports.IsDefaultOrEmpty);
 
        var importsVisitor = new ImportsVisitor(document, builder, options);
        foreach (var import in imports)
        {
            importsVisitor.SourceDocument = import.Source;
            importsVisitor.Visit(import.Root);
        }
 
        return importsVisitor.Usings;
    }
 
    private static void PostProcessImportedDirectives(DocumentIntermediateNode document)
    {
        using var _ = SpecializedPools.GetPooledReferenceEqualityHashSet<DirectiveDescriptor>(out var seenDirectives);
        var references = document.FindDescendantReferences<DirectiveIntermediateNode>();
 
        for (var i = references.Length - 1; i >= 0; i--)
        {
            var reference = references[i];
            var directive = reference.Node;
            var descriptor = directive.Directive;
            var seenDirective = !seenDirectives.Add(descriptor);
 
            if (!directive.IsImported)
            {
                continue;
            }
 
            switch (descriptor.Kind)
            {
                case DirectiveKind.SingleLine:
                    {
                        if (seenDirective && descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
                        {
                            // This directive has been overridden, it should be removed from the document.
                            break;
                        }
 
                        continue;
                    }
 
                case DirectiveKind.RazorBlock:
                case DirectiveKind.CodeBlock:
                    {
                        if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
                        {
                            // A block directive cannot be imported.
                            document.AddDiagnostic(
                                RazorDiagnosticFactory.CreateDirective_BlockDirectiveCannotBeImported(descriptor.Directive));
                        }
 
                        break;
                    }
 
                default:
                    throw new InvalidOperationException(Resources.FormatUnexpectedDirectiveKind(typeof(DirectiveKind).FullName));
            }
 
            // Overridden and invalid imported directives make it to here. They should be removed from the document.
 
            reference.Remove();
        }
    }
 
    private struct UsingReference : IEquatable<UsingReference>
    {
        public UsingReference(string @namespace, SourceSpan? source, bool hasExplicitSemicolon)
        {
            Namespace = @namespace;
            Source = source;
            HasExplicitSemicolon = hasExplicitSemicolon;
        }
        public string Namespace { get; }
 
        public SourceSpan? Source { get; }
 
        public bool HasExplicitSemicolon { get; }
 
        public override bool Equals(object other)
        {
            if (other is UsingReference reference)
            {
                return Equals(reference);
            }
 
            return false;
        }
        public bool Equals(UsingReference other)
        {
            return string.Equals(Namespace, other.Namespace, StringComparison.Ordinal);
        }
 
        public readonly bool Equals(ReadOnlySpan<char> otherNamespace)
        {
            return Namespace.AsSpan().Equals(otherNamespace, StringComparison.Ordinal);
        }
 
        public override int GetHashCode() => Namespace.GetHashCode();
    }
 
    /// <summary>
    /// Base visitor implementing shared lowering logic for all file kinds.
    /// Subclasses (<see cref="LegacyFileKindVisitor"/>, <see cref="ComponentFileKindVisitor"/>,
    /// <see cref="ComponentImportFileKindVisitor"/>) override element/attribute visitors to
    /// handle file-kind-specific semantics. Contains shared attribute value visitors, directive
    /// handling, and helper methods for source span computation.
    /// </summary>
    private class LoweringVisitor : SyntaxWalker
    {
        protected readonly IntermediateNodeBuilder _builder;
        protected readonly DocumentIntermediateNode _document;
        protected readonly List<UsingReference> _usings;
        protected readonly RazorParserOptions _options;
        /// <summary>
        /// True when currently lowering children of a unresolved attribute. Controls whether value
        /// visitors (e.g. <c>VisitCSharpExpressionLiteral</c>, <c>VisitMarkupTextLiteral</c>) produce
        /// unresolved-specific IR nodes. Set unconditionally (not just when true) to ensure correct
        /// reset between consecutive attributes.
        /// </summary>
        protected bool _insideUnresolvedAttribute;
 
        public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserOptions options)
        {
            _document = document;
            _builder = builder;
            _usings = new List<UsingReference>();
            _options = options;
        }
 
        public IReadOnlyList<UsingReference> Usings => _usings;
 
        public RazorSourceDocument SourceDocument { get; set; }
 
        public override void VisitRazorUsingDirective(RazorUsingDirectiveSyntax node)
        {
            VisitDirective(node, descriptor: null);
        }
 
        public override void VisitRazorDirective(RazorDirectiveSyntax node)
        {
            VisitDirective(node, node.DirectiveDescriptor);
        }
 
        private void VisitDirective(BaseRazorDirectiveSyntax node, DirectiveDescriptor descriptor)
        {
            IntermediateNode directiveNode;
 
            if (descriptor != null)
            {
                var diagnostics = node.GetDiagnostics();
 
                // This is an extensible directive.
                if (IsMalformed(diagnostics))
                {
                    directiveNode = new MalformedDirectiveIntermediateNode()
                    {
                        DirectiveName = descriptor.Directive,
                        Directive = descriptor,
                        Source = BuildSourceSpanFromNode(node),
                    };
                }
                else
                {
                    directiveNode = new DirectiveIntermediateNode()
                    {
                        DirectiveName = descriptor.Directive,
                        Directive = descriptor,
                        Source = BuildSourceSpanFromNode(node),
                    };
                }
 
                for (var i = 0; i < diagnostics.Length; i++)
                {
                    directiveNode.AddDiagnostic(diagnostics[i]);
                }
 
                _builder.Push(directiveNode);
            }
 
            Visit(node.Body);
 
            if (descriptor != null)
            {
                _builder.Pop();
            }
        }
 
        public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
        {
            switch (node.ChunkGenerator)
            {
                case null:
                    base.VisitCSharpStatementLiteral(node);
                    return;
                case DirectiveTokenChunkGenerator tokenChunkGenerator:
                    _builder.Add(new DirectiveTokenIntermediateNode()
                    {
                        Content = node.GetContent(),
                        DirectiveToken = tokenChunkGenerator.Descriptor,
                        Source = BuildSourceSpanFromNode(node),
                    });
                    break;
                case AddImportChunkGenerator importChunkGenerator:
                    var namespaceImport = importChunkGenerator.Namespace.Trim();
                    var namespaceSpan = BuildSourceSpanFromNode(node);
                    _usings.Add(new UsingReference(namespaceImport, namespaceSpan, importChunkGenerator.HasExplicitSemicolon));
                    break;
                case AddTagHelperChunkGenerator addTagHelperChunkGenerator:
                    {
                        IntermediateNode directiveNode;
                        if (IsMalformed(addTagHelperChunkGenerator.Diagnostics))
                        {
                            directiveNode = new MalformedDirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
                        else
                        {
                            directiveNode = new DirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
 
                        for (var i = 0; i < addTagHelperChunkGenerator.Diagnostics.Count; i++)
                        {
                            directiveNode.AddDiagnostic(addTagHelperChunkGenerator.Diagnostics[i]);
                        }
 
                        _builder.Push(directiveNode);
 
                        _builder.Add(new DirectiveTokenIntermediateNode()
                        {
                            Content = addTagHelperChunkGenerator.LookupText,
                            DirectiveToken = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Tokens[0],
                            Source = BuildSourceSpanFromNode(node),
                        });
 
                        _builder.Pop();
                        break;
                    }
                case RemoveTagHelperChunkGenerator removeTagHelperChunkGenerator:
                    {
                        IntermediateNode directiveNode;
                        if (IsMalformed(removeTagHelperChunkGenerator.Diagnostics))
                        {
                            directiveNode = new MalformedDirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
                        else
                        {
                            directiveNode = new DirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
 
                        for (var i = 0; i < removeTagHelperChunkGenerator.Diagnostics.Count; i++)
                        {
                            directiveNode.AddDiagnostic(removeTagHelperChunkGenerator.Diagnostics[i]);
                        }
 
                        _builder.Push(directiveNode);
 
                        _builder.Add(new DirectiveTokenIntermediateNode()
                        {
                            Content = removeTagHelperChunkGenerator.LookupText,
                            DirectiveToken = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Tokens[0],
                            Source = BuildSourceSpanFromNode(node),
                        });
 
                        _builder.Pop();
                        break;
                    }
                case TagHelperPrefixDirectiveChunkGenerator tagHelperPrefixChunkGenerator:
                    {
                        IntermediateNode directiveNode;
                        if (IsMalformed(tagHelperPrefixChunkGenerator.Diagnostics))
                        {
                            directiveNode = new MalformedDirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
                        else
                        {
                            directiveNode = new DirectiveIntermediateNode()
                            {
                                DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive,
                                Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
                                Source = BuildSourceSpanFromNode(node),
                            };
                        }
 
                        for (var i = 0; i < tagHelperPrefixChunkGenerator.Diagnostics.Count; i++)
                        {
                            directiveNode.AddDiagnostic(tagHelperPrefixChunkGenerator.Diagnostics[i]);
                        }
 
                        _builder.Push(directiveNode);
 
                        _builder.Add(new DirectiveTokenIntermediateNode()
                        {
                            Content = tagHelperPrefixChunkGenerator.Prefix,
                            DirectiveToken = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Tokens[0],
                            Source = BuildSourceSpanFromNode(node),
                        });
 
                        _builder.Pop();
                        break;
                    }
            }
 
            base.VisitCSharpStatementLiteral(node);
        }
 
        protected SourceSpan? BuildSourceSpanFromNode(SyntaxNode node)
        {
            if (node == null)
            {
                return null;
            }
 
            return node.GetSourceSpan(SourceDocument);
        }
 
        protected static AttributeStructure InferAttributeStructure(MarkupAttributeBlockSyntax node)
        {
            if (node.EqualsToken.Kind == SyntaxKind.None && node.Value == null)
            {
                return AttributeStructure.Minimized;
            }
 
            var lastToken = node.GetLastToken();
            if (lastToken.Kind != SyntaxKind.None)
            {
                if (lastToken.Content == "\"")
                {
                    return AttributeStructure.DoubleQuotes;
                }
 
                if (lastToken.Content == "'")
                {
                    return AttributeStructure.SingleQuotes;
                }
            }
 
            // Has an equals sign but no value/quotes (e.g. `type=`).
            // Treated as DoubleQuotes by convention.
            return AttributeStructure.DoubleQuotes;
        }
 
        protected static SyntaxTokenList MergeTokenLists(
            SyntaxTokenList? literal1,
            SyntaxTokenList? literal2,
            SyntaxTokenList? literal3 = null,
            SyntaxTokenList? literal4 = null,
            SyntaxTokenList? literal5 = null)
        {
            using var _ = ArrayPool<SyntaxTokenList>.Shared.GetPooledArraySpan(5, out var tokenLists);
            var tokenListsCount = 0;
            var count = 0;
 
            if (literal1 is { } tokens1)
            {
                tokenLists[tokenListsCount++] = tokens1;
                count += tokens1.Count;
            }
 
            if (literal2 is { } tokens2)
            {
                tokenLists[tokenListsCount++] = tokens2;
                count += tokens2.Count;
            }
 
            if (literal3 is { } tokens3)
            {
                tokenLists[tokenListsCount++] = tokens3;
                count += tokens3.Count;
            }
 
            if (literal4 is { } tokens4)
            {
                tokenLists[tokenListsCount++] = tokens4;
                count += tokens4.Count;
            }
 
            if (literal5 is { } tokens5)
            {
                tokenLists[tokenListsCount++] = tokens5;
                count += tokens5.Count;
            }
 
            if (count == 0)
            {
                return default;
            }
 
            using var builder = new PooledArrayBuilder<SyntaxToken>(count);
 
            foreach (var tokenList in tokenLists[..tokenListsCount])
            {
                builder.AddRange(tokenList);
            }
 
            return builder.ToList();
        }
 
        /// <summary>
        ///  Simple helper struct to simplify calling code that needs to skip elements
        ///  without resorting to LINQ.
        /// </summary>
        protected readonly struct ChildNodesHelper(ChildSyntaxList list, int start = 0)
        {
            public int Count { get; } = Math.Max(list.Count - start, 0);
 
            public SyntaxNodeOrToken this[int index] => list[start + index];
 
            public ChildNodesHelper Skip(int count)
            {
                return new ChildNodesHelper(list, start + count);
            }
 
            public SyntaxNodeOrToken FirstOrDefault() => Count > 0 ? this[0] : default;
 
            public bool TryCast<TNode>(out ImmutableArray<TNode> result)
            {
                // Note that this intentionally returns true for empty lists.
                // This behavior matches the expectations of code that previously called
                // ".All(x => x is TNode)" followed by ".Cast<TNode>()" via LINQ.
                // Because "All" would return true for empty lists, this method
                // needs to do the same.
 
                using var builder = new PooledArrayBuilder<TNode>(Count);
 
                for (var i = start; i < list.Count; i++)
                {
                    if (list[i].AsNode() is not TNode node)
                    {
                        result = default;
                        return false;
                    }
 
                    builder.Add(node);
                }
 
                result = builder.ToImmutableAndClear();
                return true;
            }
        }
 
        protected static MarkupTextLiteralSyntax MergeAttributeValue(MarkupLiteralAttributeValueSyntax node)
        {
            var valueTokens = MergeTokenLists(
                node.Prefix?.LiteralTokens,
                node.Value?.LiteralTokens);
 
            var rewritten = node.Prefix?.Update(valueTokens) ?? node.Value?.Update(valueTokens);
 
            rewritten = (MarkupTextLiteralSyntax)rewritten?.Green.CreateRed(node, node.Position);
 
            if (rewritten.EditHandler is { } originalEditHandler)
            {
                rewritten = rewritten.Update(rewritten.LiteralTokens, MarkupChunkGenerator.Instance, originalEditHandler);
            }
 
            return rewritten;
        }
 
        public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node)
        {
            var containsExpression = false;
 
            var descendantNodes = node.DescendantNodes(static n => n.Parent is not CSharpCodeBlockSyntax);
 
            foreach (var child in descendantNodes)
            {
                if (child is CSharpImplicitExpressionSyntax or CSharpExplicitExpressionSyntax)
                {
                    containsExpression = true;
                    break;
                }
            }
 
            if (_insideUnresolvedAttribute)
            {
                var unresolvedNode = new UnresolvedExpressionAttributeValueIntermediateNode()
                {
                    Prefix = node.Prefix?.GetContent() ?? string.Empty,
                    ContainsExpression = containsExpression,
                    Source = BuildSourceSpanFromNode(node),
                };
 
                _builder.Push(unresolvedNode);
                Visit(node.Value);
                _builder.Pop();
                return;
            }
 
            if (containsExpression)
            {
                _builder.Push(new CSharpExpressionAttributeValueIntermediateNode()
                {
                    Prefix = node.Prefix?.GetContent() ?? string.Empty,
                    Source = BuildSourceSpanFromNode(node),
                });
            }
            else
            {
                _builder.Push(new CSharpCodeAttributeValueIntermediateNode()
                {
                    Prefix = node.Prefix?.GetContent() ?? string.Empty,
                    Source = BuildSourceSpanFromNode(node),
                });
            }
 
            Visit(node.Value);
 
            _builder.Pop();
        }
 
        public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
        {
            if (_insideUnresolvedAttribute)
            {
                var unresolvedNode = new UnresolvedAttributeValueIntermediateNode()
                {
                    Prefix = node.Prefix?.GetContent() ?? string.Empty,
                    Source = BuildSourceSpanFromNode(node),
                };
 
                unresolvedNode.Children.Add(IntermediateNodeFactory.HtmlToken(
                    arg: node,
                    contentFactory: static node => node.Value?.GetContent() ?? string.Empty,
                    source: BuildSourceSpanFromNode(node.Value)));
 
                _builder.Add(unresolvedNode);
                return;
            }
 
            _builder.Push(new HtmlAttributeValueIntermediateNode()
            {
                Prefix = node.Prefix?.GetContent() ?? string.Empty,
                Source = BuildSourceSpanFromNode(node),
            });
 
            _builder.Add(IntermediateNodeFactory.HtmlToken(
                arg: node,
                contentFactory: static node => node.Value?.GetContent() ?? string.Empty,
                source: BuildSourceSpanFromNode(node.Value)));
 
            _builder.Pop();
        }
 
        public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node)
        {
            var templateNode = new TemplateIntermediateNode();
            _builder.Push(templateNode);
 
            base.VisitCSharpTemplateBlock(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(templateNode);
        }
 
        public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
        {
            if (_builder.Current is TagHelperHtmlAttributeIntermediateNode)
            {
                // If we are top level in a tag helper HTML attribute, we want to be rendered as markup.
                // This case happens for duplicate non-string bound attributes. They would be initially be categorized as
                // CSharp but since they are duplicate, they should just be markup.
                var markupLiteral = SyntaxFactory.MarkupTextLiteral(node.LiteralTokens).Green.CreateRed(node.Parent, node.Position);
                Visit(markupLiteral);
                return;
            }
 
            _builder.Add(IntermediateNodeFactory.CSharpToken(
                arg: node,
                contentFactory: static node => node.GetContent(),
                source: BuildSourceSpanFromNode(node)));
 
            base.VisitCSharpExpressionLiteral(node);
        }
 
        protected internal void VisitAttributeValue(SyntaxNode node)
        {
            if (node == null)
            {
                return;
            }
 
            var children = new ChildNodesHelper(node.ChildNodesAndTokens());
            var position = node.Position;
            SyntaxTokenList escapedAtTokens = default;
            if (children.FirstOrDefault().AsNode() is MarkupBlockSyntax { Children: [MarkupTextLiteralSyntax literalSyntax, MarkupEphemeralTextLiteralSyntax] })
            {
                // This is a special case when we have an attribute like attr="@@foo".
                // Extract the literal @ token from the first child so it can be merged with the rest of the attribute value.
                // The ephemeral @ token is ignored.
                escapedAtTokens = literalSyntax.LiteralTokens;
                children = children.Skip(1);
                position = children.Count > 0 ? children[0].Position : position;
            }
 
            if (children.TryCast<MarkupLiteralAttributeValueSyntax>(out var attributeLiteralArray))
            {
                using PooledArrayBuilder<SyntaxToken> builder = [];
 
                if (escapedAtTokens.Count > 0)
                {
                    builder.AddRange(escapedAtTokens);
                }
 
                foreach (var literal in attributeLiteralArray)
                {
                    var mergedValue = MergeAttributeValue(literal);
                    builder.AddRange(mergedValue.LiteralTokens);
                }
 
                var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList()).Green.CreateRed(node.Parent, position);
                Visit(rewritten);
            }
            else if (children.TryCast<MarkupTextLiteralSyntax>(out var markupLiteralArray))
            {
                using PooledArrayBuilder<SyntaxToken> builder = [];
 
                if (escapedAtTokens.Count > 0)
                {
                    builder.AddRange(escapedAtTokens);
                }
 
                foreach (var literal in markupLiteralArray)
                {
                    builder.AddRange(literal.LiteralTokens);
                }
 
                var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList()).Green.CreateRed(node.Parent, position);
                Visit(rewritten);
            }
            else if (children.TryCast<CSharpExpressionLiteralSyntax>(out var expressionLiteralArray))
            {
                using PooledArrayBuilder<SyntaxToken> builder = [];
 
                if (escapedAtTokens.Count > 0)
                {
                    builder.AddRange(escapedAtTokens);
                }
 
                SpanEditHandler editHandler = null;
                ISpanChunkGenerator generator = null;
                foreach (var literal in expressionLiteralArray)
                {
                    generator = literal.ChunkGenerator;
                    editHandler = literal.EditHandler;
                    builder.AddRange(literal.LiteralTokens);
                }
 
                var rewritten = SyntaxFactory.CSharpExpressionLiteral(builder.ToList(), generator, editHandler).Green.CreateRed(node.Parent, position);
                Visit(rewritten);
            }
            else
            {
                if (escapedAtTokens.Count > 0)
                {
                    // If we have escaped @ tokens but no other content to merge with,
                    // create a MarkupTextLiteral just for the escaped @ tokens
                    var rewritten = SyntaxFactory.MarkupTextLiteral(escapedAtTokens).Green.CreateRed(node.Parent, position);
                    Visit(rewritten);
                }
                else
                {
                    Visit(node);
                }
            }
        }
 
        protected void Combine(HtmlContentIntermediateNode node, SyntaxNode item)
        {
            node.Children.Add(IntermediateNodeFactory.HtmlToken(
                arg: item,
                contentFactory: static item => item.GetContent(),
                source: BuildSourceSpanFromNode(item)));
 
            if (node.Source is SourceSpan source)
            {
                node.Source = new SourceSpan(
                    source.FilePath,
                    source.AbsoluteIndex,
                    source.LineIndex,
                    source.CharacterIndex,
                    source.Length + item.Width,
                    source.LineCount,
                    source.EndCharacterIndex);
            }
        }
 
        /// <summary>
        /// Computes the source span covering just the attribute value content (between quotes).
        /// Handles three cases: non-empty value, empty value between quotes, and equals sign with
        /// no value node.
        /// </summary>
        protected SourceSpan? ComputeAttributeValueSourceSpan(MarkupAttributeBlockSyntax node)
        {
            if (node.Value != null)
            {
                var valueStart = node.ValuePrefix != null
                    ? node.ValuePrefix.EndPosition
                    : node.Value.Position;
                var valueEnd = node.ValueSuffix != null
                    ? node.ValueSuffix.Position
                    : node.Value.EndPosition;
                var valueLength = valueEnd - valueStart;
 
                if (valueLength > 0)
                {
                    var location = SourceDocument.Text.Lines.GetLinePosition(valueStart);
                    var endLocation = SourceDocument.Text.Lines.GetLinePosition(valueEnd);
                    return new SourceSpan(
                        SourceDocument.FilePath,
                        valueStart,
                        location.Line,
                        location.Character,
                        valueLength,
                        endLocation.Line - location.Line,
                        endLocation.Character);
                }
                else
                {
                    var emptyPos = node.Value.Position;
                    var location = SourceDocument.Text.Lines.GetLinePosition(emptyPos);
                    return new SourceSpan(
                        SourceDocument.FilePath,
                        emptyPos,
                        location.Line,
                        location.Character,
                        0,
                        0,
                        location.Character);
                }
            }
            else if (node.EqualsToken.Kind != SyntaxKind.None)
            {
                var valueStart = node.ValuePrefix != null
                    ? node.ValuePrefix.EndPosition
                    : node.EqualsToken.EndPosition;
                var location = SourceDocument.Text.Lines.GetLinePosition(valueStart);
                return new SourceSpan(
                    SourceDocument.FilePath,
                    valueStart,
                    location.Line,
                    location.Character,
                    0,
                    0,
                    location.Character);
            }
 
            return null;
        }
 
        /// <summary>
        /// Extracts attribute name/value pairs from an element's start tag for tag helper binding.
        /// Populates the data that <see cref="TagHelperMatchingConventions"/> uses to match tag helpers.
        /// </summary>
        protected static ImmutableArray<KeyValuePair<string, string>> ExtractAttributeData(MarkupElementSyntax node)
        {
            using var attrBuilder = new PooledArrayBuilder<KeyValuePair<string, string>>();
            if (node.MarkupStartTag != null)
            {
                foreach (var attr in node.MarkupStartTag.Attributes)
                {
                    if (attr is MarkupAttributeBlockSyntax attributeBlock)
                    {
                        attrBuilder.Add(new KeyValuePair<string, string>(
                            attributeBlock.Name.GetContent(),
                            attributeBlock.Value?.GetContent() ?? string.Empty));
                    }
                    else if (attr is MarkupMinimizedAttributeBlockSyntax minimizedAttr)
                    {
                        attrBuilder.Add(new KeyValuePair<string, string>(
                            minimizedAttr.Name.GetContent(), string.Empty));
                    }
                }
            }
 
            return attrBuilder.ToImmutable();
        }
 
        /// <summary>
        /// Aggregates child source spans into a parent source span. Uses the first child's position
        /// and sums all children's lengths. Applied to expression and template nodes after their
        /// children have been lowered.
        /// </summary>
        protected void ComputeSourceSpanFromChildren(IntermediateNode node)
        {
            if (node.Children.Count > 0)
            {
                var sourceRangeStart = node
                    .Children
                    .FirstOrDefault(child => child.Source != null)
                    ?.Source;
 
                if (sourceRangeStart != null)
                {
                    var contentLength = node.Children.Sum(child => child.Source?.Length ?? 0);
 
                    node.Source = new SourceSpan(
                        sourceRangeStart.Value.FilePath ?? SourceDocument.FilePath,
                        sourceRangeStart.Value.AbsoluteIndex,
                        sourceRangeStart.Value.LineIndex,
                        sourceRangeStart.Value.CharacterIndex,
                        contentLength,
                        sourceRangeStart.Value.LineCount,
                        sourceRangeStart.Value.EndCharacterIndex);
                }
            }
        }
    }
 
    /// <summary>
    /// Handles .cshtml files (MVC views, Razor Pages). Treats HTML markup as text content
    /// (<see cref="HtmlContentIntermediateNode"/> with merged tokens) and supports Tag Helpers.
    /// Elements inside potential tag helpers are unresolved via
    /// <see cref="UnresolvedElementIntermediateNode"/>.
    /// </summary>
    private class LegacyFileKindVisitor : LoweringVisitor
    {
        private bool _insideUnresolvedElement;
        public LegacyFileKindVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserOptions options)
            : base(document, builder, options)
        {
        }
 
        /// <summary>
        /// Lowers a markup element. Creates an <see cref="UnresolvedElementIntermediateNode"/> (unresolved)
        /// because any element could match a tag helper. Extracts attribute data for tag helper binding
        /// and sets <see cref="UnresolvedElementIntermediateNode.StartTagEndIndex"/>/<see cref="UnresolvedElementIntermediateNode.BodyEndIndex"/>
        /// for boundary tracking. Markup transitions (<c>@:</c> and <c>&lt;text&gt;</c>) are not tag
        /// helpers and fall through to the base visitor.
        /// </summary>
        public override void VisitMarkupElement(MarkupElementSyntax node)
        {
            // Markup transitions (e.g., @: or <text>) are not tag helpers and should
            // fall through to the base visitor. We only check StartTag here because
            // legacy files don't support markup transitions as end tags -- that scenario
            // is only relevant for component files (handled by ComponentFileKindVisitor).
            if (node.MarkupStartTag != null && node.MarkupStartTag.IsMarkupTransition)
            {
                base.VisitMarkupElement(node);
                return;
            }
 
            var tagName = node.MarkupStartTag?.Name.Content ?? node.MarkupEndTag?.Name.Content ?? string.Empty;
 
            var attributeData = ExtractAttributeData(node);
 
            var isSelfClosing = false;
            if (node.MarkupStartTag != null)
            {
                var lastToken = node.MarkupStartTag.GetLastToken();
                isSelfClosing = lastToken.Parent?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false;
            }
 
            var element = new UnresolvedElementIntermediateNode()
            {
                TagName = tagName,
                Source = BuildSourceSpanFromNode(node),
                IsComponent = false,
                IsEscaped = node.MarkupStartTag?.Bang is { Width: > 0 },
                IsSelfClosing = isSelfClosing,
                HasEndTag = node.MarkupEndTag != null,
                EndTagName = node.MarkupEndTag?.GetTagNameWithOptionalBang(),
                EndTagSpan = node.MarkupEndTag != null ? BuildSourceSpanFromNode(node.MarkupEndTag) : null,
                IsVoidElement = node.MarkupStartTag?.IsVoidElement() ?? false,
                StartTagNameSpan = node.MarkupStartTag?.Name.GetSourceSpan(SourceDocument),
                StartTagSpan = node.MarkupStartTag != null ? BuildSourceSpanFromNode(node.MarkupStartTag) : null,
                AttributeData = attributeData,
                HasMissingCloseAngle = node.MarkupStartTag?.CloseAngle.IsMissing ?? false,
                HasMissingEndCloseAngle = node.MarkupEndTag?.CloseAngle.IsMissing ?? false,
            };
 
            _builder.Push(element);
 
            var previousInsideFlag = _insideUnresolvedElement;
            _insideUnresolvedElement = true;
 
            if (node.MarkupStartTag != null)
            {
                VisitMarkupStartTag(node.MarkupStartTag);
            }
 
            // Now that all start-tag children have been lowered, check if any are C# expressions
            // (e.g. <div @expr>) and record this on the element so the resolution phase doesn't
            // need to scan children itself.
            foreach (var child in element.Children)
            {
                if (child is CSharpExpressionIntermediateNode or CSharpCodeIntermediateNode)
                {
                    element.HasDynamicExpressionChild = true;
                    break;
                }
            }
 
            _insideUnresolvedElement = false;
            element.StartTagEndIndex = element.Children.Count;
 
            foreach (var item in node.Body)
            {
                Visit(item);
            }
 
            element.BodyEndIndex = element.Children.Count;
 
            if (node.MarkupEndTag != null)
            {
                VisitMarkupEndTag(node.MarkupEndTag);
            }
 
            _insideUnresolvedElement = previousInsideFlag;
            _builder.Pop();
        }
 
        /// <summary>
        /// Lowers a non-minimized attribute. If inside a unresolved element (<c>_insideUnresolvedElement</c>),
        /// creates a <see cref="UnresolvedAttributeIntermediateNode"/> with two pre-lowered fallback
        /// forms: <c>AsTagHelperAttribute</c> (structured <see cref="HtmlAttributeIntermediateNode"/> with merged
        /// value tokens - used for unbound attributes when the element IS a tag helper) and
        /// <c>AsMarkupAttribute</c> (full attribute with individual tokens - used when the element is NOT
        /// a tag helper and must be unwrapped back to plain HTML markup). Handles the <c>@@</c> escape
        /// pattern in unresolved attribute values. If NOT inside an unresolved element, falls through to
        /// create a regular
        /// <see cref="HtmlAttributeIntermediateNode"/>.
        /// </summary>
        // Example
        // <input` checked="hello-world @false"`/>
        //  Name=checked
        //  Prefix= checked="
        //  Suffix="
        public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
        {
            var prefixTokens = MergeTokenLists(
                node.NamePrefix?.LiteralTokens,
                node.Name.LiteralTokens,
                node.NameSuffix?.LiteralTokens,
                new SyntaxTokenList(node.EqualsToken),
                node.ValuePrefix?.LiteralTokens);
 
            var position = node.NamePrefix?.Position ?? node.Name.Position;
            var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens).Green.CreateRed(node, position);
 
            var name = node.Name.GetContent();
 
            if (!_insideUnresolvedElement)
            {
                LowerAttributeAsHtml(node, name, prefix);
                return;
            }
 
            // Unresolved path: create deferred attribute node with fallback forms.
            var valueSourceSpan = ComputeAttributeValueSourceSpan(node);
 
            _builder.Push(new UnresolvedAttributeIntermediateNode()
            {
                AttributeName = name,
                IsMinimized = false,
                Source = BuildSourceSpanFromNode(node),
                ValueContent = node.Value?.GetContent(),
                ValueSourceSpan = valueSourceSpan,
                AttributeStructure = InferAttributeStructure(node),
                AttributeNameSpan = BuildSourceSpanFromNode(node.Name),
            });
 
            // Capture the pre-lowered fallback form (the non-tag-helper HTML form) by
            // temporarily resetting state and lowering into the unresolved node's children.
            // We then extract those children as the fallback before adding the unresolved form.
            _insideUnresolvedElement = false;
            LowerAttributeAsHtml(node, name, prefix);
            _insideUnresolvedElement = true;
 
            var unresolvedAttrNode = (UnresolvedAttributeIntermediateNode)_builder.Current;
            IntermediateNode legacyFallback = null;
            if (unresolvedAttrNode.Children.Count == 1)
            {
                legacyFallback = unresolvedAttrNode.Children[0];
            }
            else if (unresolvedAttrNode.Children.Count > 0)
            {
                var container = new MarkupElementIntermediateNode();
                container.Children.AddRange(unresolvedAttrNode.Children);
 
                legacyFallback = container;
            }
 
            unresolvedAttrNode.AsTagHelperAttribute = legacyFallback;
            unresolvedAttrNode.AsMarkupAttribute = legacyFallback;
            unresolvedAttrNode.Children.Clear();
 
            // Create HtmlAttribute with unresolved value children.
            _builder.Push(new HtmlAttributeIntermediateNode()
            {
                AttributeName = name,
                Prefix = prefix.GetContent(),
                Suffix = node.ValueSuffix?.GetContent() ?? string.Empty,
                Source = BuildSourceSpanFromNode(node),
            });
 
            _insideUnresolvedAttribute = true;
            LowerUnresolvedAttributeValue(node.Value);
            _insideUnresolvedAttribute = false;
 
            _builder.Pop();
 
            // Store the HtmlAttribute child directly on the unresolved node for O(1) access.
            if (_builder.Current is UnresolvedAttributeIntermediateNode currentUnresolved)
            {
                currentUnresolved.HtmlAttributeNode = (HtmlAttributeIntermediateNode)currentUnresolved.Children[^1];
            }
 
            _builder.Pop();
        }
 
        /// <summary>
        /// Lowers a <see cref="MarkupAttributeBlockSyntax"/> to its non-tag-helper HTML form.
        /// Used by both the non-unresolved path (direct lowering) and the unresolved path
        /// (to capture the fallback form stored on the unresolved node).
        /// </summary>
        private void LowerAttributeAsHtml(MarkupAttributeBlockSyntax node, string name, MarkupTextLiteralSyntax prefix)
        {
            if (!_options.AllowConditionalDataDashAttributes && name.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
            {
                Visit(prefix);
                Visit(node.Value);
                Visit(node.ValueSuffix);
            }
            else if (node.Value is { } blockSyntax)
            {
                var children = new ChildNodesHelper(blockSyntax.ChildNodesAndTokens());
 
                if (children.TryCast<MarkupLiteralAttributeValueSyntax>(out var attributeLiteralArray))
                {
                    using var builder = new PooledArrayBuilder<SyntaxToken>();
 
                    foreach (var literal in attributeLiteralArray)
                    {
                        var mergedValue = MergeAttributeValue(literal);
                        builder.AddRange(mergedValue.LiteralTokens);
                    }
 
                    var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList());
 
                    var mergedLiterals = MergeTokenLists(
                        prefix?.LiteralTokens,
                        rewritten.LiteralTokens,
                        node.ValueSuffix?.LiteralTokens);
 
                    var mergedAttribute = SyntaxFactory.MarkupTextLiteral(mergedLiterals).Green.CreateRed(node.Parent, node.Position);
                    Visit(mergedAttribute);
 
                    return;
                }
 
                _builder.Push(new HtmlAttributeIntermediateNode()
                {
                    AttributeName = name,
                    Prefix = prefix.GetContent(),
                    Suffix = node.ValueSuffix?.GetContent() ?? string.Empty,
                    Source = BuildSourceSpanFromNode(node),
                });
 
                VisitAttributeValue(node.Value);
 
                _builder.Pop();
            }
            else
            {
                // No value -- create empty HtmlAttribute.
                _builder.Push(new HtmlAttributeIntermediateNode()
                {
                    AttributeName = name,
                    Prefix = prefix.GetContent(),
                    Suffix = node.ValueSuffix?.GetContent() ?? string.Empty,
                    Source = BuildSourceSpanFromNode(node),
                });
 
                VisitAttributeValue(node.Value);
 
                _builder.Pop();
            }
        }
 
        /// <summary>
        /// Lowers an attribute value inside an unresolved element. Handles the @@ escape pattern
        /// (e.g. <c>Value="@@currentCount"</c>) by merging or splitting the @ literal and remaining
        /// content. Falls through to <see cref="VisitAttributeValue"/> for non-escape cases.
        /// </summary>
        private void LowerUnresolvedAttributeValue(RazorSyntaxNode value)
        {
            if (value == null)
            {
                VisitAttributeValue(value);
                return;
            }
 
            // Check for @@ escape pattern in unresolved attribute values.
            var valueChildren = value.ChildNodesAndTokens();
            if (valueChildren.Count >= 2 &&
                valueChildren[0].AsNode() is MarkupBlockSyntax { Children: [MarkupTextLiteralSyntax atLiteral, MarkupEphemeralTextLiteralSyntax] })
            {
                // Check if all remaining children are literals (can merge everything).
                var allLiteral = true;
                for (var i = 1; i < valueChildren.Count; i++)
                {
                    if (valueChildren[i].AsNode() is not MarkupLiteralAttributeValueSyntax)
                    {
                        allLiteral = false;
                        break;
                    }
                }
 
                SyntaxNode rewritten;
 
                if (allLiteral)
                {
                    // All-literal: merge @ + all literal content into one token.
                    using var mergedTokens = new PooledArrayBuilder<SyntaxToken>();
                    mergedTokens.AddRange(atLiteral.LiteralTokens);
                    for (var i = 1; i < valueChildren.Count; i++)
                    {
                        var literal = (MarkupLiteralAttributeValueSyntax)valueChildren[i].AsNode();
                        var merged = MergeAttributeValue(literal);
                        mergedTokens.AddRange(merged.LiteralTokens);
                    }
 
                    rewritten = SyntaxFactory.MarkupTextLiteral(mergedTokens.ToList()).Green.CreateRed(value.Parent, atLiteral.Position);
                }
                else
                {
                    // Mixed: just the @ literal; remaining children are visited below.
                    rewritten = SyntaxFactory.MarkupTextLiteral(atLiteral.LiteralTokens).Green.CreateRed(value.Parent, atLiteral.Position);
                }
 
                var rewrittenSource = BuildSourceSpanFromNode(rewritten);
                var unresolvedNode = new UnresolvedAttributeValueIntermediateNode()
                {
                    Prefix = string.Empty,
                    Source = rewrittenSource,
                };
                unresolvedNode.Children.Add(IntermediateNodeFactory.HtmlToken(
                    arg: (MarkupTextLiteralSyntax)rewritten,
                    contentFactory: static node => node.GetContent() ?? string.Empty,
                    source: rewrittenSource));
                _builder.Add(unresolvedNode);
 
                if (!allLiteral)
                {
                    // Visit remaining children (expressions, etc.)
                    for (var i = 1; i < valueChildren.Count; i++)
                    {
                        Visit(valueChildren[i].AsNode());
                    }
                }
            }
            else
            {
                VisitAttributeValue(value);
            }
        }
 
        /// <summary>
        /// Lowers a minimized attribute (no value, e.g. <c>checked</c>, <c>disabled</c>). If inside a
        /// unresolved element, creates a <see cref="UnresolvedAttributeIntermediateNode"/> with
        /// <c>IsMinimized = true</c> and a fallback <see cref="HtmlContentIntermediateNode"/> containing
        /// the attribute name as text.
        /// </summary>
        public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
        {
            if (_insideUnresolvedElement)
            {
                // Produce the fallback: what this minimized attribute looks like as plain HTML.
                // Minimized attributes are just html content (e.g. "checked" -> HtmlContent " checked").
                var fallbackTokens = MergeTokenLists(
                    node.NamePrefix?.LiteralTokens,
                    node.Name?.LiteralTokens);
                var fallbackLiteral = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(fallbackTokens).Green.CreateRed(node.Parent, node.Position);
                var fallbackSource = BuildSourceSpanFromNode(fallbackLiteral);
                var fallback = new HtmlContentIntermediateNode() { Source = fallbackSource };
                fallback.Children.Add(IntermediateNodeFactory.HtmlToken(
                    arg: fallbackLiteral,
                    contentFactory: static node => node.GetContent(),
                    fallbackSource));
 
                _builder.Add(new UnresolvedAttributeIntermediateNode()
                {
                    AttributeName = node.Name.GetContent(),
                    IsMinimized = true,
                    Source = BuildSourceSpanFromNode(node),
                    AttributeStructure = AttributeStructure.Minimized,
                    AttributeNameSpan = BuildSourceSpanFromNode(node.Name),
                    AsMarkupAttribute = fallback,
                });
                return;
            }
 
            if (!_options.AllowConditionalDataDashAttributes)
            {
                var name = node.Name.GetContent();
 
                if (name.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
                {
                    base.VisitMarkupMinimizedAttributeBlock(node);
                    return;
                }
            }
 
            // Minimized attributes are just html content.
            var literals = MergeTokenLists(
                node.NamePrefix?.LiteralTokens,
                node.Name?.LiteralTokens);
 
            var literal = SyntaxFactory.MarkupTextLiteral(literals).Green.CreateRed(node.Parent, node.Position);
 
            Visit(literal);
        }
 
        // CSharp expressions are broken up into blocks and spans because Razor allows Razor comments
        // inside an expression.
        // Ex:
        //      @DateTime.@*This is a comment*@Now
        //
        // We need to capture this in the IR so that we can give each piece the correct source mappings
        public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
        {
            if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode)
            {
                base.VisitCSharpExplicitExpression(node);
                return;
            }
 
            var expressionNode = new CSharpExpressionIntermediateNode();
 
            _builder.Push(expressionNode);
 
            base.VisitCSharpExplicitExpression(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(expressionNode);
        }
 
        public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
        {
            if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode)
            {
                base.VisitCSharpImplicitExpression(node);
                return;
            }
 
            var expressionNode = new CSharpExpressionIntermediateNode();
 
            _builder.Push(expressionNode);
 
            base.VisitCSharpImplicitExpression(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(expressionNode);
        }
        public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
        {
            if (node.ChunkGenerator is null or StatementChunkGenerator)
            {
                var isAttributeValue = _builder.Current is CSharpCodeAttributeValueIntermediateNode;
 
                if (!isAttributeValue)
                {
                    var statementNode = new CSharpCodeIntermediateNode()
                    {
                        Source = BuildSourceSpanFromNode(node)
                    };
                    _builder.Push(statementNode);
                }
 
                _builder.Add(IntermediateNodeFactory.CSharpToken(
                    arg: node,
                    contentFactory: static node => node.GetContent(),
                    source: BuildSourceSpanFromNode(node)));
 
                if (!isAttributeValue)
                {
                    _builder.Pop();
                }
            }
 
            base.VisitCSharpStatementLiteral(node);
        }
 
        public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
        {
            if (node.ChunkGenerator == SpanChunkGenerator.Null)
            {
                return;
            }
 
            if (node.LiteralTokens is [{ Kind: SyntaxKind.Marker, Content.Length: 0 }])
            {
                // We don't want to create IR nodes for marker tokens.
                return;
            }
 
            VisitHtmlContent(node);
        }
 
        public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
        {
            if (node.IsMarkupTransition)
            {
                // No need to visit <text> tags.
                return;
            }
 
            foreach (var child in node.LegacyChildren)
            {
                Visit(child);
            }
        }
 
        public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
        {
            if (node.IsMarkupTransition)
            {
                // No need to visit </text> tags.
                return;
            }
 
            foreach (var child in node.LegacyChildren)
            {
                Visit(child);
            }
        }
 
        private void VisitHtmlContent(SyntaxNode node)
        {
            if (node == null)
            {
                return;
            }
 
            var source = BuildSourceSpanFromNode(node);
            var currentChildren = _builder.Current.Children;
 
            // Don't merge HtmlContent across element region boundaries (start-tag -> body -> end-tag).
            // The pre-computed indices mark where each region begins, so when we're about to add
            // the first child of a new region, we must start a fresh HtmlContentIntermediateNode.
            var atBoundary = _builder.Current is UnresolvedElementIntermediateNode element
                && (currentChildren.Count == element.StartTagEndIndex
                 || currentChildren.Count == element.BodyEndIndex);
 
            if (!atBoundary && currentChildren.Count > 0 && currentChildren[currentChildren.Count - 1] is HtmlContentIntermediateNode)
            {
                var existingHtmlContent = (HtmlContentIntermediateNode)currentChildren[currentChildren.Count - 1];
 
                if (existingHtmlContent.Source == null && source == null)
                {
                    Combine(existingHtmlContent, node);
                    return;
                }
 
                if (source != null &&
                    existingHtmlContent.Source != null &&
                    existingHtmlContent.Source.Value.FilePath == source.Value.FilePath &&
                    existingHtmlContent.Source.Value.AbsoluteIndex + existingHtmlContent.Source.Value.Length == source.Value.AbsoluteIndex)
                {
                    Combine(existingHtmlContent, node);
                    return;
                }
            }
 
            var contentNode = new HtmlContentIntermediateNode()
            {
                Source = source
            };
 
            _builder.Push(contentNode);
 
            _builder.Add(IntermediateNodeFactory.HtmlToken(
                arg: node,
                contentFactory: static node => node.GetContent(),
                source));
 
            _builder.Pop();
        }
    }
 
    /// <summary>
    /// Handles .razor files (Blazor components). Treats HTML markup as structured nodes
    /// (<see cref="MarkupElementIntermediateNode"/> with tag name and attributes) and supports
    /// Components. Every element is wrapped in <see cref="UnresolvedElementIntermediateNode"/>
    /// because any element could match a component.
    /// </summary>
    private class ComponentFileKindVisitor : LoweringVisitor
    {
        public ComponentFileKindVisitor(
            DocumentIntermediateNode document,
            IntermediateNodeBuilder builder,
            RazorParserOptions options)
            : base(document, builder, options)
        {
        }
 
        /// <summary>
        /// Always creates an <see cref="UnresolvedElementIntermediateNode"/> because every element
        /// could be a component. Markup transitions (<c>@:</c> and <c>&lt;text&gt;</c>) are excluded
        /// and fall through to the base visitor. Extracts attribute data for tag helper binding and
        /// sets boundary indices for content region tracking.
        /// </summary>
        public override void VisitMarkupElement(MarkupElementSyntax node)
        {
            if ((node.MarkupStartTag != null && node.MarkupStartTag.IsMarkupTransition) ||
                (node.MarkupEndTag != null && node.MarkupEndTag.IsMarkupTransition))
            {
                base.VisitMarkupElement(node);
                return;
            }
 
            var tagName = node.MarkupStartTag?.Name.Content ?? node.MarkupEndTag?.Name.Content ?? string.Empty;
 
            var attributeData = ExtractAttributeData(node);
 
            var element = new UnresolvedElementIntermediateNode()
            {
                Source = BuildSourceSpanFromNode(node),
                TagName = tagName,
                IsComponent = true,
                IsEscaped = node.MarkupStartTag?.Bang is { Width: > 0 },
                IsSelfClosing = node.MarkupStartTag?.IsSelfClosing() ?? false,
                HasEndTag = node.MarkupEndTag != null,
                EndTagName = node.MarkupEndTag?.GetTagNameWithOptionalBang(),
                EndTagSpan = node.MarkupEndTag != null ? BuildSourceSpanFromNode(node.MarkupEndTag) : null,
                IsVoidElement = node.MarkupStartTag?.IsVoidElement() ?? false,
                StartTagNameSpan = node.MarkupStartTag?.Name.GetSourceSpan(SourceDocument),
                StartTagSpan = node.MarkupStartTag != null ? BuildSourceSpanFromNode(node.MarkupStartTag) : null,
                AttributeData = attributeData,
                HasMissingCloseAngle = node.MarkupStartTag?.CloseAngle.IsMissing ?? false,
                HasMissingEndCloseAngle = node.MarkupEndTag?.CloseAngle.IsMissing ?? false,
            };
 
            // Preserve diagnostics from the original ComponentFileKindVisitor logic.
            if (node.MarkupStartTag != null && node.MarkupEndTag != null && node.MarkupStartTag.IsVoidElement())
            {
                element.AddDiagnostic(
                    ComponentDiagnosticFactory.Create_UnexpectedClosingTagForVoidElement(
                        BuildSourceSpanFromNode(node.MarkupEndTag), node.MarkupEndTag.GetTagNameWithOptionalBang()));
            }
            else if (node.MarkupStartTag != null && node.MarkupEndTag == null && !node.MarkupStartTag.IsVoidElement() && !node.MarkupStartTag.IsSelfClosing())
            {
                element.AddDiagnostic(
                    ComponentDiagnosticFactory.Create_UnclosedTag(
                        BuildSourceSpanFromNode(node.MarkupStartTag), node.MarkupStartTag.GetTagNameWithOptionalBang()));
            }
            else if (node.MarkupStartTag == null && node.MarkupEndTag != null)
            {
                element.AddDiagnostic(
                    ComponentDiagnosticFactory.Create_UnexpectedClosingTag(
                        BuildSourceSpanFromNode(node.MarkupEndTag), node.MarkupEndTag.GetTagNameWithOptionalBang()));
            }
 
            _builder.Push(element);
 
            base.VisitMarkupElement(node);
 
            _builder.Pop();
        }
 
        public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
        {
            // We want to skip over the other misc tokens that make up a start tag, and
            // just process the attributes.
            //
            // Visit the attributes
            foreach (var block in node.Attributes)
            {
                if (block is MarkupAttributeBlockSyntax attribute)
                {
                    VisitMarkupAttributeBlock(attribute);
                }
                else if (block is MarkupMinimizedAttributeBlockSyntax minimized)
                {
                    VisitMarkupMinimizedAttributeBlock(minimized);
                }
            }
        }
 
        public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
        {
            // We want to skip over the other misc tokens that make up a start tag, and
            // just process the attributes.
            //
            // Nothing to do here
        }
 
        // Example
        // <input` checked="hello-world @false"`/>
        //  Name=checked
        //  Prefix= checked="
        //  Suffix="
        public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
        {
            var name = node.Name.GetContent();
            var isUnresolved = _builder.Current is UnresolvedElementIntermediateNode;
 
            if (isUnresolved)
            {
                var valueSourceSpan = ComputeAttributeValueSourceSpan(node);
                var source = BuildSourceSpanFromNode(node);
 
                _builder.Push(new UnresolvedAttributeIntermediateNode()
                {
                    AttributeName = name,
                    IsMinimized = false,
                    Source = source,
                    ValueContent = node.Value?.GetContent(),
                    ValueSourceSpan = valueSourceSpan,
                    AttributeStructure = InferAttributeStructure(node),
                    AttributeNameSpan = BuildSourceSpanFromNode(node.Name),
                });
 
                // Capture value-only fallback (merged adjacent literal tokens) by temporarily
                // lowering into the unresolved node's children with unresolved state reset.
                var fallbackContainer = new HtmlAttributeIntermediateNode()
                {
                    AttributeName = name,
                    Prefix = SyntaxFactory.MarkupTextLiteral(MergeTokenLists(
                        node.NamePrefix?.LiteralTokens,
                        node.Name.LiteralTokens,
                        node.NameSuffix?.LiteralTokens,
                        new SyntaxTokenList(node.EqualsToken),
                        node.ValuePrefix?.LiteralTokens)).GetContent(),
                    Suffix = node.ValueSuffix?.GetContent() ?? string.Empty,
                    Source = source,
                };
 
                if (node.Value != null)
                {
                    _builder.Push(fallbackContainer);
                    VisitAttributeValue(node.Value);
                    _builder.Pop();
                }
                var unresolvedAttrNode = (UnresolvedAttributeIntermediateNode)_builder.Current;
                // Remove fallbackContainer from children -- it was temporarily added by Push.
                unresolvedAttrNode.Children.Remove(fallbackContainer);
                unresolvedAttrNode.AsTagHelperAttribute = fallbackContainer;
 
                // Capture AsMarkupAttribute fallback by lowering the whole attribute in non-unresolved
                // context. Push a temporary container so _builder.Current is not an
                // UnresolvedElementIntermediateNode, which causes VisitMarkupAttributeBlock to
                // take the non-unresolved path.
                var fullFallbackContainer = new MarkupElementIntermediateNode();
                _builder.Push(fullFallbackContainer);
                Visit(node);
                _builder.Pop();
                unresolvedAttrNode.Children.Remove(fullFallbackContainer);
                unresolvedAttrNode.AsMarkupAttribute = fullFallbackContainer.Children.Count == 1
                    ? fullFallbackContainer.Children[0]
                    : fullFallbackContainer.Children.Count > 0 ? fullFallbackContainer : null;
            }
 
            var prefixTokens = MergeTokenLists(
                node.NamePrefix?.LiteralTokens,
                node.Name.LiteralTokens,
                node.NameSuffix?.LiteralTokens,
                new SyntaxTokenList(node.EqualsToken),
                node.ValuePrefix?.LiteralTokens);
 
            var position = node.NamePrefix?.Position ?? node.Name.Position;
            var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens).Green.CreateRed(node, position);
 
            _builder.Push(new HtmlAttributeIntermediateNode()
            {
                AttributeName = name,
                Prefix = prefix.GetContent(),
                Suffix = node.ValueSuffix?.GetContent() ?? string.Empty,
                Source = BuildSourceSpanFromNode(node),
            });
 
            _insideUnresolvedAttribute = isUnresolved;
            if (isUnresolved &&
                node.Value?.ChildNodesAndTokens() is { Count: >= 2 } valueChildren &&
                valueChildren[0].AsNode() is MarkupBlockSyntax { Children: [MarkupTextLiteralSyntax atLiteral, MarkupEphemeralTextLiteralSyntax] } &&
                valueChildren[1].AsNode() is MarkupLiteralAttributeValueSyntax)
            {
                // @@ escape pattern: merge the escaped @ with all following literal children into one unresolved node.
                using var mergedTokens = new PooledArrayBuilder<SyntaxToken>();
                mergedTokens.AddRange(atLiteral.LiteralTokens);
                for (var i = 1; i < valueChildren.Count; i++)
                {
                    if (valueChildren[i].AsNode() is MarkupLiteralAttributeValueSyntax literal)
                    {
                        var merged = MergeAttributeValue(literal);
                        mergedTokens.AddRange(merged.LiteralTokens);
                    }
                    else
                    {
                        // Mixed content after @@ -- fall through to normal Visit
                        break;
                    }
                }
 
                var atPosition = atLiteral.Position;
                var rewritten = SyntaxFactory.MarkupTextLiteral(mergedTokens.ToList()).Green.CreateRed(node.Value.Parent, atPosition);
                var rewrittenSource = BuildSourceSpanFromNode(rewritten);
                var unresolvedNode = new UnresolvedAttributeValueIntermediateNode()
                {
                    Prefix = string.Empty,
                    Source = rewrittenSource,
                };
                unresolvedNode.Children.Add(IntermediateNodeFactory.HtmlToken(
                    arg: (MarkupTextLiteralSyntax)rewritten,
                    contentFactory: static node => node.GetContent() ?? string.Empty,
                    source: rewrittenSource));
                _builder.Add(unresolvedNode);
            }
            else
            {
                Visit(node.Value);
            }
            _insideUnresolvedAttribute = false;
 
            _builder.Pop();
 
            // Store the HtmlAttribute child directly on the unresolved node for O(1) access.
            if (isUnresolved && _builder.Current is UnresolvedAttributeIntermediateNode currentUnresolved)
            {
                currentUnresolved.HtmlAttributeNode = (HtmlAttributeIntermediateNode)currentUnresolved.Children[currentUnresolved.Children.Count - 1];
            }
 
            if (isUnresolved)
            {
                _builder.Pop();
            }
        }
 
        public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
        {
            var prefixTokens = MergeTokenLists(
                node.NamePrefix?.LiteralTokens,
                node.Name.LiteralTokens);
            var position = node.NamePrefix?.Position ?? node.Name.Position;
            var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens).Green.CreateRed(node, position);
 
            var name = node.Name.GetContent();
            var source = BuildSourceSpanFromNode(node);
            var htmlAttr = new HtmlAttributeIntermediateNode()
            {
                AttributeName = name,
                Prefix = prefix.GetContent(),
                Suffix = null,
                Source = source,
            };
 
            if (_builder.Current is UnresolvedElementIntermediateNode)
            {
                _builder.Add(new UnresolvedAttributeIntermediateNode()
                {
                    AttributeName = name,
                    IsMinimized = true,
                    Source = source,
                    AttributeStructure = AttributeStructure.Minimized,
                    AttributeNameSpan = BuildSourceSpanFromNode(node.Name),
                    AsMarkupAttribute = htmlAttr,
                });
            }
            else
            {
                _builder.Add(htmlAttr);
            }
        }
 
        // Example
        // <input checked="hello-world `@false`"/>
        //  Prefix= (space)
        //  Children will contain a token for @false.
        public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
        {
            if (_builder.Current is HtmlAttributeIntermediateNode)
            {
                var attrValueSource = BuildSourceSpanFromNode(node);
 
                IntermediateNode childNode = _insideUnresolvedAttribute
                    ? new UnresolvedAttributeValueIntermediateNode()
                    {
                        Prefix = string.Empty,
                        Source = attrValueSource,
                    }
                    : new HtmlAttributeValueIntermediateNode()
                    {
                        Prefix = string.Empty,
                        Source = attrValueSource,
                    };
 
                childNode.Children.Add(IntermediateNodeFactory.HtmlToken(
                    arg: node,
                    contentFactory: static node => node.GetContent() ?? string.Empty,
                    source: attrValueSource));
 
                _builder.Add(childNode);
                return;
            }
 
            var context = node.EditHandler;
            if (node.ChunkGenerator == SpanChunkGenerator.Null)
            {
                return;
            }
 
            if (node.LiteralTokens is [{ Kind: SyntaxKind.Marker, Content.Length: 0 }])
            {
                // We don't want to create IR nodes for marker tokens.
                return;
            }
 
            // Combine chunks of HTML literal text if possible.
            var source = BuildSourceSpanFromNode(node);
            var currentChildren = _builder.Current.Children;
            if (currentChildren.Count > 0 &&
                currentChildren[currentChildren.Count - 1] is HtmlContentIntermediateNode existingHtmlContent)
            {
                if (existingHtmlContent.Source == null && source == null)
                {
                    Combine(existingHtmlContent, node);
                    return;
                }
 
                if (source != null &&
                    existingHtmlContent.Source != null &&
                    existingHtmlContent.Source.Value.FilePath == source.Value.FilePath &&
                    existingHtmlContent.Source.Value.AbsoluteIndex + existingHtmlContent.Source.Value.Length == source.Value.AbsoluteIndex)
                {
                    Combine(existingHtmlContent, node);
                    return;
                }
            }
 
            _builder.Add(new HtmlContentIntermediateNode()
            {
                Source = source,
                Children =
                {
                    IntermediateNodeFactory.HtmlToken(
                        arg: node,
                        contentFactory: static node => node.GetContent(),
                        source)
                }
            });
        }
 
        public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
        {
            // Comments are ignored by components. We skip over anything that appears inside.
        }
        // CSharp expressions are broken up into blocks and spans because Razor allows Razor comments
        // inside an expression.
        // Ex:
        //      @DateTime.@*This is a comment*@Now
        //
        // We need to capture this in the IR so that we can give each piece the correct source mappings
        public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
        {
            if (_builder.Current is HtmlAttributeIntermediateNode)
            {
                // This can happen inside a data- attribute
                _builder.Push(new CSharpExpressionAttributeValueIntermediateNode()
                {
                    Prefix = string.Empty,
                    Source = this.BuildSourceSpanFromNode(node),
                });
 
                base.VisitCSharpExplicitExpression(node);
 
                _builder.Pop();
 
                return;
            }
 
            if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode)
            {
                base.VisitCSharpExplicitExpression(node);
                return;
            }
 
            var expressionNode = new CSharpExpressionIntermediateNode();
 
            _builder.Push(expressionNode);
 
            base.VisitCSharpExplicitExpression(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(expressionNode);
        }
 
        public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
        {
            if (_builder.Current is HtmlAttributeIntermediateNode)
            {
                // This can happen inside a data- attribute
                _builder.Push(new CSharpExpressionAttributeValueIntermediateNode()
                {
                    Prefix = string.Empty,
                    Source = this.BuildSourceSpanFromNode(node),
                });
 
                base.VisitCSharpImplicitExpression(node);
 
                _builder.Pop();
 
                return;
            }
 
            if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode)
            {
                base.VisitCSharpImplicitExpression(node);
                return;
            }
 
            var expressionNode = new CSharpExpressionIntermediateNode();
 
            _builder.Push(expressionNode);
 
            base.VisitCSharpImplicitExpression(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(expressionNode);
        }
        public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
        {
            if (node.ChunkGenerator is null or StatementChunkGenerator)
            {
                var isAttributeValue = _builder.Current is CSharpCodeAttributeValueIntermediateNode;
 
                if (!isAttributeValue)
                {
                    var statementNode = new CSharpCodeIntermediateNode()
                    {
                        Source = BuildSourceSpanFromNode(node)
                    };
                    _builder.Push(statementNode);
                }
 
                _builder.Add(IntermediateNodeFactory.CSharpToken(
                    arg: node,
                    contentFactory: static node => node.GetContent(),
                    source: BuildSourceSpanFromNode(node)));
 
                if (!isAttributeValue)
                {
                    _builder.Pop();
                }
            }
 
            base.VisitCSharpStatementLiteral(node);
        }
 
    }
 
    private ref struct DirectiveAttributeName(string original)
    {
        // Directive attributes should start with '@' unless the descriptors are misconfigured.
        // In that case, we would have already logged an error.
        public readonly ReadOnlySpan<char> Span = original.StartsWith('@') ? original.AsSpan()[1..] : original;
 
        public string Text => field ??= (Span.Length < original.Length ? Span.ToString() : original);
 
        private bool? _hasParameter;
 
        public bool HasParameter => _hasParameter ??= Span.IndexOf(':') >= 0;
 
        public string TextWithoutParameter
            => field ??= Span.IndexOf(':') is int index && index >= 0 ? Span[..index].ToString() : Text;
    }
 
    /// <summary>
    /// Handles <c>_Imports.razor</c> files. Only processes directives (<c>@using</c>,
    /// <c>@namespace</c>) and code expressions. Does not support HTML elements or attributes
    /// (no components in import files) -- markup and expressions produce diagnostics.
    /// </summary>
    private class ComponentImportFileKindVisitor : LoweringVisitor
    {
        public ComponentImportFileKindVisitor(
            DocumentIntermediateNode document,
            IntermediateNodeBuilder builder,
            RazorParserOptions options)
            : base(document, builder, options)
        {
        }
 
        public override void VisitMarkupElement(MarkupElementSyntax node)
        {
            _document.AddDiagnostic(
                ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
 
            base.VisitMarkupElement(node);
        }
 
        public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
        {
            _document.AddDiagnostic(
                ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
 
            base.VisitMarkupCommentBlock(node);
        }
 
        public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
        {
            _document.AddDiagnostic(
                ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
 
            base.VisitCSharpExplicitExpression(node);
        }
 
        public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
        {
            // We typically don't want C# in imports files except for directives. But since Razor directive intellisense
            // is tied to C# intellisense during design time, we want to still generate and IR node for implicit expressions.
            // Otherwise, there will be no source mapping when someone types an `@` leading to no intellisense.
            if (node.FirstAncestorOrSelf<SyntaxNode>(n => n is MarkupStartTagSyntax || n is MarkupEndTagSyntax) != null)
            {
                // We don't care about implicit expression in attributes.
                return;
            }
 
            var expressionNode = new CSharpExpressionIntermediateNode();
 
            _builder.Push(expressionNode);
 
            base.VisitCSharpImplicitExpression(node);
 
            _builder.Pop();
 
            ComputeSourceSpanFromChildren(expressionNode);
 
            _document.AddDiagnostic(
                ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(expressionNode.Source));
 
            base.VisitCSharpImplicitExpression(node);
        }
 
        public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
        {
            if (node.FirstAncestorOrSelf<SyntaxNode>(n => n is CSharpImplicitExpressionSyntax) == null)
            {
                // We only care about implicit expressions.
                return;
            }
 
            _builder.Add(IntermediateNodeFactory.CSharpToken(
                arg: node,
                contentFactory: static node => node.GetContent(),
                source: BuildSourceSpanFromNode(node)));
        }
 
        public override void VisitCSharpStatement(CSharpStatementSyntax node)
        {
            _document.AddDiagnostic(
                ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
 
            base.VisitCSharpStatement(node);
        }
    }
 
    private class ImportsVisitor : LoweringVisitor
    {
        public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserOptions options)
            : base(document, new ImportBuilder(builder), options)
        {
        }
 
        private class ImportBuilder : IntermediateNodeBuilder
        {
            private readonly IntermediateNodeBuilder _innerBuilder;
 
            public ImportBuilder(IntermediateNodeBuilder innerBuilder)
            {
                _innerBuilder = innerBuilder;
            }
 
            public override IntermediateNode Current => _innerBuilder.Current;
 
            public override void Add(IntermediateNode node)
            {
                node.IsImported = true;
                _innerBuilder.Add(node);
            }
 
            public override IntermediateNode Build() => _innerBuilder.Build();
 
            public override void Insert(int index, IntermediateNode node)
            {
                node.IsImported = true;
                _innerBuilder.Insert(index, node);
            }
 
            public override IntermediateNode Pop() => _innerBuilder.Pop();
 
            public override void Push(IntermediateNode node)
            {
                node.IsImported = true;
                _innerBuilder.Push(node);
            }
        }
    }
 
    private static bool IsMalformed(IEnumerable<RazorDiagnostic> diagnostics)
        => diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error);
}