From 94f2f904b31c19f080f014aa15baa90bd2076198 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Fri, 15 May 2015 23:17:52 -0700 Subject: [PATCH] Add `[HtmlAttributeName(..., DictionaryAttributePrefix="prefix")]` part II - relates to #89 because that changes `string` property checks and needs this refactor - determine `string`-ness when creating `TagHelperAttributeDescriptor`s - add `TagHelperAttributeDescriptor.IsStringProperty` (set in constructor) - avoid repeated `string` comparisons and be more explicit - change `TagHelperBlockRewriter` to centralize more of the `string`-ness determination - also add `TryParseResult` DTO, avoiding multiple `out` parameters - refactor `CSharpTagHelperCodeRenderer` to allow reuse of core attribute value rendering - test all of it - add `TagHelperDescriptorTest` to confirm serialization / deserialization minor: - fix `TagHelperBlockRewriter.TryParseBlock()` end quote removal when tag is malformed nits: - remove dangling mention of fixed bug #220 - make recently-added `TagHelperBlockRewriterTest` tests realistic - multiple `TagHelperDescriptor`s for same tag helper have identical `Attributes` --- .../CSharp/CSharpTagHelperCodeRenderer.cs | 203 +++++++++--------- .../TagHelpers/TagHelperBlockRewriter.cs | 165 ++++++++------ .../TagHelperAttributeDescriptor.cs | 61 ++++-- ...iveTagHelperAttributeDescriptorComparer.cs | 6 +- .../TagHelperDescriptorFactoryTest.cs | 43 ++-- .../Generator/CSharpTagHelperRenderingTest.cs | 9 +- .../TagHelpers/TagHelperBlockRewriterTest.cs | 14 +- .../TagHelpers/TagHelperDescriptorTest.cs | 193 +++++++++++++++++ .../TagHelperParseTreeRewriterTest.cs | 89 ++++++++ 9 files changed, 584 insertions(+), 199 deletions(-) create mode 100644 test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs index 910d806376..693ecf6288 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -67,14 +67,14 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp RenderTagHelpersCreation(chunk, tagHelperDescriptors); - var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes); - var boundHTMLAttributes = attributeDescriptors.Select(descriptor => descriptor.Name); + // Determine what attributes exist in the element and divide them up. var htmlAttributes = chunk.Attributes; - var unboundHTMLAttributes = - htmlAttributes.Where(htmlAttribute => !boundHTMLAttributes.Contains(htmlAttribute.Key, - StringComparer.OrdinalIgnoreCase)); + var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes); + var unboundHtmlAttributes = htmlAttributes.Where( + attribute => !attributeDescriptors.Any( + descriptor => string.Equals(attribute.Key, descriptor.Name, StringComparison.OrdinalIgnoreCase))); - RenderUnboundHTMLAttributes(unboundHTMLAttributes); + RenderUnboundHTMLAttributes(unboundHtmlAttributes); // No need to run anything in design time mode. if (!_designTimeMode) @@ -180,22 +180,24 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp } // Render all of the bound attribute values for the tag helper. - RenderBoundHTMLAttributes(chunk.Attributes, - tagHelperVariableName, - tagHelperDescriptor.Attributes, - htmlAttributeValues); + RenderBoundHTMLAttributes( + chunk.Attributes, + tagHelperVariableName, + tagHelperDescriptor.Attributes, + htmlAttributeValues); } } - private void RenderBoundHTMLAttributes(IList> chunkAttributes, - string tagHelperVariableName, - IEnumerable attributeDescriptors, - Dictionary htmlAttributeValues) + private void RenderBoundHTMLAttributes( + IList> chunkAttributes, + string tagHelperVariableName, + IEnumerable attributeDescriptors, + Dictionary htmlAttributeValues) { foreach (var attributeDescriptor in attributeDescriptors) { var matchingAttributes = chunkAttributes.Where( - attr => string.Equals(attr.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase)); + kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase)); if (matchingAttributes.Any()) { @@ -210,31 +212,15 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp continue; } - var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); - - // Bufferable attributes are attributes that can have Razor code inside of them. - var bufferableAttribute = IsStringAttribute(attributeDescriptor); - - // Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it - // may be more than just a plain text value, it may also contain Razor code which is why we attempt - // to retrieve a plain text value here. - string textValue; - var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue); - - // If we haven't recorded a value and we need to buffer an attribute value and the value is not - // plain text then we need to prepare the value prior to setting it below. - if (!attributeValueRecorded && bufferableAttribute && !isPlainTextValue) - { - BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false); - } - // We capture the tag helpers property value accessor so we can retrieve it later (if we need to). - var valueAccessor = string.Format(CultureInfo.InvariantCulture, - "{0}.{1}", - tagHelperVariableName, - attributeDescriptor.PropertyName); + var valueAccessor = string.Format( + CultureInfo.InvariantCulture, + "{0}.{1}", + tagHelperVariableName, + attributeDescriptor.PropertyName); // If we haven't recorded this attribute value before then we need to record its value. + var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name); if (!attributeValueRecorded) { // We only need to create attribute values once per HTML element (not once per tag helper). @@ -242,58 +228,16 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp // helpers that need the value. htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor); - if (bufferableAttribute) - { - _writer.WriteStartAssignment(valueAccessor); + // Bufferable attributes are attributes that can have Razor code inside of them. Such + // attributes have string values and may be calculated using a temporary TextWriter or other + // buffer. + var bufferableAttribute = attributeDescriptor.IsStringProperty; - if (isPlainTextValue) - { - // If the attribute is bufferable but has a plain text value that means the value - // is a string which needs to be surrounded in quotes. - RenderQuotedAttributeValue(textValue, attributeDescriptor); - } - else - { - // The value contains more than plain text e.g. - // stringAttribute ="Time: @DateTime.Now" - RenderBufferedAttributeValue(attributeDescriptor); - } - - _writer.WriteLine(";"); - } - else - { - // Write out simple assignment for non-string property value. Try to keep the whole - // statement together and the #line pragma correct to make debugging possible. - using (var lineMapper = new CSharpLineMappingWriter( - _writer, - attributeValueChunk.Association.Start, - _context.SourceFile)) - { - // Place the assignment LHS to align RHS with original attribute value's indentation. - // Unfortunately originalIndent is incorrect if original line contains tabs. Unable to - // use a CSharpPaddingBuilder because the Association has no Previous node; lost the - // original Span sequence when the parse tree was rewritten. - var originalIndent = attributeValueChunk.Start.CharacterIndex; - var generatedLength = valueAccessor.Length + " = ".Length; - var newIndent = originalIndent - generatedLength; - if (newIndent > 0) - { - _writer.Indent(newIndent); - } - - _writer.WriteStartAssignment(valueAccessor); - lineMapper.MarkLineMappingStart(); - - // Write out bare expression for this attribute value. Property is not a string. - // So quoting or buffering are not helpful. - RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue); - - // End the assignment to the attribute. - lineMapper.MarkLineMappingEnd(); - _writer.WriteLine(";"); - } - } + RenderNewAttributeValueAssignment( + attributeDescriptor, + bufferableAttribute, + attributeValueChunk, + valueAccessor); // Execution contexts are a runtime feature. if (_designTimeMode) @@ -301,8 +245,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp continue; } - var attributeName = firstAttribute.Key; // We need to inform the context of the attribute value. + var attributeName = firstAttribute.Key; _writer .WriteStartInstanceMethodInvocation( ExecutionContextVariableName, @@ -325,6 +269,79 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp } } + // Render assignment of attribute value to the value accessor. + private void RenderNewAttributeValueAssignment( + TagHelperAttributeDescriptor attributeDescriptor, + bool bufferableAttribute, + Chunk attributeValueChunk, + string valueAccessor) + { + // Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it + // may be more than just a plain text value, it may also contain Razor code which is why we attempt + // to retrieve a plain text value here. + string textValue; + var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue); + + if (bufferableAttribute) + { + if (!isPlainTextValue) + { + // If we haven't recorded a value and we need to buffer an attribute value and the value is not + // plain text then we need to prepare the value prior to setting it below. + BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false); + } + + _writer.WriteStartAssignment(valueAccessor); + + if (isPlainTextValue) + { + // If the attribute is bufferable but has a plain text value that means the value + // is a string which needs to be surrounded in quotes. + RenderQuotedAttributeValue(textValue, attributeDescriptor); + } + else + { + // The value contains more than plain text e.g. stringAttribute ="Time: @DateTime.Now". + RenderBufferedAttributeValue(attributeDescriptor); + } + + _writer.WriteLine(";"); + } + else + { + // Write out simple assignment for non-string property value. Try to keep the whole + // statement together and the #line pragma correct to make debugging possible. + using (var lineMapper = new CSharpLineMappingWriter( + _writer, + attributeValueChunk.Association.Start, + _context.SourceFile)) + { + // Place the assignment LHS to align RHS with original attribute value's indentation. + // Unfortunately originalIndent is incorrect if original line contains tabs. Unable to + // use a CSharpPaddingBuilder because the Association has no Previous node; lost the + // original Span sequence when the parse tree was rewritten. + var originalIndent = attributeValueChunk.Start.CharacterIndex; + var generatedLength = valueAccessor.Length + " = ".Length; + var newIndent = originalIndent - generatedLength; + if (newIndent > 0) + { + _writer.Indent(newIndent); + } + + _writer.WriteStartAssignment(valueAccessor); + lineMapper.MarkLineMappingStart(); + + // Write out bare expression for this attribute value. Property is not a string. + // So quoting or buffering are not helpful. + RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue); + + // End the assignment to the attribute. + lineMapper.MarkLineMappingEnd(); + _writer.WriteLine(";"); + } + } + } + private void RenderUnboundHTMLAttributes(IEnumerable> unboundHTMLAttributes) { // Build out the unbound HTML attributes for the tag builder @@ -540,14 +557,6 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp } } - private static bool IsStringAttribute(TagHelperAttributeDescriptor attributeDescriptor) - { - return string.Equals( - attributeDescriptor.TypeName, - typeof(string).FullName, - StringComparison.Ordinal); - } - private static bool TryGetPlainTextValue(Chunk chunk, out string plainText) { var chunkBlock = chunk as ChunkBlock; diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs index 001d3d3deb..6d0f98ec85 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs @@ -38,15 +38,11 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal IEnumerable descriptors, ErrorSink errorSink) { - var attributes = new List>(); + // Ignore all but one descriptor per type since this method uses the TagHelperDescriptors only to get the + // contained TagHelperAttributeDescriptor's. + descriptors = descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default); - // Build a dictionary so we can easily lookup expected attribute value lookups - IReadOnlyDictionary attributeValueTypes = - descriptors.SelectMany(descriptor => descriptor.Attributes) - .Distinct(TagHelperAttributeDescriptorComparer.Default) - .ToDictionary(descriptor => descriptor.Name, - descriptor => descriptor.TypeName, - StringComparer.OrdinalIgnoreCase); + var attributes = new List>(); // We skip the first child "" or "/>". // The -2 accounts for both the start and end tags. If the tag does not have a valid structure then there's @@ -56,40 +52,38 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal foreach (var child in attributeChildren) { - KeyValuePair attribute; - bool succeeded = true; - + TryParseResult result; if (child.IsBlock) { - succeeded = TryParseBlock(tagName, (Block)child, attributeValueTypes, errorSink, out attribute); + result = TryParseBlock(tagName, (Block)child, descriptors, errorSink); } else { - succeeded = TryParseSpan((Span)child, attributeValueTypes, errorSink, out attribute); + result = TryParseSpan((Span)child, descriptors, errorSink); } // Only want to track the attribute if we succeeded in parsing its corresponding Block/Span. - if (succeeded) + if (result != null) { - // Check if it's a bound attribute that is minimized or not of type string and null or whitespace. - string attributeValueType; - if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) && - (attribute.Value == null || - !IsStringAttribute(attributeValueType) && - IsNullOrWhitespaceAttributeValue(attribute.Value))) + // Check if it's a bound attribute that is minimized or if it's a bound non-string attribute that + // is null or whitespace. + if ((result.IsBoundAttribute && result.AttributeValueNode == null) || + (result.IsBoundNonStringAttribute && + IsNullOrWhitespaceAttributeValue(result.AttributeValueNode))) { var errorLocation = GetAttributeNameStartLocation(child); errorSink.OnError( errorLocation, RazorResources.FormatRewriterError_EmptyTagHelperBoundAttribute( - attribute.Key, + result.AttributeName, tagName, - attributeValueType), - attribute.Key.Length); + GetPropertyType(result.AttributeName, descriptors)), + result.AttributeName.Length); } - attributes.Add(new KeyValuePair(attribute.Key, attribute.Value)); + attributes.Add( + new KeyValuePair(result.AttributeName, result.AttributeValueNode)); } } @@ -106,11 +100,10 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. - private static bool TryParseSpan( + private static TryParseResult TryParseSpan( Span span, - IReadOnlyDictionary attributeValueTypes, - ErrorSink errorSink, - out KeyValuePair attribute) + IEnumerable descriptors, + ErrorSink errorSink) { var afterEquals = false; var builder = new SpanBuilder @@ -235,24 +228,29 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal span.Content.Length); } - attribute = default(KeyValuePair); - - return false; + return null; } + bool isBoundNonStringAttribute; + var result = new TryParseResult + { + IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute), + AttributeName = name, + }; + result.IsBoundNonStringAttribute = isBoundNonStringAttribute; + // If we're not after an equal then we should treat the value as if it were a minimized attribute. var attributeValueBuilder = afterEquals ? builder : null; - attribute = CreateMarkupAttribute(name, attributeValueBuilder, attributeValueTypes); + result.AttributeValueNode = CreateMarkupAttribute(name, attributeValueBuilder, isBoundNonStringAttribute); - return true; + return result; } - private static bool TryParseBlock( + private static TryParseResult TryParseBlock( string tagName, Block block, - IReadOnlyDictionary attributeValueTypes, - ErrorSink errorSink, - out KeyValuePair attribute) + IEnumerable descriptors, + ErrorSink errorSink) { // TODO: Accept more than just spans: https://github.com/aspnet/Razor/issues/96. // The first child will only ever NOT be a Span if a user is doing something like: @@ -265,9 +263,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal errorSink.OnError(block.Children.First().Start, RazorResources.FormatTagHelpers_CannotHaveCSharpInTagDeclaration(tagName)); - attribute = default(KeyValuePair); - - return false; + return null; } var builder = new BlockBuilder(block); @@ -276,7 +272,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal // i.e.
if (builder.Children.Count == 1) { - return TryParseSpan(childSpan, attributeValueTypes, errorSink, out attribute); + return TryParseSpan(childSpan, descriptors, errorSink); } var textSymbol = childSpan.Symbols.FirstHtmlSymbolAs(HtmlSymbolType.Text); @@ -286,12 +282,17 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal { errorSink.OnError(childSpan.Start, RazorResources.FormatTagHelpers_AttributesMustHaveAName(tagName)); - attribute = default(KeyValuePair); - - return false; + return null; } - // TODO: Support no attribute values: https://github.com/aspnet/Razor/issues/220 + // Have a name now. Able to determine correct isBoundNonStringAttribute value. + bool isBoundNonStringAttribute; + var result = new TryParseResult + { + IsBoundAttribute = IsBoundAttribute(name, descriptors, out isBoundNonStringAttribute), + AttributeName = name, + }; + result.IsBoundNonStringAttribute = isBoundNonStringAttribute; // Remove first child i.e. foo=" builder.Children.RemoveAt(0); @@ -301,10 +302,14 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers.Internal if (!endNode.IsBlock) { var endSpan = (Span)endNode; - var endSymbol = (HtmlSymbol)endSpan.Symbols.Last(); + + // In some malformed cases e.g.

