Use the TagHelperBinder to dedupe taghelpers

This change does deduplication of taghelpers during the binding/rewriting
phase. This is needed when a taghelper has multiple sets of html
attributes that are required (behaves like an OR). This is used lots in
MVC.

The old codebase used to do this in the codegen phase, but it seems
beneficial to do as early as possible.
This commit is contained in:
Ryan Nowak 2017-01-26 14:56:49 -08:00
parent 98d5d1c70e
commit d187edbd76
7 changed files with 155 additions and 20 deletions

View File

@ -19,6 +19,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
/// </summary>
public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer();
/// <summary>
/// An instance of <see cref="TagHelperDescriptorComparer"/> that only compares
/// <see cref="TagHelperDescriptor.TypeName"/>.
/// </summary>
public static readonly TagHelperDescriptorComparer TypeName = new TypeNameTagHelperDescriptorComparer();
/// <summary>
/// Initializes a new <see cref="TagHelperDescriptorComparer"/> instance.
/// </summary>
@ -101,5 +107,34 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
return hashCodeCombiner.CombinedHash;
}
private class TypeNameTagHelperDescriptorComparer : TagHelperDescriptorComparer
{
public override bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{
if (object.ReferenceEquals(descriptorX, descriptorY))
{
return true;
}
else if (descriptorX == null ^ descriptorY == null)
{
return false;
}
else
{
return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal);
}
}
public override int GetHashCode(TagHelperDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
return descriptor.TypeName == null ? 0 : StringComparer.Ordinal.GetHashCode(descriptor.TypeName);
}
}
}
}

View File

