com.wallstop-studios.dxmessaging 2.1.1 → 2.1.3

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.
Files changed (158) hide show
  1. package/.github/workflows/dotnet-tests.yml +72 -0
  2. package/.lychee.toml +4 -2
  3. package/AGENTS.md +13 -12
  4. package/Docs/Comparisons.md +5 -5
  5. package/Docs/Install.md +2 -1
  6. package/Docs/InterceptorsAndOrdering.md +1 -1
  7. package/Docs/Performance.md +15 -13
  8. package/Docs/QuickReference.md +1 -1
  9. package/Docs/Reference.md +5 -5
  10. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  11. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +13 -2
  12. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  13. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +11 -0
  14. package/Editor/Analyzers/System.Collections.Immutable.dll +0 -0
  15. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +11 -0
  16. package/Editor/Analyzers/System.Reflection.Metadata.dll +0 -0
  17. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +13 -2
  18. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  19. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +11 -0
  20. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  21. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +3 -2
  22. package/Editor/AssemblyInfo.cs +3 -0
  23. package/Editor/AssemblyInfo.cs.meta +3 -0
  24. package/Editor/CustomEditors/MessagingComponentEditor.cs +24 -0
  25. package/Editor/DxMessagingEditorInitializer.cs +58 -1
  26. package/Editor/DxMessagingMenu.cs +38 -0
  27. package/Editor/DxMessagingMenu.cs.meta +11 -0
  28. package/Editor/DxMessagingSceneBuildProcessor.cs +81 -0
  29. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -0
  30. package/Editor/Settings/DxMessagingSettings.cs +37 -6
  31. package/Editor/Settings/DxMessagingSettingsProvider.cs +45 -7
  32. package/Editor/SetupCscRsp.cs +133 -53
  33. package/Editor/Testing/MessagingComponentEditorHarness.cs +218 -0
  34. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -0
  35. package/Editor/Testing.meta +3 -0
  36. package/README.md +10 -4
  37. package/Runtime/AssemblyInfo.cs +1 -0
  38. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +52 -0
  39. package/Runtime/Core/DataStructure/CyclicBuffer.cs +16 -0
  40. package/Runtime/Core/Diagnostics/MessageEmissionData.cs +27 -12
  41. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs +62 -0
  42. package/Runtime/Core/DxMessagingStaticState.cs +108 -0
  43. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -0
  44. package/Runtime/Core/Extensions/IListExtensions.cs +24 -0
  45. package/Runtime/Core/Extensions/MessageBusExtensions.cs +144 -2
  46. package/Runtime/Core/Extensions/MessageExtensions.cs +2 -2
  47. package/Runtime/Core/Helper/MessageCache.cs +16 -0
  48. package/Runtime/Core/Helper/MessageHelperIndexer.cs +77 -0
  49. package/Runtime/Core/InstanceId.cs +91 -3
  50. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs +31 -0
  51. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -0
  52. package/Runtime/Core/MessageBus/IMessageBus.cs +44 -16
  53. package/Runtime/Core/MessageBus/MessageBus.cs +96 -25
  54. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +46 -2
  55. package/Runtime/Core/MessageBus/MessagingRegistration.cs +63 -5
  56. package/Runtime/Core/MessageBus/RegistrationLog.cs +10 -0
  57. package/Runtime/Core/MessageHandler.cs +141 -8
  58. package/Runtime/Core/MessageRegistrationHandle.cs +59 -0
  59. package/Runtime/Core/MessageRegistrationToken.cs +20 -4
  60. package/Runtime/Core/Messages/ReflexiveMessage.cs +38 -0
  61. package/Runtime/Core/MessagingDebug.cs +16 -1
  62. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs +6 -0
  63. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs +19 -0
  64. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -0
  65. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs +6 -0
  66. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +19 -0
  67. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +10 -0
  68. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +14 -0
  69. package/Runtime/Unity/MessageAwareComponent.cs +2 -0
  70. package/Runtime/Unity/MessageBusProviderHandle.cs +4 -0
  71. package/Runtime/Unity/MessagingComponent.cs +109 -0
  72. package/Runtime/Unity/MessagingComponentInstaller.cs +2 -0
  73. package/Runtime/Unity/ScriptableMessageBusProvider.cs +2 -0
  74. package/Samples~/DI/README.md +13 -13
  75. package/Samples~/Mini Combat/README.md +15 -15
  76. package/Samples~/Mini Combat/Walkthrough.md +12 -12
  77. package/Samples~/UI Buttons + Inspector/README.md +4 -4
  78. package/SourceGenerators/Directory.Build.props +9 -0
  79. package/{Tests/Runtime/WallstopStudios.DxMessaging.Tests.Runtime.csproj.meta → SourceGenerators/Directory.Build.props.meta} +1 -1
  80. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +23 -24
  81. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +91 -27
  82. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +24 -4
  83. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DocsSnippetCompilationTests.cs +193 -0
  84. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DocsSnippetCompilationTests.cs.meta +11 -0
  85. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxAutoConstructorGeneratorDiagnosticsTests.cs +69 -0
  86. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxAutoConstructorGeneratorDiagnosticsTests.cs.meta +11 -0
  87. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxMessageIdGeneratorDiagnosticsTests.cs +66 -0
  88. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxMessageIdGeneratorDiagnosticsTests.cs.meta +11 -0
  89. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/GeneratorTestUtilities.cs +155 -0
  90. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/GeneratorTestUtilities.cs.meta +11 -0
  91. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/WallstopStudios.DxMessaging.SourceGenerators.Tests.csproj +20 -0
  92. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/WallstopStudios.DxMessaging.SourceGenerators.Tests.csproj.meta +7 -0
  93. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests.meta +8 -0
  94. package/Tests/Editor/MessagingComponentEditorHarnessTests.cs +243 -0
  95. package/Tests/Editor/MessagingComponentEditorHarnessTests.cs.meta +3 -0
  96. package/Tests/Editor/MessagingComponentSerializationTests.cs +129 -0
  97. package/Tests/Editor/MessagingComponentSerializationTests.cs.meta +3 -0
  98. package/Tests/Editor/WallstopStudios.DxMessaging.Tests.Editor.asmdef +19 -0
  99. package/Tests/Editor/WallstopStudios.DxMessaging.Tests.Editor.asmdef.meta +3 -0
  100. package/Tests/Editor.meta +3 -0
  101. package/Tests/Runtime/Benchmarks/BenchmarkSession.cs +3 -0
  102. package/Tests/Runtime/Benchmarks/BenchmarkTestBase.cs +3 -0
  103. package/Tests/Runtime/Benchmarks/ComparisonPerformanceTests.cs +3 -0
  104. package/Tests/Runtime/Benchmarks/PerformanceTests.cs +137 -0
  105. package/Tests/Runtime/Core/AlternateBusTests.cs +3 -0
  106. package/Tests/Runtime/Core/BroadcastTests.cs +3 -0
  107. package/Tests/Runtime/Core/CyclicBufferTests.cs +3 -0
  108. package/Tests/Runtime/Core/DefaultBusFallbackTests.cs +5 -2
  109. package/Tests/Runtime/Core/DiagnosticsTests.cs +6 -3
  110. package/Tests/Runtime/Core/DxMessagingStaticStateTests.cs +69 -0
  111. package/Tests/Runtime/Core/DxMessagingStaticStateTests.cs.meta +11 -0
  112. package/Tests/Runtime/Core/EdgeCaseTests.cs +3 -0
  113. package/Tests/Runtime/Core/EnablementTests.cs +3 -0
  114. package/Tests/Runtime/Core/Extensions/MessageExtensionsProviderTests.cs +2 -2
  115. package/Tests/Runtime/Core/GenericMessageTests.cs +3 -0
  116. package/Tests/Runtime/Core/GlobalAcceptAllTests.cs +3 -0
  117. package/Tests/Runtime/Core/InterceptorCancellationTests.cs +3 -0
  118. package/Tests/Runtime/Core/LifecycleTests.cs +3 -0
  119. package/Tests/Runtime/Core/MessageEmissionDataTests.cs +70 -0
  120. package/Tests/Runtime/Core/MessageEmissionDataTests.cs.meta +11 -0
  121. package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs +3 -0
  122. package/Tests/Runtime/Core/MessagingTestBase.cs +3 -0
  123. package/Tests/Runtime/Core/MutationDedupeTests.cs +3 -0
  124. package/Tests/Runtime/Core/MutationDestructionTests.cs +3 -0
  125. package/Tests/Runtime/Core/MutationDuringEmissionTests.cs +3 -0
  126. package/Tests/Runtime/Core/MutationGlobalAddTests.cs +3 -0
  127. package/Tests/Runtime/Core/MutationInterceptorTests.cs +3 -0
  128. package/Tests/Runtime/Core/MutationPostProcessorAcrossHandlersTests.cs +3 -0
  129. package/Tests/Runtime/Core/MutationPostProcessorMoreTests.cs +3 -0
  130. package/Tests/Runtime/Core/MutationPriorityTests.cs +3 -0
  131. package/Tests/Runtime/Core/NominalTests.cs +3 -0
  132. package/Tests/Runtime/Core/OrderingTests.cs +3 -0
  133. package/Tests/Runtime/Core/OverDeregistrationTests.cs +3 -0
  134. package/Tests/Runtime/Core/PostProcessorTests.cs +3 -0
  135. package/Tests/Runtime/Core/ReflexiveErrorTests.cs +3 -0
  136. package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs +4 -1
  137. package/Tests/Runtime/Core/ReflexiveTests.cs +3 -0
  138. package/Tests/Runtime/Core/RegistrationTests.cs +3 -0
  139. package/Tests/Runtime/Core/StringShorthandTests.cs +3 -0
  140. package/Tests/Runtime/Core/TargetedTests.cs +3 -0
  141. package/Tests/Runtime/Core/TypedShorthandTests.cs +3 -0
  142. package/Tests/Runtime/Core/UntargetedEquivalenceTests.cs +3 -0
  143. package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs +14 -78
  144. package/Tests/Runtime/Core/UntargetedTests.cs +3 -0
  145. package/Tests/Runtime/Integrations/Reflex/ReflexIntegrationTests.cs +4 -1
  146. package/Tests/Runtime/Integrations/VContainer/VContainerIntegrationTests.cs +3 -0
  147. package/Tests/Runtime/Integrations/Zenject/ZenjectIntegrationTests.cs +3 -0
  148. package/Tests/Runtime/Scripts/Components/GenericMessageAwareComponent.cs +3 -0
  149. package/Tests/Runtime/Scripts/Components/ManualListenerComponent.cs +3 -0
  150. package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs +3 -0
  151. package/Tests/Runtime/TestUtilities/UnityFixtureBase.cs +3 -0
  152. package/Tests/Runtime/Unity/MessageBusProviderAssetTests.cs +3 -0
  153. package/Tests/Runtime/Unity/MessageBusProviderHandleTests.cs +87 -3
  154. package/Tests/Runtime/Unity/MessagingComponentInstallerSceneTests.cs +109 -0
  155. package/Tests/Runtime/Unity/MessagingComponentInstallerSceneTests.cs.meta +11 -0
  156. package/Tests/Runtime/Unity/MessagingComponentProviderIntegrationTests.cs +159 -17
  157. package/package.json +1 -1
  158. package/Tests/Runtime/WallstopStudios.DxMessaging.Tests.Runtime.csproj +0 -7
@@ -56,6 +56,10 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
56
56
  ImmutableArray<IFieldSymbol> FieldsToInject // Public readonly non-static fields
57
57
  );
58
58
 
59
+ /// <summary>
60
+ /// Configures the incremental generator pipeline that discovers annotated types and emits constructors.
61
+ /// </summary>
62
+ /// <param name="context">Initialization context provided by Roslyn.</param>
59
63
  public void Initialize(IncrementalGeneratorInitializationContext context)
60
64
  {
61
65
  // Find all class/struct/record declarations that have attribute lists
@@ -208,7 +212,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
208
212
  // Location-specific suggestions on each non-partial container
209
213
  foreach (INamedTypeSymbol container in nonPartial)
210
214
  {
211
- SyntaxReference? sr =
215
+ SyntaxReference sr =
212
216
  container.DeclaringSyntaxReferences.FirstOrDefault();
213
217
  if (sr != null && sr.GetSyntax() is TypeDeclarationSyntax tds)
214
218
  {
@@ -258,11 +262,11 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
258
262
  ? string.Empty
259
263
  : $"namespace {namespaceName}\n{{";
260
264
  string namespaceBlockClose = string.IsNullOrEmpty(namespaceName) ? string.Empty : "}";
261
- const string indent = " ";
265
+ const string Indent = " ";
262
266
 
263
267
  // Build container wrappers for nested types so the partial can merge correctly
264
268
  var containers = new Stack<INamedTypeSymbol>();
265
- INamedTypeSymbol? current = typeSymbol.ContainingType;
269
+ INamedTypeSymbol current = typeSymbol.ContainingType;
266
270
  while (current is not null)
267
271
  {
268
272
  containers.Push(current);
@@ -271,7 +275,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
271
275
 
272
276
  var containersOpen = new StringBuilder();
273
277
  var containersClose = new StringBuilder();
274
- string currentIndent = indent; // one level inside namespace (or top-level)
278
+ string currentIndent = Indent; // one level inside namespace (or top-level)
275
279
 
276
280
  foreach (INamedTypeSymbol container in containers)
277
281
  {
@@ -306,7 +310,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
306
310
  $"{currentIndent}{containerAccessibility} partial {containerKind} {container.Name}{containerTypeParams}"
307
311
  );
308
312
  containersOpen.Append(currentIndent).AppendLine("{");
309
- currentIndent += indent;
313
+ currentIndent += Indent;
310
314
  }
311
315
 
312
316
  string innerIndent = currentIndent; // indent level for the target (innermost) type
@@ -350,13 +354,8 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
350
354
  SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers
351
355
  );
352
356
 
353
- List<(
354
- string Type,
355
- string Name,
356
- bool IsOptional,
357
- string? DefaultExpr
358
- )> parameterDetails =
359
- new List<(string Type, string Name, bool IsOptional, string? DefaultExpr)>();
357
+ List<(string Type, string Name, bool IsOptional, string DefaultExpr)> parameterDetails =
358
+ new List<(string Type, string Name, bool IsOptional, string DefaultExpr)>();
360
359
 
361
360
  // For validating expressions, use the semantic model for this type's tree
362
361
  SemanticModel semanticModel = compilation.GetSemanticModel(
@@ -368,7 +367,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
368
367
  {
369
368
  string fieldType = field.Type.ToDisplayString(fieldTypeFormat);
370
369
  string fieldName = field.Name;
371
- string? defaultExpr = null;
370
+ string defaultExpr = null;
372
371
  bool isOptional = false;
373
372
 
374
373
  foreach (AttributeData attr in field.GetAttributes())
@@ -455,10 +454,10 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
455
454
  }
456
455
  else if (arg.Kind == TypedConstantKind.Primitive)
457
456
  {
458
- object? val = arg.Value;
457
+ object val = arg.Value;
459
458
  defaultExpr = FormatLiteral(val, arg.Type);
460
459
  // Validate primitive conversion to field type
461
- ITypeSymbol? sourceType = arg.Type;
460
+ ITypeSymbol sourceType = arg.Type;
462
461
  if (sourceType != null)
463
462
  {
464
463
  Conversion conv = compilation.ClassifyConversion(
@@ -489,12 +488,12 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
489
488
  }
490
489
 
491
490
  parameterDetails.Add((fieldType, fieldName, isOptional, defaultExpr));
492
- constructorBody.AppendLine($"{indent}{indent} this.{fieldName} = {fieldName};");
491
+ constructorBody.AppendLine($"{Indent}{Indent} this.{fieldName} = {fieldName};");
493
492
  }
494
493
 
495
494
  for (int i = 0; i < parameterDetails.Count; i++)
496
495
  {
497
- (string Type, string Name, bool IsOptional, string? DefaultExpr) p =
496
+ (string Type, string Name, bool IsOptional, string DefaultExpr) p =
498
497
  parameterDetails[i];
499
498
  constructorParams.Append($"{p.Type} {p.Name}");
500
499
  if (p.IsOptional)
@@ -520,7 +519,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
520
519
  {
521
520
  currentIndent = currentIndent.Substring(
522
521
  0,
523
- Math.Max(0, currentIndent.Length - indent.Length)
522
+ Math.Max(0, currentIndent.Length - Indent.Length)
524
523
  );
525
524
  containersClose.Append(currentIndent).AppendLine("}");
526
525
  }
@@ -533,9 +532,9 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
533
532
  {{namespaceBlockOpen}}
534
533
  {{containersOpen}}{{innerIndent}}{{typeAccessibility}} partial {{typeKind}} {{typeName}}
535
534
  {{innerIndent}}{
536
- {{indent}} /// <summary>
537
- {{indent}} /// Auto-generated constructor by DxAutoGenConstructorGenerator.
538
- {{indent}} /// </summary>
535
+ {{Indent}} /// <summary>
536
+ {{Indent}} /// Auto-generated constructor by DxAutoGenConstructorGenerator.
537
+ {{Indent}} /// </summary>
539
538
  {{innerIndent}} {{constructorAccessibility}} {{typeSymbol.Name}}({{constructorParams}})
540
539
  {{innerIndent}} {
541
540
  {{constructorBody}}
@@ -546,7 +545,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
546
545
  """;
547
546
  }
548
547
 
549
- private static string FormatLiteral(object? value, ITypeSymbol? type)
548
+ private static string FormatLiteral(object value, ITypeSymbol type)
550
549
  {
551
550
  if (value == null)
552
551
  {
@@ -635,7 +634,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
635
634
  SpeculativeBindingOption.BindAsExpression
636
635
  );
637
636
 
638
- ITypeSymbol? sourceType = typeInfo.Type;
637
+ ITypeSymbol sourceType = typeInfo.Type;
639
638
  if (sourceType == null)
640
639
  {
641
640
  // Could not bind; let the compiler decide but report as invalid here
@@ -655,7 +654,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
655
654
  private static List<INamedTypeSymbol> GetNonPartialContainers(INamedTypeSymbol typeSymbol)
656
655
  {
657
656
  List<INamedTypeSymbol> result = new();
658
- INamedTypeSymbol? current = typeSymbol.ContainingType;
657
+ INamedTypeSymbol current = typeSymbol.ContainingType;
659
658
  while (current is not null)
660
659
  {
661
660
  if (!IsDeclaredFullyPartial(current))
@@ -66,9 +66,14 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
66
66
  private record struct MessageToGenerateInfo(
67
67
  INamedTypeSymbol TypeSymbol,
68
68
  TypeDeclarationSyntax DeclarationSyntax,
69
- string TargetInterfaceFullName // The specific interface like IBroadcastMessage
69
+ string TargetInterfaceFullName,
70
+ bool HasConflictingMessageAttributes
70
71
  );
71
72
 
73
+ /// <summary>
74
+ /// Configures the incremental generator pipeline that assigns deterministic message identifiers.
75
+ /// </summary>
76
+ /// <param name="context">Initialization context provided by Roslyn.</param>
72
77
  public void Initialize(IncrementalGeneratorInitializationContext context)
73
78
  {
74
79
  // Find all class/struct/record declarations with attributes
@@ -143,15 +148,15 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
143
148
  return null; // Cannot be a concrete message type
144
149
  }
145
150
 
146
- string? foundTargetInterface = null;
151
+ string foundTargetInterface = null;
147
152
  bool multipleAttributes = false;
148
153
 
149
154
  // Check attributes to find the specific message type (Broadcast, Targeted, etc.)
150
155
  foreach (AttributeData attributeData in typeSymbol.GetAttributes())
151
156
  {
152
157
  cancellationToken.ThrowIfCancellationRequested();
153
- string? currentAttributeFullName = attributeData.AttributeClass?.ToDisplayString();
154
- string? targetInterfaceForThisAttribute = null;
158
+ string currentAttributeFullName = attributeData.AttributeClass?.ToDisplayString();
159
+ string targetInterfaceForThisAttribute = null;
155
160
 
156
161
  switch (currentAttributeFullName)
157
162
  {
@@ -180,17 +185,21 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
180
185
  }
181
186
  }
182
187
 
183
- if (multipleAttributes || foundTargetInterface == null)
188
+ if (multipleAttributes)
189
+ {
190
+ foundTargetInterface = null;
191
+ }
192
+
193
+ if (foundTargetInterface == null && !multipleAttributes)
184
194
  {
185
- // Don't return info if multiple different message attrs or none found.
186
- // The Execute method will report the error for multiple attributes later.
187
195
  return null;
188
196
  }
189
197
 
190
198
  return new MessageToGenerateInfo(
191
199
  typeSymbol,
192
200
  typeDeclarationSyntax,
193
- foundTargetInterface
201
+ foundTargetInterface,
202
+ multipleAttributes
194
203
  );
195
204
  }
196
205
 
@@ -206,18 +215,19 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
206
215
  }
207
216
 
208
217
  // --- Step 1: Filter out types with multiple attributes applied ---
209
- Dictionary<ISymbol, MessageToGenerateInfo> uniqueTypes = new(
218
+ Dictionary<ISymbol, MessageToGenerateInfo> uniqueTypes = new Dictionary<
219
+ ISymbol,
220
+ MessageToGenerateInfo
221
+ >(SymbolEqualityComparer.Default);
222
+ HashSet<ISymbol> conflictingTypes = new HashSet<ISymbol>(
210
223
  SymbolEqualityComparer.Default
211
224
  );
212
- HashSet<ISymbol> typesWithMultipleAttributes = new(SymbolEqualityComparer.Default);
213
225
 
214
226
  foreach (MessageToGenerateInfo typeInfo in typesToGenerate)
215
227
  {
216
- if (uniqueTypes.ContainsKey(typeInfo.TypeSymbol))
228
+ if (typeInfo.HasConflictingMessageAttributes)
217
229
  {
218
- // If adding fails, it means the same TypeSymbol appeared multiple times.
219
- // This implies multiple different valid attributes were found, report error.
220
- if (typesWithMultipleAttributes.Add(typeInfo.TypeSymbol)) // Report only once
230
+ if (conflictingTypes.Add(typeInfo.TypeSymbol))
221
231
  {
222
232
  context.ReportDiagnostic(
223
233
  Diagnostic.Create(
@@ -226,7 +236,44 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
226
236
  typeInfo.TypeSymbol.ToDisplayString()
227
237
  )
228
238
  );
229
- // Also report for the one already in the dictionary if needed, but one report per type is usually sufficient.
239
+ }
240
+
241
+ continue;
242
+ }
243
+
244
+ if (conflictingTypes.Contains(typeInfo.TypeSymbol))
245
+ {
246
+ continue;
247
+ }
248
+
249
+ if (typeInfo.TargetInterfaceFullName is null)
250
+ {
251
+ continue;
252
+ }
253
+
254
+ if (
255
+ uniqueTypes.TryGetValue(
256
+ typeInfo.TypeSymbol,
257
+ out MessageToGenerateInfo existingInfo
258
+ )
259
+ )
260
+ {
261
+ if (
262
+ !string.Equals(
263
+ existingInfo.TargetInterfaceFullName,
264
+ typeInfo.TargetInterfaceFullName,
265
+ StringComparison.Ordinal
266
+ ) && conflictingTypes.Add(typeInfo.TypeSymbol)
267
+ )
268
+ {
269
+ context.ReportDiagnostic(
270
+ Diagnostic.Create(
271
+ MultipleAttributesError,
272
+ typeInfo.DeclarationSyntax.Identifier.GetLocation(),
273
+ typeInfo.TypeSymbol.ToDisplayString()
274
+ )
275
+ );
276
+ uniqueTypes.Remove(typeInfo.TypeSymbol);
230
277
  }
231
278
  }
232
279
  else
@@ -235,10 +282,21 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
235
282
  }
236
283
  }
237
284
 
238
- List<MessageToGenerateInfo> validSingleAttrTypes = uniqueTypes
239
- .Where(kvp => !typesWithMultipleAttributes.Contains(kvp.Key))
240
- .Select(kvp => kvp.Value)
241
- .ToList();
285
+ if (uniqueTypes.Count == 0)
286
+ {
287
+ return;
288
+ }
289
+
290
+ List<MessageToGenerateInfo> validSingleAttrTypes = new List<MessageToGenerateInfo>();
291
+ foreach (KeyValuePair<ISymbol, MessageToGenerateInfo> entry in uniqueTypes)
292
+ {
293
+ if (conflictingTypes.Contains(entry.Key))
294
+ {
295
+ continue;
296
+ }
297
+
298
+ validSingleAttrTypes.Add(entry.Value);
299
+ }
242
300
 
243
301
  if (validSingleAttrTypes.Count == 0)
244
302
  {
@@ -250,6 +308,12 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
250
308
  {
251
309
  context.CancellationToken.ThrowIfCancellationRequested();
252
310
 
311
+ string targetInterfaceFullName = messageInfo.TargetInterfaceFullName;
312
+ if (targetInterfaceFullName is null)
313
+ {
314
+ continue;
315
+ }
316
+
253
317
  // If nested, ensure all containers are declared partial; otherwise report diagnostic and skip
254
318
  if (messageInfo.TypeSymbol.ContainingType is not null)
255
319
  {
@@ -276,7 +340,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
276
340
  );
277
341
  foreach (INamedTypeSymbol container in nonPartial)
278
342
  {
279
- SyntaxReference? sr =
343
+ SyntaxReference sr =
280
344
  container.DeclaringSyntaxReferences.FirstOrDefault();
281
345
  if (sr != null && sr.GetSyntax() is TypeDeclarationSyntax tds)
282
346
  {
@@ -300,7 +364,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
300
364
 
301
365
  // Generate the partial IMessage implementation source
302
366
  string implSource = GenerateImplementationSource(
303
- messageInfo.TargetInterfaceFullName,
367
+ targetInterfaceFullName,
304
368
  messageInfo.TypeSymbol
305
369
  );
306
370
  string implHintName =
@@ -327,11 +391,11 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
327
391
  ? string.Empty
328
392
  : $"namespace {namespaceName}\n{{";
329
393
  string namespaceBlockClose = string.IsNullOrEmpty(namespaceName) ? string.Empty : "}";
330
- const string indent = " ";
394
+ const string Indent = " ";
331
395
 
332
396
  // Build container wrappers so partial can merge nested types correctly
333
397
  var containers = new Stack<INamedTypeSymbol>();
334
- INamedTypeSymbol? current = typeSymbol.ContainingType;
398
+ INamedTypeSymbol current = typeSymbol.ContainingType;
335
399
  while (current is not null)
336
400
  {
337
401
  containers.Push(current);
@@ -340,7 +404,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
340
404
 
341
405
  var containersOpen = new StringBuilder();
342
406
  var containersClose = new StringBuilder();
343
- string currentIndent = indent;
407
+ string currentIndent = Indent;
344
408
  foreach (INamedTypeSymbol container in containers)
345
409
  {
346
410
  string containerAccessibility = container.DeclaredAccessibility switch
@@ -373,7 +437,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
373
437
  $"{currentIndent}{containerAccessibility} partial {containerKind} {container.Name}{containerTypeParams}"
374
438
  );
375
439
  containersOpen.Append(currentIndent).AppendLine("{");
376
- currentIndent += indent;
440
+ currentIndent += Indent;
377
441
  }
378
442
 
379
443
  string innerIndent = currentIndent;
@@ -415,7 +479,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
415
479
  {
416
480
  currentIndent = currentIndent.Substring(
417
481
  0,
418
- Math.Max(0, currentIndent.Length - indent.Length)
482
+ Math.Max(0, currentIndent.Length - Indent.Length)
419
483
  );
420
484
  containersClose.Append(currentIndent).AppendLine("}");
421
485
  }
@@ -440,7 +504,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
440
504
  private static List<INamedTypeSymbol> GetNonPartialContainers(INamedTypeSymbol typeSymbol)
441
505
  {
442
506
  List<INamedTypeSymbol> result = new();
443
- INamedTypeSymbol? current = typeSymbol.ContainingType;
507
+ INamedTypeSymbol current = typeSymbol.ContainingType;
444
508
  while (current is not null)
445
509
  {
446
510
  if (!IsDeclaredFullyPartial(current))
@@ -2,6 +2,11 @@
2
2
  <PropertyGroup>
3
3
  <TargetFramework>netstandard2.0</TargetFramework>
4
4
  <LangVersion>latest</LangVersion>
5
+ <MicrosoftCodeAnalysisVersion>4.2.0</MicrosoftCodeAnalysisVersion>
6
+ <SystemCollectionsImmutableVersion>5.0.0</SystemCollectionsImmutableVersion>
7
+ <SystemReflectionMetadataVersion>9.0.0</SystemReflectionMetadataVersion>
8
+ <SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
9
+ <SystemTextEncodingsWebVersion>6.0.0</SystemTextEncodingsWebVersion>
5
10
  </PropertyGroup>
6
11
  <ItemGroup>
7
12
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
@@ -29,16 +34,31 @@
29
34
  <UnityAssetsEditorWallstopStudiosDir>$([System.IO.Path]::Combine('$(UnityAssetsEditorDir)', 'Wallstop Studios'))</UnityAssetsEditorWallstopStudiosDir>
30
35
  <UnityAssetsPluginsEditorWallstopDir>$([System.IO.Path]::Combine('$(UnityAssetsDir)', 'Plugins', 'Editor', 'WallstopStudios.DxMessaging'))</UnityAssetsPluginsEditorWallstopDir>
31
36
  <AnalyzerDll>$(TargetPath)</AnalyzerDll>
32
- <AnalyzerPdb>$(TargetDir)$(TargetName).pdb</AnalyzerPdb>
33
37
  </PropertyGroup>
34
38
  <ItemGroup>
35
39
  <AnalyzerOutput Include="$(AnalyzerDll)" />
36
- <AnalyzerOutput Include="$(AnalyzerPdb)" Condition="Exists('$(AnalyzerPdb)')" />
40
+ <AnalyzerDependency Include="$(NuGetPackageRoot)microsoft.codeanalysis.common\$(MicrosoftCodeAnalysisVersion)\lib\netstandard2.0\Microsoft.CodeAnalysis.dll" />
41
+ <AnalyzerDependency Include="$(NuGetPackageRoot)microsoft.codeanalysis.csharp\$(MicrosoftCodeAnalysisVersion)\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.dll" />
42
+ <AnalyzerDependency Include="$(NuGetPackageRoot)system.collections.immutable\$(SystemCollectionsImmutableVersion)\lib\netstandard2.0\System.Collections.Immutable.dll" />
43
+ <AnalyzerDependency
44
+ Include="$(NuGetPackageRoot)system.reflection.metadata\$(SystemReflectionMetadataVersion)\lib\net6.0\System.Reflection.Metadata.dll"
45
+ Condition="Exists('$(NuGetPackageRoot)system.reflection.metadata\$(SystemReflectionMetadataVersion)\lib\net6.0\System.Reflection.Metadata.dll')"
46
+ />
47
+ <AnalyzerDependency
48
+ Include="$(NuGetPackageRoot)system.reflection.metadata\$(SystemReflectionMetadataVersion)\lib\netstandard2.0\System.Reflection.Metadata.dll"
49
+ Condition="Exists('$(NuGetPackageRoot)system.reflection.metadata\$(SystemReflectionMetadataVersion)\lib\netstandard2.0\System.Reflection.Metadata.dll')"
50
+ />
51
+ <AnalyzerDependency Include="$(NuGetPackageRoot)system.runtime.compilerservices.unsafe\$(SystemRuntimeCompilerServicesUnsafeVersion)\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll" />
52
+ <AllAnalyzerFiles Include="@(AnalyzerOutput)" />
53
+ <AllAnalyzerFiles
54
+ Include="@(AnalyzerDependency)"
55
+ Condition="Exists('%(AnalyzerDependency.Identity)')"
56
+ />
37
57
  </ItemGroup>
38
58
  <!-- Copy into package Editor/Analyzers (used by SetupCscRsp and package consumers) -->
39
59
  <MakeDir Directories="$(EditorAnalyzersDir)" Condition="!Exists('$(EditorAnalyzersDir)')" />
40
60
  <Copy
41
- SourceFiles="@(AnalyzerOutput)"
61
+ SourceFiles="@(AllAnalyzerFiles)"
42
62
  DestinationFolder="$(EditorAnalyzersDir)"
43
63
  SkipUnchangedFiles="true"
44
64
  />
@@ -49,7 +69,7 @@
49
69
  Condition="Exists('$(UnityAssetsDir)') AND !Exists('$(UnityAssetsPluginsEditorWallstopDir)')"
50
70
  />
51
71
  <Copy
52
- SourceFiles="@(AnalyzerOutput)"
72
+ SourceFiles="@(AllAnalyzerFiles)"
53
73
  DestinationFolder="$(UnityAssetsPluginsEditorWallstopDir)"
54
74
  SkipUnchangedFiles="true"
55
75
  Condition="Exists('$(UnityAssetsPluginsEditorWallstopDir)')"
@@ -0,0 +1,193 @@
1
+ using System.Collections.Generic;
2
+ using System.IO;
3
+ using System.Linq;
4
+ using NUnit.Framework;
5
+
6
+ namespace WallstopStudios.DxMessaging.SourceGenerators.Tests;
7
+
8
+ [TestFixture]
9
+ public sealed class DocsSnippetCompilationTests
10
+ {
11
+ private static readonly HashSet<string> IgnoredSnippetDiagnosticIds = new(
12
+ StringComparer.OrdinalIgnoreCase
13
+ )
14
+ {
15
+ "CS0106", // modifier not valid in script (partial snippets showing members only)
16
+ "CS1001", // identifier expected (intentionally elided samples)
17
+ "CS8803", // top-level statements mixed with declarations (visual guide style snippets)
18
+ };
19
+
20
+ [Test]
21
+ public void QuickStartStep1Compiles()
22
+ {
23
+ string docsRoot = ResolveDocsRoot();
24
+ string quickStartPath = Path.Combine(docsRoot, "QuickStart.md");
25
+ Assert.That(File.Exists(quickStartPath), Is.True, $"Unable to locate {quickStartPath}.");
26
+
27
+ string snippet = ExtractFirstCodeBlock(quickStartPath, "csharp");
28
+ Assert.That(!string.IsNullOrWhiteSpace(snippet), Is.True, "QuickStart snippet not found.");
29
+
30
+ string source = $"""
31
+ using DxMessaging.Core.Messages;
32
+ using DxMessaging.Core.Attributes;
33
+ using UnityEngine;
34
+
35
+ {snippet}
36
+ """;
37
+
38
+ var diagnostics = GeneratorTestUtilities
39
+ .CompileSnippet(source)
40
+ .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
41
+ .ToArray();
42
+
43
+ if (diagnostics.Length > 0)
44
+ {
45
+ string message = string.Join(
46
+ System.Environment.NewLine,
47
+ diagnostics.Select(d => d.ToString())
48
+ );
49
+ Assert.Fail(
50
+ $"QuickStart snippet failed to compile:{System.Environment.NewLine}{message}"
51
+ );
52
+ }
53
+ }
54
+
55
+ [TestCaseSource(nameof(GetDocumentationSnippets))]
56
+ public void DocumentationSnippetsCompile(string markdownPath, string snippet)
57
+ {
58
+ Assert.That(
59
+ snippet,
60
+ Is.Not.Empty,
61
+ $"Snippet extracted from {markdownPath} should not be empty."
62
+ );
63
+
64
+ var diagnostics = GeneratorTestUtilities
65
+ .ParseSnippet(snippet)
66
+ .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
67
+ .ToArray();
68
+
69
+ var actionableDiagnostics = diagnostics
70
+ .Where(d => !IgnoredSnippetDiagnosticIds.Contains(d.Id))
71
+ .ToArray();
72
+
73
+ if (actionableDiagnostics.Length > 0)
74
+ {
75
+ string message = string.Join(
76
+ System.Environment.NewLine,
77
+ actionableDiagnostics.Select(d => d.ToString())
78
+ );
79
+ Assert.Fail(
80
+ $"Documentation snippet in {markdownPath} failed to compile:{System.Environment.NewLine}{message}"
81
+ );
82
+ }
83
+ }
84
+
85
+ private static IEnumerable<TestCaseData> GetDocumentationSnippets()
86
+ {
87
+ string docsRoot = ResolveDocsRoot();
88
+ foreach (
89
+ string markdownPath in Directory.GetFiles(docsRoot, "*.md", SearchOption.AllDirectories)
90
+ )
91
+ {
92
+ foreach (string snippet in ExtractCodeBlocks(markdownPath, "csharp"))
93
+ {
94
+ if (ShouldSkipSnippet(snippet))
95
+ {
96
+ continue;
97
+ }
98
+
99
+ yield return new TestCaseData(markdownPath, snippet).SetName(
100
+ $"{Path.GetFileName(markdownPath)} compiles"
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ private static string ExtractFirstCodeBlock(string markdownPath, string infoString)
107
+ {
108
+ return ExtractCodeBlocks(markdownPath, infoString).FirstOrDefault() ?? string.Empty;
109
+ }
110
+
111
+ private static IEnumerable<string> ExtractCodeBlocks(string markdownPath, string infoString)
112
+ {
113
+ string[] lines = File.ReadAllLines(markdownPath);
114
+ bool inBlock = false;
115
+ System.Text.StringBuilder builder = new();
116
+ foreach (string rawLine in lines)
117
+ {
118
+ string line = rawLine.TrimEnd();
119
+ if (!inBlock)
120
+ {
121
+ if (line.StartsWith("```") && line.Length > 3 && line[3..].StartsWith(infoString))
122
+ {
123
+ inBlock = true;
124
+ builder.Clear();
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (line.StartsWith("```"))
130
+ {
131
+ inBlock = false;
132
+ string snippet = builder.ToString();
133
+ if (!string.IsNullOrWhiteSpace(snippet))
134
+ {
135
+ yield return snippet;
136
+ }
137
+ continue;
138
+ }
139
+
140
+ builder.AppendLine(rawLine);
141
+ }
142
+ }
143
+
144
+ private static string ResolveDocsRoot()
145
+ {
146
+ string currentDirectoryPath = TestContext.CurrentContext.TestDirectory;
147
+ while (!string.IsNullOrEmpty(currentDirectoryPath))
148
+ {
149
+ string docsDirectory = Path.Combine(currentDirectoryPath, "Docs");
150
+ string candidate = Path.Combine(docsDirectory, "QuickStart.md");
151
+ if (File.Exists(candidate))
152
+ {
153
+ return docsDirectory;
154
+ }
155
+
156
+ string parentDirectoryPath =
157
+ Path.GetDirectoryName(currentDirectoryPath) ?? string.Empty;
158
+ if (string.IsNullOrEmpty(parentDirectoryPath))
159
+ {
160
+ break;
161
+ }
162
+
163
+ currentDirectoryPath = parentDirectoryPath;
164
+ }
165
+
166
+ throw new FileNotFoundException(
167
+ "Unable to locate Docs/QuickStart.md from the current test directory."
168
+ );
169
+ }
170
+
171
+ private static bool ShouldSkipSnippet(string snippet)
172
+ {
173
+ if (string.IsNullOrWhiteSpace(snippet))
174
+ {
175
+ return true;
176
+ }
177
+
178
+ if (snippet.Contains("..."))
179
+ {
180
+ return true;
181
+ }
182
+
183
+ foreach (char c in snippet)
184
+ {
185
+ if (c > 127 && !char.IsWhiteSpace(c))
186
+ {
187
+ return true;
188
+ }
189
+ }
190
+
191
+ return false;
192
+ }
193
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: b2dc5639a6d0fde4ba984d60757ff592
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: