File: Language\TagHelperCollection_Factories.cs
Web Access
Project: src\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;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
public abstract partial class TagHelperCollection
{
    /// <summary>
    ///  Creates a new <see cref="TagHelperCollection"/> from the specified span of tag helper descriptors.
    /// </summary>
    /// <param name="span">The span of tag helper descriptors to include in the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing the specified descriptors with automatic
    ///  deduplication based on checksums.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method automatically deduplicates descriptors based on their checksums and optimizes
    ///   the internal structure based on the number of elements. Empty spans return the singleton
    ///   <see cref="Empty"/> instance, single elements use optimized storage, and larger collections
    ///   use segmented storage with hash-based lookup tables when beneficial.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Create(ReadOnlySpan<TagHelperDescriptor> span)
    {
        return span switch
        {
            [] => Empty,
            [var singleItem] => new SingleSegmentCollection(singleItem),
            var items => BuildCollection(items),
        };
 
        static TagHelperCollection BuildCollection(ReadOnlySpan<TagHelperDescriptor> span)
        {
            using var builder = new FixedSizeBuilder(span.Length);
 
            builder.AddRange(span);
 
            return builder.ToCollection();
        }
    }
 
    /// <summary>
    ///  Creates a new <see cref="TagHelperCollection"/> from the specified immutable array of tag helper descriptors.
    /// </summary>
    /// <param name="array">The immutable array of tag helper descriptors to include in the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing the specified descriptors with automatic
    ///  deduplication based on checksums.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method leverages the memory-efficient nature of <see cref="ImmutableArray{T}"/> by using
    ///   its underlying memory directly when possible. The collection automatically deduplicates
    ///   descriptors based on their checksums.
    ///  </para>
    ///  <para>
    ///   Empty arrays return the singleton <see cref="Empty"/> instance, and single-element arrays
    ///   use optimized storage that shares the original array's memory.
    ///  </para>
    ///  <para>
    ///   This method is given higher overload resolution priority because it uses the underlying memory
    ///   of the <see cref="ImmutableArray{T}"/> and can be more efficient than
    ///   <see cref="Create(ReadOnlySpan{TagHelperDescriptor})"/>, which must create a new array to hold the elements.
    ///  </para>
    /// </remarks>
    [OverloadResolutionPriority(1)]
    public static TagHelperCollection Create(params ImmutableArray<TagHelperDescriptor> array)
    {
        // Note: We intentionally do *not* delegate to the Create(ReadOnlySpan<TagHelperDescriptor>)
        // overload, which must copy all of the elements from the span that's passed in.
        // We can use the underlying memory of the ImmutableArray directly.
        var segment = array.AsMemory();
 
        return segment.Span switch
        {
            [] => Empty,
            [TagHelperDescriptor] => new SingleSegmentCollection(segment),
            _ => BuildCollection(segment)
        };
 
        static TagHelperCollection BuildCollection(ReadOnlyMemory<TagHelperDescriptor> segment)
        {
            using var builder = new SegmentBuilder();
 
            builder.AddSegment(segment);
 
            return builder.ToCollection();
        }
    }
 
    /// <summary>
    ///  Creates a new <see cref="TagHelperCollection"/> from the specified enumerable of tag helper descriptors.
    /// </summary>
    /// <param name="source">The enumerable of tag helper descriptors to include in the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing the specified descriptors with automatic
    ///  deduplication based on checksums.
    /// </returns>
    /// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
    /// <remarks>
    ///  <para>
    ///   This method optimizes for enumerables that provide a count (such as arrays, lists, and other
    ///   collections) by pre-allocating the appropriate storage. For arbitrary enumerables without
    ///   a known count, it uses a growing buffer approach.
    ///  </para>
    ///  <para>
    ///   The collection automatically deduplicates descriptors based on their checksums and maintains
    ///   the order of first occurrence for duplicate items.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Create(IEnumerable<TagHelperDescriptor> source)
    {
        if (source.TryGetCount(out var count))
        {
            // Copy the IEnumerable to an immutable array and delegate to the
            // Create(ImmutableArray<TagHelperDescriptor>) method.
 
            // Note: We intentionally do *not* delegate to the Create(ReadOnlySpan<TagHelperDescriptor>)
            // overload, which must copy all of the elements from the span that's passed in.
            var array = new TagHelperDescriptor[count];
            source.CopyTo(array);
 
            return Create(ImmutableCollectionsMarshal.AsImmutableArray(array));
        }
 
        // Fallback for an arbitrary IEnumerable with no count.
 
        // Copy the IEnumerable to a MemoryBuilder and delegate to the other Create method.
        // Note that we can pass a span to the underlying pooled array below because
        // Create(ReadOnlySpan<TagHelperDescriptor>) copies the elements into a new array.
        using var builder = new MemoryBuilder<TagHelperDescriptor>(clearArray: true);
 
        foreach (var item in source)
        {
            builder.Append(item);
        }
 
        return Create(builder.AsMemory().Span);
    }
 
