File: Language\Components\ComponentGenericTypePass.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Razor;
 
namespace Microsoft.AspNetCore.Razor.Language.Components;
 
// This pass:
// 1. Adds diagnostics for missing generic type arguments
// 2. Rewrites the type name of the component to substitute generic type arguments
// 3. Rewrites the type names of parameters/child content to substitute generic type arguments
internal sealed class ComponentGenericTypePass : ComponentIntermediateNodePassBase, IRazorOptimizationPass
{
    // Runs after components/eventhandlers/ref/bind/templates. We want to validate every component
    // and it's usage of ChildContent.
    public override int Order => 160;
 
    protected override void ExecuteCore(
        RazorCodeDocument codeDocument,
        DocumentIntermediateNode documentNode,
        CancellationToken cancellationToken)
    {
        if (!IsComponentDocument(documentNode))
        {
            return;
        }
 
        var visitor = new Visitor();
        visitor.Visit(documentNode);
    }
 
    internal sealed class Visitor : IntermediateNodeWalker
    {
        // Incrementing ID for type inference method names
        private int _id;
 
        public override void VisitComponent(ComponentIntermediateNode node)
        {
            if (node.Component.IsGenericTypedComponent())
            {
                // Not generic, ignore.
                Process(node);
            }
 
            base.VisitDefault(node);
        }
 
        private void Process(ComponentIntermediateNode node)
        {
            // First collect all of the information we have about each type parameter
            //
            // Listing all type parameters that exist
            var bindings = new Dictionary<string, Binding>();
            var componentTypeParameters = node.Component.GetTypeParameters().ToList();
            var supplyCascadingTypeParameters = componentTypeParameters
                .Where(p => p.IsCascadingTypeParameterProperty())
                .Select(p => p.Name)
                .ToList();
            foreach (var attribute in componentTypeParameters)
            {
                bindings.Add(attribute.Name, new Binding(attribute));
            }
 
            // Listing all type arguments that have been specified.
            var hasTypeArgumentSpecified = false;
            foreach (var typeArgumentNode in node.TypeArguments)
            {
                hasTypeArgumentSpecified = true;
 
                var binding = bindings[typeArgumentNode.TypeParameterName];
                binding.Node = typeArgumentNode;
                binding.Content = typeArgumentNode.Value;
 
                // Offer this explicit type argument to descendants too
                if (supplyCascadingTypeParameters.Contains(typeArgumentNode.TypeParameterName))
                {
                    node.ProvidesCascadingGenericTypes ??= new();
                    node.ProvidesCascadingGenericTypes[typeArgumentNode.TypeParameterName] = new CascadingGenericTypeParameter
                    {
                        GenericTypeNames = new[] { typeArgumentNode.TypeParameterName },
                        ValueType = typeArgumentNode.TypeParameterName,
                        ValueExpression = $"default({binding.Content.Content})",
                    };
                }
            }
 
            if (hasTypeArgumentSpecified)
            {
                // OK this means that the developer has specified at least one type parameter.
                // Either they specified everything and its OK to rewrite, or its an error.
                if (ValidateTypeArguments(node, bindings))
                {
                    var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Node);
                    RewriteTypeNames(new GenericTypeNameRewriter(mappings), node, hasTypeArgumentSpecified);
                }
 
                return;
            }
 
            // OK if we get here that means that no type arguments were specified, so we will try to infer
            // the type.
            //
            // The actual inference is done by the C# compiler, we just emit an a method that represents the
            // use of this component.
 
            // Since we're generating code in a different namespace, we need to 'global qualify' all of the types
            // to avoid clashes with our generated code.
            RewriteTypeNames(new GlobalQualifiedTypeNameRewriter(bindings.Keys), node, hasTypeArgumentSpecified: false, bindings);
 
