com.wallstop-studios.dxmessaging 2.1.1 → 2.1.2

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 (125) hide show
  1. package/.github/workflows/dotnet-tests.yml +72 -0
  2. package/.lychee.toml +4 -2
  3. package/AGENTS.md +1 -0
  4. package/Docs/Comparisons.md +4 -4
  5. package/Docs/Install.md +2 -1
  6. package/Docs/Performance.md +13 -11
  7. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  8. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +13 -2
  9. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  10. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +11 -0
  11. package/Editor/Analyzers/System.Collections.Immutable.dll +0 -0
  12. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +11 -0
  13. package/Editor/Analyzers/System.Reflection.Metadata.dll +0 -0
  14. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +13 -2
  15. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  16. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +11 -0
  17. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  18. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +3 -2
  19. package/Editor/AssemblyInfo.cs +3 -0
  20. package/Editor/AssemblyInfo.cs.meta +3 -0
  21. package/Editor/CustomEditors/MessagingComponentEditor.cs +21 -0
  22. package/Editor/SetupCscRsp.cs +133 -53
  23. package/Editor/Testing/MessagingComponentEditorHarness.cs +218 -0
  24. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -0
  25. package/Editor/Testing.meta +3 -0
  26. package/README.md +9 -3
  27. package/Runtime/AssemblyInfo.cs +1 -0
  28. package/Runtime/Core/Diagnostics/MessageEmissionData.cs +26 -11
  29. package/Runtime/Core/Extensions/MessageBusExtensions.cs +2 -2
  30. package/Runtime/Core/Extensions/MessageExtensions.cs +2 -2
  31. package/Runtime/Core/InstanceId.cs +5 -3
  32. package/Runtime/Core/MessageBus/MessageBus.cs +4 -4
  33. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +2 -2
  34. package/Runtime/Core/MessageBus/MessagingRegistration.cs +3 -3
  35. package/Runtime/Core/MessageHandler.cs +34 -2
  36. package/Runtime/Core/MessageRegistrationToken.cs +2 -2
  37. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs +2 -0
  38. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs +2 -0
  39. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +2 -0
  40. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +2 -0
  41. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +2 -0
  42. package/Runtime/Unity/MessageAwareComponent.cs +2 -0
  43. package/Runtime/Unity/MessageBusProviderHandle.cs +4 -0
  44. package/Runtime/Unity/MessagingComponent.cs +16 -0
  45. package/Runtime/Unity/MessagingComponentInstaller.cs +2 -0
  46. package/Runtime/Unity/ScriptableMessageBusProvider.cs +2 -0
  47. package/SourceGenerators/Directory.Build.props +9 -0
  48. package/SourceGenerators/Directory.Build.props.meta +7 -0
  49. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +19 -24
  50. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +87 -27
  51. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +24 -4
  52. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DocsSnippetCompilationTests.cs +193 -0
  53. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DocsSnippetCompilationTests.cs.meta +11 -0
  54. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxAutoConstructorGeneratorDiagnosticsTests.cs +69 -0
  55. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxAutoConstructorGeneratorDiagnosticsTests.cs.meta +11 -0
  56. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxMessageIdGeneratorDiagnosticsTests.cs +66 -0
  57. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/DxMessageIdGeneratorDiagnosticsTests.cs.meta +11 -0
  58. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/GeneratorTestUtilities.cs +155 -0
  59. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/GeneratorTestUtilities.cs.meta +11 -0
  60. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/WallstopStudios.DxMessaging.SourceGenerators.Tests.csproj +20 -0
  61. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests/WallstopStudios.DxMessaging.SourceGenerators.Tests.csproj.meta +7 -0
  62. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.Tests.meta +8 -0
  63. package/Tests/Editor/MessagingComponentEditorHarnessTests.cs +243 -0
  64. package/Tests/Editor/MessagingComponentEditorHarnessTests.cs.meta +3 -0
  65. package/Tests/Editor/MessagingComponentSerializationTests.cs +129 -0
  66. package/Tests/Editor/MessagingComponentSerializationTests.cs.meta +3 -0
  67. package/Tests/Editor/WallstopStudios.DxMessaging.Tests.Editor.asmdef +19 -0
  68. package/Tests/Editor/WallstopStudios.DxMessaging.Tests.Editor.asmdef.meta +3 -0
  69. package/Tests/Editor.meta +3 -0
  70. package/Tests/Runtime/Benchmarks/BenchmarkSession.cs +3 -0
  71. package/Tests/Runtime/Benchmarks/BenchmarkTestBase.cs +3 -0
  72. package/Tests/Runtime/Benchmarks/ComparisonPerformanceTests.cs +3 -0
  73. package/Tests/Runtime/Benchmarks/PerformanceTests.cs +137 -0
  74. package/Tests/Runtime/Core/AlternateBusTests.cs +3 -0
  75. package/Tests/Runtime/Core/BroadcastTests.cs +3 -0
  76. package/Tests/Runtime/Core/CyclicBufferTests.cs +3 -0
  77. package/Tests/Runtime/Core/DefaultBusFallbackTests.cs +5 -2
  78. package/Tests/Runtime/Core/DiagnosticsTests.cs +3 -0
  79. package/Tests/Runtime/Core/EdgeCaseTests.cs +3 -0
  80. package/Tests/Runtime/Core/EnablementTests.cs +3 -0
  81. package/Tests/Runtime/Core/Extensions/MessageExtensionsProviderTests.cs +2 -2
  82. package/Tests/Runtime/Core/GenericMessageTests.cs +3 -0
  83. package/Tests/Runtime/Core/GlobalAcceptAllTests.cs +3 -0
  84. package/Tests/Runtime/Core/InterceptorCancellationTests.cs +3 -0
  85. package/Tests/Runtime/Core/LifecycleTests.cs +3 -0
  86. package/Tests/Runtime/Core/MessageEmissionDataTests.cs +70 -0
  87. package/Tests/Runtime/Core/MessageEmissionDataTests.cs.meta +11 -0
  88. package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs +3 -0
  89. package/Tests/Runtime/Core/MessagingTestBase.cs +3 -0
  90. package/Tests/Runtime/Core/MutationDedupeTests.cs +3 -0
  91. package/Tests/Runtime/Core/MutationDestructionTests.cs +3 -0
  92. package/Tests/Runtime/Core/MutationDuringEmissionTests.cs +3 -0
  93. package/Tests/Runtime/Core/MutationGlobalAddTests.cs +3 -0
  94. package/Tests/Runtime/Core/MutationInterceptorTests.cs +3 -0
  95. package/Tests/Runtime/Core/MutationPostProcessorAcrossHandlersTests.cs +3 -0
  96. package/Tests/Runtime/Core/MutationPostProcessorMoreTests.cs +3 -0
  97. package/Tests/Runtime/Core/MutationPriorityTests.cs +3 -0
  98. package/Tests/Runtime/Core/NominalTests.cs +3 -0
  99. package/Tests/Runtime/Core/OrderingTests.cs +3 -0
  100. package/Tests/Runtime/Core/OverDeregistrationTests.cs +3 -0
  101. package/Tests/Runtime/Core/PostProcessorTests.cs +3 -0
  102. package/Tests/Runtime/Core/ReflexiveErrorTests.cs +3 -0
  103. package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs +4 -1
  104. package/Tests/Runtime/Core/ReflexiveTests.cs +3 -0
  105. package/Tests/Runtime/Core/RegistrationTests.cs +3 -0
  106. package/Tests/Runtime/Core/StringShorthandTests.cs +3 -0
  107. package/Tests/Runtime/Core/TargetedTests.cs +3 -0
  108. package/Tests/Runtime/Core/TypedShorthandTests.cs +3 -0
  109. package/Tests/Runtime/Core/UntargetedEquivalenceTests.cs +3 -0
  110. package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs +14 -78
  111. package/Tests/Runtime/Core/UntargetedTests.cs +3 -0
  112. package/Tests/Runtime/Integrations/Reflex/ReflexIntegrationTests.cs +4 -1
  113. package/Tests/Runtime/Integrations/VContainer/VContainerIntegrationTests.cs +3 -0
  114. package/Tests/Runtime/Integrations/Zenject/ZenjectIntegrationTests.cs +3 -0
  115. package/Tests/Runtime/Scripts/Components/GenericMessageAwareComponent.cs +3 -0
  116. package/Tests/Runtime/Scripts/Components/ManualListenerComponent.cs +3 -0
  117. package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs +3 -0
  118. package/Tests/Runtime/TestUtilities/UnityFixtureBase.cs +3 -0
  119. package/Tests/Runtime/Unity/MessageBusProviderAssetTests.cs +3 -0
  120. package/Tests/Runtime/Unity/MessageBusProviderHandleTests.cs +87 -3
  121. package/Tests/Runtime/Unity/MessagingComponentInstallerSceneTests.cs +109 -0
  122. package/Tests/Runtime/Unity/MessagingComponentInstallerSceneTests.cs.meta +11 -0
  123. package/Tests/Runtime/Unity/MessagingComponentProviderIntegrationTests.cs +159 -17
  124. package/Tests/Runtime/WallstopStudios.DxMessaging.Tests.Runtime.csproj +20 -7
  125. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ #if UNITY_2021_3_OR_NEWER
