com.wallstop-studios.dxmessaging 2.0.0-rc21 → 2.0.0-rc22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,445 +1,306 @@
1
- namespace WallstopStudios.DxMessaging.SourceGenerators;
2
-
3
- using System;
4
- using System.Collections.Generic;
5
- using System.Collections.Immutable;
6
- using System.Linq;
7
- using System.Text;
8
- using System.Threading;
9
- using Microsoft.CodeAnalysis;
10
- using Microsoft.CodeAnalysis.CSharp;
11
- using Microsoft.CodeAnalysis.CSharp.Syntax;
12
- using Microsoft.CodeAnalysis.Text;
13
-
14
- [Generator(LanguageNames.CSharp)]
15
- public sealed class DxMessageIdGenerator : IIncrementalGenerator
1
+ namespace WallstopStudios.DxMessaging.SourceGenerators
16
2
  {
17
- // --- Constants for Attributes and Interfaces ---
18
- private const string BaseInterfaceFullName = "DxMessaging.Core.IMessage"; // Base interface
19
-
20
- private const string BroadcastAttrFullName =
21
- "DxMessaging.Core.Attributes.DxBroadcastMessageAttribute";
22
- private const string TargetedAttrFullName =
23
- "DxMessaging.Core.Attributes.DxTargetedMessageAttribute";
24
- private const string UntargetedAttrFullName =
25
- "DxMessaging.Core.Attributes.DxUntargetedMessageAttribute";
26
-
27
- private const string BroadcastInterfaceFullName = "DxMessaging.Core.Messages.IBroadcastMessage";
28
- private const string TargetedInterfaceFullName = "DxMessaging.Core.Messages.ITargetedMessage";
29
- private const string UntargetedInterfaceFullName =
30
- "DxMessaging.Core.Messages.IUntargetedMessage";
31
-
32
- // --- Diagnostics ---
33
- private static readonly DiagnosticDescriptor CollisionError = new DiagnosticDescriptor(
34
- id: "DXMSG001",
35
- title: "Message ID Collision",
36
- messageFormat: "OptimizedMessageId collision detected across different message types. The generated ID '{0}' is shared by the following types: {1}. Please rename one or more types slightly to resolve the hash collision.",
37
- category: "DxMessaging",
38
- defaultSeverity: DiagnosticSeverity.Error,
39
- isEnabledByDefault: true
40
- );
41
-
42
- private static readonly DiagnosticDescriptor CollisionWarning = new DiagnosticDescriptor(
43
- id: "DXMSG003", // New ID
44
- title: "Message ID Collision Fallback",
45
- messageFormat: "OptimizedMessageId collision detected for ID '{0}'. The following types will use a fallback implementation (HasOptimizedId = false): {1}.",
46
- category: "DxMessaging",
47
- defaultSeverity: DiagnosticSeverity.Warning, // Set severity to Warning
48
- isEnabledByDefault: true
49
- );
50
-
51
- private static readonly DiagnosticDescriptor MultipleAttributesError = new DiagnosticDescriptor(
52
- id: "DXMSG002",
53
- title: "Multiple Message Attributes",
54
- messageFormat: "Type '{0}' cannot have more than one Dx message attribute ([DxBroadcastMessage], [DxTargetedMessage], [DxUntargetedMessage]).",
55
- category: "DxMessaging",
56
- defaultSeverity: DiagnosticSeverity.Error,
57
- isEnabledByDefault: true
58
- );
59
-
60
- // Helper record to pass data through the pipeline
61
- private record struct SemanticTargetInfo(
62
- INamedTypeSymbol TypeSymbol,
63
- TypeDeclarationSyntax DeclarationSyntax,
64
- string TargetInterfaceFullName
65
- );
66
-
67
- // Helper record for final processing stage
68
- private record struct MessageInfo(
69
- INamedTypeSymbol TypeSymbol,
70
- TypeDeclarationSyntax DeclarationSyntax,
71
- string TargetInterfaceFullName,
72
- int GeneratedId
73
- );
74
-
75
- // Helper record for final assignment stage
76
- private record struct FinalMessageInfo(
77
- INamedTypeSymbol TypeSymbol,
78
- TypeDeclarationSyntax DeclarationSyntax,
79
- string TargetInterfaceFullName,
80
- int FinalId, // The ID actually assigned (hash or fallback)
81
- bool WasCollision
82
- ); // Flag indicating if it was part of a collision
83
-
84
- public void Initialize(IncrementalGeneratorInitializationContext context)
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.Collections.Immutable;
6
+ using System.Linq;
7
+ using System.Text;
8
+ using System.Threading;
9
+ using Microsoft.CodeAnalysis;
10
+ using Microsoft.CodeAnalysis.CSharp;
11
+ using Microsoft.CodeAnalysis.CSharp.Syntax;
12
+ using Microsoft.CodeAnalysis.Text;
13
+
14
+ [Generator(LanguageNames.CSharp)]
15
+ public sealed class DxMessageIdGenerator : IIncrementalGenerator
85
16
  {
86
- // --- Step 1: Find all classes/structs with attribute lists (Potential Candidates) ---
87
- IncrementalValuesProvider<TypeDeclarationSyntax> potentialTypeDeclarations =
88
- context.SyntaxProvider.CreateSyntaxProvider(
89
- predicate: static (node, _) => IsSyntaxTargetForGeneration(node), // Quick syntax filter
90
- transform: static (ctx, ct) => (TypeDeclarationSyntax)ctx.Node
91
- ); // Just pass the node
92
-
93
- // --- Step 2: Get Semantic Info and Filter by Attributes ---
94
- IncrementalValuesProvider<SemanticTargetInfo?> semanticTargets = potentialTypeDeclarations
95
- .Select(static (typeDecl, ct) => new { typeDecl, ct }) // Combine with CancellationToken if needed implicitly by GetSemanticTargetForGeneration
96
- .Combine(context.CompilationProvider)
97
- .Select(
98
- static (data, ct) =>
99
- GetSemanticTargetForGeneration(data.Left.typeDecl, data.Right, ct)
100
- );
17
+ // Base IMessage interface (used for implementation checks if needed, and property names)
18
+ // *** Assumes the user has defined this interface in their code ***
19
+ private const string BaseInterfaceFullName = "DxMessaging.Core.IMessage";
20
+
21
+ // Message Type Attribute Full Names (Ensure these match your attributes)
22
+ private const string BroadcastAttrFullName =
23
+ "DxMessaging.Core.Attributes.DxBroadcastMessageAttribute";
24
+ private const string TargetedAttrFullName =
25
+ "DxMessaging.Core.Attributes.DxTargetedMessageAttribute";
26
+ private const string UntargetedAttrFullName =
27
+ "DxMessaging.Core.Attributes.DxUntargetedMessageAttribute";
28
+
29
+ // Target Interface Full Names (Ensure these match your specific message interfaces)
30
+ private const string BroadcastInterfaceFullName =
31
+ "DxMessaging.Core.Messages.IBroadcastMessage";
32
+ private const string TargetedInterfaceFullName =
33
+ "DxMessaging.Core.Messages.ITargetedMessage";
34
+ private const string UntargetedInterfaceFullName =
35
+ "DxMessaging.Core.Messages.IUntargetedMessage";
36
+
37
+ // Diagnostics
38
+ private static readonly DiagnosticDescriptor MultipleAttributesError = new(
39
+ id: "DXMSG002",
40
+ title: "Multiple Message Attributes",
41
+ messageFormat: "Type '{0}' cannot have more than one Dx message attribute ([DxBroadcastMessage], [DxTargetedMessage], [DxUntargetedMessage]).",
42
+ category: "DxMessaging",
43
+ defaultSeverity: DiagnosticSeverity.Error,
44
+ isEnabledByDefault: true
45
+ );
101
46
 
102
- // --- Step 3: Filter out invalid targets ---
103
- IncrementalValuesProvider<SemanticTargetInfo> validSemanticTargets = semanticTargets
104
- .Where(static target => target.HasValue)
105
- .Select(static (target, _) => target!.Value); // Use non-null assertion or keep filtering
47
+ // Information needed during the generation phase for a valid message type
48
+ private record struct MessageToGenerateInfo(
49
+ INamedTypeSymbol TypeSymbol,
50
+ TypeDeclarationSyntax DeclarationSyntax,
51
+ string TargetInterfaceFullName // The specific interface like IBroadcastMessage
52
+ );
106
53
 
107
- // --- Step 4: Calculate Hash IDs ---
108
- IncrementalValuesProvider<MessageInfo> typesWithIds = validSemanticTargets.Select(
109
- static (target, ct) =>
110
- {
111
- string fullyQualifiedName = target.TypeSymbol.ToDisplayString(
112
- SymbolDisplayFormat.FullyQualifiedFormat
113
- );
114
- int generatedId = ComputeStableHashCode(fullyQualifiedName);
115
- return new MessageInfo(
116
- target.TypeSymbol,
117
- target.DeclarationSyntax,
118
- target.TargetInterfaceFullName,
119
- generatedId
54
+ public void Initialize(IncrementalGeneratorInitializationContext context)
55
+ {
56
+ // Find all class/struct/record declarations with attributes
57
+ IncrementalValuesProvider<TypeDeclarationSyntax> potentialTypeDeclarations =
58
+ context.SyntaxProvider.CreateSyntaxProvider(
59
+ predicate: static (node, _) => IsSyntaxTargetForGeneration(node),
60
+ transform: static (ctx, _) => (TypeDeclarationSyntax)ctx.Node
120
61
  );
121
- }
122
- );
123
62
 
124
- // --- Step 5: Collect all valid types with IDs ---
125
- IncrementalValueProvider<(Compilation, ImmutableArray<MessageInfo>)> compilationAndTypes =
126
- context.CompilationProvider.Combine(typesWithIds.Collect());
63
+ // Get semantic info for potential types
64
+ IncrementalValuesProvider<MessageToGenerateInfo?> semanticTargets =
65
+ potentialTypeDeclarations
66
+ .Combine(context.CompilationProvider)
67
+ .Select(
68
+ static (data, ct) =>
69
+ GetSemanticTargetForGeneration(data.Left, data.Right, ct)
70
+ );
127
71
 
128
- // --- Step 6: Generate source or diagnostics ---
129
- context.RegisterSourceOutput(
130
- compilationAndTypes,
131
- static (spc, source) => Execute(source.Item1, source.Item2, spc)
132
- );
133
- }
72
+ // Filter out nulls (types that aren't valid messages)
73
+ IncrementalValuesProvider<MessageToGenerateInfo> validSemanticTargets = semanticTargets
74
+ .Where(static target => target.HasValue)
75
+ .Select(static (target, _) => target!.Value);
134
76
 
135
- // Quick syntax filter: Checks if the node is a class or struct with any attributes
136
- private static bool IsSyntaxTargetForGeneration(SyntaxNode node) =>
137
- node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } typeDecl
138
- && (
139
- typeDecl.IsKind(SyntaxKind.ClassDeclaration)
140
- || typeDecl.IsKind(SyntaxKind.StructDeclaration)
141
- );
77
+ // Group by type symbol to handle partial classes correctly and check for multiple attributes
78
+ IncrementalValueProvider<ImmutableArray<MessageToGenerateInfo>> collectedTargets =
79
+ validSemanticTargets.Collect();
142
80
 
143
- // Semantic filter: Checks if the type has exactly one of the target attributes
144
- private static SemanticTargetInfo? GetSemanticTargetForGeneration(
145
- TypeDeclarationSyntax typeDeclarationSyntax,
146
- Compilation compilation,
147
- CancellationToken cancellationToken
148
- )
149
- {
150
- SemanticModel semanticModel = compilation.GetSemanticModel(
151
- typeDeclarationSyntax.SyntaxTree
152
- );
153
- if (
154
- semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken)
155
- is not INamedTypeSymbol typeSymbol
156
- )
157
- {
158
- return null;
81
+ IncrementalValueProvider<(
82
+ Compilation,
83
+ ImmutableArray<MessageToGenerateInfo>
84
+ )> compilationAndTypes = context.CompilationProvider.Combine(collectedTargets);
85
+
86
+ // Register the source output step
87
+ context.RegisterSourceOutput(
88
+ compilationAndTypes,
89
+ static (spc, source) => Execute(source.Item1, source.Item2, spc)
90
+ );
159
91
  }
160
92
 
161
- string? foundTargetInterface = null;
162
- bool multipleAttributes = false;
93
+ private static bool IsSyntaxTargetForGeneration(SyntaxNode node) =>
94
+ node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } typeDecl
95
+ && (
96
+ typeDecl.IsKind(SyntaxKind.ClassDeclaration)
97
+ || typeDecl.IsKind(SyntaxKind.StructDeclaration)
98
+ || typeDecl.IsKind(SyntaxKind.RecordDeclaration)
99
+ || typeDecl.IsKind(SyntaxKind.RecordStructDeclaration)
100
+ );
163
101
 
164
- foreach (AttributeData attributeData in typeSymbol.GetAttributes())
102
+ private static MessageToGenerateInfo? GetSemanticTargetForGeneration(
103
+ TypeDeclarationSyntax typeDeclarationSyntax,
104
+ Compilation compilation,
105
+ CancellationToken cancellationToken
106
+ )
165
107
  {
166
- cancellationToken.ThrowIfCancellationRequested();
167
- if (attributeData.AttributeClass == null)
168
- continue;
169
-
170
- string currentAttributeFullName = attributeData.AttributeClass.ToDisplayString();
171
- string? targetInterfaceForThisAttribute = null;
172
-
173
- switch (currentAttributeFullName)
108
+ SemanticModel semanticModel = compilation.GetSemanticModel(
109
+ typeDeclarationSyntax.SyntaxTree
110
+ );
111
+ if (
112
+ semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, cancellationToken)
113
+ is not INamedTypeSymbol typeSymbol
114
+ )
174
115
  {
175
- case BroadcastAttrFullName:
176
- targetInterfaceForThisAttribute = BroadcastInterfaceFullName;
177
- break;
178
- case TargetedAttrFullName:
179
- targetInterfaceForThisAttribute = TargetedInterfaceFullName;
180
- break;
181
- case UntargetedAttrFullName:
182
- targetInterfaceForThisAttribute = UntargetedInterfaceFullName;
183
- break;
116
+ return null;
184
117
  }
185
118
 
186
- if (targetInterfaceForThisAttribute != null)
119
+ // Ensure it's not abstract or static (if class)
120
+ if (
121
+ typeSymbol.IsAbstract
122
+ || (typeSymbol.IsStatic && typeSymbol.TypeKind == TypeKind.Class)
123
+ )
187
124
  {
188
- if (foundTargetInterface != null)
189
- {
190
- // Found more than one relevant attribute!
191
- multipleAttributes = true;
192
- break; // No need to check further attributes
193
- }
194
- foundTargetInterface = targetInterfaceForThisAttribute;
125
+ return null; // Cannot be a concrete message type
195
126
  }
196
- }
197
-
198
- if (multipleAttributes || foundTargetInterface == null)
199
- {
200
- // Report error for multiple attributes later in Execute if needed,
201
- // but don't consider this a valid target for generation now.
202
- // If foundTargetInterface is null, it didn't have any relevant attribute.
203
- return null;
204
- }
205
-
206
- // Found exactly one relevant attribute
207
- return new SemanticTargetInfo(typeSymbol, typeDeclarationSyntax, foundTargetInterface);
208
- }
209
127
 
