@@ -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