From 465ff9713df193beb9ef707022943a5a144953e7 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 13 Aug 2015 16:15:54 -0700 Subject: [PATCH] Add ability for `TagHelper`s to specify restricted children. - Specifying the `RestrictChildrenAttribute` enables `TagHelper`s to only allow other `TagHelper`s targeting specified names to be in the children. - Used the `null` value to indicate that `AllowedChildren` was not specified and therefore everything is allowed. This is the default. - Added name verification to name values to ensure that no bad values pass through the system. - Added parsing tests to validate a mixture of content generates errors when expected. #255 --- .../Properties/Resources.Designer.cs | 32 ++ .../Resources.resx | 6 + .../TagHelpers/RestrictChildrenAttribute.cs | 42 ++ .../TagHelpers/TagHelperDescriptorFactory.cs | 92 +++- .../TagHelpers/TagHelperDescriptorResolver.cs | 1 + ...aseSensitiveTagHelperDescriptorComparer.cs | 17 +- .../TagHelpers/TagHelperParseTreeRewriter.cs | 86 +++- .../Properties/RazorResources.Designer.cs | 32 ++ .../RazorResources.resx | 6 + .../TagHelpers/TagHelperDescriptor.cs | 17 + .../TagHelpers/TagHelperDescriptorComparer.cs | 19 +- .../TagHelperDescriptorFactoryTest.cs | 45 ++ .../TagHelperDescriptorResolverTest.cs | 5 + .../CSharpTagHelperRenderingTest.cs | 3 + ...TagHelperAttributeValueCodeRendererTest.cs | 1 + .../TagHelpers/TagHelperBlockRewriterTest.cs | 3 + .../TagHelperDescriptorProviderTest.cs | 1 + .../TagHelpers/TagHelperDescriptorTest.cs | 12 + .../TagHelperParseTreeRewriterTest.cs | 453 ++++++++++++++++++ 19 files changed, 859 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index 5e61f5058a..46f60316bf 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -394,6 +394,38 @@ namespace Microsoft.AspNet.Razor.Runtime return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperAttributeList_CannotAddAttribute"), p0, p1, p2, p3); } + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"); } + } + + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"), p0, p1, p2, p3); + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"); } + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index c59fd6e955..e4d0edb677 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -189,4 +189,10 @@ Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'. + + Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + + + Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs new file mode 100644 index 0000000000..aae957d5a5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/RestrictChildrenAttribute.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Restricts children of the 's element. + /// + /// Combining this attribute with a that specifies its + /// as will result in + /// this attribute being ignored. + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public class RestrictChildrenAttribute : Attribute + { + /// + /// Instantiates a new instance of the class. + /// + /// + /// The tag name of an element allowed as a child. Tag helpers must target the element. + /// + /// + /// Additional names of elements allowed as children. Tag helpers must target all such elements. + /// + public RestrictChildrenAttribute(string tagName, params string[] tagNames) + { + var concatenatedNames = new string[1 + tagNames.Length]; + concatenatedNames[0] = tagName; + + tagNames.CopyTo(concatenatedNames, 1); + + ChildTagNames = concatenatedNames; + } + + /// + /// Get the names of elements allowed as children. Tag helpers must target all such elements. + /// + public IEnumerable ChildTagNames { get; } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs index b69692ec9b..9d35198670 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -61,6 +61,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var attributeDescriptors = GetAttributeDescriptors(type, designTime, errorSink); var targetElementAttributes = GetValidTargetElementAttributes(typeInfo, errorSink); + var allowedChildren = GetAllowedChildren(typeInfo, errorSink); var tagHelperDescriptors = BuildTagHelperDescriptors( @@ -68,6 +69,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, targetElementAttributes, + allowedChildren, designTime); return tagHelperDescriptors.Distinct(TagHelperDescriptorComparer.Default); @@ -87,6 +89,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers string assemblyName, IEnumerable attributeDescriptors, IEnumerable targetElementAttributes, + IEnumerable allowedChildren, bool designTime) { TagHelperDesignTimeDescriptor typeDesignTimeDescriptor = null; @@ -118,6 +121,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, requiredAttributes: Enumerable.Empty(), + allowedChildren: allowedChildren, tagStructure: default(TagStructure), designTimeDescriptor: typeDesignTimeDescriptor) }; @@ -130,14 +134,70 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, attribute, + allowedChildren, typeDesignTimeDescriptor)); } + private static IEnumerable GetAllowedChildren(TypeInfo typeInfo, ErrorSink errorSink) + { + var restrictChildrenAttribute = typeInfo.GetCustomAttribute(inherit: false); + if (restrictChildrenAttribute == null) + { + return null; + } + + var allowedChildren = restrictChildrenAttribute.ChildTagNames; + var validAllowedChildren = GetValidAllowedChildren(allowedChildren, typeInfo.FullName, errorSink); + + if (validAllowedChildren.Any()) + { + return validAllowedChildren; + } + else + { + // All allowed children were invalid, return null to indicate that any child is acceptable. + return null; + } + } + + // Internal for unit testing + internal static IEnumerable GetValidAllowedChildren( + IEnumerable allowedChildren, + string tagHelperName, + ErrorSink errorSink) + { + var validAllowedChildren = new List(); + + foreach (var name in allowedChildren) + { + var valid = TryValidateName( + name, + whitespaceError: Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( + nameof(RestrictChildrenAttribute), + tagHelperName), + characterErrorBuilder: (invalidCharacter) => + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( + nameof(RestrictChildrenAttribute), + name, + tagHelperName, + invalidCharacter), + errorSink: errorSink); + + if (valid) + { + validAllowedChildren.Add(name); + } + } + + return validAllowedChildren; + } + private static TagHelperDescriptor BuildTagHelperDescriptor( string typeName, string assemblyName, IEnumerable attributeDescriptors, TargetElementAttribute targetElementAttribute, + IEnumerable allowedChildren, TagHelperDesignTimeDescriptor designTimeDescriptor) { var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes); @@ -148,6 +208,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributeDescriptors, requiredAttributes, + allowedChildren, targetElementAttribute.TagStructure, designTimeDescriptor); } @@ -158,6 +219,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers string assemblyName, IEnumerable attributeDescriptors, IEnumerable requiredAttributes, + IEnumerable allowedChildren, TagStructure tagStructure, TagHelperDesignTimeDescriptor designTimeDescriptor) { @@ -168,6 +230,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyName, attributes: attributeDescriptors, requiredAttributes: requiredAttributes, + allowedChildren: allowedChildren, tagStructure: tagStructure, designTimeDescriptor: designTimeDescriptor); } @@ -230,13 +293,28 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers var targetName = targetingAttributes ? Resources.TagHelperDescriptorFactory_Attribute : Resources.TagHelperDescriptorFactory_Tag; + + var validName = TryValidateName( + name, + whitespaceError: Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName), + characterErrorBuilder: (invalidCharacter) => + Resources.FormatTargetElementAttribute_InvalidName(targetName.ToLower(), name, invalidCharacter), + errorSink: errorSink); + + return validName; + } + + private static bool TryValidateName( + string name, + string whitespaceError, + Func characterErrorBuilder, + ErrorSink errorSink) + { var validName = true; if (string.IsNullOrWhiteSpace(name)) { - errorSink.OnError( - SourceLocation.Zero, - Resources.FormatTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName)); + errorSink.OnError(SourceLocation.Zero, whitespaceError); validName = false; } @@ -247,12 +325,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { - errorSink.OnError( - SourceLocation.Zero, - Resources.FormatTargetElementAttribute_InvalidName( - targetName.ToLower(), - name, - character)); + var error = characterErrorBuilder(character); + errorSink.OnError(SourceLocation.Zero, error); validName = false; } diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs index 4256deb946..7b414470eb 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -154,6 +154,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers descriptor.AssemblyName, descriptor.Attributes, descriptor.RequiredAttributes, + descriptor.AllowedChildren, descriptor.TagStructure, descriptor.DesignTimeDescriptor)); } diff --git a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs index 39998465c9..2d262ce832 100644 --- a/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor.Test.Sources/CaseSensitiveTagHelperDescriptorComparer.cs @@ -26,14 +26,19 @@ namespace Microsoft.AspNet.Razor.Test.Internal } return base.Equals(descriptorX, descriptorY) && - // Normal comparer doesn't care about the case, required attribute order, attributes or prefixes. - // In tests we do. + // Normal comparer doesn't care about the case, required attribute order, allowed children order, + // attributes or prefixes. In tests we do. string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.Ordinal) && string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.Ordinal) && Enumerable.SequenceEqual( descriptorX.RequiredAttributes, descriptorY.RequiredAttributes, StringComparer.Ordinal) && + (descriptorX.AllowedChildren == descriptorY.AllowedChildren || + Enumerable.SequenceEqual( + descriptorX.AllowedChildren, + descriptorY.AllowedChildren, + StringComparer.Ordinal)) && descriptorX.Attributes.SequenceEqual( descriptorY.Attributes, TagHelperAttributeDescriptorComparer.Default) && @@ -60,6 +65,14 @@ namespace Microsoft.AspNet.Razor.Test.Internal hashCodeCombiner.Add(requiredAttribute, StringComparer.Ordinal); } + if (descriptor.AllowedChildren != null) + { + foreach (var child in descriptor.AllowedChildren) + { + hashCodeCombiner.Add(child, StringComparer.Ordinal); + } + } + foreach (var attribute in descriptor.Attributes) { hashCodeCombiner.Add(TagHelperAttributeDescriptorComparer.Default.GetHashCode(attribute)); diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs index 4b2641f8c4..7f410bf775 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperParseTreeRewriter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Runtime.TagHelpers; @@ -16,6 +17,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { private TagHelperDescriptorProvider _provider; private Stack _trackerStack; + private TagHelperBlockTracker _currentTagHelperTracker; private Stack _blockStack; private BlockBuilder _currentBlock; @@ -56,6 +58,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { continue; } + else + { + // Non-TagHelper tag. + ValidateParentTagHelperAllowsPlainTag(childBlock, context.ErrorSink); + } // If we get to here it means that we're a normal html tag. No need to iterate any deeper into // the children of it because they wont be tag helpers. @@ -67,6 +74,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal continue; } } + else + { + ValidateParentTagHelperAllowsContent((Span)child, context.ErrorSink); + } // At this point the child is a Span or Block with Type BlockType.Tag that doesn't happen to be a // tag helper. @@ -108,7 +119,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return false; } - var tracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null; + var tracker = _currentTagHelperTracker; var tagNameScope = tracker?.Builder.TagName ?? string.Empty; if (!IsEndTag(tagBlock)) @@ -135,6 +146,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return false; } + ValidateParentTagHelperAllowsTagHelper(tagName, tagBlock, context.ErrorSink); ValidateDescriptors(descriptors, tagName, tagBlock, context.ErrorSink); // We're in a start TagHelper block. @@ -211,6 +223,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // can't recover it means there was no corresponding tag helper start tag. if (TryRecoverTagHelper(tagName, tagBlock, context)) { + ValidateParentTagHelperAllowsTagHelper(tagName, tagBlock, context.ErrorSink); ValidateTagSyntax(tagName, tagBlock, context); // Successfully recovered, move onto the next element. @@ -267,6 +280,63 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal return attributeNames; } + private void ValidateParentTagHelperAllowsContent(Span child, ErrorSink errorSink) + { + var allowedChildren = _currentTagHelperTracker?.AllowedChildren; + if (allowedChildren != null) + { + var content = child.Content; + if (!string.IsNullOrWhiteSpace(content)) + { + var trimmedStart = content.TrimStart(); + var whitespace = content.Substring(0, content.Length - trimmedStart.Length); + var errorStart = SourceLocation.Advance(child.Start, whitespace); + var length = trimmedStart.TrimEnd().Length; + var allowedChildrenString = string.Join(", ", allowedChildren); + errorSink.OnError( + errorStart, + RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent( + _currentTagHelperTracker.Builder.TagName, + allowedChildrenString), + length); + } + } + } + + private void ValidateParentTagHelperAllowsPlainTag(Block tagBlock, ErrorSink errorSink) + { + if (_currentTagHelperTracker?.AllowedChildren != null) + { + OnAllowedChildrenTagError(_currentTagHelperTracker, tagBlock, errorSink); + } + } + + private void ValidateParentTagHelperAllowsTagHelper(string tagName, Block tagBlock, ErrorSink errorSink) + { + var currentlyAllowedChildren = _currentTagHelperTracker?.AllowedChildren; + + if (currentlyAllowedChildren != null && + !currentlyAllowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase)) + { + OnAllowedChildrenTagError(_currentTagHelperTracker, tagBlock, errorSink); + } + } + + private static void OnAllowedChildrenTagError( + TagHelperBlockTracker tracker, + Block tagBlock, + ErrorSink errorSink) + { + var tagName = GetTagName(tagBlock); + var allowedChildrenString = string.Join(", ", tracker.AllowedChildren); + var errorMessage = RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag( + tagName, + tracker.Builder.TagName, + allowedChildrenString); + + errorSink.OnError(tagBlock.Start, errorMessage, tagBlock.Length); + } + private static void ValidateDescriptors( IEnumerable descriptors, string tagName, @@ -361,6 +431,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // for formatting. _trackerStack.Pop().Builder.SourceEndTag = endTag; + _currentTagHelperTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null; + BuildCurrentlyTrackedBlock(); } @@ -385,7 +457,8 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal private void TrackTagHelperBlock(TagHelperBlockBuilder builder) { - _trackerStack.Push(new TagHelperBlockTracker(builder)); + _currentTagHelperTracker = new TagHelperBlockTracker(builder); + _trackerStack.Push(_currentTagHelperTracker); TrackBlock(builder); } @@ -478,11 +551,20 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal public TagHelperBlockTracker(TagHelperBlockBuilder builder) { Builder = builder; + + if (Builder.Descriptors.Any(descriptor => descriptor.AllowedChildren != null)) + { + AllowedChildren = Builder.Descriptors + .SelectMany(descriptor => descriptor.AllowedChildren) + .Distinct(StringComparer.OrdinalIgnoreCase); + } } public TagHelperBlockBuilder Builder { get; } public uint OpenMatchingTags { get; set; } + + public IEnumerable AllowedChildren { get; } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs index 3677d1371c..5d12ab827d 100644 --- a/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNet.Razor/Properties/RazorResources.Designer.cs @@ -1514,6 +1514,38 @@ namespace Microsoft.AspNet.Razor return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag"), p0, p1, p2); } + /// + /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed. + /// + internal static string TagHelperParseTreeRewriter_CannotHaveNonTagContent + { + get { return GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"); } + } + + /// + /// The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed. + /// + internal static string FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_CannotHaveNonTagContent"), p0, p1); + } + + /// + /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed. + /// + internal static string TagHelperParseTreeRewriter_InvalidNestedTag + { + get { return GetString("TagHelperParseTreeRewriter_InvalidNestedTag"); } + } + + /// + /// The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed. + /// + internal static string FormatTagHelperParseTreeRewriter_InvalidNestedTag(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperParseTreeRewriter_InvalidNestedTag"), p0, p1, p2); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor/RazorResources.resx b/src/Microsoft.AspNet.Razor/RazorResources.resx index d0680dfa81..d099d3330c 100644 --- a/src/Microsoft.AspNet.Razor/RazorResources.resx +++ b/src/Microsoft.AspNet.Razor/RazorResources.resx @@ -419,4 +419,10 @@ Instead, wrap the contents of the block in "{{}}": Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}'). + + The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed. + + + The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tag helper(s) targeting tag name(s) '{2}' are allowed. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs index 4f2ebd329b..6c1d94d3dc 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs @@ -61,6 +61,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: assemblyName, attributes: attributes, requiredAttributes: requiredAttributes, + allowedChildren: null, tagStructure: TagStructure.Unspecified, designTimeDescriptor: null) { @@ -84,6 +85,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// The attribute names required for the tag helper to target the HTML tag. /// + /// + /// The names of elements allowed as children. Tag helpers must target all such elements. + /// + /// The expected tag structure. /// The that contains design /// time information about the tag helper. public TagHelperDescriptor( @@ -93,6 +98,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers [NotNull] string assemblyName, [NotNull] IEnumerable attributes, [NotNull] IEnumerable requiredAttributes, + IEnumerable allowedChildren, TagStructure tagStructure, TagHelperDesignTimeDescriptor designTimeDescriptor) { @@ -105,6 +111,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers RequiredAttributes = new List(requiredAttributes); TagStructure = tagStructure; DesignTimeDescriptor = designTimeDescriptor; + + if (allowedChildren != null) + { + AllowedChildren = new List(allowedChildren); + } } /// @@ -147,6 +158,12 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// public IReadOnlyList RequiredAttributes { get; } + /// + /// Get the names of elements allowed as children. Tag helpers must target all such elements. + /// + /// null indicates all children are allowed. + public IReadOnlyList AllowedChildren { get; } + /// /// The expected tag structure. /// diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs index 88cb6bcb6f..1fff142a56 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -30,7 +30,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// Determines equality based on , /// , , - /// , and . + /// , , + /// and . /// Ignores because it can be inferred directly from /// and . /// @@ -49,6 +50,13 @@ namespace Microsoft.AspNet.Razor.TagHelpers descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), StringComparer.OrdinalIgnoreCase) && + (descriptorX.AllowedChildren == descriptorY.AllowedChildren || + (descriptorX.AllowedChildren != null && + descriptorY.AllowedChildren != null && + Enumerable.SequenceEqual( + descriptorX.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase), + descriptorY.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase))) && descriptorX.TagStructure == descriptorY.TagStructure; } @@ -69,6 +77,15 @@ namespace Microsoft.AspNet.Razor.TagHelpers hashCodeCombiner.Add(attribute, StringComparer.OrdinalIgnoreCase); } + if (descriptor.AllowedChildren != null) + { + var allowedChildren = descriptor.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase); + foreach (var child in allowedChildren) + { + hashCodeCombiner.Add(child, StringComparer.OrdinalIgnoreCase); + } + } + return hashCodeCombiner.CombinedHash; } } diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index 6cebe4c8f1..b73db12361 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null) } @@ -50,6 +51,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( @@ -59,6 +61,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null), } @@ -74,6 +77,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( @@ -83,6 +87,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.Unspecified, designTimeDescriptor: null), } @@ -137,6 +142,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor { @@ -155,6 +161,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor { @@ -167,6 +174,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: new TagHelperDesignTimeDescriptor { @@ -1734,6 +1742,42 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } } + public static TheoryData InvalidRestrictChildrenNameData + { + get + { + var nullOrWhiteSpaceError = + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( + nameof(RestrictChildrenAttribute), + "SomeTagHelper"); + + return GetInvalidNameOrPrefixData( + onNameError: (invalidInput, invalidCharacter) => + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( + nameof(RestrictChildrenAttribute), + invalidInput, + "SomeTagHelper", + invalidCharacter), + whitespaceErrorString: nullOrWhiteSpaceError, + onDataError: null); + } + } + + [Theory] + [MemberData(nameof(InvalidRestrictChildrenNameData))] + public void GetValidAllowedChildren_AddsExpectedErrors(string name, string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + var expectedErrors = expectedErrorMessages.Select(message => new RazorError(message, SourceLocation.Zero)); + + // Act + TagHelperDescriptorFactory.GetValidAllowedChildren(new[] { name }, "SomeTagHelper", errorSink); + + // Assert + Assert.Equal(expectedErrors, errorSink.Errors); + } + private static TheoryData GetInvalidNameOrPrefixData( Func onNameError, string whitespaceErrorString, @@ -1973,6 +2017,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyName, attributes: attributes ?? Enumerable.Empty(), requiredAttributes: requiredAttributes ?? Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); } diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs index 8241d3d20a..d7c1f7962f 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorResolverTest.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); } @@ -48,6 +49,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: AssemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); } @@ -609,6 +611,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyB, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); @@ -1045,6 +1048,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName: assemblyB, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); @@ -1444,6 +1448,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers assemblyName, attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); } diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs index 21e80eea24..7bad3b0993 100644 --- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs @@ -1550,6 +1550,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("age", pAgePropertyInfo) }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null), new TagHelperDescriptor( @@ -1562,6 +1563,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( @@ -1575,6 +1577,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("checked", checkedPropertyInfo) }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.Unspecified, designTimeDescriptor: null) }; diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs index c5ee79ba70..21bb234948 100644 --- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/TagHelperAttributeValueCodeRendererTest.cs @@ -34,6 +34,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator new TagHelperAttributeDescriptor("type", inputTypePropertyInfo) }, requiredAttributes: new string[0], + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs index aba10bf28b..55187f361b 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -87,6 +87,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null) }; @@ -189,6 +190,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: structure1, designTimeDescriptor: null), new TagHelperDescriptor( @@ -198,6 +200,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: structure2, designTimeDescriptor: null) }; diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs index 2addac6796..b91ad18783 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorProviderTest.cs @@ -322,6 +322,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "SomeAssembly", attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null); } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs index 3471341097..a677fb3c6e 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "assembly name", attributes: Enumerable.Empty(), requiredAttributes: new[] { "required attribute one", "required attribute two" }, + allowedChildren: new[] { "allowed child one" }, tagStructure: TagStructure.Unspecified, designTimeDescriptor: new TagHelperDesignTimeDescriptor { @@ -40,6 +41,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" + "[\"required attribute one\",\"required attribute two\"]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\"]," + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{"+ $"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," + @@ -78,6 +80,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null); var expectedSerializedDescriptor = @@ -100,6 +103,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":1," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}"; @@ -135,6 +139,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: new[] { "allowed child one", "allowed child two" }, tagStructure: default(TagStructure), designTimeDescriptor: null); var expectedSerializedDescriptor = @@ -157,6 +162,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," + $"\"{ nameof(TagHelperDescriptor.TagStructure) }\":0," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}"; @@ -180,6 +186,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," + $"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" + "[\"required attribute one\",\"required attribute two\"]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":[\"allowed child one\",\"allowed child two\"]," + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":2," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":{{" + $"\"{ nameof(TagHelperDesignTimeDescriptor.Summary) }\":\"usage summary\"," + @@ -192,6 +199,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers assemblyName: "assembly name", attributes: Enumerable.Empty(), requiredAttributes: new[] { "required attribute one", "required attribute two" }, + allowedChildren: new[] { "allowed child one", "allowed child two" }, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: new TagHelperDesignTimeDescriptor { @@ -242,6 +250,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":0," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}"; var expectedDescriptor = new TagHelperDescriptor( @@ -265,6 +274,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.Unspecified, designTimeDescriptor: null); @@ -331,6 +341,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"," + $"\"{ nameof(TagHelperAttributeDescriptor.DesignTimeDescriptor) }\":null}}]," + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.AllowedChildren) }\":null," + $"\"{nameof(TagHelperDescriptor.TagStructure)}\":1," + $"\"{ nameof(TagHelperDescriptor.DesignTimeDescriptor) }\":null}}"; var expectedDescriptor = new TagHelperDescriptor( @@ -354,6 +365,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null); diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index b9872db90e..c426a3a105 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -18,6 +18,450 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers { public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase { + [Fact] + public void Rewrite_CanHandleInvalidChildrenWithWhitespace() + { + // Arrange + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var documentContent = $"

