private ComponentChildContentIntermediateNode RewriteChildContent(BoundAttributeDescriptor attribute, SourceSpan?source, IntermediateNodeCollection children)
            {
                var childContent = new ComponentChildContentIntermediateNode()
                {
                    BoundAttribute = attribute,
                    Source         = source,
                    TypeName       = attribute?.TypeName ?? ComponentsApi.RenderFragment.FullTypeName,
                };

                // There are two cases here:
                // 1. Implicit child content - the children will be non-taghelper nodes, just accept them
                // 2. Explicit child content - the children will be various tag helper nodes, that need special processing.
                for (var i = 0; i < children.Count; i++)
                {
                    var child = children[i];
                    if (child is TagHelperBodyIntermediateNode body)
                    {
                        // The body is all of the content we want to render, the rest of the children will
                        // be the attributes.
                        for (var j = 0; j < body.Children.Count; j++)
                        {
                            childContent.Children.Add(body.Children[j]);
                        }
                    }
                    else if (child is TagHelperPropertyIntermediateNode property)
                    {
                        if (property.BoundAttribute.IsChildContentParameterNameProperty())
                        {
                            // Check for each child content with a parameter name, that the parameter name is specified
                            // with literal text. For instance, the following is not allowed and should generate a diagnostic.
                            //
                            // <MyComponent><ChildContent Context="@Foo()">...</ChildContent></MyComponent>
                            if (TryGetAttributeStringContent(property, out var parameterName))
                            {
                                childContent.ParameterName = parameterName;
                                continue;
                            }

                            // The parameter name is invalid.
                            childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameter(property.Source, property.AttributeName, attribute.Name));
                            continue;
                        }

                        // This is an unrecognized attribute, this is possible if you try to do something like put 'ref' on a child content.
                        childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(property.Source, property.AttributeName, attribute.Name));
                    }
                    else if (child is TagHelperHtmlAttributeIntermediateNode a)
                    {
                        // This is an HTML attribute on a child content.
                        childContent.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidAttribute(a.Source, a.AttributeName, attribute.Name));
                    }
                    else
                    {
                        // This is some other kind of node (likely an implicit child content)
                        childContent.Children.Add(child);
                    }
                }

                return(childContent);
            }
            public override void VisitComponentChildContent(ComponentChildContentIntermediateNode node)
            {
                // Check that each child content has a unique parameter name within its scope. This is important
                // because the parameter name can be implicit, and it doesn't work well when nested.
                if (node.IsParameterized)
                {
                    for (var i = 0; i < Ancestors.Count - 1; i++)
                    {
                        var ancestor = Ancestors[i] as ComponentChildContentIntermediateNode;
                        if (ancestor != null &&
                            ancestor.IsParameterized &&
                            string.Equals(node.ParameterName, ancestor.ParameterName, StringComparison.Ordinal))
                        {
                            // Duplicate name. We report an error because this will almost certainly also lead to an error
                            // from the C# compiler that's way less clear.
                            node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentRepeatedParameterName(
                                                     node.Source,
                                                     node,
                                                     (ComponentIntermediateNode)Ancestors[0],       // Enclosing component
                                                     ancestor,                                      // conflicting child content node
                                                     (ComponentIntermediateNode)Ancestors[i + 1])); // Enclosing component of conflicting child content node
                        }
                    }
                }

                base.VisitDefault(node);
            }
        private static IReadOnlyList <IntermediateToken> GetAttributeContent(TagHelperPropertyIntermediateNode node)
        {
            var template = node.FindDescendantNodes <TemplateIntermediateNode>().FirstOrDefault();

            if (template != null)
            {
                // See comments in TemplateDiagnosticPass
                node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source));
                return(new[] { new IntermediateToken()
                               {
                                   Kind = TokenKind.CSharp, Content = string.Empty,
                               }, });
            }

            if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
            {
                // This case can be hit for a 'string' attribute. We want to turn it into
                // an expression.
                var tokens = htmlContentNode.FindDescendantNodes <IntermediateToken>();

                var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\"";
                return(new[] { new IntermediateToken()
                               {
                                   Content = content, Kind = TokenKind.CSharp,
                               } });
            }
            else
            {
                return(node.FindDescendantNodes <IntermediateToken>());
            }
        }
            public override void VisitMarkupElement(MarkupElementIntermediateNode node)
            {
                // Disallow <script> in components as per #552
                if (string.Equals(node.TagName, "script", StringComparison.OrdinalIgnoreCase))
                {
                    for (var i = 0; i < node.Children.Count; i++)
                    {
                        // We allow you to suppress this error like:
                        // <script suppress-error="BL9992" />
                        var attribute = node.Children[i] as HtmlAttributeIntermediateNode;
                        if (attribute != null &&
                            attribute.AttributeName == "suppress-error" &&
                            attribute.Children.Count == 1 &&
                            attribute.Children[0] is HtmlAttributeValueIntermediateNode value &&
                            value.Children.Count == 1 &&
                            value.Children[0] is IntermediateToken token &&
                            token.IsHtml &&
                            string.Equals(token.Content, "BL9992", StringComparison.Ordinal))
                        {
                            node.Children.RemoveAt(i);
                            return;
                        }
                    }

                    var diagnostic = ComponentDiagnosticFactory.Create_DisallowedScriptTag(node.Source);
                    node.Diagnostics.Add(diagnostic);
                }

                base.VisitDefault(node);
            }