            //
            // We need to verify that an argument was provided that 'covers' each type parameter.
            //
            // For example, consider a repeater where the generic type is the 'item' type, but the developer has
            // not set the items. We won't be able to do type inference on this and so it will just be nonsense.
            foreach (var attribute in node.Attributes)
            {
                if (attribute != null && TryFindGenericTypeNames(attribute.BoundAttribute, attribute.GloballyQualifiedTypeName, out var typeParameters))
                {
                    // Keep only type parameters defined by this component.
                    typeParameters = typeParameters.Where(bindings.ContainsKey).ToArray();
 
                    var attributeValueIsLambda = SyntaxFactory.ParseExpression(GetContent(attribute)) is LambdaExpressionSyntax;
                    var provideCascadingGenericTypes = new CascadingGenericTypeParameter
                    {
                        GenericTypeNames = typeParameters,
                        ValueType = attribute.GloballyQualifiedTypeName,
                        ValueSourceNode = attribute,
                    };
 
                    foreach (var typeName in typeParameters)
                    {
                        if (supplyCascadingTypeParameters.Contains(typeName))
                        {
                            // Advertise that this particular inferred generic type is available to descendants.
                            // There might be multiple sources for each generic type, so pick the one that has the
                            // fewest other generic types on it. For example if we could infer from either List<T>
                            // or Dictionary<T, U>, we prefer List<T>.
                            node.ProvidesCascadingGenericTypes ??= new();
                            if (!node.ProvidesCascadingGenericTypes.TryGetValue(typeName, out var existingValue)
                                || existingValue.GenericTypeNames.Count > typeParameters.Count)
                            {
                                node.ProvidesCascadingGenericTypes[typeName] = provideCascadingGenericTypes;
                            }
                        }
 
                        if (attributeValueIsLambda)
                        {
                            // For attributes whose values are lambdas, we don't know whether or not the value
                            // covers the generic type - it depends on the content of the lambda.
                            // For example, "() => 123" can cover Func<T>, but "() => null" cannot. So we'll
                            // accept cascaded generic types from ancestors if they are compatible with the lambda,
                            // hence we don't remove it from the list of uncovered generic types until after
                            // we try matching against ancestor cascades.
                            if (bindings.TryGetValue(typeName, out var binding))
                            {
                                binding.CoveredByLambda = true;
                            }
                        }
                        else
                        {
                            bindings.Remove(typeName);
                        }
                    }
                }
            }
 
            // For any remaining bindings, scan up the hierarchy of ancestor components and try to match them
            // with a cascaded generic parameter that can cover this one
            List<CascadingGenericTypeParameter>? receivesCascadingGenericTypes = null;
            foreach (var uncoveredBindingKey in bindings.Keys.ToList())
            {
                foreach (var ancestor in Ancestors)
                {
                    if (ancestor is not ComponentIntermediateNode candidateAncestor)
                    {
                        continue;
                    }
 
                    if (candidateAncestor.ProvidesCascadingGenericTypes != null
                        && candidateAncestor.ProvidesCascadingGenericTypes.TryGetValue(uncoveredBindingKey, out var genericTypeProvider))
                    {
                        // If the parameter value is an expression that includes multiple generic types, we only want
                        // to use it if we want *all* those generic types. That is, a parameter of type MyType<T0, T1>
                        // can supply types to a Child<T0, T1>, but not to a Child<T0>.
                        // This is purely to avoid blowing up the complexity of the implementation here and could be
                        // overcome in the future if we want. We'd need to figure out which extra types are unwanted,
                        // and rewrite them to some unique name, and add that to the generic parameters list of the
                        // inference methods.
                        if (genericTypeProvider.GenericTypeNames.All(GenericTypeIsUsed))
                        {
                            bindings.Remove(uncoveredBindingKey);
                            receivesCascadingGenericTypes ??= new();
                            receivesCascadingGenericTypes.Add(genericTypeProvider);
 
                            // It's sufficient to identify the closest provider for each type parameter
                            break;
                        }
 
                        bool GenericTypeIsUsed(string typeName) => componentTypeParameters
                            .Select(t => t.Name)
                            .Contains(typeName, StringComparer.Ordinal);
                    }
                }
            }
 
