Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for SQS events in Amazon.Lambda.Annotations #1758

Merged
merged 13 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Libraries/Amazon.Lambda.Annotations.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"src\\Amazon.Lambda.Annotations\\Amazon.Lambda.Annotations.csproj",
"src\\Amazon.Lambda.Core\\Amazon.Lambda.Core.csproj",
"src\\Amazon.Lambda.RuntimeSupport\\Amazon.Lambda.RuntimeSupport.csproj",
"src\\Amazon.Lambda.SQSEvents\\Amazon.Lambda.SQSEvents.csproj",
"src\\Amazon.Lambda.Serialization.SystemTextJson\\Amazon.Lambda.Serialization.SystemTextJson.csproj",
"test\\Amazon.Lambda.Annotations.SourceGenerators.Tests\\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj",
"test\\TestExecutableServerlessApp\\TestExecutableServerlessApp.csproj",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

## Release 1.5.0
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
AWSLambda0115 | AWSLambdaCSharpGenerator | Error | Invalid Usage of API Parameters
AWSLambda0116 | AWSLambdaCSharpGenerator | Error | Invalid SQSEventAttribute encountered
AWSLambda0117 | AWSLambdaCSharpGenerator | Error | Invalid Lambda Method Signature

## Release 1.1.0
### New Rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ExecutableWithNoFunctions = new DiagnosticDescriptor(id: "AWSLambda0113",
title: "Executable output with no LambdaFunction annotations",
messageFormat: "Your project is configured to output an executable and generate a static Main method, but you have not configured any methods with the 'LambdaFunction' attribute",
Expand All @@ -117,5 +117,26 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor ApiParametersOnNonApiFunction = new DiagnosticDescriptor(id: "AWSLambda0115",
title: "Invalid Usage of API Parameters",
messageFormat: "The lambda function parameters are annotated with HTTP API attributes but the lambda function itself is not annotated with an HTTP API attribute",
96malhar marked this conversation as resolved.
Show resolved Hide resolved
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidSqsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0116",
title: "Invalid SQSEventAttribute",
messageFormat: "Invalid SQSEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidLambdaMethodSignature = new DiagnosticDescriptor(id: "AWSLambda0117",
title: "Invalid Lambda Method Signature",
messageFormat: "Invalid Lambda method signature encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
}
196 changes: 13 additions & 183 deletions Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.SourceGenerator.Templates;
using Amazon.Lambda.Annotations.SourceGenerator.Writers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Amazon.Lambda.Annotations.SourceGenerator
{
Expand Down Expand Up @@ -41,12 +37,6 @@ public class Generator : ISourceGenerator
"dotnet8"
};

// Only allow alphanumeric characters
private readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$");

// Regex for the 'Name' property for API Gateway attributes - https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html
private readonly Regex _parameterAttributeNameRegex = new Regex("^[a-zA-Z0-9._$-]+$");

public Generator()
{
#if DEBUG
Expand Down Expand Up @@ -144,106 +134,46 @@ public void Execute(GeneratorExecutionContext context)
}
}

var configureMethodModel = semanticModelProvider.GetConfigureMethodModel(receiver.StartupClasses.FirstOrDefault());
var configureMethodSymbol = semanticModelProvider.GetConfigureMethodModel(receiver.StartupClasses.FirstOrDefault());

var annotationReport = new AnnotationReport();

var templateHandler = new CloudFormationTemplateHandler(_fileManager, _directoryManager);

var lambdaModels = new List<LambdaFunctionModel>();

foreach (var lambdaMethod in receiver.LambdaMethods)
foreach (var lambdaMethodDeclarationSyntax in receiver.LambdaMethods)
{
var lambdaMethodModel = semanticModelProvider.GetMethodSemanticModel(lambdaMethod);
var lambdaMethodSymbol = semanticModelProvider.GetMethodSemanticModel(lambdaMethodDeclarationSyntax);
var lambdaMethodLocation = lambdaMethodDeclarationSyntax.GetLocation();

if (!HasSerializerAttribute(context, lambdaMethodModel))
var lambdaFunctionModel = LambdaFunctionModelBuilder.BuildAndValidate(lambdaMethodSymbol, lambdaMethodLocation, configureMethodSymbol, context, isExecutable, defaultRuntime, diagnosticReporter);
if (!lambdaFunctionModel.IsValid)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingLambdaSerializer,
lambdaMethod.GetLocation()));

