diff --git a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/EventGridSourceGenerator.cs b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/EventGridSourceGenerator.cs index 2ef27fcda927..b12f6ebabce3 100644 --- a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/EventGridSourceGenerator.cs +++ b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/EventGridSourceGenerator.cs @@ -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 @@ -13,38 +18,144 @@ namespace Azure.EventGrid.Messaging.SourceGeneration /// from constant values to deserialization method for each system event. /// [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 SummaryStartTag => "".AsSpan(); + private static ReadOnlySpan SummaryEndTag => "".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 ClassDeclarations) input) => + { + Execute(sourceProductionContext, input.Compilation, input.ClassDeclarations); + }); + } + + private static void Execute(SourceProductionContext context, Compilation compilation, ImmutableArray 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 GetSystemEventNodes(Compilation compilation, ImmutableArray classes) + { + var systemEventNodes = new List(); + var eventTypeSet = new HashSet(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() + .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 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 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. @@ -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}/// "); - sourceBuilder.AppendLine( - !_isSystemEventsLibrary - ? $"{Indent}{Indent}/// The value of the Event Type stored in and " - : $"{Indent}{Indent}/// The value of the Event Type stored in "); + sourceBuilder.AppendIndentedLine(2, "/// "); + sourceBuilder.AppendIndentedLine(2, + !isSystemEventsLibrary + ? "/// The value of the Event Type stored in and " + : "/// The value of the Event Type stored in "); - sourceBuilder.AppendLine($"{Indent}{Indent}/// for the system event."); - sourceBuilder.AppendLine($"{Indent}{Indent}/// "); + sourceBuilder.AppendIndentedLine(2, $"/// for the system event."); + sourceBuilder.AppendIndentedLine(2, "/// "); // 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 systemEvents, bool isSystemEventsLibrary) { var sourceBuilder = new StringBuilder( $@"// Copyright (c) Microsoft Corporation. All rights reserved. @@ -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 {{ @@ -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(); diff --git a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SourceVisitor.cs b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SourceVisitor.cs deleted file mode 100644 index ca6ecb4bedfa..000000000000 --- a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SourceVisitor.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Xml; -using Microsoft.CodeAnalysis; - -namespace Azure.EventGrid.Messaging.SourceGeneration -{ - internal class SourceVisitor : SymbolVisitor - { - public List SystemEvents { get; } = new(); - - public override void VisitNamespace(INamespaceSymbol symbol) - { - foreach (var childSymbol in symbol.GetMembers()) - { - childSymbol.Accept(this); - } - } - - public override void VisitNamedType(INamedTypeSymbol symbol) - { - if (symbol.Name.EndsWith("EventData")) - { - string type = null; - XmlDocument xmlDoc = new(); - xmlDoc.LoadXml(symbol.GetDocumentationCommentXml()); - var xmlNode = xmlDoc.SelectSingleNode("member/summary"); - // the event name is either 3 or 4 parts, e.g. Microsoft.AppConfiguration.KeyValueDeleted or Microsoft.ResourceNotifications.HealthResources.AvailabilityStatusChanged - var match = Regex.Match(xmlNode.InnerText, "[a-zA-Z]+\\.[a-zA-Z]+\\.[a-zA-Z]+(\\.[a-zA-Z]+)?"); - if (!match.Success) - { - // We expect some EventData to not have event types if they are base types, - // e.g. ContainerRegistryEventData - return; - } - - type = $@"""{match.Value}"""; - SystemEvents.Add( - new SystemEventNode() - { - EventName = symbol.Name, - EventType = type, - DeserializeMethod = symbol.MemberNames.Single(m => m.StartsWith("Deserialize")) - }); - } - } - } -} diff --git a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/StringBuilderExtensions.cs b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/StringBuilderExtensions.cs new file mode 100644 index 000000000000..8d33f1c5ce82 --- /dev/null +++ b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/StringBuilderExtensions.cs @@ -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); + } + } +} diff --git a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SystemEventNode.cs b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SystemEventNode.cs index 1896a4544b37..cfac1dd9f297 100644 --- a/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SystemEventNode.cs +++ b/sdk/eventgrid/Azure.Messaging.EventGrid/EventGridSourceGenerator/src/SystemEventNode.cs @@ -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; } } }