1
2
  namespace DxMessaging.Unity
2
3
  {
3
4
  using System.Collections.Generic;
@@ -118,3 +119,4 @@ namespace DxMessaging.Unity
118
119
  }
119
120
  }
120
121
  }
122
+ #endif
@@ -1,3 +1,4 @@
1
+ #if UNITY_2021_3_OR_NEWER
1
2
  namespace DxMessaging.Unity
2
3
  {
3
4
  using DxMessaging.Core.MessageBus;
@@ -12,3 +13,4 @@ namespace DxMessaging.Unity
12
13
  public abstract IMessageBus Resolve();
13
14
  }
14
15
  }
16
+ #endif
@@ -0,0 +1,9 @@
1
+ <Project>
2
+ <PropertyGroup Condition="'$(MSBuildProjectName)' == 'WallstopStudios.DxMessaging.SourceGenerators.Tests'">
3
+ <SolutionDir Condition="'$(SolutionDir)' == ''">$(MSBuildThisFileDirectory)..\</SolutionDir>
4
+ <ArtifactsRoot>$(SolutionDir).artifacts/SourceGenerators.Tests/</ArtifactsRoot>
5
+ <IntermediateOutputPath>$(ArtifactsRoot)obj/$(Configuration)/</IntermediateOutputPath>
6
+ <OutputPath>$(ArtifactsRoot)bin/$(Configuration)/</OutputPath>
7
+ <VSTestResultsDirectory>$(ArtifactsRoot)TestResults/</VSTestResultsDirectory>
8
+ </PropertyGroup>
9
+ </Project>
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: 5b80e1cf4c3c72a43bbd48e4667f00f7
3
+ DefaultImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
@@ -208,7 +208,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
208
208
  // Location-specific suggestions on each non-partial container
