com.wallstop-studios.dxmessaging 2.2.0 → 3.0.1

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 (248) hide show
  1. package/CHANGELOG.md +106 -67
  2. package/CHANGELOG.md.meta +7 -7
  3. package/Editor/Analyzers/BaseCallIlInspector.cs +277 -0
  4. package/Editor/Analyzers/BaseCallIlInspector.cs.meta +11 -0
  5. package/Editor/Analyzers/BaseCallLogMessageParser.cs +295 -0
  6. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +11 -0
  7. package/Editor/Analyzers/BaseCallReportAggregator.cs +308 -0
  8. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +11 -0
  9. package/Editor/Analyzers/BaseCallTypeScanner.cs +110 -0
  10. package/Editor/Analyzers/BaseCallTypeScanner.cs.meta +11 -0
  11. package/Editor/Analyzers/BaseCallTypeScannerCore.cs +562 -0
  12. package/Editor/Analyzers/BaseCallTypeScannerCore.cs.meta +11 -0
  13. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +1122 -0
  14. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +11 -0
  15. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +44 -44
  16. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +44 -44
  17. package/Editor/Analyzers/System.Collections.Immutable.dll.meta +44 -44
  18. package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +44 -44
  19. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +44 -44
  20. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  21. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +33 -0
  22. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  23. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +72 -72
  24. package/Editor/Analyzers.meta +8 -8
  25. package/Editor/AssemblyInfo.cs.meta +3 -3
  26. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
  27. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
  28. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +420 -0
  29. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +11 -0
  30. package/Editor/CustomEditors/MessagingComponentEditor.cs +1 -1
  31. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +2 -2
  32. package/Editor/CustomEditors.meta +2 -2
  33. package/Editor/DxMessagingEditorInitializer.cs.meta +2 -2
  34. package/Editor/DxMessagingMenu.cs.meta +11 -11
  35. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
  36. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +190 -0
  37. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
  38. package/Editor/Settings/DxMessagingSettings.cs +189 -0
  39. package/Editor/Settings/DxMessagingSettings.cs.meta +2 -2
  40. package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
  41. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +2 -2
  42. package/Editor/Settings.meta +2 -2
  43. package/Editor/SetupCscRsp.cs +209 -8
  44. package/Editor/SetupCscRsp.cs.meta +2 -2
  45. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  46. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -3
  47. package/Editor/Testing.meta +3 -3
  48. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
  49. package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
  50. package/Editor.meta +8 -8
  51. package/LICENSE.md +9 -9
  52. package/LICENSE.md.meta +7 -7
  53. package/README.md +941 -900
  54. package/README.md.meta +7 -7
  55. package/Runtime/AssemblyInfo.cs +4 -0
  56. package/Runtime/AssemblyInfo.cs.meta +2 -2
  57. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +2 -2
  58. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +2 -2
  59. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
  60. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
  61. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +2 -2
  62. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +2 -2
  63. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +2 -2
  64. package/Runtime/Core/Attributes.meta +2 -2
  65. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
  66. package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
  67. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
  68. package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
  69. package/Runtime/Core/Configuration.meta +9 -0
  70. package/Runtime/Core/DataStructure/CyclicBuffer.cs +2 -2
  71. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +2 -2
  72. package/Runtime/Core/DataStructure.meta +2 -2
  73. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +2 -2
  74. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +2 -2
  75. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +2 -2
  76. package/Runtime/Core/Diagnostics.meta +2 -2
  77. package/Runtime/Core/DxMessagingStaticState.cs +19 -0
  78. package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
  79. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +2 -2
  80. package/Runtime/Core/Extensions/IListExtensions.cs.meta +2 -2
  81. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
  82. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
  83. package/Runtime/Core/Extensions.meta +8 -8
  84. package/Runtime/Core/Helper/MessageCache.cs +32 -0
  85. package/Runtime/Core/Helper/MessageCache.cs.meta +2 -2
  86. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +2 -2
  87. package/Runtime/Core/Helper.meta +2 -2
  88. package/Runtime/Core/IMessage.cs +3 -3
  89. package/Runtime/Core/IMessage.cs.meta +11 -11
  90. package/Runtime/Core/InstanceId.cs.meta +11 -11
  91. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +51 -0
  92. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta +11 -0
  93. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
  94. package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
  95. package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
  96. package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
  97. package/Runtime/Core/Internal/TypedSlots.cs +613 -0
  98. package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
  99. package/Runtime/Core/Internal.meta +9 -0
  100. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
  101. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
  102. package/Runtime/Core/MessageBus/IMessageBus.cs +177 -3
  103. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
  104. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
  105. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
  106. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
  107. package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
  108. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
  109. package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
  110. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +718 -0
  111. package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
  112. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
  113. package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
  114. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
  115. package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
  116. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
  117. package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
  118. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
  119. package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
  120. package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
  121. package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
  122. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
  123. package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
  124. package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
  125. package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
  126. package/Runtime/Core/MessageBus/Internal.meta +9 -0
  127. package/Runtime/Core/MessageBus/MessageBus.cs +2651 -500
  128. package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
  129. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
  130. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
  131. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
  132. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
  133. package/Runtime/Core/MessageBus.meta +8 -8
  134. package/Runtime/Core/MessageHandler.cs +2019 -542
  135. package/Runtime/Core/MessageHandler.cs.meta +11 -11
  136. package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
  137. package/Runtime/Core/MessageRegistrationToken.cs +7 -0
  138. package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
  139. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +2 -2
  140. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
  141. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
  142. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
  143. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +2 -2
  144. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
  145. package/Runtime/Core/Messages/StringMessage.cs.meta +2 -2
  146. package/Runtime/Core/Messages.meta +8 -8
  147. package/Runtime/Core/MessagingDebug.cs.meta +11 -11
  148. package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
  149. package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
  150. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
  151. package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
  152. package/Runtime/Core/Pooling/DxPools.cs +157 -0
  153. package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
  154. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
  155. package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
  156. package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
  157. package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
  158. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
  159. package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
  160. package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
  161. package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
  162. package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
  163. package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
  164. package/Runtime/Core/Pooling.meta +9 -0
  165. package/Runtime/Core.meta +8 -8
  166. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
  167. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
  168. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
  169. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +2 -2
  170. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
  171. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
  172. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
  173. package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
  174. package/Runtime/Unity/Integrations/Reflex.meta +8 -8
  175. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +2 -2
  176. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
  177. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
  178. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
  179. package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
  180. package/Runtime/Unity/Integrations/VContainer.meta +8 -8
  181. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +2 -2
  182. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
  183. package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
  184. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
  185. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
  186. package/Runtime/Unity/Integrations/Zenject.meta +8 -8
  187. package/Runtime/Unity/Integrations.meta +8 -8
  188. package/Runtime/Unity/MessageAwareComponent.cs +29 -0
  189. package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
  190. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
  191. package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
  192. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
  193. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
  194. package/Runtime/Unity.meta +8 -8
  195. package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
  196. package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
  197. package/Runtime.meta +8 -8
  198. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
  199. package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
  200. package/Samples~/DI/Prefabs.meta +8 -8
  201. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
  202. package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
  203. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
  204. package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
  205. package/Samples~/DI/Providers.meta +8 -8
  206. package/Samples~/DI/README.md +51 -51
  207. package/Samples~/DI/README.md.meta +7 -7
  208. package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
  209. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
  210. package/Samples~/DI/Reflex.meta +8 -8
  211. package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
  212. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
  213. package/Samples~/DI/VContainer.meta +8 -8
  214. package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
  215. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
  216. package/Samples~/DI/Zenject.meta +8 -8
  217. package/Samples~/DI.meta +8 -8
  218. package/Samples~/Mini Combat/Boot.cs.meta +11 -11
  219. package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
  220. package/Samples~/Mini Combat/Messages.cs.meta +11 -11
  221. package/Samples~/Mini Combat/Player.cs.meta +11 -11
  222. package/Samples~/Mini Combat/README.md +324 -323
  223. package/Samples~/Mini Combat/README.md.meta +7 -7
  224. package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
  225. package/Samples~/Mini Combat/Walkthrough.md +430 -430
  226. package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
  227. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
  228. package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
  229. package/Samples~/Mini Combat.meta +8 -8
  230. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
  231. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
  232. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
  233. package/Samples~/UI Buttons + Inspector/README.md +210 -209
  234. package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
  235. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
  236. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
  237. package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
  238. package/Samples~/UI Buttons + Inspector.meta +8 -8
  239. package/SourceGenerators/Directory.Build.props.meta +7 -7
  240. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
  241. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +2 -2
  242. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
  243. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
  244. package/SourceGenerators.meta +8 -8
  245. package/Third Party Notices.md +3 -3
  246. package/Third Party Notices.md.meta +7 -7
  247. package/package.json +115 -92
  248. package/package.json.meta +7 -7