@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
return applicableDescriptors;
return applicableDescriptors.Distinct(TagHelperDescriptorComparer.TypeName);
}
private bool HasRequiredParentTag(

View File

@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
var catchAllDescriptor2 = new TagHelperDescriptor
{
TagName = TagHelperDescriptorProvider.ElementCatchAllTarget,
TypeName = "CatchAllTagHelper",
TypeName = "CatchAllTagHelper2",
AssemblyName = "SomeAssembly",
RequiredAttributes = new[]
{
@ -268,7 +268,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
var provider = new TagHelperDescriptorProvider((IEnumerable<TagHelperDescriptor>)availableDescriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes, parentTagName: "p");
var resolvedDescriptors = provider.GetDescriptors(tagName, providedAttributes, parentTagName: "p").ToArray();
// Assert
Assert.Equal((IEnumerable<TagHelperDescriptor>)expectedDescriptors, resolvedDescriptors, CaseSensitiveTagHelperDescriptorComparer.Default);
@ -295,6 +295,57 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
Assert.Empty(resolvedDescriptors);
}
[Fact]
public void GetDescriptors_DeduplicatesTagHelpersByTypeName()
{
// Arrange
var descriptors = new[]
{
new TagHelperDescriptor
{
AssemblyName = "TestAssembly",
TagName = "form",
TypeName = "TestFormTagHelper",
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>()
{
new TagHelperRequiredAttributeDescriptor()
{
Name = "a",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch
}
},
},
new TagHelperDescriptor
{
AssemblyName = "TestAssembly",
TagName = "form",
TypeName = "TestFormTagHelper",
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>()
{
new TagHelperRequiredAttributeDescriptor()
{
Name = "b",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch
}
},
},
};
var provider = new TagHelperDescriptorProvider(descriptors);
// Act
var resolvedDescriptors = provider.GetDescriptors(
tagName: "form",
attributes: new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("a", "hi" ),
new KeyValuePair<string, string>("b", "there"),
},
parentTagName: "p");
// Assert
Assert.Same(descriptors[0], Assert.Single(resolvedDescriptors));
}
[Fact]
public void GetDescriptors_OnlyUnderstandsSinglePrefix()
{

View File

@ -55,6 +55,71 @@ namespace Microsoft.AspNetCore.Razor.Evolution
Assert.Equal("input", inputTagHelper.TagName);
}
[Fact]
public void Execute_RewritesTagHelpers_TagHelperMatchesElementTwice()
{
// Arrange
var engine = RazorEngine.Create(builder =>
{
builder.AddTagHelpers(new[]
{
new TagHelperDescriptor
{
AssemblyName = "TestAssembly",
TagName = "form",
TypeName = "TestFormTagHelper",
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>()
{
new TagHelperRequiredAttributeDescriptor()
{
Name = "a",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch
}
},
},
new TagHelperDescriptor
{
AssemblyName = "TestAssembly",
TagName = "form",
TypeName = "TestFormTagHelper",
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>()
{
new TagHelperRequiredAttributeDescriptor()
{
Name = "b",
NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch
}
},
}
});
});
var pass = new TagHelperBinderSyntaxTreePass()
{
Engine = engine,
};
var content =
@"
@addTagHelper *, TestAssembly
<form a=""hi"" b=""there"">
</form>";
var sourceDocument = TestRazorSourceDocument.Create(content);
var codeDocument = RazorCodeDocument.Create(sourceDocument);
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
// Act
var rewrittenTree = pass.Execute(codeDocument, originalTree);
// Assert
Assert.Empty(rewrittenTree.Diagnostics);
Assert.Equal(3, rewrittenTree.Root.Children.Count);
var formTagHelper = Assert.IsType<TagHelperBlock>(rewrittenTree.Root.Children[2]);
Assert.Equal("form", formTagHelper.TagName);
Assert.Single(formTagHelper.Descriptors);
}
[Fact]
public void Execute_NoopsWhenNoTagHelperFeature()
{

View File

@ -16,20 +16,14 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests.TestFiles
public async System.Threading.Tasks.Task ExecuteAsync()
{
__TestNamespace_InputTagHelper = CreateTagHelper<global::TestNamespace.InputTagHelper>();
__TestNamespace_InputTagHelper = CreateTagHelper<global::TestNamespace.InputTagHelper>();
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__TestNamespace_InputTagHelper.Type = "checkbox";
__TestNamespace_InputTagHelper.Type = __TestNamespace_InputTagHelper.Type;
__TestNamespace_CatchAllTagHelper.Type = __TestNamespace_InputTagHelper.Type;
__TestNamespace_CatchAllTagHelper.Type = __TestNamespace_InputTagHelper.Type;
#line 3 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/DuplicateTargetTagHelper.cshtml"
__TestNamespace_InputTagHelper.Checked = true;
#line default
#line hidden
__TestNamespace_InputTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
__TestNamespace_CatchAllTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
__TestNamespace_CatchAllTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
}
#pragma warning restore 1998

View File

@ -1,5 +1,5 @@
Source Location: (67:2,32 [4] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/DuplicateTargetTagHelper.cshtml)
|true|
Generated Location: (1664:26,41 [4] )
Generated Location: (1273:22,41 [4] )
|true|

View File

@ -36,18 +36,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests.TestFiles
);
__TestNamespace_InputTagHelper = CreateTagHelper<global::TestNamespace.InputTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper);
__TestNamespace_InputTagHelper = CreateTagHelper<global::TestNamespace.InputTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_InputTagHelper);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__TestNamespace_CatchAllTagHelper = CreateTagHelper<global::TestNamespace.CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__TestNamespace_CatchAllTagHelper);
__TestNamespace_InputTagHelper.Type = (string)__tagHelperAttribute_0.Value;
__tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_0);
__TestNamespace_InputTagHelper.Type = (string)__tagHelperAttribute_0.Value;
__tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_0);
__TestNamespace_CatchAllTagHelper.Type = (string)__tagHelperAttribute_0.Value;
__tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_0);
__TestNamespace_CatchAllTagHelper.Type = (string)__tagHelperAttribute_0.Value;
__tagHelperExecutionContext.AddTagHelperAttribute(__tagHelperAttribute_0);
#line 3 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/DuplicateTargetTagHelper.cshtml"
@ -56,8 +48,6 @@ __TestNamespace_InputTagHelper.Checked = true;
#line default
#line hidden
__tagHelperExecutionContext.AddTagHelperAttribute("checked", __TestNamespace_InputTagHelper.Checked, global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes);
__TestNamespace_InputTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
__TestNamespace_CatchAllTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
__TestNamespace_CatchAllTagHelper.Checked = __TestNamespace_InputTagHelper.Checked;
await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
if (!__tagHelperExecutionContext.Output.IsContentModified)