Skip to content

Commit 7a9b9bd

Browse files
Avoid using magic strings (#488)
1 parent 2f86b56 commit 7a9b9bd

4 files changed

Lines changed: 236 additions & 68 deletions

File tree

src/Core/gen/Eventuous.Shared.Generators/Constants.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@
33

44
namespace Eventuous.Shared.Generators;
55

6+
/// <summary>
7+
/// Constants used for type and member lookups.
8+
/// These are primarily used for symbol resolution via Compilation.GetTypeByMetadataName()
9+
/// and as fallback when symbol-based comparison is not available.
10+
/// The generators now prefer symbol-based comparisons using SymbolEqualityComparer,
11+
/// which are refactoring-safe and won't break when types are renamed.
12+
/// </summary>
613
internal static class Constants {
7-
public const string BaseNamespace = "Eventuous";
14+
/// <summary>Base namespace for Eventuous types.</summary>
15+
public const string BaseNamespace = "Eventuous";
16+
17+
/// <summary>Name of the EventType attribute class (without namespace).</summary>
818
public const string EventTypeAttribute = "EventTypeAttribute";
9-
public const string EventTypeAttrFqcn = $"{BaseNamespace}.{EventTypeAttribute}";
19+
20+
/// <summary>Fully qualified name of the EventType attribute for GetTypeByMetadataName().</summary>
21+
public const string EventTypeAttrFqcn = $"{BaseNamespace}.{EventTypeAttribute}";
1022
}

src/Core/gen/Eventuous.Shared.Generators/EventUsageAnalyzer.cs

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,43 @@ public override void Initialize(AnalysisContext context) {
3232
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
3333
context.EnableConcurrentExecution();
3434

35-
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
36-
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
35+
context.RegisterCompilationStartAction(compilationContext => {
36+
// Resolve well-known type symbols once per compilation
37+
var compilation = compilationContext.Compilation;
38+
var knownTypes = new KnownTypeSymbols(compilation);
39+
40+
compilationContext.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, knownTypes), OperationKind.Invocation);
41+
compilationContext.RegisterOperationAction(ctx => AnalyzeObjectCreation(ctx, knownTypes), OperationKind.ObjectCreation);
42+
});
3743
}
3844

