Skip to content

[EventGrid] Incremental source generator #49445

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

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

public void Execute(GeneratorExecutionContext context)
// the event name is either 3 or 4 parts, e.g. Microsoft.AppConfiguration.KeyValueDeleted or Microsoft.ResourceNotifications.HealthResources.AvailabilityStatusChanged
private static readonly Regex EventTypeRegex = new("[a-zA-Z]+\\.[a-zA-Z]+\\.[a-zA-Z]+(\\.[a-zA-Z]+)?", RegexOptions.Compiled);

private static ReadOnlySpan<char> SummaryStartTag => "<summary>".AsSpan();
private static ReadOnlySpan<char> SummaryEndTag => "</summary>".AsSpan();

public void Initialize(IncrementalGeneratorInitializationContext context)
{
_visitor = new SourceVisitor();
_isSystemEventsLibrary = context.Compilation.AssemblyName == "Azure.Messaging.EventGrid.SystemEvents";
var root = context.Compilation.GetSymbolsWithName(
"SystemEvents",
SymbolFilter.Namespace)
.Single();
_visitor.Visit(root);

context.AddSource("SystemEventNames.cs", SourceText.From(ConstructSystemEventNames(), Encoding.UTF8));
context.AddSource("SystemEventExtensions.cs", SourceText.From(ConstructSystemEventExtensions(), Encoding.UTF8));
// Get all class declarations that end with "EventData"
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is ClassDeclarationSyntax cds && cds.Identifier.Text.EndsWith("EventData"),
transform: static (ctx, cancellationToken) =>
{
var semanticModel = ctx.SemanticModel;
var classDeclaration = (ClassDeclarationSyntax)ctx.Node;

var declaredSymbol = semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken);

return declaredSymbol?.ContainingNamespace is { Name: "SystemEvents" } ? classDeclaration : null;
})
.Where(static cls => cls != null);

var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());

// Generate the source
context.RegisterSourceOutput(compilationAndClasses,
static (SourceProductionContext sourceProductionContext, (Compilation Compilation, ImmutableArray<ClassDeclarationSyntax> ClassDeclarations) input) =>
{
Execute(sourceProductionContext, input.Compilation, input.ClassDeclarations);
});
}

private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes)
{
if (classes.IsDefaultOrEmpty)
{
return;
}

var systemEventNodes = GetSystemEventNodes(compilation, classes);
if (systemEventNodes.Count <= 0)
{
return;
}

var isSystemEventsLibrary = compilation.AssemblyName == "Azure.Messaging.EventGrid.SystemEvents";

context.AddSource("SystemEventNames.cs", SourceText.From(ConstructSystemEventNames(systemEventNodes, isSystemEventsLibrary), Encoding.UTF8));
context.AddSource("SystemEventExtensions.cs", SourceText.From(ConstructSystemEventExtensions(systemEventNodes, isSystemEventsLibrary), Encoding.UTF8));
}

private static List<SystemEventNode> GetSystemEventNodes(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes)
{
var systemEventNodes = new List<SystemEventNode>();
var eventTypeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var classDeclaration in classes)
{
var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
{
continue;
}

var documentationCommentXml = classSymbol.GetDocumentationCommentXml();
if (string.IsNullOrEmpty(documentationCommentXml))
{
continue;
}

// Extract event type from documentation comments
string eventType = ExtractEventTypeFromDocumentation(documentationCommentXml);
if (string.IsNullOrEmpty(eventType))
{
// Skip if no event type is found (likely a base type)
continue;
}

if (!eventTypeSet.Add(eventType))
{
continue;
}

// Find the deserialize method
var deserializeMethod = classSymbol.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.Name.StartsWith("Deserialize", StringComparison.Ordinal))?.Name;

if (deserializeMethod == null)
{
// Skip if no deserialize method is found
continue;
}

// Create a SystemEventNode for this event
systemEventNodes.Add(new SystemEventNode(eventName: classSymbol.Name, eventType: $@"""{eventType}""", deserializeMethod: deserializeMethod));
}

return systemEventNodes;
}

public void Initialize(GeneratorInitializationContext context)
private static string ExtractEventTypeFromDocumentation(string documentationCommentXml)
{
// Uncomment to debug
//if (!Debugger.IsAttached)
//{
// Debugger.Launch();
//}
if (string.IsNullOrEmpty(documentationCommentXml))
{
return null;
}

ReadOnlySpan<char> docSpan = documentationCommentXml.AsSpan();

int summaryStartIndex = docSpan.IndexOf(SummaryStartTag);
if (summaryStartIndex < 0)
{
return null;
}

summaryStartIndex += SummaryStartTag.Length;

int summaryEndIndex = docSpan.Slice(summaryStartIndex).IndexOf(SummaryEndTag);
if (summaryEndIndex < 0)
{
return null;
}

var summaryContent = docSpan.Slice(summaryStartIndex, summaryEndIndex);

var match = EventTypeRegex.Match(summaryContent.ToString());
return match.Success ? match.Value : null;
}

private string ConstructSystemEventNames()
private static string ConstructSystemEventNames(List<SystemEventNode> systemEvents, bool isSystemEventsLibrary)
{
string ns = _isSystemEventsLibrary ? "Azure.Messaging.EventGrid.SystemEvents" : "Azure.Messaging.EventGrid";
string ns = isSystemEventsLibrary ? "Azure.Messaging.EventGrid.SystemEvents" : "Azure.Messaging.EventGrid";
var sourceBuilder = new StringBuilder(
$@"// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
Expand All @@ -62,34 +173,34 @@ namespace {ns}
public static class SystemEventNames
{{
");
for (int i = 0; i < _visitor.SystemEvents.Count; i++)
for (int i = 0; i < systemEvents.Count; i++)
{
if (i > 0)
{
sourceBuilder.AppendLine();
}
SystemEventNode sysEvent = _visitor.SystemEvents[i];
SystemEventNode sysEvent = systemEvents[i];

// Add the ref docs for each constant
sourceBuilder.AppendLine($"{Indent}{Indent}/// <summary>");
sourceBuilder.AppendLine(
!_isSystemEventsLibrary
? $"{Indent}{Indent}/// The value of the Event Type stored in <see cref=\"EventGridEvent.EventType\"/> and <see cref=\"CloudEvent.Type\"/> "
: $"{Indent}{Indent}/// The value of the Event Type stored in <see cref=\"CloudEvent.Type\"/> ");
sourceBuilder.AppendIndentedLine(2, "/// <summary>");
sourceBuilder.AppendIndentedLine(2,
!isSystemEventsLibrary
? "/// The value of the Event Type stored in <see cref=\"EventGridEvent.EventType\"/> and <see cref=\"CloudEvent.Type\"/> "
: "/// The value of the Event Type stored in <see cref=\"CloudEvent.Type\"/> ");

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

// Add the constant
sourceBuilder.AppendLine($"{Indent}{Indent}public const string {sysEvent.EventConstantName} = {sysEvent.EventType};");
sourceBuilder.AppendIndentedLine(2, $"public const string {sysEvent.EventConstantName} = {sysEvent.EventType};");
}

sourceBuilder.Append($@"{Indent}}}
}}");
sourceBuilder.AppendIndentedLine(1, @"}
}");
return sourceBuilder.ToString();
}

private string ConstructSystemEventExtensions()
private static string ConstructSystemEventExtensions(List<SystemEventNode> systemEvents, bool isSystemEventsLibrary)
{
var sourceBuilder = new StringBuilder(
$@"// Copyright (c) Microsoft Corporation. All rights reserved.
Expand All @@ -101,7 +212,7 @@ private string ConstructSystemEventExtensions()
using System.Collections.Generic;
using System.Text.Json;
using Azure.Messaging.EventGrid.SystemEvents;
{(_isSystemEventsLibrary ? "using System.ClientModel.Primitives;" : string.Empty)}
{(isSystemEventsLibrary ? "using System.ClientModel.Primitives;" : string.Empty)}

namespace Azure.Messaging.EventGrid
{{
Expand All @@ -111,17 +222,17 @@ public static object AsSystemEventData(string eventType, JsonElement data)
{{
var eventTypeSpan = eventType.AsSpan();
");
foreach (SystemEventNode sysEvent in _visitor.SystemEvents)
foreach (SystemEventNode sysEvent in systemEvents)
{
// Add each an entry for each system event to the dictionary containing a mapping from constant name to deserialization method.
sourceBuilder.AppendLine(
$"{Indent}{Indent}{Indent}if (eventTypeSpan.Equals(SystemEventNames.{sysEvent.EventConstantName}.AsSpan(), StringComparison.OrdinalIgnoreCase))");
sourceBuilder.AppendLine(
$"{Indent}{Indent}{Indent}{Indent}return {sysEvent.EventName}.{sysEvent.DeserializeMethod}(data{(_isSystemEventsLibrary ? ", null" : string.Empty)});");
sourceBuilder.AppendIndentedLine(3,
$"if (eventTypeSpan.Equals(SystemEventNames.{sysEvent.EventConstantName}.AsSpan(), StringComparison.OrdinalIgnoreCase))");
sourceBuilder.AppendIndentedLine(4,
$"return {sysEvent.EventName}.{sysEvent.DeserializeMethod}(data{(isSystemEventsLibrary ? ", null" : string.Empty)});");
}
sourceBuilder.AppendLine($"{Indent}{Indent}{Indent}return null;");
sourceBuilder.AppendLine($"{Indent}{Indent}}}");
sourceBuilder.AppendLine($"{Indent}}}");
sourceBuilder.AppendIndentedLine(3, "return null;");
sourceBuilder.AppendIndentedLine(2, "}");
sourceBuilder.AppendIndentedLine(1, "}");
sourceBuilder.AppendLine("}");

return sourceBuilder.ToString();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Text;

namespace Azure.EventGrid.Messaging.SourceGeneration
{
internal static class StringBuilderExtensions
{
public static void AppendIndentedLine(this StringBuilder sb, int indentLevel, string text)
{
sb.Append(' ', indentLevel * 4);
sb.AppendLine(text);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@

namespace Azure.EventGrid.Messaging.SourceGeneration
{
internal class SystemEventNode
internal sealed class SystemEventNode
{
public string EventName { get; set; }
public SystemEventNode(string eventName, string eventType, string deserializeMethod)
{
EventName = eventName;
EventType = eventType;
DeserializeMethod = deserializeMethod;
EventConstantName = Convert(EventName);
}

public string EventConstantName
private static string Convert(string eventName)
{
get
// special case a few events that don't follow the pattern
return eventName switch
{
// special case a few events that don't follow the pattern
return EventName switch
{
"ServiceBusDeadletterMessagesAvailableWithNoListenersEventData" => "ServiceBusDeadletterMessagesAvailableWithNoListener",
"SubscriptionDeletedEventData" => "EventGridSubscriptionDeleted",
"SubscriptionValidationEventData" => "EventGridSubscriptionValidation",
_ => EventName?.Replace("EventData", ""),
};
}
"ServiceBusDeadletterMessagesAvailableWithNoListenersEventData" => "ServiceBusDeadletterMessagesAvailableWithNoListener",
"SubscriptionDeletedEventData" => "EventGridSubscriptionDeleted",
"SubscriptionValidationEventData" => "EventGridSubscriptionValidation",
_ => eventName?.Replace("EventData", ""),
};
}

public string EventType { get; set; }
public string EventName { get; }

public string EventConstantName { get; }

public string EventType { get; }

public string DeserializeMethod { get; set; }
public string DeserializeMethod { get; }
}
}