Example #5
0
 private void ProcessAttributes(TagHelperIntermediateNode node)
 {
     for (var i = node.Children.Count - 1; i >= 0; i--)
     {
         if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode)
         {
             if (TrySimplifyContent(propertyNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper()))
             {
                 node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent(
                                          propertyNode,
                                          propertyNode.AttributeName));
                 node.Children.RemoveAt(i);
                 continue;
             }
         }
         else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
         {
             if (TrySimplifyContent(htmlNode) && node.TagHelpers.Any(t => t.IsComponentTagHelper()))
             {
                 node.Diagnostics.Add(ComponentDiagnosticFactory.Create_UnsupportedComplexContent(
                                          htmlNode,
                                          htmlNode.AttributeName));
                 node.Children.RemoveAt(i);
                 continue;
             }
         }
     }
 }
Example #6
0
        protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
        {
            if (!IsComponentDocument(documentNode))
            {
                return;
            }

            var @namespace = documentNode.FindPrimaryNamespace();
            var @class     = documentNode.FindPrimaryClass();

            if (@namespace == null || @class == null)
            {
                // Nothing to do, bail. We can't function without the standard structure.
                return;
            }

            // For each component *usage* we need to rewrite the tag helper node to map to the relevant component
            // APIs.
            var references = documentNode.FindDescendantReferences <TagHelperIntermediateNode>();

            for (var i = 0; i < references.Count; i++)
            {
                var reference = references[i];
                var node      = (TagHelperIntermediateNode)reference.Node;
                if (node.TagHelpers.Any(t => t.IsChildContentTagHelper()))
                {
                    // This is a child content tag helper. This will be rewritten when we visit its parent.
                    continue;
                }

                // The element didn't match any child content descriptors. Look for any matching component descriptors.
                var count = 0;
                for (var j = 0; j < node.TagHelpers.Count; j++)
                {
                    if (node.TagHelpers[j].IsComponentTagHelper())
                    {
                        // Only allow a single component tag helper per element. If there are multiple, we'll just consider
                        // the first one and ignore the others.
                        if (++count > 1)
                        {
                            node.Diagnostics.Add(ComponentDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers));
                            break;
                        }
                    }
                }

                if (count >= 1)
                {
                    reference.Replace(RewriteAsComponent(node, node.TagHelpers.First(t => t.IsComponentTagHelper())));
                }
                else
                {
                    reference.Replace(RewriteAsElement(node));
                }
            }
        }
        public sealed override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node)
        {
            // We used to support syntaxes like <elem onsomeevent=@{ /* some C# code */ } /> but this is no longer the
            // case.
            //
            // We provide an error for this case just to be friendly.
            var content = string.Join("", node.Children.OfType <IntermediateToken>().Select(t => t.Content));

            context.Diagnostics.Add(ComponentDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content));
            return;
        }
