Skip to content

Commit b6b38c1

Browse files
[EventGrid] Incremental source generator (#49445)
* Using a more efficient incremental source generator * Cleanup visitor * Make node immutable * Indent * The code is self-explanatory * Only scan SystemEvents * Filter duplicates * Remove catch since there is enough validation to not require it * Avoid another cast * More straightforward namespace search * AppendIndentedLine --------- Co-authored-by: Daniel Marbach <[email protected]>
1 parent 94f2fd1 commit b6b38c1

File tree

4 files changed

+193
-111
lines changed

4 files changed

+193
-111
lines changed

sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/EventGridSourceGenerator.cs

Lines changed: 155 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
47
using System.Linq;
58
using System.Text;
9+
using System.Text.RegularExpressions;
610
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
712
using Microsoft.CodeAnalysis.Text;
813

914
namespace Azure.EventGrid.Messaging.SourceGeneration
@@ -13,38 +18,144 @@ namespace Azure.EventGrid.Messaging.SourceGeneration
1318
/// from constant values to deserialization method for each system event.
1419
/// </summary>
1520
[Generator]
16-
internal class EventGridSourceGenerator : ISourceGenerator
21+
internal class EventGridSourceGenerator : IIncrementalGenerator
1722
{
18-
private SourceVisitor _visitor;
19-
private bool _isSystemEventsLibrary;
2023
private const string Indent = " ";
2124

22-
public void Execute(GeneratorExecutionContext context)
25+
// the event name is either 3 or 4 parts, e.g. Microsoft.AppConfiguration.KeyValueDeleted or Microsoft.ResourceNotifications.HealthResources.AvailabilityStatusChanged
26+
private static readonly Regex EventTypeRegex = new("[a-zA-Z]+\\.[a-zA-Z]+\\.[a-zA-Z]+(\\.[a-zA-Z]+)?", RegexOptions.Compiled);
27+
28+
private static ReadOnlySpan<char> SummaryStartTag => "<summary>".AsSpan();
29+
private static ReadOnlySpan<char> SummaryEndTag => "</summary>".AsSpan();
30+
31+
public void Initialize(IncrementalGeneratorInitializationContext context)
2332
{
24-
_visitor = new SourceVisitor();
25-
_isSystemEventsLibrary = context.Compilation.AssemblyName == "Azure.Messaging.EventGrid.SystemEvents";
26-
var root = context.Compilation.GetSymbolsWithName(
27-
"SystemEvents",
28-
SymbolFilter.Namespace)
29-
.Single();
30-
_visitor.Visit(root);
31-
32-
context.AddSource("SystemEventNames.cs", SourceText.From(ConstructSystemEventNames(), Encoding.UTF8));
33-
context.AddSource("SystemEventExtensions.cs", SourceText.From(ConstructSystemEventExtensions(), Encoding.UTF8));
33+
// Get all class declarations that end with "EventData"
34+
var classDeclarations = context.SyntaxProvider
35+
.CreateSyntaxProvider(
36+
predicate: static (s, _) => s is ClassDeclarationSyntax cds && cds.Identifier.Text.EndsWith("EventData"),
37+
transform: static (ctx, cancellationToken) =>
38+
{
39+
var semanticModel = ctx.SemanticModel;
40+
var classDeclaration = (ClassDeclarationSyntax)ctx.Node;
41+
42+
var declaredSymbol = semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken);
43+
44+
return declaredSymbol?.ContainingNamespace is { Name: "SystemEvents" } ? classDeclaration : null;
45+
})
46+
.Where(static cls => cls != null);
47+
48+
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
49+
50+
// Generate the source
51+
context.RegisterSourceOutput(compilationAndClasses,
52+
static (SourceProductionContext sourceProductionContext, (Compilation Compilation, ImmutableArray<ClassDeclarationSyntax> ClassDeclarations) input) =>
53+
{
54+
Execute(sourceProductionContext, input.Compilation, input.ClassDeclarations);
55+
});
56+
}
57+
58+
private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes)
59+
{
60+
if (classes.IsDefaultOrEmpty)
61+
{
62+
return;
63+
}
64+
65+
var systemEventNodes = GetSystemEventNodes(compilation, classes);
66+
if (systemEventNodes.Count <= 0)
67+
{
68+
return;
69+
}
70+
71+
var isSystemEventsLibrary = compilation.AssemblyName == "Azure.Messaging.EventGrid.SystemEvents";
72+
73+
context.AddSource("SystemEventNames.cs", SourceText.From(ConstructSystemEventNames(systemEventNodes, isSystemEventsLibrary), Encoding.UTF8));
74+
context.AddSource("SystemEventExtensions.cs", SourceText.From(ConstructSystemEventExtensions(systemEventNodes, isSystemEventsLibrary), Encoding.UTF8));
75+
}
76+
77+
private static List<SystemEventNode> GetSystemEventNodes(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes)
78+
{
79+
var systemEventNodes = new List<SystemEventNode>();
80+
var eventTypeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
81+
82+
foreach (var classDeclaration in classes)
83+
{
84+
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
85+
if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
86+
{
87+
continue;
88+
}
89+
90+
var documentationCommentXml = classSymbol.GetDocumentationCommentXml();
91+
if (string.IsNullOrEmpty(documentationCommentXml))
92+
{
93+
continue;
94+
}
95+
96+
// Extract event type from documentation comments
97+
string eventType = ExtractEventTypeFromDocumentation(documentationCommentXml);
98+
if (string.IsNullOrEmpty(eventType))
99+
{
100+
// Skip if no event type is found (likely a base type)
101+
continue;
102+
}
103+
104+
if (!eventTypeSet.Add(eventType))
105+
{
106+
continue;
107+
}
108+
109+
// Find the deserialize method
110+
var deserializeMethod = classSymbol.GetMembers()
111+
.OfType<IMethodSymbol>()
112+
.FirstOrDefault(m => m.Name.StartsWith("Deserialize", StringComparison.Ordinal))?.Name;
113+
114+
if (deserializeMethod == null)
115+
{
116+
// Skip if no deserialize method is found
117+
continue;
118+
}
119+
120+
// Create a SystemEventNode for this event
121+
systemEventNodes.Add(new SystemEventNode(eventName: classSymbol.Name, eventType: $@"""{eventType}""", deserializeMethod: deserializeMethod));
122+
}
123+
124+
return systemEventNodes;
34125
}
35126

