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.
- package/CHANGELOG.md +106 -67
- package/CHANGELOG.md.meta +7 -7
- package/Editor/Analyzers/BaseCallIlInspector.cs +277 -0
- package/Editor/Analyzers/BaseCallIlInspector.cs.meta +11 -0
- package/Editor/Analyzers/BaseCallLogMessageParser.cs +295 -0
- package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +11 -0
- package/Editor/Analyzers/BaseCallReportAggregator.cs +308 -0
- package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +11 -0
- package/Editor/Analyzers/BaseCallTypeScanner.cs +110 -0
- package/Editor/Analyzers/BaseCallTypeScanner.cs.meta +11 -0
- package/Editor/Analyzers/BaseCallTypeScannerCore.cs +562 -0
- package/Editor/Analyzers/BaseCallTypeScannerCore.cs.meta +11 -0
- package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +1122 -0
- package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +11 -0
- package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +44 -44
- package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +44 -44
- package/Editor/Analyzers/System.Collections.Immutable.dll.meta +44 -44
- package/Editor/Analyzers/System.Reflection.Metadata.dll.meta +44 -44
- package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +44 -44
- package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
- package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +33 -0
- package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
- package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +72 -72
- package/Editor/Analyzers.meta +8 -8
- package/Editor/AssemblyInfo.cs.meta +3 -3
- package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs +81 -0
- package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +11 -0
- package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +420 -0
- package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +11 -0
- package/Editor/CustomEditors/MessagingComponentEditor.cs +1 -1
- package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +2 -2
- package/Editor/CustomEditors.meta +2 -2
- package/Editor/DxMessagingEditorInitializer.cs.meta +2 -2
- package/Editor/DxMessagingMenu.cs.meta +11 -11
- package/Editor/DxMessagingSceneBuildProcessor.cs.meta +11 -11
- package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +190 -0
- package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +11 -0
- package/Editor/Settings/DxMessagingSettings.cs +189 -0
- package/Editor/Settings/DxMessagingSettings.cs.meta +2 -2
- package/Editor/Settings/DxMessagingSettingsProvider.cs +50 -33
- package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +2 -2
- package/Editor/Settings.meta +2 -2
- package/Editor/SetupCscRsp.cs +209 -8
- package/Editor/SetupCscRsp.cs.meta +2 -2
- package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
- package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +3 -3
- package/Editor/Testing.meta +3 -3
- package/Editor/WallstopStudios.DxMessaging.Editor.asmdef +14 -14
- package/Editor/WallstopStudios.DxMessaging.Editor.asmdef.meta +7 -7
- package/Editor.meta +8 -8
- package/LICENSE.md +9 -9
- package/LICENSE.md.meta +7 -7
- package/README.md +941 -900
- package/README.md.meta +7 -7
- package/Runtime/AssemblyInfo.cs +4 -0
- package/Runtime/AssemblyInfo.cs.meta +2 -2
- package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +2 -2
- package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +2 -2
- package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs +26 -0
- package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +11 -0
- package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +2 -2
- package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +2 -2
- package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +2 -2
- package/Runtime/Core/Attributes.meta +2 -2
- package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs +195 -0
- package/Runtime/Core/Configuration/DxMessagingRuntimeSettings.cs.meta +11 -0
- package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs +179 -0
- package/Runtime/Core/Configuration/DxMessagingRuntimeSettingsProvider.cs.meta +11 -0
- package/Runtime/Core/Configuration.meta +9 -0
- package/Runtime/Core/DataStructure/CyclicBuffer.cs +2 -2
- package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +2 -2
- package/Runtime/Core/DataStructure.meta +2 -2
- package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +2 -2
- package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +2 -2
- package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +2 -2
- package/Runtime/Core/Diagnostics.meta +2 -2
- package/Runtime/Core/DxMessagingStaticState.cs +19 -0
- package/Runtime/Core/DxMessagingStaticState.cs.meta +11 -11
- package/Runtime/Core/Extensions/EnumExtensions.cs.meta +2 -2
- package/Runtime/Core/Extensions/IListExtensions.cs.meta +2 -2
- package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +12 -12
- package/Runtime/Core/Extensions/MessageExtensions.cs.meta +11 -11
- package/Runtime/Core/Extensions.meta +8 -8
- package/Runtime/Core/Helper/MessageCache.cs +32 -0
- package/Runtime/Core/Helper/MessageCache.cs.meta +2 -2
- package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +2 -2
- package/Runtime/Core/Helper.meta +2 -2
- package/Runtime/Core/IMessage.cs +3 -3
- package/Runtime/Core/IMessage.cs.meta +11 -11
- package/Runtime/Core/InstanceId.cs.meta +11 -11
- package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +51 -0
- package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta +11 -0
- package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs +38 -0
- package/Runtime/Core/Internal/TypedGlobalSlotIndex.cs.meta +11 -0
- package/Runtime/Core/Internal/TypedSlotIndex.cs +81 -0
- package/Runtime/Core/Internal/TypedSlotIndex.cs.meta +11 -0
- package/Runtime/Core/Internal/TypedSlots.cs +613 -0
- package/Runtime/Core/Internal/TypedSlots.cs.meta +11 -0
- package/Runtime/Core/Internal.meta +9 -0
- package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +11 -11
- package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +11 -11
- package/Runtime/Core/MessageBus/IMessageBus.cs +177 -3
- package/Runtime/Core/MessageBus/IMessageBus.cs.meta +11 -11
- package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +11 -11
- package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +11 -11
- package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs +16 -0
- package/Runtime/Core/MessageBus/Internal/BusContextIndex.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs +40 -0
- package/Runtime/Core/MessageBus/Internal/BusSinkIndex.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/BusSlots.cs +718 -0
- package/Runtime/Core/MessageBus/Internal/BusSlots.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/DispatchKind.cs +38 -0
- package/Runtime/Core/MessageBus/Internal/DispatchKind.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs +20 -0
- package/Runtime/Core/MessageBus/Internal/DispatchPhase.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs +28 -0
- package/Runtime/Core/MessageBus/Internal/DispatchVariant.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs +48 -0
- package/Runtime/Core/MessageBus/Internal/IEvictableSlot.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/ISweepable.cs +15 -0
- package/Runtime/Core/MessageBus/Internal/ISweepable.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs +222 -0
- package/Runtime/Core/MessageBus/Internal/RegistrationMethodAxes.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal/SlotKey.cs +192 -0
- package/Runtime/Core/MessageBus/Internal/SlotKey.cs.meta +11 -0
- package/Runtime/Core/MessageBus/Internal.meta +9 -0
- package/Runtime/Core/MessageBus/MessageBus.cs +2651 -500
- package/Runtime/Core/MessageBus/MessageBus.cs.meta +11 -11
- package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +11 -11
- package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +11 -11
- package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +11 -11
- package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +11 -11
- package/Runtime/Core/MessageBus.meta +8 -8
- package/Runtime/Core/MessageHandler.cs +2019 -542
- package/Runtime/Core/MessageHandler.cs.meta +11 -11
- package/Runtime/Core/MessageRegistrationHandle.cs.meta +11 -11
- package/Runtime/Core/MessageRegistrationToken.cs +7 -0
- package/Runtime/Core/MessageRegistrationToken.cs.meta +11 -11
- package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +2 -2
- package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +11 -11
- package/Runtime/Core/Messages/ITargetedMessage.cs.meta +11 -11
- package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +11 -11
- package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +2 -2
- package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +11 -11
- package/Runtime/Core/Messages/StringMessage.cs.meta +2 -2
- package/Runtime/Core/Messages.meta +8 -8
- package/Runtime/Core/MessagingDebug.cs.meta +11 -11
- package/Runtime/Core/Pooling/CollectionPool.cs +266 -0
- package/Runtime/Core/Pooling/CollectionPool.cs.meta +11 -0
- package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs +30 -0
- package/Runtime/Core/Pooling/CollectionPoolDiagnostics.cs.meta +11 -0
- package/Runtime/Core/Pooling/DxPools.cs +157 -0
- package/Runtime/Core/Pooling/DxPools.cs.meta +11 -0
- package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs +106 -0
- package/Runtime/Core/Pooling/EvictionPlayerLoopHook.cs.meta +11 -0
- package/Runtime/Core/Pooling/IDxMessagingClock.cs +18 -0
- package/Runtime/Core/Pooling/IDxMessagingClock.cs.meta +11 -0
- package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs +55 -0
- package/Runtime/Core/Pooling/PoolDiagnosticsSnapshot.cs.meta +11 -0
- package/Runtime/Core/Pooling/StopwatchClock.cs +27 -0
- package/Runtime/Core/Pooling/StopwatchClock.cs.meta +11 -0
- package/Runtime/Core/Pooling/UnityRealtimeClock.cs +31 -0
- package/Runtime/Core/Pooling/UnityRealtimeClock.cs.meta +11 -0
- package/Runtime/Core/Pooling.meta +9 -0
- package/Runtime/Core.meta +8 -8
- package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +12 -12
- package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +11 -11
- package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +12 -12
- package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +2 -2
- package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs +73 -0
- package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +11 -11
- package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef +20 -20
- package/Runtime/Unity/Integrations/Reflex/WallstopStudios.DxMessaging.Reflex.asmdef.meta +7 -7
- package/Runtime/Unity/Integrations/Reflex.meta +8 -8
- package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +2 -2
- package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs +109 -1
- package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +11 -11
- package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef +30 -30
- package/Runtime/Unity/Integrations/VContainer/WallstopStudios.DxMessaging.VContainer.asmdef.meta +7 -7
- package/Runtime/Unity/Integrations/VContainer.meta +8 -8
- package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +2 -2
- package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef +30 -30
- package/Runtime/Unity/Integrations/Zenject/WallstopStudios.DxMessaging.Zenject.asmdef.meta +7 -7
- package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs +79 -1
- package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +11 -11
- package/Runtime/Unity/Integrations/Zenject.meta +8 -8
- package/Runtime/Unity/Integrations.meta +8 -8
- package/Runtime/Unity/MessageAwareComponent.cs +29 -0
- package/Runtime/Unity/MessageAwareComponent.cs.meta +11 -11
- package/Runtime/Unity/MessageBusProviderHandle.cs.meta +12 -12
- package/Runtime/Unity/MessagingComponent.cs.meta +11 -11
- package/Runtime/Unity/MessagingComponentInstaller.cs.meta +12 -12
- package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +12 -12
- package/Runtime/Unity.meta +8 -8
- package/Runtime/WallstopStudios.DxMessaging.asmdef +14 -14
- package/Runtime/WallstopStudios.DxMessaging.asmdef.meta +7 -7
- package/Runtime.meta +8 -8
- package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab +98 -98
- package/Samples~/DI/Prefabs/MessagingInstallerSample.prefab.meta +7 -7
- package/Samples~/DI/Prefabs.meta +8 -8
- package/Samples~/DI/Providers/GlobalMessageBusProvider.asset +14 -14
- package/Samples~/DI/Providers/GlobalMessageBusProvider.asset.meta +8 -8
- package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset +14 -14
- package/Samples~/DI/Providers/InitialGlobalMessageBusProvider.asset.meta +8 -8
- package/Samples~/DI/Providers.meta +8 -8
- package/Samples~/DI/README.md +51 -51
- package/Samples~/DI/README.md.meta +7 -7
- package/Samples~/DI/Reflex/SampleInstaller.cs +7 -0
- package/Samples~/DI/Reflex/SampleInstaller.cs.meta +11 -11
- package/Samples~/DI/Reflex.meta +8 -8
- package/Samples~/DI/VContainer/SampleLifetimeScope.cs +6 -1
- package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +11 -11
- package/Samples~/DI/VContainer.meta +8 -8
- package/Samples~/DI/Zenject/SampleInstaller.cs +8 -0
- package/Samples~/DI/Zenject/SampleInstaller.cs.meta +11 -11
- package/Samples~/DI/Zenject.meta +8 -8
- package/Samples~/DI.meta +8 -8
- package/Samples~/Mini Combat/Boot.cs.meta +11 -11
- package/Samples~/Mini Combat/Enemy.cs.meta +11 -11
- package/Samples~/Mini Combat/Messages.cs.meta +11 -11
- package/Samples~/Mini Combat/Player.cs.meta +11 -11
- package/Samples~/Mini Combat/README.md +324 -323
- package/Samples~/Mini Combat/README.md.meta +7 -7
- package/Samples~/Mini Combat/UIOverlay.cs.meta +11 -11
- package/Samples~/Mini Combat/Walkthrough.md +430 -430
- package/Samples~/Mini Combat/Walkthrough.md.meta +7 -7
- package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef +13 -13
- package/Samples~/Mini Combat/WallstopStudios.DxMessaging.MiniCombat.Sample.asmdef.meta +7 -7
- package/Samples~/Mini Combat.meta +8 -8
- package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +11 -11
- package/Samples~/UI Buttons + Inspector/Messages.cs.meta +11 -11
- package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +11 -11
- package/Samples~/UI Buttons + Inspector/README.md +210 -209
- package/Samples~/UI Buttons + Inspector/README.md.meta +7 -7
- package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +11 -11
- package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef +13 -13
- package/Samples~/UI Buttons + Inspector/WallstopStudios.DxMessaging.UIButtons.Sample.asmdef.meta +7 -7
- package/Samples~/UI Buttons + Inspector.meta +8 -8
- package/SourceGenerators/Directory.Build.props.meta +7 -7
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +11 -11
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +2 -2
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj.meta +7 -7
- package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.meta +8 -8
- package/SourceGenerators.meta +8 -8
- package/Third Party Notices.md +3 -3
- package/Third Party Notices.md.meta +7 -7
- package/package.json +115 -92
- 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
|
+
}
|