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

#1571 Add annotations support for executable assemblies #1573

Merged
merged 31 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5c3927c
#1571 Add annotations support for executable assemblies
Aug 21, 2023
fe69112
Add support for serialization
jeastham1993 Aug 21, 2023
5ebbf64
Add support for serialization and update annotation name
Aug 22, 2023
c73b61c
Move serialization to constructor
Aug 22, 2023
2bb0c07
Merge branch 'aws:master' into master
jeastham1993 Aug 30, 2023
0093d53
Update existing static main error
Aug 30, 2023
e54ea0d
Use syntax receiver get declared symbol
Aug 30, 2023
9484ac1
Add check for method parameter count
Aug 30, 2023
68bebac
Update to use extension method
Aug 30, 2023
a90b6ce
Fix test unit tests
Aug 30, 2023
80cbcf9
Merge branch 'master' of https://github.com/jeastham1993/aws-lambda-d…
Sep 1, 2023
0b57758
Update attribute name and add Runtime option
jeastham1993 Sep 9, 2023
0207130
Update unit tests and documentation
Sep 11, 2023
d695a6c
Update tests and comments
jeastham1993 Oct 1, 2023
5604358
Update CfN writer to always set runtime
jeastham1993 Oct 1, 2023
47d87f9
Update serverless template
Oct 26, 2023
1ae33d3
Add diaganostic error and update SLN
Oct 26, 2023
01f2595
Merge branch 'master' of https://github.com/jeastham1993/aws-lambda-d…
Oct 26, 2023
ae09362
Fix issue with parameterless response
Oct 29, 2023
5034c30
Updates to handle responses with no parameters
Oct 29, 2023
9747536
Remove BAK file and update referenced libraries
Nov 2, 2023
2c71099
Add RuntimeSupport to solution filter
Nov 2, 2023
5692d98
Update HttpResults.cs (#1602)
t0mll Nov 7, 2023
c4a08ba
Update HttpResults.cs (#1603)
t0mll Nov 7, 2023
4092dbf
Update README
Nov 8, 2023
62e11c1
Add new diagnostics
Nov 9, 2023
1132f97
Update SourceGen tests to use static strings instead of files
Nov 9, 2023
d9d5b96
Update error messages
Nov 9, 2023
aaaa5a8
Update annotations design
Nov 9, 2023
c78c6d2
Resolve build issues
jeastham1993 Nov 11, 2023
c2e4304
Merge branch 'aws:master' into master
jeastham1993 Nov 11, 2023
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
7 changes: 7 additions & 0 deletions Libraries/Libraries.sln
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.LexV2Events",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest", "test\Amazon.Lambda.RuntimeSupport.Tests\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj", "{0BD83939-458C-4EF5-8663-7098AD1200F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestExecutableServerlessApp", "test\TestExecutableServerlessApp\TestExecutableServerlessApp.csproj", "{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5
Expand Down Expand Up @@ -353,6 +355,10 @@ Global
{0BD83939-458C-4EF5-8663-7098AD1200F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BD83939-458C-4EF5-8663-7098AD1200F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BD83939-458C-4EF5-8663-7098AD1200F2}.Release|Any CPU.Build.0 = Release|Any CPU
{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD378063-C54A-44C7-9A6F-32A6A1AE94B3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -415,6 +421,7 @@ Global
{BF85932E-2DFF-41CD-8090-A672468B8FBB} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
{3C6AABF5-0372-41E0-874F-DF18ECCC7FB6} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
{0BD83939-458C-4EF5-8663-7098AD1200F2} = {B5BD0336-7D08-492C-8489-42C987E29B39}
{DD378063-C54A-44C7-9A6F-32A6A1AE94B3} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
<Generator>TextTemplatingFilePreprocessor</Generator>
<LastGenOutput>NoEventMethodBody.cs</LastGenOutput>
</None>
<None Update="Templates\ExecutableAssembly.tt">
<Generator>TextTemplatingFilePreprocessor</Generator>
<LastGenOutput>ExecutableAssembly.cs</LastGenOutput>
</None>
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -84,6 +88,11 @@
<AutoGen>True</AutoGen>
<DependentUpon>NoEventMethodBody.tt</DependentUpon>
</Compile>
<Compile Update="Templates\ExecutableAssembly.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ExecutableAssembly.tt</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public static class DiagnosticDescriptors
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor MainMethodExists = new DiagnosticDescriptor(id: "AWSLambda0104",
ashovlin marked this conversation as resolved.
Show resolved Hide resolved
title: "static Main method exists",
messageFormat: "Failed to generate Main method for LambdaGenerateMainAttribute because project already contains Main method. Existing Main methods must be removed when using LambdaGenerateMainAttribute attribute.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There are 2 spaces in between LambdaGenerateMainAttribute and attribute at the end.

category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor HttpResultsOnNonApiFunction = new DiagnosticDescriptor(id: "AWSLambda0105",
title: $"Invalid return type {nameof(IHttpResult)}",
messageFormat: $"{nameof(IHttpResult)} is not a valid return type for LambdaFunctions without {nameof(HttpApiAttribute)} or {nameof(RestApiAttribute)} attributes",
Expand Down
120 changes: 119 additions & 1 deletion Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
Expand All @@ -15,9 +16,12 @@

namespace Amazon.Lambda.Annotations.SourceGenerator
{
using System.Collections.Generic;

[Generator]
public class Generator : ISourceGenerator
{
private const string DEFAULT_LAMBDA_SERIALIZER = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";
private readonly IFileManager _fileManager = new FileManager();
private readonly IDirectoryManager _directoryManager = new DirectoryManager();

Expand Down Expand Up @@ -80,13 +84,29 @@ public void Execute(GeneratorExecutionContext context)
}
}

var isExecutable = false;

var assemblyAttributes = context.Compilation.Assembly.GetAttributes();

// Let's find the AssemblyTitleAttribute
var generateMainAttribute = assemblyAttributes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Rename variable to globalPropertiesAttribute

.FirstOrDefault(attr => attr.AttributeClass.Name == nameof(LambdaGenerateMainAttribute));

if (generateMainAttribute != null)
{
isExecutable = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the context.Compilation.CommonOptions.OutputKind property to see if it is OutputKind.ConsoleApplication. Then write a diagnostic error. I forget to change this when I was converting the blueprint in Visual Studio to use this feature and had to figure out after deployment why I was getting an Internal Error which is not a great experience.

}

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

var annotationReport = new AnnotationReport();

var templateHandler = new CloudFormationTemplateHandler(_fileManager, _directoryManager);

bool foundFatalError = false;

var lambdaModels = new List<LambdaFunctionModel>();

foreach (var lambdaMethod in receiver.LambdaMethods)
{
var lambdaMethodModel = semanticModelProvider.GetMethodSemanticModel(lambdaMethod);
Expand Down Expand Up @@ -115,8 +135,10 @@ public void Execute(GeneratorExecutionContext context)
continue;
}
}

var serializerString = GetSerializerAttribute(context, lambdaMethodModel);

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

// If there are more than one event, report them as errors
if (model.LambdaMethod.Events.Count > 1)
Expand Down Expand Up @@ -175,9 +197,27 @@ public void Execute(GeneratorExecutionContext context)
// report every generated file to build output
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{model.GeneratedMethod.ContainingType.Name}.g.cs", sourceText));

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

if (isExecutable)
{
var executableAssembly = GenerateExecutableAssemblySource(
context,
diagnosticReporter,
receiver,
lambdaModels);

if (executableAssembly == null)
{
foundFatalError = true;
return;
}

context.AddSource("Program.g.cs", SourceText.From(executableAssembly.TransformText().ToEnvironmentLineEndings(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
}

// Run the CloudFormation sync if any LambdaMethods exists. Also run if no LambdaMethods exists but there is a
// CloudFormation template in case orphaned functions in the template need to be removed.
// Both checks are required because if there is no template but there are LambdaMethods the CF template the template will be created.
Expand Down Expand Up @@ -212,11 +252,89 @@ public void Execute(GeneratorExecutionContext context)
}
}

private static ExecutableAssembly GenerateExecutableAssemblySource(
GeneratorExecutionContext context,
DiagnosticReporter diagnosticReporter,
SyntaxReceiver receiver,
List<LambdaFunctionModel> lambdaModels)
{
// Check 'Amazon.Lambda.RuntimeSupport' is referenced
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.RuntimeSupport") == null)
{
diagnosticReporter.Report(
Diagnostic.Create(
DiagnosticDescriptors.MissingDependencies,
Location.None,
"Amazon.Lambda.RuntimeSupport"));

return null;
}

foreach (var methodDeclaration in receiver.MethodDeclarations)
{
var model = context.Compilation.GetSemanticModel(methodDeclaration.SyntaxTree);
var symbol = model.GetDeclaredSymbol(methodDeclaration) as IMethodSymbol;

// Check to see if a static main method exists in the same namespace that has 0 or 1 parameters
if (symbol.Name != "Main" || !symbol.IsStatic || symbol.ContainingNamespace.Name != lambdaModels[0].LambdaMethod.ContainingAssembly || (symbol.Parameters.Length > 1))
continue;

diagnosticReporter.Report(
Diagnostic.Create(
DiagnosticDescriptors.MainMethodExists,
Location.None));

return null;
}

return new ExecutableAssembly(
lambdaModels,
lambdaModels[0].LambdaMethod.ContainingNamespace);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is the code that is causing the bug that if there are no Lambda functions we give back the "AWSLambda001 This is a bug. Please run the build ..." compilation error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a specific Diagnostic error to handle this.

}

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

private string GetSerializerAttribute(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 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 serializerString;
}

public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ private static IList<string> BuildUsings(LambdaMethodModel lambdaMethodModel,
"System",
"System.Linq",
"System.Collections.Generic",
"System.Text"
"System.Text",
"System.Threading.Tasks",
"System.IO"
};

if (configureMethodSymbol != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public interface ILambdaFunctionSerializable
/// </para>
/// </summary>
string Handler { get; }

/// <summary>
/// <para>
/// The original method name.
/// </para>
/// </summary>
string MethodName { get; }

/// <summary>
/// The name of the CloudFormation resource that is associated with the Lambda function.
Expand All @@ -34,6 +41,11 @@ public interface ILambdaFunctionSerializable
/// The IAM Role assumed by the Lambda function during its execution.
/// </summary>
string Role { get; }

/// <summary>
/// Gets or sets if the output is an executable.
/// </summary>
bool IsExecutable { get; }

/// <summary>
/// Resource based policies that grants permissions to access other AWS resources.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,24 @@ public class LambdaFunctionModel : ILambdaFunctionSerializable
/// </summary>
public TypeModel StartupType { get; set; }

/// <summary>
/// The original method name.
/// </summary>
public string MethodName => LambdaMethod.Name;

/// <summary>
/// Gets or sets fully qualified name of the serializer used for serialization or deserialization.
/// </summary>
public string Serializer { get; set; }
public string Serializer { get; set; } =
"Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";

/// <summary>
/// Gets or sets if the output is an executable.
/// </summary>
public bool IsExecutable { get; set; }

/// <inheritdoc />
public string Handler => $"{LambdaMethod.ContainingAssembly}::{GeneratedMethod.ContainingType.FullName}::{LambdaMethod.Name}";
public string Handler => IsExecutable ? LambdaMethod.ContainingAssembly : $"{LambdaMethod.ContainingAssembly}::{GeneratedMethod.ContainingType.FullName}::{LambdaMethod.Name}";

/// <inheritdoc />
public string ResourceName => LambdaMethod.LambdaFunctionAttribute.Data.ResourceName ??
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models
Expand All @@ -8,19 +9,20 @@ namespace Amazon.Lambda.Annotations.SourceGenerator.Models
/// </summary>
public static class LambdaFunctionModelBuilder
{
public static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMethodSymbol configureMethodSymbol, GeneratorExecutionContext context)
public static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMethodSymbol configureMethodSymbol, GeneratorExecutionContext context, bool isExecutable, string serializer)
{
var lambdaMethod = LambdaMethodModelBuilder.Build(lambdaMethodSymbol, configureMethodSymbol, context);
var generatedMethod = GeneratedMethodModelBuilder.Build(lambdaMethodSymbol, configureMethodSymbol, lambdaMethod, context);
var model = new LambdaFunctionModel()
{
GeneratedMethod = generatedMethod,
LambdaMethod = lambdaMethod,
Serializer = "System.Text.Json.JsonSerializer", // TODO: replace serializer with assembly serializer
Serializer = serializer,
StartupType = configureMethodSymbol != null ? TypeModelBuilder.Build(configureMethodSymbol.ContainingType, context) : null,
SourceGeneratorVersion = context.Compilation
.ReferencedAssemblyNames.FirstOrDefault(x => string.Equals(x.Name, "Amazon.Lambda.Annotations"))
?.Version.ToString()
?.Version.ToString(),
IsExecutable = isExecutable
};

return model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@

namespace Amazon.Lambda.Annotations.SourceGenerator
{
using Microsoft.CodeAnalysis.CSharp;

internal class SyntaxReceiver : ISyntaxContextReceiver
{
public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();

public List<ClassDeclarationSyntax> StartupClasses { get; private set; } = new List<ClassDeclarationSyntax>();

public List<MethodDeclarationSyntax> MethodDeclarations { get; } = new List<MethodDeclarationSyntax>();

/// <summary>
/// Path to the directory containing the .csproj file
Expand Down Expand Up @@ -50,7 +54,9 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
if (context.Node is MethodDeclarationSyntax methodDeclarationSyntax && methodDeclarationSyntax.AttributeLists.Count > 0)
{
// Get the symbol being declared by the method, and keep it if its annotated
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax);
var methodSymbol = ModelExtensions.GetDeclaredSymbol(
context.SemanticModel,
methodDeclarationSyntax);
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass.Name == nameof(LambdaFunctionAttribute)))
{
LambdaMethods.Add(methodDeclarationSyntax);
Expand All @@ -62,11 +68,23 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
// Get the symbol being declared by the class, and keep it if its annotated
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);

if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass.Name == nameof(LambdaStartupAttribute)))
{
StartupClasses.Add(classDeclarationSyntax);
}
}

if (context.Node is MethodDeclarationSyntax methodDeclaration)
{
var model = context.SemanticModel.GetDeclaredSymbol(
methodDeclaration);

if (model.Name == "Main" && model.IsStatic)
{
MethodDeclarations.Add(methodDeclaration);
}
}
}
}
}
Loading