Skip to content

Commit cca4e74

Browse files
Add AWSLambda0145 diagnostic for unregistered durable envelope types (#2464)
When a [DurableExecution] function registers the source-generator serializer (SourceGeneratorLambdaJsonSerializer<TContext>), the durable invocation envelope types DurableExecutionInvocationInput and DurableExecutionInvocationOutput must be registered on the JsonSerializerContext with [JsonSerializable]. Unlike the reflection-based DefaultLambdaJsonSerializer, the context only serializes types explicitly registered, so a missing registration fails only at invocation time. The generator now emits AWSLambda0145 (Warning) at build time when either envelope type is missing.
1 parent 83e48e4 commit cca4e74

6 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.Annotations",
5+
"Type": "Minor",
6+
"ChangelogMessages": [
7+
"Add diagnostic AWSLambda0145 (Warning) for durable execution functions. When a [DurableExecution] function registers the source-generator serializer (SourceGeneratorLambdaJsonSerializer<TContext>), the durable invocation envelope types DurableExecutionInvocationInput and DurableExecutionInvocationOutput must be registered on the JsonSerializerContext with [JsonSerializable]. The generator now warns at build time when either is missing, instead of the function failing at invocation time. Preview."
8+
]
9+
}
10+
]
11+
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ AWSLambda0139 | AWSLambdaCSharpGenerator | Error | Invalid ScheduleEventAttribut
2727
AWSLambda0142 | AWSLambdaCSharpGenerator | Error | Invalid DurableExecution method signature
2828
AWSLambda0143 | AWSLambdaCSharpGenerator | Info | DurableExecution function with explicit Role needs checkpoint permissions
2929
AWSLambda0144 | AWSLambdaCSharpGenerator | Error | Invalid DurableExecutionAttribute
30+
AWSLambda0145 | AWSLambdaCSharpGenerator | Warning | Durable execution envelope types not registered with JsonSerializerContext

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,5 +323,12 @@ public static class DiagnosticDescriptors
323323
category: "AWSLambdaCSharpGenerator",
324324
DiagnosticSeverity.Error,
325325
isEnabledByDefault: true);
326+
327+
public static readonly DiagnosticDescriptor DurableExecutionMissingSerializableEnvelope = new DiagnosticDescriptor(id: "AWSLambda0145",
328+
title: "Durable execution envelope types not registered with JsonSerializerContext",
329+
messageFormat: "The [DurableExecution] function uses SourceGeneratorLambdaJsonSerializer<{0}>, but {0} does not register {1} with a [JsonSerializable] attribute. The durable runtime serializes the invocation envelope with this context, so add [JsonSerializable(typeof({1}))] to {0} or the function will fail at invocation time.",
330+
category: "AWSLambdaCSharpGenerator",
331+
DiagnosticSeverity.Warning,
332+
isEnabledByDefault: true);
326333
}
327334
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ public static class TypeFullNames
8080
public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
8181
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";
8282

