sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs (612 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 System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { public partial class FunctionMetadataProviderGenerator { internal sealed class Parser { private readonly GeneratorExecutionContext _context; private readonly ImmutableArray<string> _functionsStringNamesToRemove; private readonly KnownTypes _knownTypes; private readonly KnownFunctionMetadataTypes _knownFunctionMetadataTypes; private DataTypeParser _dataTypeParser; private CardinalityParser _cardinalityParser; public Parser(GeneratorExecutionContext context) { _context = context; _functionsStringNamesToRemove = ImmutableArray.Create("Attribute", "Input", "Output"); _knownTypes = new KnownTypes(context.Compilation); _knownFunctionMetadataTypes = new KnownFunctionMetadataTypes(context.Compilation); _dataTypeParser = new DataTypeParser(_knownTypes); _cardinalityParser = new CardinalityParser(_knownTypes, _knownFunctionMetadataTypes, _dataTypeParser); } private Compilation Compilation => _context.Compilation; private CancellationToken CancellationToken => _context.CancellationToken; /// <summary> /// Takes in candidate methods from the user compilation and parses them to return function metadata info as GeneratorFunctionMetadata. /// </summary> /// <param name="methods">List of candidate methods from the syntax receiver.</param> /// <param name="parsingContext">An instance of <see cref="FunctionsMetadataParsingContext"/>. Optional.</param> public IReadOnlyList<GeneratorFunctionMetadata> GetFunctionMetadataInfo(List<IMethodSymbol> methods, FunctionsMetadataParsingContext? parsingContext = null) { var result = ImmutableArray.CreateBuilder<GeneratorFunctionMetadata>(); // Loop through the candidate methods (methods with any attribute associated with them) which are public. foreach (IMethodSymbol method in methods.Where(m => m.DeclaredAccessibility == Accessibility.Public)) { CancellationToken.ThrowIfCancellationRequested(); if (!FunctionsUtil.TryGetFunctionName(method, Compilation, out var funcName)) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, method.Name)); // would only reach here if the function attribute or method was not loaded, resulting in failure to retrieve name } var assemblyName = method.ContainingAssembly.Name; var scriptFile = $"{assemblyName}{parsingContext?.ScriptFileExtension ?? ".dll"}"; var newFunction = new GeneratorFunctionMetadata { Name = funcName, EntryPoint = FunctionsUtil.GetFullyQualifiedMethodName(method), Language = Constants.Languages.DotnetIsolated, ScriptFile = scriptFile }; if (!TryGetBindings(method, out IList<IDictionary<string, object>>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? retryOptions)) { continue; } if (hasHttpTrigger) { newFunction.IsHttpTrigger = true; } if (retryOptions is not null) { newFunction.Retry = retryOptions; } newFunction.RawBindings = bindings!; // won't be null b/c TryGetBindings would've failed and this line wouldn't be reached result.Add(newFunction); } return result; } private bool TryGetBindings(IMethodSymbol method, out IList<IDictionary<string, object>>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? validatedRetryOptions) { hasHttpTrigger = false; validatedRetryOptions = null; if (!TryGetMethodOutputBinding(method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? methodOutputBindings) || !TryGetParameterInputAndTriggerBindings(method, out bool supportsRetryOptions, out hasHttpTrigger, out IList<IDictionary<string, object>>? parameterInputAndTriggerBindings) || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasMethodOutputBinding, out IList<IDictionary<string, object>>? returnTypeBindings)) { bindings = null; return false; } var listSize = methodOutputBindings!.Count + parameterInputAndTriggerBindings!.Count + returnTypeBindings!.Count; var result = new List<IDictionary<string, object>>(listSize); result.AddRange(methodOutputBindings); result.AddRange(parameterInputAndTriggerBindings); result.AddRange(returnTypeBindings); bindings = result; if (retryOptions is not null) { if (supportsRetryOptions) { validatedRetryOptions = retryOptions; } else if (!supportsRetryOptions) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidRetryOptions, Location.None)); return false; } } return true; } /// <summary> /// Checks for and returns any OutputBinding attributes associated with the method. /// </summary> private bool TryGetMethodOutputBinding(IMethodSymbol method, out bool hasMethodOutputBinding, out GeneratorRetryOptions? retryOptions, out IList<IDictionary<string, object>>? bindingsList) { var attributes = method!.GetAttributes(); // methodSymbol is not null here because it's checked in IsValidAzureFunction which is called before bindings are collected/created AttributeData? outputBindingAttribute = null; hasMethodOutputBinding = false; retryOptions = null; foreach (var attribute in attributes) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType, _knownFunctionMetadataTypes.RetryAttribute)) { if (TryGetRetryOptionsFromAttribute(attribute, Location.None, out GeneratorRetryOptions? retryOptionsFromAttr)) { retryOptions = retryOptionsFromAttr; } } if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute)) { // There can only be one method output binding associated with a function. If there is more than one, we return a diagnostic error here. if (hasMethodOutputBinding) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Method", method.Name })); bindingsList = null; return false; } outputBindingAttribute = attribute; hasMethodOutputBinding = true; } } if (outputBindingAttribute != null) { if (!TryCreateBindingDictionary(outputBindingAttribute, Constants.FunctionMetadataBindingProps.ReturnBindingName, Location.None, out IDictionary<string, object>? bindings)) { bindingsList = null; return false; } bindingsList = new List<IDictionary<string, object>>(capacity: 1) { bindings! }; return true; } // we didn't find any output bindings on the method, but there were no errors // so we treat the found bindings as an empty list and return true bindingsList = new List<IDictionary<string, object>>(); return true; } private bool TryGetRetryOptionsFromAttribute(AttributeData attribute, Location location, out GeneratorRetryOptions? retryOptions) { retryOptions = null; if (TryGetAttributeProperties(attribute, null, out IDictionary<string, object?>? attrProperties)) { retryOptions = new GeneratorRetryOptions(); // Would not expect this to fail since MaxRetryCount is a required value of a retry policy attribute if (attrProperties!.TryGetValue(Constants.RetryConstants.MaxRetryCountKey, out object? maxRetryCount)) { retryOptions.MaxRetryCount = maxRetryCount!.ToString(); } // Check which strategy is being used by checking the attribute class if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.FixedDelayRetryAttribute)) { retryOptions.Strategy = RetryStrategy.FixedDelay; if (attrProperties.TryGetValue(Constants.RetryConstants.DelayIntervalKey, out object? delayInterval)) // nonnullable constructor arg of attribute, wouldn't expect this to fail { retryOptions.DelayInterval = delayInterval!.ToString(); } } else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.ExponentialBackoffRetryAttribute)) { retryOptions.Strategy = RetryStrategy.ExponentialBackoff; if (attrProperties.TryGetValue(Constants.RetryConstants.MinimumIntervalKey, out object? minimumInterval)) // nonnullable constructor arg of attribute, wouldn't expect this to fail { retryOptions.MinimumInterval = minimumInterval!.ToString(); } if (attrProperties.TryGetValue(Constants.RetryConstants.MaximumIntervalKey, out object? maximumInterval)) // nonnullable constructor arg of attribute, wouldn't expect this to fail { retryOptions.MaximumInterval = maximumInterval!.ToString(); } } else { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidRetryOptions, location)); return false; } return true; } return false; } /// <summary> /// Checks for and returns input and trigger bindings found in the parameters of the Azure Function method. /// </summary> private bool TryGetParameterInputAndTriggerBindings(IMethodSymbol method, out bool supportsRetryOptions, out bool hasHttpTrigger, out IList<IDictionary<string, object>>? bindingsList) { supportsRetryOptions = false; hasHttpTrigger = false; bindingsList = new List<IDictionary<string, object>>(); foreach (IParameterSymbol parameter in method.Parameters) { // If there's no attribute, we can assume that this parameter is not a binding if (!parameter.GetAttributes().Any()) { continue; } // Check to see if any of the attributes associated with this parameter is a BindingAttribute foreach (var attribute in parameter.GetAttributes()) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType?.BaseType, _knownFunctionMetadataTypes.BindingAttribute)) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.HttpTriggerBinding)) { hasHttpTrigger = true; } DataType dataType = _dataTypeParser.GetDataType(parameter.Type); bool cardinalityValidated = false; bool supportsDeferredBinding = SupportsDeferredBinding(attribute, parameter.Type.ToString()); if (_cardinalityParser.IsCardinalitySupported(attribute)) { DataType updatedDataType = DataType.Undefined; if (!_cardinalityParser.IsCardinalityValid(parameter, attribute, out updatedDataType)) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidCardinality, Location.None, parameter.Name)); bindingsList = null; return false; } // update the DataType of this binding with the updated type found during call to IsCardinalityValid // ex. IList<String> would be evaluated as "Undefined" by the call to GetDataType // but it would be correctly evaluated as "String" during the call to IsCardinalityValid which parses iterable collections dataType = updatedDataType; cardinalityValidated = true; } string bindingName = parameter.Name; if (!TryCreateBindingDictionary(attribute, bindingName, Location.None, out IDictionary<string, object>? bindings, supportsDeferredBinding)) { bindings = null; return false; } // If cardinality is supported and validated, but was not found in named args, constructor args, or default value attributes // default to Cardinality: One to stay in sync with legacy generator. if (cardinalityValidated && !bindings!.Keys.Contains("cardinality")) { bindings!.Add("cardinality", "One"); } if (dataType is not DataType.Undefined) { bindings!.Add("dataType", Enum.GetName(typeof(DataType), dataType)); } // check for binding capabilities var bindingCapabilitiesAttr = attribute?.AttributeClass?.GetAttributes().Where(a => (SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownFunctionMetadataTypes.BindingCapabilitiesAttribute))); if (bindingCapabilitiesAttr.FirstOrDefault() is not null) { var bindingCapabilities = bindingCapabilitiesAttr.FirstOrDefault().ConstructorArguments; if (bindingCapabilities.Any(s => string.Equals(s.Values.FirstOrDefault().Value?.ToString(), Constants.BindingCapabilities.FunctionLevelRetry, StringComparison.OrdinalIgnoreCase))) { supportsRetryOptions = true; } } bindingsList.Add(bindings!); } } } return true; } private bool SupportsDeferredBinding(AttributeData bindingAttribute, string bindingType) { var advertisedAttributes = bindingAttribute?.AttributeClass?.GetAttributes(); if (advertisedAttributes != null) { foreach (var advertisedAttribute in advertisedAttributes) { if (SymbolEqualityComparer.Default.Equals(advertisedAttribute.AttributeClass, _knownFunctionMetadataTypes.InputConverterAttributeType)) { foreach (var converter in advertisedAttribute.ConstructorArguments) { if (DoesConverterSupportDeferredBinding(converter, bindingType)) { return true; } } } } } return false; } private bool DoesConverterSupportDeferredBinding(TypedConstant converter, string bindingType) { var converterType = converter.Value as ITypeSymbol; var converterAdvertisedAttributes = converterType?.GetAttributes().ToList(); if (converterAdvertisedAttributes is not null) { bool converterAdvertisesDeferredBindingSupport = converterAdvertisedAttributes.Any( a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownFunctionMetadataTypes.SupportsDeferredBindingAttributeType)); if (converterAdvertisesDeferredBindingSupport) { bool converterAdvertisesTypes = converterAdvertisedAttributes.Any( a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _knownFunctionMetadataTypes.SupportedTargetTypeAttributeType)); if (!converterAdvertisesTypes) { // If a converter advertises deferred binding but does not explicitly advertise any types then DeferredBinding will be supported for all the types return true; } return DoesConverterSupportTargetType(converterAdvertisedAttributes, bindingType); } } return false; } private bool DoesConverterSupportTargetType(List<AttributeData> converterAdvertisedAttributes, string bindingType) { foreach (AttributeData attribute in converterAdvertisedAttributes) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _knownFunctionMetadataTypes.SupportedTargetTypeAttributeType)) { foreach (var element in attribute.ConstructorArguments) { if (string.Equals(element.Type?.GetFullName(), typeof(Type).FullName, StringComparison.Ordinal) && string.Equals(element.Value?.ToString(), bindingType, StringComparison.Ordinal)) { return true; } } } } return false; } /// <summary> /// Checks for and returns any bindings found in the Return Type of the method /// </summary> private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList<IDictionary<string, object>>? bindingsList) { ITypeSymbol? returnTypeSymbol = method.ReturnType; bindingsList = new List<IDictionary<string, object>>(); if (returnTypeSymbol is null) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, nameof(returnTypeSymbol))); bindingsList = null; return false; } if (!SymbolEqualityComparer.Default.Equals(returnTypeSymbol, _knownTypes.VoidType) && !SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskType) || // For HTTP triggers, include the return binding even if the return type is void or Task. hasHttpTrigger) { // If there is a Task<T> return type, inspect T, the inner type. if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskOfTType)) { if (returnTypeSymbol is INamedTypeSymbol namedTypeSymbol) { if (namedTypeSymbol.IsGenericType) { returnTypeSymbol = namedTypeSymbol.TypeArguments.FirstOrDefault();// Generic task should only have one type argument } } if (returnTypeSymbol is null) // need this check here b/c return type symbol takes on a new value from the inner argument type above { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, nameof(returnTypeSymbol))); bindingsList = null; return false; } } if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol, _knownFunctionMetadataTypes.HttpResponseData)) { bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); } else { if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasMethodOutputBinding, out bindingsList)) { bindingsList = null; return false; } } } return true; } private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasMethodOutputBinding, out IList<IDictionary<string, object>>? bindingsList) { var members = returnTypeSymbol.GetMembers(); var foundHttpOutput = false; var returnTypeHasOutputBindings = false; bindingsList = new List<IDictionary<string, object>>(); // initialize this without size, because it will be difficult to predict how many bindings we can find here in the user code. foreach (var prop in returnTypeSymbol.GetMembers().Where(a => a is IPropertySymbol)) { if (prop is null) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, nameof(prop))); bindingsList = null; return false; } // Check for HttpResponseData type for legacy apps if (prop is IPropertySymbol property && (SymbolEqualityComparer.Default.Equals(property.Type, _knownFunctionMetadataTypes.HttpResponseData))) { if (foundHttpOutput) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleHttpResponseTypes, Location.None, new object[] { returnTypeSymbol.Name })); bindingsList = null; return false; } foundHttpOutput = true; bindingsList.Add(GetHttpReturnBinding(prop.Name)); continue; } var propAttributes = prop.GetAttributes(); if (propAttributes.Length > 0) { var bindingAttributes = propAttributes.Where(p => p.AttributeClass!.IsOrDerivedFrom(_knownFunctionMetadataTypes.BindingAttribute)); if (bindingAttributes.Count() > 1) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Property", prop.Name.ToString() })); bindingsList = null; return false; } // Check if this property has an HttpResultAttribute on it if (HasHttpResultAttribute(prop)) { if (foundHttpOutput) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleHttpResponseTypes, Location.None, new object[] { returnTypeSymbol.Name })); bindingsList = null; return false; } foundHttpOutput = true; bindingsList.Add(GetHttpReturnBinding(prop.Name)); } else if (bindingAttributes.Any()) { if (!TryCreateBindingDictionary(bindingAttributes.FirstOrDefault(), prop.Name, prop.Locations.FirstOrDefault(), out IDictionary<string, object>? bindings)) { bindingsList = null; return false; } bindingsList.Add(bindings!); returnTypeHasOutputBindings = true; } } } if (hasHttpTrigger && !foundHttpOutput && !hasMethodOutputBinding && !returnTypeHasOutputBindings) { bindingsList.Add(GetHttpReturnBinding(Constants.FunctionMetadataBindingProps.ReturnBindingName)); } return true; } private bool HasHttpResultAttribute(ISymbol prop) { var attributes = prop.GetAttributes(); foreach (var attribute in attributes) { if (attribute.AttributeClass is not null && attribute.AttributeClass.IsOrDerivedFrom(_knownFunctionMetadataTypes.HttpResultAttribute)) { return true; } } return false; } private IDictionary<string, object> GetHttpReturnBinding(string returnBindingName) { var httpBinding = new Dictionary<string, object> { { "name", returnBindingName }, { "type", "http" }, { "direction", "Out" } }; return httpBinding; } private bool TryCreateBindingDictionary(AttributeData bindingAttrData, string bindingName, Location? bindingLocation, out IDictionary<string, object>? bindings, bool supportsDeferredBinding = false) { // Get binding info as a dictionary with keys as the property name and value as the property value if (!TryGetAttributeProperties(bindingAttrData, bindingLocation, out IDictionary<string, object?>? attributeProperties)) { bindings = null; return false; } // Grab some required binding info properties string attributeName = bindingAttrData.AttributeClass!.Name; // properly format binding types by removing "Attribute" and "Input" descriptors string bindingType = attributeName.TrimStringsFromEnd(_functionsStringNamesToRemove).ToLowerFirstCharacter(); // Set binding direction string bindingDirection = SymbolEqualityComparer.Default.Equals(bindingAttrData.AttributeClass?.BaseType, _knownFunctionMetadataTypes.OutputBindingAttribute) ? "Out" : "In"; var bindingCount = attributeProperties!.Count + 3; bindings = new Dictionary<string, object>(capacity: bindingCount) { { "name", bindingName }, { "type", bindingType }, { "direction", bindingDirection } }; if (supportsDeferredBinding) { bindings.Add("properties", new Dictionary<string, string>() { { "SupportsDeferredBinding", "True" } }); } // Add additional bindingInfo to the anonymous type because some functions have more properties than others foreach (var prop in attributeProperties!) // attributeProperties won't be null here b/c we would've exited this method earlier if it was during TryGetAttributeProperties check { var propertyName = prop.Key; if (prop.Value?.GetType().IsArray ?? false) { bindings[propertyName] = prop.Value; } else { bindings[propertyName] = prop.Value!; } } return true; } private bool TryGetAttributeProperties(AttributeData attributeData, Location? attribLocation, out IDictionary<string, object?>? attrProperties) { attrProperties = new Dictionary<string, object?>(); if (attributeData.ConstructorArguments.Any()) { if (!TryLoadConstructorArguments(attributeData, attrProperties, attribLocation)) { attrProperties = null; return false; } } foreach (var namedArgument in attributeData.NamedArguments) { if (IsArrayOrNotNull(namedArgument.Value)) { if (string.Equals(namedArgument.Key, Constants.FunctionMetadataBindingProps.IsBatchedKey) && !attrProperties.ContainsKey("cardinality") && namedArgument.Value.Value != null) { var argValue = (bool)namedArgument.Value.Value; // isBatched only takes in booleans and the generator will parse it as a bool so we can type cast this to use in the next line attrProperties["cardinality"] = argValue ? "Many" : "One"; } else { if (TryParseValueByType(namedArgument.Value, out object? argValue)) { attrProperties[namedArgument.Key] = argValue; } else { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidBindingAttributeArgument, attribLocation)); return false; } } } } // some properties have default values, so if these properties were not already defined in constructor or named arguments, we will auto-add them here foreach (var member in attributeData.AttributeClass!.GetMembers().Where(a => a is IPropertySymbol)) { var defaultValAttrList = member.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, _knownFunctionMetadataTypes.DefaultValue)); if (defaultValAttrList.SingleOrDefault() is { } defaultValAttr) // list will only be of size one b/c there cannot be duplicates of an attribute on one piece of syntax { var argName = member.Name; object arg = defaultValAttr.ConstructorArguments.SingleOrDefault().Value!; // only one constructor arg in DefaultValue attribute (the default value) if (arg is bool b && string.Equals(argName, Constants.FunctionMetadataBindingProps.IsBatchedKey)) { if (!attrProperties.Keys.Contains("cardinality")) { attrProperties["cardinality"] = b ? "Many" : "One"; } } else if (!attrProperties.Keys.Any(a => string.Equals(a, argName, StringComparison.OrdinalIgnoreCase))) // check if this property has been assigned a value already in constructor or named args { attrProperties[argName] = arg; } } } return true; } private static bool IsArrayOrNotNull(TypedConstant? namedArgument) { if (namedArgument is null) { return false; } // special handling required for array types which store values differently (cannot check namedArgument.Value.Value) if (namedArgument.Value.Kind is TypedConstantKind.Array) { return true; } else { return namedArgument.Value.Value != null; // similar to legacy generator, arguments with null values are not written to function metadata } } private bool TryLoadConstructorArguments(AttributeData attributeData, IDictionary<string, object?> arguments, Location? attributeLocation) { IMethodSymbol? attribMethodSymbol = attributeData.AttributeConstructor; // Check if the attribute constructor has any parameters if (attribMethodSymbol is null) { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, attributeLocation, nameof(attribMethodSymbol))); return false; } // It's fair to assume that constructor arguments appear before named arguments, and // that the constructor names would match the property names for (int i = 0; i < attributeData.ConstructorArguments.Length; i++) { string argumentName = attribMethodSymbol.Parameters[i].Name; OverrideBindingName(attributeData.AttributeClass!, ref argumentName); // either argumentName will remain unchanged OR be updated to the overridden name at the end of this. var arg = attributeData.ConstructorArguments[i]; if (TryParseValueByType(arg, out object? argValue)) { arguments[argumentName] = argValue; } else { _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidBindingAttributeArgument, attributeLocation)); return false; } } return true; } private bool TryParseValueByType(TypedConstant attributeArg, out object? argValue) { argValue = null; switch (attributeArg.Kind) { case TypedConstantKind.Primitive: argValue = attributeArg.Value; break; case TypedConstantKind.Enum: var enumValue = attributeArg.Type!.GetMembers() .FirstOrDefault(m => m is IFieldSymbol field && field.ConstantValue is object value && value.Equals(attributeArg.Value)); if (enumValue is null) { return false; } // we want just the enumValue symbol's name (ex: Admin, Anonymous, Function) argValue = enumValue.Name; break; case TypedConstantKind.Array: var arrayValues = attributeArg.Values.Select(a => a.Value?.ToString()).ToArray(); argValue = arrayValues; break; } return argValue is not null; } /// <summary> /// This method handles cases where an attribute property has a different function metadata binding name. /// </summary> /// <remarks> /// For example, in the BlobTriggerAttribute type, the "BlobPath" property is decorated with "MetadataBindingPropertyName" attribute /// where "path" is provided as the value to be used when generating metadata binding data, as shown below. /// /// [MetadataBindingPropertyName("path")] /// public string BlobPath { get; set; } /// /// </remarks> /// <param name="attributeClass">The attribute type represented as an <see cref="INamedTypeSymbol"/></param> /// <param name="argumentName">The argument's name as represented in the constructor. This may be overriden by the MetadataBindingPropertyName.</param> private void OverrideBindingName(INamedTypeSymbol attributeClass, ref string argumentName) { foreach (var prop in attributeClass.GetMembers().Where(a => a is IPropertySymbol)) { if (String.Equals(prop.Name, argumentName, StringComparison.OrdinalIgnoreCase)) // relies on convention where constructor parameter names match the property their value will be assigned to (JSON serialization is a precedence for this convention) { var bindingNameAttrList = prop.GetAttributes().Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, _knownFunctionMetadataTypes.BindingPropertyNameAttribute)); if (bindingNameAttrList.SingleOrDefault() is { } bindingNameAttr) // there will only be one BindingAttributeName attribute b/c there can't be duplicate attributes on a piece of syntax { argumentName = bindingNameAttr.ConstructorArguments.First().Value!.ToString(); // there is only one constructor argument for this binding attribute (the binding name override) } } } } } } }