File: Language\Extensions\DefaultTagHelperOptimizationPass.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.
 
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
 
namespace Microsoft.AspNetCore.Razor.Language.Extensions;
 
internal sealed class DefaultTagHelperOptimizationPass : IntermediateNodePassBase, IRazorOptimizationPass
{
    // Run later than default order for user code so other passes have a chance to modify the
    // tag helper nodes.
    public override int Order => DefaultFeatureOrder + 1000;
 
    protected override void ExecuteCore(
        RazorCodeDocument codeDocument,
        DocumentIntermediateNode documentNode,
        CancellationToken cancellationToken)
    {
        var @class = documentNode.FindPrimaryClass();
        if (@class == null)
        {
            // Bail if we can't find a class node, we need to be able to create fields.
            return;
        }
 
        var context = new Context(@class);
 
        // First find all tag helper nodes that require the default tag helper runtime.
        //
        // This phase lowers the conceptual nodes to default runtime nodes we only care about those.
        var tagHelperNodes = documentNode
            .FindDescendantNodes<TagHelperIntermediateNode>()
            .Where(IsTagHelperRuntimeNode)
            .ToArray();
 
        if (tagHelperNodes.Length == 0)
        {
            // If nothing uses the default runtime then we're done.
            return;
        }
 
        AddDefaultRuntime(context);
 
        // Each tagHelperNode should be rewritten to use the default tag helper runtime. That doesn't necessarily
        // mean that all of these tag helpers are the default kind, just that them are compatible with ITagHelper.
        for (var i = 0; i < tagHelperNodes.Length; i++)
        {
            var tagHelperNode = tagHelperNodes[i];
 
            RewriteBody(tagHelperNode);
            RewriteHtmlAttributes(tagHelperNode);
            AddExecute(tagHelperNode);
 
            // We need to find all of the 'default' kind tag helpers and rewrite their usage site to use the
            // extension nodes for the default tag helper runtime (ITagHelper).
            foreach (var tagHelper in tagHelperNode.TagHelpers)
            {
                RewriteUsage(context, tagHelperNode, tagHelper);
            }
        }
 
        // Then for each 'default' kind tag helper we need to generate the field that will hold it.
        foreach (var tagHelper in context.TagHelpers)
        {
            AddField(context, tagHelper);
        }
    }
 
    private void AddDefaultRuntime(Context context)
    {
        // We need to insert a node for the field that will hold the tag helper. We've already generated a field name
        // at this time and use it for all uses of the same tag helper type.
        //
        // We also want to preserve the ordering of the nodes for testability. So insert at the end of any existing
        // field nodes.
        var i = 0;
        while (i < context.Class.Children.Count && context.Class.Children[i] is FieldDeclarationIntermediateNode)
        {
            i++;
        }
 
        context.Class.Children.Insert(i, new DefaultTagHelperRuntimeIntermediateNode());
    }
 
    private void RewriteBody(TagHelperIntermediateNode node)
    {
        for (var i = 0; i < node.Children.Count; i++)
        {
            if (node.Children[i] is TagHelperBodyIntermediateNode bodyNode)
            {
                // We only expect one body node.
                node.Children[i] = new DefaultTagHelperBodyIntermediateNode(bodyNode)
                {
                    TagMode = node.TagMode,
                    TagName = node.TagName,
                };
                break;
            }
        }
    }
 
    private void AddExecute(TagHelperIntermediateNode node)
    {
        // Execute the tag helpers at the end, before leaving scope.
        node.Children.Add(new DefaultTagHelperExecuteIntermediateNode());
    }
 