class. /// /// The HTML attribute name. - /// The name of the CLR property name that corresponds to the HTML - /// attribute. + /// The name of the CLR property that corresponds to the HTML attribute. /// - /// The full name of the named (see ) property's - /// . + /// The full name of the named (see ) property's . /// - public TagHelperAttributeDescriptor(string name, - string propertyName, - string typeName) + public TagHelperAttributeDescriptor( + [NotNull] string name, + [NotNull] string propertyName, + [NotNull] string typeName) + : this( + name, + propertyName, + typeName, + isStringProperty: string.Equals(typeName, typeof(string).FullName, StringComparison.Ordinal)) + { + } + + // Internal for testing i.e. for confirming above constructor sets `IsStringProperty` as expected. + internal TagHelperAttributeDescriptor( + [NotNull] string name, + [NotNull] string propertyName, + [NotNull] string typeName, + bool isStringProperty) { Name = name; PropertyName = propertyName; TypeName = typeName; + IsStringProperty = isStringProperty; } + ///

+ /// Gets an indication whether this property is of type . + /// + /// + /// If true the is for . This causes the Razor parser + /// to allow empty values for attributes that have names matching . If false + /// empty values for such matching attributes lead to errors. + /// + public bool IsStringProperty { get; } + /// /// The HTML attribute name. /// - public string Name { get; private set; } + public string Name { get; } /// - /// The name of the CLR property name that corresponds to the HTML attribute name. + /// The name of the CLR property that corresponds to the HTML attribute. /// - public string PropertyName { get; private set; } + public string PropertyName { get; } /// - /// The full name of the named (see ) property's - /// . + /// The full name of the named (see ) property's . /// - public string TypeName { get; private set; } + public string TypeName { get; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs index cbd9655812..9fa50dbc45 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/CaseSensitiveTagHelperAttributeDescriptorComparer.cs @@ -24,13 +24,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers return true; } + // Normal comparer doesn't care about case, in tests we do. Also double-check IsStringProperty though + // it is inferred from TypeName. return base.Equals(descriptorX, descriptorY) && - // Normal comparer doesn't care about case, in tests we do. + descriptorX.IsStringProperty == descriptorY.IsStringProperty && string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal); } public override int GetHashCode(TagHelperAttributeDescriptor descriptor) { + // Rarely if ever hash TagHelperAttributeDescriptor. If we do, ignore IsStringProperty since it should + // not vary for a given TypeName i.e. will not change the bucket. return HashCodeCombiner.Start() .Add(base.GetHashCode(descriptor)) .Add(descriptor.Name, StringComparer.Ordinal) diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs index ea10fc92d9..b4cfcccd90 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperDescriptorFactoryTest.cs @@ -289,8 +289,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers typeof(InheritedOverriddenAttributeTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor("valid-attribute1", - validProperty1), + new TagHelperAttributeDescriptor("valid-attribute1", validProperty1), new TagHelperAttributeDescriptor("Something-Else", validProperty2) }) }; @@ -363,14 +362,18 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var errorSink = new ErrorSink(); - var intProperty = typeof(InheritedSingleAttributeTagHelper).GetProperty( - nameof(InheritedSingleAttributeTagHelper.IntAttribute)); + + // Also confirm isStringProperty is calculated correctly. var expectedDescriptor = new TagHelperDescriptor( "inherited-single-attribute", typeof(InheritedSingleAttributeTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor("int-attribute", intProperty) + new TagHelperAttributeDescriptor( + "int-attribute", + nameof(InheritedSingleAttributeTagHelper.IntAttribute), + typeof(int).FullName, + isStringProperty: false) }); // Act @@ -450,8 +453,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers typeof(NonPublicAccessorTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor( - "valid-attribute", validProperty) + new TagHelperAttributeDescriptor("valid-attribute", validProperty) }); // Act @@ -471,15 +473,19 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var errorSink = new ErrorSink(); - var boundProperty = typeof(NotBoundAttributeTagHelper).GetProperty( - nameof(NotBoundAttributeTagHelper.BoundProperty)); + + // Also confirm isStringProperty is calculated correctly. var expectedDescriptor = new TagHelperDescriptor( "not-bound-attribute", typeof(NotBoundAttributeTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor("bound-property", boundProperty) + new TagHelperAttributeDescriptor( + "bound-property", + nameof(NotBoundAttributeTagHelper.BoundProperty), + typeof(object).FullName, + isStringProperty: false) }); // Act @@ -516,21 +522,30 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { // Arrange var errorSink = new ErrorSink(); - var validProp = typeof(MultiTagTagHelper).GetProperty(nameof(MultiTagTagHelper.ValidAttribute)); + + // Also confirm isStringProperty is calculated correctly. var expectedDescriptors = new[] { new TagHelperDescriptor( "div", typeof(MultiTagTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor("valid-attribute", validProp) + new TagHelperAttributeDescriptor( + "valid-attribute", + nameof(MultiTagTagHelper.ValidAttribute), + typeof(string).FullName, + isStringProperty: true) }), new TagHelperDescriptor( "p", typeof(MultiTagTagHelper).FullName, AssemblyName, new[] { - new TagHelperAttributeDescriptor("valid-attribute", validProp) + new TagHelperAttributeDescriptor( + "valid-attribute", + nameof(MultiTagTagHelper.ValidAttribute), + typeof(string).FullName, + isStringProperty: true) }) }; @@ -1104,7 +1119,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private class NotBoundAttributeTagHelper { - public string BoundProperty { get; set; } + public object BoundProperty { get; set; } [HtmlAttributeNotBound] public string NotBoundProperty { get; set; } diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index 02670e278d..2ed5f01f98 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Razor.Test.Generator { public class CSharpTagHelperRenderingTest : TagHelperTestBase { - private static IEnumerable DefaultPAndInputTagHelperDescriptors - => BuildPAndInputTagHelperDescriptors(prefix: string.Empty); - private static IEnumerable PrefixedPAndInputTagHelperDescriptors - => BuildPAndInputTagHelperDescriptors("THS"); + private static IEnumerable DefaultPAndInputTagHelperDescriptors { get; } + = BuildPAndInputTagHelperDescriptors(prefix: string.Empty); + private static IEnumerable PrefixedPAndInputTagHelperDescriptors { get; } + = BuildPAndInputTagHelperDescriptors(prefix: "THS"); private static IEnumerable MinimizedTagHelpers_Descriptors { @@ -609,6 +609,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator var pAgePropertyInfo = typeof(TestType).GetProperty("Age"); var inputTypePropertyInfo = typeof(TestType).GetProperty("Type"); var checkedPropertyInfo = typeof(TestType).GetProperty("Checked"); + return new[] { new TagHelperDescriptor( diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs index 7c1886f201..71cfeaea82 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperBlockRewriterTest.cs @@ -712,13 +712,19 @@ namespace Microsoft.AspNet.Razor.TagHelpers { new TagHelperDescriptor( tagName: "input", - typeName: "InputTagHelper", + typeName: "InputTagHelper1", assemblyName: "SomeAssembly", - attributes: new TagHelperAttributeDescriptor[0], + attributes: new[] + { + new TagHelperAttributeDescriptor( + "bound-required-string", + "BoundRequiredString", + typeof(string).FullName) + }, requiredAttributes: new[] { "unbound-required" }), new TagHelperDescriptor( tagName: "input", - typeName: "InputTagHelper", + typeName: "InputTagHelper1", assemblyName: "SomeAssembly", attributes: new[] { @@ -730,7 +736,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers requiredAttributes: new[] { "bound-required-string" }), new TagHelperDescriptor( tagName: "input", - typeName: "InputTagHelper", + typeName: "InputTagHelper2", assemblyName: "SomeAssembly", attributes: new[] { diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs new file mode 100644 index 0000000000..0e00c32cec --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperDescriptorTest.cs @@ -0,0 +1,193 @@ +// 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.Linq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + public class TagHelperDescriptorTest + { + [Fact] + public void TagHelperDescriptor_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "required attribute one", "required attribute two" }); + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":" + + "[\"required attribute one\",\"required attribute two\"]}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithAttributes_CanBeSerialized() + { + // Arrange + var descriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: new[] + { + new TagHelperAttributeDescriptor( + name: "attribute one", + propertyName: "property name", + typeName: "property type name"), + new TagHelperAttributeDescriptor( + name: "attribute two", + propertyName: "property name", + typeName: typeof(string).FullName), + }, + requiredAttributes: Enumerable.Empty()); + var expectedSerializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}"; + + // Act + var serializedDescriptor = JsonConvert.SerializeObject(descriptor); + + // Assert + Assert.Equal(expectedSerializedDescriptor, serializedDescriptor, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{nameof(TagHelperDescriptor.Prefix)}\":\"prefix:\"," + + $"\"{nameof(TagHelperDescriptor.TagName)}\":\"tag name\"," + + $"\"{nameof(TagHelperDescriptor.FullTagName)}\":\"prefix:tag name\"," + + $"\"{nameof(TagHelperDescriptor.TypeName)}\":\"type name\"," + + $"\"{nameof(TagHelperDescriptor.AssemblyName)}\":\"assembly name\"," + + $"\"{nameof(TagHelperDescriptor.Attributes)}\":[]," + + $"\"{nameof(TagHelperDescriptor.RequiredAttributes)}\":" + + "[\"required attribute one\",\"required attribute two\"]}"; + var expectedDescriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: Enumerable.Empty(), + requiredAttributes: new[] { "required attribute one", "required attribute two" }); + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Empty(descriptor.Attributes); + Assert.Equal(expectedDescriptor.RequiredAttributes, descriptor.RequiredAttributes, StringComparer.Ordinal); + } + + [Fact] + public void TagHelperDescriptor_WithAttributes_CanBeDeserialized() + { + // Arrange + var serializedDescriptor = + $"{{\"{ nameof(TagHelperDescriptor.Prefix) }\":\"prefix:\"," + + $"\"{ nameof(TagHelperDescriptor.TagName) }\":\"tag name\"," + + $"\"{ nameof(TagHelperDescriptor.FullTagName) }\":\"prefix:tag name\"," + + $"\"{ nameof(TagHelperDescriptor.TypeName) }\":\"type name\"," + + $"\"{ nameof(TagHelperDescriptor.AssemblyName) }\":\"assembly name\"," + + $"\"{ nameof(TagHelperDescriptor.Attributes) }\":[" + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":false," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute one\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"property type name\"}}," + + $"{{\"{ nameof(TagHelperAttributeDescriptor.IsStringProperty) }\":true," + + $"\"{ nameof(TagHelperAttributeDescriptor.Name) }\":\"attribute two\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.PropertyName) }\":\"property name\"," + + $"\"{ nameof(TagHelperAttributeDescriptor.TypeName) }\":\"{ typeof(string).FullName }\"}}]," + + $"\"{ nameof(TagHelperDescriptor.RequiredAttributes) }\":[]}}"; + var expectedDescriptor = new TagHelperDescriptor( + prefix: "prefix:", + tagName: "tag name", + typeName: "type name", + assemblyName: "assembly name", + attributes: new[] + { + new TagHelperAttributeDescriptor( + name: "attribute one", + propertyName: "property name", + typeName: "property type name"), + new TagHelperAttributeDescriptor( + name: "attribute two", + propertyName: "property name", + typeName: typeof(string).FullName), + }, + requiredAttributes: Enumerable.Empty()); + + // Act + var descriptor = JsonConvert.DeserializeObject(serializedDescriptor); + + // Assert + Assert.NotNull(descriptor); + Assert.Equal(expectedDescriptor.Prefix, descriptor.Prefix, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TagName, descriptor.TagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.FullTagName, descriptor.FullTagName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.TypeName, descriptor.TypeName, StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.AssemblyName, descriptor.AssemblyName, StringComparer.Ordinal); + Assert.Equal(2, descriptor.Attributes.Count); + Assert.Equal(expectedDescriptor.Attributes[0].IsStringProperty, descriptor.Attributes[0].IsStringProperty); + Assert.Equal(expectedDescriptor.Attributes[0].Name, descriptor.Attributes[0].Name, StringComparer.Ordinal); + Assert.Equal( + expectedDescriptor.Attributes[0].PropertyName, + descriptor.Attributes[0].PropertyName, + StringComparer.Ordinal); + Assert.Equal( + expectedDescriptor.Attributes[0].TypeName, + descriptor.Attributes[0].TypeName, + StringComparer.Ordinal); + Assert.Equal(expectedDescriptor.Attributes[1].IsStringProperty, descriptor.Attributes[1].IsStringProperty); + Assert.Equal(expectedDescriptor.Attributes[1].Name, descriptor.Attributes[1].Name, StringComparer.Ordinal); + Assert.Equal( + expectedDescriptor.Attributes[1].PropertyName, + descriptor.Attributes[1].PropertyName, + StringComparer.Ordinal); + Assert.Equal( + expectedDescriptor.Attributes[1].TypeName, + descriptor.Attributes[1].TypeName, + StringComparer.Ordinal); + Assert.Empty(descriptor.RequiredAttributes); + } + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index f67f629591..bb96653ccb 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -3355,6 +3355,95 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers SourceLocation.Zero) } }, + { + "

> + { + new KeyValuePair( + "bar", + new MarkupBlock( + factory.Markup("false"), + factory.Markup(" > + { + new KeyValuePair( + "bar", + factory.Markup("false")) + })), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero), + new RazorError( + "TagHelper attributes must be welformed.", + absoluteIndex: 12, + lineIndex: 0, + columnIndex: 12) + } + }, + { + "

> + { + new KeyValuePair( + "bar", + factory.Markup("false'")) + })), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, + { + "

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List> + { + new KeyValuePair( + "bar", + new MarkupBlock( + factory.Markup("false'"), + factory.Markup(" >

"))) + })), + new [] + { + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatNoCloseAngle, "p"), + SourceLocation.Zero), + new RazorError( + string.Format(CultureInfo.InvariantCulture, errorFormatUnclosed, "p"), + SourceLocation.Zero) + } + }, { "

", new MarkupBlock(