209
209
  foreach (INamedTypeSymbol container in nonPartial)
210
210
  {
211
- SyntaxReference? sr =
211
+ SyntaxReference sr =
212
212
  container.DeclaringSyntaxReferences.FirstOrDefault();
213
213
  if (sr != null && sr.GetSyntax() is TypeDeclarationSyntax tds)
214
214
  {
@@ -258,11 +258,11 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
258
258
  ? string.Empty
259
259
  : $"namespace {namespaceName}\n{{";
260
260
  string namespaceBlockClose = string.IsNullOrEmpty(namespaceName) ? string.Empty : "}";
261
- const string indent = " ";
261
+ const string Indent = " ";
262
262
 
263
263
  // Build container wrappers for nested types so the partial can merge correctly
264
264
  var containers = new Stack<INamedTypeSymbol>();
265
- INamedTypeSymbol? current = typeSymbol.ContainingType;
265
+ INamedTypeSymbol current = typeSymbol.ContainingType;
266
266
  while (current is not null)
267
267
  {
268
268
  containers.Push(current);
@@ -271,7 +271,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
271
271
 
272
272
  var containersOpen = new StringBuilder();
273
273
  var containersClose = new StringBuilder();
274
- string currentIndent = indent; // one level inside namespace (or top-level)
274
+ string currentIndent = Indent; // one level inside namespace (or top-level)
275
275
 
276
276
  foreach (INamedTypeSymbol container in containers)
277
277
  {
@@ -306,7 +306,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
306
306
  $"{currentIndent}{containerAccessibility} partial {containerKind} {container.Name}{containerTypeParams}"
307
307
  );
308
308
  containersOpen.Append(currentIndent).AppendLine("{");
309
- currentIndent += indent;
309
+ currentIndent += Indent;
310
310
  }
311
311
 
312
312
  string innerIndent = currentIndent; // indent level for the target (innermost) type
@@ -350,13 +350,8 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
350
350
  SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers
351
351
  );
352
352
 
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)>();
353
+ List<(string Type, string Name, bool IsOptional, string DefaultExpr)> parameterDetails =
354
+ new List<(string Type, string Name, bool IsOptional, string DefaultExpr)>();
360
355
 
361
356
  // For validating expressions, use the semantic model for this type's tree