210
- // --- Main execution logic: collision check, warning, and source generation ---
211
- private static void Execute(
212
- Compilation compilation,
213
- ImmutableArray<MessageInfo> typesToGenerate,
214
- SourceProductionContext context
215
- )
216
- {
217
- if (typesToGenerate.IsDefaultOrEmpty)
218
- {
219
- return; // Nothing to do
220
- }
128
+ string? foundTargetInterface = null;
129
+ bool multipleAttributes = false;
221
130
 
222
- var validTypes = new List<MessageInfo>(typesToGenerate.Length);
223
- var typesWithMultipleAttributes = new HashSet<ISymbol>(SymbolEqualityComparer.Default);
224
- var generatedIds = new HashSet<int>();
131
+ // Check attributes to find the specific message type (Broadcast, Targeted, etc.)
132
+ foreach (AttributeData attributeData in typeSymbol.GetAttributes())
133
+ {
134
+ cancellationToken.ThrowIfCancellationRequested();
135
+ string? currentAttributeFullName = attributeData.AttributeClass?.ToDisplayString();
136
+ string? targetInterfaceForThisAttribute = null;
225
137
 
226
- // --- Step 1: Check for Multiple Attributes on the same type ---
227
- var groupedByType = typesToGenerate.GroupBy(
228
- m => m.TypeSymbol,
229
- SymbolEqualityComparer.Default
230
- );
138
+ switch (currentAttributeFullName)
139
+ {
140
+ case BroadcastAttrFullName:
141
+ targetInterfaceForThisAttribute = BroadcastInterfaceFullName;
142
+ break;
143
+ case TargetedAttrFullName:
144
+ targetInterfaceForThisAttribute = TargetedInterfaceFullName;
145
+ break;
146
+ case UntargetedAttrFullName:
147
+ targetInterfaceForThisAttribute = UntargetedInterfaceFullName;
148
+ break;
149
+ }
231
150
 
232
- foreach (var group in groupedByType)
233
- {
234
- if (group.Count() > 1)
235
- {
236
- ISymbol collidingSymbol = group.Key;
237
- typesWithMultipleAttributes.Add(collidingSymbol);
238
- context.ReportDiagnostic(
239
- Diagnostic.Create(
240
- MultipleAttributesError,
241
- collidingSymbol.Locations.FirstOrDefault() ?? Location.None,
242
- collidingSymbol.ToDisplayString()
151
+ if (targetInterfaceForThisAttribute != null)
152
+ {
153
+ if (
154
+ foundTargetInterface != null
155
+ && foundTargetInterface != targetInterfaceForThisAttribute
243
156
  )
244
- );
157
+ {
158
+ multipleAttributes = true;
159
+ break;
160
+ }
161
+ foundTargetInterface = targetInterfaceForThisAttribute;
162
+ }
245
163
  }
246
- else
164
+
165
+ if (multipleAttributes || foundTargetInterface == null)
247
166
  {
248
- // Add types with only one attribute to the list for further processing
249
- validTypes.Add(group.First());
167
+ // Don't return info if multiple different message attrs or none found.
168
+ // The Execute method will report the error for multiple attributes later.
169
+ return null;
250
170
  }
251
- }
252
171
 
253
- // If no types remain after filtering multi-attribute ones, exit
254
- if (validTypes.Count == 0)
255
- {
256
- return;
172
+ return new MessageToGenerateInfo(
173
+ typeSymbol,
174
+ typeDeclarationSyntax,
175
+ foundTargetInterface
176
+ );
257
177
  }
258
178
 
259
- // --- Step 2: Assign Final IDs (Handling Collisions) ---
260
- var finalAssignments = new List<FinalMessageInfo>(validTypes.Count);
261
- var collisionWarnings = new List<Diagnostic>(); // Collect warnings
262
- int fallbackIdCounter = -1; // Start fallback IDs from -1 and go down
263
-
264
- // Group by the initial hash ID to find collisions
265
- var groupedByHash = validTypes.GroupBy(m => m.GeneratedId);
266
-
267
- foreach (var group in groupedByHash)
179
+ private static void Execute(
180
+ Compilation compilation,
181
+ ImmutableArray<MessageToGenerateInfo> typesToGenerate,
182
+ SourceProductionContext context
183
+ )
268
184
  {
269
- if (group.Count() == 1)
185
+ if (typesToGenerate.IsDefaultOrEmpty)
270
186
  {
271
- // No collision for this hash ID
272
- var info = group.First();
273
- generatedIds.Add(info.GeneratedId);
274
- finalAssignments.Add(
275
- new FinalMessageInfo(
276
- info.TypeSymbol,
277
- info.DeclarationSyntax,
278
- info.TargetInterfaceFullName,
279
- info.GeneratedId,
280
- false
281
- )
282
- );
187
+ return;
283
188
  }
284
- else
285
- {
286
- // Collision detected for this hash ID!
287
- // Sort colliding types deterministically (e.g., by fully qualified name)
288
- var sortedCollidingTypes = group
289
- .OrderBy(
290
- m => m.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
291
- StringComparer.Ordinal
292
- )
293
- .ToList();
294
-
295
- // The first type keeps the original hash ID (arbitrary but deterministic choice)
296
- var firstInfo = sortedCollidingTypes[0];
297
- generatedIds.Add(firstInfo.GeneratedId);
298
- finalAssignments.Add(
299
- new FinalMessageInfo(
300
- firstInfo.TypeSymbol,
301
- firstInfo.DeclarationSyntax,
302
- firstInfo.TargetInterfaceFullName,
303
- firstInfo.GeneratedId,
304
- true
305
- )
306
- ); // Mark as part of collision
307
- // Prepare warning for this type (kept original ID but was part of collision)
308
- collisionWarnings.Add(
309
- Diagnostic.Create(
310
- CollisionWarning,
311
- firstInfo.DeclarationSyntax.Identifier.GetLocation(),
312
- firstInfo.TypeSymbol.ToDisplayString(),
313
- firstInfo.GeneratedId,
314
- group.Key
315
- )
316
- );
317
189
 
318
- // Assign sequential fallback IDs (negative numbers) to the rest
319
- for (int i = 1; i < sortedCollidingTypes.Count; i++)
190
+ // --- Step 1: Filter out types with multiple attributes applied ---
191
+ Dictionary<ISymbol, MessageToGenerateInfo> uniqueTypes = new(
192
+ SymbolEqualityComparer.Default
193
+ );
194
+ HashSet<ISymbol> typesWithMultipleAttributes = new(SymbolEqualityComparer.Default);
195
+
196
+ foreach (MessageToGenerateInfo typeInfo in typesToGenerate)
197
+ {
198
+ if (uniqueTypes.ContainsKey(typeInfo.TypeSymbol))
320
199
  {
321
- var fallbackInfo = sortedCollidingTypes[i];
322
- int assignedFallbackId = fallbackIdCounter--; // Assign next negative ID
323
- while (!generatedIds.Add(assignedFallbackId))
200
+ // If adding fails, it means the same TypeSymbol appeared multiple times.
201
+ // This implies multiple different valid attributes were found, report error.
202
+ if (typesWithMultipleAttributes.Add(typeInfo.TypeSymbol)) // Report only once
324
203
  {
325
- assignedFallbackId--;
204
+ context.ReportDiagnostic(
205
+ Diagnostic.Create(
206
+ MultipleAttributesError,
207
+ typeInfo.DeclarationSyntax.Identifier.GetLocation(),
208
+ typeInfo.TypeSymbol.ToDisplayString()
209
+ )
210
+ );
211
+ // Also report for the one already in the dictionary if needed, but one report per type is usually sufficient.
326
212
  }
327
- finalAssignments.Add(
328
- new FinalMessageInfo(
329
- fallbackInfo.TypeSymbol,
330
- fallbackInfo.DeclarationSyntax,
331
- fallbackInfo.TargetInterfaceFullName,
332
- assignedFallbackId,
333
- true
334
- )
335
- ); // Mark as part of collision, use new ID
336
- // Prepare warning for this type (received fallback ID)
337
- collisionWarnings.Add(
338
- Diagnostic.Create(
339
- CollisionWarning,
340
- fallbackInfo.DeclarationSyntax.Identifier.GetLocation(),
341
- fallbackInfo.TypeSymbol.ToDisplayString(),
342
- assignedFallbackId,
343
- group.Key
344
- )
345
- );
213
+ }
214
+ else
215
+ {
216
+ uniqueTypes[typeInfo.TypeSymbol] = typeInfo;
346
217
  }
347
218
  }
348
- }
349
219
 
350
- // --- Step 3: Report all collected warnings ---
351
- foreach (var warning in collisionWarnings)
352
- {
353
- context.ReportDiagnostic(warning);
354
- }
355
-
356
- // --- Step 4: Generate Source using the final assigned IDs ---
357
- foreach (var finalInfo in finalAssignments)
358
- {
359
- context.CancellationToken.ThrowIfCancellationRequested();
360
-
361
- // Generate source using the final ID (hash or fallback)
362
- string source = GenerateSource( // Call simplified GenerateSource
363
- finalInfo.TargetInterfaceFullName,
364
- finalInfo.TypeSymbol,
365
- finalInfo.FinalId
366
- ); // Pass the FINAL ID
367
-
368
- context.AddSource(
369
- $"{finalInfo.TypeSymbol.Name}_{finalInfo.TargetInterfaceFullName.Split('.').Last()}.g.cs",
370
- SourceText.From(source, Encoding.UTF8)
371
- );
372
- }
373
- }
220
+ List<MessageToGenerateInfo> validSingleAttrTypes = uniqueTypes
221
+ .Where(kvp => !typesWithMultipleAttributes.Contains(kvp.Key))
222
+ .Select(kvp => kvp.Value)
223
+ .ToList();
374
224
 
375
- // --- Stable Hash Function (FNV-1a - remains the same) ---
376
- private static int ComputeStableHashCode(string text)
377
- {
378
- unchecked
379
- {
380
- uint hash = 2166136261;
381
- foreach (char c in text)
225
+ if (validSingleAttrTypes.Count == 0)
382
226
  {
383
- hash = (hash ^ c) * 16777619;
227
+ return;
384
228
  }
385
- return (int)hash;
386
- }
387
- }
388
229
 
389
- // --- Source Generation Logic (Takes target interface name AND optimization flag) ---
390
- private static string GenerateSource(
391
- string targetInterfaceFullName,
392
- INamedTypeSymbol typeSymbol,
393
- int finalAssignedId
394
- ) // Now takes the final assigned ID
395
- {
396
- string namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
397
- ? string.Empty
398
- : typeSymbol.ContainingNamespace.ToDisplayString();
230
+ // --- Step 2: Generate sources for each valid message type ---
231
+ foreach (MessageToGenerateInfo messageInfo in validSingleAttrTypes)
232
+ {
233
+ context.CancellationToken.ThrowIfCancellationRequested();
399
234
 
400
- string typeNameWithGenerics = typeSymbol.ToDisplayString(
401
- SymbolDisplayFormat.MinimallyQualifiedFormat
402
- );
403
- string fullyQualifiedName = typeSymbol.ToDisplayString(
404
- SymbolDisplayFormat.FullyQualifiedFormat
405
- );
235
+ // Generate the partial IMessage implementation source
236
+ string implSource = GenerateImplementationSource(
237
+ messageInfo.TargetInterfaceFullName,
238
+ messageInfo.TypeSymbol
239
+ );
240
+ string implHintName =
241
+ $"{messageInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.IMessage.g.cs"
242
+ .Replace("global::", "")
243
+ .Replace("<", "_")
244
+ .Replace(">", "_")
245
+ .Replace(",", "_"); // Clean hint name
246
+
247
+ context.AddSource(implHintName, SourceText.From(implSource, Encoding.UTF8));
248
+ }
249
+ }
406
250
 
407
- string typeKind = typeSymbol.TypeKind switch
251
+ // Generates the partial class/struct implementing IMessage
252
+ private static string GenerateImplementationSource(
253
+ string targetInterfaceFullName, // e.g., IBroadcastMessage
254
+ INamedTypeSymbol typeSymbol
255
+ )
408
256
  {
409
- TypeKind.Class => "class",
410
- TypeKind.Struct => "struct",
411
- _ => throw new InvalidOperationException(
412
- "Unsupported type kind for message generation"
413
- ),
414
- };
415
-
416
- bool alreadyDeclaresInterface = typeSymbol.Interfaces.Any(iface =>
417
- iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
418
- == targetInterfaceFullName
419
- );
420
-
421
- string interfaceDeclaration = alreadyDeclaresInterface
422
- ? ""
423
- : $": {targetInterfaceFullName}";
424
-
425
- return $$"""
426
- // <auto-generated by DxMessageIdGenerator/>
427
- #pragma warning disable
428
- #nullable enable annotations
257
+ string namespaceName = typeSymbol.ContainingNamespace.IsGlobalNamespace
258
+ ? string.Empty
259
+ : typeSymbol.ContainingNamespace.ToDisplayString();
260
+ string namespaceBlockOpen = string.IsNullOrEmpty(namespaceName)
261
+ ? "namespace\n{"
262
+ : $"namespace {namespaceName}\n{{";
263
+ const string namespaceBlockClose = "}";
264
+ const string indent = " ";
265
+
266
+ string typeNameWithGenerics = typeSymbol.ToDisplayString(
267
+ SymbolDisplayFormat.MinimallyQualifiedFormat
268
+ );
269
+ string fullyQualifiedName = typeSymbol.ToDisplayString(
270
+ SymbolDisplayFormat.FullyQualifiedFormat
271
+ );
429
272
 
430
- namespace {{namespaceName}}
273
+ string typeKind = typeSymbol.TypeKind switch
431
274
  {
432
- partial {{typeKind}} {{typeNameWithGenerics}} {{interfaceDeclaration}}
433
- {
434
- // Explicitly implement IMessage members using the base interface name
435
-
436
- /// <inheritdoc/>
437
- System.Type {{BaseInterfaceFullName}}.MessageType => typeof({{fullyQualifiedName}});
275
+ TypeKind.Class => typeSymbol.IsRecord ? "record class" : "class",
276
+ TypeKind.Struct => typeSymbol.IsRecord ? "record struct" : "struct",
277
+ _ => throw new InvalidOperationException("Unsupported type kind"),
278
+ };
438
279
 
439
- /// <inheritdoc/>
440
- int? {{BaseInterfaceFullName}}.OptimizedMessageId => {{finalAssignedId}};
441
- }
442
- }
443
- """;
280
+ string accessibility = typeSymbol.DeclaredAccessibility switch
281
+ {
282
+ Accessibility.Public => "public ",
283
+ Accessibility.Internal => "internal ",
284
+ // Add others if necessary, default to internal if restrictive
285
+ _ => "internal ",
286
+ };
287
+
288
+ string interfaceDeclaration = $", global::{targetInterfaceFullName}";
289
+
290
+ return $$"""
291
+ // <auto-generated by DxMessageIdGenerator/>
292
+ #pragma warning disable
293
+ #nullable enable annotations
294
+
295
+ {{namespaceBlockOpen}}
296
+ {{indent}}// Partial implementation for {{typeNameWithGenerics}} to implement {{BaseInterfaceFullName}}
297
+ {{indent}}{{accessibility}}partial {{typeKind}} {{typeNameWithGenerics}} : global::{{BaseInterfaceFullName}} {{interfaceDeclaration}}
298
+ {{indent}}{
299
+ {{indent}} /// <inheritdoc/>
300
+ {{indent}} public global::System.Type MessageType => typeof({{fullyQualifiedName}});
301
+ {{indent}}}
302
+ {{namespaceBlockClose}}
303
+ """;
304
+ }
444
305
  }
445
306
  }