com.wallstop-studios.dxmessaging 3.0.1 → 3.2.0

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 (148) hide show
  1. package/CHANGELOG.md +497 -2
  2. package/Editor/Analyzers/BaseCallLogMessageParser.cs.meta +3 -3
  3. package/Editor/Analyzers/BaseCallReportAggregator.cs.meta +3 -3
  4. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs +69 -62
  5. package/Editor/Analyzers/DxMessagingConsoleHarvester.cs.meta +3 -3
  6. package/Editor/AssemblyInfo.cs.meta +9 -1
  7. package/Editor/CustomEditors/MessageAwareComponentFallbackEditor.cs.meta +3 -3
  8. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs +24 -15
  9. package/Editor/CustomEditors/MessageAwareComponentInspectorOverlay.cs.meta +3 -3
  10. package/Editor/CustomEditors/MessagingComponentEditor.cs.meta +9 -1
  11. package/Editor/DxMessagingEditorIdle.cs +62 -0
  12. package/{Runtime/Core/Internal/TypedDispatchLinkIndex.cs.meta → Editor/DxMessagingEditorIdle.cs.meta} +1 -1
  13. package/Editor/DxMessagingEditorInitializer.cs +112 -15
  14. package/Editor/DxMessagingEditorInitializer.cs.meta +9 -1
  15. package/Editor/DxMessagingEditorLog.cs +32 -0
  16. package/Editor/DxMessagingEditorLog.cs.meta +11 -0
  17. package/Editor/DxMessagingMenu.cs.meta +3 -3
  18. package/Editor/DxMessagingSceneBuildProcessor.cs.meta +3 -3
  19. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs +137 -15
  20. package/Editor/Settings/DxMessagingBaseCallIgnoreSync.cs.meta +3 -3
  21. package/Editor/Settings/DxMessagingSettings.cs +92 -31
  22. package/Editor/Settings/DxMessagingSettings.cs.meta +9 -1
  23. package/Editor/Settings/DxMessagingSettingsProvider.cs.meta +9 -1
  24. package/Editor/SetupCscRsp.cs +539 -304
  25. package/Editor/SetupCscRsp.cs.meta +9 -1
  26. package/Editor/Testing/MessagingComponentEditorHarness.cs +1 -1
  27. package/Editor/Testing/MessagingComponentEditorHarness.cs.meta +9 -1
  28. package/README.md +17 -18
  29. package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
  30. package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll.meta +3 -3
  31. package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll.meta +3 -3
  32. package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll.meta +3 -3
  33. package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll.meta +3 -3
  34. package/Runtime/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  35. package/Runtime/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +46 -0
  36. package/Runtime/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  37. package/{Editor → Runtime}/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll.meta +2 -2
  38. package/Runtime/Analyzers.meta +8 -0
  39. package/Runtime/AssemblyInfo.cs.meta +9 -1
  40. package/Runtime/Core/Attributes/DxAutoConstructorAttribute.cs.meta +9 -1
  41. package/Runtime/Core/Attributes/DxBroadcastMessageAttribute.cs.meta +9 -1
  42. package/Runtime/Core/Attributes/DxIgnoreMissingBaseCallAttribute.cs.meta +3 -3
  43. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs +2 -4
  44. package/Runtime/Core/Attributes/DxOptionalParameterAttribute.cs.meta +9 -1
  45. package/Runtime/Core/Attributes/DxTargetedMessageAttribute.cs.meta +9 -1
  46. package/Runtime/Core/Attributes/DxUntargetedMessageAttribute.cs.meta +9 -1
  47. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs +56 -0
  48. package/Runtime/Core/Attributes/Il2CppSetOptionAttribute.cs.meta +11 -0
  49. package/Runtime/Core/DataStructure/CyclicBuffer.cs +44 -26
  50. package/Runtime/Core/DataStructure/CyclicBuffer.cs.meta +9 -1
  51. package/Runtime/Core/Diagnostics/MessageEmissionData.cs.meta +9 -1
  52. package/Runtime/Core/Diagnostics/MessageRegistrationData.cs.meta +9 -1
  53. package/Runtime/Core/Diagnostics/MessageRegistrationType.cs.meta +9 -1
  54. package/Runtime/Core/DxMessagingStaticState.cs.meta +3 -3
  55. package/Runtime/Core/Extensions/EnumExtensions.cs +6 -5
  56. package/Runtime/Core/Extensions/EnumExtensions.cs.meta +9 -1
  57. package/Runtime/Core/Extensions/IListExtensions.cs.meta +9 -1
  58. package/Runtime/Core/Extensions/MessageBusExtensions.cs.meta +3 -3
  59. package/Runtime/Core/Extensions/MessageExtensions.cs +0 -60
  60. package/Runtime/Core/Extensions/MessageExtensions.cs.meta +3 -3
  61. package/Runtime/Core/Helper/MessageCache.cs.meta +9 -1
  62. package/Runtime/Core/Helper/MessageHelperIndexer.cs.meta +9 -1
  63. package/Runtime/Core/IMessage.cs.meta +3 -3
  64. package/Runtime/Core/InstanceId.cs +25 -1
  65. package/Runtime/Core/InstanceId.cs.meta +3 -3
  66. package/Runtime/Core/Internal/DxUnsafe.cs +60 -0
  67. package/Runtime/Core/Internal/DxUnsafe.cs.meta +11 -0
  68. package/Runtime/Core/Internal/FlatDispatch.cs +198 -0
  69. package/Runtime/Core/Internal/FlatDispatch.cs.meta +11 -0
  70. package/Runtime/Core/Internal/TypedSlots.cs +47 -21
  71. package/Runtime/Core/MessageBus/DiagnosticsTarget.cs.meta +3 -3
  72. package/Runtime/Core/MessageBus/GlobalMessageBusProvider.cs.meta +3 -3
  73. package/Runtime/Core/MessageBus/IMessageBus.cs +75 -40
  74. package/Runtime/Core/MessageBus/IMessageBus.cs.meta +3 -3
  75. package/Runtime/Core/MessageBus/IMessageBusProvider.cs.meta +3 -3
  76. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs +1 -0
  77. package/Runtime/Core/MessageBus/IMessageRegistrationBuilder.cs.meta +3 -3
  78. package/Runtime/Core/MessageBus/Internal/BusSlots.cs +7 -6
  79. package/Runtime/Core/MessageBus/MessageBus.cs +2864 -3413
  80. package/Runtime/Core/MessageBus/MessageBus.cs.meta +3 -3
  81. package/Runtime/Core/MessageBus/MessageBusRebindMode.cs.meta +3 -3
  82. package/Runtime/Core/MessageBus/MessageBusRegistration.cs +207 -0
  83. package/Runtime/Core/MessageBus/MessageBusRegistration.cs.meta +11 -0
  84. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs +187 -14
  85. package/Runtime/Core/MessageBus/MessageRegistrationBuilder.cs.meta +3 -3
  86. package/Runtime/Core/MessageBus/MessagingRegistration.cs.meta +3 -3
  87. package/Runtime/Core/MessageBus/RegistrationLog.cs.meta +3 -3
  88. package/Runtime/Core/MessageHandler.cs +2206 -1510
  89. package/Runtime/Core/MessageHandler.cs.meta +3 -3
  90. package/Runtime/Core/MessageRegistrationHandle.cs.meta +3 -3
  91. package/Runtime/Core/MessageRegistrationToken.cs +1667 -812
  92. package/Runtime/Core/MessageRegistrationToken.cs.meta +3 -3
  93. package/Runtime/Core/Messages/GlobalStringMessage.cs.meta +9 -1
  94. package/Runtime/Core/Messages/IBroadcastMessage.cs.meta +3 -3
  95. package/Runtime/Core/Messages/ITargetedMessage.cs.meta +3 -3
  96. package/Runtime/Core/Messages/IUntargetedMessage.cs.meta +3 -3
  97. package/Runtime/Core/Messages/ReflexiveMessage.cs.meta +9 -1
  98. package/Runtime/Core/Messages/SourcedStringMessage.cs.meta +3 -3
  99. package/Runtime/Core/Messages/StringMessage.cs.meta +9 -1
  100. package/Runtime/Core/MessagingDebug.cs.meta +3 -3
  101. package/Runtime/Unity/CurrentGlobalMessageBusProvider.cs.meta +3 -3
  102. package/Runtime/Unity/DxMessagingRuntimeInitializer.cs.meta +3 -3
  103. package/Runtime/Unity/InitialGlobalMessageBusProvider.cs.meta +3 -3
  104. package/Runtime/Unity/Integrations/Reflex/AssemblyInfo.cs.meta +9 -1
  105. package/Runtime/Unity/Integrations/Reflex/ReflexRegistrationInstaller.cs.meta +3 -3
  106. package/Runtime/Unity/Integrations/VContainer/AssemblyInfo.cs.meta +9 -1
  107. package/Runtime/Unity/Integrations/VContainer/VContainerRegistrationExtensions.cs.meta +3 -3
  108. package/Runtime/Unity/Integrations/Zenject/AssemblyInfo.cs.meta +9 -1
  109. package/Runtime/Unity/Integrations/Zenject/ZenjectRegistrationInstaller.cs.meta +3 -3
  110. package/Runtime/Unity/MessageAwareComponent.cs +46 -1
  111. package/Runtime/Unity/MessageAwareComponent.cs.meta +3 -3
  112. package/Runtime/Unity/MessageBusProviderHandle.cs.meta +3 -3
  113. package/Runtime/Unity/MessagingComponent.cs +43 -10
  114. package/Runtime/Unity/MessagingComponent.cs.meta +3 -3
  115. package/Runtime/Unity/MessagingComponentInstaller.cs.meta +3 -3
  116. package/Runtime/Unity/ScriptableMessageBusProvider.cs.meta +3 -3
  117. package/Runtime/WallstopStudios.DxMessaging.asmdef +1 -1
  118. package/Samples~/DI/README.md +7 -7
  119. package/Samples~/DI/Reflex/SampleInstaller.cs.meta +3 -3
  120. package/Samples~/DI/VContainer/SampleLifetimeScope.cs.meta +3 -3
  121. package/Samples~/DI/Zenject/SampleInstaller.cs.meta +3 -3
  122. package/Samples~/Mini Combat/Boot.cs.meta +3 -3
  123. package/Samples~/Mini Combat/Enemy.cs.meta +3 -3
  124. package/Samples~/Mini Combat/Messages.cs.meta +3 -3
  125. package/Samples~/Mini Combat/Player.cs.meta +3 -3
  126. package/Samples~/Mini Combat/UIOverlay.cs.meta +3 -3
  127. package/Samples~/UI Buttons + Inspector/DiagnosticsEnabler.cs.meta +3 -3
  128. package/Samples~/UI Buttons + Inspector/Messages.cs.meta +3 -3
  129. package/Samples~/UI Buttons + Inspector/MessagingObserver.cs.meta +3 -3
  130. package/Samples~/UI Buttons + Inspector/UIButtonEmitter.cs.meta +3 -3
  131. package/SourceGenerators/Directory.Build.props +50 -3
  132. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs +96 -63
  133. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxAutoConstructorGenerator.cs.meta +3 -3
  134. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs +745 -87
  135. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/DxMessageIdGenerator.cs.meta +9 -1
  136. package/SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators/WallstopStudios.DxMessaging.SourceGenerators.csproj +40 -46
  137. package/SourceGenerators/global.json +7 -0
  138. package/SourceGenerators/global.json.meta +7 -0
  139. package/package.json +30 -40
  140. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll +0 -0
  141. package/Editor/Analyzers/WallstopStudios.DxMessaging.Analyzer.dll.meta +0 -33
  142. package/Editor/Analyzers/WallstopStudios.DxMessaging.SourceGenerators.dll +0 -0
  143. package/Runtime/Core/Internal/TypedDispatchLinkIndex.cs +0 -51
  144. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  145. /package/{Editor → Runtime}/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  146. /package/{Editor → Runtime}/Analyzers/System.Collections.Immutable.dll +0 -0
  147. /package/{Editor → Runtime}/Analyzers/System.Reflection.Metadata.dll +0 -0
  148. /package/{Editor → Runtime}/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
@@ -5,12 +5,9 @@ namespace DxMessaging.Editor
5
5
  using System;
6
6
  using System.Collections.Generic;
7
7
  using System.IO;
8
- using System.Linq;
9
- using System.Security.Cryptography;
10
8
  using DxMessaging.Editor.Settings;
11
9
  using UnityEditor;
12
10
  using UnityEngine;
13
- using Object = UnityEngine.Object;
14
11
 
15
12
  [InitializeOnLoad]
16
13
  public static class SetupCscRsp
@@ -22,279 +19,297 @@ namespace DxMessaging.Editor
22
19
  )
23
20
  .Replace("\\", "/");
24
21
 
25
- private static readonly string[] AnalyzerDirectories =
26
- {
27
- "Packages/com.wallstop-studios.dxmessaging/Editor/Analyzers/",
28
- "Library/PackageCache/com.wallstop-studios.dxmessaging/Editor/Analyzers/",
29
- };
22
+ // Older package versions copied the analyzer + Roslyn runtime DLLs into the consumer
23
+ // project here so the source generator applied project-wide. The generator now ships
24
+ // under the package's Runtime/Analyzers folder (Unity scopes it natively to the runtime
25
+ // assembly and everything that references it, including the predefined Assembly-CSharp),
26
+ // so this in-project copy is redundant and is removed on upgrade.
27
+ internal const string LegacyAnalyzerCopyFolder =
28
+ "Assets/Plugins/Editor/WallstopStudios.DxMessaging";
30
29
 
