sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.CardinalityParser.cs (141 lines of code) (raw):

// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information.; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis; using System; using System.Linq; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { public partial class FunctionMetadataProviderGenerator { internal sealed class CardinalityParser { private readonly KnownTypes _knownTypes; private readonly KnownFunctionMetadataTypes _knownFunctionMetadataTypes; private DataTypeParser _dataTypeParser; /// <summary> /// Provides support for validating and parsing cardinality scenarios when generating function metadata. /// </summary> /// <param name="knownTypes">A collection of known types, used for symbol comparison.</param> /// <param name="knownFunctionMetadataTypes">A collection of known types from Azure Functions packages, used for symbol comparison.</param> /// <param name="dataTypeParser"><see cref="DataTypeParser"/> used to aid in parsing data types used in function metadata generation.</param> public CardinalityParser(KnownTypes knownTypes, KnownFunctionMetadataTypes knownFunctionMetadataTypes, DataTypeParser dataTypeParser) { _knownTypes = knownTypes; _knownFunctionMetadataTypes = knownFunctionMetadataTypes; _dataTypeParser = dataTypeParser; } /// <summary> /// Checks if an attribute has cardinality. /// </summary> /// <param name="attribute">The attribute to check.</param> /// <returns>Returns true if cardinality is supported, else returns false.</returns> public bool IsCardinalitySupported(AttributeData attribute) { return TryGetIsBatchedProp(attribute, out var isBatchedProp); } /// <summary> /// Checks if an attribute contains a property called "IsBatched", which indicates that cardinality is supported. /// </summary> /// <param name="attribute">The attribute to check.</param> /// <param name="isBatchedProp">The attribute's IsBatched property represented as an <see cref="ISymbol"/> or <see cref="null""/> if no IsBatched property is found.</param> /// <returns></returns> private bool TryGetIsBatchedProp(AttributeData attribute, out ISymbol? isBatchedProp) { var attrClass = attribute.AttributeClass; isBatchedProp = attrClass! .GetMembers() .SingleOrDefault(m => string.Equals(m.Name, Constants.FunctionMetadataBindingProps.IsBatchedKey, StringComparison.OrdinalIgnoreCase)); return isBatchedProp != null; } /// <summary> /// Verifies that a binding that has Cardinality (isBatched property) is valid. If isBatched is set to true, the parameter with the /// attribute must be an iterable collection. /// </summary> /// <param name="parameterSymbol">The parameter associated with a binding attribute that supports cardinality represented as an <see cref="IParameterSymbol"/>.</param> /// <param name="attribute">The binding attribute that supports cardinality.</param> /// <param name="dataType">The <see cref="DataType"/> that best represents the parameter.</param> /// <returns>Returns true if the parameter is compatible with the cardinality defined by the attribute, else returns false.</returns> public bool IsCardinalityValid(IParameterSymbol parameterSymbol, AttributeData attribute, out DataType dataType) { dataType = DataType.Undefined; var cardinalityIsNamedArg = false; // check if IsBatched is defined in the NamedArguments foreach (var arg in attribute.NamedArguments) { if (String.Equals(arg.Key, Constants.FunctionMetadataBindingProps.IsBatchedKey) && arg.Value.Value != null) { cardinalityIsNamedArg = true; var isBatched = (bool)arg.Value.Value; // isBatched takes in booleans so we can just type cast it here to use if (!isBatched) { dataType = _dataTypeParser.GetDataType(parameterSymbol.Type); return true; } } } // When "IsBatched" is not a named arg, we have to check the default value if (!cardinalityIsNamedArg) { if (!TryGetIsBatchedProp(attribute, out var isBatchedProp)) { dataType = DataType.Undefined; return false; } var defaultValAttr = isBatchedProp! .GetAttributes() .SingleOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, _knownFunctionMetadataTypes.DefaultValue)); var defaultVal = defaultValAttr!.ConstructorArguments.SingleOrDefault().Value!.ToString(); // there is only one constructor arg for the DefaultValue attribute (the default value) if (!bool.TryParse(defaultVal, out bool b) || !b) { dataType = _dataTypeParser.GetDataType(parameterSymbol.Type); return true; } } // we check if the param is an array type // we exclude byte arrays (byte[]) b/c we handle that as Cardinality.One (we handle this similar to how a char[] is basically a string) if (parameterSymbol.Type is IArrayTypeSymbol && !SymbolEqualityComparer.Default.Equals(parameterSymbol.Type, _knownTypes.ByteArray)) { dataType = _dataTypeParser.GetDataType(parameterSymbol.Type); return true; } // Check if mapping type - mapping enumerables are not valid types for Cardinality.Many if (parameterSymbol.Type.IsOrImplementsOrDerivesFrom(_knownTypes.IEnumerableOfKeyValuePair) || parameterSymbol.Type.IsOrImplementsOrDerivesFrom(_knownTypes.LookupGeneric) || parameterSymbol.Type.IsOrImplementsOrDerivesFrom(_knownTypes.DictionaryGeneric)) { return false; } var isGenericEnumerable = parameterSymbol.Type.IsOrImplementsOrDerivesFrom(_knownTypes.IEnumerableGeneric); var isEnumerable = parameterSymbol.Type.IsOrImplementsOrDerivesFrom(_knownTypes.IEnumerable); if (!_dataTypeParser.IsStringType(parameterSymbol.Type) && (isGenericEnumerable || isEnumerable)) { if (_dataTypeParser.IsStringType(parameterSymbol.Type)) { dataType = DataType.String; } else if (_dataTypeParser.IsBinaryType(parameterSymbol.Type)) { dataType = DataType.Binary; } else if (isGenericEnumerable) { dataType = ResolveIEnumerableOfT(parameterSymbol, out bool hasError); if (hasError) { return false; } return true; } return true; } // trigger input type doesn't match any of the valid cases so return false return false; } /// <summary> /// Find the underlying data type of an IEnumerableOfT (String, Binary, Undefined) /// ex. IEnumerable<byte[]> would return DataType.Binary /// </summary> private DataType ResolveIEnumerableOfT(IParameterSymbol parameterSymbol, out bool hasError) { var result = DataType.Undefined; hasError = false; var currSymbol = parameterSymbol.Type; INamedTypeSymbol? finalSymbol = null; while (currSymbol != null) { INamedTypeSymbol? genericInterfaceSymbol = null; if (currSymbol.IsOrDerivedFrom(_knownTypes.IEnumerableGeneric) && currSymbol is INamedTypeSymbol currNamedSymbol) { finalSymbol = currNamedSymbol; break; } genericInterfaceSymbol = currSymbol.Interfaces.FirstOrDefault(i => i.IsOrDerivedFrom(_knownTypes.IEnumerableGeneric)); if (genericInterfaceSymbol != null) { finalSymbol = genericInterfaceSymbol; break; } currSymbol = currSymbol.BaseType; } if (finalSymbol is null) { hasError = true; return result; } var argument = finalSymbol.TypeArguments.FirstOrDefault(); // we've already checked and discarded mapping types by this point - should be a single argument if (argument is null) { hasError = true; return result; } return _dataTypeParser.GetDataType(argument); } } } }