    /// <summary>
    ///  Merges multiple <see cref="TagHelperCollection"/> instances into a single collection.
    /// </summary>
    /// <param name="collections">The span of collections to merge.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing all unique tag helper descriptors from
    ///  the input collections, with duplicates removed based on checksums.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method efficiently merges collections by first filtering out empty collections and
    ///   those with duplicate checksums. The resulting collection maintains the order of elements
    ///   as they appear in the input collections, with the first occurrence of duplicates preserved.
    ///  </para>
    ///  <para>
    ///   If no collections are provided or all are empty, <see cref="Empty"/> is returned.
    ///   If only one non-empty unique collection is provided, that collection is returned directly.
    ///  </para>
    /// </remarks>
    [OverloadResolutionPriority(1)]
    public static TagHelperCollection Merge(params ReadOnlySpan<TagHelperCollection> collections)
    {
        switch (collections)
        {
            case []:
                return Empty;
 
            case [var singleCollection]:
                return singleCollection;
        }
 
        // First, collect the "mergeable" collections, i.e., those that are not empty and have unique checksums.
        using var _ = CollectMergeableCollections(collections, out var mergeableCollections);
 
        return mergeableCollections switch
        {
            [] => Empty,
            [var single] => single,
            _ => MergeMultipleCollections(mergeableCollections)
        };
 
        static PooledArray<TagHelperCollection> CollectMergeableCollections(
            ReadOnlySpan<TagHelperCollection> collections, out ReadOnlySpan<TagHelperCollection> result)
        {
            var pooledArray = ArrayPool<TagHelperCollection>.Shared.GetPooledArraySpan(
                minimumLength: collections.Length, clearOnReturn: true, out var destination);
 
            using var _ = ChecksumSetPool.Default.GetPooledObject(out var checksums);
            var index = 0;
 
            foreach (var collection in collections)
            {
                // Only add non-empty collections with unique checksums.
                if (!collection.IsEmpty && checksums.Add(collection.Checksum))
                {
                    destination[index++] = collection;
                }
            }
 
            result = destination[..index];
            return pooledArray;
        }
 
        static TagHelperCollection MergeMultipleCollections(ReadOnlySpan<TagHelperCollection> collections)
        {
            Debug.Assert(collections.Length >= 2);
 
            // Calculate number of segments to set the initial capacity of the SegmentBuilder.
            var segmentCount = 0;
 
            foreach (var collection in collections)
            {
                segmentCount += collection.Segments.Count;
            }
 
            using var builder = new SegmentBuilder(capacity: segmentCount);
 
            foreach (var collection in collections)
            {
                foreach (var segment in collection.Segments)
                {
                    builder.AddSegment(segment);
                }
            }
 
            return builder.ToCollection();
        }
    }
 
    /// <summary>
    ///  Merges multiple <see cref="TagHelperCollection"/> instances into a single collection.
    /// </summary>
    /// <param name="collections">The immutable array of collections to merge.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing all unique tag helper descriptors from
    ///  the input collections, with duplicates removed based on checksums.
    /// </returns>
    /// <remarks>
    ///  This method delegates to <see cref="Merge(ReadOnlySpan{TagHelperCollection})"/> for efficient
    ///  processing. See that method's documentation for detailed behavior information.
    /// </remarks>
    public static TagHelperCollection Merge(ImmutableArray<TagHelperCollection> collections)
        => Merge(collections.AsSpan());
 
    /// <summary>
    ///  Merges multiple <see cref="TagHelperCollection"/> instances into a single collection.
    /// </summary>
    /// <param name="source">The enumerable of collections to merge.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing all unique tag helper descriptors from
    ///  the input collections, with duplicates removed based on checksums.
    /// </returns>
    /// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
    /// <remarks>
    ///  <para>
    ///   This method optimizes for enumerables that provide a count by pre-allocating storage.
    ///   For arbitrary enumerables without a known count, it uses a growing buffer approach.
    ///  </para>
    ///  <para>
    ///   The method efficiently filters out empty collections and those with duplicate checksums,
    ///   maintaining the order of elements as they appear in the input collections.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Merge(IEnumerable<TagHelperCollection> source)
    {
        if (source.TryGetCount(out var count))
        {
            using var _ = ArrayPool<TagHelperCollection>.Shared.GetPooledArraySpan(
                minimumLength: count, clearOnReturn: true, out var collections);
 
            source.CopyTo(collections);
 
            return Merge(collections);
        }
 
        // Fallback for arbitrary IEnumerable
        using var builder = new MemoryBuilder<TagHelperCollection>(clearArray: true);
 
        foreach (var collection in source)
        {
            builder.Append(collection);
        }
 
        return Merge(builder.AsMemory().Span);
    }
 