31
- private static readonly string SourceGeneratorDllName =
30
+ private const string LegacySourceGeneratorDllName =
32
31
  "WallstopStudios.DxMessaging.SourceGenerators.dll";
33
32
 
34
- // The analyzer DLL is a SEPARATE assembly compiled against Roslyn 3.8.0 because Unity 2021's
35
- // analyzer loader silently rejects analyzer DLLs built against Roslyn 4.x. The source-
36
- // generator DLL above stays at Roslyn 4.x because it uses IIncrementalGenerator (4.0+).
37
- // Both DLLs ship side-by-side and both need the RoslynAnalyzer label.
38
- private static readonly string AnalyzerDllName = "WallstopStudios.DxMessaging.Analyzer.dll";
39
-
40
- // The analyzer DLLs and shared Roslyn surface ship unconditionally; they're light enough
41
- // and required for DXMSG002–DXMSG009 to function at all. The list intentionally references
42
- // a few transitive Roslyn deps that may or may not physically ship with the package; the
43
- // copy loop below silently skips any name that isn't on disk.
44
- private static readonly string[] RequiredDllNames =
33
+ // Released 2.x legacy folders predate the companion analyzer DLL, so the source generator
34
+ // is the required package-owned marker. Unknown DLLs still make the folder unsafe.
35
+ private static readonly HashSet<string> RequiredLegacyAnalyzerCopyDlls = new(
36
+ StringComparer.OrdinalIgnoreCase
37
+ )
38
+ {
39
+ LegacySourceGeneratorDllName,
40
+ };
41
+
42
+ private static readonly HashSet<string> KnownLegacyAnalyzerCopyDlls = new(
43
+ StringComparer.OrdinalIgnoreCase
44
+ )
45
45
  {
46
- SourceGeneratorDllName,
47
- AnalyzerDllName,
46
+ LegacySourceGeneratorDllName,
47
+ "WallstopStudios.DxMessaging.Analyzer.dll",
48
48
  "Microsoft.CodeAnalysis.dll",
49
49
  "Microsoft.CodeAnalysis.CSharp.dll",
50
- "System.Text.Encodings.Web.dll",
51
- "System.Reflection.Metadata.dll",
52
- "System.Runtime.CompilerServices.Unsafe.dll",
50
+ "System.Buffers.dll",
53
51
  "System.Collections.Immutable.dll",
54
52
  "System.Memory.dll",
55
- "System.Buffers.dll",
56
- "System.Threading.Tasks.Extensions.dll",
57
53
  "System.Numerics.Vectors.dll",
54
+ "System.Reflection.Metadata.dll",
55
+ "System.Runtime.CompilerServices.Unsafe.dll",
58
56
  "System.Text.Encoding.CodePages.dll",
57
+ "System.Text.Encodings.Web.dll",
58
+ "System.Threading.Tasks.Extensions.dll",
59
59
  };
60
60
 
61
- // DLLs that must be tagged with Unity's "RoslynAnalyzer" asset label so Unity's compiler
62
- // pipeline picks them up as analyzer/source-generator hosts. Other DLLs in the same folder
63
- // (Roslyn runtime, immutable collections) are plain Editor-only plugin DLLs.
64
- private static readonly HashSet<string> AnalyzerLabeledDllNames = new(
65
- StringComparer.OrdinalIgnoreCase
66
- )
61
+ private static bool loggedSkippedLegacyAnalyzerCopyCleanup;
62
+
63
+ static SetupCscRsp()
67
64
  {
68
- SourceGeneratorDllName,
69
- AnalyzerDllName,
70
- };
65
+ // Backstop only: the primary removal happens pre-compile in LegacyAnalyzerCopyCleanup.
66
+ // This catches projects whose legacy copy predates the upgrade and triggers no import.
67
+ ScheduleSetupStep(
68
+ () => TryRemoveLegacyAnalyzerCopy(),
69
+ "remove redundant in-project analyzer copy"
70
+ );
71
+ ScheduleSetupStep(EnsureCscRsp, "clean csc.rsp analyzer entries");
72
+ ScheduleAdditionalFileForIgnoreListSync();
73
+ }
71
74
 
72
- private static readonly HashSet<string> DllNames = new(StringComparer.OrdinalIgnoreCase);
75
+ internal static void ScheduleAdditionalFileForIgnoreListSync()
76
+ {
77
+ ScheduleSetupStep(
78
+ EnsureAdditionalFileForIgnoreList,
79
+ "sync csc.rsp base-call ignore additionalfile entry"
80
+ );
81
+ }
73
82
 
74
- static SetupCscRsp()
83
+ private static void ScheduleSetupStep(Action work, string description)
75
84
  {
76
- EditorApplication.delayCall += EnsureDLLsExistInAssets;
77
- EditorApplication.delayCall += EnsureCscRsp;
78
- EditorApplication.delayCall += EnsureAdditionalFileForIgnoreList;
85
+ DxMessagingEditorIdle.ScheduleAssetDatabaseMutation(() =>
86
+ RunSetupStep(work, description)
87
+ );
79
88
  }
80
89
 