39-
static ImmutableHashSet<ITypeSymbol> GetExplicitRegistrations(OperationAnalysisContext ctx) {
45+
/// <summary>
46+
/// Cache of well-known type symbols resolved from the compilation.
47+
/// This makes the analyzer refactoring-safe by using symbol comparison instead of string matching.
48+
/// </summary>
49+
sealed class KnownTypeSymbols {
50+
public INamedTypeSymbol? EventTypeAttribute { get; }
51+
public INamedTypeSymbol? TypeMapper { get; }
52+
public INamedTypeSymbol? Aggregate { get; }
53+
public INamedTypeSymbol? State { get; }
54+
public INamedTypeSymbol? CommandHandlerBuilder { get; }
55+
public INamedTypeSymbol? IDefineExecution { get; }
56+
public INamedTypeSymbol? ICommandHandlerBuilder { get; }
57+
public INamedTypeSymbol? IDefineStoreOrExecution { get; }
58+
59+
public KnownTypeSymbols(Compilation compilation) {
60+
EventTypeAttribute = compilation.GetTypeByMetadataName(EventTypeAttrFqcn);
61+
TypeMapper = compilation.GetTypeByMetadataName($"{BaseNamespace}.TypeMapper");
62+
Aggregate = compilation.GetTypeByMetadataName($"{BaseNamespace}.Aggregate`1");
63+
State = compilation.GetTypeByMetadataName($"{BaseNamespace}.State`1");
64+
CommandHandlerBuilder = compilation.GetTypeByMetadataName($"{BaseNamespace}.CommandHandlerBuilder");
65+
IDefineExecution = compilation.GetTypeByMetadataName($"{BaseNamespace}.IDefineExecution");
66+
ICommandHandlerBuilder = compilation.GetTypeByMetadataName($"{BaseNamespace}.ICommandHandlerBuilder");
67+
IDefineStoreOrExecution = compilation.GetTypeByMetadataName($"{BaseNamespace}.IDefineStoreOrExecution");
68+
}
69+
}
70+
71+
static ImmutableHashSet<ITypeSymbol> GetExplicitRegistrations(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
4072
var model = ctx.Operation.SemanticModel;
4173
if (model == null) return ImmutableHashSet<ITypeSymbol>.Empty;
4274
var root = ctx.Operation.Syntax.SyntaxTree.GetRoot();
@@ -45,12 +77,18 @@ static ImmutableHashSet<ITypeSymbol> GetExplicitRegistrations(OperationAnalysisC
4577
foreach (var invSyntax in root.DescendantNodes().OfType<InvocationExpressionSyntax>()) {
4678
if (model.GetOperation(invSyntax) is not IInvocationOperation op) continue;
4779
var m = op.TargetMethod;
80+
81+
// Use symbol comparison when available, fall back to string comparison
4882
if (m.Name != "AddType") continue;
4983
var ct = m.ContainingType;
5084
if (ct == null) continue;
51-
if (ct.Name != "TypeMapper") continue;
52-
var ns = ct.ContainingNamespace?.ToDisplayString();
53-
if (ns != BaseNamespace) continue;
85+
86+
// Prefer symbol comparison (refactoring-safe)
87+
var isTypeMapper = knownTypes.TypeMapper != null
88+
? SymbolEqualityComparer.Default.Equals(ct, knownTypes.TypeMapper)
89+
: ct.Name == "TypeMapper" && ct.ContainingNamespace?.ToDisplayString() == BaseNamespace;
90+
91+
if (!isTypeMapper) continue;
5492

5593
if (m.TypeArguments.Length == 1) {
5694
set.Add(m.TypeArguments[0]);
@@ -65,12 +103,12 @@ static ImmutableHashSet<ITypeSymbol> GetExplicitRegistrations(OperationAnalysisC
65103
return set.ToImmutable();
66104
}
67105

68-
static bool IsExplicitlyRegistered(ITypeSymbol type, OperationAnalysisContext ctx) {
69-
var set = GetExplicitRegistrations(ctx);
106+
static bool IsExplicitlyRegistered(ITypeSymbol type, OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
107+
var set = GetExplicitRegistrations(ctx, knownTypes);
70108
return set.Contains(type);
71109
}
72110

73-
static void AnalyzeInvocation(OperationAnalysisContext ctx) {
111+
static void AnalyzeInvocation(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
74112
if (ctx.Operation is not IInvocationOperation inv) return;
75113

76114
var method = inv.TargetMethod;
@@ -80,18 +118,18 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
80118
case { Name: "Apply", TypeArguments.Length: 1, Parameters.Length: 1 }: {
81119
var containing = method.ContainingType;
82120

83-
if (IsAggregate(containing)) {
121+
if (IsAggregate(containing, knownTypes)) {
84122
var eventType = method.TypeArguments[0];
85123

86-
if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
124+
if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
87125
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, inv.Syntax.GetLocation(), eventType.ToDisplayString()));
88126
}
89127
}
90128

91129
return;
92130
}
93131
// Case 1b: State<T>.When(...) invocations where an event instance is passed
94-
case { Name: "When", Parameters.Length: 1 } when IsState(method.ContainingType): {
132+
case { Name: "When", Parameters.Length: 1 } when IsState(method.ContainingType, knownTypes): {
95133
var arg = inv.Arguments.Length > 0 ? inv.Arguments[0].Value : null;
96134

97135
ITypeSymbol? eventType = null;
@@ -105,18 +143,18 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
105143
_ => arg?.Type
106144
};
107145

108-
if (eventType != null && IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
146+
if (eventType != null && IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
109147
var location = arg?.Syntax.GetLocation() ?? inv.Syntax.GetLocation();
110148
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, location, eventType.ToDisplayString()));
111149
}
112150

113151
return;
114152
}
115153
// Case 1c: State<T>.On<TEvent>(...) handler registrations
116-
case { Name: "On", TypeArguments.Length: 1 } when IsState(method.ContainingType): {
154+
case { Name: "On", TypeArguments.Length: 1 } when IsState(method.ContainingType, knownTypes): {
117155
var eventType = method.TypeArguments[0];
118156

119-
if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType) && !IsExplicitlyRegistered(eventType, ctx)) {
157+
if (IsConcreteEvent(eventType) && !HasEventTypeAttribute(eventType, knownTypes) && !IsExplicitlyRegistered(eventType, ctx, knownTypes)) {
120158
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, inv.Syntax.GetLocation(), eventType.ToDisplayString()));
121159
}
122160

@@ -127,41 +165,41 @@ static void AnalyzeInvocation(OperationAnalysisContext ctx) {
127165
// Case 2: Functional service: Act/ActAsync handlers
128166
if (method.Name is "Act" or "ActAsync") {
129167
// Heuristic: only consider the overloads that accept a delegate and are defined in CommandHandlerBuilder interfaces/classes
130-
if (!IsFunctionalServiceAct(method)) return;
168+
if (!IsFunctionalServiceAct(method, knownTypes)) return;
131169

132170
foreach (var value in inv.Arguments.Select(arg => arg.Value)) {
133171
switch (value) {
134172
case null:
135173
continue;
136174
// If the argument is a lambda, analyze its body for created event instances
137175
case IAnonymousFunctionOperation lambda:
138-
AnalyzeDelegateBodyForEventCreations(ctx, lambda.Body);
176+
AnalyzeDelegateBodyForEventCreations(ctx, lambda.Body, knownTypes);
139177

140178
break;
141179
case IConversionOperation { Operand: IAnonymousFunctionOperation lambdaConv }:
142-
AnalyzeDelegateBodyForEventCreations(ctx, lambdaConv.Body);
180+
AnalyzeDelegateBodyForEventCreations(ctx, lambdaConv.Body, knownTypes);
143181

144182
break;
145183
}
146184
}
147185
}
148186
}
149187

150-
static void AnalyzeDelegateBodyForEventCreations(OperationAnalysisContext ctx, IBlockOperation? body) {
188+
static void AnalyzeDelegateBodyForEventCreations(OperationAnalysisContext ctx, IBlockOperation? body, KnownTypeSymbols knownTypes) {
151189
if (body is null) return;
152190

153191
foreach (var op in body.Descendants()) {
154192
if (op is IObjectCreationOperation create) {
155193
var created = create.Type;
156194

157-
if (created != null && IsConcreteEvent(created) && !HasEventTypeAttribute(created) && !IsExplicitlyRegistered(created, ctx)) {
195+
if (created != null && IsConcreteEvent(created) && !HasEventTypeAttribute(created, knownTypes) && !IsExplicitlyRegistered(created, ctx, knownTypes)) {
158196
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, create.Syntax.GetLocation(), created.ToDisplayString()));
159197
}
160198
}
161199
}
162200
}
163201

164-
static void AnalyzeObjectCreation(OperationAnalysisContext ctx) {
202+
static void AnalyzeObjectCreation(OperationAnalysisContext ctx, KnownTypeSymbols knownTypes) {
165203
// Global safety net for method groups passed into Act where we couldn't traverse the body via the invocation site.
166204
// If the object creation is within a method that appears to be an Act handler (returns NewEvents/ IEnumerable<object>), warn.
167205
if (ctx.Operation is not IObjectCreationOperation create) return;
@@ -175,7 +213,7 @@ static void AnalyzeObjectCreation(OperationAnalysisContext ctx) {
175213
if (method == null) return;
176214

177215
if (ReturnsNewEvents(method)) {
178-
if (!HasEventTypeAttribute(created) && !IsExplicitlyRegistered(created, ctx)) {
216+
if (!HasEventTypeAttribute(created, knownTypes) && !IsExplicitlyRegistered(created, ctx, knownTypes)) {
179217
ctx.ReportDiagnostic(Diagnostic.Create(MissingEventTypeAttribute, create.Syntax.GetLocation(), created.ToDisplayString()));
180218
}
181219
}
@@ -218,49 +256,86 @@ static bool IsIEnumerableOfObject(INamedTypeSymbol type) {
218256
return false;
219257
}
220258

221-
static bool IsAggregate(INamedTypeSymbol? type) {
259+
static bool IsAggregate(INamedTypeSymbol? type, KnownTypeSymbols knownTypes) {
222260
if (type == null) return false;
223261

224262
// Walk base types to check if it derives from Eventuous.Aggregate<>
225263
for (var t = type; t != null; t = t.BaseType) {
226-
if (t is { Name: "Aggregate", Arity: 1 } && t.ContainingNamespace.ToDisplayString() == BaseNamespace) return true;
264+
// Prefer symbol comparison (refactoring-safe)
265+
if (knownTypes.Aggregate != null) {
266+
if (SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, knownTypes.Aggregate)) {
267+
return true;
268+
}
269+
}
270+
else {
271+
// Fallback to string comparison
272+
if (t is { Name: "Aggregate", Arity: 1 } && t.ContainingNamespace.ToDisplayString() == BaseNamespace) {
273+
return true;
274+
}
275+
}
227276
}
228277

229278
return false;
230279
}
231280

232-
static bool IsState(INamedTypeSymbol? type) {
281+
static bool IsState(INamedTypeSymbol? type, KnownTypeSymbols knownTypes) {
233282
if (type == null) return false;
234283

235284
// Walk base types to check if it derives from Eventuous.State<>
236285
for (var t = type; t != null; t = t.BaseType) {
237-
if (t is { Name: "State", Arity: 1 } && t.ContainingNamespace.ToDisplayString() == BaseNamespace) return true;
286+
// Prefer symbol comparison (refactoring-safe)
287+
if (knownTypes.State != null) {
288+
if (SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, knownTypes.State)) {
289+
return true;
290+
}
291+
}
292+
else {
293+
// Fallback to string comparison
294+
if (t is { Name: "State", Arity: 1 } && t.ContainingNamespace.ToDisplayString() == BaseNamespace) {
295+
return true;
296+
}
297+
}
238298
}
239299

240300
return false;
241301
}
242302

243-
static bool IsFunctionalServiceAct(IMethodSymbol method) {
303+
static bool IsFunctionalServiceAct(IMethodSymbol method, KnownTypeSymbols knownTypes) {
244304
// We only care about the Act methods from CommandHandlerBuilder and the related interfaces in Eventuous namespace
245305
if (method.Name is not ("Act" or "ActAsync")) return false;
246306

247307
var containing = method.ContainingType;
248308

249309
if (containing == null) return false;
250310

251-
var ns = containing.ContainingNamespace?.ToDisplayString();
311+
// Prefer symbol comparison (refactoring-safe)
312+
if (knownTypes.CommandHandlerBuilder != null || knownTypes.IDefineExecution != null ||
313+
knownTypes.ICommandHandlerBuilder != null || knownTypes.IDefineStoreOrExecution != null) {
314+
return SymbolEqualityComparer.Default.Equals(containing, knownTypes.CommandHandlerBuilder) ||
315+
SymbolEqualityComparer.Default.Equals(containing, knownTypes.IDefineExecution) ||
316+
SymbolEqualityComparer.Default.Equals(containing, knownTypes.ICommandHandlerBuilder) ||
317+
SymbolEqualityComparer.Default.Equals(containing, knownTypes.IDefineStoreOrExecution);
318+
}
252319

320+
// Fallback to string comparison
321+
var ns = containing.ContainingNamespace?.ToDisplayString();
253322
if (ns != BaseNamespace) return false;
254323

255-
// Simple name checks
256324
return containing.Name is "CommandHandlerBuilder" or "IDefineExecution" or "ICommandHandlerBuilder" or "IDefineStoreOrExecution";
257325
}
258326

259327
static bool IsConcreteEvent(ITypeSymbol type) => type.TypeKind is TypeKind.Class or TypeKind.Struct;
260328

261-
static bool HasEventTypeAttribute(ITypeSymbol type)
262-
=> (from attrClass in type.GetAttributes().Select(a => a.AttributeClass).OfType<INamedTypeSymbol>()
263-
let name = attrClass.ToDisplayString()
264-
where name == EventTypeAttrFqcn || attrClass.Name is EventTypeAttribute
265-
select attrClass).Any();
329+
static bool HasEventTypeAttribute(ITypeSymbol type, KnownTypeSymbols knownTypes) {
330+
// Prefer symbol comparison (refactoring-safe)
331+
if (knownTypes.EventTypeAttribute != null) {
332+
return type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownTypes.EventTypeAttribute));
333+
}
334+
335+
// Fallback to string comparison
336+
return (from attrClass in type.GetAttributes().Select(a => a.AttributeClass).OfType<INamedTypeSymbol>()
337+
let name = attrClass.ToDisplayString()
338+
where name == EventTypeAttrFqcn || attrClass.Name is EventTypeAttribute
339+
select attrClass).Any();
340+
}
266341
}

0 commit comments

Comments
 (0)