            // There are two remaining sources of possible generic type info which we consider
            // lower-priority than cascades from ancestors. Since these two sources *may* actually
            // resolve generic type ambiguities in some cases, we treat them as covering.
            //
            // [1] Attributes given as lambda expressions. These are lower priority than ancestor
            //     cascades because in most cases, lambdas don't provide type info
            foreach (var entryToRemove in bindings.Where(e => e.Value.CoveredByLambda).ToList())
            {
                // Treat this binding as covered, because it's possible that the lambda does provide
                // enough info for type inference to succeed.
                bindings.Remove(entryToRemove.Key);
            }
 
            // [2] Child content parameters, which are nearly always defined as untyped lambdas
            //     (at least, that's what the Razor compiler produces), but can technically be
            //     hardcoded as a RenderFragment<Something> and hence actually give type info.
            foreach (var attribute in node.ChildContents)
            {
                if (TryFindGenericTypeNames(attribute.BoundAttribute, globallyQualifiedTypeName: null, out var typeParameters))
                {
                    foreach (var typeName in typeParameters)
                    {
                        bindings.Remove(typeName);
                    }
                }
            }
 
            // If any bindings remain then this means we would never be able to infer the arguments of this
            // component usage because the user hasn't set properties that include all of the types.
            if (bindings.Count > 0)
            {
                // However we still want to generate 'type inference' code because we want the errors to be as
                // helpful as possible. So let's substitute 'object' for all of those type parameters, and add
                // an error.
                var mappings = bindings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Node);
                RewriteTypeNames(new GenericTypeNameRewriter(mappings), node, bindings: bindings);
 
                node.AddDiagnostic(ComponentDiagnosticFactory.Create_GenericComponentTypeInferenceUnderspecified(node.Source, node, node.Component.GetTypeParameters()));
            }
 