Example #8
0
        private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode node)
        {
            var template = node.FindDescendantNodes <TemplateIntermediateNode>().FirstOrDefault();

            if (template != null)
            {
                // See comments in TemplateDiagnosticPass
                node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source));
                return(new IntermediateToken()
                {
                    Kind = TokenKind.CSharp, Content = string.Empty,
                });
            }

            if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
            {
                // This case can be hit for a 'string' attribute. We want to turn it into
                // an expression.
                var content = "\"" + string.Join(string.Empty, htmlContentNode.Children.OfType <IntermediateToken>().Select(t => t.Content)) + "\"";
                return(new IntermediateToken()
                {
                    Kind = TokenKind.CSharp, Content = content
                });
            }
            else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
            {
                // This case can be hit when the attribute has an explicit @ inside, which
                // 'escapes' any special sugar we provide for codegen.
                return(GetToken(cSharpNode));
            }
            else
            {
                // This is the common case for 'mixed' content
                return(GetToken(node));
            }

            IntermediateToken GetToken(IntermediateNode parent)
            {
                if (parent.Children.Count == 1 && parent.Children[0] is IntermediateToken token)
                {
                    return(token);
                }

                // In error cases we won't have a single token, but we still want to generate the code.
                return(new IntermediateToken()
                {
                    Kind = TokenKind.CSharp,
                    Content = string.Join(string.Empty, parent.Children.OfType <IntermediateToken>().Select(t => t.Content)),
                });
            }
        }
        private void ProcessDuplicates(IntermediateNode parent)
        {
            // Reverse order because we will remove nodes.
            //
            // Each 'property' node could be duplicated if there are multiple tag helpers that match that
            // particular attribute. This is likely to happen when a component also defines something like
            // OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component.
            for (var i = parent.Children.Count - 1; i >= 0; i--)
            {
                var eventHandler = parent.Children[i] as TagHelperPropertyIntermediateNode;
                if (eventHandler != null &&
                    eventHandler.TagHelper != null &&
                    eventHandler.TagHelper.IsEventHandlerTagHelper())
                {
                    for (var j = 0; j < parent.Children.Count; j++)
                    {
                        var componentAttribute = parent.Children[j] as ComponentAttributeIntermediateNode;
                        if (componentAttribute != null &&
                            componentAttribute.TagHelper != null &&
                            componentAttribute.TagHelper.IsComponentTagHelper() &&
                            componentAttribute.AttributeName == eventHandler.AttributeName)
                        {
                            // Found a duplicate - remove the 'fallback' in favor of the component's own handling.
                            parent.Children.RemoveAt(i);
                            break;
                        }
                    }
                }
            }

            // If we still have duplicates at this point then they are genuine conflicts.
            var duplicates = parent.Children
                             .OfType <TagHelperPropertyIntermediateNode>()
                             .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
                             .GroupBy(p => p.AttributeName)
                             .Where(g => g.Count() > 1);

            foreach (var duplicate in duplicates)
            {
                parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandler_Duplicates(
                                           parent.Source,
                                           duplicate.Key,
                                           duplicate.ToArray()));
                foreach (var property in duplicate)
                {
                    parent.Children.Remove(property);
                }
            }
        }
            public override void VisitComponent(ComponentIntermediateNode node)
            {
                // Check for properties that are set by both element contents (body) and the attribute itself.
                foreach (var childContent in node.ChildContents)
                {
                    foreach (var attribute in node.Attributes)
                    {
                        if (attribute.AttributeName == childContent.AttributeName)
                        {
                            node.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentSetByAttributeAndBody(
                                                     attribute.Source,
                                                     attribute.AttributeName));
                        }
                    }
                }

                base.VisitDefault(node);
            }
            public override void VisitComponent(ComponentIntermediateNode node)
            {
                for (var i = 0; i < node.Children.Count; i++)
                {
                    // Note that we don't handle ChildContent cases here. Those have their own pass for diagnostics.
                    if (node.Children[i] is ComponentAttributeIntermediateNode attribute && attribute.AttributeName != null)
                    {
                        if (_attributes.TryGetValue(attribute.AttributeName, out var other))
                        {
                            // As a special case we want to point it out explicitly where a directive or other construct
                            // has emitted an attribute that causes a conflict. We're already looking at the lowered version
                            // of this construct, so it's easy to detect. We just need the original name to report the issue.
                            //
                            // Example: `bind-Value` will set `Value` and `ValueChanged`.
                            var originalAttributeName =
                                attribute.Annotations[ComponentMetadata.Common.OriginalAttributeName] as string ??
                                other.node.Annotations[ComponentMetadata.Common.OriginalAttributeName] as string;
                            if (originalAttributeName != null)
                            {
                                other.node.Diagnostics.Add(ComponentDiagnosticFactory.Create_DuplicateComponentParameterDirective(
                                                               other.name,
                                                               originalAttributeName,
                                                               other.node.Source ?? node.Source));
                            }
                            else
                            {
                                // This is a conflict in the code the user wrote.
                                other.node.Diagnostics.Add(ComponentDiagnosticFactory.Create_DuplicateComponentParameter(
                                                               other.name,
                                                               other.node.Source ?? node.Source));
                            }
                        }

                        // Replace the attribute we were previously tracking. Then if you have three, the two on the left will have
                        // diagnostics.
                        _attributes[attribute.AttributeName] = (attribute.AttributeName, attribute);
                    }
                }

                _attributes.Clear();
                base.VisitComponent(node);
            }
        protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
        {
            if (!IsComponentDocument(documentNode))
            {
                return;
            }

            var visitor = new Visitor();

            visitor.Visit(documentNode);

            for (var i = 0; i < visitor.Candidates.Count; i++)
            {
                var candidate = visitor.Candidates[i];
                candidate.Parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(candidate.Node.Source));

                // Remove the offending node since we don't know how to render it. This means that the user won't get C#
                // completion at this location, which is fine because it's inside an HTML attribute.
                candidate.Remove();
            }
        }
