diff --git a/DotNetWorker.sln b/DotNetWorker.sln index f18ed5711..2fe0c13d0 100644 --- a/DotNetWorker.sln +++ b/DotNetWorker.sln @@ -132,6 +132,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetIntegration", "samples\AspNetIntegration\AspNetIntegration.csproj", "{D2F67410-9933-42E8-B04A-E17634D83A30}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependentAssemblyWithFunctions", "test\DependentAssemblyWithFunctions\DependentAssemblyWithFunctions.csproj", "{AB6E1E7A-0D2C-4086-9487-566887C1E753}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -326,6 +328,10 @@ Global {D2F67410-9933-42E8-B04A-E17634D83A30}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2F67410-9933-42E8-B04A-E17634D83A30}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2F67410-9933-42E8-B04A-E17634D83A30}.Release|Any CPU.Build.0 = Release|Any CPU + {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB6E1E7A-0D2C-4086-9487-566887C1E753}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -383,6 +389,7 @@ Global {286F9EE3-00AE-4EFA-BFD8-A2E58BC809D2} = {FD7243E4-BF18-43F8-8744-BA1D17ACF378} {17BDCE12-6964-4B87-B2AC-68CE270A3E9A} = {FD7243E4-BF18-43F8-8744-BA1D17ACF378} {D2F67410-9933-42E8-B04A-E17634D83A30} = {9D6603BD-7EA2-4D11-A69C-0D9E01317FD6} + {AB6E1E7A-0D2C-4086-9487-566887C1E753} = {B5821230-6E0A-4535-88A9-ED31B6F07596} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {497D2ED4-A13E-4BCA-8D29-F30CA7D0EA4A} diff --git a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs index 6e7a88872..ae160a342 100644 --- a/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionExecutor/FunctionExecutorGenerator.Parser.cs @@ -32,8 +32,7 @@ internal ICollection GetFunctions(List /// The parameter associated with a binding attribute that supports cardinality represented as an . - /// The representation of the paramter's type if it exists. /// The binding attribute that supports cardinality. /// The that best represents the parameter. /// Returns true if the parameter is compatible with the cardinality defined by the attribute, else returns false. - public bool IsCardinalityValid(IParameterSymbol parameterSymbol, TypeSyntax? parameterTypeSyntax, AttributeData attribute, out DataType dataType) + public bool IsCardinalityValid(IParameterSymbol parameterSymbol, AttributeData attribute, out DataType dataType) { dataType = DataType.Undefined; var cardinalityIsNamedArg = false; @@ -139,12 +138,7 @@ public bool IsCardinalityValid(IParameterSymbol parameterSymbol, TypeSyntax? par } else if (isGenericEnumerable) { - if (parameterTypeSyntax is null) - { - return false; - } - - dataType = ResolveIEnumerableOfT(parameterSymbol, parameterTypeSyntax, out bool hasError); + dataType = ResolveIEnumerableOfT(parameterSymbol, out bool hasError); if (hasError) { @@ -164,10 +158,9 @@ public bool IsCardinalityValid(IParameterSymbol parameterSymbol, TypeSyntax? par /// Find the underlying data type of an IEnumerableOfT (String, Binary, Undefined) /// ex. IEnumerable would return DataType.Binary /// - private DataType ResolveIEnumerableOfT(IParameterSymbol parameterSymbol, TypeSyntax parameterSyntax, out bool hasError) + private DataType ResolveIEnumerableOfT(IParameterSymbol parameterSymbol, out bool hasError) { var result = DataType.Undefined; - var currentSyntax = parameterSyntax; hasError = false; var currSymbol = parameterSymbol.Type; diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs index 8c0d1caf6..5d4ce348a 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.Parser.cs @@ -42,34 +42,33 @@ public Parser(GeneratorExecutionContext context) /// Takes in candidate methods from the user compilation and parses them to return function metadata info as GeneratorFunctionMetadata. /// /// List of candidate methods from the syntax receiver. - public IReadOnlyList GetFunctionMetadataInfo(List methods) + public IReadOnlyList GetFunctionMetadataInfo(List methods) { var result = ImmutableArray.CreateBuilder(); - var assemblyName = Compilation.Assembly.Name; - var scriptFile = Path.Combine(assemblyName + ".dll"); - // Loop through the candidate methods (methods with any attribute associated with them) - foreach (MethodDeclarationSyntax method in methods) + foreach (IMethodSymbol method in methods) { CancellationToken.ThrowIfCancellationRequested(); - var model = Compilation.GetSemanticModel(method.SyntaxTree); - - if (!FunctionsUtil.IsValidFunctionMethod(_context, Compilation, model, method, out string? functionName)) + string? funcName = null; + if (!FunctionsUtil.TryGetFunctionName(method, Compilation, out funcName)) { - continue; + _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 = Path.Combine(assemblyName + ".dll"); + var newFunction = new GeneratorFunctionMetadata { - Name = functionName, - EntryPoint = FunctionsUtil.GetFullyQualifiedMethodName(method, model), + Name = funcName, + EntryPoint = FunctionsUtil.GetFullyQualifiedMethodName(method), Language = Constants.Languages.DotnetIsolated, ScriptFile = scriptFile }; - if (!TryGetBindings(method, model, out IList>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? retryOptions)) + if (!TryGetBindings(method, out IList>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? retryOptions)) { continue; } @@ -92,14 +91,14 @@ public IReadOnlyList GetFunctionMetadataInfo(List>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? validatedRetryOptions) + private bool TryGetBindings(IMethodSymbol method, out IList>? bindings, out bool hasHttpTrigger, out GeneratorRetryOptions? validatedRetryOptions) { hasHttpTrigger = false; validatedRetryOptions = null; - if (!TryGetMethodOutputBinding(method, model, out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? methodOutputBindings) - || !TryGetParameterInputAndTriggerBindings(method, model, out bool supportsRetryOptions, out hasHttpTrigger, out IList>? parameterInputAndTriggerBindings) - || !TryGetReturnTypeBindings(method, model, hasHttpTrigger, hasOutputBinding, out IList>? returnTypeBindings)) + if (!TryGetMethodOutputBinding(method, out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? methodOutputBindings) + || !TryGetParameterInputAndTriggerBindings(method, out bool supportsRetryOptions, out hasHttpTrigger, out IList>? parameterInputAndTriggerBindings) + || !TryGetReturnTypeBindings(method, hasHttpTrigger, hasOutputBinding, out IList>? returnTypeBindings)) { bindings = null; return false; @@ -121,7 +120,7 @@ private bool TryGetBindings(MethodDeclarationSyntax method, SemanticModel model, } else if (!supportsRetryOptions) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidRetryOptions, method.GetLocation())); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidRetryOptions, Location.None)); return false; } } @@ -132,12 +131,9 @@ private bool TryGetBindings(MethodDeclarationSyntax method, SemanticModel model, /// /// Checks for and returns any OutputBinding attributes associated with the method. /// - private bool TryGetMethodOutputBinding(MethodDeclarationSyntax method, SemanticModel model, out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) + private bool TryGetMethodOutputBinding(IMethodSymbol method,out bool hasOutputBinding, out GeneratorRetryOptions? retryOptions, out IList>? bindingsList) { - var bindingLocation = method.Identifier.GetLocation(); - - var methodSymbol = model.GetDeclaredSymbol(method); - var attributes = methodSymbol!.GetAttributes(); // methodSymbol is not null here because it's checked in IsValidAzureFunction which is called before bindings are collected/created + 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; hasOutputBinding = false; @@ -147,7 +143,7 @@ private bool TryGetMethodOutputBinding(MethodDeclarationSyntax method, SemanticM { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType, _knownFunctionMetadataTypes.RetryAttribute)) { - if (TryGetRetryOptionsFromAttribute(attribute, method.GetLocation(), out GeneratorRetryOptions? retryOptionsFromAttr)) + if (TryGetRetryOptionsFromAttribute(attribute, Location.None, out GeneratorRetryOptions? retryOptionsFromAttr)) { retryOptions = retryOptionsFromAttr; } @@ -158,7 +154,7 @@ private bool TryGetMethodOutputBinding(MethodDeclarationSyntax method, SemanticM // There can only be one output binding associated with a function. If there is more than one, we return a diagnostic error here. if (hasOutputBinding) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, bindingLocation, new object[] { "Method", method.Identifier.ToString() })); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Method", method.Name })); bindingsList = null; return false; } @@ -170,7 +166,7 @@ private bool TryGetMethodOutputBinding(MethodDeclarationSyntax method, SemanticM if (outputBindingAttribute != null) { - if (!TryCreateBindingDict(outputBindingAttribute, Constants.FunctionMetadataBindingProps.ReturnBindingName, bindingLocation, out IDictionary? bindingDict)) + if (!TryCreateBindingDict(outputBindingAttribute, Constants.FunctionMetadataBindingProps.ReturnBindingName, Location.None, out IDictionary? bindingDict)) { bindingsList = null; return false; @@ -244,29 +240,22 @@ private bool TryGetRetryOptionsFromAttribute(AttributeData attribute, Location l /// /// Checks for and returns input and trigger bindings found in the parameters of the Azure Function method. /// - private bool TryGetParameterInputAndTriggerBindings(MethodDeclarationSyntax method, SemanticModel model, out bool supportsRetryOptions, out bool hasHttpTrigger, out IList>? bindingsList) + private bool TryGetParameterInputAndTriggerBindings(IMethodSymbol method, out bool supportsRetryOptions, out bool hasHttpTrigger, out IList>? bindingsList) { supportsRetryOptions = false; hasHttpTrigger = false; bindingsList = new List>(); - foreach (ParameterSyntax parameter in method.ParameterList.Parameters) + foreach (IParameterSymbol parameter in method.Parameters) { // If there's no attribute, we can assume that this parameter is not a binding - if (parameter.AttributeLists.Count == 0) + if (!parameter.GetAttributes().Any()) { continue; } - if (model.GetDeclaredSymbol(parameter) is not IParameterSymbol parameterSymbol) - { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, parameter.Identifier.GetLocation(), nameof(parameterSymbol))); - bindingsList = null; - return false; - } - // Check to see if any of the attributes associated with this parameter is a BindingAttribute - foreach (var attribute in parameterSymbol.GetAttributes()) + foreach (var attribute in parameter.GetAttributes()) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass?.BaseType?.BaseType, _knownFunctionMetadataTypes.BindingAttribute)) { @@ -276,23 +265,18 @@ private bool TryGetParameterInputAndTriggerBindings(MethodDeclarationSyntax meth hasHttpTrigger = true; } - DataType dataType = _dataTypeParser.GetDataType(parameterSymbol.Type); + DataType dataType = _dataTypeParser.GetDataType(parameter.Type); bool cardinalityValidated = false; - bool supportsDeferredBinding = false; - - if (SupportsDeferredBinding(attribute, parameterSymbol.Type.ToString())) - { - supportsDeferredBinding = true; - } + bool supportsDeferredBinding = SupportsDeferredBinding(attribute, parameter.Type.ToString()); if (_cardinalityParser.IsCardinalitySupported(attribute)) { DataType updatedDataType = DataType.Undefined; - if (!_cardinalityParser.IsCardinalityValid(parameterSymbol, parameter.Type, attribute, out updatedDataType)) + if (!_cardinalityParser.IsCardinalityValid(parameter, attribute, out updatedDataType)) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidCardinality, parameter.Identifier.GetLocation(), parameterSymbol.Name)); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.InvalidCardinality, Location.None, parameter.Name)); bindingsList = null; return false; } @@ -304,9 +288,9 @@ private bool TryGetParameterInputAndTriggerBindings(MethodDeclarationSyntax meth cardinalityValidated = true; } - string bindingName = parameter.Identifier.ValueText; + string bindingName = parameter.Name; - if (!TryCreateBindingDict(attribute, bindingName, parameter.Identifier.GetLocation(), out IDictionary? bindingDict, supportsDeferredBinding)) + if (!TryCreateBindingDict(attribute, bindingName, Location.None, out IDictionary? bindingDict, supportsDeferredBinding)) { bindingsList = null; return false; @@ -418,15 +402,14 @@ private bool DoesConverterSupportTargetType(List converterAdverti /// /// Checks for and returns any bindings found in the Return Type of the method /// - private bool TryGetReturnTypeBindings(MethodDeclarationSyntax method, SemanticModel model, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) + private bool TryGetReturnTypeBindings(IMethodSymbol method, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) { - TypeSyntax returnTypeSyntax = method.ReturnType; - ITypeSymbol? returnTypeSymbol = model.GetSymbolInfo(returnTypeSyntax).Symbol as ITypeSymbol; + ITypeSymbol? returnTypeSymbol = method.ReturnType; bindingsList = new List>(); if (returnTypeSymbol is null) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, returnTypeSyntax.GetLocation(), nameof(returnTypeSymbol))); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, nameof(returnTypeSymbol))); bindingsList = null; return false; } @@ -437,13 +420,17 @@ private bool TryGetReturnTypeBindings(MethodDeclarationSyntax method, SemanticMo // If there is a Task return type, inspect T, the inner type. if (SymbolEqualityComparer.Default.Equals(returnTypeSymbol.OriginalDefinition, _knownTypes.TaskOfTType)) { - GenericNameSyntax genericSyntax = (GenericNameSyntax)returnTypeSyntax; - var innerTypeSyntax = genericSyntax.TypeArgumentList.Arguments.First(); // Generic task should only have one type argument - returnTypeSymbol = model.GetSymbolInfo(innerTypeSyntax).Symbol as ITypeSymbol; + 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, genericSyntax.Identifier.GetLocation(), nameof(returnTypeSymbol))); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SymbolNotFound, Location.None, nameof(returnTypeSymbol))); bindingsList = null; return false; } @@ -455,7 +442,7 @@ private bool TryGetReturnTypeBindings(MethodDeclarationSyntax method, SemanticMo } else { - if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasOutputBinding, returnTypeSyntax.GetLocation(), out bindingsList)) + if (!TryGetReturnTypePropertyBindings(returnTypeSymbol, hasHttpTrigger, hasOutputBinding, out bindingsList)) { bindingsList = null; return false; @@ -466,7 +453,7 @@ private bool TryGetReturnTypeBindings(MethodDeclarationSyntax method, SemanticMo return true; } - private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasOutputBinding, Location returnTypeLocation, out IList>? bindingsList) + private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool hasHttpTrigger, bool hasOutputBinding, out IList>? bindingsList) { var members = returnTypeSymbol.GetMembers(); var foundHttpOutput = false; @@ -486,7 +473,7 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool { if (foundHttpOutput) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleHttpResponseTypes, returnTypeLocation, new object[] { returnTypeSymbol.Name })); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleHttpResponseTypes, Location.None, new object[] { returnTypeSymbol.Name })); bindingsList = null; return false; } @@ -505,7 +492,7 @@ private bool TryGetReturnTypePropertyBindings(ITypeSymbol returnTypeSymbol, bool // validate that there's only one binding attribute per property if (foundPropertyOutputAttr) { - _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, prop.Locations.FirstOrDefault(), new object[] { "Property", prop.Name.ToString() })); + _context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MultipleBindingsGroupedTogether, Location.None, new object[] { "Property", prop.Name.ToString() })); bindingsList = null; return false; } diff --git a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs index f218d39b4..96b722e20 100644 --- a/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs +++ b/sdk/Sdk.Generators/FunctionMetadataProviderGenerator/FunctionMetadataProviderGenerator.cs @@ -2,8 +2,11 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators @@ -35,7 +38,10 @@ public void Execute(GeneratorExecutionContext context) // attempt to parse user compilation var p = new Parser(context); - IReadOnlyList functionMetadataInfo = p.GetFunctionMetadataInfo(receiver.CandidateMethods); + var entryAssemblyFuncs = GetEntryAssemblyFunctions(receiver.CandidateMethods, context); + var dependentFuncs = GetDependentAssemblyFunctions(context); + + IReadOnlyList functionMetadataInfo = p.GetFunctionMetadataInfo(entryAssemblyFuncs.Concat(dependentFuncs).ToList()); // Proceed to generate the file if function metadata info was successfully returned if (functionMetadataInfo.Count > 0) @@ -68,5 +74,56 @@ private static bool ShouldIncludeAutoGeneratedAttributes(GeneratorExecutionConte return string.Equals(value, bool.TrueString, System.StringComparison.OrdinalIgnoreCase); } + + private IEnumerable GetEntryAssemblyFunctions(List candidateMethods, GeneratorExecutionContext context) + { + IList? entryAssemblyFuncs = new List(); + + foreach (MethodDeclarationSyntax method in candidateMethods) + { + var model = context.Compilation.GetSemanticModel(method.SyntaxTree); + + if (FunctionsUtil.IsValidFunctionMethod(context, context.Compilation, model, method)) + { + IMethodSymbol? methodSymbol = (IMethodSymbol) model.GetDeclaredSymbol(method)!; + yield return methodSymbol; + } + } + } + + /// + /// Collect methods with Function attributes on them from dependent/referenced assemblies. + /// + private IEnumerable GetDependentAssemblyFunctions(GeneratorExecutionContext context) + { + foreach (var assembly in context.Compilation.SourceModule.ReferencedAssemblySymbols) + { + var namespaceSymbols = assembly.GlobalNamespace.GetMembers(); + + foreach (var namespaceSymbol in namespaceSymbols) + { + var namespaceMembers = namespaceSymbol.GetMembers(); + + foreach (var m in namespaceMembers) + { + if (m is INamedTypeSymbol namedType) + { + var typeMembers = namedType.GetMembers(); + + foreach (var typeMember in typeMembers) + { + if (typeMember is IMethodSymbol method) + { + if (FunctionsUtil.IsFunctionSymbol(method, context.Compilation)) + { + yield return method; + } + } + } + } + } + } + } + } } } diff --git a/sdk/Sdk.Generators/FunctionsUtil.cs b/sdk/Sdk.Generators/FunctionsUtil.cs index d8d2ee0fc..429400f27 100644 --- a/sdk/Sdk.Generators/FunctionsUtil.cs +++ b/sdk/Sdk.Generators/FunctionsUtil.cs @@ -4,6 +4,9 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis; using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Data; +using System; namespace Microsoft.Azure.Functions.Worker.Sdk.Generators { @@ -16,10 +19,8 @@ internal static bool IsValidFunctionMethod( GeneratorExecutionContext context, Compilation compilation, SemanticModel model, - MethodDeclarationSyntax method, - out string? functionName) + MethodDeclarationSyntax method) { - functionName = null; var methodSymbol = model.GetDeclaredSymbol(method); if (methodSymbol is null) @@ -28,12 +29,21 @@ internal static bool IsValidFunctionMethod( return false; } - foreach (var attr in methodSymbol.GetAttributes()) + if (IsFunctionSymbol(methodSymbol, compilation)) + { + return true; + } + + return false; + } + + internal static bool IsFunctionSymbol(ISymbol symbol, Compilation compilation) + { + foreach (var attr in symbol.GetAttributes()) { if (attr.AttributeClass != null && SymbolEqualityComparer.Default.Equals(attr.AttributeClass, compilation.GetTypeByMetadataName(Constants.Types.FunctionName))) { - functionName = (string)attr.ConstructorArguments.First().Value!; // If this is a function attribute this won't be null return true; } } @@ -41,17 +51,31 @@ internal static bool IsValidFunctionMethod( return false; } + internal static bool TryGetFunctionName(ISymbol symbol, Compilation compilation, out string? functionName) + { + functionName = null; + + var functionAttribute = symbol.GetAttributes() + .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, compilation.GetTypeByMetadataName(Constants.Types.FunctionName))); + + if (functionAttribute is not null) + { + functionName = (string) functionAttribute.ConstructorArguments.First().Value!; + return true; + } + + return false; + } + /// /// Gets the fully qualified name of the method. /// Ex: "MyNamespaceName.MyClassName.MyMethod" /// for a method called "MyMethod" inside the "MyClassName" type which is inside the "MyNamespaceName" namespace. /// - internal static string GetFullyQualifiedMethodName(MethodDeclarationSyntax method, SemanticModel semanticModel) + internal static string GetFullyQualifiedMethodName(IMethodSymbol method) { - var methodSymbol = semanticModel.GetDeclaredSymbol(method)!; - var fullyQualifiedClassName = methodSymbol.ContainingSymbol.ToDisplayString(); - - return $"{fullyQualifiedClassName}.{method.Identifier.ValueText}"; + var fullyQualifiedClassName = method.ContainingSymbol.ToDisplayString(); + return $"{fullyQualifiedClassName}.{method.Name}"; } } } diff --git a/sdk/release_notes.md b/sdk/release_notes.md index 7ba2857c9..fcc474fc2 100644 --- a/sdk/release_notes.md +++ b/sdk/release_notes.md @@ -15,4 +15,5 @@ ### Microsoft.Azure.Functions.Worker.Sdk.Generators - Parse named arguments by type (#1877) +- Refactor source gen to walk dependent assemblies (#1896) - Add diagnostic descriptor logs for parsing binding arguments in source gen (#1882) diff --git a/test/DependentAssemblyWithFunctions/DependencyFunction.cs b/test/DependentAssemblyWithFunctions/DependencyFunction.cs new file mode 100644 index 000000000..605b45343 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/DependencyFunction.cs @@ -0,0 +1,17 @@ + +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace DependentAssemblyWithFunctions +{ + public class DependencyFunction + { + [Function("DependencyFunc")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/DependentAssemblyWithFunctions/DependentAssemblyWithFunctions.csproj b/test/DependentAssemblyWithFunctions/DependentAssemblyWithFunctions.csproj new file mode 100644 index 000000000..34139f102 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/DependentAssemblyWithFunctions.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/test/DependentAssemblyWithFunctions/InternalFunction.cs b/test/DependentAssemblyWithFunctions/InternalFunction.cs new file mode 100644 index 000000000..6a34b7388 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/InternalFunction.cs @@ -0,0 +1,15 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace DependentAssemblyWithFunctions +{ + internal class InternalFunction + { + [Function(nameof(InternalFunction))] + public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/DependentAssemblyWithFunctions/StaticFunction.cs b/test/DependentAssemblyWithFunctions/StaticFunction.cs new file mode 100644 index 000000000..79c56a3e1 --- /dev/null +++ b/test/DependentAssemblyWithFunctions/StaticFunction.cs @@ -0,0 +1,15 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace DependentAssemblyWithFunctions +{ + public static class StaticFunction + { + [Function(nameof(StaticFunction))] + public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, + FunctionContext executionContext) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs new file mode 100644 index 000000000..621a7232d --- /dev/null +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DependentAssemblyTest.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker.Sdk.Generators; +using Xunit; + +namespace Microsoft.Azure.Functions.SdkGeneratorTests +{ + public partial class FunctionMetadataProviderGeneratorTests + { + public class DependentAssemblyTest + { + private readonly Assembly[] _referencedExtensionAssemblies; + + public DependentAssemblyTest() + { + // load all extensions used in tests (match extensions tested on E2E app? Or include ALL extensions?) + var abstractionsExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Abstractions.dll"); + var httpExtension = Assembly.LoadFrom("Microsoft.Azure.Functions.Worker.Extensions.Http.dll"); + var hostingExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.dll"); + var diExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.dll"); + var hostingAbExtension = Assembly.LoadFrom("Microsoft.Extensions.Hosting.Abstractions.dll"); + var diAbExtension = Assembly.LoadFrom("Microsoft.Extensions.DependencyInjection.Abstractions.dll"); + var dependentAssembly = Assembly.LoadFrom("DependentAssemblyWithFunctions.dll"); + + _referencedExtensionAssemblies = new[] + { + abstractionsExtension, + httpExtension, + hostingExtension, + hostingAbExtension, + diExtension, + diAbExtension, + dependentAssembly + }; + } + + [Fact] + public async Task FunctionInDependentAssemblyTest() + { + string inputCode = """ + using System; + using System.Collections.Generic; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + + namespace FunctionApp + { + public static class HttpTriggerSimple + { + [Function(nameof(HttpTriggerSimple))] + public static HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req, FunctionContext executionContext) + { + throw new NotImplementedException(); + } + } + } + """; + + string expectedGeneratedFileName = $"GeneratedFunctionMetadataProvider.g.cs"; + string expectedOutput = """ + // + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + namespace Microsoft.Azure.Functions.Worker + { + public class GeneratedFunctionMetadataProvider : IFunctionMetadataProvider + { + public Task> GetFunctionMetadataAsync(string directory) + { + var metadataList = new List(); + var Function0RawBindings = new List(); + Function0RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function0RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function0 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "HttpTriggerSimple", + EntryPoint = "FunctionApp.HttpTriggerSimple.Run", + RawBindings = Function0RawBindings, + ScriptFile = "TestProject.dll" + }; + metadataList.Add(Function0); + var Function1RawBindings = new List(); + Function1RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function1RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function1 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "DependencyFunc", + EntryPoint = "DependentAssemblyWithFunctions.DependencyFunction.Run", + RawBindings = Function1RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.dll" + }; + metadataList.Add(Function1); + var Function2RawBindings = new List(); + Function2RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function2RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function2 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "InternalFunction", + EntryPoint = "DependentAssemblyWithFunctions.InternalFunction.Run", + RawBindings = Function2RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.dll" + }; + metadataList.Add(Function2); + var Function3RawBindings = new List(); + Function3RawBindings.Add(@"{""name"":""req"",""type"":""HttpTrigger"",""direction"":""In"",""authLevel"":""Anonymous"",""methods"":[""get"",""post""]}"); + Function3RawBindings.Add(@"{""name"":""$return"",""type"":""http"",""direction"":""Out""}"); + + var Function3 = new DefaultFunctionMetadata + { + Language = "dotnet-isolated", + Name = "StaticFunction", + EntryPoint = "DependentAssemblyWithFunctions.StaticFunction.Run", + RawBindings = Function3RawBindings, + ScriptFile = "DependentAssemblyWithFunctions.dll" + }; + metadataList.Add(Function3); + + return Task.FromResult(metadataList.ToImmutableArray()); + } + } + + public static class WorkerHostBuilderFunctionMetadataProviderExtension + { + /// + /// Adds the GeneratedFunctionMetadataProvider to the service collection. + /// During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. + /// + public static IHostBuilder ConfigureGeneratedFunctionMetadataProvider(this IHostBuilder builder) + { + builder.ConfigureServices(s => + { + s.AddSingleton(); + }); + return builder; + } + } + } + """; + + await TestHelpers.RunTestAsync( + _referencedExtensionAssemblies, + inputCode, + expectedGeneratedFileName, + expectedOutput); + } + } + } +} diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs index 8478337b2..d52f5cd29 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/DiagnosticResultTests.cs @@ -76,7 +76,6 @@ public string QueueToBlob( var expectedDiagnosticResults = new List { new DiagnosticResult(DiagnosticDescriptors.MultipleBindingsGroupedTogether) - .WithSpan(17, 39, 17, 50) // these arguments are the values we pass as the message format parameters when creating the DiagnosticDescriptor instance. .WithArguments("Method", "QueueToBlob") }; @@ -131,7 +130,6 @@ public class MyOutputType var expectedDiagnosticResults = new List { new DiagnosticResult(DiagnosticDescriptors.MultipleBindingsGroupedTogether) - .WithSpan(28, 39, 28, 43) // these arguments are the values we pass as the message format parameters when creating the DiagnosticDescriptor instance. .WithArguments("Property", "Name") }; @@ -181,7 +179,6 @@ public class MultiReturnHttp var expectedDiagnosticResults = new List { new DiagnosticResult(DiagnosticDescriptors.MultipleHttpResponseTypes) - .WithSpan(11, 32, 11, 47) // these arguments are the values we pass as the message format parameters when creating the DiagnosticDescriptor instance. .WithArguments("MultiReturnHttp") }; @@ -221,7 +218,6 @@ public string Run([HttpTrigger(""get"")] string req) var expectedDiagnosticResults = new List { new DiagnosticResult(DiagnosticDescriptors.InvalidRetryOptions) - .WithSpan(10, 25, 15, 26) }; await TestHelpers.RunTestAsync( diff --git a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs index 63c5791d9..b09544bec 100644 --- a/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs +++ b/test/Sdk.Generator.Tests/FunctionMetadataProviderGeneratorTests/EventHubsBindingsTests.cs @@ -795,7 +795,6 @@ public static void InvalidEventHubsTrigger([EventHubTrigger(""test"", Connection var expectedDiagnosticResults = new List { new DiagnosticResult(DiagnosticDescriptors.InvalidCardinality) - .WithSpan(15, 146, 15, 151) // these arguments are the values we pass as the message format parameters when creating the DiagnosticDescriptor instance. .WithArguments("input") }; diff --git a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj index a5cbcdfee..838a3f7ea 100644 --- a/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj +++ b/test/Sdk.Generator.Tests/Sdk.Generator.Tests.csproj @@ -45,6 +45,7 @@ +