diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs index c3f7249bda..a91a4ab8cf 100644 --- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -55,6 +55,14 @@ public sealed class StaticFacadeStateGuardTests "Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs" }; + private static readonly string[] AsyncCancellationTokenGuardPaths = new string[] + { + "Packages/src/Editor/Infrastructure/SkillSetup/ToolSkillSynchronizer.cs", + "Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs", + "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs", + "Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs" + }; + private static readonly Regex DirectMutableStaticFieldPattern = new Regex( @"\b(private|internal|public|protected)\s+static\s+(?!readonly\b)(?!event\b)(?!extern\b)[^(\r\n;=]*[;=]", RegexOptions.Compiled); @@ -79,6 +87,14 @@ public sealed class StaticFacadeStateGuardTests @"\b(ServiceValue|RepositoryValue|RegistryValue|RegisteredUseCase)\b", RegexOptions.Compiled); + private static readonly Regex AsyncMethodSignaturePattern = new Regex( + @"\b(?:(?:private|internal|public|protected)\s+)?(?:static\s+)?async\s+(?:void|Task(?:<[^()\n]+>)?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^()\n]+>)?\s*\(([^)]*)\)", + RegexOptions.Compiled | RegexOptions.Singleline); + + private static readonly Regex CancellationTokenCtParameterPattern = new Regex( + @"\bCancellationToken\s+ct\b", + RegexOptions.Compiled); + [Test] public void MigratedFacadeFiles_WhenScanned_DoNotOwnMutableStaticState() { @@ -115,6 +131,35 @@ public void InstanceServices_WhenScanned_AreNotStaticClasses() Assert.That(violations, Is.Empty, string.Join("\n", violations)); } + [Test] + public void RefactorTargets_WhenDeclaringAsyncMethods_RequireCancellationTokenCt() + { + // Tests that R2-5 refactor targets do not add async methods without the standard ct parameter. + List violations = FindAsyncMethodsWithoutCancellationTokenCt(); + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + + [Test] + public void CancellationTokenCtParameterPattern_WhenNameOnlyStartsWithCt_DoesNotMatch() + { + // Tests that the async guard requires the exact ct parameter name. + Assert.That(CancellationTokenCtParameterPattern.IsMatch("CancellationToken ct"), Is.True); + Assert.That(CancellationTokenCtParameterPattern.IsMatch("CancellationToken cts"), Is.False); + } + + [Test] + public void AsyncMethodSignaturePattern_WhenVisibilityIsImplicitAndReturnTypeIsNestedGeneric_Matches() + { + // Tests that the async guard does not skip implicit-private methods or nested generic Task returns. + string source = "async Task> LoadAsync(CancellationToken ct) => new List();"; + Match match = AsyncMethodSignaturePattern.Match(source); + + Assert.That(match.Success, Is.True); + Assert.That(match.Groups[1].Value, Is.EqualTo("LoadAsync")); + Assert.That(match.Groups[2].Value, Is.EqualTo("CancellationToken ct")); + } + [Test] public void MutableStaticFieldLine_WhenInitializerUsesTargetTypedNew_IsReported() { @@ -344,6 +389,48 @@ private static List FindDirectStaticEventViolations() return violations; } + private static List FindAsyncMethodsWithoutCancellationTokenCt() + { + List violations = new(); + string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + + for (int pathIndex = 0; pathIndex < AsyncCancellationTokenGuardPaths.Length; pathIndex++) + { + string relativePath = AsyncCancellationTokenGuardPaths[pathIndex]; + string absolutePath = Path.Combine(projectRoot, relativePath); + string source = File.ReadAllText(absolutePath); + MatchCollection matches = AsyncMethodSignaturePattern.Matches(source); + foreach (Match match in matches) + { + string parameterList = match.Groups[2].Value; + if (CancellationTokenCtParameterPattern.IsMatch(parameterList)) + { + continue; + } + + string methodName = match.Groups[1].Value; + int lineNumber = CountLinesBefore(source, match.Index) + 1; + violations.Add($"{relativePath}:{lineNumber}: {methodName}"); + } + } + + return violations; + } + + private static int CountLinesBefore(string source, int index) + { + int lineCount = 0; + for (int charIndex = 0; charIndex < index; charIndex++) + { + if (source[charIndex] == '\n') + { + lineCount++; + } + } + + return lineCount; + } + private static List FindStaticClassViolations(string[] relativePaths) { List violations = new(); diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs index ef489c8e7b..000c2b1f41 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs @@ -211,7 +211,7 @@ private async Task ExecutePress( else if (pressWasApplied) { InputSimulationWaitOutcome releaseOutcome = - await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false); + await ReleaseKeyStateIfPossible(keyboard, key, CancellationToken.None).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { waitOutcome = InputSimulationWaitOutcome.TimedOut; @@ -310,7 +310,7 @@ private async Task ExecuteKeyDown(Keyboard keyboard, K else if (keyDownApplied && !committed) { InputSimulationWaitOutcome rollbackOutcome = - await RollbackHeldKey(keyboard, key, keyName).ConfigureAwait(false); + await RollbackHeldKey(keyboard, key, keyName, CancellationToken.None).ConfigureAwait(false); if (rollbackOutcome == InputSimulationWaitOutcome.TimedOut) { waitOutcome = InputSimulationWaitOutcome.TimedOut; @@ -357,7 +357,7 @@ private async Task ExecuteKeyUp(Keyboard keyboard, Key } InputSimulationWaitOutcome releaseOutcome = - await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false); + await ReleaseKeyStateIfPossible(keyboard, key, CancellationToken.None).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { @@ -499,26 +499,33 @@ await EditorFrameWaiter.WaitFramesOrTimeoutAsync( CancellationToken.None).ConfigureAwait(false); } - private static async Task RollbackHeldKey(Keyboard keyboard, Key key, string keyName) + private static async Task RollbackHeldKey( + Keyboard keyboard, + Key key, + string keyName, + CancellationToken ct) { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); InputSimulationWaitOutcome releaseOutcome = - await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false); + await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { ScheduleTimedOutHeldKeyCleanup(keyboard, key, keyName, false); return releaseOutcome; } - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); KeyboardKeyState.SetKeyUp(key); SimulateKeyboardOverlayState.RemoveHeldKey(keyName); return releaseOutcome; } - private static async Task ReleaseKeyStateIfPossible(Keyboard keyboard, Key key) + private static async Task ReleaseKeyStateIfPossible( + Keyboard keyboard, + Key key, + CancellationToken ct) { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); if (!CanInjectKeyboardState(keyboard)) { return InputSimulationWaitOutcome.Completed; @@ -532,7 +539,7 @@ private static async Task ReleaseKeyStateIfPossible( InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( () => KeyboardKeyState.SetKeyState(keyboard, key, false), - CancellationToken.None).ConfigureAwait(false); + ct).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { ScheduleReleaseKeyStateImmediately(keyboard, key); @@ -581,7 +588,7 @@ private static async Task CleanupTimedOutPressAsync( await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); if (pressWasApplied) { - await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false); + await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false); } KeyboardKeyState.UnregisterTransientKey(key); @@ -603,7 +610,7 @@ private static async Task CleanupTimedOutHeldKeyAsync( await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); if (keyWasApplied) { - await ReleaseKeyStateIfPossible(keyboard, key).ConfigureAwait(false); + await ReleaseKeyStateIfPossible(keyboard, key, ct).ConfigureAwait(false); } KeyboardKeyState.SetKeyUp(key); diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs index 98b6dc7577..5b04b2e6fe 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs @@ -212,7 +212,7 @@ private async Task ExecuteClick( else if (pressWasApplied) { InputSimulationWaitOutcome releaseOutcome = - await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false); + await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { waitOutcome = InputSimulationWaitOutcome.TimedOut; @@ -325,7 +325,7 @@ private async Task ExecuteLongPress( else if (pressWasApplied) { InputSimulationWaitOutcome releaseOutcome = - await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false); + await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { waitOutcome = InputSimulationWaitOutcome.TimedOut; @@ -635,9 +635,12 @@ private static List CollectPausePointHits() return hits; } - private static async Task ReleaseButtonIfPossible(Mouse mouse, RuntimeMouseButton button) + private static async Task ReleaseButtonIfPossible( + Mouse mouse, + RuntimeMouseButton button, + CancellationToken ct) { - await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(CancellationToken.None); + await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); if (!CanInjectMouseState(mouse)) { return InputSimulationWaitOutcome.Completed; @@ -651,7 +654,7 @@ private static async Task ReleaseButtonIfPossible(Mo InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate( () => MouseInputState.SetButtonState(mouse, button, false), - CancellationToken.None).ConfigureAwait(false); + ct).ConfigureAwait(false); if (releaseOutcome == InputSimulationWaitOutcome.TimedOut) { ScheduleReleaseButtonImmediately(mouse, button); @@ -714,7 +717,7 @@ private static async Task CleanupTimedOutButtonAsync( await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct); if (pressWasApplied) { - await ReleaseButtonIfPossible(mouse, button).ConfigureAwait(false); + await ReleaseButtonIfPossible(mouse, button, ct).ConfigureAwait(false); } MouseInputState.SetButtonUp(button); diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs index b83af6ff99..bbbaea3f7b 100644 --- a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs @@ -431,9 +431,17 @@ private void RefreshSkillsSection() ScheduleResizeToContent(); } - private async void RefreshUI(bool refreshSkillsSection = true) + private void RefreshUI(bool refreshSkillsSection = true) + { + RefreshUIAsync(refreshSkillsSection, CancellationToken.None).Forget(); + } + + private async Task RefreshUIAsync( + bool refreshSkillsSection, + CancellationToken ct) { CancelSkillInstallStateRefresh(); + ct.ThrowIfCancellationRequested(); RefreshAutoShowToggle(); ViewDataBinder.SetVisible(_nodejsWarning, false); ViewDataBinder.SetVisible(_nodejsOk, false); @@ -446,17 +454,18 @@ private async void RefreshUI(bool refreshSkillsSection = true) } await Task.Yield(); + ct.ThrowIfCancellationRequested(); ViewDataBinder.SetVisible(_nodejsWarning, false); ViewDataBinder.SetVisible(_nodejsOk, false); - await _cliSetupApplicationService.ForceRefreshCliVersionAsync(CancellationToken.None); + await _cliSetupApplicationService.ForceRefreshCliVersionAsync(ct); string cliVersion = _cliSetupApplicationService.GetCachedCliVersion(); bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); string requiredCliVersion = GetMinimumRequiredCliVersion(); bool cliInstalled = IsCliInstalled(cliVersion); - _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(CancellationToken.None); + _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct); _cliStepPresenter.Update( cliInstalled, @@ -500,10 +509,10 @@ private void BeginRefreshDisplayedSkillTargets(bool canManageSkills) CancellationTokenSource cts = new(); _skillInstallStateRefreshCts = cts; - RefreshDisplayedSkillTargetsAsync(cts.Token); + RefreshDisplayedSkillTargetsAsync(cts.Token).Forget(); } - private async void RefreshDisplayedSkillTargetsAsync(CancellationToken ct) + private async Task RefreshDisplayedSkillTargetsAsync(CancellationToken ct) { string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); List targets = @@ -608,9 +617,14 @@ private void UpdateSkillsStep( _isInstallingSkills); } - private async void HandleInstallCli() + private void HandleInstallCli() + { + HandleInstallCliAsync(CancellationToken.None).Forget(); + } + + private async Task HandleInstallCliAsync(CancellationToken ct) { - await RefreshCliPrimaryActionStateAsync(CancellationToken.None); + await RefreshCliPrimaryActionStateAsync(ct); string cliVersion = _cliSetupApplicationService.GetCachedCliVersion(); bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher(); @@ -619,7 +633,7 @@ private async void HandleInstallCli() cliIsDispatcher); if (ShouldRepairCliPathFromPrimaryButton(_needsCliPathSetup, state.NeedsUpdate)) { - await HandleRepairCliPathSetup(); + await HandleRepairCliPathSetup(ct); return; } @@ -638,7 +652,7 @@ private async void HandleInstallCli() { CliInstallResult result = await _cliSetupApplicationService.InstallGlobalCliAsync( UnityEngine.Application.platform, - CancellationToken.None); + ct); if (!result.Success) { @@ -656,8 +670,8 @@ private async void HandleInstallCli() await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync( UnityEngine.Application.platform, _cliSetupApplicationService, - CancellationToken.None); - _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(CancellationToken.None); + ct); + _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct); } finally { @@ -704,7 +718,7 @@ internal static bool ShouldRepairCliPathFromPrimaryButton( return CliSetupPrimaryActionPolicy.ShouldRepairCliPath(needsCliPathSetup, needsUpdate); } - private async Task HandleRepairCliPathSetup() + private async Task HandleRepairCliPathSetup(CancellationToken ct) { _isInstallingCli = true; _cliStepPresenter.Update( @@ -720,8 +734,8 @@ private async Task HandleRepairCliPathSetup() await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync( UnityEngine.Application.platform, _cliSetupApplicationService, - CancellationToken.None); - _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(CancellationToken.None); + ct); + _needsCliPathSetup = await ShouldRepairCliPathSetupAsync(ct); } finally { @@ -730,7 +744,12 @@ await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync( } } - private async void HandleInstallSkills() + private void HandleInstallSkills() + { + HandleInstallSkillsAsync(CancellationToken.None).Forget(); + } + + private async Task HandleInstallSkillsAsync(CancellationToken ct) { CancelSkillInstallStateRefresh(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); @@ -752,7 +771,7 @@ private async void HandleInstallSkills() await _skillSetupUseCase.InstallSkillFilesAsync( installableTargets, !_installSkillsFlat, - CancellationToken.None); + ct); if (shouldShowSkillsInstalledDialog) { EditorDialogHelper.ShowSkillsInstalledDialog();