// 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 Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Shared.Extensions; 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; } } 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 methods in the builder type with the given name that have a ReadOnlySpan<T> as either their // first or last parameter. var builderMethods = builderType // The method must have the name specified in the [CollectionBuilder(...)] attribute. .GetMembers(builderMethodName) .OfType<IMethodSymbol>() .Where(m => // The method must be static. m.IsStatic && // The arity of the method must match the arity of the collection type. m.Arity == collectionExpressionType.Arity && m.Parameters.Length >= 1 && // The method must have a first (or last) parameter of type System.ReadOnlySpan<E>, passed by value. (Equals(m.Parameters[0].Type.OriginalDefinition, readonlySpanOfTType) || Equals(m.Parameters.Last().Type.OriginalDefinition, readonlySpanOfTType))) .ToImmutableArray(); // Instance the construction method if generic. And filter to only those that return the collection type // being created. var constructedBuilderMethods = builderMethods .Select(m => m.Construct([.. collectionExpressionType.TypeArguments])) .Where(m => { // There is an identity conversion, implicit reference conversion, or boxing conversion from the method return type to the collection type. var conversion = compilation.ClassifyCommonConversion(m.ReturnType, collectionExpressionType); return conversion.IsIdentity || (conversion.IsImplicit && conversion.IsReference); }) .ToImmutableArray(); return constructedBuilderMethods; } } |