Example #13
0
            public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node)
            {
                // Each 'tag helper property' belongs to a specific tag helper. We want to handle
                // the cases for components, but leave others alone. This allows our other passes
                // to handle those cases.
                if (!node.TagHelper.IsComponentTagHelper())
                {
                    _children.Add(node);
                    return;
                }

                // Another special case here - this might be a type argument. These don't represent 'real' parameters
                // that get passed to the component, it needs special code generation support.
                if (node.TagHelper.IsGenericTypedComponent() && node.BoundAttribute.IsTypeParameterProperty())
                {
                    _children.Add(new ComponentTypeArgumentIntermediateNode(node));
                    return;
                }

                // Another special case here -- this might be a 'Context' parameter, which specifies the name
                // for lambda parameter for parameterized child content
                if (node.BoundAttribute.IsChildContentParameterNameProperty())
                {
                    // Check for each child content with a parameter name, that the parameter name is specified
                    // with literal text. For instance, the following is not allowed and should generate a diagnostic.
                    //
                    // <MyComponent Context="@Foo()">...</MyComponent>
                    if (TryGetAttributeStringContent(node, out var parameterName))
                    {
                        _component.ChildContentParameterName = parameterName;
                        return;
                    }

                    // The parameter name is invalid.
                    _component.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentHasInvalidParameterOnComponent(node.Source, node.AttributeName, _component.TagName));
                    return;
                }

                _children.Add(new ComponentAttributeIntermediateNode(node));
            }
        private bool ShouldGenerateField(IntermediateNode parent)
        {
            var parameters = parent.FindDescendantNodes <TagHelperDirectiveAttributeParameterIntermediateNode>();

            for (var i = 0; i < parameters.Count; i++)
            {
                var parameter = parameters[i];
                if (parameter.TagHelper.IsRefTagHelper() && parameter.BoundAttributeParameter.Name == "suppressField")
                {
                    if (parameter.HasDiagnostics)
                    {
                        parent.Diagnostics.AddRange(parameter.GetAllDiagnostics());
                    }

                    parent.Children.Remove(parameter);

                    if (parameter.AttributeStructure == AttributeStructure.Minimized)
                    {
                        return(false);
                    }

                    // We do not support non-minimized attributes here because we can't allow the value to be dynamic.
                    // As a design/experience decision, we don't let you write @ref:suppressField="false" even though
                    // we could parse it. The rationale is that it's misleading, you type something that looks like code,
                    // but it's not really.
                    parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_RefSuppressFieldNotMinimized(parameter.Source));
                }
            }

            if (parent is ComponentIntermediateNode component && component.Component.IsGenericTypedComponent())
            {
                // We cannot automatically generate a 'ref' field for generic components because we don't know
                // how to write the type.
                parent.Diagnostics.Add(ComponentDiagnosticFactory.Create_RefSuppressFieldRequiredForGeneric(parent.Source));
                return(false);
            }

            return(true);
        }
            private static bool ValidateTypeArguments(ComponentIntermediateNode node, Dictionary <string, Binding> bindings)
            {
                var missing = new List <BoundAttributeDescriptor>();

                foreach (var binding in bindings)
                {
                    if (binding.Value.Node == null || string.IsNullOrWhiteSpace(binding.Value.Content))
                    {
                        missing.Add(binding.Value.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.Diagnostics.Add(ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument(node.Source, node, missing));
                    return(false);
                }

                return(true);
            }
        /// <inheritdoc />
        protected override void OnDocumentStructureCreated(
            RazorCodeDocument codeDocument,
            NamespaceDeclarationIntermediateNode @namespace,
            ClassDeclarationIntermediateNode @class,
            MethodDeclarationIntermediateNode method)
        {
            if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var computedNamespace) ||
                !TryComputeClassName(codeDocument, out var computedClass))
            {
                // If we can't compute a nice namespace (no relative path) then just generate something
                // mangled.
                computedNamespace = FallbackRootNamespace;
                var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum());
                computedClass = $"AspNetCore_{checksum}";
            }

            var documentNode = codeDocument.GetDocumentIntermediateNode();

            if (char.IsLower(computedClass, 0))
            {
                // We don't allow component names to start with a lowercase character.
                documentNode.Diagnostics.Add(
                    ComponentDiagnosticFactory.Create_ComponentNamesCannotStartWithLowerCase(computedClass, documentNode.Source));
            }

            if (MangleClassNames)
            {
                computedClass = ComponentMetadata.MangleClassName(computedClass);
            }

            @namespace.Content = computedNamespace;
            @class.ClassName   = computedClass;
            @class.Modifiers.Clear();
            @class.Modifiers.Add("public");
            @class.Modifiers.Add("partial");

            if (FileKinds.IsComponentImport(codeDocument.GetFileKind()))
            {
                // We don't want component imports to be considered as real component.
                // But we still want to generate code for it so we can get diagnostics.
                @class.BaseType = typeof(object).FullName;

                method.ReturnType = "void";
                method.MethodName = "Execute";
                method.Modifiers.Clear();
                method.Modifiers.Add("protected");

                method.Parameters.Clear();
            }
            else
            {
                @class.BaseType = ComponentsApi.ComponentBase.FullTypeName;

                var typeParamReferences = documentNode.FindDirectiveReferences(ComponentTypeParamDirective.Directive);
                for (var i = 0; i < typeParamReferences.Count; i++)
                {
                    var typeParamNode = (DirectiveIntermediateNode)typeParamReferences[i].Node;
                    if (typeParamNode.HasDiagnostics)
                    {
                        continue;
                    }

                    @class.TypeParameters.Add(new TypeParameter()
                    {
                        ParameterName = typeParamNode.Tokens.First().Content,
                        Constraints   = typeParamNode.Tokens.Skip(1).FirstOrDefault()?.Content
                    });
                }

                method.ReturnType = "void";
                method.MethodName = ComponentsApi.ComponentBase.BuildRenderTree;
                method.Modifiers.Clear();
                method.Modifiers.Add("protected");
                method.Modifiers.Add("override");

                method.Parameters.Clear();
                method.Parameters.Add(new MethodParameter()
                {
                    ParameterName = ComponentsApi.RenderTreeBuilder.BuilderParameter,
                    TypeName      = ComponentsApi.RenderTreeBuilder.FullTypeName,
                });
            }
        }
            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);
            }
Example #18
0
            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);
            }