81
- private static void EnsureDLLsExistInAssets()
90
+ private static void RunSetupStep(Action work, string description)
82
91
  {
83
- DllNames.Clear();
84
- foreach (
85
- string dllGuid in AssetDatabase.FindAssets("t:DefaultAsset", new[] { "Assets" })
92
+ try
93
+ {
94
+ work();
95
+ }
96
+ catch (Exception ex)
97
+ {
98
+ DxMessagingEditorLog.LogError($"SetupCscRsp failed to {description}.", ex);
99
+ }
100
+ }
101
+
102
+ /// <summary>
103
+ /// Deletes the redundant in-project analyzer copy that older package versions deployed to
104
+ /// <see cref="LegacyAnalyzerCopyFolder"/>. The source generator now ships under the
105
+ /// package's <c>Runtime/Analyzers</c> folder and applies automatically, so the copy is no
106
+ /// longer needed -- and, critically, leaving it in place makes BOTH copies generate into
107
+ /// every DxMessaging-referencing assembly, emitting each member twice (CS0102). Removing it
108
+ /// is therefore an upgrade requirement, not just cleanup.
109
+ /// </summary>
110
+ /// <remarks>
111
+ /// Called from <see cref="LegacyAnalyzerCopyCleanup"/> (an <c>AssetPostprocessor</c>) so the
112
+ /// removal lands during the asset import that PRECEDES script compilation -- before the two
113
+ /// copies can both feed the compiler -- and again from the <c>[InitializeOnLoad]</c> static
114
+ /// constructor as a post-compile backstop. Idempotent (a no-op once the folder is gone) and
115
+ /// conservative: it leaves the folder untouched if it holds anything other than the analyzer
116
+ /// DLLs this package deployed there. Returns <c>true</c> only when it actually deleted the
117
+ /// folder.
118
+ /// </remarks>
119
+ internal static bool TryRemoveLegacyAnalyzerCopy()
120
+ {
121
+ if (!AssetDatabase.IsValidFolder(LegacyAnalyzerCopyFolder))
122
+ {
123
+ return false;
124
+ }
125
+
126
+ // The only shape this package ever created here is a flat set of analyzer / Roslyn
127
+ // DLLs plus their auto-generated .meta sidecars. Inspect the on-disk folder and bail
128
+ // out if a consumer repurposed it for anything else.
129
+ string absoluteFolder = Path.GetFullPath(
130
+ Path.Combine(Application.dataPath, "..", LegacyAnalyzerCopyFolder)
131
+ );
132
+ if (!Directory.Exists(absoluteFolder))
133
+ {
134
+ return false;
135
+ }
136
+
137
+ // A real subdirectory means a consumer repurposed this folder for their own content;
138
+ // preserve it. The package only ever wrote a flat set of analyzer DLLs here, so the
139
+ // safe-to-remove check below sees only files (a subfolder named "x.dll" can never be
140
+ // mistaken for a DLL).
141
+ string[] files = Directory.GetFiles(absoluteFolder);
142
+ if (Directory.GetDirectories(absoluteFolder).Length > 0)
143
+ {
144
+ LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(files);
145
+ return false;
146
+ }
147
+
148
+ if (!IsLegacyAnalyzerCopySafeToRemove(files))
149
+ {
150
+ LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(files);
151
+ return false;
152
+ }
153
+
154
+ if (AssetDatabase.DeleteAsset(LegacyAnalyzerCopyFolder))
155
+ {
156
+ Debug.Log(
157
+ "DxMessaging: removed the redundant in-project analyzer copy at "
158
+ + LegacyAnalyzerCopyFolder
159
+ + ". The source generator now ships under the package's Runtime/Analyzers "
160
+ + "folder and applies automatically; nothing needs to live under Assets."
161
+ );
162
+ return true;
163
+ }
164
+
165
+ return false;
166
+ }
167
+
168
+ private static void LogSkippedLegacyAnalyzerCopyCleanupIfNeeded(IEnumerable<string> files)
169
+ {
170
+ if (
171
+ loggedSkippedLegacyAnalyzerCopyCleanup
172
+ || !ContainsKnownLegacyAnalyzerCopyEntry(files)
86
173
  )
87
174
  {
88
- string dllPath = AssetDatabase.GUIDToAssetPath(dllGuid);
89
- if (!dllPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
175
+ return;
176
+ }
177
+
178
+ loggedSkippedLegacyAnalyzerCopyCleanup = true;
179
+ Debug.LogWarning(
180
+ "DxMessaging: found a legacy in-project analyzer copy at "
181
+ + LegacyAnalyzerCopyFolder
182
+ + " but did not remove it because the folder contains content outside "
183
+ + "the exact known package payload or is missing the required "
184
+ + "DxMessaging source-generator DLL. Move consumer-owned files out of that folder, "
185
+ + "then delete the stale DxMessaging analyzer DLLs manually to avoid "
186
+ + "double-loading analyzers."
187
+ );
188
+ }
189
+
190
+ /// <summary>
191
+ /// True when the legacy analyzer-copy folder contains the first-party source generator and
192
+ /// every entry is one of the exact analyzer/dependency DLLs this package deployed there or
193
+ /// a matching <c>.dll.meta</c> sidecar. A subfolder, a foreign DLL, or any other file makes
194
+ /// this false so the folder is preserved rather than deleted.
195
+ /// </summary>
196
+ internal static bool IsLegacyAnalyzerCopySafeToRemove(IEnumerable<string> folderEntries)
197
+ {
198
+ HashSet<string> presentRequiredDlls = new(StringComparer.OrdinalIgnoreCase);
199
+ HashSet<string> seenEntryNames = new(StringComparer.OrdinalIgnoreCase);
200
+ foreach (string entry in folderEntries ?? Array.Empty<string>())
201
+ {
202
+ if (string.IsNullOrEmpty(entry))
90
203
  {
91
204
  continue;
92
205
  }
93
206
 
94
- if (!dllPath.Contains("Assets/Plugins", StringComparison.OrdinalIgnoreCase))
207
+ string name = GetLegacyAnalyzerCopyEntryName(entry);
208
+ if (string.IsNullOrEmpty(name) || !seenEntryNames.Add(name))
95
209
  {
96
- string dllName = Path.GetFileName(dllPath);
97
- DllNames.Add(dllName);
210
+ return false;
98
211
  }
99
- }
100
212
 
101
- foreach (string requiredDllName in RequiredDllNames)
102
- {
103
- if (DllNames.Contains(requiredDllName))
213
+ if (name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
104
214
  {
215
+ if (!KnownLegacyAnalyzerCopyDlls.Contains(name))
216
+ {
217
+ return false;
218
+ }
219
+
220
+ if (RequiredLegacyAnalyzerCopyDlls.Contains(name))
221
+ {
222
+ presentRequiredDlls.Add(name);
223
+ }
224
+
105
225
  continue;
106
226
  }
107
227
 
108
- foreach (string relativeDirectory in AnalyzerDirectories)
228
+ if (name.EndsWith(".dll.meta", StringComparison.OrdinalIgnoreCase))
109
229
  {
110
- try
230
+ string dllName = name.Substring(0, name.Length - ".meta".Length);
231
+ if (!KnownLegacyAnalyzerCopyDlls.Contains(dllName))
111
232
  {
112
- string sourceFile = $"{relativeDirectory}{requiredDllName}";
113
- if (!File.Exists(sourceFile))
114
- {
115
- continue;
116
- }
117
-
118
- const string pluginsDirectory =
119
- "Assets/Plugins/Editor/WallstopStudios.DxMessaging/";
120
- string outputAsset = $"{pluginsDirectory}{requiredDllName}";
121
- if (!Directory.Exists(pluginsDirectory))
122
- {
123
- Directory.CreateDirectory(pluginsDirectory);
124
- AssetDatabase.Refresh();
125
- }
126
- bool needsCopy = FilesDiffer(sourceFile, outputAsset);
127
- if (needsCopy)
128
- {
129
- File.Copy(sourceFile, outputAsset, true);
130
- AssetDatabase.ImportAsset(outputAsset);
131
- }
132
-
133
- if (AnalyzerLabeledDllNames.Contains(requiredDllName))
134
- {
135
- Object loadedDll = AssetDatabase.LoadMainAssetAtPath(outputAsset);
136
- if (loadedDll != null)
137
- {
138
- string[] existingLabels = AssetDatabase.GetLabels(loadedDll);
139
- if (!existingLabels.Contains("RoslynAnalyzer"))
140
- {
141
- List<string> newLabels = existingLabels.ToList();
142
- newLabels.Add("RoslynAnalyzer");
143
- AssetDatabase.SetLabels(loadedDll, newLabels.ToArray());
144
- }
145
- }
146
- }
147
-
148
- if (AssetImporter.GetAtPath(outputAsset) is PluginImporter importer)
149
- {
150
- bool importerDirty = false;
151
-
152
- if (importer.GetCompatibleWithAnyPlatform())
153
- {
154
- importer.SetCompatibleWithAnyPlatform(false);
155
- importerDirty = true;
156
- }
157
-
158
- if (importer.GetExcludeFromAnyPlatform("Editor"))
159
- {
160
- importer.SetExcludeFromAnyPlatform("Editor", false);
161
- importerDirty = true;
162
- }
163
-
164
- if (!importer.GetExcludeFromAnyPlatform("Standalone"))
165
- {
166
- importer.SetExcludeFromAnyPlatform("Standalone", true);
167
- importerDirty = true;
168
- }
169
-
170
- if (importerDirty || needsCopy)
171
- {
172
- importer.SaveAndReimport();
173
- }
174
- }
175
-
176
- DllNames.Add(requiredDllName);
177
- break;
178
- }
179
- catch (Exception e)
180
- {
181
- Debug.LogError(
182
- $"Failed to copy {requiredDllName} to Assets, failed with {e}."
183
- );
233
+ return false;
184
234
  }
235
+
236
+ continue;
185
237
  }
186
- }
187
238
 
188
- if (DllNames.Count > 0)
189
- {
190
- AssetDatabase.Refresh();
239
+ return false;
191
240
  }
241
+
242
+ return presentRequiredDlls.Count == RequiredLegacyAnalyzerCopyDlls.Count;
192
243
  }
193
244
 
194
- private static bool FilesDiffer(string sourcePath, string destinationPath)
245
+ private static string GetLegacyAnalyzerCopyEntryName(string entry)
195
246
  {
196
- if (!File.Exists(destinationPath))
197
- {
198
- return true;
199
- }
247
+ string normalizedEntry = entry.Replace("\\", "/");
248
+ int lastSeparator = normalizedEntry.LastIndexOf('/');
249
+ return lastSeparator >= 0
250
+ ? normalizedEntry.Substring(lastSeparator + 1)
251
+ : normalizedEntry;
252
+ }
200
253
 
201
- FileInfo sourceInfo = new(sourcePath);
202
- FileInfo destinationInfo = new(destinationPath);
203
- if (sourceInfo.Length != destinationInfo.Length)
254
+ private static bool ContainsKnownLegacyAnalyzerCopyEntry(IEnumerable<string> folderEntries)
255
+ {
256
+ foreach (string entry in folderEntries ?? Array.Empty<string>())
204
257
  {
205
- return true;
258
+ if (string.IsNullOrEmpty(entry))
259
+ {
260
+ continue;
261
+ }
262
+
263
+ string name = GetLegacyAnalyzerCopyEntryName(entry);
264
+ if (KnownLegacyAnalyzerCopyDlls.Contains(name))
265
+ {
266
+ return true;
267
+ }
268
+
269
+ if (!name.EndsWith(".dll.meta", StringComparison.OrdinalIgnoreCase))
270
+ {
271
+ continue;
272
+ }
273
+
274
+ string dllName = name.Substring(0, name.Length - ".meta".Length);
275
+ if (KnownLegacyAnalyzerCopyDlls.Contains(dllName))
276
+ {
277
+ return true;
278
+ }
206
279
  }
207
280
 
208
- using FileStream sourceStream = File.OpenRead(sourcePath);
209
- using FileStream destinationStream = File.OpenRead(destinationPath);
210
- using SHA256 sha256 = SHA256.Create();
211
- byte[] sourceHash = sha256.ComputeHash(sourceStream);
212
- byte[] destinationHash = sha256.ComputeHash(destinationStream);
213
- return !sourceHash.AsSpan().SequenceEqual(destinationHash);
281
+ return false;
214
282
  }
215
283
 
216
284
  /// <summary>
217
- /// Synchronizes <c>csc.rsp</c> with the current set of analyzer arguments derived from the
218
- /// on-disk DLL roster.
285
+ /// Removes stale DxMessaging analyzer <c>-a:</c> entries from <c>csc.rsp</c>.
219
286
  /// </summary>
287
+ /// <remarks>
288
+ /// DxMessaging analyzers are activated solely through the RoslynAnalyzer-labeled
289
+ /// DLLs shipped under the package's <c>Runtime/Analyzers</c> folder, which Unity
290
+ /// scopes to the runtime assembly and every assembly that references it (including
291
+ /// the predefined Assembly-CSharp). A stray <c>-a:</c> registration here (for
292
+ /// example, one left behind by an older package version that copied analyzers into
293
+ /// the project) double-loads the source generator, and registering dependency DLLs
294
+ /// as analyzers makes Unity's compiler path fragile, so this method strips them.
295
+ /// </remarks>
220
296
  private static void EnsureCscRsp()
221
297
  {
222
298
  try
223
299
  {
224
300
  if (!File.Exists(RspFilePath))
225
301
  {
226
- File.WriteAllText(RspFilePath, string.Empty);
227
- AssetDatabase.ImportAsset("csc.rsp");
302
+ return;
228
303
  }
229
304
 
230
305
  string rspContent = File.ReadAllText(RspFilePath);
231
306
 
232
- // Get current valid analyzer arguments
233
- HashSet<string> currentAnalyzerArgs = new(
234
- GetAnalyzerArguments(),
235
- StringComparer.OrdinalIgnoreCase
307
+ string[] newLines = CleanDxMessagingAnalyzerLines(
308
+ rspContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries),
309
+ out bool foundStaleEntries
236
310
  );
237
311
 
238
- // Parse existing lines and filter out stale DxMessaging analyzer entries
239
- List<string> newLines = new();
240
- bool foundStaleEntries = false;
241
-
242
- foreach (
243
- string line in rspContent.Split(
244
- new[] { '\r', '\n' },
245
- StringSplitOptions.RemoveEmptyEntries
246
- )
247
- )
248
- {
249
- string trimmedLine = line.Trim();
250
-
251
- // Check if this is a DxMessaging analyzer line
252
- bool isDxMessagingAnalyzer =
253
- trimmedLine.StartsWith("-a:", StringComparison.OrdinalIgnoreCase)
254
- && (
255
- trimmedLine.Contains(
256
- "com.wallstop-studios.dxmessaging",
257
- StringComparison.OrdinalIgnoreCase
258
- )
259
- || trimmedLine.Contains(
260
- "WallstopStudios.DxMessaging",
261
- StringComparison.OrdinalIgnoreCase
262
- )
263
- );
264
-
265
- if (isDxMessagingAnalyzer)
266
- {
267
- // Only keep if it's in the current valid set
268
- if (currentAnalyzerArgs.Contains(trimmedLine))
269
- {
270
- newLines.Add(trimmedLine);
271
- }
272
- else
273
- {
274
- foundStaleEntries = true;
275
- }
276
- }
277
- else
278
- {
279
- // Keep all non-DxMessaging lines as-is
280
- newLines.Add(trimmedLine);
281
- }
282
- }
283
-
284
- // Add any new analyzer arguments that aren't already present
285
- bool foundNewEntries = false;
286
- foreach (string analyzerArgument in currentAnalyzerArgs)
287
- {
288
- if (!newLines.Contains(analyzerArgument, StringComparer.OrdinalIgnoreCase))
289
- {
290
- newLines.Add(analyzerArgument);
291
- foundNewEntries = true;
292
- }
293
- }
294
-
295
- bool modified = foundStaleEntries || foundNewEntries;
296
-
297
- if (modified)
312
+ if (foundStaleEntries)
298
313
  {
299
314
  // Write the cleaned up content
300
315
  string newContent = string.Join(Environment.NewLine, newLines);
@@ -309,8 +324,77 @@ namespace DxMessaging.Editor
309
324
  }
310
325
  catch (IOException ex)
311
326
  {
312
- Debug.LogError($"Failed to modify csc.rsp: {ex}");
327
+ DxMessagingEditorLog.LogError("Failed to modify csc.rsp.", ex);
328
+ }
329
+ }
330
+
331
+ internal static string[] CleanDxMessagingAnalyzerLines(
332
+ IEnumerable<string> lines,
333
+ out bool foundStaleEntries
334
+ )
335
+ {
336
+ List<string> newLines = new();
337
+ foundStaleEntries = false;
338
+
339
+ foreach (string line in lines ?? Array.Empty<string>())
340
+ {
341
+ string trimmedLine = line?.Trim();
342
+ if (string.IsNullOrEmpty(trimmedLine))
343
+ {
344
+ continue;
345
+ }
346
+
347
+ if (IsResponseFileComment(trimmedLine))
348
+ {
349
+ newLines.Add(trimmedLine);
350
+ continue;
351
+ }
352
+
353
+ List<string> retainedArguments = new();
354
+ bool removedFromLine = false;
355
+ foreach (string argument in SplitResponseFileArguments(trimmedLine))
356
+ {
357
+ if (IsDxMessagingAnalyzerArgument(argument))
358
+ {
359
+ foundStaleEntries = true;
360
+ removedFromLine = true;
361
+ continue;
362
+ }
363
+
364
+ retainedArguments.Add(argument);
365
+ }
366
+
367
+ if (removedFromLine)
368
+ {
369
+ if (retainedArguments.Count > 0)
370
+ {
371
+ newLines.Add(string.Join(" ", retainedArguments));
372
+ }
373
+ continue;
374
+ }
375
+
376
+ newLines.Add(trimmedLine);
313
377
  }
378
+
379
+ return newLines.ToArray();
380
+ }
381
+
382
+ private static bool IsDxMessagingAnalyzerArgument(string trimmedLine)
383
+ {
384
+ return (
385
+ TryGetCompilerOptionValue(trimmedLine, "a", out string _)
386
+ || TryGetCompilerOptionValue(trimmedLine, "analyzer", out string _)
387
+ )
388
+ && (
389
+ trimmedLine.Contains(
390
+ "com.wallstop-studios.dxmessaging",
391
+ StringComparison.OrdinalIgnoreCase
392
+ )
393
+ || trimmedLine.Contains(
394
+ "WallstopStudios.DxMessaging",
395
+ StringComparison.OrdinalIgnoreCase
396
+ )
397
+ );
314
398
  }
315
399
 
316
400
  /// <summary>
@@ -319,19 +403,14 @@ namespace DxMessaging.Editor
319
403
  /// entries pointing at moved or deleted sidecar paths are removed.
320
404
  /// </summary>
321
405
  /// <remarks>
322
- /// The sidecar is generated by <see cref="DxMessagingBaseCallIgnoreSync"/> only when there
323
- /// is content to write. csc happily runs without it, so this method does NOT auto-create.
406
+ /// The sidecar is generated by <see cref="DxMessagingBaseCallIgnoreSync"/>. csc happily
407
+ /// runs without it, so this method does NOT auto-create; sidecar writes schedule this sync
408
+ /// again after deferred regeneration completes.
324
409
  /// </remarks>
325
410
  private static void EnsureAdditionalFileForIgnoreList()
326
411
  {
327
412
  try
328
413
  {
329
- if (!File.Exists(RspFilePath))
330
- {
331
- File.WriteAllText(RspFilePath, string.Empty);
332
- AssetDatabase.ImportAsset("csc.rsp");
333
- }
334
-
335
414
  string sidecarRelativePath = DxMessagingBaseCallIgnoreSync.SidecarAssetPath;
336
415
  string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."))
337
416
  .Replace("\\", "/");
@@ -339,74 +418,28 @@ namespace DxMessaging.Editor
339
418
  .Replace("\\", "/");
340
419
 
341
420
  bool sidecarExists = File.Exists(sidecarAbsolutePath);
342
- string desiredLine = $"-additionalfile:\"{sidecarRelativePath}\"";
343
-
344
- string rspContent = File.ReadAllText(RspFilePath);
345
- List<string> newLines = new();
346
- bool foundDesired = false;
347
- bool foundStale = false;
348
-
349
- foreach (
350
- string line in rspContent.Split(
351
- new[] { '\r', '\n' },
352
- StringSplitOptions.RemoveEmptyEntries
353
- )
354
- )
421
+ if (!File.Exists(RspFilePath))
355
422
  {
356
- string trimmedLine = line.Trim();
357
-
358
- bool isDxMessagingAdditionalFile =
359
- trimmedLine.StartsWith(
360
- "-additionalfile:",
361
- StringComparison.OrdinalIgnoreCase
362
- )
363
- && trimmedLine.Contains("DxMessaging.", StringComparison.OrdinalIgnoreCase)
364
- && trimmedLine.Contains(
365
- "BaseCallIgnore",
366
- StringComparison.OrdinalIgnoreCase
367
- );
368
-
369
- if (isDxMessagingAdditionalFile)
423
+ if (!sidecarExists)
370
424
  {
371
- if (
372
- sidecarExists
373
- && string.Equals(
374
- trimmedLine,
375
- desiredLine,
376
- StringComparison.OrdinalIgnoreCase
377
- )
378
- )
379
- {
380
- if (!foundDesired)
381
- {
382
- newLines.Add(trimmedLine);
383
- foundDesired = true;
384
- }
385
- else
386
- {
387
- // Drop duplicate.
388
- foundStale = true;
389
- }
390
- }
391
- else
392
- {
393
- // Stale entry pointing at a moved/renamed/deleted sidecar; drop it.
394
- foundStale = true;
395
- }
425
+ return;
396
426
  }
397
- else
398
- {
399
- newLines.Add(trimmedLine);
400
- }
401
- }
402
427
 
403
- bool needsAppend = sidecarExists && !foundDesired;
404
- if (needsAppend)
405
- {
406
- newLines.Add(desiredLine);
428
+ File.WriteAllText(
429
+ RspFilePath,
430
+ FormatAdditionalFileArgument(sidecarRelativePath) + Environment.NewLine
431
+ );
432
+ AssetDatabase.ImportAsset("csc.rsp");
433
+ Debug.Log("Updated csc.rsp additionalfile entries.");
434
+ return;
407
435
  }
408
436
 
409
- bool modified = foundStale || needsAppend;
437
+ string[] newLines = SynchronizeAdditionalFileForIgnoreListLines(
438
+ File.ReadAllLines(RspFilePath),
439
+ sidecarRelativePath,
440
+ sidecarExists,
441
+ out bool modified
442
+ );
410
443
 
411
444
  if (modified)
412
445
  {
@@ -422,46 +455,248 @@ namespace DxMessaging.Editor
422
455
  }
423
456
  catch (IOException ex)
424
457
  {
425
- Debug.LogError($"Failed to update csc.rsp additionalfile entry: {ex}");
458
+ DxMessagingEditorLog.LogError("Failed to update csc.rsp additionalfile entry.", ex);
426
459
  }
427
460
  }
428
461
 
429
- private static IEnumerable<string> GetAnalyzerArguments()
462
+ internal static string[] SynchronizeAdditionalFileForIgnoreListLines(
463
+ IEnumerable<string> lines,
464
+ string sidecarRelativePath,
465
+ bool sidecarExists,
466
+ out bool modified
467
+ )
430
468
  {
431
- HashSet<string> yielded = new(StringComparer.OrdinalIgnoreCase);
432
- string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
469
+ string desiredLine = FormatAdditionalFileArgument(sidecarRelativePath);
470
+ List<string> newLines = new();
471
+ bool foundDesired = false;
472
+ bool foundStale = false;
433
473
 
434
- foreach (string directory in AnalyzerDirectories)
474
+ foreach (string line in lines ?? Array.Empty<string>())
435
475
  {
436
- foreach (string dllName in RequiredDllNames)
476
+ string trimmedLine = line?.Trim();
477
+ if (string.IsNullOrEmpty(trimmedLine))
437
478
  {
438
- string absoluteDirectory = Path.IsPathRooted(directory)
439
- ? directory
440
- : Path.GetFullPath(Path.Combine(projectRoot, directory));
479
+ continue;
480
+ }
481
+
482
+ if (IsResponseFileComment(trimmedLine))
483
+ {
484
+ newLines.Add(trimmedLine);
485
+ continue;
486
+ }
441
487
 
442
- string absoluteAnalyzerPath = Path.Combine(absoluteDirectory, dllName);
443
- if (!File.Exists(absoluteAnalyzerPath))
488
+ List<string> retainedArguments = new();
489
+ bool removedFromLine = false;
490
+ foreach (string argument in SplitResponseFileArguments(trimmedLine))
491
+ {
492
+ if (!IsDxMessagingBaseCallIgnoreAdditionalFile(argument))
444
493
  {
494
+ retainedArguments.Add(argument);
445
495
  continue;
446
496
  }
447
497
 
448
- string projectRelativePath = FileUtil.GetProjectRelativePath(
449
- absoluteAnalyzerPath
450
- );
451
- if (string.IsNullOrEmpty(projectRelativePath))
498
+ bool isDesired =
499
+ sidecarExists
500
+ && IsAdditionalFileArgumentForPath(argument, sidecarRelativePath);
501
+ if (!isDesired)
452
502
  {
503
+ // Stale entry pointing at a moved/renamed/deleted sidecar; drop it.
504
+ foundStale = true;
505
+ removedFromLine = true;
453
506
  continue;
454
507
  }
455
508
 
456
- string normalizedRelativePath = projectRelativePath.Replace("\\", "/");
457
- if (!yielded.Add(normalizedRelativePath))
509
+ if (foundDesired)
458
510
  {
511
+ // Drop duplicate.
512
+ foundStale = true;
513
+ removedFromLine = true;
459
514
  continue;
460
515
  }
461
516
 
462
- yield return $"-a:\"{normalizedRelativePath}\"";
517
+ retainedArguments.Add(desiredLine);
518
+ foundDesired = true;
519
+ if (!string.Equals(argument, desiredLine, StringComparison.Ordinal))
520
+ {
521
+ foundStale = true;
522
+ removedFromLine = true;
523
+ }
524
+ }
525
+
526
+ if (retainedArguments.Count > 0)
527
+ {
528
+ newLines.Add(string.Join(" ", retainedArguments));
529
+ continue;
530
+ }
531
+
532
+ if (!removedFromLine)
533
+ {
534
+ newLines.Add(trimmedLine);
535
+ }
536
+ }
537
+
538
+ bool needsAppend = sidecarExists && !foundDesired;
539
+ if (needsAppend)
540
+ {
541
+ newLines.Add(desiredLine);
542
+ }
543
+
544
+ modified = foundStale || needsAppend;
545
+ return newLines.ToArray();
546
+ }
547
+
548
+ private static string FormatAdditionalFileArgument(string sidecarRelativePath)
549
+ {
550
+ return $"-additionalfile:\"{sidecarRelativePath}\"";
551
+ }
552
+
553
+ private static bool IsResponseFileComment(string trimmedLine)
554
+ {
555
+ return trimmedLine.StartsWith("#", StringComparison.Ordinal);
556
+ }
557
+
558
+ private static bool IsDxMessagingBaseCallIgnoreAdditionalFile(string trimmedLine)
559
+ {
560
+ return TryGetCompilerOptionValue(trimmedLine, "additionalfile", out string value)
561
+ && value.Contains("DxMessaging.", StringComparison.OrdinalIgnoreCase)
562
+ && value.Contains("BaseCallIgnore", StringComparison.OrdinalIgnoreCase);
563
+ }
564
+
565
+ private static bool IsAdditionalFileArgumentForPath(
566
+ string argument,
567
+ string sidecarRelativePath
568
+ )
569
+ {
570
+ if (!TryGetCompilerOptionValue(argument, "additionalfile", out string value))
571
+ {
572
+ return false;
573
+ }
574
+
575
+ string unquotedValue = UnquoteWholeArgument(value.Trim());
576
+ return string.Equals(
577
+ unquotedValue.Replace("\\", "/"),
578
+ sidecarRelativePath,
579
+ StringComparison.OrdinalIgnoreCase
580
+ );
581
+ }
582
+
583
+ private static bool TryGetCompilerOptionValue(
584
+ string argument,
585
+ string optionName,
586
+ out string value
587
+ )
588
+ {
589
+ value = string.Empty;
590
+ if (string.IsNullOrEmpty(argument) || string.IsNullOrEmpty(optionName))
591
+ {
592
+ return false;
593
+ }
594
+
595
+ string unquotedArgument = UnquoteWholeArgument(argument.Trim());
596
+ if (unquotedArgument.Length <= optionName.Length + 1)
597
+ {
598
+ return false;
599
+ }
600
+
601
+ if (unquotedArgument[0] != '-' && unquotedArgument[0] != '/')
602
+ {
603
+ return false;
604
+ }
605
+
606
+ if (unquotedArgument[optionName.Length + 1] != ':')
607
+ {
608
+ return false;
609
+ }
610
+
611
+ if (
612
+ string.Compare(
613
+ unquotedArgument,
614
+ 1,
615
+ optionName,
616
+ 0,
617
+ optionName.Length,
618
+ StringComparison.OrdinalIgnoreCase
619
+ ) != 0
620
+ )
621
+ {
622
+ return false;
623
+ }
624
+
625
+ value = unquotedArgument.Substring(optionName.Length + 2);
626
+ return true;
627
+ }
628
+
629
+ private static string UnquoteWholeArgument(string argument)
630
+ {
631
+ if (argument.Length >= 2 && argument[0] == '"' && argument[argument.Length - 1] == '"')
632
+ {
633
+ return argument.Substring(1, argument.Length - 2).Replace("\"\"", "\"");
634
+ }
635
+
636
+ return argument;
637
+ }
638
+
639
+ private static List<string> SplitResponseFileArguments(string line)
640
+ {
641
+ List<string> arguments = new();
642
+ if (string.IsNullOrWhiteSpace(line))
643
+ {
644
+ return arguments;
645
+ }
646
+
647
+ System.Text.StringBuilder current = new();
648
+ bool inQuotes = false;
649
+ foreach (char character in line)
650
+ {
651
+ if (character == '"')
652
+ {
653
+ inQuotes = !inQuotes;
654
+ current.Append(character);
655
+ continue;
463
656
  }
657
+
658
+ if (char.IsWhiteSpace(character) && !inQuotes)
659
+ {
660
+ if (current.Length > 0)
661
+ {
662
+ arguments.Add(current.ToString());
663
+ current.Clear();
664
+ }
665
+ continue;
666
+ }
667
+
668
+ current.Append(character);
669
+ }
670
+
671
+ if (current.Length > 0)
672
+ {
673
+ arguments.Add(current.ToString());
464
674
  }
675
+
676
+ return arguments;
677
+ }
678
+ }
679
+
680
+ /// <summary>
681
+ /// Removes the legacy in-project analyzer copy (via
682
+ /// <see cref="SetupCscRsp.TryRemoveLegacyAnalyzerCopy"/>) during the asset import that PRECEDES
683
+ /// script compilation. This is the primary upgrade path: deleting the redundant copy before the
684
+ /// compiler runs prevents it and the package's Runtime/Analyzers copy from both generating into
685
+ /// the same assembly (which would emit each member twice -- CS0102). It is a cheap no-op in the
686
+ /// steady state: once the folder is gone, <c>AssetDatabase.IsValidFolder</c> short-circuits on
687
+ /// every subsequent import.
688
+ /// </summary>
689
+ internal sealed class LegacyAnalyzerCopyCleanup : AssetPostprocessor
690
+ {
691
+ // Classic four-argument signature so this compiles on every supported Unity (2021.3+).
692
+ private static void OnPostprocessAllAssets(
693
+ string[] importedAssets,
694
+ string[] deletedAssets,
695
+ string[] movedAssets,
696
+ string[] movedFromAssetPaths
697
+ )
698
+ {
699
+ SetupCscRsp.TryRemoveLegacyAnalyzerCopy();
465
700
  }
466
701
  }
467
702
  }