36-
public void Initialize(GeneratorInitializationContext context)
127+
private static string ExtractEventTypeFromDocumentation(string documentationCommentXml)
37128
{
38-
// Uncomment to debug
39-
//if (!Debugger.IsAttached)
40-
//{
41-
// Debugger.Launch();
42-
//}
129+
if (string.IsNullOrEmpty(documentationCommentXml))
130+
{
131+
return null;
132+
}
133+
134+
ReadOnlySpan<char> docSpan = documentationCommentXml.AsSpan();
135+
136+
int summaryStartIndex = docSpan.IndexOf(SummaryStartTag);
137+
if (summaryStartIndex < 0)
138+
{
139+
return null;
140+
}
141+
142+
summaryStartIndex += SummaryStartTag.Length;
143+
144+
int summaryEndIndex = docSpan.Slice(summaryStartIndex).IndexOf(SummaryEndTag);
145+
if (summaryEndIndex < 0)
146+
{
147+
return null;
148+
}
149+
150+
var summaryContent = docSpan.Slice(summaryStartIndex, summaryEndIndex);
151+
152+
var match = EventTypeRegex.Match(summaryContent.ToString());
153+
return match.Success ? match.Value : null;
43154
}
44155

45-
private string ConstructSystemEventNames()
156+
private static string ConstructSystemEventNames(List<SystemEventNode> systemEvents, bool isSystemEventsLibrary)
46157
{
47-
string ns = _isSystemEventsLibrary ? "Azure.Messaging.EventGrid.SystemEvents" : "Azure.Messaging.EventGrid";
158+
string ns = isSystemEventsLibrary ? "Azure.Messaging.EventGrid.SystemEvents" : "Azure.Messaging.EventGrid";
48159
var sourceBuilder = new StringBuilder(
49160
$@"// Copyright (c) Microsoft Corporation. All rights reserved.
50161
// Licensed under the MIT License.
@@ -62,34 +173,34 @@ namespace {ns}
62173
public static class SystemEventNames
63174
{{
64175
");
65-
for (int i = 0; i < _visitor.SystemEvents.Count; i++)
176+
for (int i = 0; i < systemEvents.Count; i++)
66177
{
67178
if (i > 0)
68179
{
69180
sourceBuilder.AppendLine();
70181
}
71-
SystemEventNode sysEvent = _visitor.SystemEvents[i];
182+
SystemEventNode sysEvent = systemEvents[i];
72183

73184
// Add the ref docs for each constant
74-
sourceBuilder.AppendLine($"{Indent}{Indent}/// <summary>");
75-
sourceBuilder.AppendLine(
76-
!_isSystemEventsLibrary
77-
? $"{Indent}{Indent}/// The value of the Event Type stored in <see cref=\"EventGridEvent.EventType\"/> and <see cref=\"CloudEvent.Type\"/> "
78-
: $"{Indent}{Indent}/// The value of the Event Type stored in <see cref=\"CloudEvent.Type\"/> ");
185+
sourceBuilder.AppendIndentedLine(2, "/// <summary>");
186+
sourceBuilder.AppendIndentedLine(2,
187+
!isSystemEventsLibrary
188+
? "/// The value of the Event Type stored in <see cref=\"EventGridEvent.EventType\"/> and <see cref=\"CloudEvent.Type\"/> "
189+
: "/// The value of the Event Type stored in <see cref=\"CloudEvent.Type\"/> ");
79190

80-
sourceBuilder.AppendLine($"{Indent}{Indent}/// for the <see cref=\"{sysEvent.EventName}\"/> system event.");
81-
sourceBuilder.AppendLine($"{Indent}{Indent}/// </summary>");
191+
sourceBuilder.AppendIndentedLine(2, $"/// for the <see cref=\"{sysEvent.EventName}\"/> system event.");
192+
sourceBuilder.AppendIndentedLine(2, "/// </summary>");
82193

83194
// Add the constant
84-
sourceBuilder.AppendLine($"{Indent}{Indent}public const string {sysEvent.EventConstantName} = {sysEvent.EventType};");
195+
sourceBuilder.AppendIndentedLine(2, $"public const string {sysEvent.EventConstantName} = {sysEvent.EventType};");
85196
}
86197

87-
sourceBuilder.Append($@"{Indent}}}
88-
}}");
198+
sourceBuilder.AppendIndentedLine(1, @"}
199+
}");
89200
return sourceBuilder.ToString();
90201
}
91202

92-
private string ConstructSystemEventExtensions()
203+
private static string ConstructSystemEventExtensions(List<SystemEventNode> systemEvents, bool isSystemEventsLibrary)
93204
{
94205
var sourceBuilder = new StringBuilder(
95206
$@"// Copyright (c) Microsoft Corporation. All rights reserved.
@@ -101,7 +212,7 @@ private string ConstructSystemEventExtensions()
101212
using System.Collections.Generic;
102213
using System.Text.Json;
103214
using Azure.Messaging.EventGrid.SystemEvents;
104-
{(_isSystemEventsLibrary ? "using System.ClientModel.Primitives;" : string.Empty)}
215+
{(isSystemEventsLibrary ? "using System.ClientModel.Primitives;" : string.Empty)}
105216
106217
namespace Azure.Messaging.EventGrid
107218
{{
@@ -111,17 +222,17 @@ public static object AsSystemEventData(string eventType, JsonElement data)
111222
{{
112223
var eventTypeSpan = eventType.AsSpan();
113224
");
114-
foreach (SystemEventNode sysEvent in _visitor.SystemEvents)
225+
foreach (SystemEventNode sysEvent in systemEvents)
115226
{
116227
// Add each an entry for each system event to the dictionary containing a mapping from constant name to deserialization method.
117-
sourceBuilder.AppendLine(
118-
$"{Indent}{Indent}{Indent}if (eventTypeSpan.Equals(SystemEventNames.{sysEvent.EventConstantName}.AsSpan(), StringComparison.OrdinalIgnoreCase))");
119-
sourceBuilder.AppendLine(
120-
$"{Indent}{Indent}{Indent}{Indent}return {sysEvent.EventName}.{sysEvent.DeserializeMethod}(data{(_isSystemEventsLibrary ? ", null" : string.Empty)});");
228+
sourceBuilder.AppendIndentedLine(3,
229+
$"if (eventTypeSpan.Equals(SystemEventNames.{sysEvent.EventConstantName}.AsSpan(), StringComparison.OrdinalIgnoreCase))");
230+
sourceBuilder.AppendIndentedLine(4,
231+
$"return {sysEvent.EventName}.{sysEvent.DeserializeMethod}(data{(isSystemEventsLibrary ? ", null" : string.Empty)});");
121232
}
122-
sourceBuilder.AppendLine($"{Indent}{Indent}{Indent}return null;");
123-
sourceBuilder.AppendLine($"{Indent}{Indent}}}");
124-
sourceBuilder.AppendLine($"{Indent}}}");
233+
sourceBuilder.AppendIndentedLine(3, "return null;");
234+
sourceBuilder.AppendIndentedLine(2, "}");
235+
sourceBuilder.AppendIndentedLine(1, "}");
125236
sourceBuilder.AppendLine("}");
126237

127238
return sourceBuilder.ToString();

sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SourceVisitor.cs

Lines changed: 0 additions & 52 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Text;
5+
6+
namespace Azure.EventGrid.Messaging.SourceGeneration
7+
{
8+
internal static class StringBuilderExtensions
9+
{
10+
public static void AppendIndentedLine(this StringBuilder sb, int indentLevel, string text)
11+
{
12+
sb.Append(' ', indentLevel * 4);
13+
sb.AppendLine(text);
14+
}
15+
}
16+
}

sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SystemEventNode.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,34 @@
33

44
namespace Azure.EventGrid.Messaging.SourceGeneration
55
{
6-
internal class SystemEventNode
6+
internal sealed class SystemEventNode
77
{
8-
public string EventName { get; set; }
8+
public SystemEventNode(string eventName, string eventType, string deserializeMethod)
9+
{
10+
EventName = eventName;
11+
EventType = eventType;
12+
DeserializeMethod = deserializeMethod;
13+
EventConstantName = Convert(EventName);
14+
}
915

10-
public string EventConstantName
16+
private static string Convert(string eventName)
1117
{
12-
get
18+
// special case a few events that don't follow the pattern
19+
return eventName switch
1320
{
14-
// special case a few events that don't follow the pattern
15-
return EventName switch
16-
{
17-
"ServiceBusDeadletterMessagesAvailableWithNoListenersEventData" => "ServiceBusDeadletterMessagesAvailableWithNoListener",
18-
"SubscriptionDeletedEventData" => "EventGridSubscriptionDeleted",
19-
"SubscriptionValidationEventData" => "EventGridSubscriptionValidation",
20-
_ => EventName?.Replace("EventData", ""),
21-
};
22-
}
21+
"ServiceBusDeadletterMessagesAvailableWithNoListenersEventData" => "ServiceBusDeadletterMessagesAvailableWithNoListener",
22+
"SubscriptionDeletedEventData" => "EventGridSubscriptionDeleted",
23+
"SubscriptionValidationEventData" => "EventGridSubscriptionValidation",
24+
_ => eventName?.Replace("EventData", ""),
25+
};
2326
}
2427

25-
public string EventType { get; set; }
28+
public string EventName { get; }
29+
30+
public string EventConstantName { get; }
31+
32+
public string EventType { get; }
2633

27-
public string DeserializeMethod { get; set; }
34+
public string DeserializeMethod { get; }
2835
}
2936
}

0 commit comments

Comments
 (0)