Example #19
0
        private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node)
        {
            // Bind works similarly to a macro, it always expands to code that the user could have written.
            //
            // For the nodes that are related to the bind-attribute rewrite them to look like a pair of
            // 'normal' HTML attributes similar to the following transformation.
            //
            // Input:   <MyComponent bind-Value="@currentCount" />
            // Output:  <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." />
            //
            // This means that the expression that appears inside of 'bind' must be an LValue or else
            // there will be errors. In general the errors that come from C# in this case are good enough
            // to understand the problem.
            //
            // The BindMethods calls are required in this case because to give us a good experience. They
            // use overloading to ensure that can get an Action<object> that will convert and set an arbitrary
            // value.
            //
            // We also assume that the element will be treated as a component for now because
            // multiple passes handle 'special' tag helpers. We have another pass that translates
            // a tag helper node back into 'regular' element when it doesn't have an associated component
            if (!TryComputeAttributeNames(
                    parent,
                    node,
                    node.AttributeName,
                    out var valueAttributeName,
                    out var changeAttributeName,
                    out var valueAttribute,
                    out var changeAttribute))
            {
                // Skip anything we can't understand. It's important that we don't crash, that will bring down
                // the build.
                node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_InvalidSyntax(
                                         node.Source,
                                         node.AttributeName));
                return(new[] { node });
            }

            var original = GetAttributeContent(node);

            if (string.IsNullOrEmpty(original.Content))
            {
                // This can happen in error cases, the parser will already have flagged this
                // as an error, so ignore it.
                return(new[] { node });
            }

            // Look for a matching format node. If we find one then we need to pass the format into the
            // two nodes we generate.
            IntermediateToken format = null;

            if (TryGetFormatNode(
                    parent,
                    node,
                    valueAttributeName,
                    out var formatNode))
            {
                // Don't write the format out as its own attribute, just capture it as a string
                // or expression.
                parent.Children.Remove(formatNode);
                format = GetAttributeContent(formatNode);
            }

            // Now rewrite the content of the value node to look like:
            //
            // BindMethods.GetValue(<code>) OR
            // BindMethods.GetValue(<code>, <format>)
            var valueExpressionTokens = new List <IntermediateToken>();

            valueExpressionTokens.Add(new IntermediateToken()
            {
                Content = $"{ComponentsApi.BindMethods.GetValue}(",
                Kind    = TokenKind.CSharp
            });
            valueExpressionTokens.Add(original);
            if (!string.IsNullOrEmpty(format?.Content))
            {
                valueExpressionTokens.Add(new IntermediateToken()
                {
                    Content = ", ",
                    Kind    = TokenKind.CSharp,
                });
                valueExpressionTokens.Add(format);
            }
            valueExpressionTokens.Add(new IntermediateToken()
            {
                Content = ")",
                Kind    = TokenKind.CSharp,
            });

            // Now rewrite the content of the change-handler node. There are two cases we care about
            // here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect
            // component attributes to always 'match' on type.
            //
            // __value => <code> = __value
            //
            // For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
            // so we use BindMethods.SetValueHandler
            //
            // BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
            // BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
            //
            // Note that the linemappings here are applied to the value attribute, not the change attribute.

            string changeExpressionContent = null;

            if (changeAttribute == null && format == null)
            {
                changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
            }
            else if (changeAttribute == null && format != null)
            {
                changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})";
            }
            else
            {
                changeExpressionContent = $"__value => {original.Content} = __value";
            }
            var changeExpressionTokens = new List <IntermediateToken>()
            {
                new IntermediateToken()
                {
                    Content = changeExpressionContent,
                    Kind    = TokenKind.CSharp
                }
            };

            if (parent is MarkupElementIntermediateNode)
            {
                var valueNode = new HtmlAttributeIntermediateNode()
                {
                    AttributeName = valueAttributeName,
                    Source        = node.Source,

                    Prefix = valueAttributeName + "=\"",
                    Suffix = "\"",
                };

                for (var i = 0; i < node.Diagnostics.Count; i++)
                {
                    valueNode.Diagnostics.Add(node.Diagnostics[i]);
                }

                valueNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
                for (var i = 0; i < valueExpressionTokens.Count; i++)
                {
                    valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
                }

                var changeNode = new HtmlAttributeIntermediateNode()
                {
                    AttributeName = changeAttributeName,
                    Source        = node.Source,

                    Prefix = changeAttributeName + "=\"",
                    Suffix = "\"",
                };

                changeNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
                for (var i = 0; i < changeExpressionTokens.Count; i++)
                {
                    changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
                }

                return(new[] { valueNode, changeNode });
            }
            else
            {
                var valueNode = new ComponentAttributeIntermediateNode(node)
                {
                    AttributeName  = valueAttributeName,
                    BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute
                    PropertyName   = valueAttribute?.GetPropertyName(),
                    TagHelper      = valueAttribute == null ? null : node.TagHelper,
                    TypeName       = valueAttribute?.IsWeaklyTyped() == false ? valueAttribute.TypeName : null,
                };

                valueNode.Children.Clear();
                valueNode.Children.Add(new CSharpExpressionIntermediateNode());
                for (var i = 0; i < valueExpressionTokens.Count; i++)
                {
                    valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
                }

                var changeNode = new ComponentAttributeIntermediateNode(node)
                {
                    AttributeName  = changeAttributeName,
                    BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute
                    PropertyName   = changeAttribute?.GetPropertyName(),
                    TagHelper      = changeAttribute == null ? null : node.TagHelper,
                    TypeName       = changeAttribute?.IsWeaklyTyped() == false ? changeAttribute.TypeName : null,
                };

                changeNode.Children.Clear();
                changeNode.Children.Add(new CSharpExpressionIntermediateNode());
                for (var i = 0; i < changeExpressionTokens.Count; i++)
                {
                    changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
                }

                return(new[] { valueNode, changeNode });
            }
        }
