diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs index 9a539690d29..abea6f12392 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs @@ -295,7 +295,7 @@ public void Execute_DirectiveWithoutQuotes_RewritesTagHelpers_TagHelperMatchesEl var formTagHelper = Assert.Single(tagHelperNodes); Assert.Equal("form", formTagHelper.TagHelperInfo.TagName); - Assert.Equal(2, formTagHelper.TagHelperInfo.BindingResult.Mappings[tagHelper].Length); + Assert.Equal(2, formTagHelper.TagHelperInfo.BindingResult.GetBoundRules(tagHelper).Length); } [Fact] @@ -340,7 +340,7 @@ public void Execute_DirectiveWithQuotes_RewritesTagHelpers_TagHelperMatchesEleme var formTagHelper = Assert.Single(tagHelperNodes); Assert.Equal("form", formTagHelper.TagHelperInfo.TagName); - Assert.Equal(2, formTagHelper.TagHelperInfo.BindingResult.Mappings[tagHelper].Length); + Assert.Equal(2, formTagHelper.TagHelperInfo.BindingResult.GetBoundRules(tagHelper).Length); } [Fact] @@ -441,6 +441,7 @@ public void Execute_SetsTagHelperDocumentContext() // Assert var context = codeDocument.GetTagHelperContext(); + Assert.NotNull(context); Assert.Null(context.Prefix); Assert.Empty(context.TagHelpers); } diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDiscoveryIntegrationTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDiscoveryIntegrationTest.cs index dac3556aad0..bd4a7604ba8 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDiscoveryIntegrationTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDiscoveryIntegrationTest.cs @@ -32,8 +32,9 @@ public class MyComponent : ComponentBase var result = CompileToCSharp(string.Empty); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.MyComponent"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + Assert.Contains(context.TagHelpers, t => t.Name == "Test.MyComponent"); } [Fact] @@ -55,15 +56,16 @@ public class MyComponent : ComponentBase var result = CompileToCSharp(string.Empty); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); - Assert.Contains(bindings.TagHelpers, t => + Assert.Contains(context.TagHelpers, t => { return t.Name == "Test.AnotherNamespace.MyComponent" && t.IsComponentFullyQualifiedNameMatch; }); - Assert.DoesNotContain(bindings.TagHelpers, t => + Assert.DoesNotContain(context.TagHelpers, t => { return t.Name == "Test.AnotherNamespace.MyComponent" && !t.IsComponentFullyQualifiedNameMatch; @@ -79,8 +81,10 @@ public void ComponentDiscovery_CanFindComponent_DefinedinCshtml() var result = CompileToCSharp("UniqueName.cshtml", cshtmlContent: string.Empty); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + + Assert.Contains(context.TagHelpers, t => t.Name == "Test.UniqueName"); } [Fact] @@ -96,8 +100,10 @@ @typeparam TItem }"); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + + Assert.Contains(context.TagHelpers, t => t.Name == "Test.UniqueName"); } [Fact] @@ -113,8 +119,10 @@ public void ComponentDiscovery_CanFindComponent_WithTypeParameterAndSemicolon() }"); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + + Assert.Contains(context.TagHelpers, t => t.Name == "Test.UniqueName"); } [Fact] @@ -132,8 +140,10 @@ @typeparam TItem3 }"); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + + Assert.Contains(context.TagHelpers, t => t.Name == "Test.UniqueName"); } [Fact] @@ -151,7 +161,9 @@ @typeparam TItem3 }"); // Assert - var bindings = result.CodeDocument.GetTagHelperContext(); - Assert.Contains(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + var context = result.CodeDocument.GetTagHelperContext(); + Assert.NotNull(context); + + Assert.Contains(context.TagHelpers, t => t.Name == "Test.UniqueName"); } } diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorCodeDocumentExtensionsTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorCodeDocumentExtensionsTest.cs index dd2d1d0ec60..fd21f3abb82 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorCodeDocumentExtensionsTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorCodeDocumentExtensionsTest.cs @@ -137,13 +137,13 @@ public void SetCSharpDocument_SetsCSharpDocument() } [Fact] - public void GetTagHelperContext_ReturnsTagHelperContext() + public void GetAndSetTagHelperContext_ReturnsTagHelperContext() { // Arrange var codeDocument = TestRazorCodeDocument.CreateEmpty(); var expected = TagHelperDocumentContext.Create(prefix: null, tagHelpers: []); - codeDocument.Items[typeof(TagHelperDocumentContext)] = expected; + codeDocument.SetTagHelperContext(expected); // Act var actual = codeDocument.GetTagHelperContext(); @@ -152,21 +152,6 @@ public void GetTagHelperContext_ReturnsTagHelperContext() Assert.Same(expected, actual); } - [Fact] - public void SetTagHelperContext_SetsTagHelperContext() - { - // Arrange - var codeDocument = TestRazorCodeDocument.CreateEmpty(); - - var expected = TagHelperDocumentContext.Create(prefix: null, tagHelpers: []); - - // Act - codeDocument.SetTagHelperContext(expected); - - // Assert - Assert.Same(expected, codeDocument.Items[typeof(TagHelperDocumentContext)]); - } - [Fact] public void TryComputeNamespace_RootNamespaceNotSet_ReturnsNull() { diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TagHelperBinderTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TagHelperBinderTest.cs index c413db5a176..5473b4cb924 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TagHelperBinderTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TagHelperBinderTest.cs @@ -38,8 +38,8 @@ public void GetBinding_ReturnsBindingWithInformation() Assert.Equal("th:div", bindingResult.TagName); Assert.Equal("body", bindingResult.ParentTagName); Assert.Equal>(expectedAttributes, bindingResult.Attributes); - Assert.Equal("th:", bindingResult.TagHelperPrefix); - Assert.Equal(divTagHelper.TagMatchingRules, bindingResult.Mappings[divTagHelper]); + Assert.Equal("th:", bindingResult.TagNamePrefix); + Assert.Equal(divTagHelper.TagMatchingRules, bindingResult.GetBoundRules(divTagHelper)); } [Fact] @@ -81,7 +81,7 @@ void TestTagName(string tagName, TagMatchingRuleDescriptor expectedBindingResult Assert.Equal(expectedDescriptors, bindingResult.Descriptors); Assert.Equal(tagName, bindingResult.TagName); - var mapping = Assert.Single(bindingResult.Mappings[multiTagHelper]); + var mapping = Assert.Single(bindingResult.GetBoundRules(multiTagHelper)); Assert.Equal(expectedBindingResult, mapping); } } @@ -136,7 +136,7 @@ void TestTagName(string tagName, TagHelperDescriptor[] expectedDescriptors, TagM for (int i = 0; i < expectedDescriptors.Length; i++) { - var mapping = Assert.Single(bindingResult.Mappings[expectedDescriptors[i]]); + var mapping = Assert.Single(bindingResult.GetBoundRules(expectedDescriptors[i])); Assert.Equal(expectedBindingResults[i], mapping); } } @@ -598,7 +598,7 @@ public void GetBinding_DescriptorWithMultipleRules_CorrectlySelectsMatchingRules // Assert var boundDescriptor = Assert.Single(binding.Descriptors); Assert.Same(multiRuleDescriptor, boundDescriptor); - var boundRules = binding.Mappings[boundDescriptor]; + var boundRules = binding.GetBoundRules(boundDescriptor); var boundRule = Assert.Single(boundRules); Assert.Equal("div", boundRule.TagName); } @@ -626,7 +626,7 @@ public void GetBinding_PrefixedParent_ReturnsBinding() // Assert var boundDescriptor = Assert.Single(bindingResult.Descriptors); Assert.Same(divDescriptor, boundDescriptor); - var boundRules = bindingResult.Mappings[boundDescriptor]; + var boundRules = bindingResult.GetBoundRules(boundDescriptor); var boundRule = Assert.Single(boundRules); Assert.Equal("div", boundRule.TagName); Assert.Equal("p", boundRule.ParentTag); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperRewritePhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperRewritePhase.cs index 4507e4e60e6..03e8a80f753 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperRewritePhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperRewritePhase.cs @@ -12,7 +12,7 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, Cancellation { var syntaxTree = codeDocument.GetPreTagHelperSyntaxTree(); var context = codeDocument.GetTagHelperContext(); - if (syntaxTree is null || context.TagHelpers.Length == 0) + if (syntaxTree is null || context is not { TagHelpers.Length: > 0 }) { // No descriptors, no-op. return; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs index 3d5afbce23d..3dbbd550198 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperBlockRewriter.cs @@ -27,10 +27,9 @@ public static TagMode GetTagMode( } var hasDirectiveAttribute = false; - foreach (var descriptor in bindingResult.Descriptors) + foreach (var boundRulesInfo in bindingResult.AllBoundRules) { - var boundRules = bindingResult.Mappings[descriptor]; - var nonDefaultRule = boundRules.FirstOrDefault(static rule => rule.TagStructure != TagStructure.Unspecified); + var nonDefaultRule = boundRulesInfo.Rules.FirstOrDefault(static rule => rule.TagStructure != TagStructure.Unspecified); if (nonDefaultRule?.TagStructure == TagStructure.WithoutEndTag) { @@ -42,6 +41,7 @@ public static TagMode GetTagMode( // vs // // We don't want this to become an error just because you added a directive attribute. + var descriptor = boundRulesInfo.Descriptor; if (descriptor.IsAnyComponentDocumentTagHelper() && !descriptor.IsComponentOrChildContentTagHelper) { hasDirectiveAttribute = true; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperParseTreeRewriter.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperParseTreeRewriter.cs index 5e1a323a65e..c75e373ea72 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperParseTreeRewriter.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperParseTreeRewriter.cs @@ -34,9 +34,9 @@ public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree, TagHelperBinde builder.AddRange(treeDiagnostics); builder.AddRange(sinkDiagnostics); - foreach (var tagHelper in binder.TagHelpers) + foreach (var descriptor in binder.Descriptors) { - foreach (var diagnostic in tagHelper.GetAllDiagnostics()) + foreach (var diagnostic in descriptor.GetAllDiagnostics()) { builder.Add(diagnostic); } @@ -137,7 +137,7 @@ public override SyntaxNode VisitMarkupElement(MarkupElementSyntax node) else { // Tag helper start tag. Keep track. - var tracker = new TagHelperTracker(_binder.TagHelperPrefix, tagHelperInfo); + var tracker = new TagHelperTracker(_binder.TagNamePrefix, tagHelperInfo); _trackerStack.Push(tracker); } } @@ -347,10 +347,9 @@ private bool TryRewriteTagHelperEnd( return false; } - foreach (var descriptor in tagHelperBinding.Descriptors) + foreach (var boundRulesInfo in tagHelperBinding.AllBoundRules) { - var boundRules = tagHelperBinding.Mappings[descriptor]; - var invalidRule = boundRules.FirstOrDefault(static rule => rule.TagStructure == TagStructure.WithoutEndTag); + var invalidRule = boundRulesInfo.Rules.FirstOrDefault(static rule => rule.TagStructure == TagStructure.WithoutEndTag); if (invalidRule != null) { @@ -359,7 +358,7 @@ private bool TryRewriteTagHelperEnd( RazorDiagnosticFactory.CreateParsing_TagHelperMustNotHaveAnEndTag( new SourceSpan(SourceLocationTracker.Advance(endTag.GetSourceLocation(_source), " Names, HashSet NameSet)> _lazyAllowedChildren; @@ -734,10 +734,10 @@ private record TagHelperTracker : TagTracker public ImmutableArray AllowedChildren => _lazyAllowedChildren.Value.Names; - public TagHelperTracker(string? tagHelperPrefix, TagHelperInfo info) + public TagHelperTracker(string? tagNamePrefix, TagHelperInfo info) : base(info.TagName, IsTagHelper: true) { - _tagHelperPrefix = tagHelperPrefix; + _tagNamePrefix = tagNamePrefix; _binding = info.BindingResult; _lazyAllowedChildren = new(CreateAllowedChildren); @@ -773,7 +773,7 @@ public bool AllowsChild(string tagName, bool nameIncludesPrefix) private HashSet CreatePrefixedAllowedChildren() { - if (_tagHelperPrefix is not string tagHelperPrefix) + if (_tagNamePrefix is not string tagNamePrefix) { return _lazyAllowedChildren.Value.NameSet; } @@ -782,7 +782,7 @@ private HashSet CreatePrefixedAllowedChildren() foreach (var childName in AllowedChildren) { - distinctSet.Add(tagHelperPrefix + childName); + distinctSet.Add(tagNamePrefix + childName); } return distinctSet; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs index ebaabeb434b..a8cd7cc9807 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Language; @@ -17,6 +19,8 @@ public sealed class RazorCodeDocument public string FileKind => ParserOptions.FileKind; + private TagHelperDocumentContext? _tagHelperContext; + private RazorCodeDocument( RazorSourceDocument source, ImmutableArray imports, @@ -52,4 +56,23 @@ public static RazorCodeDocument Create( return new RazorCodeDocument(source, imports, parserOptions, codeGenerationOptions); } + + internal bool TryGetTagHelperContext([NotNullWhen(true)] out TagHelperDocumentContext? result) + { + result = _tagHelperContext; + return result is not null; + } + + internal TagHelperDocumentContext? GetTagHelperContext() + => _tagHelperContext; + + internal TagHelperDocumentContext GetRequiredTagHelperContext() + => _tagHelperContext.AssumeNotNull(); + + internal void SetTagHelperContext(TagHelperDocumentContext context) + { + ArgHelper.ThrowIfNull(context); + + _tagHelperContext = context; + } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocumentExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocumentExtensions.cs index 58f1924a09e..3b4f69c5ad2 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocumentExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocumentExtensions.cs @@ -22,26 +22,6 @@ public static class RazorCodeDocumentExtensions private static readonly object CssScopeKey = new(); private static readonly object NamespaceKey = new(); - internal static TagHelperDocumentContext GetTagHelperContext(this RazorCodeDocument document) - { - if (document == null) - { - throw new ArgumentNullException(nameof(document)); - } - - return (TagHelperDocumentContext)document.Items[typeof(TagHelperDocumentContext)]; - } - - internal static void SetTagHelperContext(this RazorCodeDocument document, TagHelperDocumentContext context) - { - if (document == null) - { - throw new ArgumentNullException(nameof(document)); - } - - document.Items[typeof(TagHelperDocumentContext)] = context; - } - internal static IReadOnlyList GetTagHelpers(this RazorCodeDocument document) { if (document == null) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinder.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinder.cs index 24d3b5a09a2..64ea5058623 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinder.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinder.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics; using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language; @@ -14,30 +15,75 @@ namespace Microsoft.AspNetCore.Razor.Language; /// internal sealed class TagHelperBinder { - private readonly Dictionary> _registrations; + private readonly ImmutableArray _catchAllDescriptors; + private readonly ReadOnlyDictionary> _tagNameToDescriptorsMap; - public string? TagHelperPrefix { get; } - public ImmutableArray TagHelpers { get; } + public string? TagNamePrefix { get; } + public ImmutableArray Descriptors { get; } /// /// Instantiates a new instance of the . /// - /// The tag helper prefix being used by the document. - /// The s that the + /// The tag helper prefix being used by the document. + /// The s that the /// will pull from. - public TagHelperBinder(string? tagHelperPrefix, ImmutableArray tagHelpers) + public TagHelperBinder(string? tagNamePrefix, ImmutableArray descriptors) { - TagHelperPrefix = tagHelperPrefix; - TagHelpers = tagHelpers; + TagNamePrefix = tagNamePrefix; + Descriptors = descriptors.NullToEmpty(); - // To reduce the frequency of dictionary resizes we use the incoming number of descriptors as a heuristic - _registrations = new Dictionary>(tagHelpers.Length, StringComparer.OrdinalIgnoreCase); + ProcessDescriptors(descriptors, tagNamePrefix, out _tagNameToDescriptorsMap, out _catchAllDescriptors); + } + + private static void ProcessDescriptors( + ImmutableArray descriptors, + string? tagNamePrefix, + out ReadOnlyDictionary> tagNameToDescriptorsMap, + out ImmutableArray catchAllDescriptors) + { + using var catchAllBuilder = new PooledArrayBuilder(); + using var pooledMap = StringDictionaryPool.Builder>.OrdinalIgnoreCase.GetPooledObject(out var mapBuilder); + using var pooledSet = HashSetPool.GetPooledObject(out var distinctSet); - // Populate our registrations - foreach (var descriptor in tagHelpers) + // Build a map of tag name -> tag helpers. + foreach (var descriptor in descriptors) { - Register(descriptor); + if (!distinctSet.Add(descriptor)) + { + // We're already seen this descriptor, skip it. + continue; + } + + foreach (var rule in descriptor.TagMatchingRules) + { + if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName) + { + // This is a catch-all descriptor, we can keep track of it separately. + catchAllBuilder.Add(descriptor); + } + else + { + // This is a specific tag name, we need to add it to the map. + var tagName = tagNamePrefix + rule.TagName; + var builder = mapBuilder.GetOrAdd(tagName, _ => ImmutableArray.CreateBuilder()); + + builder.Add(descriptor); + } + } + } + + // Build the final dictionary with immutable arrays. + var map = new Dictionary>(capacity: mapBuilder.Count, StringComparer.OrdinalIgnoreCase); + + foreach (var (key, value) in mapBuilder) + { + map.Add(key, value.DrainToImmutable()); } + + tagNameToDescriptorsMap = new ReadOnlyDictionary>(map); + + // Build the catch all descriptors array. + catchAllDescriptors = catchAllBuilder.DrainToImmutable(); } /// @@ -49,105 +95,94 @@ public TagHelperBinder(string? tagHelperPrefix, ImmutableArrayThe parent tag name of the given tag. /// Is the parent tag of the given tag a tag helper. /// s that apply to the given HTML tag criteria. - /// Will return null if no s are a match. + /// Will return if no s are a match. public TagHelperBinding? GetBinding( string tagName, ImmutableArray> attributes, string? parentTagName, bool parentIsTagHelper) { - if (!TagHelperPrefix.IsNullOrEmpty() && - (tagName.Length <= TagHelperPrefix.Length || - !tagName.StartsWith(TagHelperPrefix, StringComparison.OrdinalIgnoreCase))) - { - // The tagName doesn't have the tag helper prefix, we can short circuit. - return null; - } + var tagNameSpan = tagName.AsSpan(); + var parentTagNameSpan = parentTagName.AsSpan(); + var tagNamePrefixSpan = TagNamePrefix.AsSpan(); - var tagNameWithoutPrefix = tagName.AsSpanOrDefault(); - var parentTagNameWithoutPrefix = parentTagName.AsSpanOrDefault(); - - if (TagHelperPrefix is { Length: var length and > 0 }) + if (!tagNamePrefixSpan.IsEmpty) { - tagNameWithoutPrefix = tagNameWithoutPrefix[length..]; + if (!tagNameSpan.StartsWith(tagNamePrefixSpan, StringComparison.OrdinalIgnoreCase)) + { + // The tag name doesn't start with the prefix. So, we're done. + return null; + } + + tagNameSpan = tagNameSpan[tagNamePrefixSpan.Length..]; if (parentIsTagHelper) { - parentTagNameWithoutPrefix = parentTagNameWithoutPrefix[length..]; + Debug.Assert( + parentTagNameSpan.StartsWith(tagNamePrefixSpan, StringComparison.OrdinalIgnoreCase), + "If the parent is a tag helper, it must start with the tag name prefix."); + + parentTagNameSpan = parentTagNameSpan[tagNamePrefixSpan.Length..]; } } - using var _ = DictionaryPool>.GetPooledObject(out var applicableDescriptors); + using var resultsBuilder = new PooledArrayBuilder(); + using var tempRulesBuilder = new PooledArrayBuilder(); + using var pooledSet = HashSetPool.GetPooledObject(out var distinctSet); // First, try any tag helpers with this tag name. - if (_registrations.TryGetValue(tagName, out var matchingDescriptors)) + if (_tagNameToDescriptorsMap.TryGetValue(tagName, out var matchingDescriptors)) { - FindApplicableDescriptors(matchingDescriptors, tagNameWithoutPrefix, parentTagNameWithoutPrefix, attributes, applicableDescriptors); + CollectBoundRulesInfo( + matchingDescriptors, + tagNameSpan, parentTagNameSpan, attributes, + ref resultsBuilder.AsRef(), ref tempRulesBuilder.AsRef(), distinctSet); } // Next, try any "catch all" descriptors. - if (_registrations.TryGetValue(TagHelperMatchingConventions.ElementCatchAllName, out var catchAllDescriptors)) - { - FindApplicableDescriptors(catchAllDescriptors, tagNameWithoutPrefix, parentTagNameWithoutPrefix, attributes, applicableDescriptors); - } - - if (applicableDescriptors.Count == 0) - { - return null; - } - - return new TagHelperBinding( - tagName, - attributes, - parentTagName, - applicableDescriptors.ToFrozenDictionary(), - TagHelperPrefix); - - static void FindApplicableDescriptors( - HashSet descriptors, - ReadOnlySpan tagNameWithoutPrefix, - ReadOnlySpan parentTagNameWithoutPrefix, + CollectBoundRulesInfo( + _catchAllDescriptors, + tagNameSpan, parentTagNameSpan, attributes, + ref resultsBuilder.AsRef(), ref tempRulesBuilder.AsRef(), distinctSet); + + return resultsBuilder.Count > 0 + ? new(resultsBuilder.DrainToImmutable(), tagName, parentTagName, attributes, TagNamePrefix) + : null; + + static void CollectBoundRulesInfo( + ImmutableArray descriptors, + ReadOnlySpan tagName, + ReadOnlySpan parentTagName, ImmutableArray> attributes, - Dictionary> applicableDescriptors) + ref PooledArrayBuilder resultsBuilder, + ref PooledArrayBuilder tempRulesBuilder, + HashSet distinctSet) { - using var applicableRules = new PooledArrayBuilder(); - foreach (var descriptor in descriptors) { + if (!distinctSet.Add(descriptor)) + { + // We're already seen this descriptor, skip it. + continue; + } + + Debug.Assert(tempRulesBuilder.Count == 0); + foreach (var rule in descriptor.TagMatchingRules) { - if (TagHelperMatchingConventions.SatisfiesRule(rule, tagNameWithoutPrefix, parentTagNameWithoutPrefix, attributes)) + if (TagHelperMatchingConventions.SatisfiesRule(rule, tagName, parentTagName, attributes)) { - applicableRules.Add(rule); + tempRulesBuilder.Add(rule); } } - if (applicableRules.Count > 0) + if (tempRulesBuilder.Count > 0) { - applicableDescriptors[descriptor] = applicableRules.DrainToImmutable(); + resultsBuilder.Add(new(descriptor, tempRulesBuilder.ToImmutable())); } - applicableRules.Clear(); + tempRulesBuilder.Clear(); } } } - - private void Register(TagHelperDescriptor descriptor) - { - foreach (var rule in descriptor.TagMatchingRules) - { - var registrationKey = rule.TagName == TagHelperMatchingConventions.ElementCatchAllName - ? TagHelperMatchingConventions.ElementCatchAllName - : TagHelperPrefix + rule.TagName; - - // Ensure there's a HashSet to add the descriptor to. - if (!_registrations.TryGetValue(registrationKey, out var descriptorSet)) - { - descriptorSet = []; - _registrations[registrationKey] = descriptorSet; - } - - descriptorSet.Add(descriptor); - } - } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinding.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinding.cs index 378586eb70e..efd2091857a 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinding.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperBinding.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; @@ -10,55 +9,79 @@ namespace Microsoft.AspNetCore.Razor.Language; internal sealed class TagHelperBinding { + public ImmutableArray AllBoundRules { get; } + public string? TagNamePrefix { get; } public string TagName { get; } public string? ParentTagName { get; } public ImmutableArray> Attributes { get; } - public FrozenDictionary> Mappings { get; } - public string? TagHelperPrefix { get; } - public ImmutableArray Descriptors => Mappings.Keys; + private ImmutableArray _descriptors; + private bool? _isAttributeMatch; internal TagHelperBinding( + ImmutableArray allBoundRules, string tagName, - ImmutableArray> attributes, string? parentTagName, - FrozenDictionary> mappings, - string? tagHelperPrefix) + ImmutableArray> attributes, + string? tagNamePrefix) { + AllBoundRules = allBoundRules; TagName = tagName; - Attributes = attributes; ParentTagName = parentTagName; - Mappings = mappings; - TagHelperPrefix = tagHelperPrefix; + Attributes = attributes; + TagNamePrefix = tagNamePrefix; } + public ImmutableArray Descriptors + { + get + { + if (_descriptors.IsDefault) + { + ImmutableInterlocked.InterlockedInitialize(ref _descriptors, AllBoundRules.SelectAsArray(x => x.Descriptor)); + } + + return _descriptors; + } + } + + public ImmutableArray GetBoundRules(TagHelperDescriptor descriptor) + => AllBoundRules.First(descriptor, static (info, d) => info.Descriptor.Equals(d)).Rules; + /// - /// Gets a value that indicates whether the the binding matched on attributes only. + /// Gets a value that indicates whether the the binding matched on attributes only. /// - /// false if the entire element should be classified as a tag helper. + /// + /// Returns if the entire element should be classified as a tag helper. + /// /// - /// If this returns true, use TagHelperFactsService.GetBoundTagHelperAttributes to find the - /// set of attributes that should be considered part of the match. + /// If this returns , use TagHelperFactsService.GetBoundTagHelperAttributes to find the + /// set of attributes that should be considered part of the match. /// public bool IsAttributeMatch { get { - foreach (var descriptor in Mappings.Keys) + return _isAttributeMatch ??= ComputeIsAttributeMatch(Descriptors); + + static bool ComputeIsAttributeMatch(ImmutableArray descriptors) { - if (!descriptor.Metadata.TryGetValue(TagHelperMetadata.Common.ClassifyAttributesOnly, out var value) || - !string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase)) + foreach (var descriptor in descriptors) { - return false; + if (!descriptor.Metadata.TryGetValue(TagHelperMetadata.Common.ClassifyAttributesOnly, out var value) || + !string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase)) + { + return false; + } } - } - // All the matching tag helpers want to be classified with **attributes only**. - // - // Ex: (components) - // - //