// If the model is not valid then skip it from further processing
foundFatalError = true;
continue;
}

// Check for necessary references
if (lambdaMethodModel.HasAttribute(context, TypeFullNames.RestApiAttribute)
|| lambdaMethodModel.HasAttribute(context, TypeFullNames.HttpApiAttribute))
{
// Check for arbitrary type from "Amazon.Lambda.APIGatewayEvents"
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies,
lambdaMethod.GetLocation(),
"Amazon.Lambda.APIGatewayEvents"));

foundFatalError = true;
continue;
}
}

var serializerInfo = GetSerializerInfoAttribute(context, lambdaMethodModel);

var model = LambdaFunctionModelBuilder.Build(lambdaMethodModel, configureMethodModel, context, isExecutable, serializerInfo, defaultRuntime);

// If there are more than one event, report them as errors
if (model.LambdaMethod.Events.Count > 1)
{
foreach (var attribute in lambdaMethodModel.GetAttributes().Where(attribute => TypeFullNames.Events.Contains(attribute.AttributeClass.ToDisplayString())))
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MultipleEventsNotSupported,
Location.Create(attribute.ApplicationSyntaxReference.SyntaxTree, attribute.ApplicationSyntaxReference.Span),
DiagnosticSeverity.Error));
}

foundFatalError = true;
// Skip multi-event lambda method from processing and check remaining lambda methods for diagnostics
continue;
}
if(model.LambdaMethod.ReturnsIHttpResults && !model.LambdaMethod.Events.Contains(EventType.API))
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpResultsOnNonApiFunction,
Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span),
DiagnosticSeverity.Error));

foundFatalError = true;
continue;
}

if (!_resourceNameRegex.IsMatch(model.ResourceName))
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidResourceName,
Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span),
DiagnosticSeverity.Error));

foundFatalError = true;
continue;
}

if (!AreLambdaMethodParametersValid(lambdaMethod, model, diagnosticReporter))
{
foundFatalError = true;
continue;
}

var template = new LambdaFunctionTemplate(model);
var template = new LambdaFunctionTemplate(lambdaFunctionModel);

string sourceText;
try
{
sourceText = template.TransformText().ToEnvironmentLineEndings();
context.AddSource($"{model.GeneratedMethod.ContainingType.Name}.g.cs", SourceText.From(sourceText, Encoding.UTF8, SourceHashAlgorithm.Sha256));
context.AddSource($"{lambdaFunctionModel.GeneratedMethod.ContainingType.Name}.g.cs", SourceText.From(sourceText, Encoding.UTF8, SourceHashAlgorithm.Sha256));
}
catch (Exception e) when (e is NotSupportedException || e is InvalidOperationException)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGenerationFailed, Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span), e.Message));
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGenerationFailed, Location.Create(lambdaMethodDeclarationSyntax.SyntaxTree, lambdaMethodDeclarationSyntax.Span), e.Message));
return;
}

// report every generated file to build output
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{model.GeneratedMethod.ContainingType.Name}.g.cs", sourceText));
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{lambdaFunctionModel.GeneratedMethod.ContainingType.Name}.g.cs", sourceText));

lambdaModels.Add(model);
annotationReport.LambdaFunctions.Add(model);
lambdaModels.Add(lambdaFunctionModel);
annotationReport.LambdaFunctions.Add(lambdaFunctionModel);
}

if (isExecutable)
Expand Down Expand Up @@ -348,110 +278,10 @@ private static ExecutableAssembly GenerateExecutableAssemblySource(
lambdaModels[0].LambdaMethod.ContainingNamespace);
}

private bool HasSerializerAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel)
{
return methodModel.ContainingAssembly.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute);
}

private LambdaSerializerInfo GetSerializerInfoAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel)
{
var serializerString = DEFAULT_LAMBDA_SERIALIZER;

ISymbol symbol = null;

// First check if method has the Lambda Serializer.
if (methodModel.HasAttribute(
context,
TypeFullNames.LambdaSerializerAttribute))
{
symbol = methodModel;
}
// Then check assembly
else if (methodModel.ContainingAssembly.HasAttribute(
context,
TypeFullNames.LambdaSerializerAttribute))
{
symbol = methodModel.ContainingAssembly;
}
// Else return the default serializer.
else
{
return new LambdaSerializerInfo(serializerString);
}

var attribute = symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass.Name == TypeFullNames.LambdaSerializerAttributeWithoutNamespace);

var serializerValue = attribute.ConstructorArguments.FirstOrDefault(kvp => kvp.Type.Name == nameof(Type)).Value;

if (serializerValue != null)
{
serializerString = serializerValue.ToString();
}

return new LambdaSerializerInfo(serializerString);
}

public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(_fileManager, _directoryManager));
}

private bool AreLambdaMethodParametersValid(MethodDeclarationSyntax declarationSyntax, LambdaFunctionModel model, DiagnosticReporter diagnosticReporter)
{
var isValid = true;
foreach (var parameter in model.LambdaMethod.Parameters)
{
if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute))
{
var fromQueryAttribute = parameter.Attributes.First(att => att.Type.FullName == TypeFullNames.FromQueryAttribute) as AttributeModel<APIGateway.FromQueryAttribute>;
// Use parameter name as key, if Name has not specified explicitly in the attribute definition.
var parameterKey = fromQueryAttribute?.Data?.Name ?? parameter.Name;

if (!parameter.Type.IsPrimitiveType() && !parameter.Type.IsPrimitiveEnumerableType())
{
isValid = false;
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.UnsupportedMethodParameterType,
Location.Create(declarationSyntax.SyntaxTree, declarationSyntax.Span),
parameterKey, parameter.Type.FullName));
}
}

foreach (var att in parameter.Attributes)
{
var parameterAttributeName = string.Empty;
switch (att.Type.FullName)
{
case TypeFullNames.FromQueryAttribute:
var fromQueryAttribute = (AttributeModel<APIGateway.FromQueryAttribute>)att;
parameterAttributeName = fromQueryAttribute.Data.Name;
break;

case TypeFullNames.FromRouteAttribute:
var fromRouteAttribute = (AttributeModel<APIGateway.FromRouteAttribute>)att;
parameterAttributeName = fromRouteAttribute.Data.Name;
break;

case TypeFullNames.FromHeaderAttribute:
var fromHeaderAttribute = (AttributeModel<APIGateway.FromHeaderAttribute>)att;
parameterAttributeName = fromHeaderAttribute.Data.Name;
break;

default:
break;
}

if (!string.IsNullOrEmpty(parameterAttributeName) && !_parameterAttributeNameRegex.IsMatch(parameterAttributeName))
{
isValid = false;
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidParameterAttributeName,
Location.Create(declarationSyntax.SyntaxTree, declarationSyntax.Span),
parameterAttributeName, parameter.Name));
}
}
}

return isValid;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
Expand Down Expand Up @@ -71,6 +72,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SQSEventAttribute), SymbolEqualityComparer.Default))
{
var data = SQSEventAttributeBuilder.Build(att);
model = new AttributeModel<SQSEventAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else
{
model = new AttributeModel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="SQSEventAttribute"/>.
/// </summary>
public class SQSEventAttributeBuilder
{
public static SQSEventAttribute Build(AttributeData att)
{
if (att.ConstructorArguments.Length != 1)
{
throw new NotSupportedException($"{TypeFullNames.SQSEventAttribute} must have constructor with 1 argument.");
}
var queue = att.ConstructorArguments[0].Value as string;
var data = new SQSEventAttribute(queue);

foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize)
{
data.BatchSize = batchSize;
}
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
{
data.Enabled = enabled;
}
else if (pair.Key == nameof(data.MaximumBatchingWindowInSeconds) && pair.Value.Value is uint maximumBatchingWindowInSeconds)
{
data.MaximumBatchingWindowInSeconds = maximumBatchingWindowInSeconds;
}
else if (pair.Key == nameof(data.Filters) && pair.Value.Value is string filters)
{
data.Filters = filters;
}
else if (pair.Key == nameof(data.MaximumConcurrency) && pair.Value.Value is uint maximumConcurrency)
{
data.MaximumConcurrency = maximumConcurrency;
}
}

return data;
}
}
}
Loading
Loading