Example #20
0
            public override void VisitTagHelperBody(TagHelperBodyIntermediateNode node)
            {
                // Wrap the component's children in a ChildContent node if we have some significant
                // content.
                if (node.Children.Count == 0)
                {
                    return;
                }

                // If we get a single HTML content node containing only whitespace,
                // then this is probably a tag that looks like '<MyComponent>  </MyComponent>
                //
                // We don't want to create a child content for this case, because it can conflict
                // with a child content that's set via an attribute. We don't want the formatting
                // of insignificant whitespace to be annoying when setting attributes directly.
                if (node.Children.Count == 1 && IsIgnorableWhitespace(node.Children[0]))
                {
                    return;
                }

                // From here we fork and behave differently based on whether the component's child content is
                // implicit or explicit.
                //
                // Explicit child content will look like: <MyComponent><ChildContent><div>...</div></ChildContent></MyComponent>
                // compared with implicit: <MyComponent><div></div></MyComponent>
                //
                // Using implicit child content:
                // 1. All content is grouped into a single child content lambda, and assigned to the property 'ChildContent'
                //
                // Using explicit child content:
                // 1. All content must be contained within 'child content' elements that are direct children
                // 2. Whitespace outside of 'child content' elements will be ignored (not an error)
                // 3. Non-whitespace outside of 'child content' elements will cause an error
                // 4. All 'child content' elements must match parameters on the component (exception for ChildContent,
                //    which is always allowed.
                // 5. Each 'child content' element will generate its own lambda, and be assigned to the property
                //    that matches the element name.
                if (!node.Children.OfType <TagHelperIntermediateNode>().Any(t => t.TagHelpers.Any(th => th.IsChildContentTagHelper())))
                {
                    // This node has implicit child content. It may or may not have an attribute that matches.
                    var attribute = _component.Component.BoundAttributes
                                    .Where(a => string.Equals(a.Name, ComponentsApi.RenderTreeBuilder.ChildContent, StringComparison.Ordinal))
                                    .FirstOrDefault();
                    _children.Add(RewriteChildContent(attribute, node.Source, node.Children));
                    return;
                }

                // OK this node has explicit child content, we can rewrite it by visiting each node
                // in sequence, since we:
                // a) need to rewrite each child content element
                // b) any significant content outside of a child content is an error
                for (var i = 0; i < node.Children.Count; i++)
                {
                    var child = node.Children[i];
                    if (IsIgnorableWhitespace(child))
                    {
                        continue;
                    }

                    if (child is TagHelperIntermediateNode tagHelperNode &&
                        tagHelperNode.TagHelpers.Any(th => th.IsChildContentTagHelper()))
                    {
                        // This is a child content element
                        var attribute = _component.Component.BoundAttributes
                                        .Where(a => string.Equals(a.Name, tagHelperNode.TagName, StringComparison.Ordinal))
                                        .FirstOrDefault();
                        _children.Add(RewriteChildContent(attribute, child.Source, child.Children));
                        continue;
                    }

                    // If we get here then this is significant content inside a component with explicit child content.
                    child.Diagnostics.Add(ComponentDiagnosticFactory.Create_ChildContentMixedWithExplicitChildContent(child.Source, _component));
                    _children.Add(child);
                }

                bool IsIgnorableWhitespace(IntermediateNode n)
                {
                    if (n is HtmlContentIntermediateNode html &&
                        html.Children.Count == 1 &&
                        html.Children[0] is IntermediateToken token &&
                        string.IsNullOrWhiteSpace(token.Content))
                    {
                        return(true);
                    }

                    return(false);
                }
            }
        protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
        {
            if (codeDocument == null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            if (documentNode == null)
            {
                throw new ArgumentNullException(nameof(documentNode));
            }

            var @namespace = documentNode.FindPrimaryNamespace();
            var @class     = documentNode.FindPrimaryClass();

            if (@namespace == null || @class == null)
            {
                return;
            }

            var directives = documentNode.FindDirectiveReferences(ComponentPageDirective.Directive);

            if (directives.Count == 0)
            {
                return;
            }

            // We don't allow @page directives in imports
            for (var i = 0; i < directives.Count; i++)
            {
                var directive = directives[i];
                if (FileKinds.IsComponentImport(codeDocument.GetFileKind()) || directive.Node.IsImported())
                {
                    directive.Node.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value));
                }
            }

            // Insert the attributes 'on-top' of the class declaration, since classes don't directly support attributes.
            var index = 0;

            for (; index < @namespace.Children.Count; index++)
            {
                if (object.ReferenceEquals(@class, @namespace.Children[index]))
                {
                    break;
                }
            }

            for (var i = 0; i < directives.Count; i++)
            {
                var pageDirective = (DirectiveIntermediateNode)directives[i].Node;

                // The parser also adds errors for invalid syntax, we just need to not crash.
                var routeToken = pageDirective.Tokens.FirstOrDefault();

                if (routeToken != null &&
                    routeToken.Content.Length >= 3 &&
                    routeToken.Content[0] == '\"' &&
                    routeToken.Content[1] == '/' &&
                    routeToken.Content[routeToken.Content.Length - 1] == '\"')
                {
                    var template = new StringSegment(routeToken.Content, 1, routeToken.Content.Length - 2);
                    @namespace.Children.Insert(index++, new RouteAttributeExtensionNode(template));
                }
                else
                {
                    pageDirective.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_MustSpecifyRoute(pageDirective.Source));
                }
            }
        }