83+
// The source-generator serializer is generic: SourceGeneratorLambdaJsonSerializer<TContext> where
84+
// TContext is the user's JsonSerializerContext. Compared against INamedTypeSymbol.ConstructedFrom by
85+
// metadata name (the `1 arity suffix is part of the metadata name for a generic type).
86+
public const string SourceGeneratorLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.SourceGeneratorLambdaJsonSerializer`1";
87+
public const string JsonSerializableAttribute = "System.Text.Json.Serialization.JsonSerializableAttribute";
88+
8389
public const string LambdaSerializerAttributeWithoutNamespace = "LambdaSerializerAttribute";
8490

8591
public static HashSet<string> ApiGatewayRequests = new HashSet<string>

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,79 @@ private static void ValidateDurableExecution(GeneratorExecutionContext context,
118118
{
119119
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.DurableExecutionExplicitRoleNeedsCheckpointPolicy, methodLocation));
120120
}
121+
122+
// When the registered serializer is the source-generator serializer, the durable invocation
123+
// envelope (DurableExecutionInvocationInput / DurableExecutionInvocationOutput) is (de)serialized
124+
// through the user's JsonSerializerContext. Unlike the reflection-based DefaultLambdaJsonSerializer,
125+
// the context only handles types explicitly registered via [JsonSerializable]. If the envelope types
126+
// are missing, the failure only surfaces at invocation time, so warn about it at build time here.
127+
ValidateDurableExecutionSerializerContext(context, lambdaMethodSymbol, methodLocation, diagnostics);
128+
}
129+
130+
private static void ValidateDurableExecutionSerializerContext(GeneratorExecutionContext context, IMethodSymbol lambdaMethodSymbol, Location methodLocation, List<Diagnostic> diagnostics)
131+
{
132+
// The serializer is registered via [assembly: LambdaSerializer(typeof(...))] or the same attribute on
133+
// the method. The method-level attribute takes precedence, matching GetSerializerInfoAttribute.
134+
var serializerAttribute = lambdaMethodSymbol.GetAttributeData(context, TypeFullNames.LambdaSerializerAttribute)
135+
?? lambdaMethodSymbol.ContainingAssembly.GetAttributeData(context, TypeFullNames.LambdaSerializerAttribute);
136+
if (serializerAttribute == null)
137+
{
138+
return;
139+
}
140+
141+
// The single constructor argument is the serializer Type. Only the source-generator serializer,
142+
// SourceGeneratorLambdaJsonSerializer<TContext>, routes serialization through a JsonSerializerContext.
143+
// Any other serializer (e.g. the reflection-based DefaultLambdaJsonSerializer) needs no registration.
144+
if (!(serializerAttribute.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is INamedTypeSymbol serializerType))
145+
{
146+
return;
147+
}
148+
149+
var sourceGeneratorSerializerSymbol = context.Compilation.GetTypeByMetadataName(TypeFullNames.SourceGeneratorLambdaSerializer);
150+
if (sourceGeneratorSerializerSymbol == null
151+
|| !SymbolEqualityComparer.Default.Equals(serializerType.ConstructedFrom?.OriginalDefinition, sourceGeneratorSerializerSymbol)
152+
|| serializerType.TypeArguments.Length != 1
153+
|| !(serializerType.TypeArguments[0] is INamedTypeSymbol contextType))
154+
{
155+
return;
156+
}
157+
158+
// Collect every type registered on the context via [JsonSerializable(typeof(T))], walking base
159+
// contexts too since [JsonSerializable] attributes are inherited across a context hierarchy.
160+
var jsonSerializableAttributeSymbol = context.Compilation.GetTypeByMetadataName(TypeFullNames.JsonSerializableAttribute);
161+
var registeredTypes = new HashSet<string>();
162+
for (var current = contextType; current != null; current = current.BaseType)
163+
{
164+
foreach (var att in current.GetAttributes())
165+
{
166+
if (!SymbolEqualityComparer.Default.Equals(att.AttributeClass, jsonSerializableAttributeSymbol))
167+
{
168+
continue;
169+
}
170+
171+
if (att.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is INamedTypeSymbol registeredType)
172+
{
173+
registeredTypes.Add(registeredType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
174+
}
175+
}
176+
}
177+
178+
var contextDisplayName = contextType.ToDisplayString();
179+
foreach (var envelopeTypeName in new[] { TypeFullNames.DurableExecutionInvocationInput, TypeFullNames.DurableExecutionInvocationOutput })
180+
{
181+
var envelopeTypeSymbol = context.Compilation.GetTypeByMetadataName(envelopeTypeName);
182+
// If the durable envelope type cannot be resolved the durable package is not referenced; other
183+
// diagnostics/compile errors will surface that, so skip rather than emit a misleading warning.
184+
if (envelopeTypeSymbol == null)
185+
{
186+
continue;
187+
}
188+
189+
if (!registeredTypes.Contains(envelopeTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
190+
{
191+
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.DurableExecutionMissingSerializableEnvelope, methodLocation, contextDisplayName, envelopeTypeName));
192+
}
193+
}
121194
}
122195

123196
private static void ValidateDurableExecutionSignature(IMethodSymbol lambdaMethodSymbol, LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Testing;
8+
using Xunit;
9+
using VerifyCS = Amazon.Lambda.Annotations.SourceGenerators.Tests.CSharpSourceGeneratorVerifier<Amazon.Lambda.Annotations.SourceGenerator.Generator>;
10+
11+
namespace Amazon.Lambda.Annotations.SourceGenerators.Tests
12+
{
13+
/// <summary>
14+
/// Tests for AWSLambda0145: when a [DurableExecution] function registers the source-generator
15+
/// serializer (SourceGeneratorLambdaJsonSerializer&lt;TContext&gt;), the durable invocation envelope
16+
/// types must be registered on TContext with [JsonSerializable], otherwise serialization fails at
17+
/// invocation time.
18+
/// </summary>
19+
public class DurableExecutionSerializerContextDiagnosticsTests
20+
{
21+
// Minimal durable SDK stubs. The real Amazon.Lambda.DurableExecution package cannot be referenced
22+
// by this test project (its AWSSDK.Core 4.x conflicts with the 3.7.x pin required by the generator
23+
// test framework), so the types the generator resolves by metadata name are supplied as source.
24+
private const string DurableStubs = @"
25+
namespace Amazon.Lambda.DurableExecution
26+
{
27+
public interface IDurableContext { }
28+
public sealed class DurableExecutionInvocationInput { }
29+
public sealed class DurableExecutionInvocationOutput { }
30+
}
31+
";
32+
33+
private static async Task<string> AnnotationsSource(string fileName) =>
34+
await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", fileName));
35+
36+
// The user source registers the serializer itself (via [assembly: LambdaSerializer]) so the default
37+
// DefaultLambdaJsonSerializer registration is intentionally not added here.
38+
private static async Task<VerifyCS.Test> NewTestAsync(string userSource)
39+
{
40+
var test = new VerifyCS.Test
41+
{
42+
TestState =
43+
{
44+
OutputKind = OutputKind.ConsoleApplication,
45+
Sources =
46+
{
47+
("Workflow.cs", userSource),
48+
("DurableStubs.cs", DurableStubs),
49+
},
50+
}
51+
};
52+
test.TestState.Sources.Add((Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await AnnotationsSource("LambdaFunctionAttribute.cs")));
53+
test.TestState.Sources.Add((Path.Combine("Amazon.Lambda.Annotations", "DurableExecutionAttribute.cs"), await AnnotationsSource("DurableExecutionAttribute.cs")));
54+
55+
// Generation proceeds (Warning severity does not halt it); ignore the per-file codegen Info
56+
// (AWSLambda0103), the generated-source list, and compiler errors from the deliberately
57+
// unimplemented JsonSerializerContext (only the generator's symbol resolution matters here).
58+
test.DisabledDiagnostics.Add("AWSLambda0103");
59+
test.TestBehaviors |= TestBehaviors.SkipGeneratedSourcesCheck;
60+
test.CompilerDiagnostics = CompilerDiagnostics.None;
61+
return test;
62+
}
63+
64+
// Builds a workflow that registers SourceGeneratorLambdaJsonSerializer<MyContext>, where MyContext
65+
// registers whichever envelope types are passed in via extraJsonSerializable.
66+
private static string WorkflowSource(string extraJsonSerializable) => $@"
67+
using System.Threading.Tasks;
68+
using System.Text.Json.Serialization;
69+
using Amazon.Lambda.Annotations;
70+
using Amazon.Lambda.Core;
71+
using Amazon.Lambda.DurableExecution;
72+
using Amazon.Lambda.Serialization.SystemTextJson;
73+
74+
[assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer<MyApp.MyContext>))]
75+
76+
namespace MyApp
77+
{{
78+
{extraJsonSerializable}
79+
public partial class MyContext : JsonSerializerContext {{ }}
80+
81+
public class Workflows
82+
{{
83+
[LambdaFunction]
84+
[DurableExecution(executionTimeout: 300)]
85+
public Task<string> Run(string input, IDurableContext ctx) => Task.FromResult(input);
86+
}}
87+
}}";
88+
89+
[Fact]
90+
public async Task BothEnvelopeTypesRegistered_NoDiagnostic()
91+
{
92+
var source = WorkflowSource(
93+
" [JsonSerializable(typeof(DurableExecutionInvocationInput))]\n" +
94+
" [JsonSerializable(typeof(DurableExecutionInvocationOutput))]");
95+
var test = await NewTestAsync(source);
96+
// No AWSLambda0145 expected.
97+
await test.RunAsync();
98+
}
99+
100+
[Fact]
101+
public async Task OutputEnvelopeTypeMissing_ReportsWarning()
102+
{
103+
var source = WorkflowSource(
104+
" [JsonSerializable(typeof(DurableExecutionInvocationInput))]");
105+
var test = await NewTestAsync(source);
106+
test.TestState.ExpectedDiagnostics.Add(
107+
new DiagnosticResult("AWSLambda0145", DiagnosticSeverity.Warning)
108+
.WithSpan("Workflow.cs", 18, 9, 20, 94)
109+
.WithArguments("MyApp.MyContext", "Amazon.Lambda.DurableExecution.DurableExecutionInvocationOutput"));
110+
await test.RunAsync();
111+
}
112+
113+
[Fact]
114+
public async Task BothEnvelopeTypesMissing_ReportsTwoWarnings()
115+
{
116+
var source = WorkflowSource(string.Empty);
117+
var test = await NewTestAsync(source);
118+
test.TestState.ExpectedDiagnostics.Add(
119+
new DiagnosticResult("AWSLambda0145", DiagnosticSeverity.Warning)
120+
.WithSpan("Workflow.cs", 18, 9, 20, 94)
121+
.WithArguments("MyApp.MyContext", "Amazon.Lambda.DurableExecution.DurableExecutionInvocationInput"));
122+
test.TestState.ExpectedDiagnostics.Add(
123+
new DiagnosticResult("AWSLambda0145", DiagnosticSeverity.Warning)
124+
.WithSpan("Workflow.cs", 18, 9, 20, 94)
125+
.WithArguments("MyApp.MyContext", "Amazon.Lambda.DurableExecution.DurableExecutionInvocationOutput"));
126+
await test.RunAsync();
127+
}
128+
129+
[Fact]
130+
public async Task DefaultSerializer_NoDiagnostic()
131+
{
132+
// The reflection-based DefaultLambdaJsonSerializer needs no [JsonSerializable] registration,
133+
// so the durable envelope check does not apply and AWSLambda0145 is never emitted.
134+
var source = @"
135+
using System.Threading.Tasks;
136+
using Amazon.Lambda.Annotations;
137+
using Amazon.Lambda.Core;
138+
using Amazon.Lambda.DurableExecution;
139+
140+
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
141+
142+
namespace MyApp
143+
{
144+
public class Workflows
145+
{
146+
[LambdaFunction]
147+
[DurableExecution(executionTimeout: 300)]
148+
public Task<string> Run(string input, IDurableContext ctx) => Task.FromResult(input);
149+
}
150+
}";
151+
var test = await NewTestAsync(source);
152+
await test.RunAsync();
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)