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>(); foreach (var attribute in node.Component.GetTypeParameters()) { bindings.Add(attribute.Name, new Binding() { Attribute = 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 = GetContent(typeArgumentNode); } 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.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); } 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(_pass.TypeNameFeature.CreateGlobalQualifiedTypeNameRewriter(bindings.Keys), node); // // 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. var attributes = node.Attributes.Select(a => a.BoundAttribute).Concat(node.ChildContents.Select(c => c.BoundAttribute)); foreach (var attribute in attributes) { if (attribute == null) { // Will be null for attributes set on the component that don't match a declared component parameter continue; } // 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> if (!attribute.IsGenericTypedProperty()) { continue; } var typeParameters = _pass.TypeNameFeature.ParseTypeParameters(attribute.TypeName); if (typeParameters.Count == 0) { bindings.Remove(attribute.TypeName); } else { for (var i = 0; i < typeParameters.Count; i++) { var typeParameter = typeParameters[i]; bindings.Remove(typeParameter.ToString()); } } } // 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.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); node.Diagnostics.Add(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[Ancestors.Count - 1]; CreateTypeInferenceMethod(documentNode, 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 = 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 = GetContent(typeArgumentNode); // 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})", }; } } 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.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); } 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(_pass.TypeNameFeature.CreateGlobalQualifiedTypeNameRewriter(bindings.Keys), node); // // 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, out var typeParameters)) { var attributeValueIsLambda = _pass.TypeNameFeature.IsLambda(GetContent(attribute)); var provideCascadingGenericTypes = new CascadingGenericTypeParameter { GenericTypeNames = typeParameters, ValueType = attribute.BoundAttribute.TypeName, 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()) { var uncoveredBinding = bindings[uncoveredBindingKey]; foreach (var candidateAncestor in Ancestors.OfType <ComponentIntermediateNode>()) { 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, 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.Content); RewriteTypeNames(_pass.TypeNameFeature.CreateGenericTypeRewriter(mappings), node); node.Diagnostics.Add(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[Ancestors.Count - 1]; CreateTypeInferenceMethod(documentNode, node, receivesCascadingGenericTypes); }