Example #22
0
        private void ProcessDuplicates(IntermediateNode node)
        {
            // Reverse order because we will remove nodes.
            //
            // Each 'property' node could be duplicated if there are multiple tag helpers that match that
            // particular attribute. This is common in our approach, which relies on 'fallback' tag helpers
            // that overlap with more specific ones.
            for (var i = node.Children.Count - 1; i >= 0; i--)
            {
                // For each usage of the general 'fallback' bind tag helper, it could duplicate
                // the usage of a more specific one. Look for duplicates and remove the fallback.
                var attribute = node.Children[i] as TagHelperPropertyIntermediateNode;
                if (attribute != null &&
                    attribute.TagHelper != null &&
                    attribute.TagHelper.IsFallbackBindTagHelper())
                {
                    for (var j = 0; j < node.Children.Count; j++)
                    {
                        var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode;
                        if (duplicate != null &&
                            duplicate.TagHelper != null &&
                            duplicate.TagHelper.IsBindTagHelper() &&
                            duplicate.AttributeName == attribute.AttributeName &&
                            !object.ReferenceEquals(attribute, duplicate))
                        {
                            // Found a duplicate - remove the 'fallback' in favor of the
                            // more specific tag helper.
                            node.Children.RemoveAt(i);
                            break;
                        }
                    }
                }

                // Also treat the general <input bind="..." /> as a 'fallback' for that case and remove it.
                // This is a workaround for a limitation where you can't write a tag helper that binds only
                // when a specific attribute is **not** present.
                if (attribute != null &&
                    attribute.TagHelper != null &&
                    attribute.TagHelper.IsInputElementFallbackBindTagHelper())
                {
                    for (var j = 0; j < node.Children.Count; j++)
                    {
                        var duplicate = node.Children[j] as TagHelperPropertyIntermediateNode;
                        if (duplicate != null &&
                            duplicate.TagHelper != null &&
                            duplicate.TagHelper.IsInputElementBindTagHelper() &&
                            duplicate.AttributeName == attribute.AttributeName &&
                            !object.ReferenceEquals(attribute, duplicate))
                        {
                            // Found a duplicate - remove the 'fallback' input tag helper in favor of the
                            // more specific tag helper.
                            node.Children.RemoveAt(i);
                            break;
                        }
                    }
                }
            }

            // If we still have duplicates at this point then they are genuine conflicts.
            var duplicates = node.Children
                             .OfType <TagHelperPropertyIntermediateNode>()
                             .GroupBy(p => p.AttributeName)
                             .Where(g => g.Count() > 1);

            foreach (var duplicate in duplicates)
            {
                node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_Duplicates(
                                         node.Source,
                                         duplicate.Key,
                                         duplicate.ToArray()));
                foreach (var property in duplicate)
                {
                    node.Children.Remove(property);
                }
            }
        }
