File: Language\TagHelperCollection.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.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Utilities;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
/// <summary>
///  Represents an immutable, ordered collection of <see cref="TagHelperDescriptor"/> instances.
/// </summary>
/// <remarks>
///  <para>
///   <see cref="TagHelperCollection"/> provides high-performance access to tag helper descriptors with
///   automatic deduplication based on <see cref="TagHelperDescriptor.Checksum"/>. The collection is
///   optimized for common operations including indexing, searching, and enumeration.
///  </para>
///  <para>
///   Collections can be created using collection expressions, factory methods, or by merging existing
///   collections. Large collections (>8 items) automatically use hash-based lookup tables for O(1)
///   search performance.
///  </para>
///  <para>
///   This type supports collection expressions:
///   <code>
///    TagHelperCollection collection = [tagHelper1, tagHelper2, tagHelper3];
///   </code>
///  </para>
/// </remarks>
[CollectionBuilder(typeof(TagHelperCollection), methodName: "Create")]
public abstract partial class TagHelperCollection : IEquatable<TagHelperCollection>, IReadOnlyList<TagHelperDescriptor>
{
    /// <summary>
    ///  Gets an empty <see cref="TagHelperCollection"/>.
    /// </summary>
    /// <returns>
    ///  A singleton empty collection instance.
    /// </returns>
    public static TagHelperCollection Empty => EmptyCollection.Instance;
 
    /// <summary>
    ///  Gets a value indicating whether the collection is empty.
    /// </summary>
    /// <returns>
    ///  <see langword="true"/> if the collection contains no elements; otherwise, <see langword="false"/>.
    /// </returns>
    public bool IsEmpty => Count == 0;
 
    private SegmentAccessor Segments => new(this);
 
    /// <summary>
    ///  Gets the number of memory segments that make up this collection.
    /// </summary>
    /// <returns>
    ///  The number of contiguous memory segments.
    /// </returns>
    protected abstract int SegmentCount { get; }
 
    /// <summary>
    ///  Gets the memory segment at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index of the segment to retrieve.</param>
    /// <returns>
    ///  A <see cref="ReadOnlyMemory{T}"/> containing the tag helper descriptors in the segment.
    /// </returns>
    protected abstract ReadOnlyMemory<TagHelperDescriptor> GetSegment(int index);
 
    /// <summary>
    ///  Gets the number of tag helper descriptors in the collection.
    /// </summary>
    /// <returns>
    ///  The total number of tag helper descriptors.
    /// </returns>
    public abstract int Count { get; }
 
    /// <summary>
    ///  Gets the <see cref="TagHelperDescriptor"/> at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index of the tag helper descriptor to retrieve.</param>
    /// <returns>
    ///  The tag helper descriptor at the specified index.
    /// </returns>
    /// <exception cref="ArgumentOutOfRangeException">
    ///  <paramref name="index"/> is less than 0 or greater than or equal to <see cref="Count"/>.
    /// </exception>
    public abstract TagHelperDescriptor this[int index] { get; }
 
    /// <summary>
    ///  Gets the computed checksum for this collection based on the checksums of all contained descriptors.
    /// </summary>
    /// <returns>
    ///  A checksum representing the content of this collection.
    /// </returns>
    internal abstract Checksum Checksum { get; }
 
    /// <summary>
    ///  Determines whether the specified object is equal to the current <see cref="TagHelperCollection"/>.
    /// </summary>
    /// <param name="obj">The object to compare with the current collection.</param>
    /// <returns>
    ///  <see langword="true"/> if the specified object is a <see cref="TagHelperCollection"/> that contains
    ///  the same tag helper descriptors in the same order; otherwise, <see langword="false"/>.
    /// </returns>
    public override bool Equals(object? obj)
        => obj is TagHelperCollection other && Equals(other);
 