{Environment.NewLine} {Environment.NewLine} Hello" + + $"{Environment.NewLine} {Environment.NewLine}

"; + var expectedErrors = new[] { + new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "p", "br"), + absoluteIndex: 9, + lineIndex: 1, + columnIndex: 9, + length: 8), + new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent("p", "br"), + absoluteIndex: 27, + lineIndex: 2, + columnIndex: 27, + length: 5), + new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "p", "br"), + absoluteIndex: 38, + lineIndex: 3, + columnIndex: 38, + length: 9), + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(Environment.NewLine + " "), + blockFactory.MarkupTagBlock(""), + factory.Markup(Environment.NewLine + " Hello" + Environment.NewLine + " "), + blockFactory.MarkupTagBlock(""), + factory.Markup(Environment.NewLine))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: new[] { "br" }, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + + [Fact] + public void Rewrite_RecoversWhenRequiredAttributeMismatchAndRestrictedChildren() + { + // Arrange + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + var documentContent = ""; + + var expectedErrors = new[] { + new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "strong", "br"), + absoluteIndex: 17, + lineIndex: 0, + columnIndex: 17, + length: 8), + new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag("strong", "strong", "br"), + absoluteIndex: 25, + lineIndex: 0, + columnIndex: 25, + length: 9), + }; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("strong", + new List> + { + new KeyValuePair("required", null) + }, + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "strong", + typeName: "StrongTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: new[] { "required" }, + allowedChildren: new[] { "br" }, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + + [Fact] + public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren_OneNull() + { + // Arrange + var factory = CreateDefaultSpanFactory(); + var documentContent = "

Hello World

"; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Hello World")), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper1", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: new[] { "strong", "br" }, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper2", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "strong", + typeName: "StrongTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "br", + typeName: "BRTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + [Fact] + public void Rewrite_CanHandleMultipleTagHelpersWithAllowedChildren() + { + // Arrange + var factory = CreateDefaultSpanFactory(); + var documentContent = "

Hello World

"; + var expectedOutput = new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Hello World")), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))); + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper1", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: new[] { "strong" }, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper2", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: new[] { "br" }, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "strong", + typeName: "StrongTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "br", + typeName: "BRTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors: new RazorError[0]); + } + + public static TheoryData AllowedChildrenData + { + get + { + var factory = CreateDefaultSpanFactory(); + var blockFactory = new BlockFactory(factory); + Func nestedTagError = + (childName, parentName, allowed, location, length) => new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_InvalidNestedTag( + childName, + parentName, + allowed), + absoluteIndex: location, + lineIndex: 0, + columnIndex: location, + length: length); + Func nestedContentError = + (parentName, allowed, location, length) => new RazorError( + RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent(parentName, allowed), + absoluteIndex: location, + lineIndex: 0, + columnIndex: location, + length: length); + + return new TheoryData, MarkupBlock, RazorError[]> + { + { + "


", + new[] { "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.SelfClosing))), + new RazorError[0] + }, + { + $"

{Environment.NewLine}
{Environment.NewLine}

", + new[] { "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(Environment.NewLine), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup(Environment.NewLine))), + new RazorError[0] + }, + { + "


", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.StartTagOnly))), + new[] { nestedTagError("br", "p", "strong", 3, 4) } + }, + { + "

Hello

", + new[] { "strong" }, + new MarkupBlock(new MarkupTagHelperBlock("p", factory.Markup("Hello"))), + new[] { nestedContentError("p", "strong", 3, 5) } + }, + { + "


", + new[] { "br", "strong" }, + new MarkupBlock(new MarkupTagHelperBlock("p", blockFactory.MarkupTagBlock("
"))), + new[] { nestedTagError("hr", "p", "br, strong", 3, 6) } + }, + { + "


Hello

", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + factory.Markup("Hello"))), + new[] { nestedTagError("br", "p", "strong", 3, 4), nestedContentError("p", "strong", 7, 5) } + }, + { + "

Title:
Something

", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong", 11, 6), + nestedTagError("br", "p", "strong", 26, 6), + nestedContentError("p", "strong", 32, 9), + } + }, + { + "

Title:
Something

", + new[] { "strong", "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong, br", 11, 6), + nestedContentError("p", "strong, br", 32, 9), + } + }, + { + "

Title:
Something

", + new[] { "strong", "br" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup(" "), + new MarkupTagHelperBlock("strong", factory.Markup("Title:")), + factory.Markup(" "), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup(" Something"))), + new[] + { + nestedContentError("strong", "strong, br", 13, 6), + nestedContentError("p", "strong, br", 38, 9), + } + }, + { + "

Title:
A Very Cool

Something

", + new[] { "strong" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + new MarkupTagHelperBlock("strong", + factory.Markup("Title:"), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + factory.Markup("A Very Cool"), + blockFactory.MarkupTagBlock("")), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedContentError("strong", "strong", 11, 6), + nestedTagError("br", "strong", "strong", 17, 4), + nestedTagError("em", "strong", "strong", 21, 4), + nestedContentError("strong", "strong", 25, 11), + nestedTagError("em", "strong", "strong", 36, 5), + nestedTagError("br", "p", "strong", 50, 6), + nestedContentError("p", "strong", 56, 9) + } + }, + { + "

Title:
A Very Cool

Something

", + new[] { "custom" }, + new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup("Title:"), + new MarkupTagHelperBlock("br", TagMode.StartTagOnly), + blockFactory.MarkupTagBlock(""), + factory.Markup("A Very Cool"), + blockFactory.MarkupTagBlock(""), + blockFactory.MarkupTagBlock(""), + new MarkupTagHelperBlock("br", TagMode.SelfClosing), + factory.Markup("Something"))), + new[] + { + nestedTagError("custom", "p", "custom", 3, 8), + nestedContentError("p", "custom", 11, 6), + nestedTagError("br", "p", "custom", 17, 4), + nestedTagError("em", "p", "custom", 21, 4), + nestedContentError("p", "custom", 25, 11), + nestedTagError("em", "p", "custom", 36, 5), + nestedTagError("custom", "p", "custom", 41, 9), + nestedTagError("br", "p", "custom", 50, 6), + nestedContentError("p", "custom", 56, 9) + } + } + }; + } + } + + [Theory] + [MemberData(nameof(AllowedChildrenData))] + public void Rewrite_UnderstandsAllowedChildren( + string documentContent, + IEnumerable allowedChildren, + MarkupBlock expectedOutput, + RazorError[] expectedErrors) + { + // Arrange + var descriptors = new TagHelperDescriptor[] + { + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "p", + typeName: "PTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: allowedChildren, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "strong", + typeName: "StrongTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: allowedChildren, + tagStructure: TagStructure.Unspecified, + designTimeDescriptor: null), + new TagHelperDescriptor( + prefix: string.Empty, + tagName: "br", + typeName: "BRTagHelper", + assemblyName: "SomeAssembly", + attributes: new TagHelperAttributeDescriptor[0], + requiredAttributes: Enumerable.Empty(), + allowedChildren: null, + tagStructure: TagStructure.WithoutEndTag, + designTimeDescriptor: null), + }; + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + + // Act & Assert + EvaluateData(descriptorProvider, documentContent, expectedOutput, expectedErrors); + } + [Fact] public void Rewrite_CanHandleStartTagOnlyTagTagMode() { @@ -33,6 +477,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null) }; @@ -68,6 +513,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null) }; @@ -104,6 +550,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.WithoutEndTag, designTimeDescriptor: null), new TagHelperDescriptor( @@ -113,6 +560,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: new TagHelperAttributeDescriptor[0], requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: TagStructure.NormalOrSelfClosing, designTimeDescriptor: null) }; @@ -1022,6 +1470,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null), new TagHelperDescriptor( @@ -1039,6 +1488,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null) }; @@ -1051,6 +1501,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null), new TagHelperDescriptor( @@ -1068,6 +1519,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers designTimeDescriptor: null), }, requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null) }; @@ -1080,6 +1532,7 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers assemblyName: "SomeAssembly", attributes: Enumerable.Empty(), requiredAttributes: Enumerable.Empty(), + allowedChildren: null, tagStructure: default(TagStructure), designTimeDescriptor: null), };