Example #23
0
        private IntermediateNode[] RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node)
        {
            // Bind works similarly to a macro, it always expands to code that the user could have written.
            //
            // For the nodes that are related to the bind-attribute rewrite them to look like a set of
            // 'normal' HTML attributes similar to the following transformation.
            //
            // Input:   <MyComponent bind-Value="@currentCount" />
            // Output:  <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." ValueExpression ="() => ...<get the value>..." />
            //
            // This means that the expression that appears inside of 'bind' must be an LValue or else
            // there will be errors. In general the errors that come from C# in this case are good enough
            // to understand the problem.
            //
            // We also support and encourage the use of EventCallback<> with bind. So in the above example
            // the ValueChanged property could be an Action<> or an EventCallback<>.
            //
            // The BindMethods calls are required with Action<> because to give us a good experience. They
            // use overloading to ensure that can get an Action<object> that will convert and set an arbitrary
            // value. We have a similar set of APIs to use with EventCallback<>.
            //
            // We also assume that the element will be treated as a component for now because
            // multiple passes handle 'special' tag helpers. We have another pass that translates
            // a tag helper node back into 'regular' element when it doesn't have an associated component
            if (!TryComputeAttributeNames(
                    parent,
                    node,
                    node.AttributeName,
                    out var valueAttributeName,
                    out var changeAttributeName,
                    out var expressionAttributeName,
                    out var valueAttribute,
                    out var changeAttribute,
                    out var expressionAttribute))
            {
                // Skip anything we can't understand. It's important that we don't crash, that will bring down
                // the build.
                node.Diagnostics.Add(ComponentDiagnosticFactory.CreateBindAttribute_InvalidSyntax(
                                         node.Source,
                                         node.AttributeName));
                return(new[] { node });
            }

            var original = GetAttributeContent(node);

            if (string.IsNullOrEmpty(original.Content))
            {
                // This can happen in error cases, the parser will already have flagged this
                // as an error, so ignore it.
                return(new[] { node });
            }

            // Look for a matching format node. If we find one then we need to pass the format into the
            // two nodes we generate.
            IntermediateToken format = null;

            if (TryGetFormatNode(
                    parent,
                    node,
                    valueAttributeName,
                    out var formatNode))
            {
                // Don't write the format out as its own attribute, just capture it as a string
                // or expression.
                parent.Children.Remove(formatNode);
                format = GetAttributeContent(formatNode);
            }

            var valueExpressionTokens  = new List <IntermediateToken>();
            var changeExpressionTokens = new List <IntermediateToken>();

            if (changeAttribute != null && changeAttribute.IsDelegateProperty())
            {
                RewriteNodesForDelegateBind(
                    original,
                    format,
                    valueAttribute,
                    changeAttribute,
                    valueExpressionTokens,
                    changeExpressionTokens);
            }
            else
            {
                RewriteNodesForEventCallbackBind(
                    original,
                    format,
                    valueAttribute,
                    changeAttribute,
                    valueExpressionTokens,
                    changeExpressionTokens);
            }

            if (parent is MarkupElementIntermediateNode)
            {
                var valueNode = new HtmlAttributeIntermediateNode()
                {
                    AttributeName = valueAttributeName,
                    Source        = node.Source,

                    Prefix = valueAttributeName + "=\"",
                    Suffix = "\"",
                };

                for (var i = 0; i < node.Diagnostics.Count; i++)
                {
                    valueNode.Diagnostics.Add(node.Diagnostics[i]);
                }

                valueNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
                for (var i = 0; i < valueExpressionTokens.Count; i++)
                {
                    valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
                }

                var changeNode = new HtmlAttributeIntermediateNode()
                {
                    AttributeName = changeAttributeName,
                    Source        = node.Source,

                    Prefix = changeAttributeName + "=\"",
                    Suffix = "\"",
                };

                changeNode.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
                for (var i = 0; i < changeExpressionTokens.Count; i++)
                {
                    changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
                }

                return(new[] { valueNode, changeNode });
            }
            else
            {
                var valueNode = new ComponentAttributeIntermediateNode(node)
                {
                    AttributeName  = valueAttributeName,
                    BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute
                    PropertyName   = valueAttribute?.GetPropertyName(),
                    TagHelper      = valueAttribute == null ? null : node.TagHelper,
                    TypeName       = valueAttribute?.IsWeaklyTyped() == false ? valueAttribute.TypeName : null,
                };

                valueNode.Children.Clear();
                valueNode.Children.Add(new CSharpExpressionIntermediateNode());
                for (var i = 0; i < valueExpressionTokens.Count; i++)
                {
                    valueNode.Children[0].Children.Add(valueExpressionTokens[i]);
                }

                var changeNode = new ComponentAttributeIntermediateNode(node)
                {
                    AttributeName  = changeAttributeName,
                    BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute
                    PropertyName   = changeAttribute?.GetPropertyName(),
                    TagHelper      = changeAttribute == null ? null : node.TagHelper,
                    TypeName       = changeAttribute?.IsWeaklyTyped() == false ? changeAttribute.TypeName : null,
                };

                changeNode.Children.Clear();
                changeNode.Children.Add(new CSharpExpressionIntermediateNode());
                for (var i = 0; i < changeExpressionTokens.Count; i++)
                {
                    changeNode.Children[0].Children.Add(changeExpressionTokens[i]);
                }

                // Finally, also emit a node for the "Expression" attribute, but only if the target
                // component is defined to accept one
                ComponentAttributeIntermediateNode expressionNode = null;
                if (expressionAttribute != null)
                {
                    expressionNode = new ComponentAttributeIntermediateNode(node)
                    {
                        AttributeName  = expressionAttributeName,
                        BoundAttribute = expressionAttribute,
                        PropertyName   = expressionAttribute.GetPropertyName(),
                        TagHelper      = node.TagHelper,
                        TypeName       = expressionAttribute.IsWeaklyTyped() ? null : expressionAttribute.TypeName,
                    };

                    expressionNode.Children.Clear();
                    expressionNode.Children.Add(new CSharpExpressionIntermediateNode());
                    expressionNode.Children[0].Children.Add(new IntermediateToken()
                    {
                        Content = $"() => {original.Content}",
                        Kind    = TokenKind.CSharp
                    });
                }

                return(expressionNode == null
                    ? new[] { valueNode, changeNode }
                    : new[] { valueNode, changeNode, expressionNode });
            }
        }