com.wallstop-studios.dxmessaging 2.0.0-rc21 → 2.0.0-rc23
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.
- package/.github/workflows/npm-publish.yml +9 -0
- package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
- package/README.md +12 -7
- package/Runtime/Core/Extensions/EnumExtensions.cs +37 -0
- package/Runtime/Core/Extensions/EnumExtensions.cs.meta +3 -0
- package/Runtime/Core/Helper/DxMessagingRuntime.cs +201 -0
- package/Runtime/Core/Helper/DxMessagingRuntime.cs.meta +3 -0
- package/Runtime/Core/Helper/MessageCache.cs +105 -0
- package/Runtime/Core/Helper/MessageCache.cs.meta +3 -0
- package/Runtime/Core/Helper/MessageHelperIndexer.cs +9 -0
- package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +3 -0
- package/Runtime/Core/Helper.meta +3 -0
- package/Runtime/Core/IMessage.cs +0 -13
- package/Runtime/Core/InstanceId.cs +0 -2
- package/Runtime/Core/MessageBus/IMessageBus.cs +10 -3
- package/Runtime/Core/MessageBus/MessageBus.cs +400 -98
- package/Runtime/Core/MessageHandler.cs +51 -119
- package/Runtime/Core/Messages/DxReflexiveMessage.cs +132 -0
- package/Runtime/Core/Messages/DxReflexiveMessage.cs.meta +3 -0
- package/Runtime/Core/MessagingDebug.cs +1 -1
- package/Runtime/Unity/MessageAwareComponent.cs +34 -13
- package/Runtime/Unity/Messages/GlobalStringMessage.cs +15 -0
- package/Runtime/Unity/Messages/GlobalStringMessage.cs.meta +3 -0
- package/Runtime/Unity/Messages/StringMessage.cs +15 -0
- package/Runtime/Unity/Messages/StringMessage.cs.meta +3 -0
- package/Runtime/Unity/Messages.meta +3 -0
- package/Runtime/WallstopStudios.DxMessaging.asmdef +1 -1
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +255 -394
- package/Tests/Runtime/Benchmarks/PerformanceTests.cs +141 -1
- package/Tests/Runtime/Core/MessagingTestBase.cs +3 -11
- package/Tests/Runtime/Scripts/Components/SimpleMessageAwareComponent.cs +12 -0
- package/package.json +1 -1
package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
102
|
+
private static MessageToGenerateInfo? GetSemanticTargetForGeneration(
|
|
103
|
+
TypeDeclarationSyntax typeDeclarationSyntax,
|
|
104
|
+
Compilation compilation,
|
|
105
|
+
CancellationToken cancellationToken
|
|
106
|
+
)
|
|
165
107
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
164
|
+
|
|
165
|
+
if (multipleAttributes || foundTargetInterface == null)
|
|
247
166
|
{
|
|
248
|
-
//
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
172
|
+
return new MessageToGenerateInfo(
|
|
173
|
+
typeSymbol,
|
|
174
|
+
typeDeclarationSyntax,
|
|
175
|
+
foundTargetInterface
|
|
176
|
+
);
|
|
257
177
|
}
|
|
258
178
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 (
|
|
185
|
+
if (typesToGenerate.IsDefaultOrEmpty)
|
|
270
186
|
{
|
|
271
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
return;
|
|
384
228
|
}
|
|
385
|
-
return (int)hash;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
229
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
273
|
+
string typeKind = typeSymbol.TypeKind switch
|
|
431
274
|
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
}
|