            // Next we need to generate a type inference 'method' node. This represents a method that we will codegen that
            // contains all of the operations on the render tree building. Calling a method to operate on the builder
            // will allow the C# compiler to perform type inference.
            var documentNode = (DocumentIntermediateNode)Ancestors[^1];
            CreateTypeInferenceMethod(documentNode, node, receivesCascadingGenericTypes);
        }
 
        private bool TryFindGenericTypeNames(BoundAttributeDescriptor? boundAttribute, string? globallyQualifiedTypeName, [NotNullWhen(true)] out IReadOnlyList<string>? typeParameters)
        {
            if (boundAttribute == null)
            {
                // Will be null for attributes set on the component that don't match a declared component parameter
                typeParameters = null;
                return false;
            }
 
            if (!boundAttribute.IsGenericTypedProperty())
            {
                typeParameters = null;
                return false;
            }
 
            // Now we need to parse the type name and extract the generic parameters.
            // Two cases;
            // 1. name is a simple identifier like TItem
            // 2. name contains type parameters like Dictionary<string, TItem>
            typeParameters = ParseTypeParameters(globallyQualifiedTypeName ?? boundAttribute.TypeName);
            if (typeParameters.Count == 0)
            {
                typeParameters = new[] { boundAttribute.TypeName };
            }
 
            return true;
        }
 
        private static string GetContent(ComponentAttributeIntermediateNode node)
        {
            return string.Join(string.Empty, node.FindDescendantNodes<CSharpIntermediateToken>().Select(t => t.Content));
        }
 
        private static bool ValidateTypeArguments(ComponentIntermediateNode node, Dictionary<string, Binding> bindings)
        {
            var missing = new List<BoundAttributeDescriptor>();
            foreach (var (_, binding) in bindings)
            {
                if (binding.Node == null || string.IsNullOrWhiteSpace(binding.Content?.Content))
                {
                    missing.Add(binding.Attribute);
                }
            }
 
            if (missing.Count > 0)
            {
                // We add our own error for this because its likely the user will see other errors due
                // to incorrect codegen without the types. Our errors message will pretty clearly indicate
                // what to do, whereas the other errors might be confusing.
                node.AddDiagnostic(ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument(node.Source, node, missing));
                return false;
            }
 
            return true;
        }
 
        private void RewriteTypeNames(TypeNameRewriter rewriter, ComponentIntermediateNode node, bool? hasTypeArgumentSpecified = null, IDictionary<string, Binding>? bindings = null)
        {
            // Rewrite the component type name
            rewriter.RewriteComponentTypeName(node);
 
            foreach (var attribute in node.Attributes)
            {
                attribute.ConcreteContainingType = node.TypeName;
 
                var globallyQualifiedTypeName = attribute.BoundAttribute?.GetGloballyQualifiedTypeName();
 
                if (attribute.TypeName != null)
                {
                    globallyQualifiedTypeName = rewriter.Rewrite(globallyQualifiedTypeName ?? attribute.TypeName);
                    attribute.GloballyQualifiedTypeName = globallyQualifiedTypeName;
                }
 
                if (attribute.BoundAttribute?.IsGenericTypedProperty() == true && attribute.TypeName != null)
                {
                    // If we know the type name, then replace any generic type parameter inside it with
                    // the known types.
                    attribute.TypeName = globallyQualifiedTypeName;
                    // This is a special case in which we are dealing with a property TItem.
                    // Given that TItem can have been defined explicitly by the user to a partially
                    // qualified type, (like MyType), we check if the globally qualified type name
                    // contains "global::" which will be the case in all cases as we've computed
                    // this information from the Roslyn symbol except for when the symbol is a generic
                    // type parameter. In which case, we mark it with an additional annotation to
                    // account for that during code generation and avoid trying to fully qualify
                    // the type name.
                    Debug.Assert(bindings != null || hasTypeArgumentSpecified == true);
                    if (hasTypeArgumentSpecified == true)
                    {
                        attribute.HasExplicitTypeName = true;
                    }
                    else if (attribute.BoundAttribute?.IsEventCallbackProperty() ?? false)
                    {
                        Debug.Assert(attribute.TypeName is not null);
                        var typeParameters = ParseTypeParameters(attribute.TypeName);
                        for (int i = 0; i < typeParameters.Count; i++)
                        {
                            var parameter = typeParameters[i];
                            if (bindings!.ContainsKey(parameter))
                            {
                                attribute.IsOpenGeneric = true;
                                break;
                            }
                        }
                    }
                }
                else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsDelegateProperty() ?? false))
                {
                    // This is a weakly typed delegate, treat it as Action<object>
                    attribute.TypeName = "global::System.Action<global::System.Object>";
                }
                else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsEventCallbackProperty() ?? false))
                {
                    // This is a weakly typed event-callback, treat it as EventCallback (non-generic)
                    attribute.TypeName = $"global::{ComponentsApi.EventCallback.FullTypeName}";
                }
                else if (attribute.TypeName == null)
                {
                    // This is a weakly typed attribute, treat it as System.Object
                    attribute.TypeName = "global::System.Object";
                }
            }
 
            foreach (var capture in node.Captures)
            {
                if (capture.IsComponentCapture)
                {
                    capture.UpdateComponentCaptureTypeName(rewriter.Rewrite(capture.ComponentCaptureTypeName));
                }
            }
 
            foreach (var childContent in node.ChildContents)
            {
                if (childContent.BoundAttribute?.IsGenericTypedProperty() == true && childContent.TypeName != null)
                {
                    // If we know the type name, then replace any generic type parameter inside it with
                    // the known types.
                    childContent.TypeName = rewriter.Rewrite(childContent.TypeName);
                }
                else if (childContent.IsParameterized)
                {
                    // This is a non-generic parameterized child content like RenderFragment<int>, leave it as is.
                }
                else
                {
                    // This is a weakly typed child content, treat it as RenderFragment
                    childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName;
                }
            }
        }
 
        private void CreateTypeInferenceMethod(DocumentIntermediateNode documentNode, ComponentIntermediateNode node, List<CascadingGenericTypeParameter>? receivesCascadingGenericTypes)
        {
            var @namespace = documentNode.FindPrimaryNamespace().AssumeNotNull().Name;
            @namespace = string.IsNullOrEmpty(@namespace) ? "__Blazor" : "__Blazor." + @namespace;
            @namespace += "." + documentNode.FindPrimaryClass().AssumeNotNull().Name;
 
            using var genericTypeConstraints = new PooledArrayBuilder<string>();
 
            foreach (var attribute in node.Component.BoundAttributes)
            {
                if (attribute.Metadata is TypeParameterMetadata { Constraints: string constraints })
                {
                    genericTypeConstraints.Add(constraints);
                }
            }
 
            var typeInferenceNode = new ComponentTypeInferenceMethodIntermediateNode()
            {
                Component = node,
 
                // Method name is generated and guaranteed not to collide, since it's unique for each
                // component call site.
                MethodName = $"Create{CSharpIdentifier.SanitizeIdentifier(node.TagName.AsSpanOrDefault())}_{_id++}",
                FullTypeName = @namespace + ".TypeInference",
 
                ReceivesCascadingGenericTypes = receivesCascadingGenericTypes,
                GenericTypeConstraints = genericTypeConstraints.ToArrayAndClear()
            };
 
            node.TypeInferenceNode = typeInferenceNode;
 
            // Now we need to insert the type inference node into the tree.
            var namespaceNode = documentNode.Children
                .OfType<NamespaceDeclarationIntermediateNode>()
                .FirstOrDefault(n => n.IsGenericTyped);
            if (namespaceNode == null)
            {
                namespaceNode = new NamespaceDeclarationIntermediateNode()
                {
                    Name = @namespace,
                    IsGenericTyped = true,
                };
 
                documentNode.Children.Add(namespaceNode);
            }
 
            var classNode = namespaceNode.Children
                .OfType<ClassDeclarationIntermediateNode>()
                .FirstOrDefault(n => n.Name == "TypeInference");
            if (classNode == null)
            {
                classNode = new ClassDeclarationIntermediateNode()
                {
                    Name = "TypeInference",
                    Modifiers = CommonModifiers.InternalStatic
                };
                namespaceNode.Children.Add(classNode);
            }
 
            classNode.Children.Add(typeInferenceNode);
        }
 
        public IReadOnlyList<string> ParseTypeParameters(string typeName)
        {
            if (typeName == null)
            {
                throw new ArgumentNullException(nameof(typeName));
            }
 
            var parsed = SyntaxFactory.ParseTypeName(typeName);
 
            if (parsed is IdentifierNameSyntax identifier)
            {
                return Array.Empty<string>();
            }
 
            if (TryParseCore(parsed) is { IsDefault: false } list)
            {
                return list;
            }
 
            return parsed.DescendantNodesAndSelf()
                .OfType<TypeArgumentListSyntax>()
                .SelectMany(arg => arg.Arguments)
                .SelectMany(t => ParseCore(t)).ToArray();
 
            static ImmutableArray<string> TryParseCore(TypeSyntax parsed)
            {
                if (parsed is ArrayTypeSyntax array)
                {
                    return ParseCore(array.ElementType);
                }
 
                if (parsed is TupleTypeSyntax tuple)
                {
                    return tuple.Elements.SelectManyAsArray(a => ParseCore(a.Type));
                }
 
                return default;
            }
 
            static ImmutableArray<string> ParseCore(TypeSyntax parsed)
            {
                // Recursively drill into arrays `T[]` and tuples `(T, T)`.
                if (TryParseCore(parsed) is { IsDefault: false } list)
                {
                    return list;
                }
 
                // When we encounter an identifier, we assume it's a type parameter.
                if (parsed is IdentifierNameSyntax identifier)
                {
                    return ImmutableArray.Create(identifier.Identifier.Text);
                }
 
                // Generic names like `C<T>` are ignored here because we will visit their type argument list
                // via the `.DescendantNodesAndSelf().OfType<TypeArgumentListSyntax>()` call above.
                return ImmutableArray<string>.Empty;
            }
        }
    }
 
 
 
    private class Binding
    {
        public Binding(BoundAttributeDescriptor attribute) => Attribute = attribute;
 
        public BoundAttributeDescriptor Attribute { get; }
 
        public IntermediateToken? Content { get; set; }
 
        public ComponentTypeArgumentIntermediateNode? Node { get; set; }
 
        public bool CoveredByLambda { get; set; }
    }
}