    private void RewriteHtmlAttributes(TagHelperIntermediateNode node)
    {
        // We need to rewrite each html attribute, so that it will get added to the execution context.
        for (var i = 0; i < node.Children.Count; i++)
        {
            if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlAttributeNode)
            {
                node.Children[i] = new DefaultTagHelperHtmlAttributeIntermediateNode(htmlAttributeNode);
            }
        }
    }
 
    private void RewriteUsage(Context context, TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
    {
        if (!tagHelper.IsDefaultKind())
        {
            return;
        }
 
        context.Add(tagHelper);
 
        // First we need to insert a node for the creation of the tag helper, and the hook up to the execution
        // context. This should come after the body node and any existing create nodes.
        //
        // If we're dealing with something totally malformed, then we'll end up just inserting at the end, and that's not
        // so bad.
        var i = 0;
 
        // Find the body node.
        while (i < node.Children.Count && node.Children[i] is TagHelperBodyIntermediateNode)
        {
            i++;
        }
        while (i < node.Children.Count && node.Children[i] is DefaultTagHelperBodyIntermediateNode)
        {
            i++;
        }
 
        // Now find the last create node.
        while (i < node.Children.Count && node.Children[i] is DefaultTagHelperCreateIntermediateNode)
        {
            i++;
        }
 
        // Now i has the right insertion point.
        node.Children.Insert(i, new DefaultTagHelperCreateIntermediateNode()
        {
            FieldName = context.GetFieldName(tagHelper),
            TagHelper = tagHelper,
            TypeName = tagHelper.TypeName,
        });
 
        // Next we need to rewrite any property nodes to use the field and property name for this
        // tag helper.
        for (i = 0; i < node.Children.Count; i++)
        {
            if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode &&
                propertyNode.TagHelper == tagHelper)
            {
                // This belongs to the current tag helper, replace it.
                node.Children[i] = new DefaultTagHelperPropertyIntermediateNode(propertyNode)
                {
                    FieldName = context.GetFieldName(tagHelper),
                    PropertyName = propertyNode.BoundAttribute.PropertyName,
                };
            }
        }
    }
 
    private void AddField(Context context, TagHelperDescriptor tagHelper)
    {
        // We need to insert a node for the field that will hold the tag helper. We've already generated a field name
        // at this time and use it for all uses of the same tag helper type.
        //
        // We also want to preserve the ordering of the nodes for testability. So insert at the end of any existing
        // field nodes.
        var i = 0;
        while (i < context.Class.Children.Count && context.Class.Children[i] is DefaultTagHelperRuntimeIntermediateNode)
        {
            i++;
        }
 
        while (i < context.Class.Children.Count && context.Class.Children[i] is FieldDeclarationIntermediateNode)
        {
            i++;
        }
 
        context.Class.Children.Insert(i, new FieldDeclarationIntermediateNode()
        {
            IsTagHelperField = true,
            Modifiers = CommonModifiers.Private,
            Name = context.GetFieldName(tagHelper),
            Type = "global::" + tagHelper.TypeName
        });
    }
 
    private bool IsTagHelperRuntimeNode(TagHelperIntermediateNode node)
    {
        foreach (var tagHelper in node.TagHelpers)
        {
            if (tagHelper.KindUsesDefaultTagHelperRuntime())
            {
                return true;
            }
        }
 
        return false;
    }
 
    private struct Context
    {
        private readonly Dictionary<TagHelperDescriptor, string> _tagHelpers;
 
        public Context(ClassDeclarationIntermediateNode @class)
        {
            Class = @class;
 
            _tagHelpers = new Dictionary<TagHelperDescriptor, string>();
        }
 
        public ClassDeclarationIntermediateNode Class { get; }
 
        public IEnumerable<TagHelperDescriptor> TagHelpers => _tagHelpers.Keys;
 
        public bool Add(TagHelperDescriptor tagHelper)
        {
            if (_tagHelpers.ContainsKey(tagHelper))
            {
                return false;
            }
 
            _tagHelpers.Add(tagHelper, GenerateFieldName(tagHelper));
            return true;
        }
 
        public string GetFieldName(TagHelperDescriptor tagHelper)
        {
            return _tagHelpers[tagHelper];
        }
 
        private static string GenerateFieldName(TagHelperDescriptor tagHelper)
        {
            return "__" + tagHelper.TypeName.Replace('.', '_');
        }
    }
}