    /// <summary>
    ///  Determines whether the specified <see cref="TagHelperCollection"/> is equal to the current collection.
    /// </summary>
    /// <param name="other">The collection to compare with the current collection.</param>
    /// <returns>
    ///  <see langword="true"/> if the specified collection contains the same tag helper descriptors
    ///  in the same order; otherwise, <see langword="false"/>.
    /// </returns>
    /// <remarks>
    ///  Equality is determined by comparing the computed checksums of both collections for performance.
    ///  Collections with the same content will have identical checksums regardless of their internal structure.
    /// </remarks>
    public bool Equals(TagHelperCollection? other)
    {
        if (other is null)
        {
            return false;
        }
 
        if (ReferenceEquals(this, other))
        {
            return true;
        }
 
        if (Count != other.Count)
        {
            return false;
        }
 
        return Checksum.Equals(other.Checksum);
    }
 
    /// <summary>
    ///  Returns a hash code for the current <see cref="TagHelperCollection"/>.
    /// </summary>
    /// <returns>
    ///  A hash code for the current collection.
    /// </returns>
    /// <remarks>
    ///  The hash code is derived from the collection's checksum, ensuring that collections
    ///  with identical content have the same hash code.
    /// </remarks>
    public override int GetHashCode()
        => Checksum.GetHashCode();
 
    /// <summary>
    ///  Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>
    ///  An <see cref="Enumerator"/> for the collection.
    /// </returns>
    public Enumerator GetEnumerator()
        => new(this);
 
    /// <summary>
    ///  Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>
    ///  An <see cref="IEnumerator{T}"/> for the collection.
    /// </returns>
    IEnumerator<TagHelperDescriptor> IEnumerable<TagHelperDescriptor>.GetEnumerator()
        => new EnumeratorImpl(this);
 
    /// <summary>
    ///  Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>
    ///  An <see cref="IEnumerator"/> for the collection.
    /// </returns>
    IEnumerator IEnumerable.GetEnumerator()
        => new EnumeratorImpl(this);
 
    /// <summary>
    ///  Searches for the specified <see cref="TagHelperDescriptor"/> and returns the zero-based index
    ///  of the first occurrence within the collection.
    /// </summary>
    /// <param name="item">The tag helper descriptor to locate in the collection.</param>
    /// <returns>
    ///  The zero-based index of the first occurrence of <paramref name="item"/> within the collection,
    ///  if found; otherwise, -1.
    /// </returns>
    /// <remarks>
    ///  The search is performed using the descriptor's checksum for efficient comparison.
    ///  For collections with more than 8 items, this operation uses a hash-based lookup table
    ///  for O(1) performance. Smaller collections use linear search.
    /// </remarks>
    public abstract int IndexOf(TagHelperDescriptor item);
 
    /// <summary>
    ///  Determines whether the collection contains a specific <see cref="TagHelperDescriptor"/>.
    /// </summary>
    /// <param name="item">The tag helper descriptor to locate in the collection.</param>
    /// <returns>
    ///  <see langword="true"/> if <paramref name="item"/> is found in the collection; otherwise, <see langword="false"/>.
    /// </returns>
    /// <remarks>
    ///  This method uses <see cref="IndexOf(TagHelperDescriptor)"/> internally and benefits from
    ///  the same performance optimizations.
    /// </remarks>
    public bool Contains(TagHelperDescriptor item)
        => IndexOf(item) >= 0;
 
    /// <summary>
    ///  Copies all the tag helper descriptors in the collection to a compatible one-dimensional span,
    ///  starting at the beginning of the target span.
    /// </summary>
    /// <param name="destination">
    ///  The one-dimensional <see cref="Span{T}"/> that is the destination of the descriptors
    ///  copied from the collection.
    /// </param>
    /// <exception cref="ArgumentException">
    ///  The <paramref name="destination"/> span is too short to contain all the descriptors in the collection.
    /// </exception>
    public abstract void CopyTo(Span<TagHelperDescriptor> destination);
 