@@ -0,0 +1,1122 @@
1
+ namespace DxMessaging.Editor.Analyzers
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.Globalization;
7
+ using System.IO;
8
+ using System.Linq;
9
+ using System.Reflection;
10
+ using DxMessaging.Editor.Settings;
11
+ using UnityEditor;
12
+ using UnityEditor.Compilation;
13
+ using UnityEngine;
14
+
15
+ /// <summary>
16
+ /// Per-type entry recorded by the inspector overlay's data feed.
17
+ /// </summary>
18
+ /// <remarks>
19
+ /// This shape is the public contract that <c>MessageAwareComponentInspectorOverlay</c>
20
+ /// consumes; the field names are kept short and lower-cased so Unity's
21
+ /// <see cref="JsonUtility"/> serializer round-trips them cleanly through the JSON cache.
22
+ /// </remarks>
23
+ [Serializable]
24
+ public sealed class BaseCallReportEntry
25
+ {
26
+ /// <summary>Fully-qualified name of the offending type.</summary>
27
+ public string typeName;
28
+
29
+ /// <summary>Method names whose overrides are missing the corresponding <c>base.*()</c> call.</summary>
30
+ public List<string> missingBaseFor = new();
31
+
32
+ /// <summary>Diagnostic IDs that contributed to this entry (e.g., DXMSG006, DXMSG007, DXMSG009, DXMSG010).</summary>
33
+ /// <remarks>
34
+ /// Note: the IL-reflection scanner classifies DXMSG009 as DXMSG007 because the two are
35
+ /// indistinguishable at the IL level. The compile-time analyzer remains authoritative for
36
+ /// the precise ID classification -- see the analyzer reference docs and the inspector
37
+ /// integration section of <c>docs/reference/analyzers.md</c>. DXMSG008 (audit-marker for
38
+ /// opted-out types) is intentionally NOT included here: opted-out types are excluded from
39
+ /// the snapshot so the overlay's "Stop ignoring" path can reason about them via the
40
+ /// project ignore list directly.
41
+ /// </remarks>
42
+ public List<string> diagnosticIds = new();
43
+
44
+ /// <summary>Source file path (best-effort) for "Open Script" actions in the inspector overlay.</summary>
45
+ public string filePath;
46
+
47
+ /// <summary>1-based line number of the first relevant diagnostic, when known.</summary>
48
+ public int line;
49
+ }
50
+
51
+ [Serializable]
52
+ internal sealed class BaseCallReportFile
53
+ {
54
+ public int version = 1;
55
+ public string generatedAt;
56
+ public List<BaseCallReportEntry> types = new();
57
+ }
58
+
59
+ /// <summary>
60
+ /// Builds the per-FQN snapshot consumed by the inspector overlay from a deterministic IL
61
+ /// reflection scanner (<see cref="BaseCallTypeScanner"/>) -- and, optionally, a legacy
62
+ /// console-scrape bridge for users who want the union of both data sources.
63
+ /// </summary>
64
+ /// <remarks>
65
+ /// <para>
66
+ /// <b>Primary source (always-on): <see cref="BaseCallTypeScanner"/>.</b> Walks loaded
67
+ /// <c>MessageAwareComponent</c> subclasses via Unity's <c>TypeCache</c> and inspects each
68
+ /// override's IL body for the base-call shape. Deterministic across Unity 2021 cache hits,
69
+ /// incremental compiles, and arbitrary domain-reload sequences -- the only inputs are the
70
+ /// loaded assemblies in the AppDomain, which do not depend on Unity's compile-pipeline
71
+ /// state. Runs on every <see cref="AssemblyReloadEvents.afterAssemblyReload"/> and on every
72
+ /// <c>CompilationPipeline.assemblyCompilationFinished</c> burst (debounced via
73
+ /// <see cref="EditorApplication.delayCall"/>).
74
+ /// </para>
75
+ /// <para>
76
+ /// <b>Secondary source (opt-in): legacy console-scrape bridge.</b> When
77
+ /// <see cref="DxMessagingSettings.UseConsoleBridge"/> is <c>true</c>, the harvester ALSO
78
+ /// reads warnings from <c>UnityEditor.LogEntries</c> via reflection and from
79
+ /// <c>CompilationPipeline.assemblyCompilationFinished</c>'s per-assembly
80
+ /// <c>CompilerMessage[]</c> payloads. This path is non-deterministic on Unity 2021 (Bee/csc
81
+ /// cache hits cause Unity to skip surfacing analyzer warnings to either store) and is the
82
+ /// reason the IL-reflection scanner exists. Default off; available for users who want the
83
+ /// union of both data sources.
84
+ /// </para>
85
+ /// <para>
86
+ /// The inspector overlay reads its snapshot from the unified per-FQN map populated here on
87
+ /// every rescan. Use the menu <c>Tools > DxMessaging > Rescan Base-Call Warnings</c> for a
88
+ /// manual force-rescan.
89
+ /// </para>
90
+ /// <para>
91
+ /// <see cref="IsAvailable"/> stays <c>true</c> as long as the static constructor itself does
92
+ /// not throw -- the IL scanner is always wired, so the overlay never falls back to its
93
+ /// degraded "harvester unavailable" HelpBox in normal operation. <see cref="LogEntriesAvailable"/>
94
+ /// continues to report whether the legacy reflection layer is bindable, for diagnostics only.
95
+ /// </para>
96
+ /// </remarks>
97
+ [InitializeOnLoad]
98
+ public static class DxMessagingConsoleHarvester
99
+ {
100
+ private const string ReportFileName = "baseCallReport.json";
101
+ private const string ReportDirectoryName = "DxMessaging";
102
+ private const double PollIntervalSeconds = 0.25;
103
+
104
+ // N1: cap the per-rescan list capacity so a 100k-warning console doesn't allocate a
105
+ // pathologically large initial backing array. The list still grows freely if the console
106
+ // really does hold more entries than this, but the OOM-edge becomes a non-issue.
107
+ private const int MaxLineListInitialCapacity = 1024;
108
+
109
+ private static readonly Dictionary<string, BaseCallReportEntry> SnapshotInternal = new(
110
+ StringComparer.Ordinal
111
+ );
112
+
113
+ // Per-assembly attribution for the LEGACY CompilationPipeline.assemblyCompilationFinished
114
+ // feed (only consulted when DxMessagingSettings.UseConsoleBridge is true). When a
115
+ // recompile no longer reports a previously-seen type (because the user fixed the missing
116
+ // base call), we drop it from the bridge merged view. Without per-assembly tracking we'd
117
+ // never know which entries to retire.
118
+ //
119
+ // Lifecycle: writes happen inside _compilationFeedLock from
120
+ // OnAssemblyCompilationFinished. Reads happen on the editor main thread inside RescanNow,
121
+ // also under the lock to flush + clear the channel atomically.
122
+ //
123
+ // The merge + retirement bookkeeping for these maps lives in
124
+ // <see cref="BaseCallReportAggregator"/> as a pure helper so it can be tested via
125
+ // dotnet-test (the harvester itself is Unity-only and cannot be loaded outside the
126
+ // editor). Mutations to _typesByAssembly and _compilationMerged go through that helper
127
+ // exclusively to keep the test surface and runtime behaviour identical.
128
+ //
129
+ // Note: starting in v2.3, the IL-reflection scanner (BaseCallTypeScanner) is the primary
130
+ // source of truth; it runs unconditionally on every rescan, regardless of bridge state.
131
+ // The bridge only contributes ADDITIONAL data, never overrides the scanner.
132
+ private static readonly Dictionary<string, HashSet<string>> _typesByAssembly = new(
133
+ StringComparer.OrdinalIgnoreCase
134
+ );
135
+
136
+ // Per-FQN merged view of every assembly's latest reports, kept in sync with
137
+ // _typesByAssembly by BaseCallReportAggregator.ApplyAssemblyReports. The final inspector
138
+ // snapshot is built by unioning this with the LogEntries-derived report.
139
+ private static readonly Dictionary<string, ParsedTypeReport> _compilationMerged = new(
140
+ StringComparer.Ordinal
141
+ );
142
+
143
+ private static readonly HashSet<string> AlreadyWarned = new(StringComparer.Ordinal);
144
+
145
+ // Lock guarding all reads/writes to _typesByAssembly and the parsed-message buffer that
146
+ // flows from OnAssemblyCompilationFinished (worker thread for the parse) into
147
+ // DrainScheduledRescan (editor main thread for snapshot integration). Unity can fire
148
+ // assemblyCompilationFinished from a non-main thread on some Editor versions; the rest
149
+ // of the harvester (LogEntries reflection, AssetDatabase, persistence) is main-thread
150
+ // only and uses simple read order, so the lock is scoped to the cross-thread channel.
151
+ private static readonly object _compilationFeedLock = new();
152
+
153
+ // Drained by DrainScheduledRescan on the next editor tick. Holds the union of all
154
+ // CompilerMessage payloads captured since the last drain, attributed to their source
155
+ // assembly so we can retire entries that the user has fixed.
156
+ private static readonly Dictionary<
157
+ string,
158
+ Dictionary<string, ParsedTypeReport>
159
+ > _pendingByAssembly = new(StringComparer.OrdinalIgnoreCase);
160
+
161
+ // True when the LogEntries reflection layer failed to bind. The harvester remains
162
+ // available via the CompilerMessage path; this flag just gates the LogEntries-specific
163
+ // code paths (Tick polling, RescanNow's reflection call). Renamed from `_disabled` so
164
+ // the name reflects what it actually means.
165
+ private static readonly bool _logEntriesDisabled;
166
+
167
+ // Reflection handles. Resolved once in the static ctor; null when the running Unity version
168
+ // does not expose the expected LogEntries shape.
169
+ private static readonly Type _logEntryType;
170
+ private static readonly MethodInfo _startGettingEntries;
171
+ private static readonly MethodInfo _endGettingEntries;
172
+ private static readonly MethodInfo _getEntryInternal;
173
+ private static readonly MethodInfo _getCount;
174
+ private static readonly FieldInfo _messageField;
175
+
176
+ private static double _lastTickTime;
177
+ private static int _lastSeenCount;
178
+
179
+ // Latch flipped on by `OnAssemblyCompilationFinished` to coalesce the burst of one-event-
180
+ // per-assembly callbacks Unity fires during a build. We schedule a single deferred
181
+ // RescanNow via `EditorApplication.delayCall` (DrainScheduledRescan) and clear the latch
182
+ // when that callback runs. Without this debounce, a 30-assembly project would queue 30
183
+ // RescanNow invocations during the very window when the editor is most fragile.
184
+ private static volatile bool _rescanScheduled;
185
+
186
+ // Tracks whether the current snapshot has been refreshed by a scan in THIS Editor session,
187
+ // or whether it was loaded eagerly from `Library/DxMessaging/baseCallReport.json` in the
188
+ // static ctor and has not yet been overwritten. The inspector overlay reads this to
189
+ // distinguish "fresh-this-session" warnings from cached-from-previous-session warnings;
190
+ // when the cache is showing, we annotate the HelpBox with a small suffix so the user
191
+ // understands the data may be stale until the first post-reload scan completes.
192
+ //
193
+ // Default `false`: the static ctor's `LoadFromDisk` runs first, so by the time anything
194
+ // observes the snapshot, either (a) the cache populated entries that pre-date this session,
195
+ // or (b) the cache was empty (truly fresh). In case (b) the overlay renders no warning
196
+ // anyway; there are no entries to annotate; so the false default is correct for both.
197
+ // Flipped to `true` after the first successful `RescanNow` post-startup; never flipped
198
+ // back to `false`. Volatile so the editor-loop reader sees the write without a memory
199
+ // barrier on Unity's pre-2022 mono runtime.
200
+ private static volatile bool _isFreshThisSession;
201
+
202
+ /// <summary>
203
+ /// Direct read of the latest console-derived report by FQN. Returns <c>true</c> if an
204
+ /// entry exists for the given fully-qualified type name. The <paramref name="entry"/>
205
+ /// reference points at the live snapshot row -- callers must not mutate it.
206
+ /// </summary>
207
+ /// <remarks>
208
+ /// All mutation happens on the main thread inside <see cref="RescanNow"/>; the inspector
209
+ /// overlay (also main thread) reads via this method one-call-per-frame-per-component, so
210
+ /// there is no race that would justify the per-access defensive copy that
211
+ /// <see cref="Snapshot"/> performs. Prefer this method in hot paths.
212
+ /// </remarks>
213
+ public static bool TryGetEntry(string fullyQualifiedTypeName, out BaseCallReportEntry entry)
214
+ {
215
+ if (string.IsNullOrEmpty(fullyQualifiedTypeName))
216
+ {
217
+ entry = null;
218
+ return false;
219
+ }
220
+ return SnapshotInternal.TryGetValue(fullyQualifiedTypeName, out entry);
221
+ }
222
+
223
+ /// <summary>
224
+ /// Read-only snapshot of the latest console-derived report, keyed by FQN.
225
+ /// </summary>
226
+ /// <remarks>
227
+ /// Each access returns a fresh dictionary copy. Prefer <see cref="TryGetEntry"/> in hot
228
+ /// paths (the inspector overlay) -- this property exists for callers that need to enumerate
229
+ /// the full snapshot.
230
+ /// </remarks>
231
+ public static IReadOnlyDictionary<string, BaseCallReportEntry> Snapshot =>
232
+ new Dictionary<string, BaseCallReportEntry>(SnapshotInternal, StringComparer.Ordinal);
233
+
234
+ /// <summary>
235
+ /// <c>true</c> as long as the harvester has at least one functioning data source.
236
+ /// </summary>
237
+ /// <remarks>
238
+ /// The <c>CompilationPipeline.assemblyCompilationFinished</c> feed is wired
239
+ /// unconditionally on every supported Unity version, so this property is effectively
240
+ /// always <c>true</c> in normal operation; it only flips to <c>false</c> when the static
241
+ /// constructor itself throws (a hard initialization failure). The LogEntries reflection
242
+ /// layer is the optional source -- see <see cref="LogEntriesAvailable"/> for that flag.
243
+ /// The inspector overlay reads this property to decide whether to render its degraded
244
+ /// HelpBox, so the contract here is "should the overlay attempt to render at all".
245
+ /// </remarks>
246
+ public static bool IsAvailable { get; private set; } = true;
247
+
248
+ /// <summary>
249
+ /// <c>true</c> when the legacy <c>UnityEditor.LogEntries</c> reflection layer resolved
250
+ /// successfully on this Unity version. The harvester does not require this to be true to
251
+ /// function -- Unity 2021's analyzer warnings flow through the CompilerMessage feed
252
+ /// instead. Exposed primarily for diagnostics / tests.
253
+ /// </summary>
254
+ public static bool LogEntriesAvailable => !_logEntriesDisabled;
255
+
256
+ /// <summary>
257
+ /// <c>true</c> once the first <see cref="RescanNow"/> of this Editor session has produced
258
+ /// a fresh snapshot; <c>false</c> while the inspector is still showing the on-disk cache
259
+ /// loaded eagerly by the static constructor.
260
+ /// </summary>
261
+ /// <remarks>
262
+ /// The inspector overlay reads this to annotate its HelpBox: when <c>false</c> AND a
263
+ /// warning is being shown, the overlay appends a "(cached from previous session --
264
+ /// refreshing...)" suffix so the user knows the data is from yesterday's scan and a fresh
265
+ /// one is in flight. The flag is set inside <see cref="RescanNow"/> and never reset, so
266
+ /// the suffix disappears as soon as the first post-reload scan lands and stays gone for
267
+ /// the rest of the session.
268
+ /// </remarks>
269
+ public static bool IsFreshThisSession => _isFreshThisSession;
270
+
271
+ /// <summary>Raised whenever the snapshot changes (post-compile, post-domain-reload, or polled console-count change).</summary>
272
+ public static event Action ReportUpdated;
273
+
274
+ static DxMessagingConsoleHarvester()
275
+ {
276
+ try
277
+ {
278
+ Type logEntriesType =
279
+ Type.GetType("UnityEditor.LogEntries,UnityEditor.dll")
280
+ // S9: legacy / future-Unity probe. UnityEditorInternal.LogEntries doesn't
281
+ // exist today, but documenting the fallback as a one-liner keeps us forward-
282
+ // compatible at zero cost.
283
+ ?? Type.GetType("UnityEditorInternal.LogEntries,UnityEditor.dll");
284
+ _logEntryType =
285
+ Type.GetType("UnityEditor.LogEntry,UnityEditor.dll")
286
+ ?? Type.GetType("UnityEditorInternal.LogEntry,UnityEditor.dll");
287
+
288
+ bool logEntriesBound = false;
289
+ if (logEntriesType is not null && _logEntryType is not null)
290
+ {
291
+ if (_logEntryType.IsValueType)
292
+ {
293
+ // S8: defensive value-type guard. If a future Unity version makes LogEntry
294
+ // a struct, Activator.CreateInstance would hand us a boxed copy and the
295
+ // GetEntry call would mutate that copy in-place; harvest would silently
296
+ // report empty. Disable the LogEntries path rather than silently producing
297
+ // a wrong result; the CompilerMessage feed still runs.
298
+ LogOnce(
299
+ "logentry-is-struct",
300
+ "LogEntry is a value type on this Unity version; LogEntries scanning disabled. Falling back to the CompilerMessage feed."
301
+ );
302
+ }
303
+ else
304
+ {
305
+ _startGettingEntries = SafeGetStaticMethod(
306
+ logEntriesType,
307
+ "StartGettingEntries"
308
+ );
309
+ _endGettingEntries = SafeGetStaticMethod(
310
+ logEntriesType,
311
+ "EndGettingEntries"
312
+ );
313
+ _getEntryInternal = SafeGetStaticMethod(logEntriesType, "GetEntryInternal");
314
+ _getCount = SafeGetStaticMethod(logEntriesType, "GetCount");
315
+ _messageField = SafeGetInstanceField(_logEntryType, "message");
316
+
317
+ logEntriesBound =
318
+ _startGettingEntries is not null
319
+ && _endGettingEntries is not null
320
+ && _getEntryInternal is not null
321
+ && _getCount is not null
322
+ && _messageField is not null;
323
+ }
324
+ }
325
+
326
+ if (!logEntriesBound)
327
+ {
328
+ // The LogEntries reflection layer is unavailable. The IL-reflection scanner
329
+ // is the primary data source so this is no longer a critical path; the
330
+ // log-once is kept for diagnostic purposes (and only matters when the user
331
+ // has enabled the legacy bridge via DxMessagingSettings.UseConsoleBridge).
332
+ LogOnce(
333
+ "reflection-fallback",
334
+ "LogEntries reflection unavailable on this Unity version. The IL-reflection "
335
+ + "scanner remains the primary data source; the legacy console-scrape bridge "
336
+ + "(opt-in via DxMessagingSettings.UseConsoleBridge) cannot read LogEntries on this version."
337
+ );
338
+ _logEntriesDisabled = true;
339
+ }
340
+
341
+ LoadFromDisk();
342
+
343
+ // AssetDatabase isn't fully ready inside the static ctor; defer the first scan one
344
+ // editor tick so settings load doesn't fight a transitional asset-import state.
345
+ EditorApplication.delayCall += SafeRescanFromCallback;
346
+ AssemblyReloadEvents.afterAssemblyReload += SafeRescanFromCallback;
347
+ CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
348
+ if (!_logEntriesDisabled)
349
+ {
350
+ EditorApplication.update += Tick;
351
+ }
352
+ }
353
+ catch (Exception ex)
354
+ {
355
+ Debug.LogWarning(
356
+ $"[DxMessaging] DxMessagingConsoleHarvester failed to initialize: {ex.Message}"
357
+ );
358
+ _logEntriesDisabled = true;
359
+ IsAvailable = false;
360
+ }
361
+ }
362
+
363
+ /// <summary>
364
+ /// Force a re-read of the editor console and drain any pending CompilerMessage payloads.
365
+ /// Called automatically on domain reload and compilation events; the menu entry exposes it
366
+ /// for manual invocation. Settings setters (e.g.
367
+ /// <see cref="DxMessagingSettings.BaseCallCheckEnabled"/>) call this via
368
+ /// <see cref="EditorApplication.delayCall"/> so a re-enable repopulates the snapshot
369
+ /// without waiting for the next polled tick.
370
+ /// </summary>
371
+ [MenuItem("Tools/Wallstop Studios/DxMessaging/Rescan Base-Call Warnings")]
372
+ public static void RescanNow()
373
+ {
374
+ if (!IsAvailable)
375
+ {
376
+ return;
377
+ }
378
+
379
+ // Critical: NEVER touch LogEntries reflection or AssetDatabase while Unity is mid-
380
+ // compile or mid-asset-update. Reading LogEntries during compilation contends with the
381
+ // compiler's own log-buffer lock and can deadlock the editor. Touching AssetDatabase
382
+ // (via TryLoadSettings → GetOrCreateSettings → CreateAsset) during compilation
383
+ // schedules an import that re-triggers compilation; an infinite-loop trap that
384
+ // permanently freezes script-compilation startup. Defer to the post-compile state
385
+ // and let the polled tick (or the explicit afterAssemblyReload hook) pick it up.
386
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
387
+ {
388
+ return;
389
+ }
390
+
391
+ DxMessagingSettings settings = TryLoadSettings();
392
+ if (settings != null && !settings._baseCallCheckEnabled)
393
+ {
394
+ bool wasNonEmpty = SnapshotInternal.Count > 0;
395
+ SnapshotInternal.Clear();
396
+ // S3: keep the per-assembly bookkeeping (_typesByAssembly + _compilationMerged)
397
+ // in lock-step. Clearing only one half leaves stale rows that the next
398
+ // ApplyAssemblyReports call would silently re-promote into the snapshot when the
399
+ // user toggles the master switch back on without an intervening recompile.
400
+ _typesByAssembly.Clear();
401
+ _compilationMerged.Clear();
402
+ lock (_compilationFeedLock)
403
+ {
404
+ _pendingByAssembly.Clear();
405
+ }
406
+ _lastSeenCount = 0;
407
+ PersistToDisk();
408
+ // The "check disabled" path still represents a successful session-time decision
409
+ // about the snapshot; flip the freshness flag so the overlay never lingers in
410
+ // "cached from previous session" mode after the user has explicitly silenced the
411
+ // check. Doing this BEFORE RaiseReportUpdated mirrors the main path's ordering.
412
+ _isFreshThisSession = true;
413
+ if (wasNonEmpty)
414
+ {
415
+ RaiseReportUpdated();
416
+ }
417
+ return;
418
+ }
419
+
420
+ // -- Primary source (always-on): IL-reflection scanner over loaded
421
+ // MessageAwareComponent subclasses. Deterministic across Unity 2021 cache hits and
422
+ // incremental compiles; replaces the lossy console-scrape harvester as the
423
+ // inspector overlay's source of truth.
424
+ Dictionary<string, BaseCallReportEntry> scannerEntries;
425
+ try
426
+ {
427
+ scannerEntries = BaseCallTypeScanner.Scan(settings);
428
+ }
429
+ catch (Exception ex)
430
+ {
431
+ LogOnce("scanner", $"BaseCallTypeScanner.Scan threw: {ex.Message}");
432
+ scannerEntries = new Dictionary<string, BaseCallReportEntry>(
433
+ StringComparer.Ordinal
434
+ );
435
+ }
436
+
437
+ // The scanner produces a complete view of all loaded subclasses on every call, so it
438
+ // fully replaces the snapshot. Build the new map up-front from the scanner's output;
439
+ // we'll union the legacy-bridge entries into it below if the user opted in.
440
+ Dictionary<string, BaseCallReportEntry> nextSnapshot = new(
441
+ scannerEntries,
442
+ StringComparer.Ordinal
443
+ );
444
+
445
+ bool useBridge = settings != null && settings._useConsoleBridge;
446
+
447
+ int currentCount = 0;
448
+ bool logEntriesHarvested = false;
449
+ if (useBridge)
450
+ {
451
+ // -- Secondary source (opt-in): LogEntries reflection (Unity 2022+ reliable path).
452
+ Dictionary<string, ParsedTypeReport> logEntriesAggregate = HarvestFromLogEntries(
453
+ out currentCount,
454
+ out logEntriesHarvested
455
+ );
456
+
457
+ // -- Secondary source (opt-in): pending CompilerMessage payloads (Unity 2021's
458
+ // primary path under the legacy bridge). Drain the cross-thread channel
459
+ // atomically.
460
+ Dictionary<string, Dictionary<string, ParsedTypeReport>> drained;
461
+ lock (_compilationFeedLock)
462
+ {
463
+ if (_pendingByAssembly.Count == 0)
464
+ {
465
+ drained = null;
466
+ }
467
+ else
468
+ {
469
+ drained = new Dictionary<string, Dictionary<string, ParsedTypeReport>>(
470
+ _pendingByAssembly,
471
+ StringComparer.OrdinalIgnoreCase
472
+ );
473
+ _pendingByAssembly.Clear();
474
+ }
475
+ }
476
+
477
+ ApplyCompilerMessageDrain(drained);
478
+
479
+ // Merge the bridge view (LogEntries + CompilerMessage) and union it INTO the
480
+ // scanner-produced snapshot. The scanner is authoritative; the bridge can only
481
+ // ADD methods/diagnostic ids it sees that the scanner missed (e.g. exotic IL
482
+ // shapes the byte walker stepped past). Bridge entries never override the
483
+ // scanner's classification.
484
+ try
485
+ {
486
+ Dictionary<string, BaseCallReportEntryDto> bridgeSnapshot =
487
+ BaseCallReportAggregator.BuildSnapshot(
488
+ logEntriesHarvested ? logEntriesAggregate : null,
489
+ _compilationMerged
490
+ );
491
+ UnionBridgeIntoSnapshot(bridgeSnapshot, nextSnapshot);
492
+ }
493
+ catch (Exception ex)
494
+ {
495
+ LogOnce("aggregate", $"Snapshot merge failed: {ex.Message}");
496
+ // Fall through with the scanner-only snapshot; partial data is better than
497
+ // wiping the snapshot when the bridge half misbehaves.
498
+ }
499
+ }
500
+ else
501
+ {
502
+ // Bridge is disabled: drop any pending CompilerMessage entries the harvester may
503
+ // have buffered (they would otherwise leak into the snapshot the next time the
504
+ // user toggles the bridge on). The bridge bookkeeping is reset below as well.
505
+ lock (_compilationFeedLock)
506
+ {
507
+ _pendingByAssembly.Clear();
508
+ }
509
+ _typesByAssembly.Clear();
510
+ _compilationMerged.Clear();
511
+ }
512
+
513
+ // Replace the live snapshot with the new view in one swap. The scanner runs over ALL
514
+ // loaded types every time, so this is a full-replace; types the user has fixed since
515
+ // the last scan disappear, types newly broken appear.
516
+ SnapshotInternal.Clear();
517
+ foreach (KeyValuePair<string, BaseCallReportEntry> kvp in nextSnapshot)
518
+ {
519
+ SnapshotInternal[kvp.Key] = kvp.Value;
520
+ }
521
+
522
+ if (useBridge && logEntriesHarvested)
523
+ {
524
+ _lastSeenCount = currentCount;
525
+ }
526
+ PersistToDisk();
527
+ // Mark the snapshot as session-fresh AFTER the persist + before the event fires, so
528
+ // that any subscriber repainting the inspector observes the same "fresh" state the
529
+ // overlay will see on its next read. Subsequent scans are no-ops on this flag.
530
+ _isFreshThisSession = true;
531
+ RaiseReportUpdated();
532
+ }
533
+
534
+ // Unions the bridge-produced DTOs into the scanner-produced snapshot. The scanner is the
535
+ // authoritative source; the bridge can only contribute methods / diagnostic ids the
536
+ // scanner missed for a type, OR a brand-new type entry the scanner did not produce (e.g.
537
+ // a subclass the scanner couldn't classify because its IL was stripped). The first non-
538
+ // empty file path / line wins, matching the bridge's pre-existing semantics.
539
+ private static void UnionBridgeIntoSnapshot(
540
+ Dictionary<string, BaseCallReportEntryDto> bridgeSnapshot,
541
+ Dictionary<string, BaseCallReportEntry> scannerSnapshot
542
+ )
543
+ {
544
+ if (bridgeSnapshot is null || bridgeSnapshot.Count == 0)
545
+ {
546
+ return;
547
+ }
548
+ foreach (KeyValuePair<string, BaseCallReportEntryDto> kvp in bridgeSnapshot)
549
+ {
550
+ BaseCallReportEntryDto dto = kvp.Value;
551
+ if (dto is null || string.IsNullOrEmpty(dto.TypeName))
552
+ {
553
+ continue;
554
+ }
555
+ if (!scannerSnapshot.TryGetValue(dto.TypeName, out BaseCallReportEntry existing))
556
+ {
557
+ existing = new BaseCallReportEntry
558
+ {
559
+ typeName = dto.TypeName,
560
+ missingBaseFor = new List<string>(dto.MissingBaseFor),
561
+ diagnosticIds = dto.DiagnosticIds.ToList(),
562
+ filePath = dto.FilePath ?? string.Empty,
563
+ line = dto.Line,
564
+ };
565
+ scannerSnapshot[dto.TypeName] = existing;
566
+ continue;
567
+ }
568
+ foreach (string method in dto.MissingBaseFor)
569
+ {
570
+ if (
571
+ !string.IsNullOrEmpty(method)
572
+ && !existing.missingBaseFor.Contains(method, StringComparer.Ordinal)
573
+ )
574
+ {
575
+ existing.missingBaseFor.Add(method);
576
+ }
577
+ }
578
+ foreach (string id in dto.DiagnosticIds)
579
+ {
580
+ if (
581
+ !string.IsNullOrEmpty(id)
582
+ && !existing.diagnosticIds.Contains(id, StringComparer.Ordinal)
583
+ )
584
+ {
585
+ existing.diagnosticIds.Add(id);
586
+ }
587
+ }
588
+ if (string.IsNullOrEmpty(existing.filePath) && !string.IsNullOrEmpty(dto.FilePath))
589
+ {
590
+ existing.filePath = dto.FilePath;
591
+ existing.line = dto.Line;
592
+ }
593
+ }
594
+ }
595
+
596
+ // Reads the editor console via LogEntries reflection. Returns the aggregated per-type
597
+ // report, the current console count, and whether the harvest actually ran (false when
598
+ // the LogEntries reflection layer is unavailable or threw). On Unity 2021 this returns
599
+ // an empty aggregate every time; the analyzer warnings flow through the CompilerMessage
600
+ // feed instead and arrive via ApplyCompilerMessageDrain.
601
+ private static Dictionary<string, ParsedTypeReport> HarvestFromLogEntries(
602
+ out int currentCount,
603
+ out bool harvested
604
+ )
605
+ {
606
+ currentCount = 0;
607
+ harvested = false;
608
+ if (_logEntriesDisabled)
609
+ {
610
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
611
+ }
612
+
613
+ try
614
+ {
615
+ currentCount = (int)_getCount.Invoke(null, null);
616
+ }
617
+ catch (Exception ex)
618
+ {
619
+ LogOnce("getcount", $"GetCount invocation failed: {ex.Message}");
620
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
621
+ }
622
+
623
+ // S4: console-clear handling. We always overwrite _lastSeenCount near the bottom of
624
+ // RescanNow, so the only point of acting on a shrunken count here is to be explicit
625
+ // about the semantic. The accumulator is rebuilt from scratch every rescan, so the
626
+ // clear case is naturally consistent; even an empty log produces an empty aggregate
627
+ // and a ReportUpdated fire that drops stale rows.
628
+
629
+ // B2 + S6: enter the get/end pair only AFTER StartGettingEntries actually succeeded.
630
+ try
631
+ {
632
+ _startGettingEntries.Invoke(null, null);
633
+ }
634
+ catch (Exception ex)
635
+ {
636
+ LogOnce("start", $"StartGettingEntries invocation failed: {ex.Message}");
637
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
638
+ }
639
+
640
+ // N1: clamp the initial capacity to a sane ceiling. The list is allowed to grow past
641
+ // this if the console really does hold more entries; we just don't blow up the heap on
642
+ // first allocation.
643
+ List<string> lines = new(Math.Min(currentCount, MaxLineListInitialCapacity));
644
+ int harvestedCount = currentCount;
645
+ try
646
+ {
647
+ if (_startGettingEntries.ReturnType == typeof(int))
648
+ {
649
+ // The Invoke return value is intentionally discarded; we re-pull via GetCount
650
+ // because the polled count is authoritative.
651
+ try
652
+ {
653
+ harvestedCount = (int)_getCount.Invoke(null, null);
654
+ }
655
+ catch
656
+ {
657
+ // Retain the previous count.
658
+ }
659
+ }
660
+
661
+ object entryInstance = Activator.CreateInstance(_logEntryType);
662
+ object[] invokeArgs = new object[2];
663
+ invokeArgs[1] = entryInstance;
664
+ for (int j = 0; j < harvestedCount; j++)
665
+ {
666
+ invokeArgs[0] = j;
667
+ try
668
+ {
669
+ _getEntryInternal.Invoke(null, invokeArgs);
670
+ }
671
+ catch (Exception ex)
672
+ {
673
+ LogOnce(
674
+ "getentry",
675
+ $"GetEntryInternal invocation failed at index {j}: {ex.Message}"
676
+ );
677
+ continue;
678
+ }
679
+
680
+ string message;
681
+ try
682
+ {
683
+ message = _messageField.GetValue(entryInstance) as string;
684
+ }
685
+ catch (Exception ex)
686
+ {
687
+ LogOnce(
688
+ "getmessage",
689
+ $"LogEntry.message read failed at index {j}: {ex.Message}"
690
+ );
691
+ continue;
692
+ }
693
+
694
+ if (!string.IsNullOrEmpty(message))
695
+ {
696
+ lines.Add(message);
697
+ }
698
+ }
699
+ }
700
+ catch (Exception ex)
701
+ {
702
+ LogOnce("harvest", $"Harvest loop failed: {ex.Message}");
703
+ }
704
+ finally
705
+ {
706
+ try
707
+ {
708
+ _endGettingEntries.Invoke(null, null);
709
+ }
710
+ catch (Exception ex)
711
+ {
712
+ LogOnce("end", $"EndGettingEntries invocation failed: {ex.Message}");
713
+ }
714
+ }
715
+
716
+ harvested = true;
717
+ try
718
+ {
719
+ return BaseCallLogMessageParser.Aggregate(lines);
720
+ }
721
+ catch (Exception ex)
722
+ {
723
+ LogOnce(
724
+ "aggregate-logentries",
725
+ $"Aggregating LogEntries lines failed: {ex.Message}"
726
+ );
727
+ return new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal);
728
+ }
729
+ }
730
+
731
+ // Folds a freshly-drained per-assembly batch into the long-lived per-assembly bookkeeping
732
+ // via <see cref="BaseCallReportAggregator.ApplyAssemblyReports"/>. The aggregator owns the
733
+ // retirement logic (a type the user fixed disappears as soon as the assembly recompiles
734
+ // without re-reporting it) and the cross-assembly survival rule (a type stays in the
735
+ // merged view as long as ANY assembly still reports it).
736
+ private static void ApplyCompilerMessageDrain(
737
+ Dictionary<string, Dictionary<string, ParsedTypeReport>> drained
738
+ )
739
+ {
740
+ if (drained is null || drained.Count == 0)
741
+ {
742
+ return;
743
+ }
744
+
745
+ foreach (KeyValuePair<string, Dictionary<string, ParsedTypeReport>> kvp in drained)
746
+ {
747
+ BaseCallReportAggregator.ApplyAssemblyReports(
748
+ kvp.Key,
749
+ kvp.Value ?? new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal),
750
+ _typesByAssembly,
751
+ _compilationMerged
752
+ );
753
+ }
754
+ }
755
+
756
+ /// <summary>
757
+ /// Hint the harvester that something external changed (e.g., a settings toggle) and the
758
+ /// next polled tick should treat the console as fresh. Cheaper than a synchronous
759
+ /// <see cref="RescanNow"/> when the caller is on a thread / context that may not be safe
760
+ /// to do reflection from.
761
+ /// </summary>
762
+ public static void RequestRescan()
763
+ {
764
+ if (!IsAvailable)
765
+ {
766
+ return;
767
+ }
768
+ // Setting _lastSeenCount to a sentinel forces the next Tick to see a count delta and
769
+ // call RescanNow on the editor's update thread (when LogEntries is wired). When
770
+ // LogEntries is unavailable, Tick is not registered, so we fall back to delayCall.
771
+ if (_logEntriesDisabled)
772
+ {
773
+ if (!_rescanScheduled)
774
+ {
775
+ _rescanScheduled = true;
776
+ EditorApplication.delayCall += DrainScheduledRescan;
777
+ }
778
+ return;
779
+ }
780
+ _lastSeenCount = -1;
781
+ }
782
+
783
+ private static void Tick()
784
+ {
785
+ // Tick is only registered when the LogEntries reflection layer is available, so we
786
+ // do NOT need to re-check _logEntriesDisabled here; but the IsAvailable guard
787
+ // protects against a future failure mode where IsAvailable is flipped to false at
788
+ // runtime.
789
+ if (!IsAvailable)
790
+ {
791
+ return;
792
+ }
793
+
794
+ // Defensive belt: never reflect into LogEntries while a compile or asset-import is
795
+ // running. Even though RescanNow() itself bails on this state, we don't want to even
796
+ // call GetCount(); the lock contention is the source of the freeze, and GetCount
797
+ // touches the same buffer.
798
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
799
+ {
800
+ return;
801
+ }
802
+
803
+ try
804
+ {
805
+ double now = EditorApplication.timeSinceStartup;
806
+ if (now - _lastTickTime < PollIntervalSeconds)
807
+ {
808
+ return;
809
+ }
810
+ _lastTickTime = now;
811
+
812
+ int currentCount;
813
+ try
814
+ {
815
+ currentCount = (int)_getCount.Invoke(null, null);
816
+ }
817
+ catch (Exception ex)
818
+ {
819
+ LogOnce("tick-count", $"GetCount during Tick failed: {ex.Message}");
820
+ return;
821
+ }
822
+
823
+ if (currentCount != _lastSeenCount)
824
+ {
825
+ RescanNow();
826
+ }
827
+ }
828
+ catch (Exception ex)
829
+ {
830
+ LogOnce("tick", $"Tick failed: {ex.Message}");
831
+ }
832
+ }
833
+
834
+ private static void OnAssemblyCompilationFinished(
835
+ string assemblyPath,
836
+ CompilerMessage[] messages
837
+ )
838
+ {
839
+ // CRITICAL: this fires for EVERY assembly compiled (10s of times per build). Running
840
+ // RescanNow synchronously here invokes LogEntries reflection while OTHER assemblies
841
+ // are still compiling; the compiler holds its log-buffer lock and our reflection
842
+ // call blocks waiting for it. Combined with AssetDatabase touches inside RescanNow,
843
+ // this caused permanent script-compilation freezes on Unity startup.
844
+ //
845
+ // S4: when the legacy console-bridge is OFF, we don't need to parse CompilerMessage
846
+ // payloads at all; the IL-reflection scanner is the sole data source and it runs
847
+ // off the AssemblyReloadEvents.afterAssemblyReload hook that fires once per build,
848
+ // not per-assembly. Bail out early so a 30-assembly build doesn't burn CPU running
849
+ // the regex-heavy parser 30 times for output we'll never read. Read the setting once
850
+ // up-front so the gate decision is consistent for the whole callback (settings can
851
+ // be edited concurrently by the Project Settings page on the main thread).
852
+ DxMessagingSettings settingsForGate = TryLoadSettings();
853
+ bool bridgeEnabled = settingsForGate != null && settingsForGate._useConsoleBridge;
854
+ if (!bridgeEnabled)
855
+ {
856
+ return;
857
+ }
858
+
859
+ // We DO parse the per-assembly CompilerMessage payload here (cheap, pure-CPU work,
860
+ // no AssetDatabase / LogEntries contact) and stash it in the cross-thread channel.
861
+ // DrainScheduledRescan (on a delayCall) folds the channel into the live snapshot
862
+ // once the compile burst is complete. This is the primary data path on Unity 2021,
863
+ // where Roslyn-analyzer warnings DO arrive in CompilerMessage[] but do NOT reliably
864
+ // appear in the LogEntries store.
865
+ try
866
+ {
867
+ if (!string.IsNullOrEmpty(assemblyPath) && messages != null)
868
+ {
869
+ List<string> lines = null;
870
+ foreach (CompilerMessage compilerMessage in messages)
871
+ {
872
+ string body = compilerMessage.message;
873
+ if (string.IsNullOrEmpty(body))
874
+ {
875
+ continue;
876
+ }
877
+ // Quick prefilter so we don't parse every CS0123 in the build. The
878
+ // analyzer always emits "DXMSG00" inside the diagnostic id.
879
+ if (body.IndexOf("DXMSG00", StringComparison.Ordinal) < 0)
880
+ {
881
+ continue;
882
+ }
883
+ lines ??= new List<string>();
884
+ lines.Add(body);
885
+ }
886
+
887
+ Dictionary<string, ParsedTypeReport> aggregated = lines is null
888
+ ? new Dictionary<string, ParsedTypeReport>(StringComparer.Ordinal)
889
+ : BaseCallLogMessageParser.Aggregate(lines);
890
+
891
+ lock (_compilationFeedLock)
892
+ {
893
+ // Even when this assembly produced zero matching messages, we still want
894
+ // an empty entry so DrainScheduledRescan can RETIRE the assembly's prior
895
+ // attribution (the user fixed every offending type in this assembly).
896
+ _pendingByAssembly[assemblyPath] = aggregated;
897
+ }
898
+ }
899
+ }
900
+ catch (Exception ex)
901
+ {
902
+ LogOnce(
903
+ "compilation-parse",
904
+ $"Failed to parse CompilerMessage payload for {assemblyPath}: {ex.Message}"
905
+ );
906
+ }
907
+
908
+ // Fix: schedule a single delayCall. delayCall fires AFTER the current event chain
909
+ // unwinds and AFTER `EditorApplication.isCompiling` flips back to false. Multiple
910
+ // delayCall registrations from the same compile burst are debounced by the
911
+ // _rescanScheduled latch; only one deferred RescanNow runs per build.
912
+ if (_rescanScheduled)
913
+ {
914
+ return;
915
+ }
916
+ _rescanScheduled = true;
917
+ EditorApplication.delayCall += DrainScheduledRescan;
918
+ }
919
+
920
+ private static void DrainScheduledRescan()
921
+ {
922
+ _rescanScheduled = false;
923
+ // delayCall can fire while still mid-compile if the editor is in a weird state.
924
+ // RescanNow has its own isCompiling/isUpdating guard; re-defer if needed.
925
+ if (EditorApplication.isCompiling || EditorApplication.isUpdating)
926
+ {
927
+ if (!_rescanScheduled)
928
+ {
929
+ _rescanScheduled = true;
930
+ EditorApplication.delayCall += DrainScheduledRescan;
931
+ }
932
+ return;
933
+ }
934
+ SafeRescanFromCallback();
935
+ }
936
+
937
+ private static void SafeRescanFromCallback()
938
+ {
939
+ try
940
+ {
941
+ RescanNow();
942
+ }
943
+ catch (Exception ex)
944
+ {
945
+ LogOnce("rescan-callback", $"RescanNow callback threw: {ex.Message}");
946
+ }
947
+ }
948
+
949
+ private static DxMessagingSettings TryLoadSettings()
950
+ {
951
+ // CRITICAL: passive load only. We must NOT call GetOrCreateSettings here; that path
952
+ // can call AssetDatabase.CreateAsset, which during script compilation schedules an
953
+ // import → re-triggers compilation → permanent freeze. The Project Settings page and
954
+ // the inspector overlay both call GetOrCreateSettings on demand (outside compilation),
955
+ // so the asset is materialised through normal user interaction. If the asset doesn't
956
+ // exist yet (fresh project, first compile), the harvester treats the snapshot as
957
+ // unconfigured and behaves as if the master toggle is enabled (default behaviour).
958
+ try
959
+ {
960
+ string[] guids = AssetDatabase.FindAssets($"t:{nameof(DxMessagingSettings)}");
961
+ if (guids == null || guids.Length == 0)
962
+ {
963
+ return null;
964
+ }
965
+ string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
966
+ if (string.IsNullOrEmpty(assetPath))
967
+ {
968
+ return null;
969
+ }
970
+ return AssetDatabase.LoadAssetAtPath<DxMessagingSettings>(assetPath);
971
+ }
972
+ catch (Exception ex)
973
+ {
974
+ LogOnce("settings", $"Could not load DxMessagingSettings: {ex.Message}");
975
+ return null;
976
+ }
977
+ }
978
+
979
+ private static MethodInfo SafeGetStaticMethod(Type type, string name)
980
+ {
981
+ try
982
+ {
983
+ return type.GetMethod(name, BindingFlags.Public | BindingFlags.Static);
984
+ }
985
+ catch (Exception ex)
986
+ {
987
+ LogOnce(
988
+ $"resolve-{name}",
989
+ $"Failed to resolve static method '{name}' on {type.FullName}: {ex.Message}"
990
+ );
991
+ return null;
992
+ }
993
+ }
994
+
995
+ private static FieldInfo SafeGetInstanceField(Type type, string name)
996
+ {
997
+ try
998
+ {
999
+ return type.GetField(name, BindingFlags.Public | BindingFlags.Instance);
1000
+ }
1001
+ catch (Exception ex)
1002
+ {
1003
+ LogOnce(
1004
+ $"resolve-field-{name}",
1005
+ $"Failed to resolve instance field '{name}' on {type.FullName}: {ex.Message}"
1006
+ );
1007
+ return null;
1008
+ }
1009
+ }
1010
+
1011
+ private static void LogOnce(string key, string message)
1012
+ {
1013
+ if (!AlreadyWarned.Add(key))
1014
+ {
1015
+ return;
1016
+ }
1017
+ Debug.LogWarning($"[DxMessaging] {message}");
1018
+ }
1019
+
1020
+ private static void RaiseReportUpdated()
1021
+ {
1022
+ try
1023
+ {
1024
+ ReportUpdated?.Invoke();
1025
+ }
1026
+ catch (Exception ex)
1027
+ {
1028
+ Debug.LogWarning($"[DxMessaging] ReportUpdated subscriber threw: {ex.Message}");
1029
+ }
1030
+ }
1031
+
1032
+ // -- JSON persistence -----------------------------------------------------------------
1033
+ // The cache survives editor restarts so the overlay has data to render before the first
1034
+ // post-launch rescan completes; it is rewritten on every successful rescan.
1035
+
1036
+ internal static void PersistToDisk()
1037
+ {
1038
+ try
1039
+ {
1040
+ string absolutePath = GetReportFilePath();
1041
+ EnsureDirectoryExists(absolutePath);
1042
+
1043
+ BaseCallReportFile file = new()
1044
+ {
1045
+ version = 1,
1046
+ generatedAt = DateTime.UtcNow.ToString(
1047
+ "yyyy-MM-ddTHH:mm:ssZ",
1048
+ CultureInfo.InvariantCulture
1049
+ ),
1050
+ types = SnapshotInternal
1051
+ .Values.OrderBy(e => e.typeName, StringComparer.Ordinal)
1052
+ .ToList(),
1053
+ };
1054
+
1055
+ string json = JsonUtility.ToJson(file, prettyPrint: true);
1056
+ File.WriteAllText(absolutePath, json);
1057
+ }
1058
+ catch (Exception ex)
1059
+ {
1060
+ LogOnce("persist", $"Failed to persist analyzer diagnostics report: {ex.Message}");
1061
+ }
1062
+ }
1063
+
1064
+ internal static void LoadFromDisk()
1065
+ {
1066
+ try
1067
+ {
1068
+ string absolutePath = GetReportFilePath();
1069
+ if (!File.Exists(absolutePath))
1070
+ {
1071
+ return;
1072
+ }
1073
+
1074
+ string json = File.ReadAllText(absolutePath);
1075
+ if (string.IsNullOrWhiteSpace(json))
1076
+ {
1077
+ return;
1078
+ }
1079
+
1080
+ BaseCallReportFile file = JsonUtility.FromJson<BaseCallReportFile>(json);
1081
+ if (file?.types == null)
1082
+ {
1083
+ return;
1084
+ }
1085
+
1086
+ SnapshotInternal.Clear();
1087
+ foreach (BaseCallReportEntry entry in file.types)
1088
+ {
1089
+ if (entry == null || string.IsNullOrEmpty(entry.typeName))
1090
+ {
1091
+ continue;
1092
+ }
1093
+ entry.missingBaseFor ??= new List<string>();
1094
+ entry.diagnosticIds ??= new List<string>();
1095
+ SnapshotInternal[entry.typeName] = entry;
1096
+ }
1097
+ }
1098
+ catch (Exception ex)
1099
+ {
1100
+ LogOnce("load", $"Failed to load analyzer diagnostics report: {ex.Message}");
1101
+ }
1102
+ }
1103
+
1104
+ internal static string GetReportFilePath()
1105
+ {
1106
+ string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."))
1107
+ .Replace("\\", "/");
1108
+ return Path.Combine(projectRoot, "Library", ReportDirectoryName, ReportFileName)
1109
+ .Replace("\\", "/");
1110
+ }
1111
+
1112
+ private static void EnsureDirectoryExists(string absolutePath)
1113
+ {
1114
+ string directory = Path.GetDirectoryName(absolutePath);
1115
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
1116
+ {
1117
+ Directory.CreateDirectory(directory);
1118
+ }
1119
+ }
1120
+ }
1121
+ #endif
1122
+ }