File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Extensions\CollectionExpressionUtilities.cs
Web Access
Project: src\src\RoslynAnalyzers\PublicApiAnalyzers\Core\Analyzers\Microsoft.CodeAnalysis.PublicApiAnalyzers.csproj (Microsoft.CodeAnalysis.PublicApiAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
 
namespace Microsoft.CodeAnalysis.Shared.Extensions;
 
internal static class CollectionExpressionUtilities
{
    public static bool IsWellKnownCollectionInterface(ITypeSymbol type)
        => IsWellKnownCollectionReadOnlyInterface(type) || IsWellKnownCollectionReadWriteInterface(type);
 
    public static bool IsWellKnownCollectionReadOnlyInterface(ITypeSymbol type)
    {
        return type.OriginalDefinition.SpecialType
            is SpecialType.System_Collections_Generic_IEnumerable_T
            or SpecialType.System_Collections_Generic_IReadOnlyCollection_T
            or SpecialType.System_Collections_Generic_IReadOnlyList_T;
    }
 
    public static bool IsWellKnownCollectionReadWriteInterface(ITypeSymbol type)
    {
        return type.OriginalDefinition.SpecialType
            is SpecialType.System_Collections_Generic_ICollection_T
            or SpecialType.System_Collections_Generic_IList_T;
    }
 
    public static bool IsConstructibleCollectionType(
        Compilation compilation,
        [NotNullWhen(true)] ITypeSymbol? type)
    {
        return IsConstructibleCollectionType(compilation, type, out _);
    }
 
    public static bool IsConstructibleCollectionType(
        Compilation compilation,
        [NotNullWhen(true)] ITypeSymbol? type,
        [NotNullWhen(true)] out ITypeSymbol? elementType)
    {
        if (type is null)
        {
            elementType = null;
            return false;
        }
 
        // Arrays are always a valid collection expression type.
        if (type is IArrayTypeSymbol arrayType)
        {
            elementType = arrayType.ElementType;
            return true;
        }
 
        // Has to be a real named type at this point.
        if (type is INamedTypeSymbol namedType)
        {
            // Span<T> and ReadOnlySpan<T> are always valid collection expression types.
            if (namedType.OriginalDefinition.Equals(compilation.SpanOfTType()) ||
                namedType.OriginalDefinition.Equals(compilation.ReadOnlySpanOfTType()))
            {
                elementType = namedType.TypeArguments.Single();
                return true;
            }
 
            var ienumerableOfTType = compilation.IEnumerableOfTType();
            var ienumerableType = compilation.IEnumerableType();
            var foundType =
                namedType.AllInterfaces.FirstOrDefault(i => i.OriginalDefinition.Equals(ienumerableOfTType)) ??
                namedType.AllInterfaces.FirstOrDefault(i => i.OriginalDefinition.Equals(ienumerableType));
            elementType = foundType?.TypeArguments.FirstOrDefault() ?? compilation.ObjectType;
 
            // If it has a [CollectionBuilder] attribute on it, it is a valid collection expression type.
            var collectionBuilderMethods = TryGetCollectionBuilderFactoryMethods(
                compilation, namedType);
            if (collectionBuilderMethods is [var builderMethod, ..])
                return true;
 
            if (IsWellKnownCollectionInterface(namedType))
                return true;
 
            // At this point, all that is left are collection-initializer types.  These need to derive from
            // System.Collections.IEnumerable, and have an invokable no-arg constructor.
 
            // Abstract type don't have invokable constructors at all.
            if (namedType.IsAbstract)
                return false;
 
            if (foundType != null)
            {
                // If they have an accessible `public C(int capacity)` constructor, the lang prefers calling that.
                var constructors = namedType.Constructors;
                var capacityConstructor = GetAccessibleInstanceConstructor(constructors, c => c.Parameters is [{ Name: "capacity", Type.SpecialType: SpecialType.System_Int32 }]);
                if (capacityConstructor != null)
                    return true;
 
                var noArgConstructor =
                    GetAccessibleInstanceConstructor(constructors, c => c.Parameters.IsEmpty) ??
                    GetAccessibleInstanceConstructor(constructors, c => c.Parameters.All(p => p.IsOptional || p.IsParams));
                if (noArgConstructor != null)
                {
                    // If we have a struct, and the constructor we find is implicitly declared, don't consider this
                    // a constructible type.  It's likely the user would just get the `default` instance of the
                    // collection (like with ImmutableArray<T>) which would then not actually work.  If the struct
                    // does have an explicit constructor though, that's a good sign it can actually be constructed
                    // safely with the no-arg `new S()` call.
                    if (!(namedType.TypeKind == TypeKind.Struct && noArgConstructor.IsImplicitlyDeclared))
                        return true;
                }
            }
        }
 
        // Anything else is not constructible.
        elementType = null;
        return false;
 
        IMethodSymbol? GetAccessibleInstanceConstructor(ImmutableArray<IMethodSymbol> constructors, Func<IMethodSymbol, bool> predicate)
        {
            var constructor = constructors.FirstOrDefault(c => !c.IsStatic && predicate(c));
            return constructor is not null && constructor.IsAccessibleWithin(compilation.Assembly) ? constructor : null;
        }
    }
 
    /// <summary>
    /// Gets the collection builder factory methods for the given collection expression type, if any.  Or <see
    /// langword="null"/> if the type does not have a valid <see cref="CollectionBuilderAttribute"/> that can be
    /// resolved.  The returned methods are guaranteed to match the language rules about what a factory method
    /// must look like.  That means, at a minimum:
    /// <list type="number">
    /// <item>they must be static</item>
    /// <item>their <see cref="IMethodSymbol.Arity"/> must match that of <paramref
    /// name="collectionExpressionType"/></item>
    /// <item>They must have a final parameter that is the <see cref="ReadOnlySpan{T}"/> containing the elements of the
    /// collection</item>
    /// </list>
    /// 
    /// Generic factory methods will be appropriately constructed to match the type arguments of <paramref
    /// name="collectionExpressionType"/>.
    /// </summary>
    public static ImmutableArray<IMethodSymbol>? TryGetCollectionBuilderFactoryMethods(
        Compilation compilation, INamedTypeSymbol collectionExpressionType)
    {
        var readonlySpanOfTType = compilation.ReadOnlySpanOfTType();
        var attribute = collectionExpressionType.GetAttributes().FirstOrDefault(
            static a => a.AttributeClass.IsCollectionBuilderAttribute());
 
        // https://github.com/dotnet/csharplang/blob/main/proposals/collection-expression-arguments.md#create-method-candidates
        // A [CollectionBuilder(...)] attribute specifies the builder type and method name of a method to be invoked to
        // construct an instance of the collection type.
        if (attribute is not { ConstructorArguments: [{ Value: INamedTypeSymbol builderType }, { Value: string builderMethodName }] })
            return null;
 
        // Find all the static methods in the builder type with the given name that have a ReadOnlySpan<T> as their last
        // parameter, matching the arity of the returned collection type.  Then construct the construction method if
        // generic. And filter to only those that return the collection type being created.
 
        var builderMethods = builderType
            .GetMembers(builderMethodName)
            .OfType<IMethodSymbol>()
            .Where(m =>
                m.IsStatic &&
                m.Arity == collectionExpressionType.Arity &&
                m.Parameters is [.., var lastParameter] &&
                Equals(lastParameter.Type.OriginalDefinition, readonlySpanOfTType))
            .Select(m => m.Arity == 0 ? m : m.Construct(ImmutableCollectionsMarshal.AsArray(collectionExpressionType.TypeArguments)!))
            .Where(m => compilation.ClassifyCommonConversion(m.ReturnType, collectionExpressionType).IsIdentityOrImplicitReference());
 
        return [.. builderMethods];
    }
}