diff --git a/src/Core/gen/Eventuous.Shared.Generators/Constants.cs b/src/Core/gen/Eventuous.Shared.Generators/Constants.cs
index 42b3fd4d..2c8db157 100644
--- a/src/Core/gen/Eventuous.Shared.Generators/Constants.cs
+++ b/src/Core/gen/Eventuous.Shared.Generators/Constants.cs
@@ -3,8 +3,20 @@
namespace Eventuous.Shared.Generators;
+///
+/// Constants used for type and member lookups.
+/// These are primarily used for symbol resolution via Compilation.GetTypeByMetadataName()
+/// and as fallback when symbol-based comparison is not available.
+/// The generators now prefer symbol-based comparisons using SymbolEqualityComparer,
+/// which are refactoring-safe and won't break when types are renamed.
+///
internal static class Constants {
- public const string BaseNamespace = "Eventuous";
+ /// Base namespace for Eventuous types.
+ public const string BaseNamespace = "Eventuous";
+
+ /// Name of the EventType attribute class (without namespace).
public const string EventTypeAttribute = "EventTypeAttribute";
- public const string EventTypeAttrFqcn = $"{BaseNamespace}.{EventTypeAttribute}";
+
+ /// Fully qualified name of the EventType attribute for GetTypeByMetadataName().
+ public const string EventTypeAttrFqcn = $"{BaseNamespace}.{EventTypeAttribute}";
}
diff --git a/src/Core/gen/Eventuous.Shared.Generators/EventUsageAnalyzer.cs b/src/Core/gen/Eventuous.Shared.Generators/EventUsageAnalyzer.cs
index 02c22dea..ae5e16b3 100644
--- a/src/Core/gen/Eventuous.Shared.Generators/EventUsageAnalyzer.cs
+++ b/src/Core/gen/Eventuous.Shared.Generators/EventUsageAnalyzer.cs
@@ -32,11 +32,43 @@ public override void Initialize(AnalysisContext context) {
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
- context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
- context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
+ context.RegisterCompilationStartAction(compilationContext => {
+ // Resolve well-known type symbols once per compilation
+ var compilation = compilationContext.Compilation;
+ var knownTypes = new KnownTypeSymbols(compilation);
+
+ compilationContext.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, knownTypes), OperationKind.Invocation);
+ compilationContext.RegisterOperationAction(ctx => AnalyzeObjectCreation(ctx, knownTypes), OperationKind.ObjectCreation);
+ });
}
- static ImmutableHashSet GetExplicitRegistrations(OperationAnalysisContext ctx) {
+ ///
+ /// Cache of well-known type symbols resolved from the compilation.
+ /// This makes the analyzer refactoring-safe by using symbol comparison instead of string matching.
+ ///
+ sealed class KnownTypeSymbols {
+ public INamedTypeSymbol? EventTypeAttribute { get; }
+ public INamedTypeSymbol? TypeMapper { get; }
+ public INamedTypeSymbol? Aggregate { get; }
+ public INamedTypeSymbol? State { get; }
+ public INamedTypeSymbol? CommandHandlerBuilder { get; }
+ public INamedTypeSymbol? IDefineExecution { get; }
+ public INamedTypeSymbol? ICommandHandlerBuilder { get; }
+ public INamedTypeSymbol? IDefineStoreOrExecution { get; }
+
+ public KnownTypeSymbols(Compilation compilation) {
+ EventTypeAttribute = compilation.GetTypeByMetadataName(EventTypeAttrFqcn);
+ TypeMapper = compilation.GetTypeByMetadataName($"{BaseNamespace}.TypeMapper");
+ Aggregate = compilation.GetTypeByMetadataName($"{BaseNamespace}.Aggregate`1");
+ State = compilation.GetTypeByMetadataName($"{BaseNamespace}.State`1");
+ CommandHandlerBuilder = compilation.GetTypeByMetadataName($"{BaseNamespace}.CommandHandlerBuilder");
+ IDefineExecution = compilation.GetTypeByMetadataName($"{BaseNamespace}.IDefineExecution");
+ ICommandHandlerBuilder = compilation.GetTypeByMetadataName($"{BaseNamespace}.ICommandHandlerBuilder");
+ IDefineStoreOrExecution = compilation.GetTypeByMetadataName($"{BaseNamespace}.IDefineStoreOrExecution");
+ }
+ }
+
+ static ImmutableHashSet GetExplicitRegistrations(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
var model = ctx.Operation.SemanticModel;
if (model == null) return ImmutableHashSet.Empty;
var root = ctx.Operation.Syntax.SyntaxTree.GetRoot();
@@ -45,12 +77,18 @@ static ImmutableHashSet GetExplicitRegistrations(OperationAnalysisC
foreach (var invSyntax in root.DescendantNodes().OfType()) {
if (model.GetOperation(invSyntax) is not IInvocationOperation op) continue;
var m = op.TargetMethod;
+
+ // Use symbol comparison when available, fall back to string comparison
if (m.Name != "AddType") continue;
var ct = m.ContainingType;
if (ct == null) continue;
- if (ct.Name != "TypeMapper") continue;
- var ns = ct.ContainingNamespace?.ToDisplayString();
- if (ns != BaseNamespace) continue;
+
+ // Prefer symbol comparison (refactoring-safe)
+ var isTypeMapper = knownTypes.TypeMapper != null
+ ? SymbolEqualityComparer.Default.Equals(ct, knownTypes.TypeMapper)
+ : ct.Name == "TypeMapper" && ct.ContainingNamespace?.ToDisplayString() == BaseNamespace;
+
+ if (!isTypeMapper) continue;
if (m.TypeArguments.Length == 1) {
set.Add(m.TypeArguments[0]);
@@ -65,12 +103,12 @@ static ImmutableHashSet GetExplicitRegistrations(OperationAnalysisC
return set.ToImmutable();
}
- static bool IsExplicitlyRegistered(ITypeSymbol type, OperationAnalysisContext ctx) {
- var set = GetExplicitRegistrations(ctx);
+ static bool IsExplicitlyRegistered(ITypeSymbol type, OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
+ var set = GetExplicitRegistrations(ctx, knownTypes);
return set.Contains(type);
}
- static void AnalyzeInvocation(OperationAnalysisContext ctx) {
+ static void AnalyzeInvocation(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
if (ctx.Operation is not IInvocationOperation inv) return;
var method = inv.TargetMethod;
@@ -80,10 +118,10 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
case { Name: "Apply", TypeArguments.Length: 1, Parameters.Length: 1 }: {
var containing = method.ContainingType;
- if (IsAggregate(containing)) {
+ if (IsAggregate(containing, knownTypes)) {
var eventType = method.TypeArguments[0];
- if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
+ if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, inv.Syntax.GetLocation(), eventType.ToDisplayString()));
}
}
@@ -91,7 +129,7 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
return;
}
// Case 1b: State.When(...) invocations where an event instance is passed
- case { Name: "When", Parameters.Length: 1 } when IsState(method.ContainingType): {
+ case { Name: "When", Parameters.Length: 1 } when IsState(method.ContainingType, knownTypes): {
var arg = inv.Arguments.Length > 0 ? inv.Arguments[0].Value : null;
ITypeSymbol? eventType = null;
@@ -105,7 +143,7 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
_ => arg?.Type
};
- if (eventType != null && IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
+ if (eventType != null && IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
var location = arg?.Syntax.GetLocation() ?? inv.Syntax.GetLocation();
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, location, eventType.ToDisplayString()));
}
@@ -113,10 +151,10 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
return;
}
// Case 1c: State.On(...) handler registrations
- case { Name: "On", TypeArguments.Length: 1 } when IsState(method.ContainingType): {
+ case { Name: "On", TypeArguments.Length: 1 } when IsState(method.ContainingType, knownTypes): {
var eventType = method.TypeArguments[0];
- if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
+ if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, inv.Syntax.GetLocation(), eventType.ToDisplayString()));
}
@@ -127,7 +165,7 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
// Case 2: Functional service: Act/ActAsync handlers
if (method.Name is "Act" or "ActAsync") {
// Heuristic: only consider the overloads that accept a delegate and are defined in CommandHandlerBuilder interfaces/classes
- if (!IsFunctionalServiceAct(method)) return;
+ if (!IsFunctionalServiceAct(method, knownTypes)) return;
foreach (var value in inv.Arguments.Select(arg => arg.Value)) {
switch (value) {
@@ -135,11 +173,11 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
continue;
// If the argument is a lambda, analyze its body for created event instances
case IAnonymousFunctionOperation lambda:
- AnalyzeDelegateBodyForEventCreations(ctx, lambda.Body);
+ AnalyzeDelegateBodyForEventCreations(ctx, lambda.Body, knownTypes);
break;
case IConversionOperation { Operand: IAnonymousFunctionOperation lambdaConv }:
- AnalyzeDelegateBodyForEventCreations(ctx, lambdaConv.Body);
+ AnalyzeDelegateBodyForEventCreations(ctx, lambdaConv.Body, knownTypes);
break;
}
@@ -147,21 +185,21 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
}
}
- static void AnalyzeDelegateBodyForEventCreations(OperationAnalysisContext ctx, IBlockOperation? body) {
+ static void AnalyzeDelegateBodyForEventCreations(OperationAnalysisContext ctx, IBlockOperation? body, KnownTypeSymbols knownTypes) {
if (body is null) return;
foreach (var op in body.Descendants()) {
if (op is IObjectCreationOperation create) {
var created = create.Type;
- if (created != null && IsConcreteEvent(created) && !HasEventTypeAttribute(created) && !IsExplicitlyRegistered(created, ctx)) {
+ if (created != null && IsConcreteEvent(created) && !HasEventTypeAttribute(created, knownTypes) && !IsExplicitlyRegistered(created, ctx, knownTypes)) {
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, create.Syntax.GetLocation(), created.ToDisplayString()));
}
}
}
}
- static void AnalyzeObjectCreation(OperationAnalysisContext ctx) {
+ static void AnalyzeObjectCreation(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
// Global safety net for method groups passed into Act where we couldn't traverse the body via the invocation site.
// If the object creation is within a method that appears to be an Act handler (returns NewEvents/ IEnumerable