    /// <summary>
    ///  Filters the collection based on a predicate and returns a new <see cref="TagHelperCollection"/>
    ///  containing only the tag helper descriptors that satisfy the condition.
    /// </summary>
    /// <typeparam name="TState">The type of the state object passed to the predicate.</typeparam>
    /// <param name="state">The state object to pass to the predicate function.</param>
    /// <param name="predicate">A function to test each tag helper descriptor for a condition.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> that contains the tag helper descriptors from the
    ///  current collection that satisfy the condition specified by <paramref name="predicate"/>.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method preserves the order of elements and automatically handles deduplication.
    ///   The resulting collection maintains the same performance characteristics as the original.
    ///  </para>
    ///  <para>
    ///   If no elements match the predicate, <see cref="Empty"/> is returned.
    ///   If all elements match the predicate, this collection is returned..
    ///  </para>
    ///  <para>
    ///   This overload allows passing state to the predicate without creating closures, which can
    ///   improve performance by avoiding allocations.
    ///  </para>
    /// </remarks>
    public TagHelperCollection Where<TState>(TState state, Func<TagHelperDescriptor, TState, bool> predicate)
    {
        if (IsEmpty)
        {
            return [];
        }
 
        // Note: We don't have to worry about checking for duplicates since this
        // collection is already de-duped.
        using var segments = new PooledArrayBuilder<ReadOnlyMemory<TagHelperDescriptor>>();
 
        foreach (var segment in Segments)
        {
            var span = segment.Span;
            var segmentStart = 0;
 
            for (var i = 0; i < span.Length; i++)
            {
                if (predicate(span[i], state))
                {
                    // Item matches predicate, continue building current segment
                    continue;
                }
 
                // Item doesn't match predicate - close current segment if it has items
                if (i > segmentStart)
                {
                    segments.Add(segment[segmentStart..i]);
                }
 
                // Start new segment after this filtered item
                segmentStart = i + 1;
            }
 
            // Close final segment if it has items
            if (segmentStart < span.Length)
            {
                segments.Add(segment[segmentStart..]);
            }
        }
 
        return segments.Count switch
        {
            0 => Empty,
 
            // If there's only one segment and its length is different from the original count,
            // we need to create a new collection. Otherwise, the predicate matched all items and
            // we can just return the current collection.
            1 => segments[0].Length != Count
                ? new SingleSegmentCollection(segments[0])
                : this,
 
            _ => new MultiSegmentCollection(segments.ToImmutableAndClear())
        };
    }
 
    /// <summary>
    ///  Filters the collection based on a predicate and returns a new <see cref="TagHelperCollection"/>
    ///  containing only the tag helper descriptors that satisfy the condition.
    /// </summary>
    /// <param name="predicate">A function to test each tag helper descriptor for a condition.</param>
    /// <returns>
    ///  A new <see cref="TagHelperCollection"/> that contains the tag helper descriptors from the
    ///  current collection that satisfy the condition specified by <paramref name="predicate"/>.
    /// </returns>
    /// <remarks>
    ///  <para>
    ///   This method preserves the order of elements and automatically handles deduplication.
    ///   The resulting collection maintains the same performance characteristics as the original.
    ///  </para>
    ///  <para>
    ///   If no elements match the predicate, <see cref="Empty"/> is returned.
    ///   If all elements match the predicate, this collection is returned..
    ///  </para>
    /// </remarks>
    public TagHelperCollection Where(Predicate<TagHelperDescriptor> predicate)
    {
        if (IsEmpty)
        {
            return [];
        }
 
        // Note: We don't have to worry about checking for duplicates since this
        // collection is already de-duped.
        using var segments = new PooledArrayBuilder<ReadOnlyMemory<TagHelperDescriptor>>();
 
        foreach (var segment in Segments)
        {
            var span = segment.Span;
            var segmentStart = 0;
 
            for (var i = 0; i < span.Length; i++)
            {
                if (predicate(span[i]))
                {
                    // Item matches predicate, continue building current segment
                    continue;
                }
 
                // Item doesn't match predicate - close current segment if it has items
                if (i > segmentStart)
                {
                    segments.Add(segment[segmentStart..i]);
                }
 
                // Start new segment after this filtered item
                segmentStart = i + 1;
            }
 
            // Close final segment if it has items
            if (segmentStart < span.Length)
            {
                segments.Add(segment[segmentStart..]);
            }
        }
 
        return segments.Count switch
        {
            0 => Empty,
 
            // If there's only one segment and its length is different from the original count,
            // we need to create a new collection. Otherwise, the predicate matched all items and
            // we can just return the current collection.
            1 => segments[0].Length != Count
                ? new SingleSegmentCollection(segments[0])
                : this,
 
            _ => new MultiSegmentCollection(segments.ToImmutableAndClear())
        };
    }
}