diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs index abea6f12392..b50d579fc1d 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DefaultRazorTagHelperBinderPhaseTest.cs @@ -1245,26 +1245,6 @@ @using static SomeProject.SomeOtherFolder.Foo Assert.Same(componentDescriptor, result); } - [Theory] - [InlineData("", "", true)] - [InlineData("Foo", "Project", true)] - [InlineData("Project.Foo", "Project", true)] - [InlineData("Project.Foo", "global::Project", true)] - [InlineData("Project.Bar.Foo", "Project.Bar", true)] - [InlineData("Project.Foo", "Project.Bar", false)] - [InlineData("Project.Foo", "global::Project.Bar", false)] - [InlineData("Project.Bar.Foo", "Project", false)] - [InlineData("Bar.Foo", "Project", false)] - public void IsTypeInNamespace_WorksAsExpected(string typeName, string @namespace, bool expected) - { - // Arrange & Act - var descriptor = CreateComponentDescriptor(typeName, typeName, "Test.dll"); - var result = DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor.IsTypeInNamespace(descriptor, @namespace); - - // Assert - Assert.Equal(expected, result); - } - [Theory] [InlineData("", "", true)] [InlineData("Foo", "Project", true)] @@ -1273,11 +1253,13 @@ public void IsTypeInNamespace_WorksAsExpected(string typeName, string @namespace [InlineData("Project.Foo", "Project.Bar", true)] [InlineData("Project.Bar.Foo", "Project", false)] [InlineData("Bar.Foo", "Project", false)] - public void IsTypeInScope_WorksAsExpected(string typeName, string currentNamespace, bool expected) + public void IsTypeNamespaceInScope_WorksAsExpected(string typeName, string currentNamespace, bool expected) { // Arrange & Act var descriptor = CreateComponentDescriptor(typeName, typeName, "Test.dll"); - var result = DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor.IsTypeInScope(descriptor, currentNamespace); + var tagHelperTypeNamespace = descriptor.GetTypeNamespace(); + + var result = DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor.IsTypeNamespaceInScope(tagHelperTypeNamespace, currentNamespace); // Assert Assert.Equal(expected, result); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperContextDiscoveryPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperContextDiscoveryPhase.cs index e349364cd1f..2a57f87eeb4 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperContextDiscoveryPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorTagHelperContextDiscoveryPhase.cs @@ -58,18 +58,18 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, Cancellation codeDocument.SetPreTagHelperSyntaxTree(syntaxTree); } - private static ReadOnlySpan GetSpanWithoutGlobalPrefix(string s) + internal static ReadOnlyMemory GetMemoryWithoutGlobalPrefix(string s) { const string globalPrefix = "global::"; - var span = s.AsSpan(); + var mem = s.AsMemory(); - if (span.StartsWith(globalPrefix.AsSpan(), StringComparison.Ordinal)) + if (mem.Span.StartsWith(globalPrefix.AsSpan(), StringComparison.Ordinal)) { - return span[globalPrefix.Length..]; + return mem[globalPrefix.Length..]; } - return span; + return mem; } internal abstract class DirectiveVisitor : SyntaxWalker @@ -241,7 +241,7 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node) continue; } - switch (GetSpanWithoutGlobalPrefix(addTagHelper.TypePattern)) + switch (GetMemoryWithoutGlobalPrefix(addTagHelper.TypePattern).Span) { case ['*']: AddMatches(nonComponentTagHelpers); @@ -287,7 +287,7 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node) continue; } - switch (GetSpanWithoutGlobalPrefix(removeTagHelper.TypePattern)) + switch (GetMemoryWithoutGlobalPrefix(removeTagHelper.TypePattern).Span) { case ['*']: RemoveMatches(nonComponentTagHelpers); @@ -334,7 +334,9 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node) internal sealed class ComponentDirectiveVisitor : DirectiveVisitor { - private readonly List _nonFullyQualifiedComponents = []; + // The list values in this dictionary are pooled + private readonly Dictionary, List> _typeNamespaceToNonFullyQualifiedComponents = new Dictionary, List>(ReadOnlyMemoryOfCharComparer.Instance); + private List? _nonFullyQualifiedComponentsWithEmptyTypeNamespace; private string? _filePath; private RazorSourceDocument? _source; @@ -347,6 +349,7 @@ public void Initialize(string filePath, IReadOnlyList tagHe Debug.Assert(!IsInitialized); _filePath = filePath; + _nonFullyQualifiedComponentsWithEmptyTypeNamespace = ListPool.Default.Get(); foreach (var tagHelper in tagHelpers.AsEnumerable()) { @@ -363,25 +366,31 @@ public void Initialize(string filePath, IReadOnlyList tagHe continue; } - _nonFullyQualifiedComponents.Add(tagHelper); + var tagHelperTypeNamespace = tagHelper.GetTypeNamespace().AsMemory(); - if (currentNamespace is null) + if (tagHelperTypeNamespace.IsEmpty) { - continue; + _nonFullyQualifiedComponentsWithEmptyTypeNamespace.Add(tagHelper); } - - if (tagHelper.IsChildContentTagHelper) + else { - // If this is a child content tag helper, we want to add it if it's original type is in scope. - // E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in scope. - if (IsTypeInScope(tagHelper, currentNamespace)) + if (!_typeNamespaceToNonFullyQualifiedComponents.TryGetValue(tagHelperTypeNamespace, out var tagHelpersList)) { - AddMatch(tagHelper); + tagHelpersList = ListPool.Default.Get(); + _typeNamespaceToNonFullyQualifiedComponents.Add(tagHelperTypeNamespace, tagHelpersList); } + + tagHelpersList.Add(tagHelper); } - else if (IsTypeInScope(tagHelper, currentNamespace)) + + if (currentNamespace is null) { - // Also, if the type is already in scope of the document's namespace, using isn't necessary. + continue; + } + + if (IsTypeNamespaceInScope(tagHelperTypeNamespace.Span, currentNamespace)) + { + // If the type is already in scope of the document's namespace, using isn't necessary. AddMatch(tagHelper); } } @@ -391,7 +400,18 @@ public void Initialize(string filePath, IReadOnlyList tagHe public override void Reset() { - _nonFullyQualifiedComponents.Clear(); + if (_nonFullyQualifiedComponentsWithEmptyTypeNamespace != null) + { + ListPool.Default.Return(_nonFullyQualifiedComponentsWithEmptyTypeNamespace); + _nonFullyQualifiedComponentsWithEmptyTypeNamespace = null; + } + + foreach (var (_, tagHelpers) in _typeNamespaceToNonFullyQualifiedComponents) + { + ListPool.Default.Return(tagHelpers); + } + + _typeNamespaceToNonFullyQualifiedComponents.Clear(); _filePath = null; _source = null; @@ -407,6 +427,8 @@ public override void Visit(RazorSyntaxTree tree) public override void VisitRazorDirective(RazorDirectiveSyntax node) { + var componentsWithEmptyTypeNamespace = _nonFullyQualifiedComponentsWithEmptyTypeNamespace.AssumeNotNull(); + var descendantLiterals = node.DescendantNodes(); foreach (var child in descendantLiterals) { @@ -455,22 +477,30 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node) continue; } - foreach (var tagHelper in _nonFullyQualifiedComponents) + if (_typeNamespaceToNonFullyQualifiedComponents.Count == 0 && componentsWithEmptyTypeNamespace.Count == 0) + { + // There aren't any non qualified components to add + continue; + } + + // Add all tag helpers that have an empty type namespace + foreach (var tagHelper in componentsWithEmptyTypeNamespace) { Debug.Assert(!tagHelper.IsComponentFullyQualifiedNameMatch, "We've already processed these."); - if (tagHelper.IsChildContentTagHelper) - { - // If this is a child content tag helper, we want to add it if it's original type is in scope of the given namespace. - // E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in this namespace. - if (IsTypeInNamespace(tagHelper, @namespace)) - { - AddMatch(tagHelper); - } - } - else if (IsTypeInNamespace(tagHelper, @namespace)) + AddMatch(tagHelper); + } + + // Remove global:: prefix from namespace. + var normalizedNamespace = GetMemoryWithoutGlobalPrefix(@namespace); + + // Add all tag helpers with a matching namespace + if (_typeNamespaceToNonFullyQualifiedComponents.TryGetValue(normalizedNamespace, out var tagHelpers)) + { + foreach (var tagHelper in tagHelpers) { - // If the type is at the top-level or if the type's namespace matches the using's namespace, add it. + Debug.Assert(!tagHelper.IsComponentFullyQualifiedNameMatch, "We've already processed these."); + AddMatch(tagHelper); } } @@ -480,32 +510,14 @@ public override void VisitRazorDirective(RazorDirectiveSyntax node) } } - internal static bool IsTypeInNamespace(TagHelperDescriptor tagHelper, string @namespace) - { - var typeNamespace = tagHelper.GetTypeNamespace(); - - if (typeNamespace.IsNullOrEmpty()) - { - // Either the typeName is not the full type name or this type is at the top level. - return true; - } - - // Remove global:: prefix from namespace. - var normalizedNamespaceSpan = GetSpanWithoutGlobalPrefix(@namespace); - - return normalizedNamespaceSpan.Equals(typeNamespace.AsSpan(), StringComparison.Ordinal); - } - - // Check if the given type is already in scope given the namespace of the current document. + // Check if a type's namespace is already in scope given the namespace of the current document. // E.g, // If the namespace of the document is `MyComponents.Components.Shared`, // then the types `MyComponents.FooComponent`, `MyComponents.Components.BarComponent`, `MyComponents.Components.Shared.BazComponent` are all in scope. // Whereas `MyComponents.SomethingElse.OtherComponent` is not in scope. - internal static bool IsTypeInScope(TagHelperDescriptor tagHelper, string @namespace) + internal static bool IsTypeNamespaceInScope(ReadOnlySpan typeNamespace, string @namespace) { - var typeNamespace = tagHelper.GetTypeNamespace(); - - if (typeNamespace.IsNullOrEmpty()) + if (typeNamespace.IsEmpty) { // Either the typeName is not the full type name or this type is at the top level. return true; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ReadOnlyMemoryOfCharComparer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ReadOnlyMemoryOfCharComparer.cs new file mode 100644 index 00000000000..802b2fd23bb --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ReadOnlyMemoryOfCharComparer.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Language; + +internal sealed class ReadOnlyMemoryOfCharComparer : IEqualityComparer> +{ + public static readonly ReadOnlyMemoryOfCharComparer Instance = new ReadOnlyMemoryOfCharComparer(); + + private ReadOnlyMemoryOfCharComparer() + { + } + + public static bool Equals(ReadOnlySpan x, ReadOnlyMemory y) + => x.SequenceEqual(y.Span); + + public bool Equals(ReadOnlyMemory x, ReadOnlyMemory y) + => x.Span.SequenceEqual(y.Span); + + public int GetHashCode(ReadOnlyMemory memory) + { +#if NET + return string.GetHashCode(memory.Span); +#else + // We don't rely on ReadOnlyMemory.GetHashCode() because it includes + // the index and length, but we just want a hash based on the characters. + var hashCombiner = HashCodeCombiner.Start(); + + foreach (var ch in memory.Span) + { + hashCombiner.Add(ch); + } + + return hashCombiner.CombinedHash; +#endif + } +}