    /// <summary>
    ///  Merges two <see cref="TagHelperCollection"/> instances into a single collection.
    /// </summary>
    /// <param name="first">The first collection to merge.</param>
    /// <param name="second">The second collection to merge.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> containing all unique tag helper descriptors from
    ///  both input collections, with duplicates removed based on checksums.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method provides optimized handling for the common case of merging exactly two collections.
    ///   It includes fast-path optimizations for empty collections and identical collections.
    ///  </para>
    ///  <para>
    ///   If either collection is empty, the other collection is returned directly.
    ///   If both collections are equal (same checksum), the first collection is returned.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Merge(TagHelperCollection first, TagHelperCollection second)
    {
        if (first.IsEmpty)
        {
            return second;
        }
 
        if (second.IsEmpty)
        {
            return first;
        }
 
        if (first.Equals(second))
        {
            return first;
        }
 
        using var _ = ArrayPool<TagHelperCollection>.Shared.GetPooledArraySpan(
            minimumLength: 2, clearOnReturn: true, out var collections);
 
        collections[0] = first;
        collections[1] = second;
 
        return Merge(collections);
    }
 
    public delegate void BuildAction(ref RefBuilder builder);
    public delegate void BuildAction<in TState>(ref RefBuilder builder, TState state);
 
    /// <summary>
    ///  Builds a new <see cref="TagHelperCollection"/> using a builder pattern with state.
    /// </summary>
    /// <typeparam name="TState">The type of the state object passed to the build action.</typeparam>
    /// <param name="state">The state object to pass to the build action.</param>
    /// <param name="action">The action that defines how to build the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> built according to the specified action.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method provides a flexible way to build collections using a callback pattern.
    ///   The builder automatically handles deduplication and optimizes the internal structure
    ///   based on the final number of elements.
    ///  </para>
    ///  <para>
    ///   The state parameter allows passing data to the build action without creating closures,
    ///   which can improve performance by avoiding allocations.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Build<TState>(TState state, BuildAction<TState> action)
    {
        var builder = new RefBuilder();
 
        return BuildCore(ref builder, state, action);
    }
 
    /// <summary>
    ///  Builds a new <see cref="TagHelperCollection"/> using a builder pattern with state and initial capacity.
    /// </summary>
    /// <typeparam name="TState">The type of the state object passed to the build action.</typeparam>
    /// <param name="state">The state object to pass to the build action.</param>
    /// <param name="initialCapacity">The initial capacity hint for the builder.</param>
    /// <param name="action">The action that defines how to build the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> built according to the specified action.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This overload allows specifying an initial capacity hint to optimize memory allocation
    ///   when the approximate number of elements is known in advance.
    ///  </para>
    ///  <para>
    ///   The state parameter allows passing data to the build action without creating closures,
    ///   improving performance by avoiding allocations.
    ///  </para>
    /// </remarks>
    public static TagHelperCollection Build<TState>(TState state, int initialCapacity, BuildAction<TState> action)
    {
        var builder = new RefBuilder(initialCapacity);
 
        return BuildCore(ref builder, state, action);
    }
 
    private static TagHelperCollection BuildCore<TState>(ref RefBuilder builder, TState state, BuildAction<TState> action)
    {
        try
        {
            action(ref builder, state);
            return builder.ToCollection();
        }
        finally
        {
            builder.Dispose();
        }
    }
 
    /// <summary>
    ///  Builds a new <see cref="TagHelperCollection"/> using a builder pattern.
    /// </summary>
    /// <param name="action">The action that defines how to build the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> built according to the specified action.
    /// </returns>
    /// <remarks>
    ///  This method provides a flexible way to build collections using a callback pattern.
    ///  The builder automatically handles deduplication and optimizes the internal structure
    ///  based on the final number of elements.
    /// </remarks>
    public static TagHelperCollection Build(BuildAction action)
    {
        var builder = new RefBuilder();
 
        return BuildCore(ref builder, action);
    }
 
    /// <summary>
    ///  Builds a new <see cref="TagHelperCollection"/> using a builder pattern with initial capacity.
    /// </summary>
    /// <param name="initialCapacity">The initial capacity hint for the builder.</param>
    /// <param name="action">The action that defines how to build the collection.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> built according to the specified action.
    /// </returns>
    /// <remarks>
    ///  This overload allows specifying an initial capacity hint to optimize memory allocation
    ///  when the approximate number of elements is known in advance.
    /// </remarks>
    public static TagHelperCollection Build(int initialCapacity, BuildAction action)
    {
        var builder = new RefBuilder(initialCapacity);
 
        return BuildCore(ref builder, action);
    }
 
    private static TagHelperCollection BuildCore(ref RefBuilder builder, BuildAction action)
    {
        try
        {
            action(ref builder);
            return builder.ToCollection();
        }
        finally
        {
            builder.Dispose();
        }
    }
}