| File: Language\TagHelperDiscoverer.cs | Web Access |
| Project: src\roslyn\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Collections.Immutable; using System.Diagnostics; using System.Threading; using Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Language; internal sealed class TagHelperDiscoverer(ImmutableArray<TagHelperProducer> producers, bool includeDocumentation, bool excludeHidden) { private readonly int _cacheKey = GetCacheKey(producers, includeDocumentation, excludeHidden); /// <summary> /// Generates a unique integer cache key based on the specified set of TagHelper producers and option flags. /// </summary> /// <remarks> /// The generated cache key is intended for efficient lookup scenarios where the combination of /// producers and options must be uniquely identified. The method supports up to 30 distinct TagHelperProducer /// kinds. /// </remarks> private static int GetCacheKey(ImmutableArray<TagHelperProducer> producers, bool includeDocumentation, bool excludeHidden) { Debug.Assert(producers.Length <= 30, "Too many TagHelperProducer kinds to fit in a cache key."); var key = 0; if (includeDocumentation) { key |= 1 << 0; } if (excludeHidden) { key |= 1 << 1; } foreach (var producer in producers) { key |= 1 << ((int)producer.Kind + 2); } return key; } public TagHelperCollection GetTagHelpers(IAssemblySymbol assembly, CancellationToken cancellationToken = default) { if (producers.IsDefaultOrEmpty) { return TagHelperCollection.Empty; } // Optimization: Check to see if this assembly might contain tag helpers before doing any work. var assemblySymbolData = SymbolCache.GetAssemblySymbolData(assembly); if (!assemblySymbolData.MightContainTagHelpers) { return TagHelperCollection.Empty; } // Check to see if we already have tag helpers cached for this assembly // and use the cached versions if we do. Roslyn shares PE assembly symbols // across compilations, so this ensures that we don't produce new tag helpers // for the same assemblies over and over again. if (assemblySymbolData.TryGetTagHelpers(_cacheKey, out var tagHelpers)) { return tagHelpers; } // We don't have tag helpers cached for this assembly, so we have to discover them. var builder = new TagHelperCollection.RefBuilder(); try { // First, let producers add any static tag helpers they might have. // Also, capture any producers that need to analyze types. using var _ = ArrayPool<TagHelperProducer>.Shared.GetPooledArraySpan( minimumLength: producers.Length, clearOnReturn: true, out var typeProducers); var index = 0; var includeNestedTypes = false; foreach (var producer in producers) { if (producer.SupportsStaticTagHelpers) { producer.AddStaticTagHelpers(assembly, ref builder); } if (producer.SupportsTypes) { typeProducers[index++] = producer; includeNestedTypes |= producer.SupportsNestedTypes; } } typeProducers = typeProducers[..index]; cancellationToken.ThrowIfCancellationRequested(); // Did another discovery request for the same assembly finish and // cache the result while we were producing static tag helpers? if (assemblySymbolData.TryGetTagHelpers(_cacheKey, out tagHelpers)) { return tagHelpers; } // Now, walk all types in the assembly and let producers add tag helpers. using var stack = new MemoryBuilder<INamespaceOrTypeSymbol>(initialCapacity: 32, clearArray: true); stack.Push(assembly.GlobalNamespace); while (!stack.IsEmpty) { cancellationToken.ThrowIfCancellationRequested(); var namespaceOrType = stack.Pop(); switch (namespaceOrType.Kind) { case SymbolKind.Namespace: var members = namespaceOrType.GetMembers(); // Note: Push members onto the stack in reverse to ensure // that they're popped off and processed in the correct order. for (var i = members.Length - 1; i >= 0; i--) { // Namespaces members are only ever namespaces or types. stack.Push((INamespaceOrTypeSymbol)members[i]); } break; case SymbolKind.NamedType: var typeSymbol = (INamedTypeSymbol)namespaceOrType; foreach (var producer in typeProducers) { if (producer.IsCandidateType(typeSymbol)) { producer.AddTagHelpersForType(typeSymbol, ref builder, cancellationToken); } } if (includeNestedTypes && namespaceOrType.DeclaredAccessibility == Accessibility.Public) { var typeMembers = namespaceOrType.GetTypeMembers(); // Note: Push members onto the stack in reverse to ensure // that they're popped off and processed in the correct order. for (var i = typeMembers.Length - 1; i >= 0; i--) { stack.Push(typeMembers[i]); } } break; } } return assemblySymbolData.AddTagHelpers(_cacheKey, builder.ToCollection()); } finally { builder.Dispose(); } } }