diff --git a/.github/workflows/code-complexity.yml b/.github/workflows/code-complexity.yml index e83ac0aa94..5cbd39e0a6 100644 --- a/.github/workflows/code-complexity.yml +++ b/.github/workflows/code-complexity.yml @@ -57,7 +57,7 @@ jobs: (cd cli && golangci-lint run --config .golangci-complexity.yml --issues-exit-code=0 --show-stats=false --output.json.path ../artifacts/go-code-complexity.json ./...) dotnet run --project tools/UnityCliLoop.CodeComplexity/UnityCliLoop.CodeComplexity.csproj -- \ --root . \ - --max-complexity 25 \ + --max-complexity 15 \ --format json \ --fail-on-exceeded false > artifacts/csharp-code-complexity.json diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs index 0ce26f7a24..431ba911cc 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs @@ -47,5 +47,17 @@ public void HasTopLevelReturn_WhenInterpolationHoleContainsNestedStringLiteral_S Assert.IsTrue(hasReturn); } + + [Test] + public void Analyze_WhenAttributedTypeHasAccessModifier_ShouldDetectTypeDeclaration() + { + // Verifies attributed public types are not mistaken for top-level statements. + string source = "[System.Serializable] public sealed class Example {}"; + + SourceShapeResult result = SourceShaper.Analyze(source); + + Assert.IsTrue(result.HasTypeDeclaration); + Assert.IsFalse(result.HasTopLevelStatements); + } } } diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index d76c52e550..49fd5afb34 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -3855,6 +3855,51 @@ public async Task HasMigrationTargetsAsync_WhenCurrentFirstPartyToolsAliasOnlyNe } } + [Test] + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesLegacyGlobalAliasForScreenshotCapture_ReturnsTrue() + { + // Verifies that startup detection carries legacy global aliases into screenshot fast scans. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using Old = io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + Path.Combine(toolDirectory, "ScreenshotTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await Old.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + [Test] public void ApplyMigration_WhenCurrentDomainLocalAliasUsesMovedContractTypes_AddsToolContractsReference() { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 2836e0f906..6e3e7da2c1 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -289,6 +289,23 @@ public void MigrateCSharpSource_WhenLegacyToolDescriptionInterpolatesCommaExpres Assert.That(result.Content, Does.Not.Contain("string.Join")); } + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeCommentsContainCommas_PreservesSupportedArguments() + { + // Verifies that commas inside comments do not split legacy attribute arguments. + string source = "using io.github.hatayama.uLoopMCP;\n" + + "[McpTool(Description = \"hello\" /*, block */\n" + + " //, line\n" + + " , DisplayDevelopmentOnly = true)] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("[UnityCliLoopTool(DisplayDevelopmentOnly = true)]")); + Assert.That(result.Content, Does.Not.Contain("Description")); + } + [Test] public void FindRegularInterpolatedStringEndIndex_WhenHoleContainsInterpolatedRawStringWithRawStringHole_FindsOuterStringEnd() { diff --git a/Assets/Tests/PlayMode/SimulateMouseUiTests.cs b/Assets/Tests/PlayMode/SimulateMouseUiTests.cs index e62dccbca6..dc41d1cc11 100644 --- a/Assets/Tests/PlayMode/SimulateMouseUiTests.cs +++ b/Assets/Tests/PlayMode/SimulateMouseUiTests.cs @@ -122,6 +122,30 @@ public IEnumerator Click_WithBypassRaycast_Should_ClickTargetBehindBlocker() Assert.AreEqual("ClickTarget", lastResponse.HitGameObjectName); } + [UnityTest] + public IEnumerator Click_WithBypassRaycastAndClickOnlyTarget_Should_FirePointerUpAndClick() + { + // Verifies click-only targets still receive pointer up before pointer click. + ClickOnlyTracker tracker = CreateClickOnlyElement("ClickOnlyTarget", Vector2.zero, new Vector2(200, 100)); + yield return null; + + Vector2 screenPos = GetScreenPosition(tracker.gameObject); + + yield return RunTool(new JObject + { + ["action"] = MouseAction.Click.ToString(), + ["x"] = screenPos.x, + ["y"] = screenPos.y, + ["bypassRaycast"] = true, + ["targetPath"] = "TestCanvas/ClickOnlyTarget" + }); + + Assert.IsTrue(lastResponse.Success); + Assert.IsTrue(tracker.PointerUpCalled, "PointerUp should be fired"); + Assert.IsTrue(tracker.PointerClickCalled, "PointerClick should be fired"); + Assert.AreEqual("ClickOnlyTarget", lastResponse.HitGameObjectName); + } + // Verifies clipped overlay UI wins over a non-GraphicRaycaster hit behind it. [UnityTest] public IEnumerator Click_Should_PreferClippedOverlayUiOverNonUiRaycastHit() @@ -937,6 +961,13 @@ private ClickTracker CreateClickableElement(string name, Vector2 anchoredPositio return go.AddComponent(); } + private ClickOnlyTracker CreateClickOnlyElement(string name, Vector2 anchoredPosition, Vector2 sizeDelta) + { + GameObject go = CreateUIElement(name, anchoredPosition, sizeDelta); + go.AddComponent(); + return go.AddComponent(); + } + private ClickTracker CreateChildClickableElement(string name, Transform parent, Vector2 anchoredPosition, Vector2 sizeDelta) { GameObject go = CreateChildUIElement(name, parent, anchoredPosition, sizeDelta); @@ -1106,6 +1137,18 @@ public class ClickTracker : MonoBehaviour, IPointerDownHandler, IPointerUpHandle public void OnPointerClick(PointerEventData eventData) { PointerClickCalled = true; } } + /// + /// Test support type that exposes click targets without pointer-down handlers. + /// + public class ClickOnlyTracker : MonoBehaviour, IPointerUpHandler, IPointerClickHandler + { + public bool PointerUpCalled { get; private set; } + public bool PointerClickCalled { get; private set; } + + public void OnPointerUp(PointerEventData eventData) { PointerUpCalled = true; } + public void OnPointerClick(PointerEventData eventData) { PointerClickCalled = true; } + } + // Tracks drag events and moves the element for testing /// /// Test support type used by editor and play mode fixtures. diff --git a/Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputReplayer.cs b/Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputReplayer.cs index bc55b20be4..488288acf7 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputReplayer.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputReplayer.cs @@ -456,17 +456,28 @@ private static Dictionary BuildKeyLookup() private void ApplyUiEvents() { - if (!_replayMousePosition.HasValue) + UiReplayFrame? replayFrame = CreateUiReplayFrame(); + if (!replayFrame.HasValue) { RestoreUiInputModules(); return; } + ApplyUiPointerActivity(replayFrame.Value); + ApplyUiPointerRelease(replayFrame.Value); + } + + private UiReplayFrame? CreateUiReplayFrame() + { + if (!_replayMousePosition.HasValue) + { + return null; + } + EventSystem? eventSystem = EventSystem.current; if (eventSystem == null) { - RestoreUiInputModules(); - return; + return null; } Vector2 screenPos = _replayMousePosition.Value; @@ -483,52 +494,98 @@ private void ApplyUiEvents() Vector2 gameViewSize = Handles.GetMainGameViewSize(); Vector2 inputPos = new(screenPos.x, gameViewSize.y - screenPos.y); - if (justPressed) + return new UiReplayFrame( + eventSystem, + screenPos, + inputPos, + gameViewSize, + leftHeld, + justPressed, + justReleased, + mouseMoved); + } + + private void ApplyUiPointerActivity(UiReplayFrame replayFrame) + { + if (replayFrame.JustPressed) { _suppressIdleUiOverlay = false; _pressTime = Time.realtimeSinceStartup; - OnUiPointerDown(screenPos, eventSystem); + OnUiPointerDown(replayFrame.ScreenPosition, replayFrame.EventSystem); SimulateMouseUiOverlayState.Update( - MouseAction.Click, inputPos, null, _currentPressTarget?.name, gameViewSize); + MouseAction.Click, + replayFrame.InputPosition, + null, + _currentPressTarget?.name, + replayFrame.GameViewSize); SimulateMouseUiOverlayState.RequestExpandAnimation(); + return; } - else if (leftHeld && (_currentPressTarget != null || _currentDragTarget != null)) - { - OnUiDrag(screenPos); - if (_isDragging) - { - Vector2 pressInputPos = new(_pressScreenPosition.x, gameViewSize.y - _pressScreenPosition.y); - SimulateMouseUiOverlayState.Update( - MouseAction.Drag, inputPos, pressInputPos, null, gameViewSize); - } - else - { - float elapsed = Time.realtimeSinceStartup - _pressTime; - if (elapsed >= 0.5f) - { - SimulateMouseUiOverlayState.Update( - MouseAction.LongPress, inputPos, null, _currentPressTarget?.name, gameViewSize); - SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed); - } - } + if (replayFrame.LeftHeld && (_currentPressTarget != null || _currentDragTarget != null)) + { + ApplyUiPointerHold(replayFrame); + return; } - else if (!_suppressIdleUiOverlay || mouseMoved) + + if (!_suppressIdleUiOverlay || replayFrame.MouseMoved) { // Keeping the overlay hidden until the pointer actually moves prevents release fade-out // from being cancelled by the next idle frame at the same position. _suppressIdleUiOverlay = false; SimulateMouseUiOverlayState.Update( - MouseAction.Click, inputPos, null, null, gameViewSize); + MouseAction.Click, + replayFrame.InputPosition, + null, + null, + replayFrame.GameViewSize); } + } - if (justReleased) + private void ApplyUiPointerHold(UiReplayFrame replayFrame) + { + OnUiDrag(replayFrame.ScreenPosition); + + if (_isDragging) { - OnUiPointerUp(screenPos, eventSystem); - _suppressIdleUiOverlay = true; - SimulateMouseUiOverlayState.RequestDissipateAnimation(); - SimulateMouseUiOverlayState.Clear(); + Vector2 pressInputPos = new( + _pressScreenPosition.x, + replayFrame.GameViewSize.y - _pressScreenPosition.y); + SimulateMouseUiOverlayState.Update( + MouseAction.Drag, + replayFrame.InputPosition, + pressInputPos, + null, + replayFrame.GameViewSize); + return; + } + + float elapsed = Time.realtimeSinceStartup - _pressTime; + if (elapsed < 0.5f) + { + return; } + + SimulateMouseUiOverlayState.Update( + MouseAction.LongPress, + replayFrame.InputPosition, + null, + _currentPressTarget?.name, + replayFrame.GameViewSize); + SimulateMouseUiOverlayState.UpdateLongPressElapsed(elapsed); + } + + private void ApplyUiPointerRelease(UiReplayFrame replayFrame) + { + if (!replayFrame.JustReleased) + { + return; + } + + OnUiPointerUp(replayFrame.ScreenPosition, replayFrame.EventSystem); + _suppressIdleUiOverlay = true; + SimulateMouseUiOverlayState.RequestDissipateAnimation(); + SimulateMouseUiOverlayState.Clear(); } private void OnUiPointerDown(Vector2 screenPos, EventSystem eventSystem) @@ -733,6 +790,38 @@ private void OnPlayModeStateChanged(PlayModeStateChange state) StopReplay(); } } + + private readonly struct UiReplayFrame + { + public UiReplayFrame( + EventSystem eventSystem, + Vector2 screenPosition, + Vector2 inputPosition, + Vector2 gameViewSize, + bool leftHeld, + bool justPressed, + bool justReleased, + bool mouseMoved) + { + EventSystem = eventSystem; + ScreenPosition = screenPosition; + InputPosition = inputPosition; + GameViewSize = gameViewSize; + LeftHeld = leftHeld; + JustPressed = justPressed; + JustReleased = justReleased; + MouseMoved = mouseMoved; + } + + public EventSystem EventSystem { get; } + public Vector2 ScreenPosition { get; } + public Vector2 InputPosition { get; } + public Vector2 GameViewSize { get; } + public bool LeftHeld { get; } + public bool JustPressed { get; } + public bool JustReleased { get; } + public bool MouseMoved { get; } + } } /// diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs index bd28527447..1a8596f388 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs @@ -36,85 +36,123 @@ public Task ExecuteAsync(ControlPlayModeSchema paramete if (parameters.StatusOnly) { - if (parameters.Action == PlayModeAction.Play && - !EditorApplication.isPlaying && - _compilationFailureGate.HasScriptCompilationFailed()) - { - ControlPlayModeCompileError[] compileErrors = - _compilationFailureProvider.GetLastFailedErrors(); - return Task.FromResult(CreateCompileErrorBlockedResponse(compileErrors)); - } - - return Task.FromResult(CreateResponse("Play mode status", false, false)); + return Task.FromResult(CreateStatusOnlyResponse(parameters)); } + ControlPlayModeActionResult actionResult = ExecuteRequestedPlayModeAction(parameters.Action); + if (actionResult.HasResponse) + { + return Task.FromResult(actionResult.Response); + } + + return Task.FromResult(CreateResponse( + actionResult.Message, + actionResult.Changed, + actionResult.WasAlreadyStopped)); + } + + private ControlPlayModeResponse CreateStatusOnlyResponse(ControlPlayModeSchema parameters) + { + if (ShouldBlockPlayForCompileErrors(parameters.Action, EditorApplication.isPlaying)) + { + ControlPlayModeCompileError[] compileErrors = + _compilationFailureProvider.GetLastFailedErrors(); + return CreateCompileErrorBlockedResponse(compileErrors); + } + + return CreateResponse("Play mode status", false, false); + } + + private ControlPlayModeActionResult ExecuteRequestedPlayModeAction(PlayModeAction action) + { string message; bool wasPaused = EditorApplication.isPaused; bool wasPlaying = EditorApplication.isPlaying; - bool changed = false; - bool wasAlreadyStopped = false; - switch (parameters.Action) + switch (action) { case PlayModeAction.Play: - if (!wasPlaying && _compilationFailureGate.HasScriptCompilationFailed()) - { - ControlPlayModeCompileError[] compileErrors = - _compilationFailureProvider.GetLastFailedErrors(); - return Task.FromResult(CreateCompileErrorBlockedResponse(compileErrors)); - } - - if (wasPaused) - { - EditorApplication.isPaused = false; - } - if (!EditorApplication.isPlaying) - { - EditorApplication.isPlaying = true; - } - changed = wasPaused || !wasPlaying; - message = wasPaused ? "Play mode resumed" : "Play mode started"; - break; + return ExecutePlayModeStart(wasPaused, wasPlaying); case PlayModeAction.Stop: - wasAlreadyStopped = !wasPlaying; - if (wasPaused) - { - EditorApplication.isPaused = false; - } - if (EditorApplication.isPlaying) - { - EditorApplication.isPlaying = false; - } - changed = wasPaused || wasPlaying; - message = wasAlreadyStopped ? "Play mode was already stopped" : "Play mode stopped"; - break; + return ExecutePlayModeStop(wasPaused, wasPlaying); case PlayModeAction.Pause: EditorApplication.isPaused = true; - changed = !wasPaused; - message = "Play mode paused"; - break; + return ControlPlayModeActionResult.FromState("Play mode paused", !wasPaused, false); case PlayModeAction.Step: - // Same API as the Editor's Next Frame button: advances one frame and - // leaves the player paused, independent of Time.timeScale. - if (!wasPlaying) - { - message = "Play mode is not running. Step requires PlayMode; start it with --action Play first."; - break; - } - EditorApplication.Step(); - changed = true; - message = "Stepped one frame; play mode is paused."; - break; + return ExecutePlayModeStep(wasPlaying); default: - message = $"Unknown action: {parameters.Action}"; - break; + message = $"Unknown action: {action}"; + return ControlPlayModeActionResult.FromState(message, false, false); + } + } + + private bool ShouldBlockPlayForCompileErrors(PlayModeAction action, bool isPlaying) + { + return action == PlayModeAction.Play && + !isPlaying && + _compilationFailureGate.HasScriptCompilationFailed(); + } + + private ControlPlayModeActionResult ExecutePlayModeStart(bool wasPaused, bool wasPlaying) + { + if (ShouldBlockPlayForCompileErrors(PlayModeAction.Play, wasPlaying)) + { + ControlPlayModeCompileError[] compileErrors = + _compilationFailureProvider.GetLastFailedErrors(); + return ControlPlayModeActionResult.FromResponse( + CreateCompileErrorBlockedResponse(compileErrors), + true); } - return Task.FromResult(CreateResponse(message, changed, wasAlreadyStopped)); + if (wasPaused) + { + EditorApplication.isPaused = false; + } + if (!EditorApplication.isPlaying) + { + EditorApplication.isPlaying = true; + } + + bool changed = wasPaused || !wasPlaying; + string message = wasPaused ? "Play mode resumed" : "Play mode started"; + return ControlPlayModeActionResult.FromState(message, changed, false); + } + + private static ControlPlayModeActionResult ExecutePlayModeStop(bool wasPaused, bool wasPlaying) + { + bool wasAlreadyStopped = !wasPlaying; + if (wasPaused) + { + EditorApplication.isPaused = false; + } + if (EditorApplication.isPlaying) + { + EditorApplication.isPlaying = false; + } + + bool changed = wasPaused || wasPlaying; + string message = wasAlreadyStopped ? "Play mode was already stopped" : "Play mode stopped"; + return ControlPlayModeActionResult.FromState(message, changed, wasAlreadyStopped); + } + + private static ControlPlayModeActionResult ExecutePlayModeStep(bool wasPlaying) + { + // Same API as the Editor's Next Frame button: advances one frame and + // leaves the player paused, independent of Time.timeScale. + if (!wasPlaying) + { + return ControlPlayModeActionResult.FromState( + "Play mode is not running. Step requires PlayMode; start it with --action Play first.", + false, + false); + } + + EditorApplication.Step(); + return ControlPlayModeActionResult.FromState("Stepped one frame; play mode is paused.", true, false); } private static ControlPlayModeResponse CreateResponse(string message, bool changed, bool wasAlreadyStopped) @@ -146,5 +184,53 @@ private static ControlPlayModeResponse CreateCompileErrorBlockedResponse( response.CompileErrorCount = errors.Length; return response; } + + private readonly struct ControlPlayModeActionResult + { + private ControlPlayModeActionResult( + string message, + bool changed, + bool wasAlreadyStopped, + ControlPlayModeResponse response, + bool hasResponse) + { + Message = message; + Changed = changed; + WasAlreadyStopped = wasAlreadyStopped; + Response = response; + HasResponse = hasResponse; + } + + public static ControlPlayModeActionResult FromState( + string message, + bool changed, + bool wasAlreadyStopped) + { + return new ControlPlayModeActionResult( + message, + changed, + wasAlreadyStopped, + null, + false); + } + + public static ControlPlayModeActionResult FromResponse( + ControlPlayModeResponse response, + bool changed) + { + return new ControlPlayModeActionResult( + response.Message, + changed, + response.WasAlreadyStopped, + response, + true); + } + + public string Message { get; } + public bool Changed { get; } + public bool WasAlreadyStopped { get; } + public ControlPlayModeResponse Response { get; } + public bool HasResponse { get; } + } } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs index e527703a87..d6d8b91810 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeLiteralHoister.cs @@ -11,6 +11,20 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools internal static class DynamicCodeLiteralHoister { private const string LiteralParameterPrefix = "__uloop_literal_"; + private static readonly Dictionary RegularStringEscapes = new() + { + { '\'', '\'' }, + { '"', '"' }, + { '\\', '\\' }, + { '0', '\0' }, + { 'a', '\a' }, + { 'b', '\b' }, + { 'f', '\f' }, + { 'n', '\n' }, + { 'r', '\r' }, + { 't', '\t' }, + { 'v', '\v' } + }; public static HoistedLiteralRewriteResult Rewrite(string source) { @@ -192,79 +206,92 @@ private static bool TryAdvanceInterpolatedStringLiteral(string source, ref int i { if (interpolationDepth > 0) { - if (TryAdvanceInterpolatedExpressionToken(source, ref index)) - { - continue; - } - - char expressionCharacter = source[index]; - if (expressionCharacter == '{') - { - interpolationDepth++; - index++; - continue; - } - - if (expressionCharacter == '}') - { - interpolationDepth--; - index++; - continue; - } - - index++; + interpolationDepth = AdvanceInterpolatedExpressionSegment( + source, + ref index, + interpolationDepth); continue; } - char current = source[index]; - if (current == '{') + InterpolatedStringAdvanceResult advanceResult = + AdvanceInterpolatedStringContentSegment(source, index, isVerbatim); + index = advanceResult.Index; + interpolationDepth = advanceResult.InterpolationDepth; + if (advanceResult.Completed) { - if (index + 1 < source.Length && source[index + 1] == '{') - { - index += 2; - continue; - } - - interpolationDepth = 1; - index++; - continue; + return true; } + } - if (current == '}') - { - if (index + 1 < source.Length && source[index + 1] == '}') - { - index += 2; - continue; - } + index = start; + return false; + } - index++; - continue; - } + private static int AdvanceInterpolatedExpressionSegment( + string source, + ref int index, + int interpolationDepth) + { + if (TryAdvanceInterpolatedExpressionToken(source, ref index)) + { + return interpolationDepth; + } - if (!isVerbatim && current == '\\') - { - AdvanceEscapedLiteralSequence(source, ref index); - continue; - } + char expressionCharacter = source[index]; + if (expressionCharacter == '{') + { + index++; + return interpolationDepth + 1; + } - if (current == '"') - { - if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"') - { - index += 2; - continue; - } + if (expressionCharacter == '}') + { + index++; + return interpolationDepth - 1; + } - index++; - return true; - } + index++; + return interpolationDepth; + } - index++; + private static InterpolatedStringAdvanceResult AdvanceInterpolatedStringContentSegment( + string source, + int index, + bool isVerbatim) + { + char current = source[index]; + if (current == '{') + { + return index + 1 < source.Length && source[index + 1] == '{' + ? new InterpolatedStringAdvanceResult(index + 2, 0, false) + : new InterpolatedStringAdvanceResult(index + 1, 1, false); } - index = start; - return false; + if (current == '}') + { + return index + 1 < source.Length && source[index + 1] == '}' + ? new InterpolatedStringAdvanceResult(index + 2, 0, false) + : new InterpolatedStringAdvanceResult(index + 1, 0, false); + } + + if (!isVerbatim && current == '\\') + { + int escapedIndex = index; + AdvanceEscapedLiteralSequence(source, ref escapedIndex); + return new InterpolatedStringAdvanceResult(escapedIndex, 0, false); + } + + if (current != '"') + { + return new InterpolatedStringAdvanceResult(index + 1, 0, false); + } + + if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"') + { + return new InterpolatedStringAdvanceResult(index + 2, 0, false); + } + + return new InterpolatedStringAdvanceResult(index + 1, 0, true); } private static bool TryMatchInterpolatedStringStart( @@ -712,87 +739,100 @@ private static bool TryUnescapeRegularStringLiteral(string token, out string val continue; } - if (index + 1 >= inner.Length) + if (!TryAppendRegularStringEscape(inner, ref index, unescaped)) { value = null; return false; } - - index++; - char escape = inner[index]; - switch (escape) - { - case '\'': - unescaped.Append('\''); - break; - case '"': - unescaped.Append('"'); - break; - case '\\': - unescaped.Append('\\'); - break; - case '0': - unescaped.Append('\0'); - break; - case 'a': - unescaped.Append('\a'); - break; - case 'b': - unescaped.Append('\b'); - break; - case 'f': - unescaped.Append('\f'); - break; - case 'n': - unescaped.Append('\n'); - break; - case 'r': - unescaped.Append('\r'); - break; - case 't': - unescaped.Append('\t'); - break; - case 'v': - unescaped.Append('\v'); - break; - case 'u': - if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue)) - { - value = null; - return false; - } - - unescaped.Append((char)unicodeValue); - break; - case 'U': - if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) || - !IsValidUtf32CodePoint(codePoint)) - { - value = null; - return false; - } - - unescaped.Append(char.ConvertFromUtf32(codePoint)); - break; - case 'x': - if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue)) - { - value = null; - return false; - } - - unescaped.Append((char)variableLengthValue); - break; - default: - value = null; - return false; - } } value = unescaped.ToString(); return true; } + private static bool TryAppendRegularStringEscape( + string inner, + ref int index, + StringBuilder unescaped) + { + if (index + 1 >= inner.Length) + { + return false; + } + + index++; + char escape = inner[index]; + if (RegularStringEscapes.ContainsKey(escape)) + { + unescaped.Append(RegularStringEscapes[escape]); + return true; + } + + return TryAppendComplexRegularStringEscape(inner, ref index, unescaped, escape); + } + + private static bool TryAppendComplexRegularStringEscape( + string inner, + ref int index, + StringBuilder unescaped, + char escape) + { + switch (escape) + { + case 'u': + return TryAppendUnicodeEscape(inner, ref index, unescaped); + case 'U': + return TryAppendUtf32Escape(inner, ref index, unescaped); + case 'x': + return TryAppendVariableLengthHexEscape(inner, ref index, unescaped); + default: + return false; + } + } + + private static bool TryAppendUnicodeEscape( + string inner, + ref int index, + StringBuilder unescaped) + { + if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue)) + { + return false; + } + + unescaped.Append((char)unicodeValue); + return true; + } + + private static bool TryAppendUtf32Escape( + string inner, + ref int index, + StringBuilder unescaped) + { + if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) || + !IsValidUtf32CodePoint(codePoint)) + { + return false; + } + + unescaped.Append(char.ConvertFromUtf32(codePoint)); + return true; + } + + private static bool TryAppendVariableLengthHexEscape( + string inner, + ref int index, + StringBuilder unescaped) + { + if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue)) + { + return false; + } + + unescaped.Append((char)variableLengthValue); + return true; + } + private static bool TryParseHexDigits(string value, ref int index, int digitCount, out int parsedValue) { int start = index + 1; @@ -845,6 +885,20 @@ private static bool IsHexDigit(char value) || (value >= 'a' && value <= 'f') || (value >= 'A' && value <= 'F'); } + + private readonly struct InterpolatedStringAdvanceResult + { + public InterpolatedStringAdvanceResult(int index, int interpolationDepth, bool completed) + { + Index = index; + InterpolationDepth = interpolationDepth; + Completed = completed; + } + + public int Index { get; } + public int InterpolationDepth { get; } + public bool Completed { get; } + } } /// diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/PreUsingResolver.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/PreUsingResolver.cs index d49a722d53..a8fd1b30be 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/PreUsingResolver.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/PreUsingResolver.cs @@ -89,61 +89,86 @@ private static HashSet ExtractExistingNamespaces(string source) pos = SourceShaper.SkipWhitespace(source, pos); if (pos >= length) break; - if (source[pos] == '#') + int afterLeadingTrivia = SkipExistingNamespaceLeadingTrivia(source, pos); + if (afterLeadingTrivia != pos) { - pos = SkipToEndOfLine(source, pos); - continue; - } - - // Skip comments that appear before using directives - int skipped = SourceShaper.AdvanceOneTokenPublic(source, pos); - if (skipped > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_') - { - pos = skipped; + pos = afterLeadingTrivia; continue; } // Handle "global using Ns;" — advance past "global" to reach "using" - int usingPos = pos; - if (SourceShaper.StartsWithKeyword(source, pos, "global")) - { - usingPos = SourceShaper.SkipWhitespace(source, pos + 6); - } - + int usingPos = GetUsingKeywordPosition(source, pos); if (!SourceShaper.StartsWithKeyword(source, usingPos, "using")) { break; } int afterUsing = SourceShaper.SkipWhitespace(source, usingPos + 5); - if (SourceShaper.StartsWithKeyword(source, afterUsing, "static") || - SourceShaper.StartsWithKeyword(source, afterUsing, "var") || - (afterUsing < length && source[afterUsing] == '(')) + if (ShouldSkipUsingDirective(source, afterUsing, length)) { pos = SkipToSemicolon(source, pos); continue; } int semiPos = source.IndexOf(';', afterUsing); - if (semiPos > afterUsing) - { - string ns = source.Substring(afterUsing, semiPos - afterUsing).Trim(); - int eqIdx = ns.IndexOf('='); - if (eqIdx >= 0) - { - ns = ns.Substring(eqIdx + 1).Trim(); - } - if (ns.Length > 0) - { - namespaces.Add(ns); - } - } + AddExistingNamespaceFromUsing(source, afterUsing, semiPos, namespaces); pos = semiPos >= 0 ? semiPos + 1 : length; } return namespaces; } + private static int SkipExistingNamespaceLeadingTrivia(string source, int pos) + { + if (source[pos] == '#') + { + return SkipToEndOfLine(source, pos); + } + + // Skip comments that appear before using directives + int skipped = SourceShaper.AdvanceOneTokenPublic(source, pos); + return skipped > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_' + ? skipped + : pos; + } + + private static int GetUsingKeywordPosition(string source, int pos) + { + return SourceShaper.StartsWithKeyword(source, pos, "global") + ? SourceShaper.SkipWhitespace(source, pos + 6) + : pos; + } + + private static bool ShouldSkipUsingDirective(string source, int afterUsing, int length) + { + return SourceShaper.StartsWithKeyword(source, afterUsing, "static") || + SourceShaper.StartsWithKeyword(source, afterUsing, "var") || + (afterUsing < length && source[afterUsing] == '('); + } + + private static void AddExistingNamespaceFromUsing( + string source, + int afterUsing, + int semiPos, + HashSet namespaces) + { + if (semiPos <= afterUsing) + { + return; + } + + string ns = source.Substring(afterUsing, semiPos - afterUsing).Trim(); + int eqIdx = ns.IndexOf('='); + if (eqIdx >= 0) + { + ns = ns.Substring(eqIdx + 1).Trim(); + } + if (ns.Length > 0) + { + namespaces.Add(ns); + } + } + internal static HashSet ExtractTypeIdentifiers(string source) { HashSet identifiers = new(System.StringComparer.Ordinal); @@ -162,7 +187,7 @@ internal static HashSet ExtractTypeIdentifiers(string source) } int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos); - if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_') + if (ShouldSkipTypeIdentifierToken(source, pos, advanced)) { prevWasDot = false; pos = advanced; @@ -178,31 +203,7 @@ internal static HashSet ExtractTypeIdentifiers(string source) if (char.IsLetter(c) || c == '_') { - int start = pos; - while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_')) - { - pos++; - } - - if (!prevWasDot && char.IsUpper(c)) - { - string identifier = source.Substring(start, pos - start); - - // Member initializers (Name = ...), named arguments (Name: ...), - // and labels (Name:) are not type candidates; skip them. - // Exclude == so comparisons are not mistakenly filtered. - int next = SourceShaper.SkipWhitespace(source, pos); - bool looksLikeMemberOrLabel = - next < length && - (source[next] == ':' || - (source[next] == '=' && (next + 1 >= length || source[next + 1] != '='))); - - if (!looksLikeMemberOrLabel && !ExcludedIdentifiers.Contains(identifier)) - { - identifiers.Add(identifier); - } - } - + pos = ConsumeTypeIdentifierCandidate(source, pos, length, prevWasDot, identifiers); prevWasDot = false; continue; } @@ -214,6 +215,56 @@ internal static HashSet ExtractTypeIdentifiers(string source) return identifiers; } + private static bool ShouldSkipTypeIdentifierToken(string source, int pos, int advanced) + { + return advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_'; + } + + private static int ConsumeTypeIdentifierCandidate( + string source, + int pos, + int length, + bool prevWasDot, + HashSet identifiers) + { + int start = pos; + while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_')) + { + pos++; + } + + if (!prevWasDot && char.IsUpper(source[start])) + { + AddTypeIdentifierCandidate(source, start, pos, length, identifiers); + } + + return pos; + } + + private static void AddTypeIdentifierCandidate( + string source, + int start, + int pos, + int length, + HashSet identifiers) + { + string identifier = source.Substring(start, pos - start); + if (LooksLikeMemberOrLabel(source, pos, length) || ExcludedIdentifiers.Contains(identifier)) + { + return; + } + + identifiers.Add(identifier); + } + + private static bool LooksLikeMemberOrLabel(string source, int pos, int length) + { + int next = SourceShaper.SkipWhitespace(source, pos); + return next < length && + (source[next] == ':' || + (source[next] == '=' && (next + 1 >= length || source[next + 1] != '='))); + } + internal static HashSet ExtractQualifiedTypeIdentifiers(string source) { HashSet identifiers = new(System.StringComparer.Ordinal); diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs index a5e8c41452..50665886a8 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs @@ -26,107 +26,205 @@ public static SourceShapeResult Analyze(string source) if (braceDepth == 0) { - if (TryMatchLineComment(source, pos, out int afterComment)) - { - pos = afterComment; - continue; - } + SourceTopLevelStep topLevelStep = AnalyzeTopLevelSourceStep( + source, + pos, + braceDepth, + result); + pos = topLevelStep.Position; + braceDepth = topLevelStep.BraceDepth; + } + else + { + pos = AdvanceInsideBlock(source, pos, ref braceDepth); + } + } - if (TryMatchBlockComment(source, pos, out int afterBlock)) - { - pos = afterBlock; - continue; - } + return result; + } - if (StartsWithKeyword(source, pos, "using")) - { - int segmentStart = pos; - int afterUsing = pos + 5; - afterUsing = SkipWhitespace(source, afterUsing); - - if (StartsWithKeyword(source, afterUsing, "static")) - { - int end = FindSemicolon(source, segmentStart); - result.UsingDirectives.Add(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd()); - pos = end + 1; - continue; - } - - // "using var" and "using (" are using-statements, not using-directives - if (afterUsing < length && (StartsWithKeyword(source, afterUsing, "var") || source[afterUsing] == '(')) - { - result.HasTopLevelStatements = true; - int end = FindStatementEnd(source, segmentStart, ref braceDepth); - result.TopLevelBodyBuilder.AppendLine(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd()); - pos = end + 1; - continue; - } - - int semiEnd = FindSemicolon(source, segmentStart); - result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd()); - pos = semiEnd + 1; - continue; - } + private static SourceTopLevelStep AnalyzeTopLevelSourceStep( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + SourceTopLevelStep? commentStep = TryAnalyzeTopLevelComment(source, pos, braceDepth); + if (commentStep.HasValue) + { + return commentStep.Value; + } - if (StartsWithKeyword(source, pos, "namespace")) - { - result.HasNamespaceDeclaration = true; - pos = SkipBlock(source, pos, ref braceDepth); - continue; - } + SourceTopLevelStep? usingStep = TryAnalyzeTopLevelUsing(source, pos, braceDepth, result); + if (usingStep.HasValue) + { + return usingStep.Value; + } - if (IsTypeDeclarationKeyword(source, pos)) - { - result.HasTypeDeclaration = true; - pos = SkipBlock(source, pos, ref braceDepth); - continue; - } + SourceTopLevelStep? declarationStep = TryAnalyzeTopLevelDeclaration(source, pos, braceDepth, result); + return declarationStep ?? AnalyzeTopLevelStatement(source, pos, braceDepth, result); + } - if (StartsWithKeyword(source, pos, "global") && StartsWithKeyword(source, SkipWhitespace(source, pos + 6), "using")) - { - int segmentStart = pos; - int semiEnd = FindSemicolon(source, segmentStart); - result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd()); - pos = semiEnd + 1; - continue; - } + private static SourceTopLevelStep? TryAnalyzeTopLevelComment( + string source, + int pos, + int braceDepth) + { + if (TryMatchLineComment(source, pos, out int afterComment)) + { + return new SourceTopLevelStep(afterComment, braceDepth); + } - if (pos < length && source[pos] == '[') - { - int afterAttr = SkipAttributeBlock(source, pos); - int nextNonWs = SkipWhitespace(source, afterAttr); - if (nextNonWs < length && IsTypeDeclarationKeyword(source, nextNonWs)) - { - result.HasTypeDeclaration = true; - pos = SkipBlock(source, nextNonWs, ref braceDepth); - continue; - } - } + if (TryMatchBlockComment(source, pos, out int afterBlock)) + { + return new SourceTopLevelStep(afterBlock, braceDepth); + } - if (IsAccessModifier(source, pos)) - { - int afterMod = SkipAccessModifiers(source, pos); - if (IsTypeDeclarationKeyword(source, afterMod)) - { - result.HasTypeDeclaration = true; - pos = SkipBlock(source, afterMod, ref braceDepth); - continue; - } - } + return null; + } - result.HasTopLevelStatements = true; - int stmtStart = pos; - int stmtEnd = FindStatementEnd(source, stmtStart, ref braceDepth); - result.TopLevelBodyBuilder.AppendLine(source.Substring(stmtStart, stmtEnd - stmtStart + 1).TrimEnd()); - pos = stmtEnd + 1; - } - else - { - pos = AdvanceInsideBlock(source, pos, ref braceDepth); - } + private static SourceTopLevelStep? TryAnalyzeTopLevelUsing( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + if (StartsWithKeyword(source, pos, "global")) + { + return TryAnalyzeGlobalUsingDirective(source, pos, braceDepth, result); } - return result; + if (!StartsWithKeyword(source, pos, "using")) + { + return null; + } + + int segmentStart = pos; + int afterUsing = SkipWhitespace(source, pos + 5); + if (StartsWithKeyword(source, afterUsing, "static")) + { + return AddUsingDirectiveStep(source, segmentStart, braceDepth, result); + } + + // "using var" and "using (" are using-statements, not using-directives + if (afterUsing < source.Length && (StartsWithKeyword(source, afterUsing, "var") || source[afterUsing] == '(')) + { + return AnalyzeTopLevelStatement(source, segmentStart, braceDepth, result); + } + + return AddUsingDirectiveStep(source, segmentStart, braceDepth, result); + } + + private static SourceTopLevelStep? TryAnalyzeGlobalUsingDirective( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + int usingPos = SkipWhitespace(source, pos + 6); + if (!StartsWithKeyword(source, usingPos, "using")) + { + return null; + } + + return AddUsingDirectiveStep(source, pos, braceDepth, result); + } + + private static SourceTopLevelStep AddUsingDirectiveStep( + string source, + int segmentStart, + int braceDepth, + SourceShapeResult result) + { + int semiEnd = FindSemicolon(source, segmentStart); + result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd()); + return new SourceTopLevelStep(semiEnd + 1, braceDepth); + } + + private static SourceTopLevelStep? TryAnalyzeTopLevelDeclaration( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + if (StartsWithKeyword(source, pos, "namespace")) + { + result.HasNamespaceDeclaration = true; + return SkipTopLevelBlock(source, pos, braceDepth); + } + + if (IsTypeDeclarationKeyword(source, pos)) + { + result.HasTypeDeclaration = true; + return SkipTopLevelBlock(source, pos, braceDepth); + } + + SourceTopLevelStep? attributedStep = TryAnalyzeAttributedTypeDeclaration(source, pos, braceDepth, result); + return attributedStep ?? TryAnalyzeModifiedTypeDeclaration(source, pos, braceDepth, result); + } + + private static SourceTopLevelStep? TryAnalyzeAttributedTypeDeclaration( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + if (pos >= source.Length || source[pos] != '[') + { + return null; + } + + int afterAttr = SkipAttributeBlock(source, pos); + int nextNonWs = SkipWhitespace(source, afterAttr); + int declarationStart = SkipAccessModifiers(source, nextNonWs); + if (declarationStart >= source.Length || !IsTypeDeclarationKeyword(source, declarationStart)) + { + return null; + } + + result.HasTypeDeclaration = true; + return SkipTopLevelBlock(source, declarationStart, braceDepth); + } + + private static SourceTopLevelStep? TryAnalyzeModifiedTypeDeclaration( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + if (!IsAccessModifier(source, pos)) + { + return null; + } + + int afterMod = SkipAccessModifiers(source, pos); + if (!IsTypeDeclarationKeyword(source, afterMod)) + { + return null; + } + + result.HasTypeDeclaration = true; + return SkipTopLevelBlock(source, afterMod, braceDepth); + } + + private static SourceTopLevelStep AnalyzeTopLevelStatement( + string source, + int pos, + int braceDepth, + SourceShapeResult result) + { + result.HasTopLevelStatements = true; + int nextBraceDepth = braceDepth; + int stmtEnd = FindStatementEnd(source, pos, ref nextBraceDepth); + result.TopLevelBodyBuilder.AppendLine(source.Substring(pos, stmtEnd - pos + 1).TrimEnd()); + return new SourceTopLevelStep(stmtEnd + 1, nextBraceDepth); + } + + private static SourceTopLevelStep SkipTopLevelBlock(string source, int pos, int braceDepth) + { + int nextBraceDepth = braceDepth; + int nextPosition = SkipBlock(source, pos, ref nextBraceDepth); + return new SourceTopLevelStep(nextPosition, nextBraceDepth); } public static string WrapIfNeeded(string source, string namespaceName, string className) @@ -327,83 +425,97 @@ internal static int AdvanceOneTokenPublic(string s, int pos) private static int AdvanceOneToken(string s, int pos) { if (pos >= s.Length) return s.Length; - char c = s[pos]; - // Line comment - if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '/') + (bool matched, int nextPosition) = TryAdvanceLineComment(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceBlockComment(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceVerbatimString(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceRawString(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceRegularString(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceCharLiteral(s, pos); + if (matched) return nextPosition; + (matched, nextPosition) = TryAdvanceInterpolatedString(s, pos); + if (matched) return nextPosition; + + return pos + 1; + } + + private static (bool Matched, int NextPosition) TryAdvanceLineComment(string s, int pos) + { + if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '/') { - int end = pos + 2; - while (end < s.Length && s[end] != '\n') end++; - return end < s.Length ? end + 1 : s.Length; + return (false, pos); } - // Block comment - if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '*') + int end = pos + 2; + while (end < s.Length && s[end] != '\n') end++; + return (true, end < s.Length ? end + 1 : s.Length); + } + + private static (bool Matched, int NextPosition) TryAdvanceBlockComment(string s, int pos) + { + if (pos + 1 >= s.Length || s[pos] != '/' || s[pos + 1] != '*') { - int end = pos + 2; - while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++; - return end + 2 < s.Length ? end + 2 : s.Length; + return (false, pos); } - // Verbatim string (@"...") - if (c == '@' && pos + 1 < s.Length && s[pos + 1] == '"') + int end = pos + 2; + while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++; + return (true, end + 2 < s.Length ? end + 2 : s.Length); + } + + private static (bool Matched, int NextPosition) TryAdvanceVerbatimString(string s, int pos) + { + if (pos + 1 >= s.Length || s[pos] != '@' || s[pos + 1] != '"') { - int end = pos + 2; - while (end < s.Length) - { - if (s[end] == '"') - { - if (end + 1 < s.Length && s[end + 1] == '"') { end += 2; continue; } - return end + 1; - } - end++; - } - return s.Length; + return (false, pos); } - // Raw string literal (""" ... """) - if (c == '"' && pos + 2 < s.Length && s[pos + 1] == '"' && s[pos + 2] == '"') + return (true, SkipVerbatimString(s, pos + 2)); + } + + private static (bool Matched, int NextPosition) TryAdvanceRawString(string s, int pos) + { + if (pos + 2 >= s.Length || s[pos] != '"' || s[pos + 1] != '"' || s[pos + 2] != '"') { - int end = pos + 3; - while (end + 2 < s.Length) - { - if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"') return end + 3; - end++; - } - return s.Length; + return (false, pos); } - // Regular string literal ("...") - if (c == '"') + return (true, SkipRawString(s, pos + 3)); + } + + private static (bool Matched, int NextPosition) TryAdvanceRegularString(string s, int pos) + { + if (s[pos] != '"') { - int end = pos + 1; - while (end < s.Length && s[end] != '"') - { - if (s[end] == '\\') end++; // skip escaped char - end++; - } - return end < s.Length ? end + 1 : s.Length; + return (false, pos); } - // Char literal ('x') - if (c == '\'') + return (true, SkipRegularString(s, pos + 1)); + } + + private static (bool Matched, int NextPosition) TryAdvanceCharLiteral(string s, int pos) + { + if (s[pos] != '\'') { - int end = pos + 1; - while (end < s.Length && s[end] != '\'') - { - if (s[end] == '\\') end++; - end++; - } - return end < s.Length ? end + 1 : s.Length; + return (false, pos); } - // Interpolated string ($"...") - if (c == '$' && pos + 1 < s.Length && s[pos + 1] == '"') + return (true, SkipCharLiteral(s, pos + 1)); + } + + private static (bool Matched, int NextPosition) TryAdvanceInterpolatedString(string s, int pos) + { + if (pos + 1 >= s.Length || s[pos] != '$' || s[pos + 1] != '"') { - return SkipInterpolatedString(s, pos + 2); + return (false, pos); } - return pos + 1; + return (true, SkipInterpolatedString(s, pos + 2)); } private static int SkipInterpolatedString(string s, int pos) @@ -453,39 +565,10 @@ private static int SkipInterpolationHole(string s, int pos) while (end < s.Length && depth > 0) { - if (s[end] == '\\') - { - end += 2; - continue; - } - - if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"') - { - end = SkipVerbatimString(s, end + 2); - continue; - } - - if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"') - { - end = SkipInterpolatedString(s, end + 2); - continue; - } - - if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"') - { - end = SkipRawString(s, end + 3); - continue; - } - - if (s[end] == '"') - { - end = SkipRegularString(s, end + 1); - continue; - } - - if (s[end] == '\'') + int afterLiteral = SkipInterpolationHoleLiteral(s, end); + if (afterLiteral != end) { - end = SkipCharLiteral(s, end + 1); + end = afterLiteral; continue; } @@ -509,6 +592,36 @@ private static int SkipInterpolationHole(string s, int pos) return end; } + private static int SkipInterpolationHoleLiteral(string s, int end) + { + if (s[end] == '\\') + { + return end + 2; + } + + if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"') + { + return SkipVerbatimString(s, end + 2); + } + + if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"') + { + return SkipInterpolatedString(s, end + 2); + } + + if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"') + { + return SkipRawString(s, end + 3); + } + + if (s[end] == '"') + { + return SkipRegularString(s, end + 1); + } + + return s[end] == '\'' ? SkipCharLiteral(s, end + 1) : end; + } + private static int SkipRegularString(string s, int pos) { int end = pos; @@ -578,6 +691,18 @@ private static int SkipCharLiteral(string s, int pos) return end < s.Length ? end + 1 : s.Length; } + + private readonly struct SourceTopLevelStep + { + public SourceTopLevelStep(int position, int braceDepth) + { + Position = position; + BraceDepth = braceDepth; + } + + public int Position { get; } + public int BraceDepth { get; } + } } /// diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs index de77e49ea3..8ce102e22f 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs @@ -71,9 +71,7 @@ await WarmForegroundExecutionPathIfNeededAsync(parameters, cancellationToken) return cancelledResponse; } - ExecuteDynamicCodeResponse response = ConvertExecutionResultToResponse( - finalResult, - originalCode); + ExecuteDynamicCodeResponse response = ConvertExecutionResultToResponse(finalResult); response.EmitTimingsInJsonResponse = parameters.IncludeTimings; // Why: domain-reload timeouts can complete while Unity's synchronization context is stalled. bool domainReloadWaitRequired = @@ -397,9 +395,7 @@ private static void LogExecutionException(Exception ex, string correlationId) "Investigate error cause and improve error handling"); } - private ExecuteDynamicCodeResponse ConvertExecutionResultToResponse( - ExecutionResult result, - string originalCode) + private ExecuteDynamicCodeResponse ConvertExecutionResultToResponse(ExecutionResult result) { ExecuteDynamicCodeResponse response = new() { Success = result.Success, @@ -412,63 +408,85 @@ private ExecuteDynamicCodeResponse ConvertExecutionResultToResponse( if (!result.Success) { - DynamicCodeFriendlyError friendlyError = _friendlyErrorConverter.Convert(result); - response.ErrorMessage = friendlyError.FriendlyMessage; - response.Logs = result.Logs != null ? new List(result.Logs) : new List(); - AddFriendlyFailureDetails(response.Logs, friendlyError); - - if (result.CompilationErrors?.Any() == true) - { - response.Diagnostics = BuildDiagnostics( - result.CompilationErrors, - result.UpdatedCode, - result.AmbiguousTypeCandidates); - response.CompilationErrors = response.Diagnostics; - - int total = response.Diagnostics.Count; - int unique = response.Diagnostics - .GroupBy(error => new { error.Line, error.Column, error.ErrorCode, error.Message }) - .Count(); - CompilationErrorDto first = response.Diagnostics.First(); - response.DiagnosticsSummary = - $"Errors: {unique} unique ({total} total). First at L{first.Line}: {first.ErrorCode} {first.Message}"; - - response.Logs.Add(response.DiagnosticsSummary); - } - - response.UpdatedCode = result.UpdatedCode ?? response.UpdatedCode; + ApplyFailureResponseDetails(response, result); } if (result.Exception != null) { - if (response.Logs == null) - { - response.Logs = new List(); - } - - response.Logs.Add($"Exception: {result.Exception.Message}"); - if (!string.IsNullOrEmpty(result.Exception.StackTrace)) - { - response.Logs.Add($"Stack Trace: {result.Exception.StackTrace}"); - } + ApplyExceptionResponseDetails(response, result.Exception); } if (result.AutoInjectedNamespaces != null && result.AutoInjectedNamespaces.Count > 0) { - if (response.Logs == null) - { - response.Logs = new List(); - } - - string usingList = string.Join(" ", result.AutoInjectedNamespaces.Select(ns => $"using {ns};")); - response.Logs.Add( - $"Performance hint: Auto-resolved {result.AutoInjectedNamespaces.Count} missing using directive(s): " - + $"{usingList} — Include them in your code to skip auto-resolution and improve compilation speed."); + AddAutoInjectedNamespaceHint(response, result.AutoInjectedNamespaces); } return response; } + private void ApplyFailureResponseDetails( + ExecuteDynamicCodeResponse response, + ExecutionResult result) + { + DynamicCodeFriendlyError friendlyError = _friendlyErrorConverter.Convert(result); + response.ErrorMessage = friendlyError.FriendlyMessage; + response.Logs = result.Logs != null ? new List(result.Logs) : new List(); + AddFriendlyFailureDetails(response.Logs, friendlyError); + ApplyCompilationDiagnostics(response, result); + response.UpdatedCode = result.UpdatedCode ?? response.UpdatedCode; + } + + private static void ApplyCompilationDiagnostics( + ExecuteDynamicCodeResponse response, + ExecutionResult result) + { + if (result.CompilationErrors?.Any() != true) + { + return; + } + + response.Diagnostics = BuildDiagnostics( + result.CompilationErrors, + result.UpdatedCode, + result.AmbiguousTypeCandidates); + response.CompilationErrors = response.Diagnostics; + response.DiagnosticsSummary = CreateDiagnosticsSummary(response.Diagnostics); + response.Logs.Add(response.DiagnosticsSummary); + } + + private static string CreateDiagnosticsSummary(List diagnostics) + { + int total = diagnostics.Count; + int unique = diagnostics + .GroupBy(error => new { error.Line, error.Column, error.ErrorCode, error.Message }) + .Count(); + CompilationErrorDto first = diagnostics.First(); + return $"Errors: {unique} unique ({total} total). First at L{first.Line}: {first.ErrorCode} {first.Message}"; + } + + private static void ApplyExceptionResponseDetails( + ExecuteDynamicCodeResponse response, + Exception exception) + { + response.Logs ??= new List(); + response.Logs.Add($"Exception: {exception.Message}"); + if (!string.IsNullOrEmpty(exception.StackTrace)) + { + response.Logs.Add($"Stack Trace: {exception.StackTrace}"); + } + } + + private static void AddAutoInjectedNamespaceHint( + ExecuteDynamicCodeResponse response, + List autoInjectedNamespaces) + { + response.Logs ??= new List(); + string usingList = string.Join(" ", autoInjectedNamespaces.Select(ns => $"using {ns};")); + response.Logs.Add( + $"Performance hint: Auto-resolved {autoInjectedNamespaces.Count} missing using directive(s): " + + $"{usingList} — Include them in your code to skip auto-resolution and improve compilation speed."); + } + private static void AddFriendlyFailureDetails( List logs, DynamicCodeFriendlyError friendlyError) diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/AwaitableHelper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/AwaitableHelper.cs index 436f00f731..116420c611 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/AwaitableHelper.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/AwaitableHelper.cs @@ -24,116 +24,168 @@ public static async Task AwaitIfNeeded(object value, CancellationToken c // Task and Task if (typeof(Task).IsAssignableFrom(valueType)) { - Task task = (Task)value; - await AwaitTaskWithCancellationAsync(task, cancellationToken).ConfigureAwait(false); - - if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Task<>)) - { - PropertyInfo resultProperty = valueType.GetProperty("Result"); - if (resultProperty != null) - { - object taskResult = resultProperty.GetValue(value); - return taskResult; - } - } - - return null; + return await AwaitTaskResultIfNeededAsync(value, valueType, cancellationToken) + .ConfigureAwait(false); } // ValueTask and ValueTask via AsTask if available if (IsValueTask(valueType)) { - Task asTask = ConvertValueTaskToTask(value); - await AwaitTaskWithCancellationAsync(asTask, cancellationToken).ConfigureAwait(false); - return null; + return await AwaitValueTaskResultIfNeededAsync(value, cancellationToken) + .ConfigureAwait(false); } if (IsGenericValueTask(valueType)) { - Task asTask = ConvertGenericValueTaskToTask(value); - await AwaitTaskWithCancellationAsync(asTask, cancellationToken).ConfigureAwait(false); + return await AwaitGenericValueTaskResultIfNeededAsync(value, valueType, cancellationToken) + .ConfigureAwait(false); + } - Type[] genericArgs = valueType.GetGenericArguments(); - if (genericArgs != null && genericArgs.Length == 1) - { - Type taskType = typeof(Task<>).MakeGenericType(genericArgs[0]); - PropertyInfo resultProperty = taskType.GetProperty("Result"); - if (resultProperty != null) - { - object resultValue = resultProperty.GetValue(asTask); - return resultValue; - } - } + // Awaitable pattern fallback (e.g., UniTask/UniTask or custom awaitables) + return await AwaitCustomAwaitableIfNeededAsync(value, valueType, cancellationToken) + .ConfigureAwait(false); + } + + private static async Task AwaitTaskResultIfNeededAsync( + object value, + Type valueType, + CancellationToken cancellationToken) + { + Task task = (Task)value; + await AwaitTaskWithCancellationAsync(task, cancellationToken).ConfigureAwait(false); + return TryReadGenericTaskResult(value, valueType); + } + + private static object TryReadGenericTaskResult(object value, Type valueType) + { + if (!valueType.IsGenericType || valueType.GetGenericTypeDefinition() != typeof(Task<>)) + { return null; } - // Awaitable pattern fallback (e.g., UniTask/UniTask or custom awaitables) + PropertyInfo resultProperty = valueType.GetProperty("Result"); + return resultProperty != null ? resultProperty.GetValue(value) : null; + } + + private static async Task AwaitValueTaskResultIfNeededAsync( + object value, + CancellationToken cancellationToken) + { + Task asTask = ConvertValueTaskToTask(value); + await AwaitTaskWithCancellationAsync(asTask, cancellationToken).ConfigureAwait(false); + return null; + } + + private static async Task AwaitGenericValueTaskResultIfNeededAsync( + object value, + Type valueType, + CancellationToken cancellationToken) + { + Task asTask = ConvertGenericValueTaskToTask(value); + await AwaitTaskWithCancellationAsync(asTask, cancellationToken).ConfigureAwait(false); + return TryReadGenericValueTaskResult(asTask, valueType); + } + + private static object TryReadGenericValueTaskResult(Task asTask, Type valueType) + { + Type[] genericArgs = valueType.GetGenericArguments(); + if (genericArgs == null || genericArgs.Length != 1) + { + return null; + } + + Type taskType = typeof(Task<>).MakeGenericType(genericArgs[0]); + PropertyInfo resultProperty = taskType.GetProperty("Result"); + return resultProperty != null ? resultProperty.GetValue(asTask) : null; + } + + private static async Task AwaitCustomAwaitableIfNeededAsync( + object value, + Type valueType, + CancellationToken cancellationToken) + { MethodInfo getAwaiterMethod = valueType.GetMethod("GetAwaiter", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null); - if (getAwaiterMethod != null) + if (getAwaiterMethod == null) { - object awaiter = getAwaiterMethod.Invoke(value, null); - if (awaiter == null) - { - return value; - } + return value; + } - Type awaiterType = awaiter.GetType(); - PropertyInfo isCompletedProperty = awaiterType.GetProperty("IsCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); - MethodInfo getResultMethod = awaiterType.GetMethod("GetResult", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null); - MethodInfo onCompletedMethod = awaiterType.GetMethod("OnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null); - MethodInfo unsafeOnCompletedMethod = awaiterType.GetMethod("UnsafeOnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null); + object awaiter = getAwaiterMethod.Invoke(value, null); + if (awaiter == null) + { + return value; + } - if (isCompletedProperty != null) - { - bool isCompleted = (bool)isCompletedProperty.GetValue(awaiter); - if (isCompleted) - { - object completedResult = InvokeGetResultSafely(getResultMethod, awaiter); - return completedResult; - } - } + return await AwaitCustomAwaiterAsync(awaiter, cancellationToken).ConfigureAwait(false); + } - TaskCompletionSource tcs = new(); + private static async Task AwaitCustomAwaiterAsync( + object awaiter, + CancellationToken cancellationToken) + { + Type awaiterType = awaiter.GetType(); + PropertyInfo isCompletedProperty = awaiterType.GetProperty("IsCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); + MethodInfo getResultMethod = awaiterType.GetMethod("GetResult", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null); + MethodInfo onCompletedMethod = awaiterType.GetMethod("OnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null); + MethodInfo unsafeOnCompletedMethod = awaiterType.GetMethod("UnsafeOnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null); - Action continuation = () => - { - try - { - object continuationResult = InvokeGetResultSafely(getResultMethod, awaiter); - tcs.TrySetResult(continuationResult); - } - catch (TargetInvocationException tie) - { - Exception inner = tie.InnerException ?? tie; - tcs.TrySetException(inner); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - }; + if (IsCustomAwaiterAlreadyCompleted(isCompletedProperty, awaiter)) + { + return InvokeGetResultSafely(getResultMethod, awaiter); + } - if (unsafeOnCompletedMethod != null) - { - unsafeOnCompletedMethod.Invoke(awaiter, new object[] { continuation }); - } - else if (onCompletedMethod != null) - { - onCompletedMethod.Invoke(awaiter, new object[] { continuation }); - } - else - { - // No completion registration; treat as completed - object immediateResult = InvokeGetResultSafely(getResultMethod, awaiter); - return immediateResult; - } + TaskCompletionSource tcs = new(); + Action continuation = () => CompleteCustomAwaiterTask(tcs, getResultMethod, awaiter); - object awaited = await AwaitObjectTaskWithCancellationAsync(tcs.Task, cancellationToken) - .ConfigureAwait(false); - return awaited; + if (unsafeOnCompletedMethod != null) + { + unsafeOnCompletedMethod.Invoke(awaiter, new object[] { continuation }); } + else if (onCompletedMethod != null) + { + onCompletedMethod.Invoke(awaiter, new object[] { continuation }); + } + else + { + // No completion registration; treat as completed + return InvokeGetResultSafely(getResultMethod, awaiter); + } + + object awaited = await AwaitObjectTaskWithCancellationAsync(tcs.Task, cancellationToken) + .ConfigureAwait(false); + return awaited; + } - // Not awaitable; return as-is - return value; + private static bool IsCustomAwaiterAlreadyCompleted( + PropertyInfo isCompletedProperty, + object awaiter) + { + if (isCompletedProperty == null) + { + return false; + } + + return (bool)isCompletedProperty.GetValue(awaiter); + } + + private static void CompleteCustomAwaiterTask( + TaskCompletionSource tcs, + MethodInfo getResultMethod, + object awaiter) + { + try + { + object continuationResult = InvokeGetResultSafely(getResultMethod, awaiter); + tcs.TrySetResult(continuationResult); + } + catch (TargetInvocationException tie) + { + Exception inner = tie.InnerException ?? tie; + tcs.TrySetException(inner); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } } /// diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs index a4919e17e7..1f890ea98b 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs @@ -55,17 +55,71 @@ public ComponentPropertyInfo[] SerializeProperties(Component component) /// Extract value from SerializedProperty based on its type /// private object GetSerializedPropertyValue(SerializedProperty property) + { + if (property.propertyType == SerializedPropertyType.ObjectReference) + { + return GetObjectReferenceValue(property); + } + + if (property.propertyType == SerializedPropertyType.Enum) + { + return property.enumNames[property.enumValueIndex]; + } + + if (property.propertyType == SerializedPropertyType.Integer || + property.propertyType == SerializedPropertyType.LayerMask) + { + return property.intValue; + } + + if (IsPrimitiveSerializedPropertyType(property.propertyType)) + { + return GetPrimitiveSerializedPropertyValue(property); + } + + return IsUnityStructSerializedPropertyType(property.propertyType) + ? GetUnityStructSerializedPropertyValue(property) + : null; + } + + private static bool IsPrimitiveSerializedPropertyType(SerializedPropertyType propertyType) + { + return propertyType == SerializedPropertyType.Boolean || + propertyType == SerializedPropertyType.Float || + propertyType == SerializedPropertyType.String; + } + + private static bool IsUnityStructSerializedPropertyType(SerializedPropertyType propertyType) + { + return propertyType == SerializedPropertyType.Color || + propertyType == SerializedPropertyType.Vector2 || + propertyType == SerializedPropertyType.Vector3 || + propertyType == SerializedPropertyType.Vector4 || + propertyType == SerializedPropertyType.Rect || + propertyType == SerializedPropertyType.Bounds || + propertyType == SerializedPropertyType.Quaternion; + } + + private static object GetPrimitiveSerializedPropertyValue(SerializedProperty property) { switch (property.propertyType) { - case SerializedPropertyType.Integer: - return property.intValue; case SerializedPropertyType.Boolean: return property.boolValue; case SerializedPropertyType.Float: return property.floatValue; - case SerializedPropertyType.String: + default: + UnityEngine.Debug.Assert( + property.propertyType == SerializedPropertyType.String, + "Primitive property serialization only supports bool, float, and string."); return property.stringValue; + } + } + + private static object GetUnityStructSerializedPropertyValue(SerializedProperty property) + { + switch (property.propertyType) + { case SerializedPropertyType.Color: return property.colorValue; case SerializedPropertyType.Vector2: @@ -78,16 +132,11 @@ private object GetSerializedPropertyValue(SerializedProperty property) return property.rectValue; case SerializedPropertyType.Bounds: return property.boundsValue; - case SerializedPropertyType.Quaternion: - return property.quaternionValue; - case SerializedPropertyType.Enum: - return property.enumNames[property.enumValueIndex]; - case SerializedPropertyType.LayerMask: - return property.intValue; - case SerializedPropertyType.ObjectReference: - return GetObjectReferenceValue(property); default: - return null; // Unsupported property types + UnityEngine.Debug.Assert( + property.propertyType == SerializedPropertyType.Quaternion, + "Unity struct property serialization only supports Unity value structs."); + return property.quaternionValue; } } diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs index cccb9da8c1..c6572c5bd5 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs @@ -33,82 +33,122 @@ public async Task SimulateMouseUiAsync( string correlationId = UnityCliLoopConstants.GenerateCorrelationId(); + EventSystem? eventSystem = EventSystem.current; + UnityCliLoopMouseUiSimulationResult? validationFailure = ValidateSimulationStart(parameters, eventSystem); + if (validationFailure != null) + { + return validationFailure; + } + Debug.Assert(eventSystem != null, "ValidateSimulationStart must reject a missing EventSystem."); + EventSystem activeEventSystem = eventSystem!; + + LogSimulationStart(parameters, correlationId); + EnsureOverlayExists(); + + UnityCliLoopMouseUiSimulationResult? dragStateFailure = ValidateActiveDragState(parameters); + if (dragStateFailure != null) + { + return dragStateFailure; + } + + UnityCliLoopMouseUiSimulationResult response = + await ExecuteMouseAction(parameters, activeEventSystem, ct).ConfigureAwait(false); + LogSimulationComplete(parameters, response, correlationId); + + return response; + } + + private static UnityCliLoopMouseUiSimulationResult? ValidateSimulationStart( + MouseUiSimulationCommand parameters, + EventSystem? eventSystem) + { if (!EditorApplication.isPlaying) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = "PlayMode is not active. Use control-play-mode tool to start PlayMode first.", - Action = parameters.Action.ToString() - }; + return CreateFailure( + parameters, + "PlayMode is not active. Use control-play-mode tool to start PlayMode first."); } - EventSystem? eventSystem = EventSystem.current; if (eventSystem == null) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = "No EventSystem found in the scene. Ensure an EventSystem GameObject exists.", - Action = parameters.Action.ToString() - }; + return CreateFailure( + parameters, + "No EventSystem found in the scene. Ensure an EventSystem GameObject exists."); } + return ValidateSimulationRequestOptions(parameters); + } + + private static UnityCliLoopMouseUiSimulationResult? ValidateSimulationRequestOptions( + MouseUiSimulationCommand parameters) + { if (parameters.Action != MouseAction.Click && parameters.Action != MouseAction.LongPress && parameters.DragSpeed < 0f) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = $"DragSpeed must be non-negative, got: {parameters.DragSpeed}", - Action = parameters.Action.ToString() - }; + return CreateFailure(parameters, $"DragSpeed must be non-negative, got: {parameters.DragSpeed}"); } - // uGUI drag controls (ScrollRect, Slider) only respond to left-button drags if (IsDragAction(parameters.Action) && parameters.Button != MouseButton.Left) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = $"Drag actions only support Left button (uGUI ignores non-left drags), got: {parameters.Button}", - Action = parameters.Action.ToString() - }; + return CreateFailure( + parameters, + $"Drag actions only support Left button (uGUI ignores non-left drags), got: {parameters.Button}"); } if (parameters.BypassRaycast && !SupportsBypassRaycast(parameters.Action)) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = "BypassRaycast is not supported for this action.", - Action = parameters.Action.ToString() - }; + return CreateFailure(parameters, "BypassRaycast is not supported for this action."); } if (parameters.BypassRaycast && RequiresBypassTargetPath(parameters.Action) && string.IsNullOrWhiteSpace(parameters.TargetPath)) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = "TargetPath is required when BypassRaycast is true for Click, LongPress, Drag, or DragStart.", - Action = parameters.Action.ToString() - }; + return CreateFailure( + parameters, + "TargetPath is required when BypassRaycast is true for Click, LongPress, Drag, or DragStart."); } if (!string.IsNullOrWhiteSpace(parameters.DropTargetPath) && parameters.Action != MouseAction.Drag && parameters.Action != MouseAction.DragEnd) { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = "DropTargetPath supports Drag and DragEnd only.", - Action = parameters.Action.ToString() - }; + return CreateFailure(parameters, "DropTargetPath supports Drag and DragEnd only."); } + return null; + } + + private static UnityCliLoopMouseUiSimulationResult? ValidateActiveDragState(MouseUiSimulationCommand parameters) + { + if (!MouseDragState.IsDragging || !RequiresIdlePointer(parameters.Action)) + { + return null; + } + + return CreateFailure( + parameters, + $"Cannot {parameters.Action.ToString()} while a split drag is active. Call DragEnd first."); + } + + private static bool RequiresIdlePointer(MouseAction action) + { + return action == MouseAction.Click || action == MouseAction.Drag || action == MouseAction.LongPress; + } + + private static UnityCliLoopMouseUiSimulationResult CreateFailure( + MouseUiSimulationCommand parameters, + string message) + { + return new UnityCliLoopMouseUiSimulationResult + { + Success = false, + Message = message, + Action = parameters.Action.ToString() + }; + } + + private static void LogSimulationStart(MouseUiSimulationCommand parameters, string correlationId) + { VibeLogger.LogInfo( "simulate_mouse_start", "Mouse simulation started", @@ -123,61 +163,49 @@ public async Task SimulateMouseUiAsync( }, correlationId: correlationId ); + } - EnsureOverlayExists(); - - // Single-pointer model: Click, one-shot Drag, and LongPress are invalid while a split drag is held - if (MouseDragState.IsDragging && - (parameters.Action == MouseAction.Click || parameters.Action == MouseAction.Drag || parameters.Action == MouseAction.LongPress)) - { - return new UnityCliLoopMouseUiSimulationResult - { - Success = false, - Message = $"Cannot {parameters.Action.ToString()} while a split drag is active. Call DragEnd first.", - Action = parameters.Action.ToString() - }; - } - - UnityCliLoopMouseUiSimulationResult response; + private static void LogSimulationComplete( + MouseUiSimulationCommand parameters, + UnityCliLoopMouseUiSimulationResult response, + string correlationId) + { + VibeLogger.LogInfo( + "simulate_mouse_complete", + $"Mouse simulation completed: {response.Message}", + new { Action = parameters.Action.ToString(), Success = response.Success }, + correlationId: correlationId + ); + } + private async Task ExecuteMouseAction( + MouseUiSimulationCommand parameters, + EventSystem eventSystem, + CancellationToken ct) + { switch (parameters.Action) { case MouseAction.Click: - response = await ExecuteClick(parameters, eventSystem, ct).ConfigureAwait(false); - break; + return await ExecuteClick(parameters, eventSystem, ct).ConfigureAwait(false); case MouseAction.Drag: - response = await ExecuteDragOneShot(parameters, eventSystem, ct).ConfigureAwait(false); - break; + return await ExecuteDragOneShot(parameters, eventSystem, ct).ConfigureAwait(false); case MouseAction.DragStart: - response = await ExecuteDragStart(parameters, eventSystem, ct).ConfigureAwait(false); - break; + return await ExecuteDragStart(parameters, eventSystem, ct).ConfigureAwait(false); case MouseAction.DragMove: - response = await ExecuteDragMove(parameters, ct).ConfigureAwait(false); - break; + return await ExecuteDragMove(parameters, ct).ConfigureAwait(false); case MouseAction.DragEnd: - response = await ExecuteDragEnd(parameters, ct).ConfigureAwait(false); - break; + return await ExecuteDragEnd(parameters, ct).ConfigureAwait(false); case MouseAction.LongPress: - response = await ExecuteLongPress(parameters, eventSystem, ct).ConfigureAwait(false); - break; + return await ExecuteLongPress(parameters, eventSystem, ct).ConfigureAwait(false); default: throw new ArgumentException($"Unknown mouse action: {parameters.Action}"); } - - VibeLogger.LogInfo( - "simulate_mouse_complete", - $"Mouse simulation completed: {response.Message}", - new { Action = parameters.Action.ToString(), Success = response.Success }, - correlationId: correlationId - ); - - return response; } private static void EnsureOverlayExists() @@ -218,67 +246,15 @@ private async Task ExecuteClick( { Vector2 inputPos = new(parameters.X, parameters.Y); Vector2 screenPos = InputToScreen(inputPos); - RaycastResult? hit = parameters.BypassRaycast ? null : RaycastUI(screenPos, eventSystem); - - PointerEventData.InputButton inputButton = ToInputButton(parameters.Button); - PointerEventData pointerData = new(eventSystem) + PointerEventData pointerData = CreatePointerPressData(eventSystem, screenPos, parameters.Button); + ResolvedPointerTargets resolvedTargets = + ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.Click); + if (resolvedTargets.FailureResponse != null) { - position = screenPos, - pressPosition = screenPos, - button = inputButton - }; - - GameObject? target = null; - GameObject? pressTarget = null; - GameObject? clickTarget = null; - GameObject? rawTarget = null; - - if (parameters.BypassRaycast) - { - if (!TryResolveGameObjectPath( - parameters.TargetPath, - "TargetPath", - MouseAction.Click, - inputPos, - out rawTarget, - out UnityCliLoopMouseUiSimulationResult? failureResponse)) - { - return failureResponse!; - } - - RaycastResult directRaycast = CreateDirectRaycastResult(rawTarget!); - pointerData.pointerCurrentRaycast = directRaycast; - pointerData.pointerPressRaycast = directRaycast; - - pressTarget = ExecuteEvents.GetEventHandler(rawTarget!); - clickTarget = ExecuteEvents.GetEventHandler(rawTarget!); - target = pressTarget ?? clickTarget; - - if (target != null) - { - pointerData.pointerPress = target; - pointerData.rawPointerPress = rawTarget; - } + return resolvedTargets.FailureResponse; } - else if (hit != null) - { - rawTarget = hit.Value.gameObject; - pointerData.pointerCurrentRaycast = hit.Value; - pointerData.pointerPressRaycast = hit.Value; - - // Execute dispatches only to the exact target; composite controls (Button with Text child) need hierarchy traversal - pressTarget = ExecuteEvents.GetEventHandler(rawTarget); - clickTarget = ExecuteEvents.GetEventHandler(rawTarget); - target = pressTarget ?? clickTarget; - if (target != null) - { - pointerData.pointerPress = target; - pointerData.rawPointerPress = rawTarget; - } - } - - if (parameters.BypassRaycast && target == null) + if (parameters.BypassRaycast && resolvedTargets.Target == null) { return new UnityCliLoopMouseUiSimulationResult { @@ -290,8 +266,8 @@ private async Task ExecuteClick( }; } - string? targetName = target?.name; - bool hitTarget = target != null; + string? targetName = resolvedTargets.Target?.name; + bool hitTarget = resolvedTargets.Target != null; SimulateMouseUiOverlayState.Update( MouseAction.Click, inputPos, null, targetName, Handles.GetMainGameViewSize()); @@ -305,20 +281,7 @@ private async Task ExecuteClick( await MainThreadSwitcher.SwitchToMainThread(ct); // Fire click events after expand animation so the user sees where the click lands - if (rawTarget != null) - { - if (pressTarget != null) - { - ExecuteEvents.ExecuteHierarchy( - rawTarget, pointerData, ExecuteEvents.pointerDownHandler); - ExecuteEvents.Execute(pressTarget, pointerData, ExecuteEvents.pointerUpHandler); - } - - if (clickTarget != null) - { - ExecuteEvents.Execute(clickTarget, pointerData, ExecuteEvents.pointerClickHandler); - } - } + ExecutePointerClickEvents(resolvedTargets, pointerData); bool dissipateCompleted = await PlayDissipateAnimation(ct).ConfigureAwait(false); if (!dissipateCompleted) @@ -346,62 +309,15 @@ private async Task ExecuteLongPress( Vector2 inputPos = new(parameters.X, parameters.Y); Vector2 screenPos = InputToScreen(inputPos); - RaycastResult? hit = parameters.BypassRaycast ? null : RaycastUI(screenPos, eventSystem); - - PointerEventData.InputButton inputButton = ToInputButton(parameters.Button); - PointerEventData pointerData = new(eventSystem) - { - position = screenPos, - pressPosition = screenPos, - button = inputButton - }; - - GameObject? target = null; - GameObject? rawTarget = null; - - if (parameters.BypassRaycast) - { - if (!TryResolveGameObjectPath( - parameters.TargetPath, - "TargetPath", - MouseAction.LongPress, - inputPos, - out rawTarget, - out UnityCliLoopMouseUiSimulationResult? failureResponse)) - { - return failureResponse!; - } - - RaycastResult directRaycast = CreateDirectRaycastResult(rawTarget!); - pointerData.pointerCurrentRaycast = directRaycast; - pointerData.pointerPressRaycast = directRaycast; - - target = ExecuteEvents.GetEventHandler(rawTarget!) - ?? ExecuteEvents.GetEventHandler(rawTarget!); - - if (target != null) - { - pointerData.pointerPress = target; - pointerData.rawPointerPress = rawTarget; - } - } - else if (hit != null) + PointerEventData pointerData = CreatePointerPressData(eventSystem, screenPos, parameters.Button); + ResolvedPointerTargets resolvedTargets = + ResolvePressablePointerTargets(parameters, eventSystem, inputPos, screenPos, pointerData, MouseAction.LongPress); + if (resolvedTargets.FailureResponse != null) { - rawTarget = hit.Value.gameObject; - pointerData.pointerCurrentRaycast = hit.Value; - pointerData.pointerPressRaycast = hit.Value; - - target = ExecuteEvents.GetEventHandler(rawTarget) - ?? ExecuteEvents.GetEventHandler(rawTarget); - - if (target != null) - { - pointerData.pointerPress = target; - pointerData.rawPointerPress = rawTarget; - } + return resolvedTargets.FailureResponse; } - if (parameters.BypassRaycast && target == null) + if (parameters.BypassRaycast && resolvedTargets.Target == null) { return new UnityCliLoopMouseUiSimulationResult { @@ -413,9 +329,9 @@ private async Task ExecuteLongPress( }; } - string? targetName = target?.name; - bool hitTarget = target != null; - bool shouldReleasePointer = rawTarget != null && target != null; + string? targetName = resolvedTargets.Target?.name; + bool hitTarget = resolvedTargets.Target != null; + bool shouldReleasePointer = resolvedTargets.RawTarget != null && resolvedTargets.Target != null; SimulateMouseUiOverlayState.Update( MouseAction.LongPress, inputPos, null, targetName, Handles.GetMainGameViewSize()); @@ -428,11 +344,7 @@ private async Task ExecuteLongPress( } await MainThreadSwitcher.SwitchToMainThread(ct); - if (rawTarget != null && target != null) - { - ExecuteEvents.ExecuteHierarchy( - rawTarget, pointerData, ExecuteEvents.pointerDownHandler); - } + ExecuteLongPressPointerDown(resolvedTargets, pointerData); try { @@ -459,7 +371,7 @@ private async Task ExecuteLongPress( if (shouldReleasePointer) { ExecuteCleanupOnMainThread( - () => ExecuteEvents.Execute(target!, pointerData, ExecuteEvents.pointerUpHandler)); + () => ExecuteEvents.Execute(resolvedTargets.Target!, pointerData, ExecuteEvents.pointerUpHandler)); } } @@ -474,6 +386,142 @@ private async Task ExecuteLongPress( return CreateLongPressResult(parameters, inputPos, targetName, hitTarget); } + private static PointerEventData CreatePointerPressData( + EventSystem eventSystem, + Vector2 screenPos, + MouseButton button) + { + return new PointerEventData(eventSystem) + { + position = screenPos, + pressPosition = screenPos, + button = ToInputButton(button) + }; + } + + private static ResolvedPointerTargets ResolvePressablePointerTargets( + MouseUiSimulationCommand parameters, + EventSystem eventSystem, + Vector2 inputPos, + Vector2 screenPos, + PointerEventData pointerData, + MouseAction action) + { + if (parameters.BypassRaycast) + { + return ResolveBypassPressablePointerTargets(parameters, inputPos, pointerData, action); + } + + RaycastResult? hit = RaycastUI(screenPos, eventSystem); + if (hit == null) + { + return ResolvedPointerTargets.Empty; + } + + return ResolveRaycastPressablePointerTargets(hit.Value, pointerData); + } + + private static ResolvedPointerTargets ResolveBypassPressablePointerTargets( + MouseUiSimulationCommand parameters, + Vector2 inputPos, + PointerEventData pointerData, + MouseAction action) + { + if (!TryResolveGameObjectPath( + parameters.TargetPath, + "TargetPath", + action, + inputPos, + out GameObject? rawTarget, + out UnityCliLoopMouseUiSimulationResult? failureResponse)) + { + return ResolvedPointerTargets.Failure(failureResponse); + } + + RaycastResult directRaycast = CreateDirectRaycastResult(rawTarget!); + pointerData.pointerCurrentRaycast = directRaycast; + pointerData.pointerPressRaycast = directRaycast; + + return CreateResolvedPressablePointerTargets(rawTarget!, pointerData); + } + + private static ResolvedPointerTargets ResolveRaycastPressablePointerTargets( + RaycastResult hit, + PointerEventData pointerData) + { + GameObject rawTarget = hit.gameObject; + pointerData.pointerCurrentRaycast = hit; + pointerData.pointerPressRaycast = hit; + + return CreateResolvedPressablePointerTargets(rawTarget, pointerData); + } + + private static ResolvedPointerTargets CreateResolvedPressablePointerTargets( + GameObject rawTarget, + PointerEventData pointerData) + { + // Execute dispatches only to the exact target; composite controls need hierarchy traversal. + GameObject? pressTarget = ExecuteEvents.GetEventHandler(rawTarget); + GameObject? clickTarget = ExecuteEvents.GetEventHandler(rawTarget); + GameObject? target = pressTarget ?? clickTarget; + if (target != null) + { + pointerData.pointerPress = target; + pointerData.rawPointerPress = rawTarget; + } + + return ResolvedPointerTargets.Success(rawTarget, pressTarget, clickTarget, target); + } + + private static void ExecutePointerClickEvents( + ResolvedPointerTargets resolvedTargets, + PointerEventData pointerData) + { + if (resolvedTargets.RawTarget == null) + { + return; + } + + if (resolvedTargets.PressTarget != null) + { + ExecuteEvents.ExecuteHierarchy( + resolvedTargets.RawTarget, + pointerData, + ExecuteEvents.pointerDownHandler); + } + + if (resolvedTargets.Target != null) + { + ExecuteEvents.Execute( + resolvedTargets.Target, + pointerData, + ExecuteEvents.pointerUpHandler); + } + + if (resolvedTargets.ClickTarget != null) + { + ExecuteEvents.Execute( + resolvedTargets.ClickTarget, + pointerData, + ExecuteEvents.pointerClickHandler); + } + } + + private static void ExecuteLongPressPointerDown( + ResolvedPointerTargets resolvedTargets, + PointerEventData pointerData) + { + if (resolvedTargets.RawTarget == null || resolvedTargets.Target == null) + { + return; + } + + ExecuteEvents.ExecuteHierarchy( + resolvedTargets.RawTarget, + pointerData, + ExecuteEvents.pointerDownHandler); + } + private PointerEventData InitiateDrag( EventSystem eventSystem, Vector2 screenPos, @@ -1463,6 +1511,48 @@ public TargetPathLookupResult(GameObject? target, int matchCount) public int MatchCount { get; } } + private readonly struct ResolvedPointerTargets + { + private ResolvedPointerTargets( + GameObject? rawTarget, + GameObject? pressTarget, + GameObject? clickTarget, + GameObject? target, + UnityCliLoopMouseUiSimulationResult? failureResponse) + { + RawTarget = rawTarget; + PressTarget = pressTarget; + ClickTarget = clickTarget; + Target = target; + FailureResponse = failureResponse; + } + + public static ResolvedPointerTargets Empty { get; } = + new(null, null, null, null, null); + + public GameObject? RawTarget { get; } + public GameObject? PressTarget { get; } + public GameObject? ClickTarget { get; } + public GameObject? Target { get; } + public UnityCliLoopMouseUiSimulationResult? FailureResponse { get; } + + public static ResolvedPointerTargets Success( + GameObject rawTarget, + GameObject? pressTarget, + GameObject? clickTarget, + GameObject? target) + { + return new ResolvedPointerTargets(rawTarget, pressTarget, clickTarget, target, null); + } + + public static ResolvedPointerTargets Failure( + UnityCliLoopMouseUiSimulationResult? failureResponse) + { + Debug.Assert(failureResponse != null, "Failure response must exist when target resolution fails."); + return new ResolvedPointerTargets(null, null, null, null, failureResponse); + } + } + private static bool IsDragAction(MouseAction action) { return action == MouseAction.Drag diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs index 26d4de24c2..8f2631e34c 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs @@ -57,148 +57,256 @@ internal static string[] SplitAttributeArguments(string argumentsSource) { Debug.Assert(argumentsSource != null, "argumentsSource must not be null"); - List arguments = new(); - int argumentStartIndex = 0; - int nestingDepth = 0; - bool isInRegularString = false; - bool isInVerbatimString = false; - bool isInCharLiteral = false; - bool isInRawString = false; - int rawStringQuoteCount = 0; - for (int i = 0; i < argumentsSource.Length; i++) + AttributeArgumentSplitter splitter = new(argumentsSource); + return splitter.Split(); + } + + private enum AttributeArgumentScanMode + { + None, + RegularString, + VerbatimString, + CharLiteral, + RawString, + LineComment, + BlockComment + } + + /// + /// Splits attribute argument text while ignoring commas inside nested syntax and literals. + /// + private sealed class AttributeArgumentSplitter + { + private readonly string _source; + private readonly List _arguments = new(); + private int _argumentStartIndex; + private int _nestingDepth; + private AttributeArgumentScanMode _mode; + private int _rawStringQuoteCount; + + internal AttributeArgumentSplitter(string source) { - char current = argumentsSource[i]; - if (isInRegularString) + Debug.Assert(source != null, "source must not be null"); + + _source = source; + } + + internal string[] Split() + { + for (int index = 0; index < _source.Length; index++) { - if (current == '\\') - { - i++; - continue; - } + index = ConsumeCharacter(index); + } - if (current == '"') - { - isInRegularString = false; - } + _arguments.Add(_source.Substring(_argumentStartIndex)); + return _arguments.ToArray(); + } - continue; + private int ConsumeCharacter(int index) + { + if (_mode != AttributeArgumentScanMode.None) + { + return ConsumeActiveMode(index); } - if (isInVerbatimString) + (bool enteredComment, int commentIndex) = TryEnterComment(index); + if (enteredComment) { - if (current != '"') - { - continue; - } + return commentIndex; + } - if (i + 1 < argumentsSource.Length && argumentsSource[i + 1] == '"') - { - i++; - continue; - } + (bool enteredLiteral, int nextIndex) = TryEnterLiteral(index); + if (enteredLiteral) + { + return nextIndex; + } - isInVerbatimString = false; - continue; + char current = _source[index]; + if (current == '(' || current == '[' || current == '{') + { + _nestingDepth++; + return index; } - if (isInRawString) + if (current == ')' || current == ']' || current == '}') { - if (HasRepeatedCharacterAt(argumentsSource, i, '"', rawStringQuoteCount)) - { - i += rawStringQuoteCount - 1; - isInRawString = false; - } + _nestingDepth = Math.Max(0, _nestingDepth - 1); + return index; + } - continue; + if (current == ',' && _nestingDepth == 0) + { + _arguments.Add(_source.Substring(_argumentStartIndex, index - _argumentStartIndex)); + _argumentStartIndex = index + 1; } - if (isInCharLiteral) + return index; + } + + private int ConsumeActiveMode(int index) + { + switch (_mode) { - if (current == '\\') - { - i++; - continue; - } + case AttributeArgumentScanMode.RegularString: + return ConsumeRegularString(index); + case AttributeArgumentScanMode.VerbatimString: + return ConsumeVerbatimString(index); + case AttributeArgumentScanMode.CharLiteral: + return ConsumeCharLiteral(index); + case AttributeArgumentScanMode.RawString: + return ConsumeRawString(index); + case AttributeArgumentScanMode.LineComment: + return ConsumeLineComment(index); + case AttributeArgumentScanMode.BlockComment: + return ConsumeBlockComment(index); + default: + return index; + } + } - if (current == '\'') - { - isInCharLiteral = false; - } + private int ConsumeRegularString(int index) + { + if (_source[index] == '\\') + { + return index + 1; + } - continue; + if (_source[index] == '"') + { + _mode = AttributeArgumentScanMode.None; } - if (IsRawStringStart(argumentsSource, i)) + return index; + } + + private int ConsumeVerbatimString(int index) + { + if (_source[index] != '"') { - int dollarCount = CountRepeatedCharacter(argumentsSource, i, '$'); - int quoteIndex = i + dollarCount; - rawStringQuoteCount = CountRepeatedCharacter(argumentsSource, quoteIndex, '"'); - isInRawString = true; - i = quoteIndex + rawStringQuoteCount - 1; - continue; + return index; } - if (StartsWith(argumentsSource, i, "@\"") || - StartsWith(argumentsSource, i, "$@\"") || - StartsWith(argumentsSource, i, "@$\"")) + if (index + 1 < _source.Length && _source[index + 1] == '"') { - isInVerbatimString = true; - i += GetStringPrefixLength(argumentsSource, i); - continue; + return index + 1; } - if (StartsWith(argumentsSource, i, "$\"")) + _mode = AttributeArgumentScanMode.None; + return index; + } + + private int ConsumeCharLiteral(int index) + { + if (_source[index] == '\\') { - int interpolatedStringEndIndex = - ThirdPartyToolMigrationInterpolatedStringRules.FindRegularInterpolatedStringEndIndex( - argumentsSource, - i); - if (interpolatedStringEndIndex >= 0) - { - i = interpolatedStringEndIndex; - continue; - } + return index + 1; + } - isInRegularString = true; - i++; - continue; + if (_source[index] == '\'') + { + _mode = AttributeArgumentScanMode.None; } - if (current == '"') + return index; + } + + private int ConsumeRawString(int index) + { + if (HasRepeatedCharacterAt(_source, index, '"', _rawStringQuoteCount)) { - isInRegularString = true; - continue; + _mode = AttributeArgumentScanMode.None; + return index + _rawStringQuoteCount - 1; } - if (current == '\'') + return index; + } + + private int ConsumeLineComment(int index) + { + if (_source[index] == '\n' || _source[index] == '\r') { - isInCharLiteral = true; - continue; + _mode = AttributeArgumentScanMode.None; } - if (current == '(' || current == '[' || current == '{') + return index; + } + + private int ConsumeBlockComment(int index) + { + if (StartsWith(_source, index, "*/")) { - nestingDepth++; - continue; + _mode = AttributeArgumentScanMode.None; + return index + 1; } - if (current == ')' || current == ']' || current == '}') + return index; + } + + private (bool EnteredComment, int NextIndex) TryEnterComment(int index) + { + if (StartsWith(_source, index, "//")) { - nestingDepth = Math.Max(0, nestingDepth - 1); - continue; + _mode = AttributeArgumentScanMode.LineComment; + return (true, index + 1); } - if (current != ',' || nestingDepth != 0) + if (StartsWith(_source, index, "/*")) { - continue; + _mode = AttributeArgumentScanMode.BlockComment; + return (true, index + 1); } - arguments.Add(argumentsSource.Substring(argumentStartIndex, i - argumentStartIndex)); - argumentStartIndex = i + 1; + return (false, index); } - arguments.Add(argumentsSource.Substring(argumentStartIndex)); - return arguments.ToArray(); + private (bool EnteredLiteral, int NextIndex) TryEnterLiteral(int index) + { + if (IsRawStringStart(_source, index)) + { + int dollarCount = CountRepeatedCharacter(_source, index, '$'); + int quoteIndex = index + dollarCount; + _rawStringQuoteCount = CountRepeatedCharacter(_source, quoteIndex, '"'); + _mode = AttributeArgumentScanMode.RawString; + return (true, quoteIndex + _rawStringQuoteCount - 1); + } + + if (StartsWith(_source, index, "@\"") || + StartsWith(_source, index, "$@\"") || + StartsWith(_source, index, "@$\"")) + { + _mode = AttributeArgumentScanMode.VerbatimString; + return (true, index + GetStringPrefixLength(_source, index)); + } + + if (StartsWith(_source, index, "$\"")) + { + int interpolatedStringEndIndex = + ThirdPartyToolMigrationInterpolatedStringRules.FindRegularInterpolatedStringEndIndex( + _source, + index); + if (interpolatedStringEndIndex >= 0) + { + return (true, interpolatedStringEndIndex); + } + + _mode = AttributeArgumentScanMode.RegularString; + return (true, index + 1); + } + + if (_source[index] == '"') + { + _mode = AttributeArgumentScanMode.RegularString; + return (true, index); + } + + if (_source[index] == '\'') + { + _mode = AttributeArgumentScanMode.CharLiteral; + return (true, index); + } + + return (false, index); + } } internal static string GetNamedArgumentValueOrNull(string argument, string argumentName) diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs index aa6b05061d..bcb82cece9 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs @@ -5,7 +5,6 @@ using System.Linq; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyReferenceResolver; -using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyScopedNameMap; namespace io.github.hatayama.UnityCliLoop.Infrastructure { @@ -32,335 +31,24 @@ internal static MigrationAssemblyUsage FindMigrationAssemblyUsage( .ToList(); List assemblyReferenceDirectories = CreateAssemblyReferenceDirectories(asmdefFilePaths, asmrefFilePaths); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentToolContractsDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentApplicationDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentDomainAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyDeclaredTypeNamesByDirectory = - new(StringComparer.Ordinal); - HashSet registrarAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainMetadataAssemblyDirectories = new(StringComparer.Ordinal); - HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = new(StringComparer.Ordinal); + ThirdPartyToolMigrationAssemblyUsageScanState scanState = + new(projectRoot, asmdefDirectories, assemblyReferenceDirectories); Dictionary sourceByCSharpFilePath = new(StringComparer.Ordinal); foreach (string csharpFilePath in csharpFilePaths) { string source = ThirdPartyToolMigrationFileAccess.ReadAllText(csharpFilePath); sourceByCSharpFilePath.Add(csharpFilePath, source); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - AddAssemblyScopedNames( - assemblyDeclaredTypeNamesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); - if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) - { - legacyAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalUsing(source)) - { - assemblyScopedLegacyDirectories.Add(assemblyDirectory); - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalToolInfoTypeAlias(source)) - { - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalToolInfoTypeAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainGlobalUsing(source)) - { - assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainUsing(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainNamespaceAlias(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentDomainGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) - { - assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) - { - assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationUsing(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationNamespaceAlias(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentApplicationGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source)) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentFirstPartyToolsGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } + scanState.RecordInitialSourceFacts(source, csharpFilePath); } foreach (string csharpFilePath in csharpFilePaths) { string source = sourceByCSharpFilePath[csharpFilePath]; - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - string[] legacyAssemblyAliases = Array.Empty(); - if (assemblyScopedLegacyAliasesByDirectory.TryGetValue( - assemblyDirectory, - out HashSet legacyAssemblyAliasSet)) - { - legacyAssemblyAliases = legacyAssemblyAliasSet - .OrderBy(alias => alias, StringComparer.Ordinal) - .ToArray(); - } - bool hasLegacyCSharpApi = ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source); - string[] currentApplicationAssemblyAliases = - GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory); - bool hasCurrentApplicationSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( - source, - assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), - currentApplicationAssemblyAliases, - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory)); - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainHelperApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases) || - ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - hasLegacyCSharpApi || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || - assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases, - GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory)) || - hasCurrentApplicationSourceTarget) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - string[] currentDomainAssemblyAliases = - GetAssemblyScopedNames(assemblyScopedCurrentDomainAliasesByDirectory, assemblyDirectory); - string[] currentDomainNamespaceAliases = - ThirdPartyToolMigrationAliasRules.GetCombinedCurrentDomainNamespaceAliases( - source, - currentDomainAssemblyAliases); - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory)) || - ThirdPartyToolMigrationRules.ContainsCurrentDomainContractAliasReference( - source, - currentDomainNamespaceAliases)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - bool hasAssemblyScopedCurrentToolContractsUsing = - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentFirstPartyToolsUsing = - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); - string[] assemblyDeclaredTypeNames = - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); - string[] currentFirstPartyToolsAssemblyAliases = - GetAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory); - bool hasLegacyEditorWindowCaptureUtilitySourceTarget = - ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentFirstPartyToolsContractSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotContractApiForAssembly( - source, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentFirstPartyScreenshotReferenceRequirement = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - hasLegacyCSharpApi || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || - hasAssemblyScopedCurrentToolContractsUsing, - legacyAssemblyAliases, - assemblyDeclaredTypeNames) || - hasLegacyEditorWindowCaptureUtilitySourceTarget || - hasCurrentFirstPartyToolsContractSourceTarget || - hasCurrentFirstPartyScreenshotReferenceRequirement) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (hasCurrentFirstPartyScreenshotReferenceRequirement) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) - { - applicationReferenceAssemblyDirectories.Add(registrarAssemblyDirectory); + scanState.RecordReferenceRequirements(source, csharpFilePath); } - return new MigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentDomainAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); + return scanState.CreateUsage(); } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs index 13df1f2349..2078b0444e 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyReferenceResolver; -using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyScopedNameMap; namespace io.github.hatayama.UnityCliLoop.Infrastructure { @@ -44,381 +43,34 @@ await CreateAssemblyReferenceDirectoriesAsync( sourceFileCache, progressCounter, ct); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentToolContractsDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentApplicationDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentDomainAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyDeclaredTypeNamesByDirectory = - new(StringComparer.Ordinal); - HashSet registrarAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainMetadataAssemblyDirectories = new(StringComparer.Ordinal); - HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = new(StringComparer.Ordinal); + ThirdPartyToolMigrationAssemblyUsageScanState scanState = + new(projectRoot, asmdefDirectories, assemblyReferenceDirectories); foreach (string csharpFilePath in csharpFilePaths) { if (ct.IsCancellationRequested) { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); + return scanState.CreateUsage(); } string source = sourceFileCache.ReadAllText(csharpFilePath); await progressCounter.ReportProcessedItemAsync(ct); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - AddAssemblyScopedNames( - assemblyDeclaredTypeNamesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); - if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) - { - legacyAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalUsing(source)) - { - assemblyScopedLegacyDirectories.Add(assemblyDirectory); - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalToolInfoTypeAlias(source)) - { - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalToolInfoTypeAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainGlobalUsing(source)) - { - assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainUsing(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainNamespaceAlias(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentDomainGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) - { - assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) - { - assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationUsing(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationNamespaceAlias(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentApplicationGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source)) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentFirstPartyToolsGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } + scanState.RecordInitialSourceFacts(source, csharpFilePath); } foreach (string csharpFilePath in csharpFilePaths) { if (ct.IsCancellationRequested) { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); + return scanState.CreateUsage(); } string source = sourceFileCache.ReadAllText(csharpFilePath); await progressCounter.ReportProcessedItemAsync(ct); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - string[] legacyAssemblyAliases = Array.Empty(); - if (assemblyScopedLegacyAliasesByDirectory.TryGetValue( - assemblyDirectory, - out HashSet legacyAssemblyAliasSet)) - { - legacyAssemblyAliases = legacyAssemblyAliasSet - .OrderBy(alias => alias, StringComparer.Ordinal) - .ToArray(); - } - bool hasLegacyCSharpApi = ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source); - string[] currentApplicationAssemblyAliases = - GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory); - bool hasCurrentApplicationSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( - source, - assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), - currentApplicationAssemblyAliases, - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory)); - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainHelperApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases) || - ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - hasLegacyCSharpApi || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || - assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases, - currentApplicationAssemblyAliases, - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory)) || - hasCurrentApplicationSourceTarget) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - string[] currentDomainAssemblyAliases = - GetAssemblyScopedNames(assemblyScopedCurrentDomainAliasesByDirectory, assemblyDirectory); - string[] currentDomainNamespaceAliases = - ThirdPartyToolMigrationAliasRules.GetCombinedCurrentDomainNamespaceAliases( - source, - currentDomainAssemblyAliases); - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory)) || - ThirdPartyToolMigrationRules.ContainsCurrentDomainContractAliasReference( - source, - currentDomainNamespaceAliases)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - bool hasAssemblyScopedCurrentToolContractsUsing = - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentFirstPartyToolsUsing = - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); - string[] assemblyDeclaredTypeNames = - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); - string[] currentFirstPartyToolsAssemblyAliases = - GetAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory); - bool hasLegacyEditorWindowCaptureUtilitySourceTarget = - ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentFirstPartyToolsContractSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotContractApiForAssembly( - source, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentFirstPartyScreenshotReferenceRequirement = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - hasLegacyCSharpApi || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || - hasAssemblyScopedCurrentToolContractsUsing, - legacyAssemblyAliases, - assemblyDeclaredTypeNames) || - hasLegacyEditorWindowCaptureUtilitySourceTarget || - hasCurrentFirstPartyToolsContractSourceTarget || - hasCurrentFirstPartyScreenshotReferenceRequirement) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (hasCurrentFirstPartyScreenshotReferenceRequirement) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) - { - applicationReferenceAssemblyDirectories.Add(registrarAssemblyDirectory); + scanState.RecordReferenceRequirements(source, csharpFilePath); } - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); + return scanState.CreateUsage(); } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs new file mode 100644 index 0000000000..6c5715aea9 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs @@ -0,0 +1,552 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyReferenceResolver; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyScopedNameMap; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Accumulates assembly-scoped facts used by migration planning and fast target scanning. + /// + internal sealed class ThirdPartyToolMigrationAssemblyUsageScanState + { + private readonly string _projectRoot; + + public ThirdPartyToolMigrationAssemblyUsageScanState( + string projectRoot, + List asmdefDirectories, + List assemblyReferenceDirectories) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "assemblyReferenceDirectories must not be null"); + + _projectRoot = projectRoot; + AsmdefDirectories = asmdefDirectories; + AssemblyReferenceDirectories = assemblyReferenceDirectories; + } + + public List AsmdefDirectories { get; } + public List AssemblyReferenceDirectories { get; } + public HashSet LegacyAssemblyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet AssemblyScopedLegacyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet AssemblyScopedCurrentToolContractsDirectories { get; } = new(StringComparer.Ordinal); + public HashSet AssemblyScopedCurrentApplicationDirectories { get; } = new(StringComparer.Ordinal); + public HashSet AssemblyScopedCurrentDomainDirectories { get; } = new(StringComparer.Ordinal); + public HashSet AssemblyScopedCurrentFirstPartyToolsDirectories { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyScopedLegacyAliasesByDirectory { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyScopedLegacyToolInfoAliasesByDirectory { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyScopedCurrentApplicationAliasesByDirectory { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyScopedCurrentDomainAliasesByDirectory { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory { get; } = + new(StringComparer.Ordinal); + public Dictionary> AssemblyDeclaredTypeNamesByDirectory { get; } = + new(StringComparer.Ordinal); + public HashSet RegistrarAssemblyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet ToolContractsReferenceAssemblyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet ApplicationReferenceAssemblyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet DomainReferenceAssemblyDirectories { get; } = new(StringComparer.Ordinal); + public HashSet FirstPartyScreenshotReferenceAssemblyDirectories { get; } = + new(StringComparer.Ordinal); + + public bool HasReferenceRequirements => + ToolContractsReferenceAssemblyDirectories.Count > 0 || + ApplicationReferenceAssemblyDirectories.Count > 0 || + DomainReferenceAssemblyDirectories.Count > 0 || + FirstPartyScreenshotReferenceAssemblyDirectories.Count > 0; + + public bool RecordInitialSourceFacts(string source, string csharpFilePath) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + return false; + } + + string assemblyDirectory = FindAssemblyDirectory(csharpFilePath); + AddAssemblyScopedNames( + AssemblyDeclaredTypeNamesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); + RecordLegacySourceFacts( + source, + assemblyDirectory, + recordGlobalUsingAsLegacyAssemblyTarget: false); + RecordCurrentDomainSourceFacts(source, assemblyDirectory); + RecordCurrentToolContractsSourceFacts(source, assemblyDirectory); + RecordCurrentApplicationSourceFacts(source, assemblyDirectory); + RecordCurrentFirstPartyToolsSourceFacts(source, assemblyDirectory); + RecordRegistrarSourceFacts(source, assemblyDirectory); + RecordDomainMetadataSourceFacts(source, assemblyDirectory); + return true; + } + + public bool RecordTargetScanInitialSourceFacts(string source, string csharpFilePath) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + return false; + } + + string assemblyDirectory = FindAssemblyDirectory(csharpFilePath); + AddAssemblyScopedNames( + AssemblyDeclaredTypeNamesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); + RecordLegacySourceFacts( + source, + assemblyDirectory, + recordGlobalUsingAsLegacyAssemblyTarget: true); + RecordCurrentDomainSourceFacts(source, assemblyDirectory); + RecordCurrentToolContractsSourceFacts(source, assemblyDirectory); + RecordCurrentApplicationSourceFacts(source, assemblyDirectory); + RecordCurrentFirstPartyToolsSourceFacts(source, assemblyDirectory); + return true; + } + + public void RecordReferenceRequirements(string source, string csharpFilePath) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + return; + } + + string assemblyDirectory = FindAssemblyDirectory(csharpFilePath); + string[] legacyAssemblyAliases = GetAssemblyScopedNames( + AssemblyScopedLegacyAliasesByDirectory, + assemblyDirectory); + string[] assemblyDeclaredTypeNames = GetAssemblyScopedNames( + AssemblyDeclaredTypeNamesByDirectory, + assemblyDirectory); + bool hasLegacyCSharpApi = ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source); + RecordToolContractsRequirement( + source, + assemblyDirectory, + legacyAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyCSharpApi); + RecordRegistrarDomainReturnRequirement(source, assemblyDirectory, legacyAssemblyAliases); + RecordDomainContractRequirement(source, assemblyDirectory); + RecordFirstPartyScreenshotRequirement( + source, + assemblyDirectory, + legacyAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyCSharpApi); + RecordEditorWindowCaptureTimeoutRequirement( + source, + assemblyDirectory, + legacyAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyCSharpApi); + } + + public MigrationAssemblyUsage CreateUsage() + { + AddRegistrarApplicationReferences(); + return CreateMigrationAssemblyUsage( + AsmdefDirectories, + AssemblyReferenceDirectories, + LegacyAssemblyDirectories, + AssemblyScopedLegacyDirectories, + AssemblyScopedCurrentToolContractsDirectories, + AssemblyScopedCurrentApplicationDirectories, + AssemblyScopedCurrentDomainDirectories, + AssemblyScopedCurrentFirstPartyToolsDirectories, + AssemblyScopedLegacyAliasesByDirectory, + AssemblyScopedLegacyToolInfoAliasesByDirectory, + AssemblyScopedCurrentApplicationAliasesByDirectory, + AssemblyScopedCurrentDomainAliasesByDirectory, + AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + AssemblyDeclaredTypeNamesByDirectory, + ToolContractsReferenceAssemblyDirectories, + ApplicationReferenceAssemblyDirectories, + DomainReferenceAssemblyDirectories, + FirstPartyScreenshotReferenceAssemblyDirectories); + } + + public MigrationAssemblyUsage CreateReferenceRequirementUsage() + { + return new MigrationAssemblyUsage( + AsmdefDirectories, + AssemblyReferenceDirectories, + LegacyAssemblyDirectories, + AssemblyScopedLegacyDirectories, + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + CreateAssemblyScopedLegacyAliasesByDirectory(AssemblyScopedLegacyAliasesByDirectory), + CreateAssemblyScopedLegacyAliasesByDirectory(AssemblyScopedLegacyToolInfoAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(AssemblyScopedCurrentApplicationAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(AssemblyScopedCurrentDomainAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(AssemblyDeclaredTypeNamesByDirectory), + ToolContractsReferenceAssemblyDirectories, + ApplicationReferenceAssemblyDirectories, + DomainReferenceAssemblyDirectories, + FirstPartyScreenshotReferenceAssemblyDirectories); + } + + private string FindAssemblyDirectory(string csharpFilePath) + { + return FindNearestAssemblyDirectory( + csharpFilePath, + AsmdefDirectories, + AssemblyReferenceDirectories, + _projectRoot); + } + + private void RecordLegacySourceFacts( + string source, + string assemblyDirectory, + bool recordGlobalUsingAsLegacyAssemblyTarget) + { + if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) + { + LegacyAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalUsing(source)) + { + if (recordGlobalUsingAsLegacyAssemblyTarget) + { + LegacyAssemblyDirectories.Add(assemblyDirectory); + } + + AssemblyScopedLegacyDirectories.Add(assemblyDirectory); + AddAssemblyScopedLegacyAliases( + AssemblyScopedLegacyAliasesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetLegacyGlobalNamespaceAliases(source)); + } + + if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalToolInfoTypeAlias(source)) + { + AddAssemblyScopedLegacyAliases( + AssemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetLegacyGlobalToolInfoTypeAliases(source)); + } + } + + private void RecordCurrentDomainSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainGlobalUsing(source)) + { + AssemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); + DomainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainUsing(source)) + { + DomainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainNamespaceAlias(source)) + { + DomainReferenceAssemblyDirectories.Add(assemblyDirectory); + AddAssemblyScopedNames( + AssemblyScopedCurrentDomainAliasesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetCurrentDomainGlobalNamespaceAliases(source)); + } + } + + private void RecordCurrentToolContractsSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) + { + AssemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); + } + } + + private void RecordCurrentApplicationSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) + { + AssemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); + ApplicationReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationUsing(source)) + { + ApplicationReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationNamespaceAlias(source)) + { + ApplicationReferenceAssemblyDirectories.Add(assemblyDirectory); + AddAssemblyScopedNames( + AssemblyScopedCurrentApplicationAliasesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetCurrentApplicationGlobalNamespaceAliases(source)); + } + } + + private void RecordCurrentFirstPartyToolsSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) + { + AssemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); + FirstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source)) + { + FirstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + AddAssemblyScopedNames( + AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetCurrentFirstPartyToolsGlobalNamespaceAliases(source)); + } + } + + private void RecordRegistrarSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source)) + { + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + { + RegistrarAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) + { + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) + { + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + private void RecordDomainMetadataSourceFacts(string source, string assemblyDirectory) + { + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) + { + DomainReferenceAssemblyDirectories.Add(assemblyDirectory); + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + private void RecordToolContractsRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyCSharpApi) + { + string[] currentApplicationAssemblyAliases = GetAssemblyScopedNames( + AssemblyScopedCurrentApplicationAliasesByDirectory, + assemblyDirectory); + bool hasCurrentApplicationSourceTarget = + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( + source, + AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + currentApplicationAssemblyAliases, + assemblyDeclaredTypeNames); + if (!ContainsToolContractsReferenceRequirement( + source, + assemblyDirectory, + legacyAssemblyAliases, + currentApplicationAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyCSharpApi, + hasCurrentApplicationSourceTarget)) + { + return; + } + + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + private bool ContainsToolContractsReferenceRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyCSharpApi, + bool hasCurrentApplicationSourceTarget) + { + bool canUseLegacyAssemblyApi = + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + hasLegacyCSharpApi || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || + AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || + AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory); + return ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( + source, + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + canUseLegacyAssemblyApi, + legacyAssemblyAliases, + currentApplicationAssemblyAliases, + assemblyDeclaredTypeNames) || + hasCurrentApplicationSourceTarget; + } + + private void RecordRegistrarDomainReturnRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases) + { + if (!ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( + source, + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases)) + { + return; + } + + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + private void RecordDomainContractRequirement(string source, string assemblyDirectory) + { + string[] currentDomainAssemblyAliases = GetAssemblyScopedNames( + AssemblyScopedCurrentDomainAliasesByDirectory, + assemblyDirectory); + string[] currentDomainNamespaceAliases = + ThirdPartyToolMigrationAliasRules.GetCombinedCurrentDomainNamespaceAliases( + source, + currentDomainAssemblyAliases); + if (!ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( + source, + AssemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory)) && + !ThirdPartyToolMigrationRules.ContainsCurrentDomainContractAliasReference( + source, + currentDomainNamespaceAliases)) + { + return; + } + + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + private void RecordFirstPartyScreenshotRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyCSharpApi) + { + string[] currentFirstPartyToolsAssemblyAliases = GetAssemblyScopedNames( + AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDirectory); + bool hasCurrentFirstPartyScreenshotReferenceRequirement = + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + if (ContainsFirstPartyScreenshotToolContractsRequirement( + source, + assemblyDirectory, + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyCSharpApi, + hasCurrentFirstPartyScreenshotReferenceRequirement)) + { + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (hasCurrentFirstPartyScreenshotReferenceRequirement) + { + FirstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + private bool ContainsFirstPartyScreenshotToolContractsRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyCSharpApi, + bool hasCurrentFirstPartyScreenshotReferenceRequirement) + { + bool hasAssemblyScopedCurrentToolContractsUsing = + AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory); + bool hasAssemblyScopedCurrentFirstPartyToolsUsing = + AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); + return ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + hasLegacyCSharpApi || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || + hasAssemblyScopedCurrentToolContractsUsing, + legacyAssemblyAliases, + assemblyDeclaredTypeNames) || + ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( + source, + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotContractApiForAssembly( + source, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames) || + hasCurrentFirstPartyScreenshotReferenceRequirement; + } + + private void RecordEditorWindowCaptureTimeoutRequirement( + string source, + string assemblyDirectory, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyCSharpApi) + { + if (!ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( + source, + AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) || hasLegacyCSharpApi, + AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases, + GetAssemblyScopedNames(AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, assemblyDirectory), + assemblyDeclaredTypeNames)) + { + return; + } + + ToolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + private void AddRegistrarApplicationReferences() + { + foreach (string registrarAssemblyDirectory in RegistrarAssemblyDirectories) + { + ApplicationReferenceAssemblyDirectories.Add(registrarAssemblyDirectory); + } + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs.meta new file mode 100644 index 0000000000..24482bc499 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageScanState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f01225e709fc4bd0ad6521d74f263ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs index 6fd2d85cf0..c2ae307508 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs @@ -100,265 +100,658 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac "currentFirstPartyToolsAssemblyAliases must not be null"); Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); - string migratedContent = source; - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - string[] currentApplicationNamespaceAliases = GetCombinedCurrentApplicationNamespaceAliases( - source, - currentApplicationAssemblyAliases); - string[] currentDomainNamespaceAliases = GetCombinedCurrentDomainNamespaceAliases( - source, - currentDomainAssemblyAliases); - string[] currentFirstPartyToolsNamespaceAliases = GetCombinedCurrentFirstPartyToolsNamespaceAliases( - source, - currentFirstPartyToolsAssemblyAliases); - bool hasLegacyNamespaceUsage = RegexMatchesCode(source, LegacyNamespaceRegex); - bool hasLegacyNamespaceUsingDirective = RegexMatchesCode(source, LegacyNamespaceUsingRegex); - bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); - bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); - bool hasCurrentDomainUsingDirective = - RegexMatchesCode(source, CurrentDomainUsingRegex) || - RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); - bool hasCurrentToolContractsNamespaceUsage = RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); - bool hasCurrentToolContractsUsingDirective = - RegexMatchesCode(source, CurrentToolContractsUsingRegex); - bool hasCurrentFirstPartyToolsNamespaceUsage = - RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); - bool hasCurrentFirstPartyToolsUsingDirective = - RegexMatchesCode(source, CurrentFirstPartyToolsUsingRegex) || - RegexMatchesCode(source, CurrentFirstPartyToolsGlobalUsingRegex); - bool canUseCurrentToolContracts = - hasCurrentToolContractsNamespaceUsage || - hasAssemblyScopedCurrentToolContractsUsing; - bool canUseBareCurrentToolContracts = - hasLegacyAssemblySource || - hasLegacyNamespaceUsingDirective || - hasCurrentToolContractsUsingDirective || - hasAssemblyScopedCurrentToolContractsUsing; - bool canPreserveBareCurrentToolContractsReferences = - hasCurrentToolContractsUsingDirective || - hasAssemblyScopedCurrentToolContractsUsing; - bool canMigrateBareLegacyToolAttribute = - hasLegacyAssemblySource || - hasLegacyNamespaceUsage || - legacyNamespaceAliases.Length > 0; - bool canMigrateBareLegacyEditorWindowCaptureUtility = - canMigrateBareLegacyToolAttribute || - canUseCurrentToolContracts || - hasAssemblyScopedCurrentFirstPartyToolsUsing || - hasCurrentFirstPartyToolsNamespaceUsage; - bool canUseBareCurrentFirstPartyTools = - hasAssemblyScopedCurrentFirstPartyToolsUsing || - hasCurrentFirstPartyToolsUsingDirective; - bool shouldQualifyBareEditorWindowCaptureUtilityTimeout = - !canUseBareCurrentToolContracts; - bool canMigrateBareLegacyFirstPartyScreenshotApi = - canMigrateBareLegacyToolAttribute || - canUseCurrentToolContracts || - hasAssemblyScopedCurrentFirstPartyToolsUsing || - hasCurrentFirstPartyToolsNamespaceUsage; - bool canMigrateBareCurrentDomainContractType = - hasAssemblyScopedCurrentDomainUsing || - hasCurrentDomainUsingDirective; - bool canMigrateBareLegacyApplicationApi = - canMigrateBareLegacyToolAttribute || - canUseCurrentToolContracts || - hasAssemblyScopedCurrentApplicationUsing || - hasCurrentApplicationNamespaceUsage; - bool canMigrateBareLegacyApplicationTypeName = - canMigrateBareLegacyToolAttribute || - canUseCurrentToolContracts || - hasAssemblyScopedCurrentApplicationUsing || - hasCurrentApplicationNamespaceUsage; - bool canMigrateBareLegacyToolInfoConstructor = - canMigrateBareLegacyToolAttribute; - bool canMigrateAmbiguousBareLegacyToolInfoConstructor = - canMigrateBareLegacyToolAttribute && - !hasCurrentDomainNamespaceUsage; - bool hasLocalLegacyMarker = ContainsLegacyToolMigrationMarker(source); - bool shouldApplyContractRenames = hasLegacyAssemblySource || hasLocalLegacyMarker; - bool shouldApplyRegistrarRenames = shouldApplyContractRenames && - RegexMatchesCode(source, LegacyRegistrarRegex); - bool shouldApplyDomainMetadataRenames = shouldApplyContractRenames && - RegexMatchesCode(source, LegacyDomainMetadataRegex); - int replacementCount = 0; - List removedPlayerLoopTimingSignatures = new(); - migratedContent = ReplaceLegacyToolAttributesInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - ref replacementCount); - migratedContent = ReplaceLegacyToolInfoConstructorsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolInfoConstructor, - canMigrateAmbiguousBareLegacyToolInfoConstructor, - legacyAssemblyToolInfoAliases, - ref replacementCount); - migratedContent = ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - ref replacementCount); - (string editorDelayMigratedContent, int editorDelayReplacementCount) = - ReplaceLegacyEditorDelayFrameCallsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute || canUseCurrentToolContracts, - !canUseBareCurrentToolContracts); - migratedContent = editorDelayMigratedContent; - replacementCount += editorDelayReplacementCount; - (string timerDelayMigratedContent, int timerDelayReplacementCount) = - ReplaceLegacyTimerDelayNamedArgumentsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute || canUseCurrentToolContracts); - migratedContent = timerDelayMigratedContent; - replacementCount += timerDelayReplacementCount; - (string mainThreadSwitcherMigratedContent, int mainThreadSwitcherReplacementCount) = - ReplaceLegacyMainThreadSwitcherCallsInCode( - migratedContent, + CSharpLegacyAssemblyMigrationContext migrationContext = + CSharpLegacyAssemblyMigrationContext.Create( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentApplicationUsing, + hasAssemblyScopedCurrentDomainUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyAssemblyAliases, + legacyAssemblyToolInfoAliases, + currentApplicationAssemblyAliases, + currentDomainAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + migrationContext.ApplyToolMetadataReplacements(); + migrationContext.ApplyDelayReplacements(); + migrationContext.ApplyMainThreadSwitcherReplacements(); + migrationContext.ApplyEditorWindowCaptureAndTypeReplacements(); + migrationContext.ApplyContractRenames(); + migrationContext.ApplyRegistrarRenames(); + migrationContext.ApplyCurrentPublicContractNamespaceReplacements(); + return migrationContext.CreateResult(); + } + + /// + /// Carries the derived migration context while applying C# source rewrite phases. + /// + private sealed class CSharpLegacyAssemblyMigrationContext + { + private readonly string[] _legacyNamespaceAliases; + private readonly string[] _legacyAssemblyToolInfoAliases; + private readonly string[] _currentApplicationNamespaceAliases; + private readonly string[] _currentDomainNamespaceAliases; + private readonly string[] _currentFirstPartyToolsNamespaceAliases; + private readonly string[] _assemblyDeclaredTypeNames; + private readonly bool _canUseCurrentToolContracts; + private readonly bool _canUseBareCurrentToolContracts; + private readonly bool _canPreserveBareCurrentToolContractsReferences; + private readonly bool _canMigrateBareLegacyToolAttribute; + private readonly bool _canMigrateBareLegacyEditorWindowCaptureUtility; + private readonly bool _canUseBareCurrentFirstPartyTools; + private readonly bool _shouldQualifyBareEditorWindowCaptureUtilityTimeout; + private readonly bool _canMigrateBareLegacyFirstPartyScreenshotApi; + private readonly bool _canMigrateBareCurrentDomainContractType; + private readonly bool _canMigrateBareLegacyApplicationApi; + private readonly bool _canMigrateBareLegacyApplicationTypeName; + private readonly bool _canMigrateAmbiguousBareLegacyToolInfoConstructor; + private readonly bool _shouldApplyContractRenames; + private readonly bool _shouldApplyRegistrarRenames; + private readonly bool _shouldApplyDomainMetadataRenames; + private readonly List _removedPlayerLoopTimingSignatures = + new(); + private string _migratedContent; + private int _replacementCount; + + private CSharpLegacyAssemblyMigrationContext( + string source, + string[] legacyNamespaceAliases, + string[] legacyAssemblyToolInfoAliases, + string[] currentApplicationNamespaceAliases, + string[] currentDomainNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + string[] assemblyDeclaredTypeNames, + bool canUseCurrentToolContracts, + bool canUseBareCurrentToolContracts, + bool canPreserveBareCurrentToolContractsReferences, + bool canMigrateBareLegacyToolAttribute, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool canUseBareCurrentFirstPartyTools, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + bool canMigrateBareCurrentDomainContractType, + bool canMigrateBareLegacyApplicationApi, + bool canMigrateBareLegacyApplicationTypeName, + bool canMigrateAmbiguousBareLegacyToolInfoConstructor, + bool shouldApplyContractRenames, + bool shouldApplyRegistrarRenames, + bool shouldApplyDomainMetadataRenames) + { + _migratedContent = source; + _legacyNamespaceAliases = legacyNamespaceAliases; + _legacyAssemblyToolInfoAliases = legacyAssemblyToolInfoAliases; + _currentApplicationNamespaceAliases = currentApplicationNamespaceAliases; + _currentDomainNamespaceAliases = currentDomainNamespaceAliases; + _currentFirstPartyToolsNamespaceAliases = currentFirstPartyToolsNamespaceAliases; + _assemblyDeclaredTypeNames = assemblyDeclaredTypeNames; + _canUseCurrentToolContracts = canUseCurrentToolContracts; + _canUseBareCurrentToolContracts = canUseBareCurrentToolContracts; + _canPreserveBareCurrentToolContractsReferences = canPreserveBareCurrentToolContractsReferences; + _canMigrateBareLegacyToolAttribute = canMigrateBareLegacyToolAttribute; + _canMigrateBareLegacyEditorWindowCaptureUtility = + canMigrateBareLegacyEditorWindowCaptureUtility; + _canUseBareCurrentFirstPartyTools = canUseBareCurrentFirstPartyTools; + _shouldQualifyBareEditorWindowCaptureUtilityTimeout = + shouldQualifyBareEditorWindowCaptureUtilityTimeout; + _canMigrateBareLegacyFirstPartyScreenshotApi = canMigrateBareLegacyFirstPartyScreenshotApi; + _canMigrateBareCurrentDomainContractType = canMigrateBareCurrentDomainContractType; + _canMigrateBareLegacyApplicationApi = canMigrateBareLegacyApplicationApi; + _canMigrateBareLegacyApplicationTypeName = canMigrateBareLegacyApplicationTypeName; + _canMigrateAmbiguousBareLegacyToolInfoConstructor = + canMigrateAmbiguousBareLegacyToolInfoConstructor; + _shouldApplyContractRenames = shouldApplyContractRenames; + _shouldApplyRegistrarRenames = shouldApplyRegistrarRenames; + _shouldApplyDomainMetadataRenames = shouldApplyDomainMetadataRenames; + } + + public static CSharpLegacyAssemblyMigrationContext Create( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentApplicationUsing, + bool hasAssemblyScopedCurrentDomainUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] legacyAssemblyToolInfoAliases, + string[] currentApplicationAssemblyAliases, + string[] currentDomainAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + string[] currentApplicationNamespaceAliases = GetCombinedCurrentApplicationNamespaceAliases( + source, + currentApplicationAssemblyAliases); + string[] currentDomainNamespaceAliases = GetCombinedCurrentDomainNamespaceAliases( + source, + currentDomainAssemblyAliases); + string[] currentFirstPartyToolsNamespaceAliases = + GetCombinedCurrentFirstPartyToolsNamespaceAliases( + source, + currentFirstPartyToolsAssemblyAliases); + CSharpLegacyAssemblyMigrationCapabilities capabilities = + CSharpLegacyAssemblyMigrationCapabilities.Create( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentApplicationUsing, + hasAssemblyScopedCurrentDomainUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyNamespaceAliases); + return new CSharpLegacyAssemblyMigrationContext( + source, legacyNamespaceAliases, + legacyAssemblyToolInfoAliases, currentApplicationNamespaceAliases, - canMigrateBareLegacyApplicationApi, - assemblyDeclaredTypeNames); - migratedContent = mainThreadSwitcherMigratedContent; - replacementCount += mainThreadSwitcherReplacementCount; - bool hasMainThreadSwitcherMigrationContext = - mainThreadSwitcherReplacementCount > 0 || - ContainsMigratedMainThreadSwitcherSwitchCall(migratedContent); - if (hasMainThreadSwitcherMigrationContext) + currentDomainNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames, + capabilities.CanUseCurrentToolContracts, + capabilities.CanUseBareCurrentToolContracts, + capabilities.CanPreserveBareCurrentToolContractsReferences, + capabilities.CanMigrateBareLegacyToolAttribute, + capabilities.CanMigrateBareLegacyEditorWindowCaptureUtility, + capabilities.CanUseBareCurrentFirstPartyTools, + capabilities.ShouldQualifyBareEditorWindowCaptureUtilityTimeout, + capabilities.CanMigrateBareLegacyFirstPartyScreenshotApi, + capabilities.CanMigrateBareCurrentDomainContractType, + capabilities.CanMigrateBareLegacyApplicationApi, + capabilities.CanMigrateBareLegacyApplicationTypeName, + capabilities.CanMigrateAmbiguousBareLegacyToolInfoConstructor, + capabilities.ShouldApplyContractRenames, + capabilities.ShouldApplyRegistrarRenames, + capabilities.ShouldApplyDomainMetadataRenames); + } + + public void ApplyToolMetadataReplacements() + { + _migratedContent = ReplaceLegacyToolAttributesInCode( + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyToolAttribute, + ref _replacementCount); + _migratedContent = ReplaceLegacyToolInfoConstructorsInCode( + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyToolAttribute, + _canMigrateAmbiguousBareLegacyToolInfoConstructor, + _legacyAssemblyToolInfoAliases, + ref _replacementCount); + _migratedContent = ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyToolAttribute, + ref _replacementCount); + } + + public void ApplyDelayReplacements() + { + (string editorDelayMigratedContent, int editorDelayReplacementCount) = + ReplaceLegacyEditorDelayFrameCallsInCode( + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyToolAttribute || _canUseCurrentToolContracts, + !_canUseBareCurrentToolContracts); + ApplyReplacementResult(editorDelayMigratedContent, editorDelayReplacementCount); + (string timerDelayMigratedContent, int timerDelayReplacementCount) = + ReplaceLegacyTimerDelayNamedArgumentsInCode( + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyToolAttribute || _canUseCurrentToolContracts); + ApplyReplacementResult(timerDelayMigratedContent, timerDelayReplacementCount); + } + + public void ApplyMainThreadSwitcherReplacements() + { + (string migratedContent, int replacementCount) = + ReplaceLegacyMainThreadSwitcherCallsInCode( + _migratedContent, + _legacyNamespaceAliases, + _currentApplicationNamespaceAliases, + _canMigrateBareLegacyApplicationApi, + _assemblyDeclaredTypeNames); + ApplyReplacementResult(migratedContent, replacementCount); + if (replacementCount == 0 && !ContainsMigratedMainThreadSwitcherSwitchCall(_migratedContent)) + { + return; + } + + RemovePlayerLoopTimingParameters(); + RemoveUnusedPlayerLoopTimingDeclarations(); + } + + public void ApplyEditorWindowCaptureAndTypeReplacements() + { + (string editorWindowCaptureMigratedContent, int editorWindowCaptureReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + _migratedContent, + _legacyNamespaceAliases, + _currentFirstPartyToolsNamespaceAliases, + _canMigrateBareLegacyEditorWindowCaptureUtility, + _shouldQualifyBareEditorWindowCaptureUtilityTimeout, + _canPreserveBareCurrentToolContractsReferences, + _canUseBareCurrentFirstPartyTools, + _assemblyDeclaredTypeNames); + ApplyReplacementResult(editorWindowCaptureMigratedContent, editorWindowCaptureReplacementCount); + _migratedContent = ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( + _migratedContent, + _legacyNamespaceAliases, + _currentFirstPartyToolsNamespaceAliases, + _canMigrateBareLegacyFirstPartyScreenshotApi, + _canPreserveBareCurrentToolContractsReferences, + _assemblyDeclaredTypeNames, + ref _replacementCount); + _migratedContent = ReplaceCurrentDomainContractTypeNamesInCode( + _migratedContent, + _currentDomainNamespaceAliases, + _canMigrateBareCurrentDomainContractType, + _canPreserveBareCurrentToolContractsReferences, + _assemblyDeclaredTypeNames, + ref _replacementCount); + _migratedContent = ReplaceLegacyApplicationTypeNamesInCode( + _migratedContent, + _legacyNamespaceAliases, + _currentApplicationNamespaceAliases, + _canMigrateBareLegacyApplicationTypeName, + _canPreserveBareCurrentToolContractsReferences, + _assemblyDeclaredTypeNames, + ref _replacementCount); + _migratedContent = ReplaceLegacyRegistrarAliasesInCode( + _migratedContent, + _legacyNamespaceAliases, + ref _replacementCount); + } + + public void ApplyContractRenames() + { + if (!_shouldApplyContractRenames) + { + return; + } + + _migratedContent = ReplaceLegacyDomainTypeNamesInCode( + _migratedContent, + _legacyNamespaceAliases, + ref _replacementCount); + _migratedContent = ReplaceLegacyContractTypeNamesInCode( + _migratedContent, + _legacyNamespaceAliases, + ref _replacementCount); + ApplyCSharpReplacementRules(); + } + + public void ApplyRegistrarRenames() + { + if (!_shouldApplyRegistrarRenames && !_shouldApplyDomainMetadataRenames) + { + return; + } + + if (_shouldApplyRegistrarRenames) + { + _migratedContent = ReplaceUnqualifiedLegacyRegistrarReferencesInCode( + _migratedContent, + ref _replacementCount); + } + + ApplyRegistrarReplacementRules(); + _migratedContent = ReplaceLegacyToolInfoTypeReferencesInCode( + _migratedContent, + ref _replacementCount); + } + + public void ApplyCurrentPublicContractNamespaceReplacements() + { + _migratedContent = ReplaceCurrentPublicContractNamespacesInCode( + _migratedContent, + ref _replacementCount); + } + + public ThirdPartyToolMigrationContentResult CreateResult() + { + return new ThirdPartyToolMigrationContentResult( + _migratedContent, + _replacementCount, + _removedPlayerLoopTimingSignatures.ToArray()); + } + + private void RemovePlayerLoopTimingParameters() { string[] migratedCalleeMethodNames = Array.Empty(); while (true) { ( - string playerLoopTimingMigratedContent, - int playerLoopTimingReplacementCount, + string migratedContent, + int replacementCount, RemovedLegacyPlayerLoopTimingSignature[] localRemovedTimingSignatures) = RemoveLegacyPlayerLoopTimingParametersInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyApplicationApi, + _migratedContent, + _legacyNamespaceAliases, + _canMigrateBareLegacyApplicationApi, migratedCalleeMethodNames); - if (playerLoopTimingReplacementCount == 0) + if (replacementCount == 0) { - break; + return; } - migratedContent = playerLoopTimingMigratedContent; - replacementCount += playerLoopTimingReplacementCount; - removedPlayerLoopTimingSignatures.AddRange(localRemovedTimingSignatures); + ApplyReplacementResult(migratedContent, replacementCount); + _removedPlayerLoopTimingSignatures.AddRange(localRemovedTimingSignatures); (string timingCallerMigratedContent, int timingCallerReplacementCount) = RemoveLegacyPlayerLoopTimingCallerArgumentsInCode( - migratedContent, + _migratedContent, localRemovedTimingSignatures, - legacyNamespaceAliases); - migratedContent = timingCallerMigratedContent; - replacementCount += timingCallerReplacementCount; + _legacyNamespaceAliases); + ApplyReplacementResult(timingCallerMigratedContent, timingCallerReplacementCount); migratedCalleeMethodNames = localRemovedTimingSignatures .Select(signature => signature.MethodName) .ToArray(); } - - (string unusedTimingMigratedContent, int unusedTimingReplacementCount) = - RemoveUnusedLegacyPlayerLoopTimingDeclarationsInCode(migratedContent); - migratedContent = unusedTimingMigratedContent; - replacementCount += unusedTimingReplacementCount; } - (string editorWindowCaptureMigratedContent, int editorWindowCaptureReplacementCount) = - ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( - migratedContent, - legacyNamespaceAliases, - currentFirstPartyToolsNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility, - shouldQualifyBareEditorWindowCaptureUtilityTimeout, - canPreserveBareCurrentToolContractsReferences, - canUseBareCurrentFirstPartyTools, - assemblyDeclaredTypeNames); - migratedContent = editorWindowCaptureMigratedContent; - replacementCount += editorWindowCaptureReplacementCount; - migratedContent = ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - currentFirstPartyToolsNamespaceAliases, - canMigrateBareLegacyFirstPartyScreenshotApi, - canPreserveBareCurrentToolContractsReferences, - assemblyDeclaredTypeNames, - ref replacementCount); - migratedContent = ReplaceCurrentDomainContractTypeNamesInCode( - migratedContent, - currentDomainNamespaceAliases, - canMigrateBareCurrentDomainContractType, - canPreserveBareCurrentToolContractsReferences, - assemblyDeclaredTypeNames, - ref replacementCount); - migratedContent = ReplaceLegacyApplicationTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - currentApplicationNamespaceAliases, - canMigrateBareLegacyApplicationTypeName, - canPreserveBareCurrentToolContractsReferences, - assemblyDeclaredTypeNames, - ref replacementCount); - migratedContent = ReplaceLegacyRegistrarAliasesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); - - if (shouldApplyContractRenames) + private void RemoveUnusedPlayerLoopTimingDeclarations() { - migratedContent = ReplaceLegacyDomainTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); - - migratedContent = ReplaceLegacyContractTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); + (string migratedContent, int replacementCount) = + RemoveUnusedLegacyPlayerLoopTimingDeclarationsInCode(_migratedContent); + ApplyReplacementResult(migratedContent, replacementCount); + } + private void ApplyCSharpReplacementRules() + { foreach (ReplacementRule rule in CSharpReplacementRules) { - migratedContent = ReplaceRegexInCode( - migratedContent, + _migratedContent = ReplaceRegexInCode( + _migratedContent, rule.PatternRegex, _ => rule.Replacement, - ref replacementCount); + ref _replacementCount); } } - if (shouldApplyRegistrarRenames || shouldApplyDomainMetadataRenames) + private void ApplyRegistrarReplacementRules() { - if (shouldApplyRegistrarRenames) - { - migratedContent = ReplaceUnqualifiedLegacyRegistrarReferencesInCode( - migratedContent, - ref replacementCount); - } - foreach (ReplacementRule rule in RegistrarReplacementRules) { - migratedContent = ReplaceRegexInCode( - migratedContent, + _migratedContent = ReplaceRegexInCode( + _migratedContent, rule.PatternRegex, _ => rule.Replacement, - ref replacementCount); + ref _replacementCount); } + } + + private void ApplyReplacementResult(string migratedContent, int replacementCount) + { + _migratedContent = migratedContent; + _replacementCount += replacementCount; + } + } - migratedContent = ReplaceLegacyToolInfoTypeReferencesInCode( - migratedContent, - ref replacementCount); + private readonly struct CSharpLegacyAssemblyMigrationCapabilities + { + private CSharpLegacyAssemblyMigrationCapabilities( + bool canUseCurrentToolContracts, + bool canUseBareCurrentToolContracts, + bool canPreserveBareCurrentToolContractsReferences, + bool canMigrateBareLegacyToolAttribute, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool canUseBareCurrentFirstPartyTools, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + bool canMigrateBareCurrentDomainContractType, + bool canMigrateBareLegacyApplicationApi, + bool canMigrateBareLegacyApplicationTypeName, + bool canMigrateAmbiguousBareLegacyToolInfoConstructor, + bool shouldApplyContractRenames, + bool shouldApplyRegistrarRenames, + bool shouldApplyDomainMetadataRenames) + { + CanUseCurrentToolContracts = canUseCurrentToolContracts; + CanUseBareCurrentToolContracts = canUseBareCurrentToolContracts; + CanPreserveBareCurrentToolContractsReferences = canPreserveBareCurrentToolContractsReferences; + CanMigrateBareLegacyToolAttribute = canMigrateBareLegacyToolAttribute; + CanMigrateBareLegacyEditorWindowCaptureUtility = canMigrateBareLegacyEditorWindowCaptureUtility; + CanUseBareCurrentFirstPartyTools = canUseBareCurrentFirstPartyTools; + ShouldQualifyBareEditorWindowCaptureUtilityTimeout = + shouldQualifyBareEditorWindowCaptureUtilityTimeout; + CanMigrateBareLegacyFirstPartyScreenshotApi = canMigrateBareLegacyFirstPartyScreenshotApi; + CanMigrateBareCurrentDomainContractType = canMigrateBareCurrentDomainContractType; + CanMigrateBareLegacyApplicationApi = canMigrateBareLegacyApplicationApi; + CanMigrateBareLegacyApplicationTypeName = canMigrateBareLegacyApplicationTypeName; + CanMigrateAmbiguousBareLegacyToolInfoConstructor = + canMigrateAmbiguousBareLegacyToolInfoConstructor; + ShouldApplyContractRenames = shouldApplyContractRenames; + ShouldApplyRegistrarRenames = shouldApplyRegistrarRenames; + ShouldApplyDomainMetadataRenames = shouldApplyDomainMetadataRenames; } - migratedContent = ReplaceCurrentPublicContractNamespacesInCode( - migratedContent, - ref replacementCount); + public bool CanUseCurrentToolContracts { get; } + public bool CanUseBareCurrentToolContracts { get; } + public bool CanPreserveBareCurrentToolContractsReferences { get; } + public bool CanMigrateBareLegacyToolAttribute { get; } + public bool CanMigrateBareLegacyEditorWindowCaptureUtility { get; } + public bool CanUseBareCurrentFirstPartyTools { get; } + public bool ShouldQualifyBareEditorWindowCaptureUtilityTimeout { get; } + public bool CanMigrateBareLegacyFirstPartyScreenshotApi { get; } + public bool CanMigrateBareCurrentDomainContractType { get; } + public bool CanMigrateBareLegacyApplicationApi { get; } + public bool CanMigrateBareLegacyApplicationTypeName { get; } + public bool CanMigrateAmbiguousBareLegacyToolInfoConstructor { get; } + public bool ShouldApplyContractRenames { get; } + public bool ShouldApplyRegistrarRenames { get; } + public bool ShouldApplyDomainMetadataRenames { get; } - return new ThirdPartyToolMigrationContentResult( - migratedContent, - replacementCount, - removedPlayerLoopTimingSignatures.ToArray()); + public static CSharpLegacyAssemblyMigrationCapabilities Create( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentApplicationUsing, + bool hasAssemblyScopedCurrentDomainUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyNamespaceAliases) + { + CSharpMigrationSourceUsage usage = DetectMigrationSourceUsage(source); + bool canUseCurrentToolContracts = CanUseCurrentToolContractsFromUsage( + usage, + hasAssemblyScopedCurrentToolContractsUsing); + bool canUseBareCurrentToolContracts = CanUseBareCurrentToolContractsFromUsage( + usage, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing); + bool canMigrateBareLegacyToolAttribute = CanMigrateBareLegacyToolAttributeFromUsage( + usage, + hasLegacyAssemblySource, + legacyNamespaceAliases); + bool canMigrateBareLegacyApplicationApi = CanMigrateBareLegacyApplicationApiFromUsage( + usage, + canMigrateBareLegacyToolAttribute, + canUseCurrentToolContracts, + hasAssemblyScopedCurrentApplicationUsing); + bool canMigrateBareLegacyFirstPartyApi = CanMigrateBareLegacyFirstPartyApiFromUsage( + usage, + canMigrateBareLegacyToolAttribute, + canUseCurrentToolContracts, + hasAssemblyScopedCurrentFirstPartyToolsUsing); + bool canUseBareCurrentFirstPartyTools = CanUseBareCurrentFirstPartyToolsFromUsage( + usage, + hasAssemblyScopedCurrentFirstPartyToolsUsing); + bool canMigrateBareCurrentDomainContractType = + CanMigrateBareCurrentDomainContractTypeFromUsage(usage, hasAssemblyScopedCurrentDomainUsing); + bool hasLocalLegacyMarker = ContainsLegacyToolMigrationMarker(source); + bool shouldApplyContractRenames = hasLegacyAssemblySource || hasLocalLegacyMarker; + return new CSharpLegacyAssemblyMigrationCapabilities( + canUseCurrentToolContracts, + canUseBareCurrentToolContracts, + CanPreserveBareCurrentToolContractsReferencesFromUsage( + usage, + hasAssemblyScopedCurrentToolContractsUsing), + canMigrateBareLegacyToolAttribute, + canMigrateBareLegacyFirstPartyApi, + canUseBareCurrentFirstPartyTools, + !canUseBareCurrentToolContracts, + canMigrateBareLegacyFirstPartyApi, + canMigrateBareCurrentDomainContractType, + canMigrateBareLegacyApplicationApi, + canMigrateBareLegacyApplicationApi, + CanMigrateAmbiguousBareLegacyToolInfoConstructorFromUsage( + usage, + canMigrateBareLegacyToolAttribute), + shouldApplyContractRenames, + ShouldApplyRegistrarRenamesFromUsage(source, shouldApplyContractRenames), + ShouldApplyDomainMetadataRenamesFromUsage(source, shouldApplyContractRenames)); + } + + private static CSharpMigrationSourceUsage DetectMigrationSourceUsage(string source) + { + bool hasCurrentDomainUsingDirective = + RegexMatchesCode(source, CurrentDomainUsingRegex) || + RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); + bool hasCurrentFirstPartyToolsUsingDirective = + RegexMatchesCode(source, CurrentFirstPartyToolsUsingRegex) || + RegexMatchesCode(source, CurrentFirstPartyToolsGlobalUsingRegex); + + return new CSharpMigrationSourceUsage( + RegexMatchesCode(source, LegacyNamespaceRegex), + RegexMatchesCode(source, LegacyNamespaceUsingRegex), + RegexMatchesCode(source, CurrentApplicationNamespaceRegex), + RegexMatchesCode(source, CurrentDomainNamespaceRegex), + hasCurrentDomainUsingDirective, + RegexMatchesCode(source, CurrentToolContractsNamespaceRegex), + RegexMatchesCode(source, CurrentToolContractsUsingRegex), + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex), + hasCurrentFirstPartyToolsUsingDirective); + } + + private static bool CanUseCurrentToolContractsFromUsage( + CSharpMigrationSourceUsage usage, + bool hasAssemblyScopedCurrentToolContractsUsing) + { + return usage.HasCurrentToolContractsNamespaceUsage || hasAssemblyScopedCurrentToolContractsUsing; + } + + private static bool CanUseBareCurrentToolContractsFromUsage( + CSharpMigrationSourceUsage usage, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing) + { + return hasLegacyAssemblySource || + usage.HasLegacyNamespaceUsingDirective || + usage.HasCurrentToolContractsUsingDirective || + hasAssemblyScopedCurrentToolContractsUsing; + } + + private static bool CanPreserveBareCurrentToolContractsReferencesFromUsage( + CSharpMigrationSourceUsage usage, + bool hasAssemblyScopedCurrentToolContractsUsing) + { + return usage.HasCurrentToolContractsUsingDirective || hasAssemblyScopedCurrentToolContractsUsing; + } + + private static bool CanMigrateBareLegacyToolAttributeFromUsage( + CSharpMigrationSourceUsage usage, + bool hasLegacyAssemblySource, + string[] legacyNamespaceAliases) + { + return hasLegacyAssemblySource || + usage.HasLegacyNamespaceUsage || + legacyNamespaceAliases.Length > 0; + } + + private static bool CanMigrateBareLegacyApplicationApiFromUsage( + CSharpMigrationSourceUsage usage, + bool canMigrateBareLegacyToolAttribute, + bool canUseCurrentToolContracts, + bool hasAssemblyScopedCurrentApplicationUsing) + { + return canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + hasAssemblyScopedCurrentApplicationUsing || + usage.HasCurrentApplicationNamespaceUsage; + } + + private static bool CanMigrateBareLegacyFirstPartyApiFromUsage( + CSharpMigrationSourceUsage usage, + bool canMigrateBareLegacyToolAttribute, + bool canUseCurrentToolContracts, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing) + { + return canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + hasAssemblyScopedCurrentFirstPartyToolsUsing || + usage.HasCurrentFirstPartyToolsNamespaceUsage; + } + + private static bool CanUseBareCurrentFirstPartyToolsFromUsage( + CSharpMigrationSourceUsage usage, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing) + { + return hasAssemblyScopedCurrentFirstPartyToolsUsing || + usage.HasCurrentFirstPartyToolsUsingDirective; + } + + private static bool CanMigrateBareCurrentDomainContractTypeFromUsage( + CSharpMigrationSourceUsage usage, + bool hasAssemblyScopedCurrentDomainUsing) + { + return hasAssemblyScopedCurrentDomainUsing || usage.HasCurrentDomainUsingDirective; + } + + private static bool CanMigrateAmbiguousBareLegacyToolInfoConstructorFromUsage( + CSharpMigrationSourceUsage usage, + bool canMigrateBareLegacyToolAttribute) + { + return canMigrateBareLegacyToolAttribute && !usage.HasCurrentDomainNamespaceUsage; + } + + private static bool ShouldApplyRegistrarRenamesFromUsage( + string source, + bool shouldApplyContractRenames) + { + return shouldApplyContractRenames && RegexMatchesCode(source, LegacyRegistrarRegex); + } + + private static bool ShouldApplyDomainMetadataRenamesFromUsage( + string source, + bool shouldApplyContractRenames) + { + return shouldApplyContractRenames && RegexMatchesCode(source, LegacyDomainMetadataRegex); + } + + private readonly struct CSharpMigrationSourceUsage + { + public CSharpMigrationSourceUsage( + bool hasLegacyNamespaceUsage, + bool hasLegacyNamespaceUsingDirective, + bool hasCurrentApplicationNamespaceUsage, + bool hasCurrentDomainNamespaceUsage, + bool hasCurrentDomainUsingDirective, + bool hasCurrentToolContractsNamespaceUsage, + bool hasCurrentToolContractsUsingDirective, + bool hasCurrentFirstPartyToolsNamespaceUsage, + bool hasCurrentFirstPartyToolsUsingDirective) + { + HasLegacyNamespaceUsage = hasLegacyNamespaceUsage; + HasLegacyNamespaceUsingDirective = hasLegacyNamespaceUsingDirective; + HasCurrentApplicationNamespaceUsage = hasCurrentApplicationNamespaceUsage; + HasCurrentDomainNamespaceUsage = hasCurrentDomainNamespaceUsage; + HasCurrentDomainUsingDirective = hasCurrentDomainUsingDirective; + HasCurrentToolContractsNamespaceUsage = hasCurrentToolContractsNamespaceUsage; + HasCurrentToolContractsUsingDirective = hasCurrentToolContractsUsingDirective; + HasCurrentFirstPartyToolsNamespaceUsage = hasCurrentFirstPartyToolsNamespaceUsage; + HasCurrentFirstPartyToolsUsingDirective = hasCurrentFirstPartyToolsUsingDirective; + } + + public bool HasLegacyNamespaceUsage { get; } + public bool HasLegacyNamespaceUsingDirective { get; } + public bool HasCurrentApplicationNamespaceUsage { get; } + public bool HasCurrentDomainNamespaceUsage { get; } + public bool HasCurrentDomainUsingDirective { get; } + public bool HasCurrentToolContractsNamespaceUsage { get; } + public bool HasCurrentToolContractsUsingDirective { get; } + public bool HasCurrentFirstPartyToolsNamespaceUsage { get; } + public bool HasCurrentFirstPartyToolsUsingDirective { get; } + } } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs index 27b569bb35..97ed8998db 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs @@ -53,6 +53,24 @@ namespace io.github.hatayama.UnityCliLoop.Infrastructure { internal static class ThirdPartyToolMigrationDetectionRules { + private static readonly string[] MigrationCandidateFragments = + { + LegacyNamespace, + CurrentNamespace, + CurrentApplicationNamespace, + CurrentDomainNamespace, + CurrentFirstPartyToolsNamespace, + "McpTool", + "CustomToolManager", + LegacyEditorDelayTypeName, + LegacyTimerDelayTypeName, + LegacyMainThreadSwitcherTypeName, + LegacyPlayerLoopTimingTypeName, + LegacyEditorWindowCaptureUtilityTypeName, + "UnityCliLoopToolRegistrar", + "ToolInfo" + }; + internal static bool ContainsLegacyCSharpApi(string source) { Debug.Assert(source != null, "source must not be null"); @@ -64,52 +82,29 @@ internal static bool ContainsMigrationCandidateText(string source) { Debug.Assert(source != null, "source must not be null"); - if (ContainsTextFragment(source, LegacyNamespace) || - ContainsTextFragment(source, CurrentNamespace) || - ContainsTextFragment(source, CurrentApplicationNamespace) || - ContainsTextFragment(source, CurrentDomainNamespace) || - ContainsTextFragment(source, CurrentFirstPartyToolsNamespace) || - ContainsTextFragment(source, "McpTool") || - ContainsTextFragment(source, "CustomToolManager") || - ContainsTextFragment(source, LegacyEditorDelayTypeName) || - ContainsTextFragment(source, LegacyTimerDelayTypeName) || - ContainsTextFragment(source, LegacyMainThreadSwitcherTypeName) || - ContainsTextFragment(source, LegacyPlayerLoopTimingTypeName) || - ContainsTextFragment(source, LegacyEditorWindowCaptureUtilityTypeName) || - ContainsTextFragment(source, "UnityCliLoopToolRegistrar") || - ContainsTextFragment(source, "ToolInfo")) - { - return true; - } - - foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) - { - if (ContainsTextFragment(source, rule.LegacyName) || - ContainsTextFragment(source, rule.CurrentName)) - { - return true; - } - } + return ContainsAnyTextFragment(source, MigrationCandidateFragments) || + ContainsAnyReplacementRuleName(source, ToolContractTypeReplacementRules) || + ContainsAnyReplacementRuleName(source, DomainTypeReplacementRules) || + ContainsAnyReplacementRuleName(source, ApplicationTypeReplacementRules) || + ContainsAnyReplacementRuleName(source, FirstPartyScreenshotTypeReplacementRules); + } - foreach (TypeReplacementRule rule in DomainTypeReplacementRules) + private static bool ContainsAnyTextFragment(string source, string[] fragments) + { + foreach (string fragment in fragments) { - if (ContainsTextFragment(source, rule.LegacyName) || - ContainsTextFragment(source, rule.CurrentName)) + if (ContainsTextFragment(source, fragment)) { return true; } } - foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) - { - if (ContainsTextFragment(source, rule.LegacyName) || - ContainsTextFragment(source, rule.CurrentName)) - { - return true; - } - } + return false; + } - foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + private static bool ContainsAnyReplacementRuleName(string source, TypeReplacementRule[] rules) + { + foreach (TypeReplacementRule rule in rules) { if (ContainsTextFragment(source, rule.LegacyName) || ContainsTextFragment(source, rule.CurrentName)) diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs index f228ade261..ec23f52972 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs @@ -348,6 +348,7 @@ internal static async Task CollectFastFirstPartyScreenshotRequirementsAsyn List assemblyReferenceDirectories, string projectRoot, HashSet legacyAssemblyDirectories, + Dictionary> assemblyScopedLegacyAliasesByDirectory, HashSet assemblyScopedCurrentToolContractsDirectories, HashSet assemblyScopedCurrentFirstPartyToolsDirectories, Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, @@ -363,6 +364,9 @@ internal static async Task CollectFastFirstPartyScreenshotRequirementsAsyn "assemblyReferenceDirectories must not be null"); Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); Debug.Assert(legacyAssemblyDirectories != null, "legacyAssemblyDirectories must not be null"); + Debug.Assert( + assemblyScopedLegacyAliasesByDirectory != null, + "assemblyScopedLegacyAliasesByDirectory must not be null"); Debug.Assert( assemblyScopedCurrentToolContractsDirectories != null, "assemblyScopedCurrentToolContractsDirectories must not be null"); @@ -407,83 +411,210 @@ internal static async Task CollectFastFirstPartyScreenshotRequirementsAsyn asmdefDirectories, assemblyReferenceDirectories, projectRoot); - string[] assemblyDeclaredTypeNames = - GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); - string[] currentFirstPartyToolsAssemblyAliases = - GetAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory); - bool hasLegacyEditorWindowCaptureUtilitySourceTarget = - ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( - source, - legacyAssemblyDirectories.Contains(assemblyDirectory), - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - Array.Empty(), - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasLegacyScreenshotSourceTarget = - ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - legacyAssemblyDirectories.Contains(assemblyDirectory) || - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty(), - assemblyDeclaredTypeNames) || - hasLegacyEditorWindowCaptureUtilitySourceTarget; - bool hasCurrentScreenshotReferenceRequirement = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentFirstPartyToolsContractSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotContractApiForAssembly( - source, - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - bool hasCurrentRenderingCaptureSourceTarget = - ThirdPartyToolMigrationRules.ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( - source, - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - if (hasLegacyScreenshotSourceTarget || - hasCurrentFirstPartyToolsContractSourceTarget || - hasCurrentScreenshotReferenceRequirement) + bool foundMigrationTarget = CollectFastFirstPartyScreenshotRequirementsForSource( + source, + assemblyDirectory, + legacyAssemblyDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + toolContractsReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + if (foundMigrationTarget) { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + return true; } - if (hasCurrentRenderingCaptureSourceTarget || - hasCurrentScreenshotReferenceRequirement) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } + } - if (ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( - source, - legacyAssemblyDirectories.Contains(assemblyDirectory), - assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - Array.Empty(), - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } + return false; + } - if (hasLegacyScreenshotSourceTarget || - hasCurrentRenderingCaptureSourceTarget || - hasCurrentFirstPartyToolsContractSourceTarget) - { - return true; - } + private static bool CollectFastFirstPartyScreenshotRequirementsForSource( + string source, + string assemblyDirectory, + HashSet legacyAssemblyDirectories, + Dictionary> assemblyScopedLegacyAliasesByDirectory, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet firstPartyScreenshotReferenceAssemblyDirectories) + { + string[] assemblyDeclaredTypeNames = + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); + string[] legacyAssemblyAliases = + GetAssemblyScopedNames(assemblyScopedLegacyAliasesByDirectory, assemblyDirectory); + string[] currentFirstPartyToolsAssemblyAliases = + GetAssemblyScopedNames(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, assemblyDirectory); + FirstPartyScreenshotRequirementScan scan = ScanFastFirstPartyScreenshotRequirement( + source, + assemblyDirectory, + legacyAssemblyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + + if (scan.RequiresToolContractsReference) + { + toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + if (scan.RequiresFirstPartyScreenshotReference) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); } - return false; + return scan.HasMigrationTarget; + } + + private static FirstPartyScreenshotRequirementScan ScanFastFirstPartyScreenshotRequirement( + string source, + string assemblyDirectory, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + bool hasLegacyEditorWindowCaptureUtilitySourceTarget = + HasLegacyEditorWindowCaptureUtilitySourceTarget( + source, + assemblyDirectory, + legacyAssemblyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + bool hasLegacyScreenshotSourceTarget = HasLegacyScreenshotSourceTarget( + source, + assemblyDirectory, + legacyAssemblyDirectories, + assemblyScopedCurrentToolContractsDirectories, + legacyAssemblyAliases, + assemblyDeclaredTypeNames, + hasLegacyEditorWindowCaptureUtilitySourceTarget); + bool hasCurrentScreenshotReferenceRequirement = + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + bool hasCurrentFirstPartyToolsContractSourceTarget = + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotContractApiForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + bool hasCurrentRenderingCaptureSourceTarget = + ThirdPartyToolMigrationRules.ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + bool hasTimeoutMigration = HasLegacyEditorWindowCaptureUtilityTimeoutMigration( + source, + assemblyDirectory, + legacyAssemblyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + return new FirstPartyScreenshotRequirementScan( + hasLegacyScreenshotSourceTarget || + hasCurrentFirstPartyToolsContractSourceTarget || + hasCurrentScreenshotReferenceRequirement || + hasTimeoutMigration, + hasCurrentRenderingCaptureSourceTarget || + hasCurrentScreenshotReferenceRequirement, + hasLegacyScreenshotSourceTarget || + hasCurrentRenderingCaptureSourceTarget || + hasCurrentFirstPartyToolsContractSourceTarget || + hasTimeoutMigration); + } + + private static bool HasLegacyEditorWindowCaptureUtilitySourceTarget( + string source, + string assemblyDirectory, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + } + + private static bool HasLegacyScreenshotSourceTarget( + string source, + string assemblyDirectory, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames, + bool hasLegacyEditorWindowCaptureUtilitySourceTarget) + { + return ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory) || + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + legacyAssemblyAliases, + assemblyDeclaredTypeNames) || + hasLegacyEditorWindowCaptureUtilitySourceTarget; + } + + private static bool HasLegacyEditorWindowCaptureUtilityTimeoutMigration( + string source, + string assemblyDirectory, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + } + + private readonly struct FirstPartyScreenshotRequirementScan + { + public FirstPartyScreenshotRequirementScan( + bool requiresToolContractsReference, + bool requiresFirstPartyScreenshotReference, + bool hasMigrationTarget) + { + RequiresToolContractsReference = requiresToolContractsReference; + RequiresFirstPartyScreenshotReference = requiresFirstPartyScreenshotReference; + HasMigrationTarget = hasMigrationTarget; + } + + public bool RequiresToolContractsReference { get; } + public bool RequiresFirstPartyScreenshotReference { get; } + public bool HasMigrationTarget { get; } } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs index ebaca4997b..bb84656305 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs @@ -310,96 +310,64 @@ public MigrationAssemblyUsage( HashSet domainReferenceAssemblyDirectories, HashSet firstPartyScreenshotReferenceAssemblyDirectories) { - Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); - Debug.Assert( - assemblyReferenceDirectories != null, - "assemblyReferenceDirectories must not be null"); - Debug.Assert(legacyAssemblyDirectories != null, "legacyAssemblyDirectories must not be null"); - Debug.Assert( - assemblyScopedLegacyDirectories != null, - "assemblyScopedLegacyDirectories must not be null"); - Debug.Assert( - assemblyScopedCurrentToolContractsDirectories != null, - "assemblyScopedCurrentToolContractsDirectories must not be null"); - Debug.Assert( - assemblyScopedCurrentApplicationDirectories != null, - "assemblyScopedCurrentApplicationDirectories must not be null"); - Debug.Assert( - assemblyScopedCurrentDomainDirectories != null, - "assemblyScopedCurrentDomainDirectories must not be null"); - Debug.Assert( - assemblyScopedCurrentFirstPartyToolsDirectories != null, - "assemblyScopedCurrentFirstPartyToolsDirectories must not be null"); - Debug.Assert( - assemblyScopedLegacyAliasesByDirectory != null, - "assemblyScopedLegacyAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedLegacyToolInfoAliasesByDirectory != null, - "assemblyScopedLegacyToolInfoAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedCurrentApplicationAliasesByDirectory != null, - "assemblyScopedCurrentApplicationAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedCurrentDomainAliasesByDirectory != null, - "assemblyScopedCurrentDomainAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory != null, - "assemblyScopedCurrentFirstPartyToolsAliasesByDirectory must not be null"); - Debug.Assert( - assemblyDeclaredTypeNamesByDirectory != null, - "assemblyDeclaredTypeNamesByDirectory must not be null"); - Debug.Assert( - toolContractsReferenceAssemblyDirectories != null, - "toolContractsReferenceAssemblyDirectories must not be null"); - Debug.Assert( - applicationReferenceAssemblyDirectories != null, - "applicationReferenceAssemblyDirectories must not be null"); - Debug.Assert( - domainReferenceAssemblyDirectories != null, - "domainReferenceAssemblyDirectories must not be null"); - Debug.Assert( - firstPartyScreenshotReferenceAssemblyDirectories != null, - "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); - - AsmdefDirectories = asmdefDirectories ?? - throw new ArgumentNullException(nameof(asmdefDirectories)); - AssemblyReferenceDirectories = assemblyReferenceDirectories ?? - throw new ArgumentNullException(nameof(assemblyReferenceDirectories)); - LegacyAssemblyDirectories = legacyAssemblyDirectories ?? - throw new ArgumentNullException(nameof(legacyAssemblyDirectories)); - AssemblyScopedLegacyDirectories = assemblyScopedLegacyDirectories ?? - throw new ArgumentNullException(nameof(assemblyScopedLegacyDirectories)); - AssemblyScopedCurrentToolContractsDirectories = assemblyScopedCurrentToolContractsDirectories ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentToolContractsDirectories)); - AssemblyScopedCurrentApplicationDirectories = assemblyScopedCurrentApplicationDirectories ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentApplicationDirectories)); - AssemblyScopedCurrentDomainDirectories = assemblyScopedCurrentDomainDirectories ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentDomainDirectories)); - AssemblyScopedCurrentFirstPartyToolsDirectories = assemblyScopedCurrentFirstPartyToolsDirectories ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentFirstPartyToolsDirectories)); - AssemblyScopedLegacyAliasesByDirectory = assemblyScopedLegacyAliasesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyScopedLegacyAliasesByDirectory)); - AssemblyScopedLegacyToolInfoAliasesByDirectory = assemblyScopedLegacyToolInfoAliasesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyScopedLegacyToolInfoAliasesByDirectory)); - AssemblyScopedCurrentApplicationAliasesByDirectory = - assemblyScopedCurrentApplicationAliasesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentApplicationAliasesByDirectory)); - AssemblyScopedCurrentDomainAliasesByDirectory = - assemblyScopedCurrentDomainAliasesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentDomainAliasesByDirectory)); - AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory = - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory)); - AssemblyDeclaredTypeNamesByDirectory = assemblyDeclaredTypeNamesByDirectory ?? - throw new ArgumentNullException(nameof(assemblyDeclaredTypeNamesByDirectory)); - ToolContractsReferenceAssemblyDirectories = toolContractsReferenceAssemblyDirectories ?? - throw new ArgumentNullException(nameof(toolContractsReferenceAssemblyDirectories)); - ApplicationReferenceAssemblyDirectories = applicationReferenceAssemblyDirectories ?? - throw new ArgumentNullException(nameof(applicationReferenceAssemblyDirectories)); - DomainReferenceAssemblyDirectories = domainReferenceAssemblyDirectories ?? - throw new ArgumentNullException(nameof(domainReferenceAssemblyDirectories)); - FirstPartyScreenshotReferenceAssemblyDirectories = firstPartyScreenshotReferenceAssemblyDirectories ?? - throw new ArgumentNullException(nameof(firstPartyScreenshotReferenceAssemblyDirectories)); + AsmdefDirectories = RequireNotNull(asmdefDirectories, nameof(asmdefDirectories)); + AssemblyReferenceDirectories = + RequireNotNull(assemblyReferenceDirectories, nameof(assemblyReferenceDirectories)); + LegacyAssemblyDirectories = + RequireNotNull(legacyAssemblyDirectories, nameof(legacyAssemblyDirectories)); + AssemblyScopedLegacyDirectories = + RequireNotNull(assemblyScopedLegacyDirectories, nameof(assemblyScopedLegacyDirectories)); + AssemblyScopedCurrentToolContractsDirectories = RequireNotNull( + assemblyScopedCurrentToolContractsDirectories, + nameof(assemblyScopedCurrentToolContractsDirectories)); + AssemblyScopedCurrentApplicationDirectories = RequireNotNull( + assemblyScopedCurrentApplicationDirectories, + nameof(assemblyScopedCurrentApplicationDirectories)); + AssemblyScopedCurrentDomainDirectories = RequireNotNull( + assemblyScopedCurrentDomainDirectories, + nameof(assemblyScopedCurrentDomainDirectories)); + AssemblyScopedCurrentFirstPartyToolsDirectories = RequireNotNull( + assemblyScopedCurrentFirstPartyToolsDirectories, + nameof(assemblyScopedCurrentFirstPartyToolsDirectories)); + AssemblyScopedLegacyAliasesByDirectory = RequireNotNull( + assemblyScopedLegacyAliasesByDirectory, + nameof(assemblyScopedLegacyAliasesByDirectory)); + AssemblyScopedLegacyToolInfoAliasesByDirectory = RequireNotNull( + assemblyScopedLegacyToolInfoAliasesByDirectory, + nameof(assemblyScopedLegacyToolInfoAliasesByDirectory)); + AssemblyScopedCurrentApplicationAliasesByDirectory = RequireNotNull( + assemblyScopedCurrentApplicationAliasesByDirectory, + nameof(assemblyScopedCurrentApplicationAliasesByDirectory)); + AssemblyScopedCurrentDomainAliasesByDirectory = RequireNotNull( + assemblyScopedCurrentDomainAliasesByDirectory, + nameof(assemblyScopedCurrentDomainAliasesByDirectory)); + AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory = RequireNotNull( + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + nameof(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory)); + AssemblyDeclaredTypeNamesByDirectory = + RequireNotNull(assemblyDeclaredTypeNamesByDirectory, nameof(assemblyDeclaredTypeNamesByDirectory)); + ToolContractsReferenceAssemblyDirectories = RequireNotNull( + toolContractsReferenceAssemblyDirectories, + nameof(toolContractsReferenceAssemblyDirectories)); + ApplicationReferenceAssemblyDirectories = + RequireNotNull(applicationReferenceAssemblyDirectories, nameof(applicationReferenceAssemblyDirectories)); + DomainReferenceAssemblyDirectories = + RequireNotNull(domainReferenceAssemblyDirectories, nameof(domainReferenceAssemblyDirectories)); + FirstPartyScreenshotReferenceAssemblyDirectories = RequireNotNull( + firstPartyScreenshotReferenceAssemblyDirectories, + nameof(firstPartyScreenshotReferenceAssemblyDirectories)); + } + + private static T RequireNotNull(T value, string parameterName) + where T : class + { + Debug.Assert(value != null, $"{parameterName} must not be null"); + if (value == null) + { + throw new ArgumentNullException(parameterName); + } + + return value; } public List AsmdefDirectories { get; } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs index 580b9ec1c1..917e9b7227 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs @@ -34,158 +34,32 @@ internal static MigrationPlan Create(string projectRoot) inventory.CSharpFilePaths, inventory.AsmdefFilePaths, inventory.AsmrefFilePaths); - List changes = new(); - int replacementCount = 0; + MigrationPlanAccumulator accumulator = new(); string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); - Dictionary> - removedPlayerLoopTimingSignaturesByAssemblyDirectory = new(StringComparer.Ordinal); - - foreach (string csharpFilePath in inventory.CSharpFilePaths) - { - string source = ThirdPartyToolMigrationFileAccess.ReadAllText(csharpFilePath); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source) && - !ThirdPartyToolMigrationRules.ContainsLegacyTypeAliasReference(source, legacyToolInfoAliases)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - assemblyUsage.AsmdefDirectories, - assemblyUsage.AssemblyReferenceDirectories, - projectRoot); - string[] legacyAssemblyAliases; - if (!assemblyUsage.AssemblyScopedLegacyAliasesByDirectory.TryGetValue( - assemblyDirectory, - out legacyAssemblyAliases)) - { - legacyAssemblyAliases = Array.Empty(); - } - string[] legacyAssemblyToolInfoAliases; - if (!assemblyUsage.AssemblyScopedLegacyToolInfoAliasesByDirectory.TryGetValue( - assemblyDirectory, - out legacyAssemblyToolInfoAliases)) - { - legacyAssemblyToolInfoAliases = Array.Empty(); - } - string[] currentApplicationAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentApplicationAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentApplicationAssemblyAliases)) - { - currentApplicationAssemblyAliases = Array.Empty(); - } - string[] currentDomainAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentDomainAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentDomainAssemblyAliases)) - { - currentDomainAssemblyAliases = Array.Empty(); - } - string[] currentFirstPartyToolsAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentFirstPartyToolsAssemblyAliases)) - { - currentFirstPartyToolsAssemblyAliases = Array.Empty(); - } - string[] assemblyDeclaredTypeNames; - if (!assemblyUsage.AssemblyDeclaredTypeNamesByDirectory.TryGetValue( - assemblyDirectory, - out assemblyDeclaredTypeNames)) - { - assemblyDeclaredTypeNames = Array.Empty(); - } - bool hasAssemblyScopedCurrentToolContractsUsing = - assemblyUsage.AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentApplicationUsing = - assemblyUsage.AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentDomainUsing = - assemblyUsage.AssemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentFirstPartyToolsUsing = - assemblyUsage.AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); - bool hasLegacyAssemblySource = - assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( - source, - hasLegacyAssemblySource, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentApplicationUsing, - hasAssemblyScopedCurrentDomainUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - legacyAssemblyToolInfoAliases, - currentApplicationAssemblyAliases, - currentDomainAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); - AddRemovedPlayerLoopTimingSignatures( - removedPlayerLoopTimingSignaturesByAssemblyDirectory, - assemblyDirectory, - result.RemovedPlayerLoopTimingSignatures); - } - - replacementCount += ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + ProcessCSharpMigrationFiles( inventory.CSharpFilePaths, projectRoot, assemblyUsage, - changes, - removedPlayerLoopTimingSignaturesByAssemblyDirectory, - ThirdPartyToolMigrationFileAccess.ReadAllText); - - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) - { - bool hasLegacyCSharpSource; - bool requiresToolContractsReference; - bool requiresApplicationReference; - bool requiresDomainReference; - bool requiresFirstPartyScreenshotReference; - bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( - asmdefFilePath, - projectRoot, - assemblyUsage, - out hasLegacyCSharpSource, - out requiresToolContractsReference, - out requiresApplicationReference, - out requiresDomainReference, - out requiresFirstPartyScreenshotReference); - string source = ThirdPartyToolMigrationFileAccess.ReadAllText(asmdefFilePath); - if (!hasAssemblyMigrationRequirement && - !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) - { - continue; - } + legacyToolInfoAliases, + ThirdPartyToolMigrationFileAccess.ReadAllText, + accumulator); - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateAsmdefSource( - source, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference, - requiresFirstPartyScreenshotReference); - if (!result.Changed) - { - continue; - } + accumulator.AddReplacementCount(ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + inventory.CSharpFilePaths, + projectRoot, + assemblyUsage, + accumulator.Changes, + accumulator.RemovedPlayerLoopTimingSignaturesByAssemblyDirectory, + ThirdPartyToolMigrationFileAccess.ReadAllText)); - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); - } + ProcessAsmdefMigrationFiles( + inventory.AsmdefFilePaths, + projectRoot, + assemblyUsage, + ThirdPartyToolMigrationFileAccess.ReadAllText, + accumulator); - return new MigrationPlan( - changes, - replacementCount, - projectFingerprint); + return accumulator.ToMigrationPlan(projectFingerprint); } internal static async Task CreateAsync( @@ -224,11 +98,8 @@ internal static async Task CreateAsync( return MigrationPlan.Empty; } - List changes = new(); - int replacementCount = 0; + MigrationPlanAccumulator accumulator = new(); string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); - Dictionary> - removedPlayerLoopTimingSignaturesByAssemblyDirectory = new(StringComparer.Ordinal); foreach (string csharpFilePath in inventory.CSharpFilePaths) { @@ -239,95 +110,13 @@ internal static async Task CreateAsync( string source = sourceFileCache.ReadAllText(csharpFilePath); await progressCounter.ReportProcessedItemAsync(ct); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source) && - !ThirdPartyToolMigrationRules.ContainsLegacyTypeAliasReference(source, legacyToolInfoAliases)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( + CSharpMigrationFileResult? csharpResult = CreateCSharpMigrationFileResult( csharpFilePath, - assemblyUsage.AsmdefDirectories, - assemblyUsage.AssemblyReferenceDirectories, - projectRoot); - string[] legacyAssemblyAliases; - if (!assemblyUsage.AssemblyScopedLegacyAliasesByDirectory.TryGetValue( - assemblyDirectory, - out legacyAssemblyAliases)) - { - legacyAssemblyAliases = Array.Empty(); - } - string[] legacyAssemblyToolInfoAliases; - if (!assemblyUsage.AssemblyScopedLegacyToolInfoAliasesByDirectory.TryGetValue( - assemblyDirectory, - out legacyAssemblyToolInfoAliases)) - { - legacyAssemblyToolInfoAliases = Array.Empty(); - } - string[] currentApplicationAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentApplicationAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentApplicationAssemblyAliases)) - { - currentApplicationAssemblyAliases = Array.Empty(); - } - string[] currentDomainAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentDomainAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentDomainAssemblyAliases)) - { - currentDomainAssemblyAliases = Array.Empty(); - } - string[] currentFirstPartyToolsAssemblyAliases; - if (!assemblyUsage.AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory.TryGetValue( - assemblyDirectory, - out currentFirstPartyToolsAssemblyAliases)) - { - currentFirstPartyToolsAssemblyAliases = Array.Empty(); - } - string[] assemblyDeclaredTypeNames; - if (!assemblyUsage.AssemblyDeclaredTypeNamesByDirectory.TryGetValue( - assemblyDirectory, - out assemblyDeclaredTypeNames)) - { - assemblyDeclaredTypeNames = Array.Empty(); - } - bool hasAssemblyScopedCurrentToolContractsUsing = - assemblyUsage.AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentApplicationUsing = - assemblyUsage.AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentDomainUsing = - assemblyUsage.AssemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory); - bool hasAssemblyScopedCurrentFirstPartyToolsUsing = - assemblyUsage.AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); - bool hasLegacyAssemblySource = - assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( - source, - hasLegacyAssemblySource, - hasAssemblyScopedCurrentToolContractsUsing, - hasAssemblyScopedCurrentApplicationUsing, - hasAssemblyScopedCurrentDomainUsing, - hasAssemblyScopedCurrentFirstPartyToolsUsing, - legacyAssemblyAliases, - legacyAssemblyToolInfoAliases, - currentApplicationAssemblyAliases, - currentDomainAssemblyAliases, - currentFirstPartyToolsAssemblyAliases, - assemblyDeclaredTypeNames); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); - AddRemovedPlayerLoopTimingSignatures( - removedPlayerLoopTimingSignaturesByAssemblyDirectory, - assemblyDirectory, - result.RemovedPlayerLoopTimingSignatures); + source, + projectRoot, + assemblyUsage, + legacyToolInfoAliases); + accumulator.AddCSharpResult(csharpResult); } if (ct.IsCancellationRequested) @@ -335,13 +124,13 @@ internal static async Task CreateAsync( return MigrationPlan.Empty; } - replacementCount += ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + accumulator.AddReplacementCount(ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( inventory.CSharpFilePaths, projectRoot, assemblyUsage, - changes, - removedPlayerLoopTimingSignaturesByAssemblyDirectory, - sourceFileCache.ReadAllText); + accumulator.Changes, + accumulator.RemovedPlayerLoopTimingSignaturesByAssemblyDirectory, + sourceFileCache.ReadAllText)); foreach (string asmdefFilePath in inventory.AsmdefFilePaths) { @@ -350,52 +139,165 @@ internal static async Task CreateAsync( return MigrationPlan.Empty; } - bool hasLegacyCSharpSource; - bool requiresToolContractsReference; - bool requiresApplicationReference; - bool requiresDomainReference; - bool requiresFirstPartyScreenshotReference; - bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( - asmdefFilePath, - projectRoot, - assemblyUsage, - out hasLegacyCSharpSource, - out requiresToolContractsReference, - out requiresApplicationReference, - out requiresDomainReference, - out requiresFirstPartyScreenshotReference); string source = sourceFileCache.ReadAllText(asmdefFilePath); await progressCounter.ReportProcessedItemAsync(ct); - if (!hasAssemblyMigrationRequirement && - !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) - { - continue; - } + MigrationFileResult? asmdefResult = CreateAsmdefMigrationFileResult( + asmdefFilePath, + source, + projectRoot, + assemblyUsage); + accumulator.AddFileResult(asmdefResult); + } - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateAsmdefSource( - source, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference, - requiresFirstPartyScreenshotReference); - if (!result.Changed) - { - continue; - } + progressCounter.ReportComplete(); + return accumulator.ToMigrationPlan(projectFingerprint); + } - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); + private static void ProcessCSharpMigrationFiles( + List csharpFilePaths, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + string[] legacyToolInfoAliases, + Func readAllText, + MigrationPlanAccumulator accumulator) + { + foreach (string csharpFilePath in csharpFilePaths) + { + string source = readAllText(csharpFilePath); + CSharpMigrationFileResult? result = CreateCSharpMigrationFileResult( + csharpFilePath, + source, + projectRoot, + assemblyUsage, + legacyToolInfoAliases); + accumulator.AddCSharpResult(result); } + } - progressCounter.ReportComplete(); - return new MigrationPlan( - changes, - replacementCount, - projectFingerprint); + private static CSharpMigrationFileResult? CreateCSharpMigrationFileResult( + string csharpFilePath, + string source, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + string[] legacyToolInfoAliases) + { + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source) && + !ThirdPartyToolMigrationRules.ContainsLegacyTypeAliasReference(source, legacyToolInfoAliases)) + { + return null; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + assemblyUsage.AsmdefDirectories, + assemblyUsage.AssemblyReferenceDirectories, + projectRoot); + ThirdPartyToolMigrationContentResult result = MigrateCSharpFileSource( + source, + assemblyDirectory, + assemblyUsage); + return result.Changed + ? new CSharpMigrationFileResult(csharpFilePath, assemblyDirectory, result) + : null; } + private static ThirdPartyToolMigrationContentResult MigrateCSharpFileSource( + string source, + string assemblyDirectory, + MigrationAssemblyUsage assemblyUsage) + { + string[] legacyAssemblyAliases = + GetStringArrayFromDirectoryMap(assemblyUsage.AssemblyScopedLegacyAliasesByDirectory, assemblyDirectory); + bool hasLegacyAssemblySource = + assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); + + return ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + assemblyUsage.AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyUsage.AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + assemblyUsage.AssemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyUsage.AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases, + GetStringArrayFromDirectoryMap( + assemblyUsage.AssemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyDirectory), + GetStringArrayFromDirectoryMap( + assemblyUsage.AssemblyScopedCurrentApplicationAliasesByDirectory, + assemblyDirectory), + GetStringArrayFromDirectoryMap( + assemblyUsage.AssemblyScopedCurrentDomainAliasesByDirectory, + assemblyDirectory), + GetStringArrayFromDirectoryMap( + assemblyUsage.AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDirectory), + GetStringArrayFromDirectoryMap( + assemblyUsage.AssemblyDeclaredTypeNamesByDirectory, + assemblyDirectory)); + } + + private static string[] GetStringArrayFromDirectoryMap( + Dictionary namesByDirectory, + string assemblyDirectory) + { + return namesByDirectory.TryGetValue(assemblyDirectory, out string[] names) + ? names + : Array.Empty(); + } + + private static void ProcessAsmdefMigrationFiles( + List asmdefFilePaths, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + Func readAllText, + MigrationPlanAccumulator accumulator) + { + foreach (string asmdefFilePath in asmdefFilePaths) + { + string source = readAllText(asmdefFilePath); + MigrationFileResult? result = CreateAsmdefMigrationFileResult( + asmdefFilePath, + source, + projectRoot, + assemblyUsage); + accumulator.AddFileResult(result); + } + } + + private static MigrationFileResult? CreateAsmdefMigrationFileResult( + string asmdefFilePath, + string source, + string projectRoot, + MigrationAssemblyUsage assemblyUsage) + { + bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( + asmdefFilePath, + projectRoot, + assemblyUsage, + out bool hasLegacyCSharpSource, + out bool requiresToolContractsReference, + out bool requiresApplicationReference, + out bool requiresDomainReference, + out bool requiresFirstPartyScreenshotReference); + if (!hasAssemblyMigrationRequirement && + !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) + { + return null; + } + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + return result.Changed + ? new MigrationFileResult(asmdefFilePath, result.Content, result.ReplacementCount) + : null; + } internal static int GetPreviewWorkItemCount(ProjectFileInventory inventory) { @@ -405,5 +307,82 @@ internal static int GetPreviewWorkItemCount(ProjectFileInventory inventory) (inventory.AsmdefFilePaths.Count * 2) + inventory.AsmrefFilePaths.Count; } + + private sealed class MigrationPlanAccumulator + { + public List Changes { get; } = new(); + + public Dictionary> + RemovedPlayerLoopTimingSignaturesByAssemblyDirectory { get; } = new(StringComparer.Ordinal); + + private int ReplacementCount { get; set; } + + public void AddCSharpResult(CSharpMigrationFileResult? result) + { + if (!result.HasValue) + { + return; + } + + AddReplacementCount(result.Value.Result.ReplacementCount); + Changes.Add(new MigrationFileChange(result.Value.FilePath, result.Value.Result.Content)); + AddRemovedPlayerLoopTimingSignatures( + RemovedPlayerLoopTimingSignaturesByAssemblyDirectory, + result.Value.AssemblyDirectory, + result.Value.Result.RemovedPlayerLoopTimingSignatures); + } + + public void AddFileResult(MigrationFileResult? result) + { + if (!result.HasValue) + { + return; + } + + AddReplacementCount(result.Value.ReplacementCount); + Changes.Add(new MigrationFileChange(result.Value.FilePath, result.Value.Content)); + } + + public void AddReplacementCount(int replacementCount) + { + ReplacementCount += replacementCount; + } + + public MigrationPlan ToMigrationPlan(MigrationProjectFingerprint projectFingerprint) + { + return new MigrationPlan(Changes, ReplacementCount, projectFingerprint); + } + } + + private readonly struct CSharpMigrationFileResult + { + public CSharpMigrationFileResult( + string filePath, + string assemblyDirectory, + ThirdPartyToolMigrationContentResult result) + { + FilePath = filePath; + AssemblyDirectory = assemblyDirectory; + Result = result; + } + + public string FilePath { get; } + public string AssemblyDirectory { get; } + public ThirdPartyToolMigrationContentResult Result { get; } + } + + private readonly struct MigrationFileResult + { + public MigrationFileResult(string filePath, string content, int replacementCount) + { + FilePath = filePath; + Content = content; + ReplacementCount = replacementCount; + } + + public string FilePath { get; } + public string Content { get; } + public int ReplacementCount { get; } + } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs index b09b1af6b9..34a328130d 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs @@ -9,7 +9,6 @@ using io.github.hatayama.UnityCliLoop.Domain; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyReferenceResolver; -using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyScopedNameMap; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefMigrationRequirementResolver; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationFastAssemblyRequirementCollector; using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationFastSourceTargetDetector; @@ -46,6 +45,71 @@ internal static async Task HasMigrationTargetAsync(string projectRoot, Can return false; } + ThirdPartyToolMigrationAssemblyUsageScanState scanState = + CreateScanState(projectRoot, inventory); + (bool hasInitialCSharpTarget, int inspectedEntryCount) = + await ScanInitialCSharpSourcesAsync(inventory, scanState, ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasInitialCSharpTarget) + { + return true; + } + + bool hasCSharpSourceTarget = + await ContainsFastCSharpSourceTargetAsync(inventory, scanState, projectRoot, ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasCSharpSourceTarget) + { + return true; + } + + bool hasReferenceSourceTarget = + await CollectCSharpReferenceRequirementsAsync(inventory, scanState, projectRoot, ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasReferenceSourceTarget) + { + return true; + } + + bool hasAsmdefSourceTarget = await ContainsFastAsmdefSourceTargetAsync( + inventory, + inspectedEntryCount, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasAsmdefSourceTarget) + { + return true; + } + + if (!scanState.HasReferenceRequirements) + { + return false; + } + + MigrationAssemblyUsage assemblyUsage = scanState.CreateReferenceRequirementUsage(); + return ContainsAsmdefReferenceTarget(inventory, projectRoot, assemblyUsage, ct); + } + + private static ThirdPartyToolMigrationAssemblyUsageScanState CreateScanState( + string projectRoot, + ProjectFileInventory inventory) + { List asmdefDirectories = inventory.AsmdefFilePaths .Select(path => Path.GetDirectoryName(path) ?? string.Empty) .Where(path => !string.IsNullOrEmpty(path)) @@ -54,265 +118,170 @@ internal static async Task HasMigrationTargetAsync(string projectRoot, Can List assemblyReferenceDirectories = inventory.AsmrefFilePaths.Count == 0 ? new List() : CreateAssemblyReferenceDirectories(inventory.AsmdefFilePaths, inventory.AsmrefFilePaths); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentToolContractsDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentApplicationDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentDomainAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory = - new(StringComparer.Ordinal); - HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyDeclaredTypeNamesByDirectory = - new(StringComparer.Ordinal); + return new ThirdPartyToolMigrationAssemblyUsageScanState( + projectRoot, + asmdefDirectories, + assemblyReferenceDirectories); + } + + private static async Task<(bool hasTarget, int inspectedEntryCount)> ScanInitialCSharpSourcesAsync( + ProjectFileInventory inventory, + ThirdPartyToolMigrationAssemblyUsageScanState scanState, + CancellationToken ct) + { int inspectedEntryCount = 0; foreach (string csharpFilePath in inventory.CSharpFilePaths) { if (ct.IsCancellationRequested) { - return false; + return (false, inspectedEntryCount); } string source = ThirdPartyToolMigrationFileAccess.ReadAllText(csharpFilePath); if (ContainsFastCSharpMigrationTarget(source)) { - return true; - } - - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } - - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - string[] declaredTypeNames = ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source); - AddAssemblyScopedNames( - assemblyDeclaredTypeNamesByDirectory, - assemblyDirectory, - declaredTypeNames); - if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) - { - legacyAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalUsing(source)) - { - legacyAssemblyDirectories.Add(assemblyDirectory); - assemblyScopedLegacyDirectories.Add(assemblyDirectory); - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyGlobalToolInfoTypeAlias(source)) - { - AddAssemblyScopedLegacyAliases( - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetLegacyGlobalToolInfoTypeAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainGlobalUsing(source)) - { - assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainUsing(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainNamespaceAlias(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentDomainGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) - { - assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) - { - assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationUsing(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationNamespaceAlias(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentApplicationGlobalNamespaceAliases(source)); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source)) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - AddAssemblyScopedNames( - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDirectory, - ThirdPartyToolMigrationRules.GetCurrentFirstPartyToolsGlobalNamespaceAliases(source)); + return (true, inspectedEntryCount); } + scanState.RecordTargetScanInitialSourceFacts(source, csharpFilePath); inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } + await YieldPreviewProgressAsync(inspectedEntryCount); } - bool hasCSharpSourceTarget = await ContainsFastCSharpSourceMigrationTargetAsync( + return (false, inspectedEntryCount); + } + + private static async Task ContainsFastCSharpSourceTargetAsync( + ProjectFileInventory inventory, + ThirdPartyToolMigrationAssemblyUsageScanState scanState, + string projectRoot, + CancellationToken ct) + { + return await ContainsFastCSharpSourceMigrationTargetAsync( inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, + scanState.AsmdefDirectories, + scanState.AssemblyReferenceDirectories, projectRoot, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, + scanState.LegacyAssemblyDirectories, + scanState.AssemblyScopedLegacyDirectories, + scanState.AssemblyScopedLegacyAliasesByDirectory, + scanState.AssemblyScopedLegacyToolInfoAliasesByDirectory, + scanState.AssemblyScopedCurrentToolContractsDirectories, + scanState.AssemblyScopedCurrentApplicationDirectories, + scanState.AssemblyScopedCurrentDomainDirectories, + scanState.AssemblyScopedCurrentFirstPartyToolsDirectories, + scanState.AssemblyScopedCurrentApplicationAliasesByDirectory, + scanState.AssemblyScopedCurrentDomainAliasesByDirectory, + scanState.AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + scanState.AssemblyDeclaredTypeNamesByDirectory, ct); + } + + private static async Task CollectCSharpReferenceRequirementsAsync( + ProjectFileInventory inventory, + ThirdPartyToolMigrationAssemblyUsageScanState scanState, + string projectRoot, + CancellationToken ct) + { + await CollectBaseReferenceRequirementsAsync(inventory, scanState, projectRoot, ct); if (ct.IsCancellationRequested) { return false; } - if (hasCSharpSourceTarget) + await CollectAssemblyScopedReferenceRequirementsAsync(inventory, scanState, projectRoot, ct); + if (ct.IsCancellationRequested) { - return true; + return false; } + return await CollectFastFirstPartyScreenshotRequirementsAsync( + inventory.CSharpFilePaths, + scanState.AsmdefDirectories, + scanState.AssemblyReferenceDirectories, + projectRoot, + scanState.LegacyAssemblyDirectories, + scanState.AssemblyScopedLegacyAliasesByDirectory, + scanState.AssemblyScopedCurrentToolContractsDirectories, + scanState.AssemblyScopedCurrentFirstPartyToolsDirectories, + scanState.AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + scanState.AssemblyDeclaredTypeNamesByDirectory, + scanState.ToolContractsReferenceAssemblyDirectories, + scanState.FirstPartyScreenshotReferenceAssemblyDirectories, + ct); + } + + private static async Task CollectBaseReferenceRequirementsAsync( + ProjectFileInventory inventory, + ThirdPartyToolMigrationAssemblyUsageScanState scanState, + string projectRoot, + CancellationToken ct) + { await CollectFastAssemblyReferenceRequirementsAsync( inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, + scanState.AsmdefDirectories, + scanState.AssemblyReferenceDirectories, projectRoot, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - legacyAssemblyDirectories, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, + scanState.AssemblyScopedCurrentToolContractsDirectories, + scanState.AssemblyScopedCurrentDomainDirectories, + scanState.AssemblyScopedCurrentApplicationDirectories, + scanState.AssemblyScopedCurrentApplicationAliasesByDirectory, + scanState.AssemblyScopedCurrentDomainAliasesByDirectory, + scanState.AssemblyDeclaredTypeNamesByDirectory, + scanState.LegacyAssemblyDirectories, + scanState.ToolContractsReferenceAssemblyDirectories, + scanState.ApplicationReferenceAssemblyDirectories, + scanState.DomainReferenceAssemblyDirectories, ct); - if (ct.IsCancellationRequested) - { - return false; - } + } - if (assemblyScopedCurrentToolContractsDirectories.Count > 0) + private static async Task CollectAssemblyScopedReferenceRequirementsAsync( + ProjectFileInventory inventory, + ThirdPartyToolMigrationAssemblyUsageScanState scanState, + string projectRoot, + CancellationToken ct) + { + if (scanState.AssemblyScopedCurrentToolContractsDirectories.Count > 0) { await CollectFastAssemblyScopedCurrentToolContractsRequirementsAsync( inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, + scanState.AsmdefDirectories, + scanState.AssemblyReferenceDirectories, projectRoot, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentDomainDirectories, - assemblyScopedCurrentApplicationDirectories, - assemblyScopedCurrentApplicationAliasesByDirectory, - assemblyScopedCurrentDomainAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - legacyAssemblyDirectories, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, + scanState.AssemblyScopedCurrentToolContractsDirectories, + scanState.AssemblyScopedCurrentDomainDirectories, + scanState.AssemblyScopedCurrentApplicationDirectories, + scanState.AssemblyScopedCurrentApplicationAliasesByDirectory, + scanState.AssemblyScopedCurrentDomainAliasesByDirectory, + scanState.AssemblyDeclaredTypeNamesByDirectory, + scanState.LegacyAssemblyDirectories, + scanState.ToolContractsReferenceAssemblyDirectories, + scanState.ApplicationReferenceAssemblyDirectories, + scanState.DomainReferenceAssemblyDirectories, ct); - if (ct.IsCancellationRequested) - { - return false; - } } - if (assemblyScopedCurrentDomainDirectories.Count > 0) + if (scanState.AssemblyScopedCurrentDomainDirectories.Count == 0) { - await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( - inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot, - assemblyScopedCurrentDomainDirectories, - domainReferenceAssemblyDirectories, - ct); - if (ct.IsCancellationRequested) - { - return false; - } + return; } - bool hasFirstPartyScreenshotSourceTarget = await CollectFastFirstPartyScreenshotRequirementsAsync( + await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, + scanState.AsmdefDirectories, + scanState.AssemblyReferenceDirectories, projectRoot, - legacyAssemblyDirectories, - assemblyScopedCurrentToolContractsDirectories, - assemblyScopedCurrentFirstPartyToolsDirectories, - assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, - assemblyDeclaredTypeNamesByDirectory, - toolContractsReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories, + scanState.AssemblyScopedCurrentDomainDirectories, + scanState.DomainReferenceAssemblyDirectories, ct); - if (ct.IsCancellationRequested) - { - return false; - } - - if (hasFirstPartyScreenshotSourceTarget) - { - return true; - } + } + private static async Task ContainsFastAsmdefSourceTargetAsync( + ProjectFileInventory inventory, + int inspectedEntryCount, + CancellationToken ct) + { + int currentInspectedEntryCount = inspectedEntryCount; foreach (string asmdefFilePath in inventory.AsmdefFilePaths) { if (ct.IsCancellationRequested) @@ -326,41 +295,19 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( return true; } - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } - } - - if (toolContractsReferenceAssemblyDirectories.Count == 0 && - applicationReferenceAssemblyDirectories.Count == 0 && - domainReferenceAssemblyDirectories.Count == 0 && - firstPartyScreenshotReferenceAssemblyDirectories.Count == 0) - { - return false; + currentInspectedEntryCount++; + await YieldPreviewProgressAsync(currentInspectedEntryCount); } - MigrationAssemblyUsage assemblyUsage = new( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - new HashSet(StringComparer.Ordinal), - new HashSet(StringComparer.Ordinal), - new HashSet(StringComparer.Ordinal), - new HashSet(StringComparer.Ordinal), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentDomainAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), - CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); + return false; + } + private static bool ContainsAsmdefReferenceTarget( + ProjectFileInventory inventory, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + CancellationToken ct) + { foreach (string asmdefFilePath in inventory.AsmdefFilePaths) { if (ct.IsCancellationRequested) @@ -376,5 +323,15 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( return false; } + + private static async Task YieldPreviewProgressAsync(int inspectedEntryCount) + { + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize != 0) + { + return; + } + + await Task.Yield(); + } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs index a53d8932b1..06d1d1b3b1 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs @@ -341,33 +341,34 @@ internal static string ReadMemberTargetExpressionBeforeMethodName(string source, Debug.Assert(source != null, "source must not be null"); Debug.Assert(methodNameIndex >= 0, "methodNameIndex must not be negative"); - int index = methodNameIndex - 1; - while (index >= 0 && char.IsWhiteSpace(source[index])) - { - index--; - } + int index = SkipWhitespaceBackward(source, methodNameIndex - 1); if (index < 0 || source[index] != '.') { return string.Empty; } - index--; - while (index >= 0 && char.IsWhiteSpace(source[index])) - { - index--; - } + index = SkipNullableMemberAccessorSuffix(source, index - 1); + + int expressionEndIndex = index + 1; + index = ReadMemberTargetStartIndex(source, index); - if (index >= 0 && (source[index] == '?' || source[index] == '!')) + return source.Substring(index + 1, expressionEndIndex - index - 1).Trim(); + } + + private static int SkipNullableMemberAccessorSuffix(string source, int index) + { + index = SkipWhitespaceBackward(source, index); + if (index < 0 || (source[index] != '?' && source[index] != '!')) { - index--; - while (index >= 0 && char.IsWhiteSpace(source[index])) - { - index--; - } + return index; } - int expressionEndIndex = index + 1; + return SkipWhitespaceBackward(source, index - 1); + } + + private static int ReadMemberTargetStartIndex(string source, int index) + { while (index >= 0) { if (IsIdentifierCharacter(source[index]) || source[index] == '.') @@ -385,7 +386,7 @@ internal static string ReadMemberTargetExpressionBeforeMethodName(string source, break; } - return source.Substring(index + 1, expressionEndIndex - index - 1).Trim(); + return index; } } } diff --git a/Packages/src/Runtime/SimulateKeyboard/KeySymbolMap.cs b/Packages/src/Runtime/SimulateKeyboard/KeySymbolMap.cs index 1718325ab8..07decfb439 100644 --- a/Packages/src/Runtime/SimulateKeyboard/KeySymbolMap.cs +++ b/Packages/src/Runtime/SimulateKeyboard/KeySymbolMap.cs @@ -25,6 +25,22 @@ public static class KeySymbolMap { "ScrollLock", "ScrLk" }, }; + private static readonly Dictionary PlatformSymbols = new() + { + { "LeftMeta", ("\u2318", "\u229E") }, // ⌘ or ⊞ + { "RightMeta", ("\u2318", "\u229E") }, + { "LeftWindows", ("\u2318", "\u229E") }, + { "RightWindows", ("\u2318", "\u229E") }, + { "LeftCtrl", ("\u2303", "Ctrl") }, // ⌃ or Ctrl + { "RightCtrl", ("\u2303", "Ctrl") }, + { "LeftAlt", ("\u2325", "Alt") }, // ⌥ or Alt + { "RightAlt", ("\u2325", "Alt") }, + { "LeftShift", ("\u21E7", "Shift") }, // ⇧ or Shift + { "RightShift", ("\u21E7", "Shift") }, + { "Backspace", ("\u232B", "BS") }, // ⌫ or BS + { "Delete", ("\u2326", "Del") } // ⌦ or Del + }; + // Meta key maps to ⌘ on macOS, ⊞ on Windows/Linux private static bool IsMac => UnityEngine.Application.platform == RuntimePlatform.OSXEditor || @@ -32,36 +48,9 @@ public static class KeySymbolMap public static string GetSymbol(string keyName) { - // Key.LeftMeta.ToString() returns "LeftWindows" in Input System - if (keyName == "LeftMeta" || keyName == "RightMeta" || - keyName == "LeftWindows" || keyName == "RightWindows") - { - return IsMac ? "\u2318" : "\u229E"; // ⌘ or ⊞ - } - - if (keyName == "LeftCtrl" || keyName == "RightCtrl") - { - return IsMac ? "\u2303" : "Ctrl"; // ⌃ or Ctrl - } - - if (keyName == "LeftAlt" || keyName == "RightAlt") - { - return IsMac ? "\u2325" : "Alt"; // ⌥ or Alt - } - - if (keyName == "LeftShift" || keyName == "RightShift") - { - return IsMac ? "\u21E7" : "Shift"; // ⇧ or Shift - } - - if (keyName == "Backspace") - { - return IsMac ? "\u232B" : "BS"; // ⌫ or BS - } - - if (keyName == "Delete") + if (PlatformSymbols.TryGetValue(keyName, out (string macSymbol, string otherSymbol) platformSymbol)) { - return IsMac ? "\u2326" : "Del"; // ⌦ or Del + return IsMac ? platformSymbol.macSymbol : platformSymbol.otherSymbol; } if (Symbols.TryGetValue(keyName, out string symbol)) diff --git a/cli/.golangci-complexity.yml b/cli/.golangci-complexity.yml index ce744fdccb..9dc2be50a9 100644 --- a/cli/.golangci-complexity.yml +++ b/cli/.golangci-complexity.yml @@ -6,7 +6,7 @@ linters: - cyclop settings: cyclop: - max-complexity: 25 + max-complexity: 15 formatters: enable: diff --git a/cli/internal/cli/completion.go b/cli/internal/cli/completion.go index e106d2d000..cad3a7cbb0 100644 --- a/cli/internal/cli/completion.go +++ b/cli/internal/cli/completion.go @@ -31,97 +31,113 @@ func tryHandleCompletionRequest(args []string, cache toolsCache, stdout io.Write return false, 0 } - if args[0] == listCommandsFlag { - printCommandNames(cache, stdout) - return true, 0 + if handled, code := tryHandleCompletionListRequest(args, cache, stdout, stderr); handled { + return true, code } - if args[0] == listOptionsFlag { - if len(args) < 2 { - writeErrorEnvelope(stderr, (&argumentError{ - message: "--list-options requires a command name", - option: listOptionsFlag, - command: completionCommand, - nextActions: []string{"Pass the command name after `--list-options`."}, - }).toCLIError(errorContext{command: completionCommand})) - return true, 1 - } - printOptionsForCommand(args[1], cache, stdout) - return true, 0 + if args[0] != completionCommand { + return false, 0 } + return true, runCompletionCommand(args[1:], cache, stdout, stderr) +} - if args[0] != completionCommand { +func tryHandleCompletionListRequest(args []string, cache toolsCache, stdout io.Writer, stderr io.Writer) (bool, int) { + if len(args) == 0 { return false, 0 } - if len(args) >= 2 && args[1] == listCommandsFlag { + + switch args[0] { + case listCommandsFlag: printCommandNames(cache, stdout) return true, 0 - } - if len(args) >= 2 && args[1] == listOptionsFlag { - if len(args) < 3 { - writeErrorEnvelope(stderr, (&argumentError{ - message: "--list-options requires a command name", - option: listOptionsFlag, - command: completionCommand, - nextActions: []string{"Pass the command name after `--list-options`."}, - }).toCLIError(errorContext{command: completionCommand})) + case listOptionsFlag: + if len(args) < 2 { + writeMissingCompletionCommandName(stderr) return true, 1 } - printOptionsForCommand(args[2], cache, stdout) + printOptionsForCommand(args[1], cache, stdout) return true, 0 + default: + return false, 0 + } +} + +func runCompletionCommand(args []string, cache toolsCache, stdout io.Writer, stderr io.Writer) int { + if handled, code := tryHandleCompletionListRequest(args, cache, stdout, stderr); handled { + return code } - if containsHelpRequest(args[1:]) { + if containsHelpRequest(args) { printCompletionHelp(stdout) - return true, 0 + return 0 } - request, err := parseCompletionRequest(args[1:]) + request, err := parseCompletionRequest(args) if err != nil { writeClassifiedError(stderr, err, errorContext{command: completionCommand}) - return true, 1 + return 1 } + return runCompletionRequest(request, stdout, stderr) +} +func runCompletionRequest(request completionRequest, stdout io.Writer, stderr io.Writer) int { shellName := request.shell if shellName == "" { shellName = detectShell() } if shellName == "" { - writeErrorEnvelope(stderr, cliError{ - ErrorCode: errorCodeInvalidArgument, - Phase: errorPhaseArgumentParsing, - Message: "Could not detect shell.", - Retryable: false, - SafeToRetry: false, - Command: completionCommand, - NextActions: []string{"Pass `--shell bash`, `--shell zsh`, `--shell powershell`, or `--shell pwsh`."}, - }) - return true, 1 + writeCompletionShellDetectionError(stderr) + return 1 } script := getCompletionScript(shellName) if !request.install { writeLine(stdout, script) - return true, 0 + return 0 } configPath, err := getShellConfigPath(shellName) if err != nil { writeClassifiedError(stderr, err, errorContext{command: completionCommand}) - return true, 1 + return 1 } if err := installCompletionScript(configPath, shellName, script); err != nil { writeClassifiedError(stderr, err, errorContext{command: completionCommand}) - return true, 1 + return 1 } + writeCompletionInstallResult(stdout, shellName, configPath) + return 0 +} + +func writeMissingCompletionCommandName(stderr io.Writer) { + writeErrorEnvelope(stderr, (&argumentError{ + message: "--list-options requires a command name", + option: listOptionsFlag, + command: completionCommand, + nextActions: []string{"Pass the command name after `--list-options`."}, + }).toCLIError(errorContext{command: completionCommand})) +} + +func writeCompletionShellDetectionError(stderr io.Writer) { + writeErrorEnvelope(stderr, cliError{ + ErrorCode: errorCodeInvalidArgument, + Phase: errorPhaseArgumentParsing, + Message: "Could not detect shell.", + Retryable: false, + SafeToRetry: false, + Command: completionCommand, + NextActions: []string{"Pass `--shell bash`, `--shell zsh`, `--shell powershell`, or `--shell pwsh`."}, + }) +} + +func writeCompletionInstallResult(stdout io.Writer, shellName string, configPath string) { writeFormat(stdout, "Completion installed to %s\n", configPath) if isPowerShellShell(shellName) { writeLine(stdout, "Restart PowerShell to enable completion.") - return true, 0 + return } writeFormat(stdout, "Run 'source %s' or restart your shell to enable completion.\n", configPath) - return true, 0 } func shouldHandleCompletionRequest(args []string) bool { diff --git a/cli/internal/cli/connection_retry.go b/cli/internal/cli/connection_retry.go index b6f35495a3..640ada06bc 100644 --- a/cli/internal/cli/connection_retry.go +++ b/cli/internal/cli/connection_retry.go @@ -181,11 +181,7 @@ func sendWithTransientConnectionRetryAndResponseTimeout( retryTicker := time.NewTicker(serverConnectionRetryPoll) defer retryTicker.Stop() for { - client := unityipc.NewClient(connection, version) - if responseTimeout > 0 { - client = client.WithResponseTimeout(responseTimeout) - } - client = client.WithMainThreadStallHandler(func(stallSeconds float64) { + client := newConnectionRetryClient(connection, responseTimeout, func(stallSeconds float64) { focusController.handleMainThreadStall(ctx, stallSeconds) }) // Each attempt keeps the base accept bound: a dispatched request that never gets @@ -199,69 +195,43 @@ func sendWithTransientConnectionRetryAndResponseTimeout( // usually absorbs back-to-back tool calls without bothering the caller. lastOutcome = outcome lastErr = err - if time.Since(startedAt) >= serverConnectionRetryTimeout { - if ctx.Err() != nil { - return lastOutcome, ctx.Err() - } - return lastOutcome, lastErr - } - select { - case <-retryContext.Done(): - if ctx.Err() != nil { - return lastOutcome, ctx.Err() - } - return lastOutcome, lastErr - case <-retryTicker.C: + if finished, finalOutcome, finalErr := finishBusyRetry( + ctx, + retryContext, + startedAt, + retryTicker, + lastOutcome, + lastErr, + ); finished { + return finalOutcome, finalErr } continue } if !shouldRetryUndispatchedConnection(err, outcome) { - // A transport error after a busy response in this window must not mask the - // busy; the server answered moments ago, so busy is the truer diagnosis. - // An RPC error is a real Unity answer, not a transport artifact, and must - // surface as-is. The transport error is not compared against the window - // deadline because the connection deadline can fire microseconds before - // the context reports expiry. - if err != nil && !isRPCError(err) && isUnityServerBusyRPCError(lastErr) { - if ctx.Err() != nil { - return outcome, ctx.Err() - } - return lastOutcome, lastErr - } - if reason, ok := connectionRetryFocusReasonForError(err, outcome, responseTimeout); ok { - focusController.tryFocus(ctx, reason, err) - focusController.keepUnityFocusedAfterReturn() - } - return outcome, err + return finishNonRetryableConnectionAttempt( + ctx, + outcome, + err, + lastOutcome, + lastErr, + responseTimeout, + focusController, + ) } runningProcess, processErr := findRunningUnityProcessForConnectionRetry(retryContext, connection.ProjectRoot) - if processErr != nil { - if retryContext.Err() != nil { - if ctx.Err() != nil { - return outcome, ctx.Err() - } - // A busy response seen during the window is the truer diagnosis than a - // final dial cut short by the expiring retry context. - if isUnityServerBusyRPCError(lastErr) { - return lastOutcome, lastErr - } - return outcome, unityServerNotRespondingError{ - projectRoot: connection.ProjectRoot, - endpoint: connection.Endpoint.Address, - cause: err, - } - } - return outcome, processErr - } - if runningProcess == nil { - // Same masking as the probe-error path: a busy response seen during the - // window proves a server answered moments ago, so it is a truer diagnosis - // than a final dial cut short by the expiring retry context. - if retryContext.Err() != nil && isUnityServerBusyRPCError(lastErr) { - return lastOutcome, lastErr - } - return outcome, err + if finished, finalOutcome, finalErr := finishUndispatchedRetryProbe( + ctx, + retryContext, + connection, + outcome, + err, + processErr, + runningProcess, + lastOutcome, + lastErr, + ); finished { + return finalOutcome, finalErr } focusController.tryFocusProcess( retryContext, @@ -272,27 +242,16 @@ func sendWithTransientConnectionRetryAndResponseTimeout( lastOutcome = outcome lastErr = err - if time.Since(startedAt) >= unityAliveRetryWindow() { - if ctx.Err() != nil { - return lastOutcome, ctx.Err() - } - return lastOutcome, unityServerNotRespondingError{ - projectRoot: connection.ProjectRoot, - endpoint: connection.Endpoint.Address, - cause: lastErr, - } - } - select { - case <-retryContext.Done(): - if ctx.Err() != nil { - return lastOutcome, ctx.Err() - } - return lastOutcome, unityServerNotRespondingError{ - projectRoot: connection.ProjectRoot, - endpoint: connection.Endpoint.Address, - cause: lastErr, - } - case <-retryTicker.C: + if finished, finalOutcome, finalErr := finishUnityAliveRetryWait( + ctx, + retryContext, + startedAt, + retryTicker, + connection, + lastOutcome, + lastErr, + ); finished { + return finalOutcome, finalErr } } } diff --git a/cli/internal/cli/connection_retry_flow.go b/cli/internal/cli/connection_retry_flow.go new file mode 100644 index 0000000000..4b9d81035e --- /dev/null +++ b/cli/internal/cli/connection_retry_flow.go @@ -0,0 +1,152 @@ +package cli + +import ( + "context" + "time" + + "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +) + +func newConnectionRetryClient( + connection unityipc.Connection, + responseTimeout time.Duration, + mainThreadStallHandler func(float64), +) *unityipc.Client { + client := unityipc.NewClient(connection, version) + if responseTimeout > 0 { + client = client.WithResponseTimeout(responseTimeout) + } + return client.WithMainThreadStallHandler(mainThreadStallHandler) +} + +func finishBusyRetry( + ctx context.Context, + retryContext context.Context, + startedAt time.Time, + retryTicker *time.Ticker, + lastOutcome unityipc.UnitySendOutcome, + lastErr error, +) (bool, unityipc.UnitySendOutcome, error) { + if time.Since(startedAt) >= serverConnectionRetryTimeout { + if ctx.Err() != nil { + return true, lastOutcome, ctx.Err() + } + return true, lastOutcome, lastErr + } + select { + case <-retryContext.Done(): + if ctx.Err() != nil { + return true, lastOutcome, ctx.Err() + } + return true, lastOutcome, lastErr + case <-retryTicker.C: + return false, lastOutcome, nil + } +} + +func finishNonRetryableConnectionAttempt( + ctx context.Context, + outcome unityipc.UnitySendOutcome, + err error, + lastOutcome unityipc.UnitySendOutcome, + lastErr error, + responseTimeout time.Duration, + focusController *connectionRetryFocusController, +) (unityipc.UnitySendOutcome, error) { + // A transport error after a busy response in this window must not mask the + // busy; the server answered moments ago, so busy is the truer diagnosis. + // An RPC error is a real Unity answer, not a transport artifact, and must + // surface as-is. The transport error is not compared against the window + // deadline because the connection deadline can fire microseconds before + // the context reports expiry. + if err != nil && !isRPCError(err) && isUnityServerBusyRPCError(lastErr) { + if ctx.Err() != nil { + return outcome, ctx.Err() + } + return lastOutcome, lastErr + } + if reason, ok := connectionRetryFocusReasonForError(err, outcome, responseTimeout); ok { + focusController.tryFocus(ctx, reason, err) + focusController.keepUnityFocusedAfterReturn() + } + return outcome, err +} + +func finishUndispatchedRetryProbe( + ctx context.Context, + retryContext context.Context, + connection unityipc.Connection, + outcome unityipc.UnitySendOutcome, + err error, + processErr error, + runningProcess *unityProcess, + lastOutcome unityipc.UnitySendOutcome, + lastErr error, +) (bool, unityipc.UnitySendOutcome, error) { + if processErr != nil { + if retryContext.Err() == nil { + return true, outcome, processErr + } + if ctx.Err() != nil { + return true, outcome, ctx.Err() + } + // A busy response seen during the window is the truer diagnosis than a + // final dial cut short by the expiring retry context. + if isUnityServerBusyRPCError(lastErr) { + return true, lastOutcome, lastErr + } + return true, outcome, newUnityServerNotRespondingError(connection, err) + } + if runningProcess != nil { + return false, outcome, nil + } + // Same masking as the probe-error path: a busy response seen during the + // window proves a server answered moments ago, so it is a truer diagnosis + // than a final dial cut short by the expiring retry context. + if retryContext.Err() != nil && isUnityServerBusyRPCError(lastErr) { + return true, lastOutcome, lastErr + } + return true, outcome, err +} + +func finishUnityAliveRetryWait( + ctx context.Context, + retryContext context.Context, + startedAt time.Time, + retryTicker *time.Ticker, + connection unityipc.Connection, + lastOutcome unityipc.UnitySendOutcome, + lastErr error, +) (bool, unityipc.UnitySendOutcome, error) { + if time.Since(startedAt) >= unityAliveRetryWindow() { + finalOutcome, finalErr := finishUnityAliveRetry(ctx, connection, lastOutcome, lastErr) + return true, finalOutcome, finalErr + } + select { + case <-retryContext.Done(): + finalOutcome, finalErr := finishUnityAliveRetry(ctx, connection, lastOutcome, lastErr) + return true, finalOutcome, finalErr + case <-retryTicker.C: + return false, lastOutcome, nil + } +} + +func finishUnityAliveRetry( + ctx context.Context, + connection unityipc.Connection, + lastOutcome unityipc.UnitySendOutcome, + lastErr error, +) (unityipc.UnitySendOutcome, error) { + if ctx.Err() != nil { + return lastOutcome, ctx.Err() + } + return lastOutcome, newUnityServerNotRespondingError(connection, lastErr) +} + +func newUnityServerNotRespondingError(connection unityipc.Connection, cause error) unityServerNotRespondingError { + return unityServerNotRespondingError{ + projectRoot: connection.ProjectRoot, + endpoint: connection.Endpoint.Address, + cause: cause, + } +} diff --git a/cli/internal/cli/error_envelope.go b/cli/internal/cli/error_envelope.go index 289a268e3d..df604b8749 100644 --- a/cli/internal/cli/error_envelope.go +++ b/cli/internal/cli/error_envelope.go @@ -5,7 +5,6 @@ import ( "errors" "io" "net" - "strings" "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" ) @@ -139,191 +138,6 @@ func responseTimeoutAfterAcceptError(err error, context errorContext) cliError { } } -func classifyError(err error, context errorContext) cliError { - if err == nil { - return internalCLIError("unknown CLI error", context) - } - - var argumentErr *argumentError - if errors.As(err, &argumentErr) { - return argumentErr.toCLIError(context) - } - - var startupTimeoutErr launchStartupTimeoutError - if errors.As(err, &startupTimeoutErr) { - return unityStartupTimeoutCLIError(startupTimeoutErr, context) - } - - var processExitTimeoutErr launchProcessExitTimeoutError - if errors.As(err, &processExitTimeoutErr) { - return unityProcessExitTimeoutCLIError(processExitTimeoutErr, context) - } - - var notRespondingErr unityServerNotRespondingError - if errors.As(err, ¬RespondingErr) { - return cliError{ - ErrorCode: errorCodeUnityNotReachable, - Phase: errorPhaseConnection, - Message: "Unity is running for this project, but the Unity CLI Loop server is not responding.", - Retryable: true, - SafeToRetry: true, - ProjectRoot: firstNonEmpty(context.projectRoot, notRespondingErr.projectRoot), - Command: context.command, - NextActions: []string{ - "Wait and retry; Unity may be starting, importing assets, compiling, or reloading scripts.", - "Run `uloop focus-window` if Unity appears stalled in the background.", - "Confirm that the command targets the intended Unity project and the Editor package is installed.", - }, - Details: map[string]any{ - "Endpoint": notRespondingErr.endpoint, - "Cause": notRespondingErr.causeText(), - }, - } - } - - var editorUnresponsiveErr *unityipc.EditorUnresponsiveError - if errors.As(err, &editorUnresponsiveErr) { - return unityEditorUnresponsiveError(editorUnresponsiveErr, context) - } - - var connectionErr *unityipc.ConnectionAttemptError - if errors.As(err, &connectionErr) { - return cliError{ - ErrorCode: errorCodeUnityNotReachable, - Phase: errorPhaseConnection, - Message: "The Unity CLI Loop server is not reachable for this project.", - Retryable: true, - SafeToRetry: true, - ProjectRoot: firstNonEmpty(context.projectRoot, connectionErr.ProjectRoot), - Command: context.command, - NextActions: []string{ - "If Unity is closed, run `uloop launch`.", - "If Unity is starting, compiling, or reloading scripts, wait and retry.", - "Confirm that the command targets the intended Unity project.", - }, - Details: map[string]any{ - "Endpoint": connectionErr.Endpoint, - "Cause": connectionAttemptCause(connectionErr), - }, - } - } - - var rpcErr *unityipc.RPCError - if errors.As(err, &rpcErr) { - details := map[string]any{ - "Code": rpcErr.Code, - "Message": rpcErr.Message, - } - var decodedData map[string]any - if len(rpcErr.Data) > 0 { - var data any - if json.Unmarshal(rpcErr.Data, &data) == nil { - details["Data"] = data - if typedData, ok := data.(map[string]any); ok { - decodedData = typedData - } - } else { - details["Data"] = string(rpcErr.Data) - } - } - if rpcDataType(decodedData) == "cli_update_required" { - return cliUpdateRequiredError(rpcErr, details, decodedData, context) - } - if rpcDataType(decodedData) == "server_busy" { - return unityServerBusyError(rpcErr, details, decodedData, context) - } - return cliError{ - ErrorCode: errorCodeUnityRPCError, - Phase: errorPhaseUnityRPC, - Message: rpcErr.Message, - Retryable: false, - SafeToRetry: false, - ProjectRoot: context.projectRoot, - Command: context.command, - NextActions: []string{ - "Read the Unity error details and fix the request or project state before retrying.", - }, - Details: details, - } - } - - message := err.Error() - if message == "unity project not found. Use --project-path option to specify the target" || - strings.HasPrefix(message, "not a Unity project:") || - strings.HasPrefix(message, "--project-path does not point to a Unity project:") { - return cliError{ - ErrorCode: errorCodeProjectNotFound, - Phase: errorPhaseProjectResolve, - Message: message, - Retryable: false, - SafeToRetry: false, - Command: context.command, - NextActions: []string{ - "Run the command from inside a Unity project.", - "Pass `--project-path ` when targeting another Unity project.", - }, - } - } - - if message == updateUnsupportedOSMessage { - return cliError{ - ErrorCode: errorCodeInvalidArgument, - Phase: errorPhaseExecution, - Message: message, - Retryable: false, - SafeToRetry: false, - Command: context.command, - NextActions: []string{ - "Run `uloop update` on macOS or Windows.", - "Install the latest uloop launcher manually on this platform.", - }, - } - } - - if message == installUnsupportedOSMessage { - return cliError{ - ErrorCode: errorCodeInvalidArgument, - Phase: errorPhaseExecution, - Message: message, - Retryable: false, - SafeToRetry: false, - Command: context.command, - NextActions: []string{ - "Run `uloop install` on Windows.", - "Use the platform-specific installer for this system.", - }, - } - } - - if message == uninstallUnsupportedOSMessage { - return cliError{ - ErrorCode: errorCodeInvalidArgument, - Phase: errorPhaseExecution, - Message: message, - Retryable: false, - SafeToRetry: false, - Command: context.command, - NextActions: []string{ - "Run `uloop uninstall` on macOS or Windows.", - "Remove the uloop launcher binary manually on this platform.", - }, - } - } - - return internalCLIError(message, context) -} - -func rpcDataType(data map[string]any) string { - if data == nil { - return "" - } - value, ok := data["type"].(string) - if !ok { - return "" - } - return value -} - func unityServerBusyError( rpcErr *unityipc.RPCError, details map[string]any, diff --git a/cli/internal/cli/error_envelope_classification.go b/cli/internal/cli/error_envelope_classification.go new file mode 100644 index 0000000000..4e88293219 --- /dev/null +++ b/cli/internal/cli/error_envelope_classification.go @@ -0,0 +1,241 @@ +package cli + +import ( + "encoding/json" + "errors" + "strings" + + "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +) + +func classifyError(err error, context errorContext) cliError { + if err == nil { + return internalCLIError("unknown CLI error", context) + } + + if classifiedError, ok := classifyTypedError(err, context); ok { + return classifiedError + } + + return classifyMessageError(err.Error(), context) +} + +func classifyTypedError(err error, context errorContext) (cliError, bool) { + var argumentErr *argumentError + if errors.As(err, &argumentErr) { + return argumentErr.toCLIError(context), true + } + + var startupTimeoutErr launchStartupTimeoutError + if errors.As(err, &startupTimeoutErr) { + return unityStartupTimeoutCLIError(startupTimeoutErr, context), true + } + + var processExitTimeoutErr launchProcessExitTimeoutError + if errors.As(err, &processExitTimeoutErr) { + return unityProcessExitTimeoutCLIError(processExitTimeoutErr, context), true + } + + if classifiedError, ok := classifyUnityConnectionError(err, context); ok { + return classifiedError, true + } + + var rpcErr *unityipc.RPCError + if errors.As(err, &rpcErr) { + return classifyRPCError(rpcErr, context), true + } + + return cliError{}, false +} + +func classifyUnityConnectionError(err error, context errorContext) (cliError, bool) { + var notRespondingErr unityServerNotRespondingError + if errors.As(err, ¬RespondingErr) { + return unityServerNotRespondingCLIError(notRespondingErr, context), true + } + + var editorUnresponsiveErr *unityipc.EditorUnresponsiveError + if errors.As(err, &editorUnresponsiveErr) { + return unityEditorUnresponsiveError(editorUnresponsiveErr, context), true + } + + var connectionErr *unityipc.ConnectionAttemptError + if errors.As(err, &connectionErr) { + return connectionAttemptCLIError(connectionErr, context), true + } + + return cliError{}, false +} + +func unityServerNotRespondingCLIError(err unityServerNotRespondingError, context errorContext) cliError { + return cliError{ + ErrorCode: errorCodeUnityNotReachable, + Phase: errorPhaseConnection, + Message: "Unity is running for this project, but the Unity CLI Loop server is not responding.", + Retryable: true, + SafeToRetry: true, + ProjectRoot: firstNonEmpty(context.projectRoot, err.projectRoot), + Command: context.command, + NextActions: []string{ + "Wait and retry; Unity may be starting, importing assets, compiling, or reloading scripts.", + "Run `uloop focus-window` if Unity appears stalled in the background.", + "Confirm that the command targets the intended Unity project and the Editor package is installed.", + }, + Details: map[string]any{ + "Endpoint": err.endpoint, + "Cause": err.causeText(), + }, + } +} + +func connectionAttemptCLIError(err *unityipc.ConnectionAttemptError, context errorContext) cliError { + return cliError{ + ErrorCode: errorCodeUnityNotReachable, + Phase: errorPhaseConnection, + Message: "The Unity CLI Loop server is not reachable for this project.", + Retryable: true, + SafeToRetry: true, + ProjectRoot: firstNonEmpty(context.projectRoot, err.ProjectRoot), + Command: context.command, + NextActions: []string{ + "If Unity is closed, run `uloop launch`.", + "If Unity is starting, compiling, or reloading scripts, wait and retry.", + "Confirm that the command targets the intended Unity project.", + }, + Details: map[string]any{ + "Endpoint": err.Endpoint, + "Cause": connectionAttemptCause(err), + }, + } +} + +func classifyRPCError(rpcErr *unityipc.RPCError, context errorContext) cliError { + details, decodedData := rpcErrorDetails(rpcErr) + switch rpcDataType(decodedData) { + case "cli_update_required": + return cliUpdateRequiredError(rpcErr, details, decodedData, context) + case "server_busy": + return unityServerBusyError(rpcErr, details, decodedData, context) + default: + return genericRPCError(rpcErr, details, context) + } +} + +func rpcErrorDetails(rpcErr *unityipc.RPCError) (map[string]any, map[string]any) { + details := map[string]any{ + "Code": rpcErr.Code, + "Message": rpcErr.Message, + } + if len(rpcErr.Data) == 0 { + return details, nil + } + + var data any + if json.Unmarshal(rpcErr.Data, &data) != nil { + details["Data"] = string(rpcErr.Data) + return details, nil + } + + details["Data"] = data + decodedData, _ := data.(map[string]any) + return details, decodedData +} + +func genericRPCError(rpcErr *unityipc.RPCError, details map[string]any, context errorContext) cliError { + return cliError{ + ErrorCode: errorCodeUnityRPCError, + Phase: errorPhaseUnityRPC, + Message: rpcErr.Message, + Retryable: false, + SafeToRetry: false, + ProjectRoot: context.projectRoot, + Command: context.command, + NextActions: []string{ + "Read the Unity error details and fix the request or project state before retrying.", + }, + Details: details, + } +} + +func classifyMessageError(message string, context errorContext) cliError { + if isProjectNotFoundMessage(message) { + return cliError{ + ErrorCode: errorCodeProjectNotFound, + Phase: errorPhaseProjectResolve, + Message: message, + Retryable: false, + SafeToRetry: false, + Command: context.command, + NextActions: []string{ + "Run the command from inside a Unity project.", + "Pass `--project-path ` when targeting another Unity project.", + }, + } + } + + if unsupportedError, ok := unsupportedPlatformError(message, context); ok { + return unsupportedError + } + + return internalCLIError(message, context) +} + +func isProjectNotFoundMessage(message string) bool { + return message == "unity project not found. Use --project-path option to specify the target" || + strings.HasPrefix(message, "not a Unity project:") || + strings.HasPrefix(message, "--project-path does not point to a Unity project:") +} + +func unsupportedPlatformError(message string, context errorContext) (cliError, bool) { + switch message { + case updateUnsupportedOSMessage: + return invalidArgumentExecutionError( + message, + context, + []string{ + "Run `uloop update` on macOS or Windows.", + "Install the latest uloop launcher manually on this platform.", + }), true + case installUnsupportedOSMessage: + return invalidArgumentExecutionError( + message, + context, + []string{ + "Run `uloop install` on Windows.", + "Use the platform-specific installer for this system.", + }), true + case uninstallUnsupportedOSMessage: + return invalidArgumentExecutionError( + message, + context, + []string{ + "Run `uloop uninstall` on macOS or Windows.", + "Remove the uloop launcher binary manually on this platform.", + }), true + default: + return cliError{}, false + } +} + +func invalidArgumentExecutionError(message string, context errorContext, nextActions []string) cliError { + return cliError{ + ErrorCode: errorCodeInvalidArgument, + Phase: errorPhaseExecution, + Message: message, + Retryable: false, + SafeToRetry: false, + Command: context.command, + NextActions: nextActions, + } +} + +func rpcDataType(data map[string]any) string { + if data == nil { + return "" + } + value, ok := data["type"].(string) + if !ok { + return "" + } + return value +} diff --git a/cli/internal/cli/launch.go b/cli/internal/cli/launch.go index 95fe69ef71..d75720152b 100644 --- a/cli/internal/cli/launch.go +++ b/cli/internal/cli/launch.go @@ -84,108 +84,18 @@ func parseLaunchOptions(args []string, globalProjectPath string) (launchOptions, } for index := 0; index < len(args); index++ { - arg := args[index] - switch { - case arg == "-r" || arg == "--restart": - options.restart = true - case arg == "-q" || arg == "--quit": - options.quit = true - case arg == "-d" || arg == "--delete-recovery": - options.deleteRecovery = true - case arg == "-a" || arg == "-f" || arg == "--add-unity-hub" || arg == "--favorite" || arg == "--unity-hub-entry": - return launchOptions{}, &argumentError{ - message: "Native launch does not support Unity Hub registration options.", - option: arg, - command: launchCommandName, - nextActions: []string{"Remove the Unity Hub registration option and retry `uloop launch`."}, - } - case arg == "-p" || arg == "--platform": - value, consumed, err := readLaunchOptionValue(arg, args, index) - if err != nil { - return launchOptions{}, err - } - options.platform = value - if consumed { - index++ - } - case strings.HasPrefix(arg, "--platform="): - value, _, err := readLaunchOptionValue(arg, args, index) - if err != nil { - return launchOptions{}, err - } - options.platform = value - case arg == "--max-depth": - value, consumed, err := readLaunchOptionValue(arg, args, index) - if err != nil { - return launchOptions{}, err - } - maxDepth, err := strconv.Atoi(value) - if err != nil { - return launchOptions{}, invalidValueArgumentError("--max-depth", value, "integer") - } - options.maxDepth = maxDepth - if consumed { - index++ - } - case strings.HasPrefix(arg, "--max-depth="): - value := strings.TrimPrefix(arg, "--max-depth=") - maxDepth, err := strconv.Atoi(value) - if err != nil { - return launchOptions{}, invalidValueArgumentError("--max-depth", value, "integer") - } - options.maxDepth = maxDepth - case strings.HasPrefix(arg, "-"): - return launchOptions{}, &argumentError{ - message: "Unknown launch option: " + arg, - option: arg, - command: launchCommandName, - nextActions: []string{"Run `uloop launch --help` to inspect supported launch options."}, - } - default: - if options.projectPath != "" { - return launchOptions{}, &argumentError{ - message: "Unexpected extra launch argument: " + arg, - received: arg, - command: launchCommandName, - nextActions: []string{"Pass only one project path to `uloop launch`."}, - } - } - options.projectPath = arg + nextIndex, err := applyLaunchOption(&options, args, index) + if err != nil { + return launchOptions{}, err } + index = nextIndex } return options, nil } -func readLaunchOptionValue(option string, args []string, index int) (string, bool, error) { - if strings.Contains(option, "=") { - parts := strings.SplitN(option, "=", 2) - if parts[1] == "" { - return "", false, missingValueArgumentError(parts[0]) - } - return parts[1], false, nil - } - if index+1 >= len(args) || isInvalidLaunchOptionValue(option, args[index+1]) { - return "", false, missingValueArgumentError(option) - } - return args[index+1], true, nil -} - -func isInvalidLaunchOptionValue(option string, value string) bool { - if option == "--max-depth" { - return isNextOptionToken(value) - } - return strings.HasPrefix(value, "-") -} - func runLaunch(ctx context.Context, options launchOptions, startPath string, stdout io.Writer, stderr io.Writer) int { - if options.projectPath == "" { - depthInfo := strconv.Itoa(options.maxDepth) - if options.maxDepth == -1 { - depthInfo = "unlimited" - } - writeFormat(stdout, "Searching for Unity project under %s (max-depth: %s)...\n\n", startPath, depthInfo) - } + writeLaunchProjectSearch(stdout, options, startPath) projectRoot, err := resolveLaunchProjectRoot(startPath, options) if err != nil { @@ -193,48 +103,18 @@ func runLaunch(ctx context.Context, options launchOptions, startPath string, std return 1 } - if options.deleteRecovery { - if err := os.RemoveAll(filepath.Join(projectRoot, recoveryDirectoryPath)); err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) - return 1 - } + if !deleteLaunchRecoveryIfRequested(options, projectRoot, stderr) { + return 1 } - runningProcess, err := findRunningUnityProcessForLaunch(ctx, projectRoot) - if err != nil { - // Sandboxes can block the process scan (e.g. /bin/ps). A responding project IPC - // proves Unity is running, so plain launch must not fail on the scan alone. - // Restart and quit still fail because they need a process id to kill. - if !options.restart && !options.quit && probeProjectIpcForLaunchFallback(ctx, projectRoot) == nil { - return writeDetectionFallbackLaunchReadyResponse(stdout, stderr, projectRoot, err) - } - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) - return 1 + runningProcess, handled, code := findLaunchRunningProcess(ctx, options, projectRoot, stdout, stderr) + if handled { + return code } if runningProcess != nil { - if !options.restart && !options.quit { - logLaunchExistingFocus(ctx, projectRoot, runningProcess.pid) - spinner := newLaunchSpinner(stdout, stderr) - defer spinner.Stop() - writeLaunchReadinessWait(stdout, spinner) - if err := waitForLaunchReadiness(ctx, projectRoot); err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) - return 1 - } - spinner.Stop() - return writeExistingLaunchReadyResponse(stdout, stderr, projectRoot, runningProcess.pid) - } - if err := killUnityProcessForLaunch(runningProcess.pid); err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) - return 1 - } - if err := waitForUnityProcessExitForLaunch(ctx, projectRoot, runningProcess.pid, launchProcessExitPoll, launchProcessExitTimeout); err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) - return 1 - } - if options.quit { - return writeLaunchQuitResponse(stdout, stderr, projectRoot, &runningProcess.pid, launchStoppedMessage) + if handled, code := handleExistingLaunchProcess(ctx, options, projectRoot, runningProcess, stdout, stderr); handled { + return code } } @@ -242,6 +122,98 @@ func runLaunch(ctx context.Context, options launchOptions, startPath string, std return writeLaunchQuitResponse(stdout, stderr, projectRoot, nil, launchNoProcessMessage) } + return startUnityAndWaitForReadiness(ctx, options, projectRoot, runningProcess, stdout, stderr) +} + +func writeLaunchProjectSearch(stdout io.Writer, options launchOptions, startPath string) { + if options.projectPath != "" { + return + } + depthInfo := strconv.Itoa(options.maxDepth) + if options.maxDepth == -1 { + depthInfo = "unlimited" + } + writeFormat(stdout, "Searching for Unity project under %s (max-depth: %s)...\n\n", startPath, depthInfo) +} + +func deleteLaunchRecoveryIfRequested(options launchOptions, projectRoot string, stderr io.Writer) bool { + if !options.deleteRecovery { + return true + } + if err := os.RemoveAll(filepath.Join(projectRoot, recoveryDirectoryPath)); err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) + return false + } + return true +} + +func findLaunchRunningProcess( + ctx context.Context, + options launchOptions, + projectRoot string, + stdout io.Writer, + stderr io.Writer, +) (*unityProcess, bool, int) { + runningProcess, err := findRunningUnityProcessForLaunch(ctx, projectRoot) + if err == nil { + return runningProcess, false, 0 + } + // Sandboxes can block the process scan (e.g. /bin/ps). A responding project IPC + // proves Unity is running, so plain launch must not fail on the scan alone. + // Restart and quit still fail because they need a process id to kill. + if !options.restart && !options.quit && probeProjectIpcForLaunchFallback(ctx, projectRoot) == nil { + return nil, true, writeDetectionFallbackLaunchReadyResponse(stdout, stderr, projectRoot, err) + } + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) + return nil, true, 1 +} + +func handleExistingLaunchProcess( + ctx context.Context, + options launchOptions, + projectRoot string, + runningProcess *unityProcess, + stdout io.Writer, + stderr io.Writer, +) (bool, int) { + if !options.restart && !options.quit { + return true, waitForExistingLaunchReadiness(ctx, projectRoot, runningProcess.pid, stdout, stderr) + } + if err := killUnityProcessForLaunch(runningProcess.pid); err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) + return true, 1 + } + if err := waitForUnityProcessExitForLaunch(ctx, projectRoot, runningProcess.pid, launchProcessExitPoll, launchProcessExitTimeout); err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) + return true, 1 + } + if options.quit { + return true, writeLaunchQuitResponse(stdout, stderr, projectRoot, &runningProcess.pid, launchStoppedMessage) + } + return false, 0 +} + +func waitForExistingLaunchReadiness(ctx context.Context, projectRoot string, pid int, stdout io.Writer, stderr io.Writer) int { + logLaunchExistingFocus(ctx, projectRoot, pid) + spinner := newLaunchSpinner(stdout, stderr) + defer spinner.Stop() + writeLaunchReadinessWait(stdout, spinner) + if err := waitForLaunchReadiness(ctx, projectRoot); err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) + return 1 + } + spinner.Stop() + return writeExistingLaunchReadyResponse(stdout, stderr, projectRoot, pid) +} + +func startUnityAndWaitForReadiness( + ctx context.Context, + options launchOptions, + projectRoot string, + runningProcess *unityProcess, + stdout io.Writer, + stderr io.Writer, +) int { removedStaleTemp, err := cleanStaleUnityTemp(projectRoot) if err != nil { writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName}) diff --git a/cli/internal/cli/launch_options.go b/cli/internal/cli/launch_options.go new file mode 100644 index 0000000000..284eec2855 --- /dev/null +++ b/cli/internal/cli/launch_options.go @@ -0,0 +1,124 @@ +package cli + +import ( + "strconv" + "strings" +) + +func applyLaunchOption(options *launchOptions, args []string, index int) (int, error) { + arg := args[index] + switch { + case arg == "-r" || arg == "--restart": + options.restart = true + return index, nil + case arg == "-q" || arg == "--quit": + options.quit = true + return index, nil + case arg == "-d" || arg == "--delete-recovery": + options.deleteRecovery = true + return index, nil + case isUnsupportedLaunchHubOption(arg): + return index, unsupportedLaunchHubOptionError(arg) + case arg == "-p" || arg == "--platform" || strings.HasPrefix(arg, "--platform="): + return applyLaunchPlatformOption(options, args, index) + case arg == "--max-depth" || strings.HasPrefix(arg, "--max-depth="): + return applyLaunchMaxDepthOption(options, args, index) + case strings.HasPrefix(arg, "-"): + return index, unknownLaunchOptionError(arg) + default: + return applyLaunchProjectPathArgument(options, arg, index) + } +} + +func isUnsupportedLaunchHubOption(arg string) bool { + return arg == "-a" || + arg == "-f" || + isUnsupportedLaunchHubLongOption(arg, "--add-unity-hub") || + isUnsupportedLaunchHubLongOption(arg, "--favorite") || + isUnsupportedLaunchHubLongOption(arg, "--unity-hub-entry") +} + +func isUnsupportedLaunchHubLongOption(arg string, option string) bool { + return arg == option || strings.HasPrefix(arg, option+"=") +} + +func unsupportedLaunchHubOptionError(arg string) error { + return &argumentError{ + message: "Native launch does not support Unity Hub registration options.", + option: arg, + command: launchCommandName, + nextActions: []string{"Remove the Unity Hub registration option and retry `uloop launch`."}, + } +} + +func unknownLaunchOptionError(arg string) error { + return &argumentError{ + message: "Unknown launch option: " + arg, + option: arg, + command: launchCommandName, + nextActions: []string{"Run `uloop launch --help` to inspect supported launch options."}, + } +} + +func applyLaunchPlatformOption(options *launchOptions, args []string, index int) (int, error) { + value, consumed, err := readLaunchOptionValue(args[index], args, index) + if err != nil { + return index, err + } + options.platform = value + return nextLaunchOptionIndex(index, consumed), nil +} + +func applyLaunchMaxDepthOption(options *launchOptions, args []string, index int) (int, error) { + value, consumed, err := readLaunchOptionValue(args[index], args, index) + if err != nil { + return index, err + } + maxDepth, err := strconv.Atoi(value) + if err != nil || maxDepth < -1 { + return index, invalidValueArgumentError("--max-depth", value, "integer >= -1") + } + options.maxDepth = maxDepth + return nextLaunchOptionIndex(index, consumed), nil +} + +func applyLaunchProjectPathArgument(options *launchOptions, arg string, index int) (int, error) { + if options.projectPath != "" { + return index, &argumentError{ + message: "Unexpected extra launch argument: " + arg, + received: arg, + command: launchCommandName, + nextActions: []string{"Pass only one project path to `uloop launch`."}, + } + } + options.projectPath = arg + return index, nil +} + +func nextLaunchOptionIndex(index int, consumed bool) int { + if consumed { + return index + 1 + } + return index +} + +func readLaunchOptionValue(option string, args []string, index int) (string, bool, error) { + if strings.Contains(option, "=") { + parts := strings.SplitN(option, "=", 2) + if parts[1] == "" { + return "", false, missingValueArgumentError(parts[0]) + } + return parts[1], false, nil + } + if index+1 >= len(args) || isInvalidLaunchOptionValue(option, args[index+1]) { + return "", false, missingValueArgumentError(option) + } + return args[index+1], true, nil +} + +func isInvalidLaunchOptionValue(option string, value string) bool { + if option == "--max-depth" { + return isNextOptionToken(value) + } + return strings.HasPrefix(value, "-") +} diff --git a/cli/internal/cli/launch_test.go b/cli/internal/cli/launch_test.go index 1593903eef..c5f772ae2b 100644 --- a/cli/internal/cli/launch_test.go +++ b/cli/internal/cli/launch_test.go @@ -54,6 +54,41 @@ func TestParseLaunchOptionsRejectsUnityHubRegistration(t *testing.T) { } } +func TestParseLaunchOptionsRejectsUnityHubRegistrationEqualsValues(t *testing.T) { + // Verifies deprecated Unity Hub flags keep launch-specific guidance in --flag=value form. + for _, arg := range []string{"--add-unity-hub=true", "--favorite=true", "--unity-hub-entry=sample"} { + _, err := parseLaunchOptions([]string{arg}, "") + if err == nil { + t.Fatalf("expected Unity Hub registration option error for %s", arg) + } + + var argErr *argumentError + if !errors.As(err, &argErr) { + t.Fatalf("expected argumentError for %s, got %T", arg, err) + } + if argErr.message != "Native launch does not support Unity Hub registration options." { + t.Fatalf("message mismatch for %s: %s", arg, argErr.message) + } + } +} + +func TestParseLaunchOptionsRejectsMaxDepthBelowUnlimitedSentinel(t *testing.T) { + // Verifies --max-depth only accepts -1 as the unlimited sentinel. + _, err := parseLaunchOptions([]string{"--max-depth", "-2"}, "") + + if err == nil { + t.Fatal("expected invalid max-depth error") + } + + var argErr *argumentError + if !errors.As(err, &argErr) { + t.Fatalf("expected argumentError, got %T", err) + } + if argErr.expectedType != "integer >= -1" { + t.Fatalf("expectedType mismatch: %s", argErr.expectedType) + } +} + func TestParseLaunchOptionsRejectsEmptyPlatformEqualsValue(t *testing.T) { // Verifies --platform= cannot silently drop the requested build target. _, err := parseLaunchOptions([]string{"--platform="}, "") diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index 8d776be47f..2cff2aeea8 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "strings" "time" "github.com/hatayama/unity-cli-loop/cli/internal/project" @@ -21,17 +20,8 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder return 1 } - if len(remainingArgs) == 0 || isHelpRequest(remainingArgs) { - printHelpForResolvedProject(stdout, projectPath) - return 0 - } - if isVersionJSONRequest(remainingArgs) { - writeVersionJSON(stdout) - return 0 - } - if isVersionRequest(remainingArgs) { - writeLine(stdout, version) - return 0 + if handled, code := tryHandleGlobalInfoRequest(remainingArgs, projectPath, stdout); handled { + return code } command := remainingArgs[0] @@ -43,123 +33,25 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder return 1 } - if shouldHandleCompletionRequest(remainingArgs) { - completionTools := loadCompletionTools(startPath, projectPath) - if handled, code := tryHandleCompletionRequest(remainingArgs, completionTools, stdout, stderr); handled { - return code - } - } - if isUnknownLeadingOption(command) { - writeClassifiedError(stderr, &argumentError{ - message: "Unknown global option: " + command, - option: command, - nextActions: []string{"Run `uloop --help` to inspect supported global options."}, - }, errorContext{}) - return 1 - } - if handled, code := tryHandleUpdateRequest(ctx, remainingArgs, stdout, stderr); handled { - return code - } - if handled, code := tryHandleInstallRequest(ctx, remainingArgs, stdout, stderr); handled { - return code - } - if handled, code := tryHandleUninstallRequest(ctx, remainingArgs, stdout, stderr); handled { - return code - } - if handled, code := tryHandleLaunchRequest(ctx, remainingArgs, startPath, projectPath, stdout, stderr); handled { - return code - } - if handled, code := tryHandleSkillsRequest(remainingArgs, startPath, projectPath, stdout, stderr); handled { + if handled, code := tryHandlePreConnectionRequest( + ctx, + remainingArgs, + command, + commandArgs, + startPath, + projectPath, + stdout, + stderr, + ); handled { return code } - if containsHelpRequest(commandArgs) { - if handled, code := tryHandleCommandHelp(command, startPath, projectPath, stdout, stderr); handled { - return code - } - } connection, err := project.ResolveConnection(startPath, projectPath) if err != nil { writeClassifiedError(stderr, err, errorContext{command: command}) return 1 } - if isSettingsManagedNativeToolCommand(command) && - isToolDisabledByToolSettings(command, loadDisabledTools(connection.ProjectRoot)) { - writeErrorEnvelope(stderr, nativeToolDisabledError(connection.ProjectRoot, command)) - return 1 - } - switch command { - case "list": - return runList(ctx, connection, stdout, stderr) - case "sync": - return runSync(ctx, connection, stdout, stderr) - case "focus-window": - return runFocusWindow(ctx, connection.ProjectRoot, stdout, stderr) - case pausePointWaitCommandName: - return runWaitForPausePointCommand(ctx, connection, commandArgs, stdout, stderr) - case pausePointStatusUserCommandName: - return runPausePointStatusCommand(ctx, connection, commandArgs, stdout, stderr) - default: - tool, cache, ok, err := findToolForCommand(connection.ProjectRoot, command) - if err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: connection.ProjectRoot, command: command}) - return 1 - } - if !ok { - writeErrorEnvelope(stderr, unknownCommandError(command, cache, errorContext{ - projectRoot: connection.ProjectRoot, - command: command, - })) - return 1 - } - - commandArgs, dynamicCodeFilePath, err := extractDynamicCodeFileFlag(command, commandArgs) - if err != nil { - writeClassifiedError(stderr, err, errorContext{ - projectRoot: connection.ProjectRoot, - command: command, - }) - return 1 - } - - params, nestedProjectPath, err := buildToolParams(commandArgs, tool) - if err != nil { - writeClassifiedError(stderr, err, errorContext{ - projectRoot: connection.ProjectRoot, - command: command, - }) - return 1 - } - if err := applyDynamicCodeFileParam(params, dynamicCodeFilePath); err != nil { - writeClassifiedError(stderr, err, errorContext{ - projectRoot: connection.ProjectRoot, - command: command, - }) - return 1 - } - if nestedProjectPath != "" { - nestedConnection, err := project.ResolveConnection(startPath, nestedProjectPath) - if err != nil { - writeClassifiedError(stderr, err, errorContext{ - projectRoot: connection.ProjectRoot, - command: command, - }) - return 1 - } - nestedProjectPath = nestedConnection.ProjectRoot - } - if nestedProjectPath != "" && nestedProjectPath != connection.ProjectRoot { - writeErrorEnvelope(stderr, (&argumentError{ - message: "--project-path must target the same Unity project for this command", - option: "--project-path", - expectedType: "path", - command: command, - nextActions: []string{"Use one `--project-path ` value for the target Unity project."}, - }).toCLIError(errorContext{projectRoot: connection.ProjectRoot, command: command})) - return 1 - } - return runTool(ctx, connection, command, params, stdout, stderr) - } + return runResolvedProjectCommand(ctx, connection, command, commandArgs, startPath, stdout, stderr) } func writeVersionJSON(stdout io.Writer) { @@ -173,10 +65,6 @@ func writeVersionJSON(stdout io.Writer) { writeLine(stdout, string(content)) } -func isUnknownLeadingOption(command string) bool { - return strings.HasPrefix(command, "-") -} - func runTool(ctx context.Context, connection unityipc.Connection, command string, params map[string]any, stdout io.Writer, stderr io.Writer) int { if shouldWaitForCompileDomainReload(command, params) { return runCompileWithDomainReloadWait(ctx, connection, params, stdout, stderr) diff --git a/cli/internal/cli/run_dispatch.go b/cli/internal/cli/run_dispatch.go new file mode 100644 index 0000000000..99a12be233 --- /dev/null +++ b/cli/internal/cli/run_dispatch.go @@ -0,0 +1,199 @@ +package cli + +import ( + "context" + "io" + "strings" + + "github.com/hatayama/unity-cli-loop/cli/internal/project" + "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +) + +func tryHandleGlobalInfoRequest(args []string, projectPath string, stdout io.Writer) (bool, int) { + if len(args) == 0 || isHelpRequest(args) { + printHelpForResolvedProject(stdout, projectPath) + return true, 0 + } + if isVersionJSONRequest(args) { + writeVersionJSON(stdout) + return true, 0 + } + if isVersionRequest(args) { + writeLine(stdout, version) + return true, 0 + } + return false, 0 +} + +func tryHandlePreConnectionRequest( + ctx context.Context, + remainingArgs []string, + command string, + commandArgs []string, + startPath string, + projectPath string, + stdout io.Writer, + stderr io.Writer, +) (bool, int) { + if shouldHandleCompletionRequest(remainingArgs) { + completionTools := loadCompletionTools(startPath, projectPath) + if handled, code := tryHandleCompletionRequest(remainingArgs, completionTools, stdout, stderr); handled { + return true, code + } + } + if isUnknownLeadingOption(command) { + writeClassifiedError(stderr, &argumentError{ + message: "Unknown global option: " + command, + option: command, + nextActions: []string{"Run `uloop --help` to inspect supported global options."}, + }, errorContext{}) + return true, 1 + } + if handled, code := tryHandleUpdateRequest(ctx, remainingArgs, stdout, stderr); handled { + return true, code + } + if handled, code := tryHandleInstallRequest(ctx, remainingArgs, stdout, stderr); handled { + return true, code + } + if handled, code := tryHandleUninstallRequest(ctx, remainingArgs, stdout, stderr); handled { + return true, code + } + if handled, code := tryHandleLaunchRequest(ctx, remainingArgs, startPath, projectPath, stdout, stderr); handled { + return true, code + } + if handled, code := tryHandleSkillsRequest(remainingArgs, startPath, projectPath, stdout, stderr); handled { + return true, code + } + if containsHelpRequest(commandArgs) { + if handled, code := tryHandleCommandHelp(command, startPath, projectPath, stdout, stderr); handled { + return true, code + } + } + return false, 0 +} + +func runResolvedProjectCommand( + ctx context.Context, + connection unityipc.Connection, + command string, + commandArgs []string, + startPath string, + stdout io.Writer, + stderr io.Writer, +) int { + if isSettingsManagedNativeToolCommand(command) && + isToolDisabledByToolSettings(command, loadDisabledTools(connection.ProjectRoot)) { + writeErrorEnvelope(stderr, nativeToolDisabledError(connection.ProjectRoot, command)) + return 1 + } + switch command { + case "list": + return runList(ctx, connection, stdout, stderr) + case "sync": + return runSync(ctx, connection, stdout, stderr) + case "focus-window": + return runFocusWindow(ctx, connection.ProjectRoot, stdout, stderr) + case pausePointWaitCommandName: + return runWaitForPausePointCommand(ctx, connection, commandArgs, stdout, stderr) + case pausePointStatusUserCommandName: + return runPausePointStatusCommand(ctx, connection, commandArgs, stdout, stderr) + default: + return runDynamicProjectTool(ctx, connection, command, commandArgs, startPath, stdout, stderr) + } +} + +func runDynamicProjectTool( + ctx context.Context, + connection unityipc.Connection, + command string, + commandArgs []string, + startPath string, + stdout io.Writer, + stderr io.Writer, +) int { + tool, cache, ok, err := findToolForCommand(connection.ProjectRoot, command) + if err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: connection.ProjectRoot, command: command}) + return 1 + } + if !ok { + writeErrorEnvelope(stderr, unknownCommandError(command, cache, errorContext{ + projectRoot: connection.ProjectRoot, + command: command, + })) + return 1 + } + + params, nestedProjectPath, ok := prepareDynamicToolParams( + command, + commandArgs, + tool, + connection, + startPath, + stderr, + ) + if !ok { + return 1 + } + if nestedProjectPath != "" && nestedProjectPath != connection.ProjectRoot { + writeErrorEnvelope(stderr, (&argumentError{ + message: "--project-path must target the same Unity project for this command", + option: "--project-path", + expectedType: "path", + command: command, + nextActions: []string{"Use one `--project-path ` value for the target Unity project."}, + }).toCLIError(errorContext{projectRoot: connection.ProjectRoot, command: command})) + return 1 + } + return runTool(ctx, connection, command, params, stdout, stderr) +} + +func prepareDynamicToolParams( + command string, + commandArgs []string, + tool toolDefinition, + connection unityipc.Connection, + startPath string, + stderr io.Writer, +) (map[string]any, string, bool) { + commandArgs, dynamicCodeFilePath, err := extractDynamicCodeFileFlag(command, commandArgs) + if err != nil { + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: command, + }) + return nil, "", false + } + + params, nestedProjectPath, err := buildToolParams(commandArgs, tool) + if err != nil { + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: command, + }) + return nil, "", false + } + if err := applyDynamicCodeFileParam(params, dynamicCodeFilePath); err != nil { + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: command, + }) + return nil, "", false + } + if nestedProjectPath == "" { + return params, "", true + } + nestedConnection, err := project.ResolveConnection(startPath, nestedProjectPath) + if err != nil { + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: command, + }) + return nil, "", false + } + return params, nestedConnection.ProjectRoot, true +} + +func isUnknownLeadingOption(command string) bool { + return strings.HasPrefix(command, "-") +} diff --git a/cli/internal/cli/skills.go b/cli/internal/cli/skills.go index 33dcf26531..38c1ad90d1 100644 --- a/cli/internal/cli/skills.go +++ b/cli/internal/cli/skills.go @@ -126,21 +126,7 @@ func tryHandleSkillsRequest(args []string, startPath string, globalProjectPath s } if isV3MigrationSkillSubcommand(subcommand) { - if len(options.targets) == 0 { - printSkillsTargetGuidance(subcommand, stdout) - return true, 0 - } - switch subcommand { - case "install-v3-migration": - skills, err := collectV3MigrationSkillDefinition(projectRoot) - if err != nil { - writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: skillsCommandName}) - return true, 1 - } - return true, runV3MigrationSkillInstall(projectRoot, skills, options, stdout, stderr) - case "uninstall-v3-migration": - return true, runV3MigrationSkillUninstall(projectRoot, options, stdout, stderr) - } + return true, runV3MigrationSkillsSubcommand(subcommand, projectRoot, options, stdout, stderr) } skills, err := collectSkillDefinitions(projectRoot) @@ -149,23 +135,7 @@ func tryHandleSkillsRequest(args []string, startPath string, globalProjectPath s return true, 1 } - switch subcommand { - case "list": - return true, runSkillsList(projectRoot, skills, options, stdout, stderr) - case "install": - if len(options.targets) == 0 { - printSkillsTargetGuidance("install", stdout) - return true, 0 - } - return true, runSkillsInstall(projectRoot, skills, options, stdout, stderr) - case "uninstall": - if len(options.targets) == 0 { - printSkillsTargetGuidance("uninstall", stdout) - return true, 0 - } - return true, runSkillsUninstall(projectRoot, skills, options, stdout, stderr) - } - return true, 1 + return true, runSkillsSubcommand(subcommand, projectRoot, skills, options, stdout, stderr) } func parseSkillsOptions(args []string) (skillCommandOptions, error) { @@ -410,39 +380,11 @@ func installSkillsForTarget(projectRoot string, target skillTarget, skills []ski } } - disabledTools := []string{} - if !global { - disabledTools = loadDisabledTools(projectRoot) - } + disabledTools := loadDisabledToolsForSkillInstall(projectRoot, global) for _, skill := range skills { - if isSkillDisabledByToolSettings(skill, disabledTools) { - if err := removeSkillFromAllLayouts(baseDir, skill.name); err != nil { - return skillInstallResult{}, err - } - continue - } - - status, err := getSkillStatus(baseDir, skill, grouped) - if err != nil { - return skillInstallResult{}, err - } - destinationDir := getPreferredSkillDir(baseDir, skill.name, grouped) - if status == "installed" { - result.skipped++ - continue - } - if err := syncSkillDirectory(skill.sourceDirectory, destinationDir); err != nil { + if err := installSkillForTarget(baseDir, skill, disabledTools, grouped, &result); err != nil { return skillInstallResult{}, err } - alternateDir := getPreferredSkillDir(baseDir, skill.name, !grouped) - if err := os.RemoveAll(alternateDir); err != nil { - return skillInstallResult{}, err - } - if status == "outdated" { - result.updated++ - continue - } - result.installed++ } if !grouped { if err := removeEmptyDir(getPreferredSkillDir(baseDir, managedSkillsDir, false)); err != nil { @@ -452,6 +394,49 @@ func installSkillsForTarget(projectRoot string, target skillTarget, skills []ski return result, nil } +func loadDisabledToolsForSkillInstall(projectRoot string, global bool) []string { + if global { + return []string{} + } + return loadDisabledTools(projectRoot) +} + +func installSkillForTarget( + baseDir string, + skill skillDefinition, + disabledTools []string, + grouped bool, + result *skillInstallResult, +) error { + if isSkillDisabledByToolSettings(skill, disabledTools) { + return removeSkillFromAllLayouts(baseDir, skill.name) + } + + status, err := getSkillStatus(baseDir, skill, grouped) + if err != nil { + return err + } + if status == "installed" { + result.skipped++ + return nil + } + + destinationDir := getPreferredSkillDir(baseDir, skill.name, grouped) + if err := syncSkillDirectory(skill.sourceDirectory, destinationDir); err != nil { + return err + } + alternateDir := getPreferredSkillDir(baseDir, skill.name, !grouped) + if err := os.RemoveAll(alternateDir); err != nil { + return err + } + if status == "outdated" { + result.updated++ + return nil + } + result.installed++ + return nil +} + func uninstallSkillsForTarget(projectRoot string, target skillTarget, skills []skillDefinition, global bool, grouped bool) (int, int, error) { removed := 0 notFound := 0 diff --git a/cli/internal/cli/skills_dispatch.go b/cli/internal/cli/skills_dispatch.go new file mode 100644 index 0000000000..3e4c60501a --- /dev/null +++ b/cli/internal/cli/skills_dispatch.go @@ -0,0 +1,78 @@ +package cli + +import "io" + +func runV3MigrationSkillsSubcommand( + subcommand string, + projectRoot string, + options skillCommandOptions, + stdout io.Writer, + stderr io.Writer, +) int { + if len(options.targets) == 0 { + printSkillsTargetGuidance(subcommand, stdout) + return 0 + } + + switch subcommand { + case "install-v3-migration": + skills, err := collectV3MigrationSkillDefinition(projectRoot) + if err != nil { + writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: skillsCommandName}) + return 1 + } + return runV3MigrationSkillInstall(projectRoot, skills, options, stdout, stderr) + case "uninstall-v3-migration": + return runV3MigrationSkillUninstall(projectRoot, options, stdout, stderr) + default: + return 1 + } +} + +func runSkillsSubcommand( + subcommand string, + projectRoot string, + skills []skillDefinition, + options skillCommandOptions, + stdout io.Writer, + stderr io.Writer, +) int { + switch subcommand { + case "list": + return runSkillsList(projectRoot, skills, options, stdout, stderr) + case "install": + return runSkillsInstallWithGuidance(projectRoot, skills, options, stdout, stderr) + case "uninstall": + return runSkillsUninstallWithGuidance(projectRoot, skills, options, stdout, stderr) + default: + return 1 + } +} + +func runSkillsInstallWithGuidance( + projectRoot string, + skills []skillDefinition, + options skillCommandOptions, + stdout io.Writer, + stderr io.Writer, +) int { + if len(options.targets) == 0 { + printSkillsTargetGuidance("install", stdout) + return 0 + } + return runSkillsInstall(projectRoot, skills, options, stdout, stderr) +} + +func runSkillsUninstallWithGuidance( + projectRoot string, + skills []skillDefinition, + options skillCommandOptions, + stdout io.Writer, + stderr io.Writer, +) int { + if len(options.targets) == 0 { + printSkillsTargetGuidance("uninstall", stdout) + return 0 + } + return runSkillsUninstall(projectRoot, skills, options, stdout, stderr) +} diff --git a/cli/internal/cli/tools.go b/cli/internal/cli/tools.go index 10d6ba3846..34b62bdd9a 100644 --- a/cli/internal/cli/tools.go +++ b/cli/internal/cli/tools.go @@ -254,14 +254,7 @@ func findProperty(tool toolDefinition, kebabName string) (string, toolProperty, func convertValue(value string, property toolProperty, option string) (any, error) { switch strings.ToLower(property.Type) { case "boolean": - switch strings.ToLower(value) { - case "true": - return true, nil - case "false": - return false, nil - default: - return nil, invalidValueArgumentError(option, value, "boolean") - } + return convertBooleanValue(value, option) case "integer": parsed, err := strconv.Atoi(value) if err != nil { @@ -275,28 +268,51 @@ func convertValue(value string, property toolProperty, option string) (any, erro } return parsed, nil case "array": - if strings.HasPrefix(value, "[") { - var parsed []any - if err := json.Unmarshal([]byte(value), &parsed); err != nil { - return nil, invalidValueArgumentError(option, value, "array") - } - return parsed, nil - } - parts := strings.Split(value, ",") - result := make([]string, 0, len(parts)) - for _, part := range parts { - result = append(result, strings.TrimSpace(part)) - } - return result, nil + return convertArrayValue(value, option) case "object": - var parsed map[string]any + return convertObjectValue(value, option) + default: + return value, nil + } +} + +func convertBooleanValue(value string, option string) (bool, error) { + switch strings.ToLower(value) { + case "true": + return true, nil + case "false": + return false, nil + default: + return false, invalidValueArgumentError(option, value, "boolean") + } +} + +func convertArrayValue(value string, option string) (any, error) { + if strings.HasPrefix(value, "[") { + var parsed []any if err := json.Unmarshal([]byte(value), &parsed); err != nil { - return nil, invalidValueArgumentError(option, value, "object") + return nil, invalidValueArgumentError(option, value, "array") } return parsed, nil - default: - return value, nil } + + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + result = append(result, strings.TrimSpace(part)) + } + return result, nil +} + +func convertObjectValue(value string, option string) (map[string]any, error) { + var parsed map[string]any + if err := json.Unmarshal([]byte(value), &parsed); err != nil { + return nil, invalidValueArgumentError(option, value, "object") + } + if parsed == nil { + return nil, invalidValueArgumentError(option, value, "object") + } + return parsed, nil } func optionNameForProperty(toolName string, propertyName string, property toolProperty) string { diff --git a/cli/internal/cli/tools_test.go b/cli/internal/cli/tools_test.go index a0e5c64e83..9d2e5454c8 100644 --- a/cli/internal/cli/tools_test.go +++ b/cli/internal/cli/tools_test.go @@ -48,6 +48,24 @@ func TestBuildToolParamsConvertsSchemaTypes(t *testing.T) { } } +func TestBuildToolParamsRejectsNullObjectValue(t *testing.T) { + // Tests that object schema arguments must parse to JSON objects rather than null. + tool := toolDefinition{ + Name: "sample-tool", + InputSchema: inputSchema{ + Properties: map[string]toolProperty{ + "Payload": {Type: "object"}, + }, + }, + } + + _, _, err := buildToolParams([]string{"--payload", "null"}, tool) + + if err == nil { + t.Fatal("expected null object value to be rejected") + } +} + // Tests that default-enabled boolean tool arguments are disabled through --no-* flags. func TestBuildToolParamsConvertsDefaultTrueBooleanToNegatedFlag(t *testing.T) { tool := toolDefinition{ diff --git a/cli/internal/unityipc/client.go b/cli/internal/unityipc/client.go index 064928ba0a..1985e993e6 100644 --- a/cli/internal/unityipc/client.go +++ b/cli/internal/unityipc/client.go @@ -229,80 +229,148 @@ func (client *Client) SendWithProgressOutcomeAcceptContext( progress(ProgressEventAccepted) } - cancelAccept() - heartbeatSilence := client.getHeartbeatSilence(response.ULoop.HeartbeatIntervalSeconds) - absoluteDeadline := time.Now().Add(client.getResponseTimeout()) - if err := applyPostAcceptDeadline(conn, heartbeatSilence, absoluteDeadline); err != nil { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, err + return client.readAcceptedResponse( + ctx, + conn, + reader, + progress, + cancelAccept, + startedAt, + timing, + outcome, + response, + ) + } + + return finishRPCResponse(response, outcome, timing, startedAt) +} + +func (client *Client) readAcceptedResponse( + ctx context.Context, + conn net.Conn, + reader *bufio.Reader, + progress ProgressFunc, + cancelAccept context.CancelFunc, + startedAt time.Time, + timing UnitySendTiming, + outcome UnitySendOutcome, + response rpcResponse, +) (UnitySendOutcome, error) { + cancelAccept() + heartbeatSilence := client.getHeartbeatSilence(response.ULoop.HeartbeatIntervalSeconds) + absoluteDeadline := time.Now().Add(client.getResponseTimeout()) + if err := applyPostAcceptDeadline(conn, heartbeatSilence, absoluteDeadline); err != nil { + return finishOutcomeWithError(outcome, timing, startedAt, err) + } + stopCancelWatcher := watchConnectionCancellation(ctx, conn) + defer stopCancelWatcher() + + for { + nextResponse, err := readRPCResponse(reader, &timing) + if err != nil { + return client.finishAcceptedReadError( + ctx, + outcome, + timing, + startedAt, + heartbeatSilence, + absoluteDeadline, + err, + ) + } + response = nextResponse + if response.ULoop.Phase != rpcResponsePhaseHeartbeat { + break } - stopCancelWatcher := watchConnectionCancellation(ctx, conn) - defer stopCancelWatcher() - - for { - response, err = readRPCResponse(reader, &timing) - if err != nil { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - if ctx.Err() != nil { - return outcome, ctx.Err() - } - if heartbeatSilence > 0 && isDeadlineExpiry(err) && time.Now().Before(absoluteDeadline) { - return outcome, fmt.Errorf( - "no response or heartbeat from Unity for %s; the connection or server stalled: %w", - heartbeatSilence, err) - } - return outcome, err - } - if response.ULoop.Phase != rpcResponsePhaseHeartbeat { - break - } - if ctx.Err() != nil { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, ctx.Err() - } - - stallSeconds := response.ULoop.MainThreadStallSeconds - if stallSeconds >= mainThreadStallProgressThresholdSeconds { - if client.mainThreadStallHandler != nil { - client.mainThreadStallHandler(stallSeconds) - } - if progress != nil { - progress(fmt.Sprintf( - "Unity main thread stuck %.0fs; check modal/long operation...", - stallSeconds)) - } - } - if stallSeconds >= client.getMainThreadStallLimit().Seconds() { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, &EditorUnresponsiveError{StallSeconds: stallSeconds} - } - if heartbeatSilence > 0 { - if err := applyPostAcceptDeadline(conn, heartbeatSilence, absoluteDeadline); err != nil { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, err - } - } + if ctx.Err() != nil { + return finishOutcomeWithError(outcome, timing, startedAt, ctx.Err()) + } + if err := client.handleHeartbeatResponse( + conn, + response, + progress, + heartbeatSilence, + absoluteDeadline, + ); err != nil { + return finishOutcomeWithError(outcome, timing, startedAt, err) } } + return finishRPCResponse(response, outcome, timing, startedAt) +} + +func (client *Client) finishAcceptedReadError( + ctx context.Context, + outcome UnitySendOutcome, + timing UnitySendTiming, + startedAt time.Time, + heartbeatSilence time.Duration, + absoluteDeadline time.Time, + err error, +) (UnitySendOutcome, error) { + if ctx.Err() != nil { + return finishOutcomeWithError(outcome, timing, startedAt, ctx.Err()) + } + if heartbeatSilence > 0 && isDeadlineExpiry(err) && time.Now().Before(absoluteDeadline) { + return finishOutcomeWithError( + outcome, + timing, + startedAt, + fmt.Errorf( + "no response or heartbeat from Unity for %s; the connection or server stalled: %w", + heartbeatSilence, + err), + ) + } + return finishOutcomeWithError(outcome, timing, startedAt, err) +} + +func (client *Client) handleHeartbeatResponse( + conn net.Conn, + response rpcResponse, + progress ProgressFunc, + heartbeatSilence time.Duration, + absoluteDeadline time.Time, +) error { + stallSeconds := response.ULoop.MainThreadStallSeconds + if stallSeconds >= mainThreadStallProgressThresholdSeconds { + client.reportMainThreadStall(stallSeconds, progress) + } + if stallSeconds >= client.getMainThreadStallLimit().Seconds() { + return &EditorUnresponsiveError{StallSeconds: stallSeconds} + } + if heartbeatSilence > 0 { + return applyPostAcceptDeadline(conn, heartbeatSilence, absoluteDeadline) + } + return nil +} + +func (client *Client) reportMainThreadStall(stallSeconds float64, progress ProgressFunc) { + if client.mainThreadStallHandler != nil { + client.mainThreadStallHandler(stallSeconds) + } + if progress != nil { + progress(fmt.Sprintf( + "Unity main thread stuck %.0fs; check modal/long operation...", + stallSeconds)) + } +} + +func finishRPCResponse( + response rpcResponse, + outcome UnitySendOutcome, + timing UnitySendTiming, + startedAt time.Time, +) (UnitySendOutcome, error) { if response.Error != nil { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, &RPCError{ + return finishOutcomeWithError(outcome, timing, startedAt, &RPCError{ Code: response.Error.Code, Message: response.Error.Message, Data: response.Error.Data, - } + }) } if len(response.Result) == 0 { - timing.Total = time.Since(startedAt) - outcome.Timing = timing - return outcome, fmt.Errorf("UNITY_NO_RESPONSE") + return finishOutcomeWithError(outcome, timing, startedAt, fmt.Errorf("UNITY_NO_RESPONSE")) } outcome.Result = response.Result @@ -311,6 +379,17 @@ func (client *Client) SendWithProgressOutcomeAcceptContext( return outcome, nil } +func finishOutcomeWithError( + outcome UnitySendOutcome, + timing UnitySendTiming, + startedAt time.Time, + err error, +) (UnitySendOutcome, error) { + timing.Total = time.Since(startedAt) + outcome.Timing = timing + return outcome, err +} + func (client *Client) getAcceptTimeout() time.Duration { if client.acceptTimeout > 0 { return client.acceptTimeout diff --git a/cli/internal/unityipc/client_test.go b/cli/internal/unityipc/client_test.go index fba0ed234e..e21de13490 100644 --- a/cli/internal/unityipc/client_test.go +++ b/cli/internal/unityipc/client_test.go @@ -56,35 +56,7 @@ func TestSendIncludesCliVersionWithoutProjectIdentityMetadata(t *testing.T) { captured := make(chan map[string]any, 1) serverErr := make(chan error, 1) - go func() { - conn, err := listener.Accept() - if err != nil { - serverErr <- err - return - } - defer func() { - _ = conn.Close() - }() - - payload, err := Read(bufio.NewReader(conn)) - if err != nil { - serverErr <- err - return - } - - var request map[string]any - if err := json.Unmarshal(payload, &request); err != nil { - serverErr <- err - return - } - captured <- request - - response := []byte(`{"jsonrpc":"2.0","result":{"ok":true},"id":1}`) - if err := Write(conn, response); err != nil { - serverErr <- err - return - } - }() + go captureClientMetadataRequest(listener, captured, serverErr) connection := Connection{ Endpoint: Endpoint{ @@ -102,25 +74,65 @@ func TestSendIncludesCliVersionWithoutProjectIdentityMetadata(t *testing.T) { case err := <-serverErr: t.Fatalf("server failed: %v", err) case request := <-captured: - if _, ok := request["x-uloop"]; ok { - t.Fatalf("request should not include x-uloop metadata: %#v", request["x-uloop"]) - } - metadata, ok := request["uloop"].(map[string]any) - if !ok { - t.Fatalf("request should include uloop metadata: %#v", request) - } - if metadata["cliVersion"] != "3.0.0-beta.6" { - t.Fatalf("cli version metadata mismatch: %#v", metadata) - } - if metadata["protocolVersion"] != float64(clicontract.Current.ProtocolVersion) { - t.Fatalf("protocol version metadata mismatch: %#v", metadata) - } - if metadata["acceptsDispatchAck"] != true { - t.Fatalf("dispatch ack metadata mismatch: %#v", metadata) - } - if metadata["acceptsHeartbeat"] != true { - t.Fatalf("heartbeat metadata mismatch: %#v", metadata) - } + assertClientMetadataRequest(t, request) + } +} + +func captureClientMetadataRequest( + listener net.Listener, + captured chan<- map[string]any, + serverErr chan<- error, +) { + conn, err := listener.Accept() + if err != nil { + serverErr <- err + return + } + defer func() { + _ = conn.Close() + }() + + payload, err := Read(bufio.NewReader(conn)) + if err != nil { + serverErr <- err + return + } + + var request map[string]any + if err := json.Unmarshal(payload, &request); err != nil { + serverErr <- err + return + } + captured <- request + + response := []byte(`{"jsonrpc":"2.0","result":{"ok":true},"id":1}`) + if err := Write(conn, response); err != nil { + serverErr <- err + return + } +} + +func assertClientMetadataRequest(t *testing.T, request map[string]any) { + t.Helper() + + if _, ok := request["x-uloop"]; ok { + t.Fatalf("request should not include x-uloop metadata: %#v", request["x-uloop"]) + } + metadata, ok := request["uloop"].(map[string]any) + if !ok { + t.Fatalf("request should include uloop metadata: %#v", request) + } + if metadata["cliVersion"] != "3.0.0-beta.6" { + t.Fatalf("cli version metadata mismatch: %#v", metadata) + } + if metadata["protocolVersion"] != float64(clicontract.Current.ProtocolVersion) { + t.Fatalf("protocol version metadata mismatch: %#v", metadata) + } + if metadata["acceptsDispatchAck"] != true { + t.Fatalf("dispatch ack metadata mismatch: %#v", metadata) + } + if metadata["acceptsHeartbeat"] != true { + t.Fatalf("heartbeat metadata mismatch: %#v", metadata) } } diff --git a/scripts/check-code-complexity.sh b/scripts/check-code-complexity.sh index e4c0a6bda5..e2a6a6c6b0 100755 --- a/scripts/check-code-complexity.sh +++ b/scripts/check-code-complexity.sh @@ -2,7 +2,7 @@ set -eu ROOT_DIR=$(CDPATH= cd "$(dirname "$0")/.." && pwd) -MAX_COMPLEXITY=${CODE_COMPLEXITY_MAX_COMPLEXITY:-25} +MAX_COMPLEXITY=${CODE_COMPLEXITY_MAX_COMPLEXITY:-15} FAIL_ON_EXCEEDED=$(printf '%s' "${CODE_COMPLEXITY_FAIL_ON_EXCEEDED:-false}" | tr '[:upper:]' '[:lower:]') GO_CONFIG="$ROOT_DIR/cli/.golangci-complexity.yml" TEMP_GO_CONFIG= @@ -18,7 +18,7 @@ cleanup() { trap cleanup 0 1 2 15 -if [ "$MAX_COMPLEXITY" != "25" ]; then +if [ "$MAX_COMPLEXITY" != "15" ]; then TEMP_GO_CONFIG="$ROOT_DIR/cli/.golangci-complexity.$$.yml" awk -v max_complexity="$MAX_COMPLEXITY" ' $1 == "max-complexity:" { diff --git a/tools/UnityCliLoop.CodeComplexity/CodeComplexityAnalyzerRunner.cs b/tools/UnityCliLoop.CodeComplexity/CodeComplexityAnalyzerRunner.cs index 92b0757a3c..1f62380c2b 100644 --- a/tools/UnityCliLoop.CodeComplexity/CodeComplexityAnalyzerRunner.cs +++ b/tools/UnityCliLoop.CodeComplexity/CodeComplexityAnalyzerRunner.cs @@ -132,7 +132,7 @@ private static ImmutableArray CreateMetadataReferences() private static CodeMetricsAdditionalText CreateCodeMetricsConfig(int maxComplexity) { string defaultConfigPath = Path.Combine(AppContext.BaseDirectory, "CodeMetricsConfig.txt"); - if (maxComplexity == 25 && File.Exists(defaultConfigPath)) + if (maxComplexity == 15 && File.Exists(defaultConfigPath)) { return CodeMetricsAdditionalText.FromFile(defaultConfigPath); } diff --git a/tools/UnityCliLoop.CodeComplexity/CodeComplexityModels.cs b/tools/UnityCliLoop.CodeComplexity/CodeComplexityModels.cs index 2dfc84dcf7..f6de81887e 100644 --- a/tools/UnityCliLoop.CodeComplexity/CodeComplexityModels.cs +++ b/tools/UnityCliLoop.CodeComplexity/CodeComplexityModels.cs @@ -45,7 +45,7 @@ public static CodeComplexityOptions Default(string rootPath) { return new CodeComplexityOptions( rootPath, - maxComplexity: 25, + maxComplexity: 15, includeNonProduction: false, ReportFormat.Table, failOnExceeded: false); diff --git a/tools/UnityCliLoop.CodeComplexity/CodeMetricsConfig.txt b/tools/UnityCliLoop.CodeComplexity/CodeMetricsConfig.txt index ab59995aad..b5bf6fd7f6 100644 --- a/tools/UnityCliLoop.CodeComplexity/CodeMetricsConfig.txt +++ b/tools/UnityCliLoop.CodeComplexity/CodeMetricsConfig.txt @@ -1 +1 @@ -CA1502: 25 +CA1502: 15 diff --git a/tools/UnityCliLoop.CodeComplexity/CommandLineOptions.cs b/tools/UnityCliLoop.CodeComplexity/CommandLineOptions.cs index 0855c07dd3..d9875b7632 100644 --- a/tools/UnityCliLoop.CodeComplexity/CommandLineOptions.cs +++ b/tools/UnityCliLoop.CodeComplexity/CommandLineOptions.cs @@ -165,7 +165,7 @@ public static string CreateHelpText() "", "Options:", " --root Repository root. Defaults to current directory.", - " --max-complexity CA1502 threshold. Defaults to 25.", + " --max-complexity CA1502 threshold. Defaults to 15.", " --include-non-production true|false Include Assets and tests sources. Defaults to false.", " --format table|json Output format. Defaults to table.", " --fail-on-exceeded true|false Exit 1 when CA1502 diagnostics exist. Defaults to false.");