362
357
  SemanticModel semanticModel = compilation.GetSemanticModel(
@@ -368,7 +363,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
368
363
  {
369
364
  string fieldType = field.Type.ToDisplayString(fieldTypeFormat);
370
365
  string fieldName = field.Name;
371
- string? defaultExpr = null;
366
+ string defaultExpr = null;
372
367
  bool isOptional = false;
373
368
 
374
369
  foreach (AttributeData attr in field.GetAttributes())
@@ -455,10 +450,10 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
455
450
  }
456
451
  else if (arg.Kind == TypedConstantKind.Primitive)
457
452
  {
458
- object? val = arg.Value;
453
+ object val = arg.Value;
459
454
  defaultExpr = FormatLiteral(val, arg.Type);
460
455
  // Validate primitive conversion to field type
461
- ITypeSymbol? sourceType = arg.Type;
456
+ ITypeSymbol sourceType = arg.Type;
462
457
  if (sourceType != null)
463
458
  {
464
459
  Conversion conv = compilation.ClassifyConversion(
@@ -489,12 +484,12 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
489
484
  }
490
485
 
491
486
  parameterDetails.Add((fieldType, fieldName, isOptional, defaultExpr));
492
- constructorBody.AppendLine($"{indent}{indent} this.{fieldName} = {fieldName};");
487
+ constructorBody.AppendLine($"{Indent}{Indent} this.{fieldName} = {fieldName};");
493
488
  }
494
489
 
495
490
  for (int i = 0; i < parameterDetails.Count; i++)
496
491
  {
497
- (string Type, string Name, bool IsOptional, string? DefaultExpr) p =
492
+ (string Type, string Name, bool IsOptional, string DefaultExpr) p =
498
493
  parameterDetails[i];
499
494
  constructorParams.Append($"{p.Type} {p.Name}");
500
495
  if (p.IsOptional)
@@ -520,7 +515,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
520
515
  {
521
516
  currentIndent = currentIndent.Substring(
522
517
  0,
523
- Math.Max(0, currentIndent.Length - indent.Length)
518
+ Math.Max(0, currentIndent.Length - Indent.Length)
524
519
  );
525
520
  containersClose.Append(currentIndent).AppendLine("}");
526
521
  }
@@ -533,9 +528,9 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
533
528
  {{namespaceBlockOpen}}
534
529
  {{containersOpen}}{{innerIndent}}{{typeAccessibility}} partial {{typeKind}} {{typeName}}
535
530
  {{innerIndent}}{
536
- {{indent}} /// <summary>
537
- {{indent}} /// Auto-generated constructor by DxAutoGenConstructorGenerator.
538
- {{indent}} /// </summary>
531
+ {{Indent}} /// <summary>
532
+ {{Indent}} /// Auto-generated constructor by DxAutoGenConstructorGenerator.
533
+ {{Indent}} /// </summary>
539
534
  {{innerIndent}} {{constructorAccessibility}} {{typeSymbol.Name}}({{constructorParams}})
540
535
  {{innerIndent}} {
541
536
  {{constructorBody}}
@@ -546,7 +541,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
546
541
  """;
547
542
  }
548
543
 
549
- private static string FormatLiteral(object? value, ITypeSymbol? type)
544
+ private static string FormatLiteral(object value, ITypeSymbol type)
550
545
  {
551
546
  if (value == null)
552
547
  {
@@ -635,7 +630,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
635
630
  SpeculativeBindingOption.BindAsExpression
636
631
  );
637
632
 
638
- ITypeSymbol? sourceType = typeInfo.Type;
633
+ ITypeSymbol sourceType = typeInfo.Type;
639
634
  if (sourceType == null)
640
635
  {
641
636
  // Could not bind; let the compiler decide but report as invalid here
@@ -655,7 +650,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
655
650
  private static List<INamedTypeSymbol> GetNonPartialContainers(INamedTypeSymbol typeSymbol)
656
651
  {
657
652
  List<INamedTypeSymbol> result = new();
658
- INamedTypeSymbol? current = typeSymbol.ContainingType;
653
+ INamedTypeSymbol current = typeSymbol.ContainingType;
659
654
  while (current is not null)
660
655
  {
661
656
  if (!IsDeclaredFullyPartial(current))
@@ -66,7 +66,8 @@ 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
 
72
73
  public void Initialize(IncrementalGeneratorInitializationContext context)
@@ -143,15 +144,15 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
143
144
  return null; // Cannot be a concrete message type
144
145
  }
145
146
 
146
- string? foundTargetInterface = null;
147
+ string foundTargetInterface = null;
147
148
  bool multipleAttributes = false;
148
149
 
149
150
  // Check attributes to find the specific message type (Broadcast, Targeted, etc.)
150
151
  foreach (AttributeData attributeData in typeSymbol.GetAttributes())
151
152
  {
152
153
  cancellationToken.ThrowIfCancellationRequested();
153
- string? currentAttributeFullName = attributeData.AttributeClass?.ToDisplayString();
154
- string? targetInterfaceForThisAttribute = null;
154
+ string currentAttributeFullName = attributeData.AttributeClass?.ToDisplayString();
155
+ string targetInterfaceForThisAttribute = null;
155
156
 
156
157
  switch (currentAttributeFullName)
157
158
  {
@@ -180,17 +181,21 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
180
181
  }
181
182
  }
182
183
 
183
- if (multipleAttributes || foundTargetInterface == null)
184
+ if (multipleAttributes)
185
+ {
186
+ foundTargetInterface = null;
187
+ }
188
+
189
+ if (foundTargetInterface == null && !multipleAttributes)
184
190
  {
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
191
  return null;
188
192
  }
189
193
 
190
194
  return new MessageToGenerateInfo(
191
195
  typeSymbol,
192
196
  typeDeclarationSyntax,
193
- foundTargetInterface
197
+ foundTargetInterface,
198
+ multipleAttributes
194
199
  );
195
200
  }
196
201
 
@@ -206,18 +211,19 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
206
211
  }
207
212
 
208
213
  // --- Step 1: Filter out types with multiple attributes applied ---
209
- Dictionary<ISymbol, MessageToGenerateInfo> uniqueTypes = new(
214
+ Dictionary<ISymbol, MessageToGenerateInfo> uniqueTypes = new Dictionary<
215
+ ISymbol,
216
+ MessageToGenerateInfo
217
+ >(SymbolEqualityComparer.Default);
218
+ HashSet<ISymbol> conflictingTypes = new HashSet<ISymbol>(
210
219
  SymbolEqualityComparer.Default
211
220
  );
212
- HashSet<ISymbol> typesWithMultipleAttributes = new(SymbolEqualityComparer.Default);
213
221
 
214
222
  foreach (MessageToGenerateInfo typeInfo in typesToGenerate)
215
223
  {
216
- if (uniqueTypes.ContainsKey(typeInfo.TypeSymbol))
224
+ if (typeInfo.HasConflictingMessageAttributes)
217
225
  {
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
226
+ if (conflictingTypes.Add(typeInfo.TypeSymbol))
221
227
  {
222
228
  context.ReportDiagnostic(
223
229
  Diagnostic.Create(
@@ -226,7 +232,44 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
226
232
  typeInfo.TypeSymbol.ToDisplayString()
227
233
  )
228
234
  );
229
- // Also report for the one already in the dictionary if needed, but one report per type is usually sufficient.
235
+ }
236
+
237
+ continue;
238
+ }
239
+
240
+ if (conflictingTypes.Contains(typeInfo.TypeSymbol))
241
+ {
242
+ continue;
243
+ }
244
+
245
+ if (typeInfo.TargetInterfaceFullName is null)
246
+ {
247
+ continue;
248
+ }
249
+
250
+ if (
251
+ uniqueTypes.TryGetValue(
252
+ typeInfo.TypeSymbol,
253
+ out MessageToGenerateInfo existingInfo
254
+ )
255
+ )
256
+ {
257
+ if (
258
+ !string.Equals(
259
+ existingInfo.TargetInterfaceFullName,
260
+ typeInfo.TargetInterfaceFullName,
261
+ StringComparison.Ordinal
262
+ ) && conflictingTypes.Add(typeInfo.TypeSymbol)
263
+ )
264
+ {
265
+ context.ReportDiagnostic(
266
+ Diagnostic.Create(
267
+ MultipleAttributesError,
268
+ typeInfo.DeclarationSyntax.Identifier.GetLocation(),
269
+ typeInfo.TypeSymbol.ToDisplayString()
270
+ )
271
+ );
272
+ uniqueTypes.Remove(typeInfo.TypeSymbol);
230
273
  }
231
274
  }
232
275
  else
@@ -235,10 +278,21 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
235
278
  }
236
279
  }
237
280
 
238
- List<MessageToGenerateInfo> validSingleAttrTypes = uniqueTypes
239
- .Where(kvp => !typesWithMultipleAttributes.Contains(kvp.Key))
240
- .Select(kvp => kvp.Value)
241
- .ToList();
281
+ if (uniqueTypes.Count == 0)
282
+ {
283
+ return;
284
+ }
285
+
286
+ List<MessageToGenerateInfo> validSingleAttrTypes = new List<MessageToGenerateInfo>();
287
+ foreach (KeyValuePair<ISymbol, MessageToGenerateInfo> entry in uniqueTypes)
288
+ {
289
+ if (conflictingTypes.Contains(entry.Key))
290
+ {
291
+ continue;
292
+ }
293
+
294
+ validSingleAttrTypes.Add(entry.Value);
295
+ }
242
296
 
243
297
  if (validSingleAttrTypes.Count == 0)
244
298
  {
@@ -250,6 +304,12 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
250
304
  {
251
305
  context.CancellationToken.ThrowIfCancellationRequested();
252
306
 
307
+ string targetInterfaceFullName = messageInfo.TargetInterfaceFullName;
308
+ if (targetInterfaceFullName is null)
309
+ {
310
+ continue;
311
+ }
312
+
253
313
  // If nested, ensure all containers are declared partial; otherwise report diagnostic and skip
254
314
  if (messageInfo.TypeSymbol.ContainingType is not null)
255
315
  {
@@ -276,7 +336,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
276
336
  );
277
337
  foreach (INamedTypeSymbol container in nonPartial)
278
338
  {
279
- SyntaxReference? sr =
339
+ SyntaxReference sr =
280
340
  container.DeclaringSyntaxReferences.FirstOrDefault();
281
341
  if (sr != null && sr.GetSyntax() is TypeDeclarationSyntax tds)
282
342
  {
@@ -300,7 +360,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
300
360
 
301
361
  // Generate the partial IMessage implementation source
302
362
  string implSource = GenerateImplementationSource(
303
- messageInfo.TargetInterfaceFullName,
363
+ targetInterfaceFullName,
304
364
  messageInfo.TypeSymbol
305
365
  );
306
366
  string implHintName =
@@ -327,11 +387,11 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
327
387
  ? string.Empty
328
388
  : $"namespace {namespaceName}\n{{";
329
389
  string namespaceBlockClose = string.IsNullOrEmpty(namespaceName) ? string.Empty : "}";
330
- const string indent = " ";
390
+ const string Indent = " ";
331
391
 
332
392
  // Build container wrappers so partial can merge nested types correctly
333
393
  var containers = new Stack<INamedTypeSymbol>();
334
- INamedTypeSymbol? current = typeSymbol.ContainingType;
394
+ INamedTypeSymbol current = typeSymbol.ContainingType;
335
395
  while (current is not null)
336
396
  {
337
397
  containers.Push(current);
@@ -340,7 +400,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
340
400
 
341
401
  var containersOpen = new StringBuilder();
342
402
  var containersClose = new StringBuilder();
343
- string currentIndent = indent;
403
+ string currentIndent = Indent;
344
404
  foreach (INamedTypeSymbol container in containers)
345
405
  {
346
406
  string containerAccessibility = container.DeclaredAccessibility switch
@@ -373,7 +433,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
373
433
  $"{currentIndent}{containerAccessibility} partial {containerKind} {container.Name}{containerTypeParams}"
374
434
  );
375
435
  containersOpen.Append(currentIndent).AppendLine("{");
376
- currentIndent += indent;
436
+ currentIndent += Indent;
377
437
  }
378
438
 
379
439
  string innerIndent = currentIndent;
@@ -415,7 +475,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
415
475
  {
416
476
  currentIndent = currentIndent.Substring(
417
477
  0,
418
- Math.Max(0, currentIndent.Length - indent.Length)
478
+ Math.Max(0, currentIndent.Length - Indent.Length)
419
479
  );
420
480
  containersClose.Append(currentIndent).AppendLine("}");
421
481
  }
@@ -440,7 +500,7 @@ namespace WallstopStudios.DxMessaging.SourceGenerators
440
500
  private static List<INamedTypeSymbol> GetNonPartialContainers(INamedTypeSymbol typeSymbol)
441
501
  {
442
502
  List<INamedTypeSymbol> result = new();
443
- INamedTypeSymbol? current = typeSymbol.ContainingType;
503
+ INamedTypeSymbol current = typeSymbol.ContainingType;
444
504
  while (current is not null)
445
505
  {
446
506
  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: