Skip to content

Commit

Permalink
feat: Add support for SQS events in Amazon.Lambda.Annotations (#1758)
Browse files Browse the repository at this point in the history
  • Loading branch information
96malhar committed Jun 17, 2024
1 parent 3ef0bc9 commit f2c3c07
Show file tree
Hide file tree
Showing 52 changed files with 2,480 additions and 306 deletions.
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
Expand Up @@ -4,6 +4,11 @@
<AssemblyVersion>1.4.0</AssemblyVersion>
<TargetFramework>netstandard2.0</TargetFramework>

<!--This assembly needs to access internal methods inside the Amazon.Lambda.Annotations assembly.
Both these assemblies need to be strongly signed for the InternalsVisibleTo attribute to take effect.-->
<AssemblyOriginatorKeyFile>..\..\..\buildtools\public.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>

<!-- This is required to allow copying all the dependencies to bin directory which can be copied after to nuget package based on nuspec -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
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",
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
Loading

0 comments on commit f2c3c07

Please sign in to comment.