diff --git a/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs b/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs index 166a31c49a..c36f4165ed 100644 --- a/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs +++ b/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Dev diff --git a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs index 702dbcbf70..25d3f20943 100644 --- a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs +++ b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.ToolContracts; diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index 9b15e98e2b..a99beb808e 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -13,6 +13,7 @@ using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.Infrastructure; using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { @@ -175,13 +176,13 @@ public async Task ProcessRequest_WhenFirstToolWaitsForMainThread_ReturnsServerBu MainThreadSwitcher.RegisterService(dispatcher); UnityCliLoopEditorStateSnapshot.SetPlayStateForTesting(isPlaying: false, isPaused: false); - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new SingleFlightTestTool()); Task firstResponseTask = null; @@ -218,7 +219,7 @@ public async Task ProcessRequest_WhenFirstToolWaitsForMainThread_ReturnsServerBu await DrainTaskIfNeeded(firstResponseTask); await DrainTaskIfNeeded(secondResponseTask); UnityCliLoopEditorStateSnapshot.ClearForTesting(); - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } } @@ -231,13 +232,13 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows MainThreadSwitcher.RegisterService(dispatcher); UnityCliLoopEditorStateSnapshot.SetPlayStateForTesting(isPlaying: false, isPaused: false); - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new ExecuteDynamicCodeTestTool()); service.RegisterCustomTool(new SingleFlightTestTool()); @@ -286,7 +287,7 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows await DrainTaskIfNeeded(secondDynamicCodeTask); await DrainTaskIfNeeded(otherToolTask); UnityCliLoopEditorStateSnapshot.ClearForTesting(); - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } } @@ -295,13 +296,13 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows public async Task ProcessRequest_WhenCompileWaitsForDomainReload_KeepsAcceptedRequestAliveAfterDisconnect() { // Verifies long compile waits are allowed to persist their result after the CLI response deadline closes. - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new CompileDispatchPolicyTestTool()); bool cancelOnClientDisconnect = true; @@ -326,7 +327,7 @@ public async Task ProcessRequest_WhenCompileWaitsForDomainReload_KeepsAcceptedRe } finally { - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); } } @@ -334,13 +335,13 @@ public async Task ProcessRequest_WhenCompileWaitsForDomainReload_KeepsAcceptedRe public async Task ProcessRequest_WhenCompileOmitsReloadWait_KeepsAcceptedRequestAliveAfterDisconnect() { // Verifies missing compile reload-wait params preserve the default wait contract. - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new CompileDispatchPolicyTestTool()); bool cancelOnClientDisconnect = true; @@ -362,7 +363,7 @@ public async Task ProcessRequest_WhenCompileOmitsReloadWait_KeepsAcceptedRequest } finally { - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); } } @@ -370,13 +371,13 @@ public async Task ProcessRequest_WhenCompileOmitsReloadWait_KeepsAcceptedRequest public async Task ProcessRequest_WhenCompileDoesNotWaitForDomainReload_CancelsOnClientDisconnect() { // Verifies fire-and-forget compile requests still cancel when the CLI connection goes away. - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new CompileDispatchPolicyTestTool()); bool cancelOnClientDisconnect = false; @@ -401,7 +402,7 @@ public async Task ProcessRequest_WhenCompileDoesNotWaitForDomainReload_CancelsOn } finally { - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); } } @@ -409,13 +410,13 @@ public async Task ProcessRequest_WhenCompileDoesNotWaitForDomainReload_CancelsOn public async Task ProcessRequest_WhenCompileUsesCamelCaseNoReloadWait_CancelsOnClientDisconnect() { // Verifies JSON-RPC compile dispatch policy matches the camelCase tool deserializer contract. - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new CompileDispatchPolicyTestTool()); bool cancelOnClientDisconnect = false; @@ -440,7 +441,7 @@ public async Task ProcessRequest_WhenCompileUsesCamelCaseNoReloadWait_CancelsOnC } finally { - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); } } @@ -451,13 +452,13 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs CapturingMainThreadDispatcher dispatcher = new(); MainThreadSwitcher.RegisterService(dispatcher); - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); string hierarchyResponse = null; Task hierarchyResponseTask = null; @@ -502,7 +503,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs await DrainTaskIfNeeded(hierarchyResponseTask); await DrainTaskIfNeeded(logsResponseTask); DeleteHierarchyFileFromResponse(hierarchyResponse); - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } } @@ -547,13 +548,13 @@ public async Task ProcessRequest_WhenMainThreadSwitchIsCanceled_ReleasesExecutio CapturingMainThreadDispatcher dispatcher = new(); MainThreadSwitcher.RegisterService(dispatcher); - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); service.RegisterCustomTool(new SingleFlightTestTool()); using CancellationTokenSource cancellationSource = new CancellationTokenSource(); @@ -590,7 +591,7 @@ public async Task ProcessRequest_WhenMainThreadSwitchIsCanceled_ReleasesExecutio dispatcher.RunContinuations(); await DrainTaskIfNeeded(canceledResponseTask); await DrainTaskIfNeeded(secondResponseTask); - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } } diff --git a/Assets/Tests/Editor/MainThreadSwitcherTests.cs b/Assets/Tests/Editor/MainThreadSwitcherTests.cs index 64c0a9f03f..f475acb329 100644 --- a/Assets/Tests/Editor/MainThreadSwitcherTests.cs +++ b/Assets/Tests/Editor/MainThreadSwitcherTests.cs @@ -5,7 +5,7 @@ using UnityEngine; using UnityEngine.TestTools; -using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { diff --git a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs index 6851e8f3e5..08a80c4a22 100644 --- a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs +++ b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs @@ -65,7 +65,7 @@ public void PlatformResultTypes_WhenLoaded_CompileUnderDomainAssembly() Assert.That(validationResultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); Assert.That(forceCompileUnknownResultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); - Assert.That(serviceResultAssemblyName, Is.EqualTo(DomainAssemblyName)); + Assert.That(serviceResultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); } [Test] @@ -233,7 +233,7 @@ public void ToolCorePolicy_WhenLoaded_CompilesUnderDomainAssembly() Assert.That(registryAssemblyName, Is.EqualTo(DomainAssemblyName)); Assert.That(internalToolNameProviderAssemblyName, Is.EqualTo(DomainAssemblyName)); Assert.That(classifierAssemblyName, Is.EqualTo(DomainAssemblyName)); - Assert.That(catalogItemAssemblyName, Is.EqualTo(DomainAssemblyName)); + Assert.That(catalogItemAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); Assert.That(domainReloadServiceAssemblyName, Is.EqualTo(DomainAssemblyName)); } @@ -384,18 +384,18 @@ public void FindGameObjectsUseCase_WhenLoaded_CompilesUnderApplicationAssembly() } [Test] - public void ScreenshotTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + public void ScreenshotContracts_WhenLoaded_CompileUnderToolContractsAssembly() { - // Tests that bundled screenshot implementation types stay inside the first-party tool assembly. + // Tests that screenshot public contracts are exposed from the shared ToolContracts assembly. string serviceAssemblyName = typeof(IUnityCliLoopScreenshotService).Assembly.GetName().Name; string requestAssemblyName = typeof(UnityCliLoopScreenshotRequest).Assembly.GetName().Name; string resultAssemblyName = typeof(UnityCliLoopScreenshotResult).Assembly.GetName().Name; string elementAssemblyName = typeof(UIElementInfo).Assembly.GetName().Name; - Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); - Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); - Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); - Assert.That(elementAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(serviceAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(requestAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(resultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(elementAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); } [Test] @@ -1205,7 +1205,10 @@ private static string[] ReadResolvedReferences(string relativeAsmdefPath) private static bool IsPublicAutoReferencedAssembly(string assemblyName) { - return string.Equals(assemblyName, ToolContractsAssemblyName, StringComparison.Ordinal) || + return string.Equals(assemblyName, ApplicationAssemblyName, StringComparison.Ordinal) || + string.Equals(assemblyName, DomainAssemblyName, StringComparison.Ordinal) || + string.Equals(assemblyName, ScreenshotAssemblyName, StringComparison.Ordinal) || + string.Equals(assemblyName, ToolContractsAssemblyName, StringComparison.Ordinal) || string.Equals(assemblyName, PausePointsRuntimeAssemblyName, StringComparison.Ordinal); } diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index 71ed355e8a..828f9d7644 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -96,6 +96,74 @@ public void TryReadJsonObjectForMigration_WhenReadThrowsUnauthorizedAccessExcept Assert.That(jsonObject, Is.Null); } + [Test] + public void GetRelativePathSegments_WhenPathContainsParentSegments_NormalizesBeforeSplitting() + { + // Verifies that path ancestry checks use canonical paths before deriving Unity-relative segments. + string projectRoot = Path.Combine(Path.GetTempPath(), "UnityCliLoopPathRoot"); + string filePath = Path.Combine( + projectRoot, + "..", + "UnityCliLoopPathRoot", + "Assets", + "VendorTools", + "HelloTool.cs"); + + string[] segments = ThirdPartyToolMigrationAssemblyReferenceResolver.GetRelativePathSegments( + filePath, + projectRoot); + + Assert.That(segments, Is.EqualTo(new[] { "Assets", "VendorTools", "HelloTool.cs" })); + } + + [Test] + public void IsSameOrChildPath_WhenChildContainsParentSegments_NormalizesBeforeComparison() + { + // Verifies that assembly-directory ancestry checks are based on canonical paths. + string projectRoot = Path.Combine(Path.GetTempPath(), "UnityCliLoopPathRoot"); + string parentPath = Path.Combine(projectRoot, "Assets", "VendorTools"); + string childPath = Path.Combine( + projectRoot, + "Assets", + "Other", + "..", + "VendorTools", + "Nested"); + + bool isSameOrChild = ThirdPartyToolMigrationAssemblyReferenceResolver.IsSameOrChildPath( + childPath, + parentPath); + + Assert.That(isSameOrChild, Is.True); + } + + [Test] + public void IsSameOrChildPath_WhenEitherPathIsEmpty_ThrowsArgumentException() + { + // Verifies that empty paths fail fast instead of normalizing to the process directory. + string nonEmptyPath = Path.GetTempPath(); + + ArgumentException childException = Assert.Throws( + () => ThirdPartyToolMigrationAssemblyReferenceResolver.IsSameOrChildPath(string.Empty, nonEmptyPath)); + ArgumentException parentException = Assert.Throws( + () => ThirdPartyToolMigrationAssemblyReferenceResolver.IsSameOrChildPath(nonEmptyPath, string.Empty)); + + Assert.That(childException.ParamName, Is.EqualTo("childPath")); + Assert.That(parentException.ParamName, Is.EqualTo("parentPath")); + } + + [Test] + public void ReadAsmdefGuidReferenceFromMetaFile_WhenMetaReadThrowsIOException_ReturnsEmpty() + { + // Verifies that unreadable .meta files do not abort assembly reference discovery. + string guidReference = + ThirdPartyToolMigrationAssemblyReferenceResolver.ReadAsmdefGuidReferenceFromMetaFile( + "Assets/VendorTools/VendorTools.Editor.asmdef.meta", + _ => throw new IOException("locked")); + + Assert.That(guidReference, Is.Empty); + } + [Test] public void ApplyMigration_WhenLegacyToolAssemblyExists_RewritesSourceAndAsmdef() { @@ -189,9 +257,9 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public void ApplyMigration_WhenManualRegistrationExists_AddsApplicationReference() + public void ApplyMigration_WhenManualRegistrationExists_RewritesToToolContractsReference() { - // Verifies that migrated manual registration source keeps access to the V3 registrar assembly. + // Verifies that migrated manual registration source uses the public ToolContracts registrar assembly. string projectRoot = CreateProjectRoot(); try { @@ -220,9 +288,9 @@ public static void Register(IUnityTool tool) Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(toolPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.RegisterCustomTool")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.RegisterCustomTool")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally @@ -232,9 +300,9 @@ public static void Register(IUnityTool tool) } [Test] - public void ApplyMigration_WhenCurrentManualRegistrationExists_PreservesApplicationReference() + public void ApplyMigration_WhenCurrentManualRegistrationExists_RewritesApplicationNamespace() { - // Verifies that mixed assemblies keep current registrar dependencies while legacy tools migrate. + // Verifies that mixed assemblies move current registrar dependencies to ToolContracts while legacy tools migrate. string projectRoot = CreateProjectRoot(); try { @@ -276,11 +344,12 @@ public static void Register(object tool) ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FileCount, Is.EqualTo(3)); Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(registrationPath), Does.Contain("UnityCliLoopToolRegistrar")); + Assert.That(File.ReadAllText(registrationPath), Does.Contain( + "using io.github.hatayama.UnityCliLoop.ToolContracts;")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally @@ -290,9 +359,9 @@ public static void Register(object tool) } [Test] - public void ApplyMigration_WhenCurrentManualRegistrationExistsWithLegacyAsmdefName_AddsApplicationReference() + public void ApplyMigration_WhenCurrentManualRegistrationExistsWithLegacyAsmdefName_RewritesApplicationNamespace() { - // Verifies that partially migrated registrar code receives the asmdef refs it already requires. + // Verifies that partially migrated registrar code moves to ToolContracts and receives its asmdef ref. string projectRoot = CreateProjectRoot(); try { @@ -320,10 +389,11 @@ public static void Register(object tool) ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(registrationPath), Is.EqualTo(registrationSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(registrationPath), Does.Contain( + "using io.github.hatayama.UnityCliLoop.ToolContracts;")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally @@ -365,7 +435,7 @@ public void ApplyMigration_WhenLegacyRuntimeAsmdefNameExists_RewritesToRuntimeRe [Test] public void ApplyMigration_WhenCurrentManualRegistrationExistsWithLegacyAsmdefGuid_AddsContractReferences() { - // Verifies that partially migrated GUID refs are expanded to the assemblies current registrar APIs expose. + // Verifies that partially migrated GUID refs add ToolContracts while source moves off Application. string projectRoot = CreateProjectRoot(); try { @@ -394,8 +464,11 @@ public static void Register(IUnityCliLoopTool tool) ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(registrationPath), Is.EqualTo(registrationSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(registrationPath), Does.Contain( + "using io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(File.ReadAllText(registrationPath), Does.Not.Contain( + "using io.github.hatayama.UnityCliLoop.Application;")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); @@ -454,9 +527,131 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public void ApplyMigration_WhenCurrentRegistrarExistsWithToolContractsAsmdefReference_AddsApplicationReference() + public void ApplyMigration_WhenLocalUIElementInfoWasAlreadyQualified_PreservesExplicitFirstPartyReference() + { + // Verifies that project-wide migration keeps explicit first-party DTO references. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CurrentElementTool.cs"); + File.WriteAllText(toolPath, @"using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CurrentElementTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + private List BuildFirstPartyElements() + { + List elements = new(); + elements.Add(CreateFirstPartyElement()); + return elements; + } + + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); + } + + private UIElementInfo CreateProjectElement() + { + return new UIElementInfo(); + } +} + +public sealed class ElementSchema : UnityCliLoopToolSchema +{ +} + +public sealed class ElementResponse : UnityCliLoopToolResponse +{ +} + +public sealed class UIElementInfo +{ +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); + + Assert.That(preview.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(migratedSource, Does.Contain( + "private List BuildFirstPartyElements()")); + Assert.That(migratedSource, Does.Contain( + "private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement()")); + Assert.That(migratedSource, Does.Contain( + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); + Assert.That(migratedSource, Does.Contain("private UIElementInfo CreateProjectElement()")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenQualifiedFirstPartyDtoSharesLocalName_UsesToolContractsReference() + { + // Verifies that unambiguous first-party DTO references add the public ToolContracts asmdef dependency. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CurrentElementTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string toolSource = @"public sealed class CurrentElementTool +{ + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); + } + + private UIElementInfo CreateProjectElement() + { + return new UIElementInfo(); + } +} + +public sealed class UIElementInfo +{ +}"; + File.WriteAllText(toolPath, toolSource); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(toolPath), Is.EqualTo(toolSource)); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenCurrentRegistrarExistsWithToolContractsAsmdefReference_RewritesApplicationNamespace() { - // Verifies that already-replaced asmdef refs still receive missing current assembly dependencies. + // Verifies that already-replaced asmdef refs keep using the public ToolContracts assembly. string projectRoot = CreateProjectRoot(); try { @@ -486,9 +681,12 @@ public static void Register(IUnityCliLoopTool tool) ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(registrationPath), Is.EqualTo(registrationSource)); + Assert.That(File.ReadAllText(registrationPath), Does.Contain( + "using io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(File.ReadAllText(registrationPath), Does.Not.Contain( + "using io.github.hatayama.UnityCliLoop.Application;")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally @@ -498,9 +696,9 @@ public static void Register(IUnityCliLoopTool tool) } [Test] - public void ApplyMigration_WhenLegacyRegistrarReturnIsUsedWithoutExplicitToolInfo_AddsDomainReference() + public void ApplyMigration_WhenLegacyRegistrarReturnIsUsedWithoutExplicitToolInfo_UsesToolContractsReference() { - // Verifies that registrar return types add the Domain dependency even without explicit ToolInfo usage. + // Verifies that registrar return types use ToolContracts without explicit ToolInfo usage. string projectRoot = CreateProjectRoot(); try { @@ -529,10 +727,10 @@ public static string GetLabel() Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(registrationPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally { @@ -541,7 +739,7 @@ public static string GetLabel() } [Test] - public void ApplyMigration_WhenDomainMetadataExistsWithoutManualRegistration_AddsDomainReference() + public void ApplyMigration_WhenDomainMetadataExistsWithoutManualRegistration_UsesToolContractsReference() { // Verifies that ToolInfo-only metadata helpers migrate their asmdef dependency. string projectRoot = CreateProjectRoot(); @@ -572,9 +770,9 @@ public static ToolInfo[] GetTools() Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(metadataPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ToolInfo[]")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo[]")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally @@ -584,9 +782,9 @@ public static ToolInfo[] GetTools() } [Test] - public void ApplyMigration_WhenLegacyDomainHelperExists_AddsDomainReference() + public void ApplyMigration_WhenLegacyDomainHelperExists_UsesToolContractsReference() { - // Verifies that helpers moved to Domain migrate their source and asmdef dependency together. + // Verifies that helpers moved to ToolContracts migrate their source and asmdef dependency together. string projectRoot = CreateProjectRoot(); try { @@ -620,11 +818,11 @@ public static ToolSettingsCatalogItem[] GetCatalog() Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult CreateResult")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult CreateResult")); Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ToolSettingsCatalogItem[] GetCatalog")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ToolSettingsCatalogItem[] GetCatalog")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally @@ -634,9 +832,9 @@ public static ToolSettingsCatalogItem[] GetCatalog() } [Test] - public void ApplyMigration_WhenCurrentDomainMetadataExistsWithLegacyAsmdefGuid_AddsDomainReference() + public void ApplyMigration_WhenCurrentDomainMetadataExistsWithLegacyAsmdefGuid_RewritesDomainNamespace() { - // Verifies that partially migrated metadata helpers receive the asmdef refs they already require. + // Verifies that partially migrated metadata helpers move to ToolContracts and receive its asmdef ref. string projectRoot = CreateProjectRoot(); try { @@ -664,10 +862,11 @@ public static ToolInfo[] GetTools() ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(metadataPath), Is.EqualTo(metadataSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(metadataPath), Does.Contain( + "using io.github.hatayama.UnityCliLoop.ToolContracts;")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally @@ -677,9 +876,9 @@ public static ToolInfo[] GetTools() } [Test] - public void ApplyMigration_WhenAssemblyUsesCurrentDomainGlobalUsingAndSplitMetadata_AddsDomainReference() + public void ApplyMigration_WhenAssemblyUsesCurrentDomainGlobalUsingAndSplitMetadata_RewritesDomainNamespace() { - // Verifies that split V3 Domain metadata files receive their required asmdef references. + // Verifies that split V3 Domain metadata files move to ToolContracts and receive their asmdef reference. string projectRoot = CreateProjectRoot(); try { @@ -708,11 +907,12 @@ public static ToolInfo[] GetTools() ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(globalUsingPath), Is.EqualTo(globalUsingSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); Assert.That(File.ReadAllText(metadataPath), Is.EqualTo(metadataSource)); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally @@ -907,20 +1107,24 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitContra } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitScreenshotHelpersAndAsmdef() { - // Verifies that assembly-level migration does not rename unrelated project types. + // Verifies that split files using old screenshot helpers receive the required ToolContracts assembly reference. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string unrelatedPath = Path.Combine(toolDirectory, "BaseToolSchema.cs"); + string helperPath = Path.Combine(toolDirectory, "ScreenshotHelper.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - string unrelatedSource = "public sealed class BaseToolSchema {}"; File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(unrelatedPath, unrelatedSource); + File.WriteAllText(helperPath, @"using UnityEditor; + +public sealed class ScreenshotHelper +{ + public EditorWindow[] FindWindows() => EditorWindowCaptureUtility.FindWindowsByName(""Game"", WindowMatchMode.exact); +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ @@ -931,10 +1135,13 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(helperPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.FindWindowsByName")); + Assert.That(File.ReadAllText(helperPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode.exact")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -943,48 +1150,58 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles } [Test] - public void ApplyMigration_WhenAssemblyUsesFileScopedLegacyUsing_KeepsUnrelatedBareTypes() + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyScreenshotCapture_KeepsToolContractsReference() { - // Verifies that file-scoped imports do not grant legacy type context to sibling files. + // Verifies that partially migrated files finish screenshot helper migration using ToolContracts only. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); - string unrelatedPath = Path.Combine(toolDirectory, "UnrelatedMetadata.cs"); + string toolPath = Path.Combine(toolDirectory, "CaptureTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - string unrelatedSource = - "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; - File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; -[McpTool] -public sealed class HelloTool : AbstractUnityTool +public sealed class CaptureTool : UnityCliLoopTool { + public override string ToolName => ""capture""; + + protected override async Task ExecuteAsync(CaptureSchema parameters, CancellationToken ct) + { + Texture2D texture = await EditorWindowCaptureUtility.CaptureWindowAsync(parameters.Window, 1.0f, ct); + return new CaptureResponse(texture != null); + } } -public sealed class HelloSchema : BaseToolSchema +public sealed class CaptureSchema : UnityCliLoopToolSchema { + public EditorWindow Window { get; set; } } -public sealed class HelloResponse : BaseToolResponse +public sealed class CaptureResponse : UnityCliLoopToolResponse { + public bool Success { get; } + public CaptureResponse(bool success) { Success = success; } }"); - File.WriteAllText(unrelatedPath, unrelatedSource); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(toolPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -993,34 +1210,52 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSplitContractFiles() + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyMainThreadSwitcher_KeepsToolContractsReference() { - // Verifies that collection-shaped legacy type references migrate with their assembly. + // Verifies that partially migrated files finish main-thread switcher migration using ToolContracts only. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string listPath = Path.Combine(toolDirectory, "ToolList.cs"); + string toolPath = Path.Combine(toolDirectory, "MainThreadTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - listPath, - "public sealed class ToolList { public System.Collections.Generic.List Tools; }"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class MainThreadTool : UnityCliLoopTool +{ + protected override async Task ExecuteAsync(MainThreadSchema parameters, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + return new MainThreadResponse(); + } +} + +public sealed class MainThreadSchema : UnityCliLoopToolSchema +{ +} + +public sealed class MainThreadResponse : UnityCliLoopToolResponse +{ +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(listPath), Does.Contain( - "System.Collections.Generic.List")); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(toolPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(File.ReadAllText(toolPath), Does.Not.Contain("PlayerLoopTiming")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1029,36 +1264,43 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSpli } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomainHelpers() + public void ApplyMigration_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsToolContractsReference() { - // Verifies that split Domain helper files receive source and asmdef migration through global usings. + // Verifies that regular legacy usings add the ToolContracts asmdef reference for migrated main-thread calls. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string helperPath = Path.Combine(toolDirectory, "ToolHelper.cs"); + string toolPath = Path.Combine(toolDirectory, "MainThreadTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - helperPath, - "public static class ToolHelper { public static ServiceResult Create() => null; }"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""references"": [] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult Create")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); + + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1067,34 +1309,41 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomain } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturnContractTypes() + public void ApplyMigration_WhenLegacyUsingUsesBareFirstPartyScreenshotDto_AddsToolContractsReference() { - // Verifies that helper return types relying on global legacy usings migrate with their assembly. + // Verifies that regular legacy usings that migrate screenshot DTOs repair asmdef references. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string responseFactoryPath = Path.Combine(toolDirectory, "ResponseFactory.cs"); + string toolPath = Path.Combine(toolDirectory, "ElementTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - responseFactoryPath, - "public static class ResponseFactory { public static BaseToolResponse Create() => null; }"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +public sealed class ElementTool +{ + public UIElementInfo CreateElement() + { + return new UIElementInfo(); + } +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""references"": [] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(responseFactoryPath), Does.Contain("UnityCliLoopToolResponse Create")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); + + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1103,34 +1352,45 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturn } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareToolAttributes() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_RewritesApplicationNamespace() { - // Verifies that attribute-only files relying on global legacy usings migrate with their assembly. + // Verifies that split files relying on a current Application global using move to ToolContracts. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string toolAttributePath = Path.Combine(toolDirectory, "HelloTool.Attribute.cs"); + string mainThreadPath = Path.Combine(toolDirectory, "MainThreadUsage.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - toolAttributePath, - "[McpTool]\npublic sealed partial class HelloTool\n{\n}"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.Application;"); + File.WriteAllText(mainThreadPath, @"using System.Threading; +using System.Threading.Tasks; + +public sealed class MainThreadUsage +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(ct); + } +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(toolAttributePath), Does.Contain("[UnityCliLoopTool]")); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(File.ReadAllText(mainThreadPath), Does.Contain( + "await MainThreadSwitcher.SwitchToMainThread(ct);")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1139,21 +1399,28 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareTo } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedFiles() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_RewritesBareMainThreadSwitcherTiming() { - // Verifies that global namespace aliases provide legacy context to sibling files. + // Verifies that split files relying on a current Application global using drop removed timing arguments. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string toolAttributePath = Path.Combine(toolDirectory, "HelloTool.Attribute.cs"); + string mainThreadPath = Path.Combine(toolDirectory, "MainThreadUsage.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using Old = io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - toolAttributePath, - "[Old.McpTool]\npublic sealed partial class HelloTool\n{\n private Old.IUnityTool tool;\n}"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.Application;"); + File.WriteAllText(mainThreadPath, @"using System.Threading; +using System.Threading.Tasks; + +public sealed class MainThreadUsage +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ @@ -1163,13 +1430,12 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQ ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(mainThreadPath); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(toolAttributePath), Does.Contain( - "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool")); - Assert.That(File.ReadAllText(toolAttributePath), Does.Contain("Old.IUnityCliLoopTool")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(result.FilePaths.Contains(mainThreadPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "await MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1178,21 +1444,28 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQ } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedContractTypes() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalAlias_RewritesQualifiedMainThreadSwitcherTiming() { - // Verifies that alias-qualified legacy contract types migrate in sibling files. + // Verifies that split files relying on a current Application global alias drop removed timing arguments. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string responseFactoryPath = Path.Combine(toolDirectory, "ResponseFactory.cs"); + string mainThreadPath = Path.Combine(toolDirectory, "MainThreadUsage.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using Old = io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - responseFactoryPath, - "public static class ResponseFactory { public static Old.BaseToolResponse Create() => null; }"); + File.WriteAllText(globalUsingPath, "global using App = io.github.hatayama.UnityCliLoop.Application;"); + File.WriteAllText(mainThreadPath, @"using System.Threading; +using System.Threading.Tasks; + +public sealed class MainThreadUsage +{ + public async Task RunAsync(CancellationToken ct) + { + await App.MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ @@ -1202,11 +1475,15 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQ ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(mainThreadPath); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(responseFactoryPath), Does.Contain("Old.UnityCliLoopToolResponse Create")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(result.FilePaths.Contains(mainThreadPath), Is.True); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using App = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(migratedSource, Does.Contain( + "await App.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1215,47 +1492,49 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQ } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_RewritesSplitAliasConstructors() + public void ApplyMigration_WhenLegacyTimingWrapperCallerIsSplit_RewritesCallerArguments() { - // Verifies that global type aliases provide constructor migration context to sibling files. + // Verifies that timing wrapper signature changes update callers in other source files. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string metadataPath = Path.Combine(toolDirectory, "ToolMetadataProvider.cs"); - string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText( - globalUsingPath, - "global using LegacyToolInfo = io.github.hatayama.uLoopMCP.ToolInfo;"); - File.WriteAllText( - metadataPath, - @"using io.github.hatayama.UnityCliLoop.ToolContracts; + string wrapperPath = Path.Combine(toolDirectory, "MainThreadWrapper.cs"); + string callerPath = Path.Combine(toolDirectory, "MainThreadCaller.cs"); + File.WriteAllText(wrapperPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -public static class ToolMetadataProvider +public sealed class MainThreadWrapper { - public static LegacyToolInfo Create(ToolParameterSchema schema) + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) { - return new LegacyToolInfo(""hello"", ""description"", schema); + await MainThreadSwitcher.SwitchToMainThread(loop, ct); } }"); - File.WriteAllText(asmdefPath, @"{ - ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + File.WriteAllText(callerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedWrapperSource = File.ReadAllText(wrapperPath); + string migratedCallerSource = File.ReadAllText(callerPath); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( - "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.Domain.ToolInfo;")); - Assert.That(File.ReadAllText(metadataPath), Does.Contain( - "new io.github.hatayama.UnityCliLoop.Domain.ToolInfo(\"hello\", schema)")); - Assert.That(File.ReadAllText(metadataPath), Does.Not.Contain("\"description\", schema")); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(migratedWrapperSource, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(migratedCallerSource, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(migratedWrapperSource, Does.Not.Contain("PlayerLoopTiming")); + Assert.That(migratedCallerSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1264,36 +1543,63 @@ public static LegacyToolInfo Create(ToolParameterSchema schema) } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrelatedBareTypes() + public void ApplyMigration_WhenLegacyTimingWrapperCallerIsInAnotherAssembly_RewritesCallerArguments() { - // Verifies that ToolInfo type aliases do not grant full legacy namespace context to sibling files. + // Verifies that public timing wrapper signature changes update callers in other asmdef assemblies. string projectRoot = CreateProjectRoot(); try { + string helperDirectory = Path.Combine(projectRoot, "Assets", "VendorHelpers"); string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(helperDirectory); Directory.CreateDirectory(toolDirectory); - string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string unrelatedPath = Path.Combine(toolDirectory, "UnrelatedMetadata.cs"); - string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - string unrelatedSource = "public sealed class UnrelatedMetadata { public BaseToolSchema Schema; }"; - File.WriteAllText( - globalUsingPath, - "global using LegacyToolInfo = io.github.hatayama.uLoopMCP.ToolInfo;"); - File.WriteAllText(unrelatedPath, unrelatedSource); - File.WriteAllText(asmdefPath, @"{ + string wrapperPath = Path.Combine(helperDirectory, "MainThreadWrapper.cs"); + string callerPath = Path.Combine(toolDirectory, "MainThreadCaller.cs"); + string helperAsmdefPath = Path.Combine(helperDirectory, "VendorHelpers.Editor.asmdef"); + string toolAsmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(wrapperPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"); + File.WriteAllText(callerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText(helperAsmdefPath, @"{ + ""name"": ""VendorHelpers.Editor"", + ""references"": [] +}"); + File.WriteAllText(toolAsmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""VendorHelpers.Editor"" ] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( - "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.Domain.ToolInfo;")); - Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + string migratedWrapperSource = File.ReadAllText(wrapperPath); + string migratedCallerSource = File.ReadAllText(callerPath); + + Assert.That(result.FilePaths.Contains(wrapperPath), Is.True); + Assert.That(result.FilePaths.Contains(callerPath), Is.True); + Assert.That(migratedWrapperSource, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(migratedCallerSource, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(migratedCallerSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1302,42 +1608,96 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrela } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderAsmref_RewritesReferencedAsmdef() + public void ApplyMigration_WhenUnrelatedAssemblyHasSameTimingWrapper_KeepsUnreachableCallerArguments() { - // Verifies that asmref folders mark the referenced asmdef as the migrated assembly. + // Verifies that cross-file timing rewrites do not cross asmdef boundaries without a reference. string projectRoot = CreateProjectRoot(); try { - string asmdefDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); - string asmrefDirectory = Path.Combine(projectRoot, "Assets", "VendorToolParts"); - Directory.CreateDirectory(asmdefDirectory); - Directory.CreateDirectory(asmrefDirectory); - string toolPath = Path.Combine(asmrefDirectory, "HelloTool.cs"); - string asmrefPath = Path.Combine(asmrefDirectory, "VendorTools.Editor.asmref"); - string asmdefPath = Path.Combine(asmdefDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + string helperDirectory = Path.Combine(projectRoot, "Assets", "VendorHelpers"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + string unrelatedDirectory = Path.Combine(projectRoot, "Assets", "UnrelatedTools"); + Directory.CreateDirectory(helperDirectory); + Directory.CreateDirectory(toolDirectory); + Directory.CreateDirectory(unrelatedDirectory); + string helperWrapperPath = Path.Combine(helperDirectory, "MainThreadWrapper.cs"); + string toolCallerPath = Path.Combine(toolDirectory, "MainThreadCaller.cs"); + string unrelatedWrapperPath = Path.Combine(unrelatedDirectory, "MainThreadWrapper.cs"); + string unrelatedCallerPath = Path.Combine(unrelatedDirectory, "MainThreadCaller.cs"); + string helperAsmdefPath = Path.Combine(helperDirectory, "VendorHelpers.Editor.asmdef"); + string toolAsmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string unrelatedAsmdefPath = Path.Combine(unrelatedDirectory, "UnrelatedTools.Editor.asmdef"); + File.WriteAllText(helperWrapperPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -[McpTool] -public sealed class HelloTool : AbstractUnityTool +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"); + File.WriteAllText(toolCallerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadCaller { + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } }"); - File.WriteAllText(asmrefPath, @"{ - ""reference"": ""VendorTools.Editor"" + File.WriteAllText(unrelatedWrapperPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } }"); - File.WriteAllText(asmdefPath, @"{ + File.WriteAllText(unrelatedCallerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText(helperAsmdefPath, @"{ + ""name"": ""VendorHelpers.Editor"", + ""references"": [] +}"); + File.WriteAllText(toolAsmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""VendorHelpers.Editor"" ] }"); + File.WriteAllText(unrelatedAsmdefPath, @"{ + ""name"": ""UnrelatedTools.Editor"", + ""references"": [] +}"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + string migratedToolCallerSource = File.ReadAllText(toolCallerPath); + string migratedUnrelatedWrapperSource = File.ReadAllText(unrelatedWrapperPath); + string migratedUnrelatedCallerSource = File.ReadAllText(unrelatedCallerPath); + + Assert.That(result.FilePaths.Contains(toolCallerPath), Is.True); + Assert.That(migratedToolCallerSource, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(migratedUnrelatedWrapperSource, Does.Contain( + "public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(migratedUnrelatedCallerSource, Does.Contain( + "return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); } finally { @@ -1346,48 +1706,67 @@ public sealed class HelloTool : AbstractUnityTool } [Test] - public void ApplyMigration_WhenNestedAsmrefOverridesAncestorAsmdef_RewritesReferencedAsmdef() + public void ApplyMigration_WhenCrossFileTimingWrapperCallChain_RevisitsStaleOuterParameter() { - // Verifies that nested asmref folders use their referenced assembly instead of an ancestor asmdef. + // Verifies that cross-file wrapper rewrites also remove newly stale timing parameters. string projectRoot = CreateProjectRoot(); try { - string ancestorDirectory = Path.Combine(projectRoot, "Assets", "OuterAssembly"); - string asmrefDirectory = Path.Combine(ancestorDirectory, "VendorToolParts"); - string targetDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); - Directory.CreateDirectory(asmrefDirectory); - Directory.CreateDirectory(targetDirectory); - string ancestorAsmdefPath = Path.Combine(ancestorDirectory, "OuterAssembly.Editor.asmdef"); - string toolPath = Path.Combine(asmrefDirectory, "HelloTool.cs"); - string asmrefPath = Path.Combine(asmrefDirectory, "VendorTools.Editor.asmref"); - string targetAsmdefPath = Path.Combine(targetDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(ancestorAsmdefPath, @"{ - ""name"": ""OuterAssembly.Editor"", - ""references"": [] -}"); - File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string innerPath = Path.Combine(toolDirectory, "InnerWrapper.cs"); + string outerPath = Path.Combine(toolDirectory, "OuterWrapper.cs"); + string callerPath = Path.Combine(toolDirectory, "MainThreadCaller.cs"); + File.WriteAllText(innerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -[McpTool] -public sealed class HelloTool : AbstractUnityTool +public sealed class InnerWrapper { + public async Task InnerAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } }"); - File.WriteAllText(asmrefPath, @"{ - ""reference"": ""VendorTools.Editor"" + File.WriteAllText(outerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class OuterWrapper +{ + public Task OuterAsync(InnerWrapper inner, PlayerLoopTiming loop, CancellationToken ct) + { + return inner.InnerAsync(loop, ct); + } }"); - File.WriteAllText(targetAsmdefPath, @"{ - ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + File.WriteAllText(callerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadCaller +{ + public Task CallAsync(OuterWrapper outer, InnerWrapper inner, CancellationToken ct) + { + return outer.OuterAsync(inner, PlayerLoopTiming.Update, ct); + } }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(targetAsmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(ancestorAsmdefPath), Does.Not.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + string migratedInnerSource = File.ReadAllText(innerPath); + string migratedOuterSource = File.ReadAllText(outerPath); + string migratedCallerSource = File.ReadAllText(callerPath); + + Assert.That(result.FilePaths.Contains(innerPath), Is.True); + Assert.That(result.FilePaths.Contains(outerPath), Is.True); + Assert.That(result.FilePaths.Contains(callerPath), Is.True); + Assert.That(migratedInnerSource, Does.Contain("public async Task InnerAsync(CancellationToken ct)")); + Assert.That(migratedOuterSource, Does.Contain( + "public Task OuterAsync(InnerWrapper inner, CancellationToken ct)")); + Assert.That(migratedOuterSource, Does.Contain("return inner.InnerAsync(ct);")); + Assert.That(migratedCallerSource, Does.Contain("return outer.OuterAsync(inner, ct);")); + Assert.That(migratedOuterSource, Does.Not.Contain("PlayerLoopTiming")); + Assert.That(migratedCallerSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1396,25 +1775,67 @@ public sealed class HelloTool : AbstractUnityTool } [Test] - public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSplitContractFiles() + public void ApplyMigration_WhenAssemblyUsesCurrentToolContractsGlobalUsing_RewritesSplitPartialHelpers() { - // Verifies that predefined assemblies get the same assembly-level migration as asmdef assemblies. + // Verifies that split files relying on a current ToolContracts global using finish partial migration. string projectRoot = CreateProjectRoot(); try { - string toolDirectory = Path.Combine(projectRoot, "Assets", "Editor", "VendorTools"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string schemaPath = Path.Combine(toolDirectory, "HelloSchema.cs"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(schemaPath, "public sealed class HelloSchema : BaseToolSchema {}"); + string toolPath = Path.Combine(toolDirectory, "PartialTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.ToolContracts;"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class PartialTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + await EditorDelay.DelayFrame(2, ct); + await TimerDelay.Wait(10, cancellationToken: ct); + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + Texture2D texture = await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + return texture; + } +} + +public sealed class PartialSchema : UnityCliLoopToolSchema +{ +} + +public sealed class PartialResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(2)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(schemaPath), Does.Contain("UnityCliLoopToolSchema")); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(migratedSource, Does.Contain( + "EditorFrameWaiter.WaitFramesOrTimeoutAsync(2, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)")); + Assert.That(migratedSource, Does.Contain("TimerDelay.Wait(10, ct: ct)")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Not.Contain("EditorDelay")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); + Assert.That(migratedSource, Does.Not.Contain("cancellationToken:")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1423,59 +1844,44 @@ public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSpl } [Test] - public void ApplyMigration_WhenNoAsmdefEditorAssemblyUsesGlobalLegacyUsing_KeepsRuntimeFiles() + public void ApplyMigration_WhenCurrentFirstPartyToolsCaptureNeedsTimeout_AddsToolContractsReference() { - // Verifies that predefined editor migration does not rewrite the separate runtime assembly. + // Verifies that migrated capture timeout constants have a direct ToolContracts asmdef reference. string projectRoot = CreateProjectRoot(); try { - string editorDirectory = Path.Combine(projectRoot, "Assets", "Editor", "VendorTools"); - string runtimeDirectory = Path.Combine(projectRoot, "Assets", "Scripts"); - Directory.CreateDirectory(editorDirectory); - Directory.CreateDirectory(runtimeDirectory); - string globalUsingPath = Path.Combine(editorDirectory, "GlobalUsings.cs"); - string runtimePath = Path.Combine(runtimeDirectory, "BaseToolSchema.cs"); - string runtimeSource = "public sealed class BaseToolSchema {}"; - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(runtimePath, runtimeSource); - - ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CurrentScreenshotTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(runtimePath), Is.EqualTo(runtimeSource)); - } - finally - { - Directory.Delete(projectRoot, recursive: true); - } - } - - [Test] - public void ApplyMigration_WhenNoAsmdefFirstPassRuntimeUsesGlobalLegacyUsing_KeepsRegularRuntimeFiles() - { - // Verifies that Unity predefined firstpass runtime scripts do not migrate regular runtime siblings. - string projectRoot = CreateProjectRoot(); - try - { - string firstPassDirectory = Path.Combine(projectRoot, "Assets", "Plugins"); - string runtimeDirectory = Path.Combine(projectRoot, "Assets", "Scripts"); - Directory.CreateDirectory(firstPassDirectory); - Directory.CreateDirectory(runtimeDirectory); - string globalUsingPath = Path.Combine(firstPassDirectory, "GlobalUsings.cs"); - string runtimePath = Path.Combine(runtimeDirectory, "UnrelatedMetadata.cs"); - string runtimeSource = - "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(runtimePath, runtimeSource); +public sealed class CurrentScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(runtimePath), Is.EqualTo(runtimeSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1484,29 +1890,50 @@ public void ApplyMigration_WhenNoAsmdefFirstPassRuntimeUsesGlobalLegacyUsing_Kee } [Test] - public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_KeepsRegularEditorFiles() + public void ApplyMigration_WhenAssemblyUsesCurrentFirstPartyToolsGlobalUsing_RewritesSplitCapture() { - // Verifies that Unity predefined firstpass editor scripts do not migrate regular editor siblings. + // Verifies that split files relying on a current FirstPartyTools global using finish capture migration. string projectRoot = CreateProjectRoot(); try { - string firstPassEditorDirectory = Path.Combine(projectRoot, "Assets", "Plugins", "Editor"); - string editorDirectory = Path.Combine(projectRoot, "Assets", "Editor"); - Directory.CreateDirectory(firstPassEditorDirectory); - Directory.CreateDirectory(editorDirectory); - string globalUsingPath = Path.Combine(firstPassEditorDirectory, "GlobalUsings.cs"); - string editorPath = Path.Combine(editorDirectory, "UnrelatedMetadata.cs"); - string editorSource = - "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(editorPath, editorSource); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string toolPath = Path.Combine(toolDirectory, "CurrentScreenshotTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class CurrentScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(editorPath), Is.EqualTo(editorSource)); + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(migratedSource, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1515,41 +1942,53 @@ public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_Keep } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsingAndSplitManualRegistration_AddsApplicationReference() + public void ApplyMigration_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_RewritesQualifiedSplitCapture() { - // Verifies that manual registration files relying on assembly-level legacy detection get required refs. + // Verifies that split files relying on a current FirstPartyTools global alias finish capture migration. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); - string registrationPath = Path.Combine(toolDirectory, "ManualToolRegistration.cs"); + string toolPath = Path.Combine(toolDirectory, "CurrentScreenshotTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); - File.WriteAllText(registrationPath, @"public static class ManualToolRegistration + File.WriteAllText(globalUsingPath, "global using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class CurrentScreenshotTool { - public static void Register(IUnityTool tool) + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) { - CustomToolManager.RegisterCustomTool(tool); + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } }"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" ] }"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(3)); - Assert.That(File.ReadAllText(registrationPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.RegisterCustomTool")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); - Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(migratedSource, Does.Not.Contain( + "return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync")); } finally { @@ -1558,32 +1997,33 @@ public static void Register(IUnityTool tool) } [Test] - public void PreviewMigration_WhenUnrelatedCustomToolManagerExists_KeepsAsmdef() + public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that project-owned type names do not trigger migration dependencies by themselves. + // Verifies that a current FirstPartyTools global using by itself moves to ToolContracts. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string sourcePath = Path.Combine(toolDirectory, "CustomToolManager.cs"); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - string source = "public sealed class CustomToolManager {}"; - string asmdefSource = @"{ + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [] -}"; - File.WriteAllText(sourcePath, source); - File.WriteAllText(asmdefPath, asmdefSource); +}"); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(preview.HasTargets, Is.False); - Assert.That(result.FileCount, Is.EqualTo(0)); - Assert.That(File.ReadAllText(sourcePath), Is.EqualTo(source)); - Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { @@ -1592,57 +2032,33 @@ public void PreviewMigration_WhenUnrelatedCustomToolManagerExists_KeepsAsmdef() } [Test] - public void ApplyMigration_WhenUserSidecarFilesExist_PreservesSidecarFiles() + public void ApplyMigration_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that project-wide source rewrites do not treat user sidecars as migration scratch files. + // Verifies that a current FirstPartyTools namespace alias by itself moves to ToolContracts. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); - string tempSidecarPath = toolPath + ".tmp"; - string backupSidecarPath = toolPath + ".bak"; - File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; - -[McpTool] -public sealed class HelloTool : AbstractUnityTool -{ + string aliasUsingPath = Path.Combine(toolDirectory, "AliasUsing.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(aliasUsingPath, "using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] }"); - File.WriteAllText(tempSidecarPath, "user temp sidecar"); - File.WriteAllText(backupSidecarPath, "user backup sidecar"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(tempSidecarPath), Is.EqualTo("user temp sidecar")); - Assert.That(File.ReadAllText(backupSidecarPath), Is.EqualTo("user backup sidecar")); - } - finally - { - Directory.Delete(projectRoot, recursive: true); - } - } - - [Test] - public void PreviewMigration_WhenLegacyToolExistsUnderExcludedDirectory_IgnoresFile() - { - // Verifies that generated Unity folders are not scanned for third-party tools. - string projectRoot = CreateProjectRoot(); - try - { - string generatedDirectory = Path.Combine(projectRoot, "Library"); - Directory.CreateDirectory(generatedDirectory); - File.WriteAllText( - Path.Combine(generatedDirectory, "GeneratedTool.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class GeneratedTool {}"); - - ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); - - Assert.That(preview.HasTargets, Is.False); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(result.FilePaths.Contains(aliasUsingPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(aliasUsingPath), Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { @@ -1651,22 +2067,34 @@ public void PreviewMigration_WhenLegacyToolExistsUnderExcludedDirectory_IgnoresF } [Test] - public void PreviewMigration_WhenLegacyToolExistsOutsideAssets_IgnoresFile() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles() { - // Verifies that repository tooling outside Unity source roots is not migrated. + // Verifies that assembly-level migration does not rename unrelated project types. string projectRoot = CreateProjectRoot(); try { - string toolsDirectory = Path.Combine(projectRoot, "tools"); - Directory.CreateDirectory(toolsDirectory); - File.WriteAllText( - Path.Combine(toolsDirectory, "LegacyToolFixture.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class LegacyToolFixture {}"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string unrelatedPath = Path.Combine(toolDirectory, "BaseToolSchema.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string unrelatedSource = "public sealed class BaseToolSchema {}"; + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(unrelatedPath, unrelatedSource); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(preview.HasTargets, Is.False); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); } finally { @@ -1675,16 +2103,19 @@ public void PreviewMigration_WhenLegacyToolExistsOutsideAssets_IgnoresFile() } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderAssetsPackagesDirectory_RewritesAssetsFiles() + public void ApplyMigration_WhenAssemblyUsesFileScopedLegacyUsing_KeepsUnrelatedBareTypes() { - // Verifies that only Unity's project-root Packages directory is excluded from migration scans. + // Verifies that file-scoped imports do not grant legacy type context to sibling files. string projectRoot = CreateProjectRoot(); try { - string toolDirectory = Path.Combine(projectRoot, "Assets", "Packages", "VendorTools"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string unrelatedPath = Path.Combine(toolDirectory, "UnrelatedMetadata.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string unrelatedSource = + "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; [McpTool] @@ -1699,6 +2130,7 @@ public sealed class HelloSchema : BaseToolSchema public sealed class HelloResponse : BaseToolResponse { }"); + File.WriteAllText(unrelatedPath, unrelatedSource); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ @@ -1711,6 +2143,7 @@ public sealed class HelloResponse : BaseToolResponse Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); } finally @@ -1720,51 +2153,1725 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderPackagesDirectory_KeepsPackageFiles() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSplitContractFiles() { - // Verifies that Package Manager and embedded package contents are not rewritten in place. + // Verifies that collection-shaped legacy type references migrate with their assembly. string projectRoot = CreateProjectRoot(); try { - string packageDirectory = Path.Combine( - projectRoot, - "Packages", - "com.example.legacy-tool", - "Editor"); - Directory.CreateDirectory(packageDirectory); - string toolPath = Path.Combine(packageDirectory, "PackageTool.cs"); - string asmdefPath = Path.Combine(packageDirectory, "LegacyPackageTool.Editor.asmdef"); - string toolSource = @"using io.github.hatayama.uLoopMCP; - -[McpTool] -public sealed class PackageTool : AbstractUnityTool -{ -} - -public sealed class PackageToolSchema : BaseToolSchema -{ -} - -public sealed class PackageToolResponse : BaseToolResponse -{ -}"; - string asmdefSource = @"{ - ""name"": ""LegacyPackageTool.Editor"", + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string listPath = Path.Combine(toolDirectory, "ToolList.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + listPath, + "public sealed class ToolList { public System.Collections.Generic.List Tools; }"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", ""references"": [ ""GUID:214998e563c124e8a88199b2dd1f522d"" ] -}"; - File.WriteAllText(toolPath, toolSource); - File.WriteAllText(asmdefPath, asmdefSource); +}"); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(preview.HasTargets, Is.False); - Assert.That(result.FileCount, Is.EqualTo(0)); - Assert.That(File.ReadAllText(toolPath), Is.EqualTo(toolSource)); - Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(listPath), Does.Contain( + "System.Collections.Generic.List")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomainHelpers() + { + // Verifies that split Domain helper files receive source and asmdef migration through global usings. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string helperPath = Path.Combine(toolDirectory, "ToolHelper.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + helperPath, + "public static class ToolHelper { public static ServiceResult Create() => null; }"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(helperPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult Create")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturnContractTypes() + { + // Verifies that helper return types relying on global legacy usings migrate with their assembly. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string responseFactoryPath = Path.Combine(toolDirectory, "ResponseFactory.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + responseFactoryPath, + "public static class ResponseFactory { public static BaseToolResponse Create() => null; }"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(responseFactoryPath), Does.Contain("UnityCliLoopToolResponse Create")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareToolAttributes() + { + // Verifies that attribute-only files relying on global legacy usings migrate with their assembly. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string toolAttributePath = Path.Combine(toolDirectory, "HelloTool.Attribute.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + toolAttributePath, + "[McpTool]\npublic sealed partial class HelloTool\n{\n}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(toolAttributePath), Does.Contain("[UnityCliLoopTool]")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedFiles() + { + // Verifies that global namespace aliases provide legacy context to sibling files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string toolAttributePath = Path.Combine(toolDirectory, "HelloTool.Attribute.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using Old = io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + toolAttributePath, + "[Old.McpTool]\npublic sealed partial class HelloTool\n{\n private Old.IUnityTool tool;\n}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(toolAttributePath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool")); + Assert.That(File.ReadAllText(toolAttributePath), Does.Contain("Old.IUnityCliLoopTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedContractTypes() + { + // Verifies that alias-qualified legacy contract types migrate in sibling files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string responseFactoryPath = Path.Combine(toolDirectory, "ResponseFactory.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using Old = io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + responseFactoryPath, + "public static class ResponseFactory { public static Old.BaseToolResponse Create() => null; }"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(responseFactoryPath), Does.Contain("Old.UnityCliLoopToolResponse Create")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_RewritesSplitAliasConstructors() + { + // Verifies that global type aliases provide constructor migration context to sibling files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string metadataPath = Path.Combine(toolDirectory, "ToolMetadataProvider.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText( + globalUsingPath, + "global using LegacyToolInfo = io.github.hatayama.uLoopMCP.ToolInfo;"); + File.WriteAllText( + metadataPath, + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public static class ToolMetadataProvider +{ + public static LegacyToolInfo Create(ToolParameterSchema schema) + { + return new LegacyToolInfo(""hello"", ""description"", schema); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo;")); + Assert.That(File.ReadAllText(metadataPath), Does.Contain( + "new io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo(\"hello\", schema)")); + Assert.That(File.ReadAllText(metadataPath), Does.Not.Contain("\"description\", schema")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrelatedBareTypes() + { + // Verifies that ToolInfo type aliases do not grant full legacy namespace context to sibling files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string unrelatedPath = Path.Combine(toolDirectory, "UnrelatedMetadata.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string unrelatedSource = "public sealed class UnrelatedMetadata { public BaseToolSchema Schema; }"; + File.WriteAllText( + globalUsingPath, + "global using LegacyToolInfo = io.github.hatayama.uLoopMCP.ToolInfo;"); + File.WriteAllText(unrelatedPath, unrelatedSource); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo;")); + Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenLegacyToolExistsUnderAsmref_RewritesReferencedAsmdef() + { + // Verifies that asmref folders mark the referenced asmdef as the migrated assembly. + string projectRoot = CreateProjectRoot(); + try + { + string asmdefDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + string asmrefDirectory = Path.Combine(projectRoot, "Assets", "VendorToolParts"); + Directory.CreateDirectory(asmdefDirectory); + Directory.CreateDirectory(asmrefDirectory); + string toolPath = Path.Combine(asmrefDirectory, "HelloTool.cs"); + string asmrefPath = Path.Combine(asmrefDirectory, "VendorTools.Editor.asmref"); + string asmdefPath = Path.Combine(asmdefDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +}"); + File.WriteAllText(asmrefPath, @"{ + ""reference"": ""VendorTools.Editor"" +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNestedAsmrefOverridesAncestorAsmdef_RewritesReferencedAsmdef() + { + // Verifies that nested asmref folders use their referenced assembly instead of an ancestor asmdef. + string projectRoot = CreateProjectRoot(); + try + { + string ancestorDirectory = Path.Combine(projectRoot, "Assets", "OuterAssembly"); + string asmrefDirectory = Path.Combine(ancestorDirectory, "VendorToolParts"); + string targetDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(asmrefDirectory); + Directory.CreateDirectory(targetDirectory); + string ancestorAsmdefPath = Path.Combine(ancestorDirectory, "OuterAssembly.Editor.asmdef"); + string toolPath = Path.Combine(asmrefDirectory, "HelloTool.cs"); + string asmrefPath = Path.Combine(asmrefDirectory, "VendorTools.Editor.asmref"); + string targetAsmdefPath = Path.Combine(targetDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(ancestorAsmdefPath, @"{ + ""name"": ""OuterAssembly.Editor"", + ""references"": [] +}"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +}"); + File.WriteAllText(asmrefPath, @"{ + ""reference"": ""VendorTools.Editor"" +}"); + File.WriteAllText(targetAsmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(targetAsmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(ancestorAsmdefPath), Does.Not.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenSplitProjectScreenshotDtosExist_KeepsProjectDtoReferences() + { + // Verifies that assembly-level DTO declarations protect split custom tool references from first-party rewrites. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CaptureTool.cs"); + string dtoPath = Path.Combine(toolDirectory, "CaptureDtos.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CaptureTool : UnityCliLoopTool +{ + public async Task ExecuteAsync(ScreenshotSchema parameters, CancellationToken ct) + { + Texture2D texture = await EditorWindowCaptureUtility.CaptureWindowAsync(parameters.Window, 1.0f, ct); + List elements = new(); + return new ScreenshotResponse { Elements = elements }; + } +}"); + File.WriteAllText(dtoPath, @"using System.Collections.Generic; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotSchema : UnityCliLoopToolSchema +{ + public EditorWindow Window { get; set; } +} + +public sealed class ScreenshotResponse : UnityCliLoopToolResponse +{ + public List Elements { get; set; } = new List(); +} + +public sealed class UIElementInfo +{ +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedToolSource = File.ReadAllText(toolPath); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(migratedToolSource, Does.Contain("UnityCliLoopTool")); + Assert.That(migratedToolSource, Does.Contain("Task ExecuteAsync")); + Assert.That(migratedToolSource, Does.Contain("List elements")); + Assert.That(migratedToolSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotSchema")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotResponse")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo")); + Assert.That(File.ReadAllText(dtoPath), Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSplitContractFiles() + { + // Verifies that predefined assemblies get the same assembly-level migration as asmdef assemblies. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "Editor", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string schemaPath = Path.Combine(toolDirectory, "HelloSchema.cs"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(schemaPath, "public sealed class HelloSchema : BaseToolSchema {}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(schemaPath), Does.Contain("UnityCliLoopToolSchema")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNoAsmdefCurrentToolContractsFileUsesLegacyScreenshotCapture_RewritesSource() + { + // Verifies that predefined editor assemblies finish partially migrated screenshot helpers. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "Editor", "CustomTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CaptureTool.cs"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CaptureTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + Texture2D texture = await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + return texture; + } +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(toolPath); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(migratedSource, Does.Contain( + "UnityCliLoopTool")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Not.Contain("await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNoAsmdefEditorAssemblyUsesGlobalLegacyUsing_KeepsRuntimeFiles() + { + // Verifies that predefined editor migration does not rewrite the separate runtime assembly. + string projectRoot = CreateProjectRoot(); + try + { + string editorDirectory = Path.Combine(projectRoot, "Assets", "Editor", "VendorTools"); + string runtimeDirectory = Path.Combine(projectRoot, "Assets", "Scripts"); + Directory.CreateDirectory(editorDirectory); + Directory.CreateDirectory(runtimeDirectory); + string globalUsingPath = Path.Combine(editorDirectory, "GlobalUsings.cs"); + string runtimePath = Path.Combine(runtimeDirectory, "BaseToolSchema.cs"); + string runtimeSource = "public sealed class BaseToolSchema {}"; + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(runtimePath, runtimeSource); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(runtimePath), Is.EqualTo(runtimeSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNoAsmdefFirstPassRuntimeUsesGlobalLegacyUsing_KeepsRegularRuntimeFiles() + { + // Verifies that Unity predefined firstpass runtime scripts do not migrate regular runtime siblings. + string projectRoot = CreateProjectRoot(); + try + { + string firstPassDirectory = Path.Combine(projectRoot, "Assets", "Plugins"); + string runtimeDirectory = Path.Combine(projectRoot, "Assets", "Scripts"); + Directory.CreateDirectory(firstPassDirectory); + Directory.CreateDirectory(runtimeDirectory); + string globalUsingPath = Path.Combine(firstPassDirectory, "GlobalUsings.cs"); + string runtimePath = Path.Combine(runtimeDirectory, "UnrelatedMetadata.cs"); + string runtimeSource = + "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(runtimePath, runtimeSource); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(runtimePath), Is.EqualTo(runtimeSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_KeepsRegularEditorFiles() + { + // Verifies that Unity predefined firstpass editor scripts do not migrate regular editor siblings. + string projectRoot = CreateProjectRoot(); + try + { + string firstPassEditorDirectory = Path.Combine(projectRoot, "Assets", "Plugins", "Editor"); + string editorDirectory = Path.Combine(projectRoot, "Assets", "Editor"); + Directory.CreateDirectory(firstPassEditorDirectory); + Directory.CreateDirectory(editorDirectory); + string globalUsingPath = Path.Combine(firstPassEditorDirectory, "GlobalUsings.cs"); + string editorPath = Path.Combine(editorDirectory, "UnrelatedMetadata.cs"); + string editorSource = + "public sealed class UnrelatedMetadata { public BaseToolResponse Response; }"; + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(editorPath, editorSource); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); + Assert.That(File.ReadAllText(editorPath), Is.EqualTo(editorSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsingAndSplitManualRegistration_AddsToolContractsReference() + { + // Verifies that manual registration files relying on assembly-level legacy detection get ToolContracts refs. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string registrationPath = Path.Combine(toolDirectory, "ManualToolRegistration.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(registrationPath, @"public static class ManualToolRegistration +{ + public static void Register(IUnityTool tool) + { + CustomToolManager.RegisterCustomTool(tool); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(3)); + Assert.That(File.ReadAllText(registrationPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.RegisterCustomTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void PreviewMigration_WhenUnrelatedCustomToolManagerExists_KeepsAsmdef() + { + // Verifies that project-owned type names do not trigger migration dependencies by themselves. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string sourcePath = Path.Combine(toolDirectory, "CustomToolManager.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string source = "public sealed class CustomToolManager {}"; + string asmdefSource = @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"; + File.WriteAllText(sourcePath, source); + File.WriteAllText(asmdefPath, asmdefSource); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(preview.HasTargets, Is.False); + Assert.That(result.FileCount, Is.EqualTo(0)); + Assert.That(File.ReadAllText(sourcePath), Is.EqualTo(source)); + Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenUserSidecarFilesExist_PreservesSidecarFiles() + { + // Verifies that project-wide source rewrites do not treat user sidecars as migration scratch files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string tempSidecarPath = toolPath + ".tmp"; + string backupSidecarPath = toolPath + ".bak"; + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +}"); + File.WriteAllText(tempSidecarPath, "user temp sidecar"); + File.WriteAllText(backupSidecarPath, "user backup sidecar"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(tempSidecarPath), Is.EqualTo("user temp sidecar")); + Assert.That(File.ReadAllText(backupSidecarPath), Is.EqualTo("user backup sidecar")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void PreviewMigration_WhenLegacyToolExistsUnderExcludedDirectory_IgnoresFile() + { + // Verifies that generated Unity folders are not scanned for third-party tools. + string projectRoot = CreateProjectRoot(); + try + { + string generatedDirectory = Path.Combine(projectRoot, "Library"); + Directory.CreateDirectory(generatedDirectory); + File.WriteAllText( + Path.Combine(generatedDirectory, "GeneratedTool.cs"), + "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class GeneratedTool {}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + + Assert.That(preview.HasTargets, Is.False); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void PreviewMigration_WhenLegacyToolExistsOutsideAssets_IgnoresFile() + { + // Verifies that repository tooling outside Unity source roots is not migrated. + string projectRoot = CreateProjectRoot(); + try + { + string toolsDirectory = Path.Combine(projectRoot, "tools"); + Directory.CreateDirectory(toolsDirectory); + File.WriteAllText( + Path.Combine(toolsDirectory, "LegacyToolFixture.cs"), + "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class LegacyToolFixture {}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + + Assert.That(preview.HasTargets, Is.False); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenLegacyToolExistsUnderAssetsPackagesDirectory_RewritesAssetsFiles() + { + // Verifies that only Unity's project-root Packages directory is excluded from migration scans. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "Packages", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +} + +public sealed class HelloSchema : BaseToolSchema +{ +} + +public sealed class HelloResponse : BaseToolResponse +{ +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenLegacyToolExistsUnderPackagesDirectory_KeepsPackageFiles() + { + // Verifies that Package Manager and embedded package contents are not rewritten in place. + string projectRoot = CreateProjectRoot(); + try + { + string packageDirectory = Path.Combine( + projectRoot, + "Packages", + "com.example.legacy-tool", + "Editor"); + Directory.CreateDirectory(packageDirectory); + string toolPath = Path.Combine(packageDirectory, "PackageTool.cs"); + string asmdefPath = Path.Combine(packageDirectory, "LegacyPackageTool.Editor.asmdef"); + string toolSource = @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class PackageTool : AbstractUnityTool +{ +} + +public sealed class PackageToolSchema : BaseToolSchema +{ +} + +public sealed class PackageToolResponse : BaseToolResponse +{ +}"; + string asmdefSource = @"{ + ""name"": ""LegacyPackageTool.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"; + File.WriteAllText(toolPath, toolSource); + File.WriteAllText(asmdefPath, asmdefSource); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(preview.HasTargets, Is.False); + Assert.That(result.FileCount, Is.EqualTo(0)); + Assert.That(File.ReadAllText(toolPath), Is.EqualTo(toolSource)); + Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderAssets_ReturnsTrue() + { + // Verifies that startup detection can find V2 custom tools without building a migration preview. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "HelloTool.cs"), + "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class HelloTool {}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets_ReturnsTrue() + { + // Verifies that startup detection catches old assembly names without relying on source files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""uLoopMCP.Editor"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsExistsWithLegacyAsmdefGuid_ReturnsTrue() + { + // Verifies that startup detection catches partially migrated tools that still need asmdef repair. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "CurrentHelloTool.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +[UnityCliLoopTool] +public sealed class CurrentHelloTool : UnityCliLoopTool +{ +} + +public sealed class HelloSchema : UnityCliLoopToolSchema +{ +} + +public sealed class HelloResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenLegacyGlobalUsingAndBareToolAttributeAreSplit_ReturnsTrue() + { + // Verifies that startup detection treats legacy global usings as assembly-scoped. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText( + Path.Combine(toolDirectory, "BareAttributeTool.cs"), + @"[McpTool] +public sealed class BareAttributeTool +{ +}"); + 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 async Task HasMigrationTargetsAsync_WhenUnrelatedAsmdefJsonIsMalformed_StillDetectsAsmdefRepair() + { + // Verifies that unrelated malformed asmdefs do not block startup detection for repairable tools. + string projectRoot = CreateProjectRoot(); + try + { + string unrelatedDirectory = Path.Combine(projectRoot, "Assets", "Unrelated"); + Directory.CreateDirectory(unrelatedDirectory); + File.WriteAllText(Path.Combine(unrelatedDirectory, "Broken.asmdef"), "{"); + + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "CurrentHelloTool.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +[UnityCliLoopTool] +public sealed class CurrentHelloTool : UnityCliLoopTool +{ +} + +public sealed class HelloSchema : UnityCliLoopToolSchema +{ +} + +public sealed class HelloResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentDomainGlobalUsingAndSplitMetadata_ReturnsTrue() + { + // Verifies that startup detection treats current Domain global usings as assembly-scoped. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using io.github.hatayama.UnityCliLoop.Domain;"); + File.WriteAllText( + Path.Combine(toolDirectory, "ToolMetadataProvider.cs"), + @"public static class ToolMetadataProvider +{ + public static ToolInfo[] GetTools() + { + return new ToolInfo[0]; + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_ReturnsTrue() + { + // Verifies that preview detection reports a missing screenshot reference for current FirstPartyTools global using. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + 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 async Task HasMigrationTargetsAsync_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_ReturnsTrue() + { + // Verifies that preview detection reports a missing screenshot reference for current FirstPartyTools alias using. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "AliasUsing.cs"), + "using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + 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 async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentApplicationGlobalAlias_ReturnsTrue() + { + // Verifies that preview detection treats current Application global aliases as assembly-scoped. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using App = io.github.hatayama.UnityCliLoop.Application;"); + File.WriteAllText( + Path.Combine(toolDirectory, "MainThreadUsage.cs"), + @"using System.Threading; +using System.Threading.Tasks; + +public sealed class MainThreadUsage +{ + public async Task RunAsync(CancellationToken ct) + { + await App.MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_ReturnsTrue() + { + // Verifies that preview detection treats current FirstPartyTools global aliases as assembly-scoped. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText( + Path.Combine(toolDirectory, "CurrentScreenshotTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class CurrentScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentToolContractsGlobalUsingAndSplitPartialHelpers_ReturnsTrue() + { + // Verifies that startup detection treats current ToolContracts global usings as assembly-scoped. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using io.github.hatayama.UnityCliLoop.ToolContracts;"); + File.WriteAllText( + Path.Combine(toolDirectory, "PartialTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; + +public sealed class PartialTool : UnityCliLoopTool +{ + public async Task RunAsync(CancellationToken ct) + { + await EditorDelay.DelayFrame(2, ct); + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +} + +public sealed class PartialSchema : UnityCliLoopToolSchema +{ +} + +public sealed class PartialResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsGlobalUsingOnlyNeedsHelperRewrite_ReturnsTrue() + { + // Verifies that startup detection reports source-only helper migrations after asmdef refs are already fixed. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "GlobalUsings.cs"), + "global using io.github.hatayama.UnityCliLoop.ToolContracts;"); + File.WriteAllText( + Path.Combine(toolDirectory, "PartialTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; + +public sealed class PartialTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"", + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenAssemblyDeclaresMainThreadSwitcherForCurrentToolContractsFile_ReturnsFalse() + { + // Verifies that startup detection uses assembly-scoped type declarations before adding V3 references. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "AUsage.cs"), + @"using System.Threading.Tasks; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class MainThreadUsage +{ + public Task RunAsync() + { + return MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "ZMainThreadSwitcher.cs"), + @"using System.Threading.Tasks; + +public enum PlayerLoopTiming +{ + Update +} + +public static class MainThreadSwitcher +{ + public static Task SwitchToMainThread(PlayerLoopTiming timing) + { + return Task.CompletedTask; + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.False); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentSplitScreenshotDtosDoNotUseScreenshotApis_ReturnsFalse() + { + // Verifies that startup detection protects split local DTOs before screenshot reference checks. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "ATool.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CurrentScreenshotTool : UnityCliLoopTool +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "ZDtos.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotSchema : UnityCliLoopToolSchema +{ +} + +public sealed class ScreenshotResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.False); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentRenderingCaptureNeedsDiscard_ReturnsTrue() + { + // Verifies that startup detection reports current rendering capture deconstruction rewrites. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "RenderingCapture.cs"), + @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync( + 1.0f, + UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, + ct); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsFileUsesLegacyScreenshotCaptureWithoutAsmdef_ReturnsTrue() + { + // Verifies that startup detection reports source-only screenshot signature migrations. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "ScreenshotTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +} + +public sealed class ScreenshotSchema : UnityCliLoopToolSchema +{ +} + +public sealed class ScreenshotResponse : UnityCliLoopToolResponse +{ +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsFileUsesLegacyScreenshotCaptureAndReferencesExist_ReturnsTrue() + { + // Verifies that startup detection is not hidden by already-correct asmdef references. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "ScreenshotTool.cs"), + @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +} + +public sealed class ScreenshotSchema : UnityCliLoopToolSchema +{ +} + +public sealed class ScreenshotResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentApplicationGuidReferenceExists_ReturnsTrue() + { + // Verifies that current V3 Application namespace usage still triggers source migration. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "CurrentManualToolRegistration.cs"), + @"using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public static class CurrentManualToolRegistration +{ + public static void Register(IUnityCliLoopTool tool) + { + UnityCliLoopToolRegistrar.RegisterCustomTool(tool); + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"", + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task HasMigrationTargetsAsync_WhenCurrentRegistrarReturnMissingDomainReference_ReturnsTrue() + { + // Verifies that startup detection catches registrar return calls that still need Domain asmdef refs. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + File.WriteAllText( + Path.Combine(toolDirectory, "ToolCountLabel.cs"), + @"using io.github.hatayama.UnityCliLoop.Application; + +public static class ToolCountLabel +{ + public static string GetLabel() + { + return $""Tools: {UnityCliLoopToolRegistrar.GetRegisteredCustomTools().Length}""; + } +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"", + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); } finally { @@ -1773,23 +3880,27 @@ public sealed class PackageToolResponse : BaseToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderAssets_ReturnsTrue() + public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderPackagesDirectory_ReturnsFalse() { - // Verifies that startup detection can find V2 custom tools without building a migration preview. + // Verifies that startup detection keeps Package Manager contents outside migration scope. string projectRoot = CreateProjectRoot(); try { - string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); - Directory.CreateDirectory(toolDirectory); + string packageDirectory = Path.Combine( + projectRoot, + "Packages", + "com.example.legacy-tool", + "Editor"); + Directory.CreateDirectory(packageDirectory); File.WriteAllText( - Path.Combine(toolDirectory, "HelloTool.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class HelloTool {}"); + Path.Combine(packageDirectory, "PackageTool.cs"), + "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class PackageTool {}"); ThirdPartyToolMigrationFileService service = new(); bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(hasTargets, Is.False); } finally { @@ -1798,28 +3909,47 @@ public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderAssets_Retur } [Test] - public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets_ReturnsTrue() + public void PreviewMigration_WhenCacheIsInvalidated_RefreshesChangedProject() { - // Verifies that startup detection catches old assembly names without relying on source files. + // Verifies that repeated setup wizard previews can reuse scans and refresh after invalidation. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +} + +public sealed class HelloSchema : BaseToolSchema +{ +} + +public sealed class HelloResponse : BaseToolResponse +{ +}"); + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""uLoopMCP.Editor"" + ""GUID:214998e563c124e8a88199b2dd1f522d"" ] }"); ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview firstPreview = service.PreviewMigration(projectRoot); + File.WriteAllText(toolPath, "public sealed class HelloTool {}"); + ThirdPartyToolMigrationPreview cachedPreview = service.PreviewMigration(projectRoot); + service.InvalidatePreviewCache(); + ThirdPartyToolMigrationPreview refreshedPreview = service.PreviewMigration(projectRoot); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - - Assert.That(hasTargets, Is.True); + Assert.That(firstPreview.HasTargets, Is.True); + Assert.That(cachedPreview.FileCount, Is.EqualTo(firstPreview.FileCount)); + Assert.That(refreshedPreview.HasTargets, Is.False); } finally { @@ -1828,33 +3958,31 @@ public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets } [Test] - public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsExistsWithLegacyAsmdefGuid_ReturnsTrue() + public async Task PreviewMigrationAsync_WhenProjectChangesAfterCachedPreview_RefreshesCurrentProject() { - // Verifies that startup detection catches partially migrated tools that still need asmdef repair. + // Verifies that migration wizard Refresh reads the current files instead of reusing a stale preview. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "CurrentHelloTool.cs"), - @"using io.github.hatayama.UnityCliLoop.ToolContracts; + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; -[UnityCliLoopTool] -public sealed class CurrentHelloTool : UnityCliLoopTool +[McpTool] +public sealed class HelloTool : AbstractUnityTool { } -public sealed class HelloSchema : UnityCliLoopToolSchema +public sealed class HelloSchema : BaseToolSchema { } -public sealed class HelloResponse : UnityCliLoopToolResponse +public sealed class HelloResponse : BaseToolResponse { }"); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ ""GUID:214998e563c124e8a88199b2dd1f522d"" @@ -1862,10 +3990,16 @@ public sealed class HelloResponse : UnityCliLoopToolResponse }"); ThirdPartyToolMigrationFileService service = new(); + Progress progress = new(); + ThirdPartyToolMigrationPreview firstPreview = + await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); + File.WriteAllText(toolPath, "public sealed class HelloTool {}"); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + ThirdPartyToolMigrationPreview refreshedPreview = + await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(firstPreview.HasTargets, Is.True); + Assert.That(refreshedPreview.HasTargets, Is.False); } finally { @@ -1874,9 +4008,9 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenUnrelatedAsmdefJsonIsMalformed_StillDetectsAsmdefRepair() + public async Task PreviewMigrationAsync_WhenUnrelatedAsmdefJsonIsMalformedAndNoAsmrefs_PreviewsAsmdefRepair() { - // Verifies that unrelated malformed asmdefs do not block startup detection for repairable tools. + // Verifies that unrelated malformed asmdefs do not block previewing repairable asmdef changes. string projectRoot = CreateProjectRoot(); try { @@ -1912,10 +4046,13 @@ public sealed class HelloResponse : UnityCliLoopToolResponse }"); ThirdPartyToolMigrationFileService service = new(); + Progress progress = new(); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + ThirdPartyToolMigrationPreview preview = + await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(preview.HasTargets, Is.True); + Assert.That(preview.FileCount, Is.EqualTo(1)); } finally { @@ -1924,25 +4061,33 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentDomainGlobalUsingAndSplitMetadata_ReturnsTrue() + public async Task PreviewMigrationAsync_WhenUnrelatedAsmrefJsonIsMalformed_PreviewsAsmdefRepair() { - // Verifies that startup detection treats current Domain global usings as assembly-scoped. + // Verifies that malformed asmrefs do not block async preview scans for repairable assemblies. string projectRoot = CreateProjectRoot(); try { + string unrelatedDirectory = Path.Combine(projectRoot, "Assets", "Unrelated"); + Directory.CreateDirectory(unrelatedDirectory); + File.WriteAllText(Path.Combine(unrelatedDirectory, "Broken.asmref"), "{"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); File.WriteAllText( - Path.Combine(toolDirectory, "GlobalUsings.cs"), - "global using io.github.hatayama.UnityCliLoop.Domain;"); - File.WriteAllText( - Path.Combine(toolDirectory, "ToolMetadataProvider.cs"), - @"public static class ToolMetadataProvider + Path.Combine(toolDirectory, "CurrentHelloTool.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +[UnityCliLoopTool] +public sealed class CurrentHelloTool : UnityCliLoopTool +{ +} + +public sealed class HelloSchema : UnityCliLoopToolSchema +{ +} + +public sealed class HelloResponse : UnityCliLoopToolResponse { - public static ToolInfo[] GetTools() - { - return new ToolInfo[0]; - } }"); File.WriteAllText( Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), @@ -1954,10 +4099,13 @@ public static ToolInfo[] GetTools() }"); ThirdPartyToolMigrationFileService service = new(); + Progress progress = new(); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + ThirdPartyToolMigrationPreview preview = + await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(preview.HasTargets, Is.True); + Assert.That(preview.FileCount, Is.EqualTo(1)); } finally { @@ -1966,17 +4114,17 @@ public static ToolInfo[] GetTools() } [Test] - public async Task HasMigrationTargetsAsync_WhenCurrentApplicationGuidReferenceExists_ReturnsFalse() + public void PreviewMigration_WhenCurrentApplicationGuidReferenceExists_ReportsSourceMigration() { - // Verifies that current V3 Application references do not trigger the startup migration prompt. + // Verifies that V3 Application namespace usage is reported as source migration. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "CurrentManualToolRegistration.cs"), - @"using io.github.hatayama.UnityCliLoop.Application; + string registrationPath = Path.Combine(toolDirectory, "CurrentManualToolRegistration.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string registrationSource = @"using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.ToolContracts; public static class CurrentManualToolRegistration @@ -1985,22 +4133,26 @@ public static void Register(IUnityCliLoopTool tool) { UnityCliLoopToolRegistrar.RegisterCustomTool(tool); } -}"); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ +}"; + string asmdefSource = @"{ ""name"": ""VendorTools.Editor"", ""references"": [ ""GUID:214998e563c124e8a88199b2dd1f522d"", ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] -}"); +}"; + File.WriteAllText(registrationPath, registrationSource); + File.WriteAllText(asmdefPath, asmdefSource); ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - - Assert.That(hasTargets, Is.False); + Assert.That(preview.HasTargets, Is.True); + Assert.That(preview.FileCount, Is.EqualTo(1)); + Assert.That(preview.FilePaths.Contains(registrationPath), Is.True); + Assert.That(preview.FilePaths.Contains(asmdefPath), Is.False); + Assert.That(File.ReadAllText(registrationPath), Is.EqualTo(registrationSource)); + Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); } finally { @@ -2009,69 +4161,61 @@ public static void Register(IUnityCliLoopTool tool) } [Test] - public async Task HasMigrationTargetsAsync_WhenCurrentRegistrarReturnMissingDomainReference_ReturnsTrue() + public async Task PreviewMigrationAsync_WhenProjectContainsManyFiles_ReportsIncrementalProgress() { - // Verifies that startup detection catches registrar return calls that still need Domain asmdef refs. + // Verifies that setup wizard previews report progress before the full scan finishes. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "ToolCountLabel.cs"), - @"using io.github.hatayama.UnityCliLoop.Application; + for (int i = 0; i < 48; i++) + { + File.WriteAllText( + Path.Combine(toolDirectory, $"Plain{i}.cs"), + $"public sealed class Plain{i} {{}}"); + } -public static class ToolCountLabel + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; + +[McpTool] +public sealed class HelloTool : AbstractUnityTool +{ +} + +public sealed class HelloSchema : BaseToolSchema +{ +} + +public sealed class HelloResponse : BaseToolResponse { - public static string GetLabel() - { - return $""Tools: {UnityCliLoopToolRegistrar.GetRegisteredCustomTools().Length}""; - } }"); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"", - ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ""GUID:214998e563c124e8a88199b2dd1f522d"" ] }"); + List reports = new(); + RecordingMigrationProgress progress = new(reports); ThirdPartyToolMigrationFileService service = new(); - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - - Assert.That(hasTargets, Is.True); - } - finally - { - Directory.Delete(projectRoot, recursive: true); - } - } - - [Test] - public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderPackagesDirectory_ReturnsFalse() - { - // Verifies that startup detection keeps Package Manager contents outside migration scope. - string projectRoot = CreateProjectRoot(); - try - { - string packageDirectory = Path.Combine( - projectRoot, - "Packages", - "com.example.legacy-tool", - "Editor"); - Directory.CreateDirectory(packageDirectory); - File.WriteAllText( - Path.Combine(packageDirectory, "PackageTool.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class PackageTool {}"); - - ThirdPartyToolMigrationFileService service = new(); - - bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + ThirdPartyToolMigrationPreview preview = + await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - Assert.That(hasTargets, Is.False); + Assert.That(preview.HasTargets, Is.True); + Assert.That(reports.Count, Is.GreaterThan(1)); + Assert.That( + reports.Any(report => + report.TotalItemCount > 0 && + report.ProcessedItemCount > 0 && + report.ProcessedItemCount < report.TotalItemCount), + Is.True); + ThirdPartyToolMigrationProgress lastReport = reports[reports.Count - 1]; + Assert.That(lastReport.ProcessedItemCount, Is.EqualTo(lastReport.TotalItemCount)); } finally { @@ -2080,14 +4224,21 @@ public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderPackagesDire } [Test] - public void PreviewMigration_WhenCacheIsInvalidated_RefreshesChangedProject() + public async Task ApplyMigrationAsync_WhenProjectContainsManyFiles_ReportsProgressAndWritesChanges() { - // Verifies that repeated setup wizard previews can reuse scans and refresh after invalidation. + // Verifies that setup wizard migration applies file writes after reporting incremental progress. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); + for (int i = 0; i < 48; i++) + { + File.WriteAllText( + Path.Combine(toolDirectory, $"Plain{i}.cs"), + $"public sealed class Plain{i} {{}}"); + } + string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; @@ -2111,16 +4262,25 @@ public sealed class HelloResponse : BaseToolResponse ] }"); + List reports = new(); + RecordingMigrationProgress progress = new(reports); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview firstPreview = service.PreviewMigration(projectRoot); - File.WriteAllText(toolPath, "public sealed class HelloTool {}"); - ThirdPartyToolMigrationPreview cachedPreview = service.PreviewMigration(projectRoot); - service.InvalidatePreviewCache(); - ThirdPartyToolMigrationPreview refreshedPreview = service.PreviewMigration(projectRoot); - Assert.That(firstPreview.HasTargets, Is.True); - Assert.That(cachedPreview.FileCount, Is.EqualTo(firstPreview.FileCount)); - Assert.That(refreshedPreview.HasTargets, Is.False); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync(projectRoot, progress, CancellationToken.None); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(reports.Count, Is.GreaterThan(1)); + Assert.That( + reports.Any(report => + report.TotalItemCount > 0 && + report.ProcessedItemCount > 0 && + report.ProcessedItemCount < report.TotalItemCount), + Is.True); + ThirdPartyToolMigrationProgress lastReport = reports[reports.Count - 1]; + Assert.That(lastReport.ProcessedItemCount, Is.EqualTo(lastReport.TotalItemCount)); } finally { @@ -2129,48 +4289,47 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public async Task PreviewMigrationAsync_WhenProjectChangesAfterCachedPreview_RefreshesCurrentProject() + public async Task ApplyMigrationAsync_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsToolContractsReference() { - // Verifies that migration wizard Refresh reads the current files instead of reusing a stale preview. + // Verifies that async migration also adds ToolContracts asmdef references for regular legacy usings. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); + string toolPath = Path.Combine(toolDirectory, "MainThreadTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; - -[McpTool] -public sealed class HelloTool : AbstractUnityTool -{ -} - -public sealed class HelloSchema : BaseToolSchema -{ -} + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -public sealed class HelloResponse : BaseToolResponse +public sealed class MainThreadTool { + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } }"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""references"": [] }"); ThirdPartyToolMigrationFileService service = new(); - Progress progress = new(); - ThirdPartyToolMigrationPreview firstPreview = - await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - File.WriteAllText(toolPath, "public sealed class HelloTool {}"); - - ThirdPartyToolMigrationPreview refreshedPreview = - await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - - Assert.That(firstPreview.HasTargets, Is.True); - Assert.That(refreshedPreview.HasTargets, Is.False); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); + + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -2179,37 +4338,29 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public async Task PreviewMigrationAsync_WhenUnrelatedAsmdefJsonIsMalformedAndNoAsmrefs_PreviewsAsmdefRepair() + public async Task ApplyMigrationAsync_WhenAssemblyUsesCurrentApplicationGlobalAlias_RewritesQualifiedMainThreadSwitcherTiming() { - // Verifies that unrelated malformed asmdefs do not block previewing repairable asmdef changes. + // Verifies that async migration uses current Application global aliases from sibling files. string projectRoot = CreateProjectRoot(); try { - string unrelatedDirectory = Path.Combine(projectRoot, "Assets", "Unrelated"); - Directory.CreateDirectory(unrelatedDirectory); - File.WriteAllText(Path.Combine(unrelatedDirectory, "Broken.asmdef"), "{"); - string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "CurrentHelloTool.cs"), - @"using io.github.hatayama.UnityCliLoop.ToolContracts; - -[UnityCliLoopTool] -public sealed class CurrentHelloTool : UnityCliLoopTool -{ -} - -public sealed class HelloSchema : UnityCliLoopToolSchema -{ -} + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string mainThreadPath = Path.Combine(toolDirectory, "MainThreadUsage.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using App = io.github.hatayama.UnityCliLoop.Application;"); + File.WriteAllText(mainThreadPath, @"using System.Threading; +using System.Threading.Tasks; -public sealed class HelloResponse : UnityCliLoopToolResponse +public sealed class MainThreadUsage { + public async Task RunAsync(CancellationToken ct) + { + await App.MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } }"); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ ""GUID:214998e563c124e8a88199b2dd1f522d"" @@ -2217,13 +4368,20 @@ public sealed class HelloResponse : UnityCliLoopToolResponse }"); ThirdPartyToolMigrationFileService service = new(); - Progress progress = new(); - - ThirdPartyToolMigrationPreview preview = - await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); - - Assert.That(preview.HasTargets, Is.True); - Assert.That(preview.FileCount, Is.EqualTo(1)); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedSource = File.ReadAllText(mainThreadPath); + + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(result.FilePaths.Contains(mainThreadPath), Is.True); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using App = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(migratedSource, Does.Contain( + "await App.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -2232,51 +4390,37 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public async Task PreviewMigrationAsync_WhenUnrelatedAsmrefJsonIsMalformed_PreviewsAsmdefRepair() + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that malformed asmrefs do not block async preview scans for repairable assemblies. + // Verifies that async migration rewrites current FirstPartyTools global using to ToolContracts. string projectRoot = CreateProjectRoot(); try { - string unrelatedDirectory = Path.Combine(projectRoot, "Assets", "Unrelated"); - Directory.CreateDirectory(unrelatedDirectory); - File.WriteAllText(Path.Combine(unrelatedDirectory, "Broken.asmref"), "{"); - string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - File.WriteAllText( - Path.Combine(toolDirectory, "CurrentHelloTool.cs"), - @"using io.github.hatayama.UnityCliLoop.ToolContracts; - -[UnityCliLoopTool] -public sealed class CurrentHelloTool : UnityCliLoopTool -{ -} - -public sealed class HelloSchema : UnityCliLoopToolSchema -{ -} - -public sealed class HelloResponse : UnityCliLoopToolResponse -{ -}"); - File.WriteAllText( - Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), - @"{ + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""references"": [] }"); ThirdPartyToolMigrationFileService service = new(); - Progress progress = new(); - - ThirdPartyToolMigrationPreview preview = - await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(preview.HasTargets, Is.True); - Assert.That(preview.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { @@ -2285,42 +4429,57 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public void PreviewMigration_WhenCurrentApplicationGuidReferenceExists_KeepsAsmdef() + public async Task ApplyMigrationAsync_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_RewritesQualifiedSplitCapture() { - // Verifies that V3 Application references are not reported as legacy asmdef migration. + // Verifies that async migration uses current FirstPartyTools global aliases from sibling files. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string registrationPath = Path.Combine(toolDirectory, "CurrentManualToolRegistration.cs"); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string toolPath = Path.Combine(toolDirectory, "CurrentScreenshotTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); - string registrationSource = @"using io.github.hatayama.UnityCliLoop.Application; -using io.github.hatayama.UnityCliLoop.ToolContracts; + File.WriteAllText(globalUsingPath, "global using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(toolPath, @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; -public static class CurrentManualToolRegistration +public sealed class CurrentScreenshotTool { - public static void Register(IUnityCliLoopTool tool) + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) { - UnityCliLoopToolRegistrar.RegisterCustomTool(tool); + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } -}"; - string asmdefSource = @"{ +}"); + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"", - ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" ] -}"; - File.WriteAllText(registrationPath, registrationSource); - File.WriteAllText(asmdefPath, asmdefSource); +}"); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedSource = File.ReadAllText(toolPath); - Assert.That(preview.HasTargets, Is.False); - Assert.That(File.ReadAllText(registrationPath), Is.EqualTo(registrationSource)); - Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(result.FilePaths.Contains(globalUsingPath), Is.True); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(migratedSource, Does.Not.Contain( + "return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync")); } finally { @@ -2329,21 +4488,53 @@ public static void Register(IUnityCliLoopTool tool) } [Test] - public async Task PreviewMigrationAsync_WhenProjectContainsManyFiles_ReportsIncrementalProgress() + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that setup wizard previews report progress before the full scan finishes. + // Verifies that async migration rewrites current FirstPartyTools alias using to ToolContracts. string projectRoot = CreateProjectRoot(); try { string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - for (int i = 0; i < 48; i++) - { - File.WriteAllText( - Path.Combine(toolDirectory, $"Plain{i}.cs"), - $"public sealed class Plain{i} {{}}"); - } + string aliasUsingPath = Path.Combine(toolDirectory, "AliasUsing.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(aliasUsingPath, "using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedAsmdef = File.ReadAllText(asmdefPath); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(result.FilePaths.Contains(aliasUsingPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(aliasUsingPath), Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + [Test] + public async Task ApplyMigrationAsync_WhenCanceledAfterPlanCompletes_SkipsWritesBeforeApply() + { + // Verifies that cancellation before the first file write leaves project files untouched. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); string toolPath = Path.Combine(toolDirectory, "HelloTool.cs"); string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); File.WriteAllText(toolPath, @"using io.github.hatayama.uLoopMCP; @@ -2367,23 +4558,67 @@ public sealed class HelloResponse : BaseToolResponse ] }"); - List reports = new(); - RecordingMigrationProgress progress = new(reports); + using CancellationTokenSource cts = new(); + CancelOnCompleteMigrationProgress progress = new(cts); ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationPreview preview = - await service.PreviewMigrationAsync(projectRoot, progress, CancellationToken.None); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync(projectRoot, progress, cts.Token); - Assert.That(preview.HasTargets, Is.True); - Assert.That(reports.Count, Is.GreaterThan(1)); - Assert.That( - reports.Any(report => - report.TotalItemCount > 0 && - report.ProcessedItemCount > 0 && - report.ProcessedItemCount < report.TotalItemCount), - Is.True); - ThirdPartyToolMigrationProgress lastReport = reports[reports.Count - 1]; - Assert.That(lastReport.ProcessedItemCount, Is.EqualTo(lastReport.TotalItemCount)); + Assert.That(result.FileCount, Is.EqualTo(0)); + Assert.That(cts.IsCancellationRequested, Is.True); + Assert.That(File.ReadAllText(toolPath), Does.Contain("AbstractUnityTool")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task ApplyMigrationAsync_WhenCurrentSplitScreenshotDtosDoNotUseScreenshotApis_KeepsAsmdef() + { + // Verifies that async migration waits for assembly-level DTO discovery before screenshot reference checks. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "ATool.cs"); + string dtoPath = Path.Combine(toolDirectory, "ZDtos.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + string asmdefSource = @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"; + File.WriteAllText(toolPath, @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CurrentScreenshotTool : UnityCliLoopTool +{ +}"); + File.WriteAllText(dtoPath, @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotSchema : UnityCliLoopToolSchema +{ +} + +public sealed class ScreenshotResponse : UnityCliLoopToolResponse +{ +}"); + File.WriteAllText(asmdefPath, asmdefSource); + + ThirdPartyToolMigrationFileService service = new(); + Progress progress = new(); + + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync(projectRoot, progress, CancellationToken.None); + + Assert.That(result.FileCount, Is.EqualTo(0)); + Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + Assert.That(File.ReadAllText(toolPath), Does.Contain("UnityCliLoopTool")); } finally { @@ -2420,5 +4655,27 @@ public void Report(ThirdPartyToolMigrationProgress value) _reports.Add(value); } } + + private sealed class CancelOnCompleteMigrationProgress : IProgress + { + private readonly CancellationTokenSource _cts; + + public CancelOnCompleteMigrationProgress(CancellationTokenSource cts) + { + Assert.That(cts, Is.Not.Null); + + _cts = cts; + } + + public void Report(ThirdPartyToolMigrationProgress value) + { + if (value.TotalItemCount <= 0 || value.ProcessedItemCount < value.TotalItemCount) + { + return; + } + + _cts.Cancel(); + } + } } } diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 2a484b15a5..840d363c32 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -109,139 +109,2680 @@ public void MigrateCSharpSource_WhenLegacyToolAttributeDescriptionIsRawStringWit Assert.That(result.Content, Does.Not.Contain("\"\"\"\"say \"hi\", world\"\"\"\"")); } + [Test] + public void MigrateCSharpSource_WhenLegacyToolDescriptionInterpolatesCommaExpression_DropsDescriptionArgument() + { + // Verifies that commas inside interpolated-string expressions do not split legacy attribute arguments. + string source = "using io.github.hatayama.uLoopMCP;\n" + + "[McpTool(Description = $\"{string.Join(\",\", values)}\", 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")); + Assert.That(result.Content, Does.Not.Contain("string.Join")); + } + + [Test] + public void FindRegularInterpolatedStringEndIndex_WhenHoleContainsInterpolatedRawStringWithRawStringHole_FindsOuterStringEnd() + { + // Verifies that raw-string delimiters inside nested interpolation holes do not close the outer string. + string source = "$\"{Format($$\"\"\"outer {{ \"\"\" } , still text \"\"\" }} final\"\"\")}\", next"; + int expectedEndIndex = source.IndexOf(", next") - 1; + + int endIndex = ThirdPartyToolMigrationInterpolatedStringRules.FindRegularInterpolatedStringEndIndex( + source, + 0); + + Assert.That(endIndex, Is.EqualTo(expectedEndIndex)); + } + [Test] public void MigrateCSharpSource_WhenLegacyToolAttributeHasSecurityArgument_RewritesSecurityArgument() { - // Verifies that supported security metadata keeps compiling after the enum rename. - string source = "using io.github.hatayama.uLoopMCP;\n" + - "[McpTool(RequiredSecuritySetting = SecuritySettings.None)] public sealed class HelloTool {}"; + // Verifies that supported security metadata keeps compiling after the enum rename. + string source = "using io.github.hatayama.uLoopMCP;\n" + + "[McpTool(RequiredSecuritySetting = SecuritySettings.None)] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "[UnityCliLoopTool(RequiredSecuritySetting = UnityCliLoopSecuritySetting.None)]")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeUsesSecurityAlias_KeepsAliasIdentifier() + { + // Verifies that aliases containing the old enum name are not rewritten as partial identifiers. + string source = @"using LegacySecuritySettings = io.github.hatayama.uLoopMCP.SecuritySettings; +using io.github.hatayama.uLoopMCP; + +[McpTool(RequiredSecuritySetting = LegacySecuritySettings.None)] +public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "using LegacySecuritySettings = io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopSecuritySetting;")); + Assert.That(result.Content, Does.Contain( + "[UnityCliLoopTool(RequiredSecuritySetting = LegacySecuritySettings.None)]")); + Assert.That(result.Content, Does.Not.Contain("LegacyUnityCliLoopSecuritySetting")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeSharesAttributeList_RewritesOnlyLegacyToolEntry() + { + // Verifies that valid C# attribute lists migrate the tool attribute without dropping sibling attributes. + string source = "using io.github.hatayama.uLoopMCP;\n" + + "[McpTool(Description = \"hello\"), System.Obsolete] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("[UnityCliLoopTool, System.Obsolete]")); + Assert.That(result.Content, Does.Not.Contain("McpTool")); + Assert.That(result.Content, Does.Not.Contain("Description")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolDescriptionContainsBracket_RewritesAttribute() + { + // Verifies that description text cannot terminate the attribute-list scan early. + string source = "using io.github.hatayama.uLoopMCP;\n" + + "[McpTool(Description = \"Use [foo]\")] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("[UnityCliLoopTool]")); + Assert.That(result.Content, Does.Not.Contain("McpTool")); + Assert.That(result.Content, Does.Not.Contain("Description")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeIsQualified_RewritesQualifiedAttribute() + { + // Verifies that tools without a namespace import still receive a compilable V3 attribute. + string source = + "[io.github.hatayama.uLoopMCP.McpToolAttribute(Description = \"hello\")] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool]")); + Assert.That(result.Content, Does.Not.Contain("McpTool")); + Assert.That(result.Content, Does.Not.Contain("Description")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeIsGlobalQualified_RewritesGlobalQualifiedAttribute() + { + // Verifies that global-qualified V2 attributes do not bypass argument cleanup. + string source = + "[global::io.github.hatayama.uLoopMCP.McpTool(Description = \"hello\")] public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool]")); + Assert.That(result.Content, Does.Not.Contain("McpTool")); + Assert.That(result.Content, Does.Not.Contain("Description")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyToolAttributeIsAliasQualified_RewritesAliasQualifiedAttribute() + { + // Verifies that namespace alias attribute shorthand migrates to a resolvable V3 attribute. + string source = @"using Old = io.github.hatayama.uLoopMCP; + +[Old.McpTool(DisplayDevelopmentOnly = true)] +public sealed class HelloTool {}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool(DisplayDevelopmentOnly = true)]")); + Assert.That(result.Content, Does.Not.Contain("Old.McpTool")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyPublicHelpersAreUsed_RewritesHelperTypes() + { + // Verifies that migrated custom tools keep compiling when they override helper-driven schema behavior. + string source = @"using io.github.hatayama.uLoopMCP; + +public sealed class HelloTool +{ + public ToolParameterSchema ParameterSchema => ToolParameterSchemaGenerator.FromDto(); + + private void Fail() + { + throw new ParameterValidationException(""bad""); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("UnityCliLoopToolParameterSchemaGenerator")); + Assert.That(result.Content, Does.Contain("UnityCliLoopToolParameterValidationException")); + Assert.That(result.Content, Does.Not.Match(@"\bToolParameterSchemaGenerator\b")); + Assert.That(result.Content, Does.Not.Match(@"\bParameterValidationException\b")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMcpConstantsAreUsed_RewritesConstantsType() + { + // Verifies that legacy public constants references resolve to the V3 constants type. + string source = + "using io.github.hatayama.uLoopMCP;\n" + + "\n" + + "public static class ToolConstants\n" + + "{\n" + + " public const string Name = McpConstants.PROJECT_NAME;\n" + + " public static string QualifiedName => io.github.hatayama.uLoopMCP.McpConstants.PROJECT_NAME;\n" + + "}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("UnityCliLoopConstants.PROJECT_NAME")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.PROJECT_NAME")); + Assert.That(result.Content, Does.Not.Contain("McpConstants")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyEditorDelayIsUsed_RewritesFrameWait() + { + // Verifies that legacy frame waits migrate to the V3 frame waiter API. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(CancellationToken ct) + { + await EditorDelay.DelayFrame(1, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("using io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(result.Content, Does.Contain( + "await EditorFrameWaiter.WaitFramesOrTimeoutAsync(1, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Not.Contain("EditorDelay")); + Assert.That(result.Content, Does.Not.Contain("DelayFrame")); + } + + [Test] + public void MigrateCSharpSource_WhenOnlyQualifiedCurrentContractsExist_QualifiesEditorDelayReplacement() + { + // Verifies that helper rewrites remain resolvable when current contracts are only fully qualified. + string source = @"using System.Threading; +using System.Threading.Tasks; + +public sealed class DelayTool : io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool +{ + public async Task CaptureAsync(CancellationToken ct) + { + await EditorDelay.DelayFrame(2, ct); + } +} + +public sealed class DelaySchema : io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolSchema +{ +} + +public sealed class DelayResponse : io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolResponse +{ +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.EditorFrameWaiter.WaitFramesOrTimeoutAsync(2, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Not.Contain( + "await EditorFrameWaiter.WaitFramesOrTimeoutAsync(2, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + } + + [Test] + public void MigrateCSharpSourceForLegacyAssembly_WhenFileReliesOnGlobalUsing_RewritesEditorDelay() + { + // Verifies that split files relying on a legacy global using migrate frame waits. + string source = @"using System.Threading; +using System.Threading.Tasks; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(CancellationToken ct) + { + await EditorDelay.DelayFrame(cancellationToken: ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: System.Array.Empty()); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await EditorFrameWaiter.WaitFramesOrTimeoutAsync(1, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Not.Contain("EditorDelay")); + Assert.That(result.Content, Does.Not.Contain("DelayFrame")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyEditorWindowCaptureUtilityIsUsed_RewritesCaptureCall() + { + // Verifies that legacy window capture calls migrate to the V3 screenshot utility signature. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSourceForLegacyAssembly_WhenFileReliesOnGlobalUsing_RewritesEditorWindowCaptureUtility() + { + // Verifies that split files relying on a legacy global using migrate window capture calls. + string source = @"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 EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: System.Array.Empty()); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSourceForLegacyAssembly_WhenAssemblyUsesCurrentFirstPartyToolsGlobalUsing_RewritesEditorWindowCaptureUtility() + { + // Verifies that split files relying on a current FirstPartyTools global using finish capture migration. + string source = @"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 EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: false, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: true, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: System.Array.Empty()); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSourceForLegacyAssembly_WhenAssemblyDeclaresCaptureUtility_KeepsBareLocalHelperCall() + { + // Verifies that bare capture calls are not rebound when the project owns the helper type. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +public sealed class ScreenshotTool +{ + public Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +} + +public static class EditorWindowCaptureUtility +{ + public static Task CaptureWindowAsync( + EditorWindow window, + float resolutionScale, + CancellationToken ct) + { + return Task.FromResult(null); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: new[] { "EditorWindowCaptureUtility" }); + + Assert.That(result.Changed, Is.False); + Assert.That(result.Content, Does.Contain( + "return EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentToolContractsFileHasLegacyEditorWindowCaptureUtility_RewritesCaptureCall() + { + // Verifies that partially migrated files still finish the window capture helper migration. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentFirstPartyToolsFileHasLegacyEditorWindowCaptureUtility_RewritesCaptureCall() + { + // Verifies that partially migrated screenshot helper files still receive the V3 timeout argument. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentQualifiedCaptureWindowLacksTimeout_DoesNotDoubleQualify() + { + // Verifies that already qualified capture calls receive the timeout without duplicating namespaces. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.io.github.hatayama.UnityCliLoop.FirstPartyTools")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentFirstPartyToolsAliasCaptureLacksTimeout_RewritesCaptureCall() + { + // Verifies that current FirstPartyTools namespace aliases move to ToolContracts and receive the V3 timeout argument. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain("Fpt.UnityCliLoopConstants")); + } + + [Test] + public void MigrateCSharpSource_WhenCaptureWindowExpressionBodiedAwait_ProjectsTexture() + { + // Verifies that expression-bodied awaits keep the legacy Texture2D return shape. + string source = @"using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) => + await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + + public Func> CreateCapture() + { + return async (window, ct) => await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "=>\n (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Contain( + "=> (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Not.Contain( + "=> await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyCaptureUsesNamedCancellationToken_RewritesArgumentName() + { + // Verifies that migrated screenshot capture calls use the V3 cancellation token argument name. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, cancellationToken: ct); + } + + public async Task CaptureRenderingAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, cancellationToken: ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct: ct)")); + Assert.That(result.Content, Does.Contain( + "CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct: ct)")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyCaptureUsesNamedArgumentsInDifferentOrder_ReordersArguments() + { + // Verifies that legal named-argument capture calls keep the V3 positional timeout in the right slot. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync( + cancellationToken: ct, + window: window, + resolutionScale: 1.0f); + } + + public async Task CaptureRenderingAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync( + cancellationToken: ct, + resolutionScale: 1.0f); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct: ct)")); + Assert.That(result.Content, Does.Contain( + "CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct: ct)")); + Assert.That(result.Content, Does.Not.Contain("CaptureWindowAsync(ct: ct")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + } + + [Test] + public void MigrateCSharpSource_WhenQualifiedLegacyCaptureIsUsed_QualifiesTimeoutConstant() + { + // Verifies that capture rewrites keep timeout constants resolvable without a ToolContracts using directive. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using Legacy = io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureQualifiedAsync(EditorWindow window, CancellationToken ct) + { + return await io.github.hatayama.uLoopMCP.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + public async Task CaptureAliasAsync(EditorWindow window, CancellationToken ct) + { + return await Legacy.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(result.Content, Does.Contain("Legacy.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS")); + Assert.That(result.Content, Does.Contain( + "using Legacy = io.github.hatayama.UnityCliLoop.ToolContracts;")); + } + + [Test] + public void MigrateCSharpSource_WhenCaptureWindowUsesConfigureAwait_PreservesConfigureAwaitOnTask() + { + // Verifies that texture extraction happens after awaiting the configured capture task. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct).ConfigureAwait(false); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ConfigureAwait(false)).texture;")); + Assert.That(result.Content, Does.Not.Contain(".texture.ConfigureAwait")); + } + + [Test] + public void MigrateCSharpSource_WhenCaptureWindowAwaitIgnoresResult_KeepsStatementValid() + { + // Verifies that ignored capture results do not become invalid property-access statements. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct).ConfigureAwait(false); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ConfigureAwait(false);")); + Assert.That(result.Content, Does.Not.Contain(".texture;")); + } + + [Test] + public void MigrateCSharpSource_WhenCaptureWindowTaskIsReturned_MapsTaskTextureResult() + { + // Verifies that non-awaited legacy capture tasks keep their old Task shape. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotTool +{ + public Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopCaptureTask => __unityCliLoopCaptureTask.GetAwaiter().GetResult().texture)")); + Assert.That(result.Content, Does.Not.Contain( + "return io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyScreenshotHelpersAreUsed_RewritesFirstPartyReferences() + { + // Verifies that screenshot helper types moved out of the public tool contract namespace are fully qualified. + string source = @"using UnityEditor; +using io.github.hatayama.uLoopMCP; + +public sealed class ScreenshotHelper +{ + public WindowMatchMode MatchMode => WindowMatchMode.contains; + public CaptureMode CaptureMode => CaptureMode.rendering; + public ScreenshotInfo CreateInfo() => new ScreenshotInfo(); + public UIElementInfo CreateElement() => new UIElementInfo(); + public EditorWindow[] FindWindows() => EditorWindowCaptureUtility.FindWindowsByName(""Game"", WindowMatchMode.exact); + public string[] GetNames() => EditorWindowCaptureUtility.GetOpenWindowNames(); +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode MatchMode")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode.contains")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.CaptureMode CaptureMode")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.CaptureMode.rendering")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotInfo()")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.FindWindowsByName")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.GetOpenWindowNames")); + } + + [Test] + public void MigrateCSharpSource_WhenFileDeclaresLocalUIElementInfo_KeepsLocalReferences() + { + // Verifies that project DTOs sharing first-party screenshot names are not rewritten. + string source = @"using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class LocalTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + private (List clickableElements, List draggableElements) Classify() + { + List clickableElements = new(); + List draggableElements = new(); + UIElementInfo elementInfo = CreateElementInfo(); + clickableElements.Add(elementInfo); + return (clickableElements, draggableElements); + } + + private UIElementInfo CreateElementInfo() + { + return new UIElementInfo(); + } +} + +public sealed class LocalSchema : UnityCliLoopToolSchema +{ +} + +public sealed class LocalResponse : UnityCliLoopToolResponse +{ +} + +public sealed class UIElementInfo +{ +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(result.Content, Does.Contain("private UIElementInfo CreateElementInfo()")); + Assert.That(result.Content, Does.Contain("List clickableElements")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo")); + } + + [Test] + public void MigrateCSharpSource_WhenFileDeclaresLocalUIElementInfoAndUsesExplicitFirstPartyInfo_PreservesExplicitReference() + { + // Verifies that explicit first-party DTO references are not rebound to project DTOs. + string source = @"using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class LocalTool : UnityCliLoopTool +{ + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + private List BuildFirstPartyElements() + { + List elements = new(); + elements.Add(CreateFirstPartyElement()); + return elements; + } + + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); + } + + private UIElementInfo CreateProjectElement() + { + return new UIElementInfo(); + } +} + +public sealed class LocalSchema : UnityCliLoopToolSchema +{ +} + +public sealed class LocalResponse : UnityCliLoopToolResponse +{ +} + +public sealed class UIElementInfo +{ +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "private List BuildFirstPartyElements()")); + Assert.That(result.Content, Does.Contain( + "List elements = new();")); + Assert.That(result.Content, Does.Contain( + "private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement()")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); + Assert.That(result.Content, Does.Contain("private UIElementInfo CreateProjectElement()")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsUsed_ProjectsLegacyTuple() + { + // Verifies that old rendering capture deconstruction keeps the legacy two-item tuple shape. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "(texture, yOffset) = await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset))")); + Assert.That(result.Content, Does.Not.Contain("CaptureGameRenderingAsync(1.0f, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentGameRenderingCaptureIsUsed_KeepsCurrentTuple() + { + // Verifies that already-current rendering captures keep the V3 three-item tuple shape. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + bool timedOut = false; + (texture, yOffset, timedOut) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Content, Does.Contain( + "(texture, yOffset, timedOut) = await")); + Assert.That(result.Content, Does.Contain( + "CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Not.Contain(".ContinueWith")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentBareGameRenderingCaptureUsesTwoItemDeconstruction_AddsDiscard() + { + // Verifies that bare current rendering captures deconstruct the V3 timeout result. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "(texture, yOffset, _) = await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync")); + Assert.That(result.Content, Does.Not.Contain("(texture, yOffset) = await")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentRenderingCaptureUsesFirstPartyAlias_AddsDiscard() + { + // Verifies that current FirstPartyTools aliases move to ToolContracts and receive the V3 rendering discard. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await Fpt.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(result.Content, Does.Contain( + "(texture, yOffset, _) = await Fpt.EditorWindowCaptureUtility.CaptureGameRenderingAsync")); + } + + [Test] + public void MigrateCSharpSource_WhenProjectCaptureHelperUsesTwoItemDeconstruction_KeepsProjectHelperCall() + { + // Verifies that discard insertion only targets the V3 first-party capture helper. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class RenderingCapture +{ + public async Task CaptureAsync(CancellationToken ct) + { + Texture2D texture = null; + int yOffset = 0; + (texture, yOffset) = await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct); + } +} + +public static class EditorWindowCaptureUtility +{ + public static Task<(Texture2D texture, int yOffset)> CaptureGameRenderingAsync( + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct) + { + return Task.FromResult<(Texture2D texture, int yOffset)>((null, 0)); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.False); + Assert.That(result.Content, Does.Contain("(texture, yOffset) = await")); + Assert.That(result.Content, Does.Not.Contain("(texture, yOffset, _) = await")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsReturned_ProjectsLegacyTuple() + { + // Verifies that return-await rendering captures keep returning the legacy two-item tuple. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class RenderingCapture +{ + public async Task<(Texture2D texture, int yOffset)> CaptureAsync(CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset));")); + Assert.That(result.Content, Does.Not.Contain( + "return await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureTaskIsReturned_MapsTaskTupleResult() + { + // Verifies that task-returning rendering captures keep the legacy Task two-item tuple. + string source = @"using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using io.github.hatayama.uLoopMCP; + +public sealed class RenderingCapture +{ + public Task<(Texture2D texture, int yOffset)> CaptureAsync(CancellationToken ct) + { + return EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset));")); + Assert.That(result.Content, Does.Not.Contain( + "return EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimerDelayNamedCancellationTokenIsUsed_RewritesArgumentName() + { + // Verifies that TimerDelay named arguments keep compiling after the V3 cancellation token rename. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class DelayTool +{ + public async Task RunAsync(CancellationToken ct) + { + await TimerDelay.Wait(10, cancellationToken: ct); + await TimerDelay.WaitThenExecuteOnMainThread(10, () => {}, cancellationToken: ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("await TimerDelay.Wait(10, ct: ct);")); + Assert.That(result.Content, Does.Contain( + "await TimerDelay.WaitThenExecuteOnMainThread(10, () => {}, ct: ct);")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherIsUsed_RewritesApplicationReferences() + { + // Verifies that main-thread switch helpers moved to Application keep compiling after timing arguments were removed. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + await MainThreadSwitcher.SwitchToMainThread(timing: PlayerLoopTiming.PostLateUpdate, cancellationToken: ct); + await MainThreadSwitcher.SwitchToMainThread(timing: default, cancellationToken: ct); + await MainThreadSwitcher.SwitchToMainThread(timing); + await MainThreadSwitcher.SwitchToMainThread(timing, ct); + await MainThreadSwitcher.SwitchToMainThread(default, ct); + SwitchToMainThreadAwaitable awaitable = MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update); + bool isMainThread = MainThreadSwitcher.IsMainThread; + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.SwitchToMainThreadAwaitable awaitable = io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread();")); + Assert.That(result.Content, Does.Contain( + "bool isMainThread = io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.IsMainThread;")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(timing)")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(timing, ct)")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(default, ct)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + Assert.That(result.Content, Does.Not.Contain("timing:")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentApplicationAliasSwitcherHasLegacyTiming_RemovesTimingArgument() + { + // Verifies that partially migrated Application aliases move to ToolContracts and receive the V3 argument shape. + string source = @"using System.Threading; +using System.Threading.Tasks; +using App = io.github.hatayama.UnityCliLoop.Application; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await App.MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "using App = io.github.hatayama.UnityCliLoop.ToolContracts;")); + Assert.That(result.Content, Does.Contain( + "await App.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming.Update")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherUsesGlobalQualifiedTiming_RemovesTimingArgument() + { + // Verifies that global-qualified legacy timing values are not mistaken for named arguments. + string source = @"using System.Threading; +using System.Threading.Tasks; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread( + global::io.github.hatayama.uLoopMCP.PlayerLoopTiming.Update, + ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming.Update")); + Assert.That(result.Content, Does.Not.Contain("global::")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherUsesOutOfOrderNamedCancellationToken_PreservesToken() + { + // Verifies that named cancellation tokens are preserved even when callers pass them before timing. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(cancellationToken: ct, timing: PlayerLoopTiming.Update); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread();")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + Assert.That(result.Content, Does.Not.Contain("timing:")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherUsesSingleTimingVariable_RemovesStaleTimingParameter() + { + // Verifies that timing-only wrapper parameters do not leave V3 migration output uncompilable. + string source = @"using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop) + { + await MainThreadSwitcher.SwitchToMainThread(loop); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync()")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread();")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(loop)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherUsesSingleCancelVariable_PreservesToken() + { + // Verifies that ambiguous single-token switch calls keep cancellation-like arguments. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken cancel) + { + await MainThreadSwitcher.SwitchToMainThread(cancel); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("SwitchToMainThread(cancel);")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread();")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyMainThreadSwitcherUsesSingleLoopVariables_RemovesTimingArguments() + { + // Verifies that common single-token timing variables still migrate to the no-argument V3 call. + string source = @"using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + private PlayerLoopTiming loop; + + public async Task RunAsync(PlayerLoopTiming playerLoop) + { + await MainThreadSwitcher.SwitchToMainThread(playerLoop); + await MainThreadSwitcher.SwitchToMainThread(this.loop); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("SwitchToMainThread();")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(playerLoop)")); + Assert.That(result.Content, Does.Not.Contain("SwitchToMainThread(this.loop)")); + } + + [Test] + public void MigrateCSharpSource_WhenPartiallyMigratedSwitcherLeavesTimingParameter_RemovesStaleParameter() + { + // Verifies that rerunning migration cleans stale timing parameters after the switch call was already migrated. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; +using io.github.hatayama.UnityCliLoop.Application; + +public sealed class MainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming loop")); + } + + [Test] + public void MigrateCSharpSource_WhenUnrelatedPlayerLoopTimingFieldExists_KeepsDeclaration() + { + // Verifies that timing declaration cleanup does not mutate project-owned timing types without switch migration. + string source = @"using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class ProjectState +{ + [SerializeField] + private PlayerLoopTiming loop; +} + +public enum PlayerLoopTiming +{ + Update +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.False); + Assert.That(result.Content, Does.Contain("[SerializeField]")); + Assert.That(result.Content, Does.Contain("private PlayerLoopTiming loop;")); + } + + [Test] + public void MigrateCSharpSource_WhenUnusedTimingFieldHasAttributes_RemovesAttributeBlock() + { + // Verifies that removing a stale timing field does not leave its attributes attached to the next member. + string source = @"using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + [SerializeField] + [UnityEngine.Tooltip(""Legacy timing"")] + private PlayerLoopTiming loop; + + public string Name => ""ready""; + + public async Task RunAsync() + { + await MainThreadSwitcher.SwitchToMainThread(loop); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Not.Contain("[SerializeField]")); + Assert.That(result.Content, Does.Not.Contain("Legacy timing")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming loop")); + Assert.That(result.Content, Does.Contain("public string Name => \"ready\";")); + } + + [Test] + public void MigrateCSharpSource_WhenProjectDefinesMainThreadSwitcher_KeepsProjectHelperCall() + { + // Verifies that bare main-thread switch calls are not rebound when the project owns the helper type. + string source = @"using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public Task RunAsync(PlayerLoopTiming loop) + { + return MainThreadSwitcher.SwitchToMainThread(loop); + } +} + +public static class MainThreadSwitcher +{ + public static Task SwitchToMainThread(PlayerLoopTiming loop) + { + return Task.CompletedTask; + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Content, Does.Contain("return MainThreadSwitcher.SwitchToMainThread(loop);")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher")); + } + + [Test] + public void MigrateCSharpSourceForLegacyAssembly_WhenAssemblyDeclaresMainThreadSwitcher_KeepsProjectHelperCall() + { + // Verifies that helper ownership discovered in a sibling file prevents rebinding bare calls. + string source = @"using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public Task RunAsync(PlayerLoopTiming loop) + { + return MainThreadSwitcher.SwitchToMainThread(loop); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: new[] { "MainThreadSwitcher" }); + + Assert.That(result.Content, Does.Contain("return MainThreadSwitcher.SwitchToMainThread(loop);")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperHasSameFileCaller_UpdatesCallerArguments() + { + // Verifies that wrapper call sites stay aligned when a stale timing parameter is removed. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + + public Task CallAsync() + { + return RunAsync(PlayerLoopTiming.Update, CancellationToken.None); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return RunAsync(CancellationToken.None);")); + Assert.That(result.Content, Does.Not.Contain("RunAsync(PlayerLoopTiming.Update, CancellationToken.None)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperCallerOmitsOptionalToken_UpdatesCallerArguments() + { + // Verifies that callers omitting optional trailing parameters stay aligned after timing removal. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct = default) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + + public Task CallAsync() + { + return RunAsync(PlayerLoopTiming.Update); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct = default)")); + Assert.That(result.Content, Does.Contain("return RunAsync();")); + Assert.That(result.Content, Does.Not.Contain("RunAsync(PlayerLoopTiming.Update)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingLambdaUsesTimingParameter_KeepsLambdaSignature() + { + // Verifies that lambda parameter lists are not treated as method declarations. + string source = @"using System; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public enum PlayerLoopTiming +{ + Update +} + +public sealed class MainThreadTool +{ + public Func Create() + { + return async (PlayerLoopTiming loop) => + { + await MainThreadSwitcher.SwitchToMainThread(loop); + }; + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("async (PlayerLoopTiming loop) =>")); + Assert.That(result.Content, Does.Not.Contain("async () =>")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperIsGeneric_UpdatesGenericCallerArguments() + { + // Verifies that generic wrapper call sites stay aligned after stale timing parameters are removed. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + + public Task CallAsync(CancellationToken ct) + { + return RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("RunAsync(PlayerLoopTiming.Update, ct)")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenSameNameTimingCallerTargetsDifferentType_KeepsOtherTypeArguments() + { + // Verifies that caller rewrites target the migrated owner type instead of every same-name method. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public enum PlayerLoopTiming +{ + Update +} + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class OtherWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.FromResult(loop.ToString()); + } +} + +public sealed class MainThreadCaller +{ + public Task CallMigratedAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + + public Task CallOtherAsync(OtherWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenSameNameTimingCallerTargetsDifferentNamespace_KeepsOtherTypeArguments() + { + // Verifies that caller rewrites distinguish migrated owner types across namespaces. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Migrated +{ + public sealed class Wrapper + { + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } +} + +namespace Other +{ + public enum PlayerLoopTiming + { + Update + } + + public sealed class Wrapper + { + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.FromResult(loop.ToString()); + } + } + + public sealed class Caller + { + public Task CallAsync(Wrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("namespace Migrated")); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerUsesQualifiedOtherTarget_KeepsOtherTypeArguments() + { + // Verifies that qualified receivers are not reduced to the final identifier before matching. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Other +{ + public sealed class Wrapper + { + public static Task RunStaticAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } + + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } + } + + public sealed class WrapperFactory + { + public Wrapper Wrapper { get; } + } +} + +namespace Migrated +{ + public sealed class Wrapper + { + public static async Task RunStaticAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } + + public sealed class MainThreadCaller + { + public Task CallOtherStaticAsync(CancellationToken ct) + { + return global::Other.Wrapper.RunStaticAsync(PlayerLoopTiming.Update, ct); + } + + public Task CallOtherMemberAsync(Other.WrapperFactory factory, CancellationToken ct) + { + return factory.Wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "public static async Task RunStaticAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "return global::Other.Wrapper.RunStaticAsync(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Contain( + "return factory.Wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerUsesThisQualifiedField_RewritesCallerArguments() + { + // Verifies that this-qualified fields still match migrated wrapper receiver types. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + private MainThreadWrapper wrapper; + + public Task CallAsync(CancellationToken ct) + { + return this.wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return this.wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("this.wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerUsesFieldDeclaredAfterCall_RewritesCallerArguments() + { + // Verifies that type members declared after the call still match migrated wrapper receiver types. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + + private MainThreadWrapper wrapper; +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerUsesPropertyDeclaredAfterCall_RewritesCallerArguments() + { + // Verifies that later properties are treated as type members, not out-of-scope locals. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + + private MainThreadWrapper wrapper { get; } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerOnlyHasLocalDeclaredAfterCall_KeepsCallerArguments() + { + // Verifies that later local declarations are not treated as receiver declarations in scope. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class OtherWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(OtherWrapper wrapper, CancellationToken ct) + { + Task task = wrapper.RunAsync(PlayerLoopTiming.Update, ct); + MainThreadWrapper wrapper2 = new MainThreadWrapper(); + return task; + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("Task task = wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Not.Contain("Task task = wrapper.RunAsync(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenPreviousMethodLocalMatchesMigratedType_KeepsFieldReceiverArguments() + { + // Verifies that locals from previous methods do not shadow the actual receiver field. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class OtherWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } +} + +public sealed class MainThreadCaller +{ + public void Prepare() + { + MainThreadWrapper wrapper = new MainThreadWrapper(); + } + + public Task CallAsync(CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + + private OtherWrapper wrapper; +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Not.Contain("return wrapper.RunAsync(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLaterMethodParameterMatchesMigratedType_KeepsFieldReceiverArguments() + { + // Verifies that later method parameters are not treated as type member receiver declarations. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class OtherWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + + private OtherWrapper wrapper; + + public void Configure(MainThreadWrapper wrapper) + { + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Not.Contain("return wrapper.RunAsync(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenTimingCallerUsesImportedWrapperType_RewritesCallerArguments() + { + // Verifies that using-imported receiver types match migrated wrapper signatures across namespaces. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Helpers +{ + public sealed class Wrapper + { + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } +} + +namespace Tools +{ + using Helpers; + + public sealed class Caller + { + public Task CallAsync(Wrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenCommentLooksLikeMigratedDeclaration_KeepsOtherTypeArguments() + { + // Verifies that declaration-shaped text in comments does not retarget caller rewrites. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MigratedWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class OtherWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(OtherWrapper wrapper, CancellationToken ct) + { + // MigratedWrapper wrapper; + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenNamespacedStaticTimingWrapperIsCalled_RewritesCallerArguments() + { + // Verifies that static wrapper calls inside the same namespace match the migrated owner type. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Tools +{ + public static class Wrapper + { + public static async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } + + public sealed class Caller + { + public Task CallAsync(CancellationToken ct) + { + return Wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public static async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return Wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("Wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenNamespacedNestedTimingWrapperIsFullyQualified_RewritesCallerArguments() + { + // Verifies that namespace-qualified nested wrapper calls match the migrated owner type. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Tools +{ + public sealed class Outer + { + public static class Wrapper + { + public static async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } + } + + public sealed class Caller + { + public Task CallAsync(CancellationToken ct) + { + return Tools.Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public static async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return Tools.Outer.Wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("Tools.Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenOtherNamespaceHasSameNestedTimingWrapperName_KeepsOtherCallerArguments() + { + // Verifies that nested wrapper caller rewrites do not cross namespace ownership. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Tools +{ + public sealed class Outer + { + public static class Wrapper + { + public static async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } + } +} + +namespace Other +{ + public sealed class Caller + { + public Task CallAsync(CancellationToken ct) + { + return Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } + + public sealed class Outer + { + public static class Wrapper + { + public static Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public static async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Not.Contain("return Outer.Wrapper.RunAsync(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperCallerUsesVarLocal_UpdatesCallerArguments() + { + // Verifies that var locals initialized with the migrated owner type keep wrapper call sites compiling. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(CancellationToken ct) + { + var wrapper = new MainThreadWrapper(); + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenNestedLegacyTimingWrapperIsCalled_UpdatesCallerArguments() + { + // Verifies that nested migrated owner types keep qualified wrapper call sites compiling. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +namespace Tools +{ + public sealed class Outer + { + public static class Wrapper + { + public static async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + } + } + + public sealed class Caller + { + public Task CallAsync(CancellationToken ct) + { + return Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public static async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return Outer.Wrapper.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("Outer.Wrapper.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperCallerUsesNullConditional_UpdatesCallerArguments() + { + // Verifies that null-conditional calls stay aligned with migrated wrapper signatures. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper?.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper?.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper?.RunAsync(PlayerLoopTiming.Update, ct)")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperCallerUsesNullableReceiver_UpdatesCallerArguments() + { + // Verifies that nullable wrapper references still match the migrated owner type. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper? wrapper, CancellationToken ct) + { + return wrapper?.RunAsync(PlayerLoopTiming.Update, ct) ?? Task.CompletedTask; + } + + public Task CallRequiredAsync(MainThreadWrapper? wrapper, CancellationToken ct) + { + return wrapper!.RunAsync(PlayerLoopTiming.Update, ct); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain( - "[UnityCliLoopTool(RequiredSecuritySetting = UnityCliLoopSecuritySetting.None)]")); + Assert.That(result.Content, Does.Contain("public async Task RunAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return wrapper?.RunAsync(ct) ?? Task.CompletedTask;")); + Assert.That(result.Content, Does.Contain("return wrapper!.RunAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("wrapper?.RunAsync(PlayerLoopTiming.Update, ct)")); + Assert.That(result.Content, Does.Not.Contain("wrapper!.RunAsync(PlayerLoopTiming.Update, ct)")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolAttributeUsesSecurityAlias_KeepsAliasIdentifier() + public void MigrateCSharpSource_WhenLegacyTimingWrapperCallChain_RevisitsStaleOuterParameter() { - // Verifies that aliases containing the old enum name are not rewritten as partial identifiers. - string source = @"using LegacySecuritySettings = io.github.hatayama.uLoopMCP.SecuritySettings; + // Verifies that chained wrapper migrations remove timing parameters exposed by earlier caller rewrites. + string source = @"using System.Threading; +using System.Threading.Tasks; using io.github.hatayama.uLoopMCP; -[McpTool(RequiredSecuritySetting = LegacySecuritySettings.None)] -public sealed class HelloTool {}"; +public sealed class MainThreadWrapper +{ + public async Task InnerAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } + + public Task OuterAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return InnerAsync(loop, ct); + } + + public Task CallAsync(CancellationToken ct) + { + return OuterAsync(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public async Task InnerAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("public Task OuterAsync(CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return InnerAsync(ct);")); + Assert.That(result.Content, Does.Contain("return OuterAsync(ct);")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingWrapperIsExpressionBodied_RemovesStaleTimingParameter() + { + // Verifies that expression-bodied main-thread wrappers do not keep the removed timing type. + string source = @"using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public SwitchToMainThreadAwaitable Switch(PlayerLoopTiming loop) => + MainThreadSwitcher.SwitchToMainThread(loop); +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public io.github.hatayama.UnityCliLoop.ToolContracts.SwitchToMainThreadAwaitable Switch() =>")); Assert.That(result.Content, Does.Contain( - "using LegacySecuritySettings = io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopSecuritySetting;")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread()")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + } + + [Test] + public void MigrateCSharpSource_WhenConstructorUsesLegacyTimingSwitcher_PreservesConstructorSignature() + { + // Verifies that constructor parameters are not removed without constructor call-site migration. + string source = @"using System.Threading; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadWrapper +{ + public MainThreadWrapper(PlayerLoopTiming loop, CancellationToken ct) + { + MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +} + +public sealed class MainThreadCaller +{ + public MainThreadWrapper Create(CancellationToken ct) + { + return new MainThreadWrapper(PlayerLoopTiming.Update, ct); + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "[UnityCliLoopTool(RequiredSecuritySetting = LegacySecuritySettings.None)]")); - Assert.That(result.Content, Does.Not.Contain("LegacyUnityCliLoopSecuritySetting")); + "public MainThreadWrapper(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain("return new MainThreadWrapper(PlayerLoopTiming.Update, ct);")); + Assert.That(result.Content, Does.Not.Contain("public MainThreadWrapper(CancellationToken ct)")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolAttributeSharesAttributeList_RewritesOnlyLegacyToolEntry() + public void MigrateCSharpSource_WhenUnrelatedLocalTimingParameterIsStillUsed_PreservesParameter() { - // Verifies that valid C# attribute lists migrate the tool attribute without dropping sibling attributes. - string source = "using io.github.hatayama.uLoopMCP;\n" + - "[McpTool(Description = \"hello\"), System.Obsolete] public sealed class HelloTool {}"; + // Verifies that timing-parameter cleanup only removes stale main-thread wrapper parameters. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public enum PlayerLoopTiming +{ + Update +} + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } + + public string Format(PlayerLoopTiming loop) + { + return loop.ToString(); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("[UnityCliLoopTool, System.Obsolete]")); - Assert.That(result.Content, Does.Not.Contain("McpTool")); - Assert.That(result.Content, Does.Not.Contain("Description")); + Assert.That(result.Content, Does.Contain("public string Format(PlayerLoopTiming loop)")); + Assert.That(result.Content, Does.Contain("return loop.ToString();")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolDescriptionContainsBracket_RewritesAttribute() + public void MigrateCSharpSource_WhenUnrelatedTimingParameterIsUnused_PreservesSignature() { - // Verifies that description text cannot terminate the attribute-list scan early. - string source = "using io.github.hatayama.uLoopMCP;\n" + - "[McpTool(Description = \"Use [foo]\")] public sealed class HelloTool {}"; + // Verifies that cleanup does not remove unused timing parameters from unrelated public methods. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } + + public void Configure(PlayerLoopTiming loop) + { + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("[UnityCliLoopTool]")); - Assert.That(result.Content, Does.Not.Contain("McpTool")); - Assert.That(result.Content, Does.Not.Contain("Description")); + Assert.That(result.Content, Does.Contain("public void Configure(PlayerLoopTiming loop)")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolAttributeIsQualified_RewritesQualifiedAttribute() + public void MigrateCSharpSource_WhenUnrelatedTimingParameterMentionsSwitcherInText_PreservesSignature() { - // Verifies that tools without a namespace import still receive a compilable V3 attribute. - string source = - "[io.github.hatayama.uLoopMCP.McpToolAttribute(Description = \"hello\")] public sealed class HelloTool {}"; + // Verifies that comments and strings do not make unrelated timing parameters look migratable. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } + + public void Configure(PlayerLoopTiming loop) + { + string label = ""SwitchToMainThread""; + // MainThreadSwitcher.SwitchToMainThread(loop); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain( - "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool]")); - Assert.That(result.Content, Does.Not.Contain("McpTool")); - Assert.That(result.Content, Does.Not.Contain("Description")); + Assert.That(result.Content, Does.Contain("public void Configure(PlayerLoopTiming loop)")); + Assert.That(result.Content, Does.Contain("string label = \"SwitchToMainThread\";")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolAttributeIsGlobalQualified_RewritesGlobalQualifiedAttribute() + public void MigrateCSharpSource_WhenLegacyTimingOverrideUsesSwitcher_PreservesSignature() { - // Verifies that global-qualified V2 attributes do not bypass argument cleanup. - string source = - "[global::io.github.hatayama.uLoopMCP.McpTool(Description = \"hello\")] public sealed class HelloTool {}"; + // Verifies that override signatures are not changed independently from their base contract. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public abstract class BaseTool +{ + public abstract Task RunAsync(PlayerLoopTiming loop, CancellationToken ct); +} + +public sealed class MainThreadTool : BaseTool +{ + public override async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool]")); - Assert.That(result.Content, Does.Not.Contain("McpTool")); - Assert.That(result.Content, Does.Not.Contain("Description")); + "public override async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] - public void MigrateCSharpSource_WhenLegacyToolAttributeIsAliasQualified_RewritesAliasQualifiedAttribute() + public void MigrateCSharpSource_WhenLegacyTimingInterfaceImplementationUsesSwitcher_PreservesSignature() { - // Verifies that namespace alias attribute shorthand migrates to a resolvable V3 attribute. - string source = @"using Old = io.github.hatayama.uLoopMCP; + // Verifies that interface implementations are not changed independently from their interface contract. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -[Old.McpTool(DisplayDevelopmentOnly = true)] -public sealed class HelloTool {}"; +public interface IMainThreadTool +{ + Task RunAsync(PlayerLoopTiming loop, CancellationToken ct); +} + +public sealed class MainThreadTool : IMainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "[io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopTool(DisplayDevelopmentOnly = true)]")); - Assert.That(result.Content, Does.Not.Contain("Old.McpTool")); + "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] - public void MigrateCSharpSource_WhenLegacyPublicHelpersAreUsed_RewritesHelperTypes() + public void MigrateCSharpSource_WhenLegacyTimingVirtualMethodUsesSwitcher_PreservesSignature() { - // Verifies that migrated custom tools keep compiling when they override helper-driven schema behavior. - string source = @"using io.github.hatayama.uLoopMCP; + // Verifies that virtual signatures are not changed independently from derived overrides. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -public sealed class HelloTool +public class MainThreadTool { - public ToolParameterSchema ParameterSchema => ToolParameterSchemaGenerator.FromDto(); - - private void Fail() + public virtual async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) { - throw new ParameterValidationException(""bad""); + await MainThreadSwitcher.SwitchToMainThread(loop, ct); } }"; @@ -249,33 +2790,72 @@ private void Fail() ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("UnityCliLoopToolParameterSchemaGenerator")); - Assert.That(result.Content, Does.Contain("UnityCliLoopToolParameterValidationException")); - Assert.That(result.Content, Does.Not.Match(@"\bToolParameterSchemaGenerator\b")); - Assert.That(result.Content, Does.Not.Match(@"\bParameterValidationException\b")); + Assert.That(result.Content, Does.Contain( + "public virtual async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] - public void MigrateCSharpSource_WhenLegacyMcpConstantsAreUsed_RewritesConstantsType() + public void MigrateCSharpSource_WhenLegacyTimingExternalInterfaceImplementationUsesSwitcher_PreservesSignature() { - // Verifies that legacy public constants references resolve to the V3 constants type. - string source = - "using io.github.hatayama.uLoopMCP;\n" + - "\n" + - "public static class ToolConstants\n" + - "{\n" + - " public const string Name = McpConstants.PROJECT_NAME;\n" + - " public static string QualifiedName => io.github.hatayama.uLoopMCP.McpConstants.PROJECT_NAME;\n" + - "}"; + // Verifies that possible external interface implementations keep their public contract shape. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool : IMainThreadTool +{ + public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"; ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("UnityCliLoopConstants.PROJECT_NAME")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.PROJECT_NAME")); - Assert.That(result.Content, Does.Not.Contain("McpConstants")); + "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenUnrelatedTimingParameterCallsHelperSwitcher_PreservesSignature() + { + // Verifies that unrelated helper methods named SwitchToMainThread do not justify dropping timing parameters. + string source = @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; + +public sealed class MainThreadTool +{ + public async Task RunAsync(CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } + + public void Configure(PlayerLoopTiming loop, Helper helper) + { + helper.SwitchToMainThread(); + } +} + +public sealed class Helper +{ + public void SwitchToMainThread() + { + } +}"; + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSource(source); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain("public void Configure(PlayerLoopTiming loop, Helper helper)")); + Assert.That(result.Content, Does.Contain("helper.SwitchToMainThread();")); } [Test] @@ -296,9 +2876,9 @@ public static ToolInfo[] GetTools() ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("io.github.hatayama.UnityCliLoop.Domain.ToolInfo[]")); + Assert.That(result.Content, Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo[]")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); } [Test] @@ -325,15 +2905,15 @@ public static ToolSettingsCatalogItem[] GetCatalog() Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult CreateResult")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult CreateResult")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult.SuccessResult")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult.SuccessResult")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ToolSettingsCatalogItem[] GetCatalog")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ToolSettingsCatalogItem[] GetCatalog")); Assert.That(result.Content, Does.Contain( - "new io.github.hatayama.UnityCliLoop.Domain.ToolSettingsCatalogItem[0]")); - Assert.That(result.Content, Does.Not.Contain("ToolContracts.ServiceResult")); - Assert.That(result.Content, Does.Not.Contain("ToolContracts.ToolSettingsCatalogItem")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.ToolSettingsCatalogItem[0]")); + Assert.That(result.Content, Does.Not.Contain("uLoopMCP.ServiceResult")); + Assert.That(result.Content, Does.Not.Contain("uLoopMCP.ToolSettingsCatalogItem")); } [Test] @@ -355,7 +2935,7 @@ public static ToolSettingsCatalogItem Create(bool displayDevelopmentOnly, bool i Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "new io.github.hatayama.UnityCliLoop.Domain.ToolSettingsCatalogItem(\"hello\", displayDevelopmentOnly, isThirdParty)")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.ToolSettingsCatalogItem(\"hello\", displayDevelopmentOnly, isThirdParty)")); Assert.That(result.Content, Does.Not.Contain("\"description\", displayDevelopmentOnly")); } @@ -383,11 +2963,11 @@ public static Old.ToolSettingsCatalogItem[] GetCatalog() Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult CreateResult")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult CreateResult")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult.SuccessResult")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult.SuccessResult")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ToolSettingsCatalogItem[] GetCatalog")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ToolSettingsCatalogItem[] GetCatalog")); Assert.That(result.Content, Does.Not.Contain("Old.ServiceResult")); Assert.That(result.Content, Does.Not.Contain("Old.ToolSettingsCatalogItem")); } @@ -411,7 +2991,7 @@ public static string GetLabel() Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); Assert.That(result.Content, Does.Not.Contain("{CustomToolManager")); } @@ -434,7 +3014,7 @@ public static string GetLabel() Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); Assert.That(result.Content, Does.Not.Contain("{CustomToolManager")); } @@ -457,7 +3037,7 @@ public void MigrateCSharpSource_WhenLegacyApiIsUsedInsideInterpolatedRawString_R Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); Assert.That(result.Content, Does.Not.Contain("{CustomToolManager")); } @@ -481,7 +3061,7 @@ public void MigrateCSharpSource_WhenLegacyApiIsUsedInsideMultiDollarRawString_Re Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("Literal { braces }")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar.GetRegisteredCustomTools")); Assert.That(result.Content, Does.Not.Contain("{{CustomToolManager")); } @@ -530,7 +3110,7 @@ public static ToolInfo[] GetTools() ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("io.github.hatayama.UnityCliLoop.Domain.ToolInfo[]")); + Assert.That(result.Content, Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo[]")); Assert.That(result.Content, Does.Not.Match(@"(?")); - Assert.That(result.Content, Does.Not.Contain("public string io.github.hatayama.UnityCliLoop.Domain.ToolInfo")); + Assert.That(result.Content, Does.Not.Contain("public string io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo")); } [Test] @@ -1038,7 +3618,7 @@ public void CustomToolManager() Assert.That(result.Content, Does.Contain("public sealed class CustomToolManager")); Assert.That(result.Content, Does.Contain("public void CustomToolManager()")); Assert.That(result.Content, Does.Not.Contain( - "public sealed class io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar")); + "public sealed class io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar")); } [Test] @@ -1095,8 +3675,14 @@ public void MigrateCSharpSourceForLegacyAssembly_WhenFileReliesOnGlobalUsing_Rew ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( source, hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, legacyAssemblyAliases: System.Array.Empty(), - legacyAssemblyToolInfoAliases: System.Array.Empty()); + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: System.Array.Empty()); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("UnityCliLoopToolSchema")); @@ -1113,12 +3699,18 @@ public void MigrateCSharpSourceForLegacyAssembly_WhenFileReliesOnGlobalUsing_Rew ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( source, hasLegacyAssemblySource: true, + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, legacyAssemblyAliases: System.Array.Empty(), - legacyAssemblyToolInfoAliases: System.Array.Empty()); + legacyAssemblyToolInfoAliases: System.Array.Empty(), + currentApplicationAssemblyAliases: System.Array.Empty(), + currentFirstPartyToolsAssemblyAliases: System.Array.Empty(), + assemblyDeclaredTypeNames: System.Array.Empty()); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult Create")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult Create")); } [Test] @@ -1147,6 +3739,33 @@ public void ContainsLegacyAssemblyScopedApi_WhenLegacyDomainHelpersAreUsed_Retur Assert.That(containsLegacyApi, Is.True); } + [Test] + public void ContainsLegacyAssemblyScopedApi_WhenLegacyEditorDelayIsUsed_ReturnsTrue() + { + // Verifies that split files using old frame waits are migrated with their legacy assembly. + string source = "public sealed class Tool { public async Task Run() { await EditorDelay.DelayFrame(1); } }"; + + bool containsLegacyApi = ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi( + source, + System.Array.Empty()); + + Assert.That(containsLegacyApi, Is.True); + } + + [Test] + public void ContainsLegacyAssemblyScopedApi_WhenLegacyEditorWindowCaptureUtilityIsUsed_ReturnsTrue() + { + // Verifies that split files using old window capture helpers are migrated with their legacy assembly. + string source = + "public sealed class Tool { public async Task Run() { await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } }"; + + bool containsLegacyApi = ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi( + source, + System.Array.Empty()); + + Assert.That(containsLegacyApi, Is.True); + } + [Test] public void MigrateCSharpSource_WhenLegacyNamespaceLikeTextExists_KeepsContent() { @@ -1177,7 +3796,8 @@ public void MigrateAsmdefSource_WhenLegacyReferenceIsUsed_RewritesToToolContract hasLegacyCSharpSource: false, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); @@ -1202,7 +3822,8 @@ public void MigrateAsmdefSource_WhenLegacyRuntimeReferenceIsUsed_RewritesToRunti hasLegacyCSharpSource: false, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:c956a21f824994ef087b6de566690b3d")); @@ -1227,7 +3848,8 @@ public void MigrateAsmdefSource_WhenOnlyLegacyGuidIsUsedWithoutLegacySource_Keep hasLegacyCSharpSource: false, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.False); Assert.That(result.Content, Is.EqualTo(source)); @@ -1250,7 +3872,8 @@ public void MigrateAsmdefSource_WhenLegacyGuidIsUsedWithLegacySource_RewritesToT hasLegacyCSharpSource: true, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); @@ -1271,7 +3894,8 @@ public void MigrateAsmdefSource_WhenLegacySourceHasNoReferencesArray_AddsToolCon hasLegacyCSharpSource: true, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain(@"""references"": [")); @@ -1295,7 +3919,8 @@ public void MigrateAsmdefSource_WhenCurrentToolContractsUsesApplicationGuid_Adds hasLegacyCSharpSource: false, requiresToolContractsReference: true, requiresApplicationReference: false, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); @@ -1320,7 +3945,8 @@ public void MigrateAsmdefSource_WhenCurrentApplicationGuidAlreadyExists_DoesNotA hasLegacyCSharpSource: false, requiresToolContractsReference: true, requiresApplicationReference: true, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.False); Assert.That(result.Content, Is.EqualTo(source)); @@ -1328,9 +3954,9 @@ public void MigrateAsmdefSource_WhenCurrentApplicationGuidAlreadyExists_DoesNotA } [Test] - public void MigrateAsmdefSource_WhenManualRegistrationIsUsed_KeepsApplicationReference() + public void MigrateAsmdefSource_WhenManualRegistrationIsUsed_RewritesToToolContractsReference() { - // Verifies that migrated manual registration code can reference the V3 registrar assembly. + // Verifies that migrated manual registration code references the public ToolContracts assembly. string source = @"{ ""name"": ""MyCompany.Tools.Editor"", ""references"": [ @@ -1344,18 +3970,19 @@ public void MigrateAsmdefSource_WhenManualRegistrationIsUsed_KeepsApplicationRef hasLegacyCSharpSource: true, requiresToolContractsReference: false, requiresApplicationReference: true, - requiresDomainReference: false); + requiresDomainReference: false, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(result.Content, Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + Assert.That(result.Content, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); Assert.That(result.Content, Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } [Test] - public void MigrateAsmdefSource_WhenDomainMetadataIsUsed_AddsDomainReference() + public void MigrateAsmdefSource_WhenDomainMetadataIsUsed_RewritesToToolContractsReference() { - // Verifies that ToolInfo-only helper assemblies can resolve the V3 Domain metadata type. + // Verifies that ToolInfo-only helper assemblies can resolve metadata through ToolContracts. string source = @"{ ""name"": ""MyCompany.Tools.Editor"", ""references"": [ @@ -1369,18 +3996,19 @@ public void MigrateAsmdefSource_WhenDomainMetadataIsUsed_AddsDomainReference() hasLegacyCSharpSource: true, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: true); + requiresDomainReference: true, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(result.Content, Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(result.Content, Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(result.Content, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } [Test] - public void MigrateAsmdefSource_WhenCurrentDomainMetadataRequiresDomainReference_AddsToolContractsReference() + public void MigrateAsmdefSource_WhenCurrentDomainMetadataRequiresReference_AddsToolContractsReference() { - // Verifies that direct V3 Domain consumers also receive transitive ToolContracts access. + // Verifies that direct V3 Domain consumers are redirected through ToolContracts access. string source = @"{ ""name"": ""MyCompany.Tools.Editor"", ""references"": [] @@ -1392,11 +4020,12 @@ public void MigrateAsmdefSource_WhenCurrentDomainMetadataRequiresDomainReference hasLegacyCSharpSource: false, requiresToolContractsReference: false, requiresApplicationReference: false, - requiresDomainReference: true); + requiresDomainReference: true, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(result.Content, Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(result.Content, Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); Assert.That(result.Content, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } @@ -1419,7 +4048,8 @@ public void MigrateAsmdefSource_WhenCurrentReferencesUseAssemblyNames_DoesNotAdd hasLegacyCSharpSource: false, requiresToolContractsReference: true, requiresApplicationReference: true, - requiresDomainReference: true); + requiresDomainReference: true, + requiresFirstPartyScreenshotReference: false); Assert.That(result.Changed, Is.False); Assert.That(result.Content, Is.EqualTo(source)); diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index 99acbedcb0..41e22cbf79 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -50,16 +50,33 @@ public void GetMigrationStatusText_WhenTargetsExist_ReturnsFileCount( } [Test] - public void GetMigrationProgressText_WhenProgressExists_ReturnsCheckCount() + public void GetMigrationProgressText_WhenPreviewProgressExists_ReturnsCheckCount() { // Verifies that the migration wizard reports scan progress while migration targets are unknown. ThirdPartyToolMigrationProgress progress = new(3, 12); - string text = ThirdPartyToolMigrationWizardWindow.GetMigrationProgressText(progress); + string text = ThirdPartyToolMigrationWizardWindow.GetMigrationProgressText( + progress, + isMigrating: false); + + Assert.That( + text, + Is.EqualTo("Scanning project for V3 custom tool migration...\n3/12 steps complete.")); + } + + [Test] + public void GetMigrationProgressText_WhenApplyProgressExists_ReturnsMigrationCount() + { + // Verifies that the migration wizard distinguishes apply progress from preview scans. + ThirdPartyToolMigrationProgress progress = new(4, 12); + + string text = ThirdPartyToolMigrationWizardWindow.GetMigrationProgressText( + progress, + isMigrating: true); Assert.That( text, - Is.EqualTo("Scanning project for V3 custom tool migration...\n3/12 checks complete.")); + Is.EqualTo("Migrating project files for V3 custom tools...\n4/12 steps complete.")); } [TestCase(false, false, false, "Check required")] @@ -113,17 +130,66 @@ public void ShouldReportMigrationProgress_ReturnsExpectedValue( [TestCase(false, false, false)] public void ShouldApplyMigrationProgress_ReturnsExpectedValue( bool isCancellationRequested, - bool hasActivePreview, + bool hasActiveOperation, bool expected) { // Verifies that stale migration progress cannot overwrite a rendered preview result. bool shouldApply = ThirdPartyToolMigrationWizardWindow.ShouldApplyMigrationProgress( isCancellationRequested, - hasActivePreview); + hasActiveOperation); Assert.That(shouldApply, Is.EqualTo(expected)); } + [TestCase(1)] + [TestCase(0)] + public void ShouldRefreshAfterMigration_WhenMigrationCompletes_ReturnsFalse(int migratedFileCount) + { + // Verifies that a completed migration does not immediately start another scan. + ThirdPartyToolMigrationResult result = + new(migratedFileCount, migratedFileCount, System.Array.Empty()); + + bool shouldRefresh = ThirdPartyToolMigrationWizardWindow.ShouldRefreshAfterMigration(result); + + Assert.That(shouldRefresh, Is.False); + } + + [TestCase(false, 0, true)] + [TestCase(true, 0, false)] + [TestCase(false, 1, true)] + [TestCase(true, 1, true)] + public void ShouldFinishMigrationOnMainThread_ReturnsExpectedValue( + bool isCancellationRequested, + int migratedFileCount, + bool expected) + { + // Verifies that completed file writes still reach the main-thread asset refresh even after late cancellation. + ThirdPartyToolMigrationResult result = + new(migratedFileCount, migratedFileCount, System.Array.Empty()); + + bool shouldFinish = ThirdPartyToolMigrationWizardWindow.ShouldFinishMigrationOnMainThread( + isCancellationRequested, + result); + + Assert.That(shouldFinish, Is.EqualTo(expected)); + } + + [TestCase(true, false, true)] + [TestCase(true, true, false)] + [TestCase(false, false, false)] + public void ShouldRefreshAfterInterruptedMigration_ReturnsExpectedValue( + bool isMigrationCompletionPending, + bool isCancellationRequested, + bool expected) + { + // Verifies that failed async migrations restore the wizard while user cancellations stay quiet. + bool shouldRefresh = ThirdPartyToolMigrationWizardWindow.ShouldRefreshAfterInterruptedMigration( + isMigrationCompletionPending, + isCancellationRequested); + + Assert.That(shouldRefresh, Is.EqualTo(expected)); + } + [Test] public void PrepareForOpen_PopulatesWindowStateBeforeShowing() { diff --git a/Assets/Tests/Editor/UIElementAnnotatorTests.cs b/Assets/Tests/Editor/UIElementAnnotatorTests.cs index 471c9d7353..91acc9b760 100644 --- a/Assets/Tests/Editor/UIElementAnnotatorTests.cs +++ b/Assets/Tests/Editor/UIElementAnnotatorTests.cs @@ -4,6 +4,7 @@ using UnityEngine.UI; using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { diff --git a/Assets/Tests/Editor/UnityCliLoopToolRegistryTests.cs b/Assets/Tests/Editor/UnityCliLoopToolRegistryTests.cs index 78c82559b0..88733fb1e2 100644 --- a/Assets/Tests/Editor/UnityCliLoopToolRegistryTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopToolRegistryTests.cs @@ -9,6 +9,7 @@ using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.Infrastructure; using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.Tests.Editor { @@ -449,7 +450,7 @@ public async Task ExecuteToolAsync_WhenToolReturnsResponse_DoesNotAddProtocolVer public void StaticRegistrarCustomToolMethods_RegisterAndUnregisterManualTool() { // Tests that extension-facing static registrar APIs still delegate to the shared registry. - UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); UnityCliLoopToolRegistrarService service = new( new EmptyInternalToolNameProvider(), @@ -457,24 +458,24 @@ public void StaticRegistrarCustomToolMethods_RegisterAndUnregisterManualTool() new UnityCliLoopToolExecutionService()); ManualRegistrationTool tool = new(); - UnityCliLoopToolRegistrar.RegisterService(service); + ApplicationRegistrar.RegisterService(service); try { - UnityCliLoopToolRegistrar.RegisterCustomTool(tool); + ApplicationRegistrar.RegisterCustomTool(tool); - string[] toolNames = UnityCliLoopToolRegistrar.GetRegisteredCustomTools() + string[] toolNames = ApplicationRegistrar.GetRegisteredCustomTools() .Select(toolInfo => toolInfo.Name) .ToArray(); - Assert.That(UnityCliLoopToolRegistrar.IsCustomToolRegistered(tool.ToolName), Is.True); + Assert.That(ApplicationRegistrar.IsCustomToolRegistered(tool.ToolName), Is.True); Assert.That(toolNames, Does.Contain(tool.ToolName)); - UnityCliLoopToolRegistrar.UnregisterCustomTool(tool.ToolName); + ApplicationRegistrar.UnregisterCustomTool(tool.ToolName); - Assert.That(UnityCliLoopToolRegistrar.IsCustomToolRegistered(tool.ToolName), Is.False); + Assert.That(ApplicationRegistrar.IsCustomToolRegistered(tool.ToolName), Is.False); } finally { - UnityCliLoopToolRegistrar.RegisterService(previousService); + ApplicationRegistrar.RegisterService(previousService); } } diff --git a/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs b/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs index 42d53b794c..e15fbfd741 100644 --- a/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs +++ b/Packages/src/Editor/Application/UnityCliLoopServerStartupService.cs @@ -1,4 +1,5 @@ using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Application { diff --git a/Packages/src/Editor/Application/UnityCliLoopToolRegistrar.cs b/Packages/src/Editor/Application/UnityCliLoopToolRegistrar.cs index 895966b08e..26bece1ecd 100644 --- a/Packages/src/Editor/Application/UnityCliLoopToolRegistrar.cs +++ b/Packages/src/Editor/Application/UnityCliLoopToolRegistrar.cs @@ -11,7 +11,7 @@ namespace io.github.hatayama.UnityCliLoop.Application /// /// Manages the shared editor tool registry and custom tool registrations. /// - public sealed class UnityCliLoopToolRegistrarService + public sealed class UnityCliLoopToolRegistrarService : IUnityCliLoopToolRegistrar { private readonly IInternalToolNameProvider _internalToolNameProvider; private readonly UnityCliLoopToolExecutionService _toolExecutionService; diff --git a/Packages/src/Editor/Application/UseCases/ThirdPartyToolMigrationUseCase.cs b/Packages/src/Editor/Application/UseCases/ThirdPartyToolMigrationUseCase.cs index 3fcb896add..359be2fab4 100644 --- a/Packages/src/Editor/Application/UseCases/ThirdPartyToolMigrationUseCase.cs +++ b/Packages/src/Editor/Application/UseCases/ThirdPartyToolMigrationUseCase.cs @@ -52,5 +52,16 @@ public ThirdPartyToolMigrationResult ApplyMigration(string projectRoot) return _migrationPort.ApplyMigration(projectRoot); } + + public Task ApplyMigrationAsync( + string projectRoot, + IProgress progress, + CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(progress != null, "progress must not be null"); + + return _migrationPort.ApplyMigrationAsync(projectRoot, progress, ct); + } } } diff --git a/Packages/src/Editor/Application/UseCases/UnityCliLoopServerShutdownUseCase.cs b/Packages/src/Editor/Application/UseCases/UnityCliLoopServerShutdownUseCase.cs index 7ee920c005..5cd72cbb7c 100644 --- a/Packages/src/Editor/Application/UseCases/UnityCliLoopServerShutdownUseCase.cs +++ b/Packages/src/Editor/Application/UseCases/UnityCliLoopServerShutdownUseCase.cs @@ -2,6 +2,7 @@ using System.Threading; using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Application { diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs index dac57ea5a5..96c0f2d04d 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs @@ -2,6 +2,8 @@ using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.Infrastructure; using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; +using ToolContractsRegistrar = io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.CompositionRoot { @@ -12,6 +14,7 @@ internal sealed class UnityCliLoopApplicationRegistration { internal UnityCliLoopApplicationServices Register() { + VibeLogger.InitializeForEditorStartup(); ToolSettingsRepository toolSettingsRepository = new(); ToolSettingsService toolSettingsService = new(toolSettingsRepository); UnityCliLoopEditorSettingsRepository editorSettingsRepository = new(); @@ -26,7 +29,8 @@ internal UnityCliLoopApplicationServices Register() new SkillInstallLayoutInternalToolNameProvider(), toolSettingsService, new UnityCliLoopToolExecutionService()); - UnityCliLoopToolRegistrar.RegisterService(toolRegistrarService); + ApplicationRegistrar.RegisterService(toolRegistrarService); + ToolContractsRegistrar.RegisterService(toolRegistrarService); ToolSettingsUseCaseRegistry.Register(new ToolSettingsUseCase( toolSettingsService, toolRegistrarService)); diff --git a/Packages/src/Editor/Domain/ServiceResult.cs b/Packages/src/Editor/Domain/ServiceResult.cs deleted file mode 100644 index e1e34dfdff..0000000000 --- a/Packages/src/Editor/Domain/ServiceResult.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace io.github.hatayama.UnityCliLoop.Domain -{ - /// - /// Domain value for platform operation results. - /// - /// Type of result data - public class ServiceResult - { - /// - /// Whether execution was successful - /// - public bool Success { get; } - - /// - /// Data of execution result - /// - public T Data { get; } - - /// - /// Error message when execution fails - /// - public string ErrorMessage { get; } - - /// - /// Create ServiceResult - /// - /// Execution result - /// Result data - /// Error message (null on success) - public ServiceResult(bool success, T data = default, string errorMessage = null) - { - Success = success; - Data = data; - ErrorMessage = errorMessage; - } - - /// - /// Create success result - /// - /// Result data - /// ServiceResult representing success - public static ServiceResult SuccessResult(T data) => new(true, data); - - /// - /// Create failure result - /// - /// Error message - /// ServiceResult representing failure - public static ServiceResult FailureResult(string errorMessage) => new(false, default, errorMessage); - } -} diff --git a/Packages/src/Editor/Domain/ThirdPartyToolMigrationData.cs b/Packages/src/Editor/Domain/ThirdPartyToolMigrationData.cs index f18fc1de90..6b0ec7f355 100644 --- a/Packages/src/Editor/Domain/ThirdPartyToolMigrationData.cs +++ b/Packages/src/Editor/Domain/ThirdPartyToolMigrationData.cs @@ -30,7 +30,7 @@ public ThirdPartyToolMigrationPreview(int fileCount, int replacementCount, strin } /// - /// Reports preview scan progress so editor UI can repaint while project files are inspected. + /// Reports migration progress so editor UI can repaint while project files are inspected. /// public readonly struct ThirdPartyToolMigrationProgress { @@ -83,6 +83,10 @@ Task PreviewMigrationAsync( CancellationToken ct); Task HasMigrationTargetsAsync(string projectRoot, CancellationToken ct); ThirdPartyToolMigrationResult ApplyMigration(string projectRoot); + Task ApplyMigrationAsync( + string projectRoot, + IProgress progress, + CancellationToken ct); } /// diff --git a/Packages/src/Editor/Domain/UnityCliLoopToolRegistry.cs b/Packages/src/Editor/Domain/UnityCliLoopToolRegistry.cs index ad40de93af..c43ed5b986 100644 --- a/Packages/src/Editor/Domain/UnityCliLoopToolRegistry.cs +++ b/Packages/src/Editor/Domain/UnityCliLoopToolRegistry.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using Newtonsoft.Json; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -183,24 +182,4 @@ public bool IsToolRegistered(string toolName) } } - - /// - /// Catalog DTO returned by the CLI tool-details command. - /// - public class ToolInfo - { - [JsonProperty("name")] public string Name { get; } - - [JsonProperty("parameterSchema")] public ToolParameterSchema ParameterSchema { get; } - - [JsonProperty("displayDevelopmentOnly")] public bool DisplayDevelopmentOnly { get; } - - public ToolInfo(string name, ToolParameterSchema parameterSchema, bool displayDevelopmentOnly = false) - { - Name = name; - ParameterSchema = parameterSchema; - DisplayDevelopmentOnly = displayDevelopmentOnly; - } - } - } diff --git a/Packages/src/Editor/FirstPartyTools/Common/EditorUtility/DomainReloadDisableScopeRecovery.cs b/Packages/src/Editor/FirstPartyTools/Common/EditorUtility/DomainReloadDisableScopeRecovery.cs index e5aa325a1b..bb1d5936a3 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/EditorUtility/DomainReloadDisableScopeRecovery.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/EditorUtility/DomainReloadDisableScopeRecovery.cs @@ -21,8 +21,7 @@ internal static class DomainReloadDisableScopeRecovery internal static string MarkerFilePathForTests => DomainReloadDisableScopeRecoveryConstants.MarkerFilePath; internal static string TempFilePathForTests => DomainReloadDisableScopeRecoveryConstants.TempFilePath; - [InitializeOnLoadMethod] - private static void RestorePendingSettingsOnEditorLoad() + internal static void RestoreForEditorStartup() { if (AssetDatabase.IsAssetImportWorkerProcess()) { diff --git a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs index 97bdc4df15..0420ae88cd 100644 --- a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs @@ -8,10 +8,12 @@ public static class FirstPartyToolsEditorStartup { public static void Initialize() { + DomainReloadDisableScopeRecovery.RestoreForEditorStartup(); ExternalSceneChangeTracker.Initialize(); ControlPlayModeEditorStartup.Initialize(); ExecuteDynamicCodeEditorStartup.Initialize(); GetLogsEditorStartup.Initialize(); + ScreenshotEditorStartup.Initialize(); #if ULOOP_HAS_TEST_FRAMEWORK RunTestsTestFrameworkStartup.Initialize(); #endif diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs new file mode 100644 index 0000000000..180fb42002 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs @@ -0,0 +1,62 @@ +#nullable enable +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.ToolContracts; +using ToolContractsCaptureUtility = io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Bridges the public capture contract to the bundled screenshot implementation. + /// + internal sealed class EditorWindowCaptureService : IEditorWindowCaptureService + { + public EditorWindow[] FindWindowsByName(string windowName, WindowMatchMode matchMode) + { + return EditorWindowCaptureUtility.FindWindowsByName(windowName, matchMode); + } + + public Task<(Texture2D? texture, bool timedOut)> CaptureWindowAsync( + EditorWindow window, + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct) + { + return EditorWindowCaptureUtility.CaptureWindowAsync( + window, + resolutionScale, + timeoutMilliseconds, + ct); + } + + public string[] GetOpenWindowNames() + { + return EditorWindowCaptureUtility.GetOpenWindowNames(); + } + + public Task<(Texture2D? texture, int yOffset, bool timedOut)> CaptureGameRenderingAsync( + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct) + { + return EditorWindowCaptureUtility.CaptureGameRenderingAsync( + resolutionScale, + timeoutMilliseconds, + ct); + } + } + + /// + /// Registers screenshot services for public contract facades. + /// + internal static class ScreenshotEditorStartup + { + internal static void Initialize() + { + ToolContractsCaptureUtility.RegisterService(new EditorWindowCaptureService()); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs.meta b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs.meta new file mode 100644 index 0000000000..872e16f606 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 876def00bafba4c07b15aaaa4ee0131e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs index c53f1d2b57..74a3c44fcb 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementAnnotator.cs @@ -3,6 +3,8 @@ using UnityEngine.EventSystems; using UnityEngine.UI; +using io.github.hatayama.UnityCliLoop.ToolContracts; + namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { // Creates a temporary Screen Space Overlay Canvas that draws bounding boxes and labels diff --git a/Packages/src/Editor/FirstPartyTools/UnityCLILoop.FirstPartyTools.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/UnityCLILoop.FirstPartyTools.Editor.asmdef index 5b96c037c7..ba50f844c3 100644 --- a/Packages/src/Editor/FirstPartyTools/UnityCLILoop.FirstPartyTools.Editor.asmdef +++ b/Packages/src/Editor/FirstPartyTools/UnityCLILoop.FirstPartyTools.Editor.asmdef @@ -15,6 +15,7 @@ "GUID:9ed21973545c44e28af522b049dec47d", "GUID:a338f3f8ae84147109d351d16d557552", "GUID:a0bdbd2c5705643fbb9aef9fac8fd46a", + "GUID:d427b32aad9cb44fc8e962437c9dbcd8", "GUID:679888bc3e9c44c19ab0d62ff8701dcf", "GUID:9c34b9a25e1464dec91601291eade23f", "GUID:32708dd5a1f18474e8a4deddc56e8806" diff --git a/Packages/src/Editor/Infrastructure/Api/GetToolDetailsBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/GetToolDetailsBridgeCommand.cs index caef3017f1..51f919e6a8 100644 --- a/Packages/src/Editor/Infrastructure/Api/GetToolDetailsBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/GetToolDetailsBridgeCommand.cs @@ -3,6 +3,8 @@ using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.Infrastructure { @@ -18,7 +20,7 @@ public static GetToolDetailsResponse Execute(JToken paramsToken) { bool includeDevelopmentOnly = ReadIncludeDevelopmentOnly(paramsToken); - UnityCliLoopToolRegistry registry = UnityCliLoopToolRegistrar.GetRegistry(); + UnityCliLoopToolRegistry registry = ApplicationRegistrar.GetRegistry(); ToolInfo[] allTools = registry.GetRegisteredTools(); ToolInfo[] filteredTools = allTools; diff --git a/Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs b/Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs index 13e1923288..1812244588 100644 --- a/Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs +++ b/Packages/src/Editor/Infrastructure/Api/UnityApiHandler.cs @@ -5,6 +5,7 @@ using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.Infrastructure { @@ -36,7 +37,7 @@ public static class UnityApiHandler /// Get command registry /// Use this registry when adding new commands /// - public static UnityCliLoopToolRegistry CommandRegistry => UnityCliLoopToolRegistrar.GetRegistry(); + public static UnityCliLoopToolRegistry CommandRegistry => ApplicationRegistrar.GetRegistry(); /// /// Generic command execution method @@ -59,7 +60,7 @@ public static async Task ExecuteCommandAsync( return response; } - response = await UnityCliLoopToolRegistrar.ExecuteToolAsync( + response = await ApplicationRegistrar.ExecuteToolAsync( commandName, paramsToken, ct); diff --git a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs index e731173252..f584b4b941 100644 --- a/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs +++ b/Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs @@ -8,6 +8,7 @@ using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; using io.github.hatayama.UnityCliLoop.ToolContracts; +using ApplicationRegistrar = io.github.hatayama.UnityCliLoop.Application.UnityCliLoopToolRegistrar; namespace io.github.hatayama.UnityCliLoop.Infrastructure { @@ -261,7 +262,7 @@ internal async Task StartServerWithUseCaseAsync() // for compatibility with existing code _bridgeServer = result.ServerInstance; - UnityCliLoopToolRegistrar.WarmupRegistry(); + ApplicationRegistrar.WarmupRegistry(); await MarkServerReadyAsync("manual-start", cancellationToken); } @@ -646,7 +647,7 @@ public async Task StartRecoveryIfNeededAsync(bool isAfterCompile, CancellationTo // Clear reconnection-related flags on successful recovery _sessionStateService.ClearReconnectingFlags(); _sessionStateService.ClearPostCompileReconnectingUI(); - UnityCliLoopToolRegistrar.WarmupRegistry(); + ApplicationRegistrar.WarmupRegistry(); await MarkServerReadyAsync("server-recovery", cancellationToken); ActivateStartupProtection(5000); diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs new file mode 100644 index 0000000000..d311a088cb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationAliasRules + { + internal static string[] GetLegacyNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, LegacyNamespaceAliasRegex, "alias"); + } + + internal static string[] GetLegacyToolInfoTypeAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, LegacyToolInfoTypeAliasRegex, "alias"); + } + + internal static string[] GetCurrentFirstPartyToolsNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, CurrentFirstPartyToolsNamespaceAliasRegex, "alias"); + } + + internal static string[] GetCurrentApplicationNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, CurrentApplicationNamespaceAliasRegex, "alias"); + } + + internal static string[] GetCombinedLegacyNamespaceAliases( + string source, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + return GetLegacyNamespaceAliases(source) + .Concat(legacyAssemblyAliases) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + internal static string[] GetCombinedLegacyToolInfoTypeAliases( + string source, + string[] legacyAssemblyToolInfoAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); + + return GetLegacyToolInfoTypeAliases(source) + .Concat(legacyAssemblyToolInfoAliases) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + internal static string[] GetCombinedCurrentApplicationNamespaceAliases( + string source, + string[] currentApplicationAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentApplicationAssemblyAliases != null, + "currentApplicationAssemblyAliases must not be null"); + + return GetCurrentApplicationNamespaceAliases(source) + .Concat(currentApplicationAssemblyAliases) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + internal static string[] GetCombinedCurrentFirstPartyToolsNamespaceAliases( + string source, + string[] currentFirstPartyToolsAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "currentFirstPartyToolsAssemblyAliases must not be null"); + + return GetCurrentFirstPartyToolsNamespaceAliases(source) + .Concat(currentFirstPartyToolsAssemblyAliases) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + + internal static string[] GetRegexGroupValuesInCode(string source, Regex regex, string groupName) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(regex != null, "regex must not be null"); + Debug.Assert(!string.IsNullOrEmpty(groupName), "groupName must not be null or empty"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + List values = new(); + MatchCollection matches = regex.Matches(source); + foreach (Match match in matches) + { + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + values.Add(match.Groups[groupName].Value); + } + + return values.ToArray(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs.meta new file mode 100644 index 0000000000..d93b28273b --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c1890251f62174a7a8a02d862143dcf5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs new file mode 100644 index 0000000000..629b8388e6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs @@ -0,0 +1,396 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationApiDetectionRules + { + internal static bool ContainsLegacyRegistrarApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ContainsLegacyRegistrarApiForAssembly( + source, + hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), + legacyAssemblyAliases: Array.Empty()); + } + + internal static bool ContainsLegacyRegistrarApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + bool canMigrateBareLegacyRegistrar = + hasLegacyAssemblySource || + ContainsLegacyToolMigrationMarker(source) || + legacyNamespaceAliases.Length > 0; + return ContainsLegacyRegistrarReference( + source, + canMigrateBareLegacyRegistrar, + legacyNamespaceAliases); + } + + internal static bool ContainsCurrentRegistrarApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentRegistrarRegex); + } + + internal static bool ContainsLegacyApplicationApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert( + currentApplicationAssemblyAliases != null, + "currentApplicationAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + string[] currentApplicationNamespaceAliases = GetCombinedCurrentApplicationNamespaceAliases( + source, + currentApplicationAssemblyAliases); + return ContainsLegacyApplicationReference( + source, + hasLegacyAssemblySource, + legacyNamespaceAliases, + currentApplicationNamespaceAliases, + assemblyDeclaredTypeNames); + } + + internal static bool ContainsCurrentApplicationApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); + return ContainsCurrentApplicationApiForAssembly( + source, + hasCurrentApplicationNamespaceUsage, + Array.Empty(), + GetDeclaredTypeNames(source)); + } + + internal static bool ContainsCurrentApplicationApiForAssembly( + string source, + bool hasAssemblyScopedCurrentApplicationUsing, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentApplicationAssemblyAliases != null, + "currentApplicationAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); + string[] currentApplicationNamespaceAliases = GetCombinedCurrentApplicationNamespaceAliases( + source, + currentApplicationAssemblyAliases); + return ContainsCurrentApplicationReference( + source, + hasAssemblyScopedCurrentApplicationUsing || hasCurrentApplicationNamespaceUsage, + currentApplicationNamespaceAliases, + assemblyDeclaredTypeNames); + } + + internal static bool ContainsRegistrarDomainReturnApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ContainsRegistrarDomainReturnApiForAssembly( + source, + hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), + legacyAssemblyAliases: Array.Empty()); + } + + internal static bool ContainsRegistrarDomainReturnApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + if (RegexMatchesCode(source, CurrentRegistrarDomainReturnRegex)) + { + return true; + } + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + bool canMigrateBareLegacyRegistrar = + hasLegacyAssemblySource || + ContainsLegacyToolMigrationMarker(source) || + legacyNamespaceAliases.Length > 0; + return ContainsLegacyRegistrarDomainReturnReference( + source, + canMigrateBareLegacyRegistrar, + legacyNamespaceAliases); + } + + internal static bool ContainsCurrentToolContractsApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); + } + + internal static bool ContainsLegacyDomainMetadataApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, LegacyDomainMetadataRegex) || + ContainsLegacyDomainHelperApiForAssembly( + source, + hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), + legacyAssemblyAliases: Array.Empty()); + } + + internal static bool ContainsLegacyDomainHelperApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + return ContainsLegacyDomainHelperReference( + source, + hasLegacyAssemblySource, + legacyNamespaceAliases); + } + + internal static bool ContainsCurrentDomainMetadataApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ContainsCurrentDomainMetadataApiForAssembly( + source, + hasAssemblyScopedCurrentDomainUsing: false); + } + + internal static bool ContainsCurrentDomainMetadataApiForAssembly( + string source, + bool hasAssemblyScopedCurrentDomainUsing) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); + bool canUseBareCurrentDomainType = hasAssemblyScopedCurrentDomainUsing || hasCurrentDomainNamespaceUsage; + return RegexMatchesCode(source, CurrentDomainMetadataRegex) || + ContainsCurrentDomainHelperApiForAssembly(source, canUseBareCurrentDomainType) || + (canUseBareCurrentDomainType && RegexMatchesCode(source, LegacyDomainMetadataRegex)); + } + + internal static bool ContainsLegacyFirstPartyScreenshotApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + return ContainsLegacyFirstPartyScreenshotReference( + source, + hasLegacyAssemblySource, + legacyNamespaceAliases, + assemblyDeclaredTypeNames); + } + + internal static bool ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "currentFirstPartyToolsAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + string[] currentFirstPartyToolsNamespaceAliases = GetCombinedCurrentFirstPartyToolsNamespaceAliases( + source, + currentFirstPartyToolsAssemblyAliases); + bool canMigrateBareLegacyEditorWindowCaptureUtility = + CanMigrateBareLegacyEditorWindowCaptureUtilityForAssembly( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyNamespaceAliases); + return ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + assemblyDeclaredTypeNames, + requiresTimeoutArgumentMigration: false); + } + + internal static bool ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "currentFirstPartyToolsAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + string[] currentFirstPartyToolsNamespaceAliases = GetCombinedCurrentFirstPartyToolsNamespaceAliases( + source, + currentFirstPartyToolsAssemblyAliases); + bool canMigrateBareLegacyEditorWindowCaptureUtility = + CanMigrateBareLegacyEditorWindowCaptureUtilityForAssembly( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyNamespaceAliases); + return ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + assemblyDeclaredTypeNames, + requiresTimeoutArgumentMigration: true); + } + + internal static bool ContainsCurrentFirstPartyScreenshotApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + return ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + hasCurrentFirstPartyToolsNamespaceUsage, + Array.Empty(), + Array.Empty()); + } + + internal static bool ContainsCurrentFirstPartyScreenshotApiForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "currentFirstPartyToolsAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + string[] currentFirstPartyToolsNamespaceAliases = GetCombinedCurrentFirstPartyToolsNamespaceAliases( + source, + currentFirstPartyToolsAssemblyAliases); + bool canUseBareCurrentFirstPartyScreenshotType = + hasAssemblyScopedCurrentFirstPartyToolsUsing || + hasCurrentFirstPartyToolsNamespaceUsage; + return ContainsCurrentFirstPartyScreenshotReference( + source, + canUseBareCurrentFirstPartyScreenshotType, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames); + } + + internal static bool ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "currentFirstPartyToolsAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + string[] currentFirstPartyToolsNamespaceAliases = GetCombinedCurrentFirstPartyToolsNamespaceAliases( + source, + currentFirstPartyToolsAssemblyAliases); + return ContainsCurrentCaptureGameRenderingDeconstructionMigration( + source, + hasAssemblyScopedCurrentFirstPartyToolsUsing || hasCurrentFirstPartyToolsNamespaceUsage, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs.meta new file mode 100644 index 0000000000..24658abdef --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd6b52838ee9a41caad929a5be8d8d0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs new file mode 100644 index 0000000000..245c38a782 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationApplicationDetectionRules + { + internal static bool ContainsLegacyTimerDelayInvocation( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyTimerDelay) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyTimerDelayInvocationRegex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyTimerDelayInvocationMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyTimerDelay)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsLegacyApplicationReference( + string source, + bool canMigrateBareLegacyApplicationApi, + string[] aliases, + string[] currentApplicationNamespaceAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + Debug.Assert( + currentApplicationNamespaceAliases != null, + "currentApplicationNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(source, rule.LegacyName) || + assemblyDeclaredTypeNames.Contains(rule.LegacyName); + + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + foreach (string alias in aliases) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) + { + return true; + } + } + + if (canMigrateBareLegacyApplicationApi && + !hasProtectedTypeDeclaration && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + if (ContainsLegacyMainThreadSwitcherSwitchCall( + source, + aliases, + currentApplicationNamespaceAliases, + canMigrateBareLegacyApplicationApi, + assemblyDeclaredTypeNames)) + { + return true; + } + + return false; + } + + internal static bool ContainsLegacyMainThreadSwitcherSwitchCall( + string source, + string[] legacyNamespaceAliases, + string[] currentApplicationNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentApplicationNamespaceAliases != null, + "currentApplicationNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyMainThreadSwitcherSwitchRegex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyMainThreadSwitcherCallMatch( + source, + match, + legacyNamespaceAliases, + currentApplicationNamespaceAliases, + canMigrateBareLegacyMainThreadSwitcher, + assemblyDeclaredTypeNames)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsCurrentApplicationReference( + string source, + bool canUseBareCurrentApplicationType, + string[] currentApplicationNamespaceAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentApplicationNamespaceAliases != null, + "currentApplicationNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(source, rule.CurrentName) || + assemblyDeclaredTypeNames.Contains(rule.CurrentName); + + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + foreach (string alias in currentApplicationNamespaceAliases) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.CurrentName)) + { + return true; + } + } + + if (canUseBareCurrentApplicationType && + !hasProtectedTypeDeclaration && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) + { + return true; + } + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs.meta new file mode 100644 index 0000000000..18a7c7f62a --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b344e5d7979e44038a3f72d802333f38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs new file mode 100644 index 0000000000..26d4de24c2 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationArgumentRules + { + 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++) + { + char current = argumentsSource[i]; + if (isInRegularString) + { + if (current == '\\') + { + i++; + continue; + } + + if (current == '"') + { + isInRegularString = false; + } + + continue; + } + + if (isInVerbatimString) + { + if (current != '"') + { + continue; + } + + if (i + 1 < argumentsSource.Length && argumentsSource[i + 1] == '"') + { + i++; + continue; + } + + isInVerbatimString = false; + continue; + } + + if (isInRawString) + { + if (HasRepeatedCharacterAt(argumentsSource, i, '"', rawStringQuoteCount)) + { + i += rawStringQuoteCount - 1; + isInRawString = false; + } + + continue; + } + + if (isInCharLiteral) + { + if (current == '\\') + { + i++; + continue; + } + + if (current == '\'') + { + isInCharLiteral = false; + } + + continue; + } + + if (IsRawStringStart(argumentsSource, i)) + { + int dollarCount = CountRepeatedCharacter(argumentsSource, i, '$'); + int quoteIndex = i + dollarCount; + rawStringQuoteCount = CountRepeatedCharacter(argumentsSource, quoteIndex, '"'); + isInRawString = true; + i = quoteIndex + rawStringQuoteCount - 1; + continue; + } + + if (StartsWith(argumentsSource, i, "@\"") || + StartsWith(argumentsSource, i, "$@\"") || + StartsWith(argumentsSource, i, "@$\"")) + { + isInVerbatimString = true; + i += GetStringPrefixLength(argumentsSource, i); + continue; + } + + if (StartsWith(argumentsSource, i, "$\"")) + { + int interpolatedStringEndIndex = + ThirdPartyToolMigrationInterpolatedStringRules.FindRegularInterpolatedStringEndIndex( + argumentsSource, + i); + if (interpolatedStringEndIndex >= 0) + { + i = interpolatedStringEndIndex; + continue; + } + + isInRegularString = true; + i++; + continue; + } + + if (current == '"') + { + isInRegularString = true; + continue; + } + + if (current == '\'') + { + isInCharLiteral = true; + continue; + } + + if (current == '(' || current == '[' || current == '{') + { + nestingDepth++; + continue; + } + + if (current == ')' || current == ']' || current == '}') + { + nestingDepth = Math.Max(0, nestingDepth - 1); + continue; + } + + if (current != ',' || nestingDepth != 0) + { + continue; + } + + arguments.Add(argumentsSource.Substring(argumentStartIndex, i - argumentStartIndex)); + argumentStartIndex = i + 1; + } + + arguments.Add(argumentsSource.Substring(argumentStartIndex)); + return arguments.ToArray(); + } + + internal static string GetNamedArgumentValueOrNull(string argument, string argumentName) + { + Debug.Assert(argument != null, "argument must not be null"); + Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); + + int colonIndex = FindNamedArgumentColonIndex(argument); + if (colonIndex <= 0) + { + return null; + } + + string possibleArgumentName = argument.Substring(0, colonIndex).Trim(); + if (!string.Equals(possibleArgumentName, argumentName, StringComparison.Ordinal)) + { + return null; + } + + string value = argument.Substring(colonIndex + 1).Trim(); + return value.Length == 0 ? null : value; + } + + internal static int FindNamedArgumentColonIndex(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + for (int index = 0; index < argument.Length; index++) + { + if (argument[index] != ':') + { + continue; + } + + bool isAliasQualifierColon = + (index + 1 < argument.Length && argument[index + 1] == ':') || + (index > 0 && argument[index - 1] == ':'); + if (isAliasQualifierColon) + { + continue; + } + + return index; + } + + return -1; + } + + internal static int FindInvocationClosingParenthesisIndex( + string source, + CodeTextMask codeTextMask, + int openParenthesisIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openParenthesisIndex >= 0, "openParenthesisIndex must not be negative"); + + int nestedParenthesisDepth = 0; + for (int i = openParenthesisIndex + 1; i < source.Length; i++) + { + if (!codeTextMask.IsCodeAt(i)) + { + continue; + } + + if (source[i] == '(') + { + nestedParenthesisDepth++; + continue; + } + + if (source[i] != ')') + { + continue; + } + + if (nestedParenthesisDepth == 0) + { + return i; + } + + nestedParenthesisDepth--; + } + + return -1; + } + + internal static bool IsNamedAttributeArgument(string argument, string argumentName) + { + Debug.Assert(!string.IsNullOrWhiteSpace(argument), "argument must not be null or whitespace"); + Debug.Assert(!string.IsNullOrWhiteSpace(argumentName), "argumentName must not be null or whitespace"); + + if (!argument.StartsWith(argumentName, StringComparison.Ordinal)) + { + return false; + } + + for (int i = argumentName.Length; i < argument.Length; i++) + { + char current = argument[i]; + if (char.IsWhiteSpace(current)) + { + continue; + } + + return current == '='; + } + + return false; + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs.meta new file mode 100644 index 0000000000..0aee87b2fe --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0fd95b988784d43969bab730d2c5465c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs new file mode 100644 index 0000000000..07e6f5f6a6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Reads Unity asmdef GUID references from generated .meta files for asmref resolution. + /// + internal static class ThirdPartyToolMigrationAsmdefMetaGuidReader + { + internal static string ReadAsmdefGuidReferenceFromAsmdefPath(string asmdefFilePath) + { + Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); + + string metaPath = asmdefFilePath + ".meta"; + if (!File.Exists(metaPath)) + { + return string.Empty; + } + + return ReadAsmdefGuidReferenceFromMetaFile(metaPath, File.ReadLines); + } + + internal static string ReadAsmdefGuidReferenceFromMetaFile( + string metaPath, + Func> readLines) + { + Debug.Assert(!string.IsNullOrEmpty(metaPath), "metaPath must not be null or empty"); + Debug.Assert(readLines != null, "readLines must not be null"); + + try + { + foreach (string line in readLines(metaPath)) + { + string trimmedLine = line.Trim(); + if (!trimmedLine.StartsWith("guid:", StringComparison.Ordinal)) + { + continue; + } + + string guid = trimmedLine.Substring("guid:".Length).Trim(); + return guid.Length == 0 ? string.Empty : $"GUID:{guid}"; + } + } + catch (Exception ex) when (IsSkippableAssemblyMetaReadException(ex)) + { + UnityEngine.Debug.LogWarning( + $"[UnityCliLoop] Skipping unreadable asmdef meta file at {metaPath}: {ex.Message}"); + return string.Empty; + } + + return string.Empty; + } + + private static bool IsSkippableAssemblyMetaReadException(Exception ex) + { + Debug.Assert(ex != null, "ex must not be null"); + + return ex is IOException || + ex is UnauthorizedAccessException; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs.meta new file mode 100644 index 0000000000..69ffa053a9 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88ba6533ca6cb47aeb7a31d936dc8174 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs new file mode 100644 index 0000000000..f9e7192014 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Determines whether an asmdef needs source or reference migration. + /// + internal static class ThirdPartyToolMigrationAsmdefMigrationRequirementResolver + { + internal static bool ContainsAsmdefMigrationTarget( + string asmdefFilePath, + string projectRoot, + MigrationAssemblyUsage assemblyUsage) + { + Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or 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 = File.ReadAllText(asmdefFilePath); + if (!hasAssemblyMigrationRequirement && + !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) + { + return false; + } + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + + return result.Changed; + } + + internal static bool TryGetAsmdefMigrationRequirements( + string asmdefFilePath, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + out bool hasLegacyCSharpSource, + out bool requiresToolContractsReference, + out bool requiresApplicationReference, + out bool requiresDomainReference, + out bool requiresFirstPartyScreenshotReference) + { + Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? projectRoot; + hasLegacyCSharpSource = assemblyUsage.LegacyAssemblyDirectories.Contains(asmdefDirectory); + requiresToolContractsReference = + assemblyUsage.ToolContractsReferenceAssemblyDirectories.Contains(asmdefDirectory); + requiresApplicationReference = + assemblyUsage.ApplicationReferenceAssemblyDirectories.Contains(asmdefDirectory); + requiresDomainReference = + assemblyUsage.DomainReferenceAssemblyDirectories.Contains(asmdefDirectory); + requiresFirstPartyScreenshotReference = + assemblyUsage.FirstPartyScreenshotReferenceAssemblyDirectories.Contains(asmdefDirectory); + return hasLegacyCSharpSource || + requiresToolContractsReference || + requiresApplicationReference || + requiresDomainReference || + requiresFirstPartyScreenshotReference; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs.meta new file mode 100644 index 0000000000..3759c9a529 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52c4b7d430eee4a5e87d22512d214314 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs new file mode 100644 index 0000000000..e7b6cf4527 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationAsmdefRules + { + internal static string[] GetMigratedAsmdefReferences( + string reference, + bool hasLegacyCSharpSource, + bool requiresToolContractsReference, + bool requiresApplicationReference, + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) + { + if (string.Equals(reference, LegacyEditorAssemblyName, StringComparison.Ordinal)) + { + return GetMigratedLegacyEditorReferences( + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + } + + if (string.Equals(reference, LegacyRuntimeAssemblyName, StringComparison.Ordinal)) + { + return new[] { CurrentRuntimeGuidReference }; + } + + if (hasLegacyCSharpSource && + string.Equals(reference, LegacyEditorAssemblyGuidReference, StringComparison.Ordinal)) + { + return GetMigratedLegacyEditorReferences( + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + } + + return new[] { reference }; + } + + internal static string[] GetMigratedLegacyEditorReferences( + bool requiresApplicationReference, + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) + { + List references = new() + { + CurrentToolContractsGuidReference + }; + + return references.ToArray(); + } + + internal static void AddRequiredCurrentAsmdefReferences( + JArray references, + HashSet addedReferences, + bool requiresToolContractsReference, + bool requiresApplicationReference, + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference, + ref int replacementCount) + { + Debug.Assert(references != null, "references must not be null"); + Debug.Assert(addedReferences != null, "addedReferences must not be null"); + + if (requiresToolContractsReference || + requiresApplicationReference || + requiresDomainReference || + requiresFirstPartyScreenshotReference) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentToolContractsGuidReference, + ref replacementCount); + } + } + + internal static void AddRequiredCurrentAsmdefReference( + JArray references, + HashSet addedReferences, + string reference, + ref int replacementCount) + { + Debug.Assert(references != null, "references must not be null"); + Debug.Assert(addedReferences != null, "addedReferences must not be null"); + Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); + + string referenceKey = GetCurrentAsmdefReferenceKey(reference); + if (!addedReferences.Add(referenceKey)) + { + return; + } + + references.Add(reference); + replacementCount++; + } + + internal static string GetCurrentAsmdefReferenceKey(string reference) + { + Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); + + if (IsCurrentAsmdefReference( + reference, + CurrentRuntimeAssemblyName, + CurrentRuntimeGuidReference)) + { + return CurrentRuntimeAssemblyName; + } + + if (IsCurrentAsmdefReference( + reference, + CurrentToolContractsAssemblyName, + CurrentToolContractsGuidReference)) + { + return CurrentToolContractsAssemblyName; + } + + if (IsCurrentAsmdefReference( + reference, + CurrentApplicationAssemblyName, + CurrentApplicationGuidReference)) + { + return CurrentApplicationAssemblyName; + } + + if (IsCurrentAsmdefReference( + reference, + CurrentDomainAssemblyName, + CurrentDomainGuidReference)) + { + return CurrentDomainAssemblyName; + } + + if (IsCurrentAsmdefReference( + reference, + CurrentFirstPartyToolsScreenshotAssemblyName, + CurrentFirstPartyToolsScreenshotGuidReference)) + { + return CurrentFirstPartyToolsScreenshotAssemblyName; + } + + return reference; + } + + internal static bool IsCurrentAsmdefReference( + string reference, + string assemblyName, + string guidReference) + { + Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(guidReference), "guidReference must not be null or empty"); + + return string.Equals(reference, assemblyName, StringComparison.Ordinal) || + string.Equals(reference, guidReference, StringComparison.Ordinal); + } + internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( + string source, + bool hasLegacyCSharpSource, + bool requiresToolContractsReference, + bool requiresApplicationReference, + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) + { + Debug.Assert(source != null, "source must not be null"); + + JObject asmdef = JObject.Parse(source); + JToken referencesToken = asmdef["references"]; + JArray references = referencesToken == null ? new JArray() : referencesToken as JArray; + if (referencesToken != null && references == null) + { + return new ThirdPartyToolMigrationContentResult( + source, + 0, + Array.Empty()); + } + + int replacementCount = 0; + HashSet addedReferences = new(StringComparer.Ordinal); + JArray migratedReferences = new(); + foreach (JToken referenceToken in references) + { + string reference = referenceToken.Value() ?? string.Empty; + string[] migratedReferenceItems = GetMigratedAsmdefReferences( + reference, + hasLegacyCSharpSource, + requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + bool referenceChanged = migratedReferenceItems.Length != 1 || + !string.Equals(migratedReferenceItems[0], reference, StringComparison.Ordinal); + if (referenceChanged) + { + replacementCount++; + } + + foreach (string migratedReference in migratedReferenceItems) + { + string migratedReferenceKey = GetCurrentAsmdefReferenceKey(migratedReference); + if (!addedReferences.Add(migratedReferenceKey)) + { + continue; + } + + migratedReferences.Add(migratedReference); + } + } + + AddRequiredCurrentAsmdefReferences( + migratedReferences, + addedReferences, + hasLegacyCSharpSource || requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference, + ref replacementCount); + + if (replacementCount == 0) + { + return new ThirdPartyToolMigrationContentResult( + source, + 0, + Array.Empty()); + } + + asmdef["references"] = migratedReferences; + return new ThirdPartyToolMigrationContentResult( + asmdef.ToString(Formatting.Indented), + replacementCount, + Array.Empty()); + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs.meta new file mode 100644 index 0000000000..0014e0db28 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b2e32d61b0fd4a3eaae36a6800f7649 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs new file mode 100644 index 0000000000..62ebc5ff59 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyScopedNameMap; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Resolves asmdef, asmref, and implicit Unity assembly directories for migration planning. + /// + internal static class ThirdPartyToolMigrationAssemblyReferenceResolver + { + internal static List CreateAssemblyReferenceDirectories( + List asmdefFilePaths, + List asmrefFilePaths) + { + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + + if (asmrefFilePaths.Count == 0) + { + return new List(); + } + + Dictionary asmdefDirectoriesByReference = CreateAsmdefDirectoryMap(asmdefFilePaths); + List assemblyReferenceDirectories = new(); + foreach (string asmrefFilePath in asmrefFilePaths) + { + if (!TryReadJsonObjectFromFile(asmrefFilePath, out JObject asmref)) + { + continue; + } + + string reference = asmref["reference"]?.Value() ?? string.Empty; + if (reference.Length == 0) + { + continue; + } + + if (!asmdefDirectoriesByReference.TryGetValue(reference, out string targetAssemblyDirectory)) + { + continue; + } + + string sourceDirectory = Path.GetDirectoryName(asmrefFilePath) ?? string.Empty; + if (sourceDirectory.Length == 0) + { + continue; + } + + assemblyReferenceDirectories.Add( + new AssemblyReferenceDirectory(sourceDirectory, targetAssemblyDirectory)); + } + + return assemblyReferenceDirectories + .OrderByDescending(assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) + .ToList(); + } + + internal static Dictionary CreateReferencedAssemblyDirectoriesByDirectory( + List asmdefDirectories) + { + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + + List asmdefFilePaths = GetAsmdefFilePathsFromDirectories(asmdefDirectories); + Dictionary asmdefDirectoriesByReference = CreateAsmdefDirectoryMap(asmdefFilePaths); + Dictionary> referencedDirectoriesByDirectory = new(StringComparer.Ordinal); + foreach (string asmdefFilePath in asmdefFilePaths) + { + string assemblyDirectory = Path.GetDirectoryName(asmdefFilePath) ?? string.Empty; + if (assemblyDirectory.Length == 0) + { + continue; + } + + if (!TryReadJsonObjectFromFile(asmdefFilePath, out JObject asmdef)) + { + continue; + } + + if (asmdef["references"] is not JArray references) + { + continue; + } + + foreach (JToken referenceToken in references) + { + string reference = referenceToken.Value() ?? string.Empty; + if (!asmdefDirectoriesByReference.TryGetValue(reference, out string referencedAssemblyDirectory)) + { + continue; + } + + AddAssemblyScopedNames( + referencedDirectoriesByDirectory, + assemblyDirectory, + new[] { referencedAssemblyDirectory }); + } + } + + return CreateAssemblyScopedNamesByDirectory(referencedDirectoriesByDirectory); + } + + internal static List GetAsmdefFilePathsFromDirectories(List asmdefDirectories) + { + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + + List asmdefFilePaths = new(); + foreach (string asmdefDirectory in asmdefDirectories) + { + if (!Directory.Exists(asmdefDirectory)) + { + continue; + } + + asmdefFilePaths.AddRange(Directory.GetFiles( + asmdefDirectory, + "*.asmdef", + SearchOption.TopDirectoryOnly)); + } + + return asmdefFilePaths + .OrderBy(asmdefFilePath => asmdefFilePath, StringComparer.Ordinal) + .ToList(); + } + + internal static async Task> CreateAssemblyReferenceDirectoriesAsync( + List asmdefFilePaths, + List asmrefFilePaths, + ThirdPartyToolMigrationSourceFileCache sourceFileCache, + MigrationProgressCounter progressCounter, + CancellationToken ct) + { + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); + Debug.Assert(progressCounter != null, "progressCounter must not be null"); + + if (asmrefFilePaths.Count == 0) + { + return new List(); + } + + Dictionary asmdefDirectoriesByReference = + await CreateAsmdefDirectoryMapAsync(asmdefFilePaths, sourceFileCache, progressCounter, ct); + List assemblyReferenceDirectories = new(); + foreach (string asmrefFilePath in asmrefFilePaths) + { + if (ct.IsCancellationRequested) + { + return assemblyReferenceDirectories + .OrderByDescending( + assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) + .ToList(); + } + + if (!TryReadJsonObjectFromCache(asmrefFilePath, sourceFileCache, out JObject asmref)) + { + await progressCounter.ReportProcessedItemAsync(ct); + continue; + } + + await progressCounter.ReportProcessedItemAsync(ct); + string reference = asmref["reference"]?.Value() ?? string.Empty; + if (reference.Length == 0) + { + continue; + } + + if (!asmdefDirectoriesByReference.TryGetValue(reference, out string targetAssemblyDirectory)) + { + continue; + } + + string sourceDirectory = Path.GetDirectoryName(asmrefFilePath) ?? string.Empty; + if (sourceDirectory.Length == 0) + { + continue; + } + + assemblyReferenceDirectories.Add( + new AssemblyReferenceDirectory(sourceDirectory, targetAssemblyDirectory)); + } + + return assemblyReferenceDirectories + .OrderByDescending(assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) + .ToList(); + } + + internal static Dictionary CreateAsmdefDirectoryMap(List asmdefFilePaths) + { + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + + Dictionary asmdefDirectoriesByReference = new(StringComparer.Ordinal); + foreach (string asmdefFilePath in asmdefFilePaths) + { + string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? string.Empty; + if (asmdefDirectory.Length == 0) + { + continue; + } + + if (!TryReadJsonObjectFromFile(asmdefFilePath, out JObject asmdef)) + { + continue; + } + + string assemblyName = asmdef["name"]?.Value() ?? string.Empty; + AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); + AddAsmdefDirectoryReference( + asmdefDirectoriesByReference, + ReadAsmdefGuidReferenceFromAsmdefPath(asmdefFilePath), + asmdefDirectory); + } + + return asmdefDirectoriesByReference; + } + + internal static async Task> CreateAsmdefDirectoryMapAsync( + List asmdefFilePaths, + ThirdPartyToolMigrationSourceFileCache sourceFileCache, + MigrationProgressCounter progressCounter, + CancellationToken ct) + { + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); + Debug.Assert(progressCounter != null, "progressCounter must not be null"); + + Dictionary asmdefDirectoriesByReference = new(StringComparer.Ordinal); + foreach (string asmdefFilePath in asmdefFilePaths) + { + if (ct.IsCancellationRequested) + { + return asmdefDirectoriesByReference; + } + + string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? string.Empty; + if (asmdefDirectory.Length == 0) + { + await progressCounter.ReportProcessedItemAsync(ct); + continue; + } + + if (!TryReadJsonObjectFromCache(asmdefFilePath, sourceFileCache, out JObject asmdef)) + { + await progressCounter.ReportProcessedItemAsync(ct); + continue; + } + + await progressCounter.ReportProcessedItemAsync(ct); + string assemblyName = asmdef["name"]?.Value() ?? string.Empty; + AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); + AddAsmdefDirectoryReference( + asmdefDirectoriesByReference, + ReadAsmdefGuidReferenceFromAsmdefPath(asmdefFilePath), + asmdefDirectory); + } + + return asmdefDirectoriesByReference; + } + + internal static bool TryReadJsonObjectFromFile(string filePath, out JObject jsonObject) + { + return TryReadJsonObjectForMigration(filePath, File.ReadAllText, out jsonObject); + } + + internal static bool TryReadJsonObjectFromCache( + string filePath, + ThirdPartyToolMigrationSourceFileCache sourceFileCache, + out JObject jsonObject) + { + Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); + + return TryReadJsonObjectForMigration(filePath, sourceFileCache.ReadAllText, out jsonObject); + } + + internal static bool TryReadJsonObjectForMigration( + string filePath, + Func readAllText, + out JObject jsonObject) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(readAllText != null, "readAllText must not be null"); + + try + { + jsonObject = JObject.Parse(readAllText(filePath)); + return true; + } + catch (Exception ex) when (IsSkippableAssemblyJsonReadException(ex)) + { + UnityEngine.Debug.LogWarning( + $"[UnityCliLoop] Skipping unreadable or malformed assembly JSON at {filePath}: {ex.Message}"); + jsonObject = null; + return false; + } + } + + internal static bool IsSkippableAssemblyJsonReadException(Exception ex) + { + Debug.Assert(ex != null, "ex must not be null"); + + return ex is JsonException || + ex is IOException || + ex is UnauthorizedAccessException; + } + + internal static void AddAsmdefDirectoryReference( + Dictionary asmdefDirectoriesByReference, + string reference, + string asmdefDirectory) + { + Debug.Assert(asmdefDirectoriesByReference != null, "asmdefDirectoriesByReference must not be null"); + Debug.Assert(reference != null, "reference must not be null"); + Debug.Assert(!string.IsNullOrEmpty(asmdefDirectory), "asmdefDirectory must not be null or empty"); + + if (reference.Length == 0 || asmdefDirectoriesByReference.ContainsKey(reference)) + { + return; + } + + asmdefDirectoriesByReference.Add(reference, asmdefDirectory); + } + + internal static string ReadAsmdefGuidReferenceFromAsmdefPath(string asmdefFilePath) + { + return ThirdPartyToolMigrationAsmdefMetaGuidReader.ReadAsmdefGuidReferenceFromAsmdefPath( + asmdefFilePath); + } + + internal static string ReadAsmdefGuidReferenceFromMetaFile( + string metaPath, + Func> readLines) + { + return ThirdPartyToolMigrationAsmdefMetaGuidReader.ReadAsmdefGuidReferenceFromMetaFile( + metaPath, + readLines); + } + + internal static string FindNearestAssemblyDirectory( + string csharpFilePath, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "assemblyReferenceDirectories must not be null"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + string csharpDirectory = Path.GetDirectoryName(csharpFilePath) ?? string.Empty; + string matchedAssemblyDirectory = string.Empty; + int matchedSourceDirectoryLength = -1; + foreach (string asmdefDirectory in asmdefDirectories) + { + if (!IsSameOrChildPath(csharpDirectory, asmdefDirectory) || + asmdefDirectory.Length <= matchedSourceDirectoryLength) + { + continue; + } + + matchedAssemblyDirectory = asmdefDirectory; + matchedSourceDirectoryLength = asmdefDirectory.Length; + } + + foreach (AssemblyReferenceDirectory assemblyReferenceDirectory in assemblyReferenceDirectories) + { + string sourceDirectory = assemblyReferenceDirectory.SourceDirectory; + if (!IsSameOrChildPath(csharpDirectory, sourceDirectory) || + sourceDirectory.Length <= matchedSourceDirectoryLength) + { + continue; + } + + matchedAssemblyDirectory = assemblyReferenceDirectory.TargetAssemblyDirectory; + matchedSourceDirectoryLength = sourceDirectory.Length; + } + + if (matchedAssemblyDirectory.Length > 0) + { + return matchedAssemblyDirectory; + } + + return GetImplicitAssemblyDirectory(csharpFilePath, projectRoot); + } + + internal static string GetImplicitAssemblyDirectory(string csharpFilePath, string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + bool isEditorAssemblyPath = IsEditorAssemblyPath(csharpFilePath, projectRoot); + bool isFirstPassAssemblyPath = IsFirstPassAssemblyPath(csharpFilePath, projectRoot); + string implicitAssemblyDirectoryName = GetImplicitAssemblyDirectoryName( + isEditorAssemblyPath, + isFirstPassAssemblyPath); + return Path.Combine(projectRoot, implicitAssemblyDirectoryName); + } + + internal static string GetImplicitAssemblyDirectoryName( + bool isEditorAssemblyPath, + bool isFirstPassAssemblyPath) + { + if (isEditorAssemblyPath && isFirstPassAssemblyPath) + { + return ThirdPartyToolMigrationFileServiceConstants.ImplicitFirstPassEditorAssemblyDirectoryName; + } + + if (isFirstPassAssemblyPath) + { + return ThirdPartyToolMigrationFileServiceConstants.ImplicitFirstPassRuntimeAssemblyDirectoryName; + } + + return isEditorAssemblyPath + ? ThirdPartyToolMigrationFileServiceConstants.ImplicitEditorAssemblyDirectoryName + : ThirdPartyToolMigrationFileServiceConstants.ImplicitRuntimeAssemblyDirectoryName; + } + + internal static bool IsEditorAssemblyPath(string csharpFilePath, string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + string[] pathSegments = GetRelativePathSegments(csharpFilePath, projectRoot); + return pathSegments.Any( + pathSegment => string.Equals(pathSegment, "Editor", StringComparison.OrdinalIgnoreCase)); + } + + internal static bool IsFirstPassAssemblyPath(string csharpFilePath, string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + string[] pathSegments = GetRelativePathSegments(csharpFilePath, projectRoot); + if (pathSegments.Length < 2 || + !string.Equals(pathSegments[0], "Assets", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return string.Equals(pathSegments[1], "Plugins", StringComparison.OrdinalIgnoreCase) || + string.Equals(pathSegments[1], "Standard Assets", StringComparison.OrdinalIgnoreCase) || + string.Equals(pathSegments[1], "Pro Standard Assets", StringComparison.OrdinalIgnoreCase); + } + + internal static string[] GetRelativePathSegments(string filePath, string projectRoot) + { + return ThirdPartyToolMigrationPathRules.GetRelativePathSegments(filePath, projectRoot); + } + + internal static bool IsSameOrChildPath(string childPath, string parentPath) + { + return ThirdPartyToolMigrationPathRules.IsSameOrChildPath(childPath, parentPath); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs.meta new file mode 100644 index 0000000000..446e110a89 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 677476a948cf5443895b30717b385c1f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs new file mode 100644 index 0000000000..9d8d47ca92 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Builds assembly-scoped alias and declared-type lookup tables for migration planning. + /// + internal static class ThirdPartyToolMigrationAssemblyScopedNameMap + { + internal static MigrationAssemblyUsage CreateMigrationAssemblyUsage( + List asmdefDirectories, + List assemblyReferenceDirectories, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedLegacyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + Dictionary> assemblyScopedLegacyAliasesByDirectory, + Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, + Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, + Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet applicationReferenceAssemblyDirectories, + 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( + 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( + 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"); + + return new MigrationAssemblyUsage( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + } + + internal static void AddAssemblyScopedLegacyAliases( + Dictionary> aliasesByDirectory, + string assemblyDirectory, + string[] aliases) + { + AddAssemblyScopedNames(aliasesByDirectory, assemblyDirectory, aliases); + } + + internal static void AddAssemblyScopedNames( + Dictionary> namesByDirectory, + string assemblyDirectory, + string[] names) + { + Debug.Assert(namesByDirectory != null, "namesByDirectory must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + Debug.Assert(names != null, "names must not be null"); + + if (names.Length == 0) + { + return; + } + + if (!namesByDirectory.TryGetValue(assemblyDirectory, out HashSet nameSet)) + { + nameSet = new HashSet(StringComparer.Ordinal); + namesByDirectory.Add(assemblyDirectory, nameSet); + } + + foreach (string name in names) + { + nameSet.Add(name); + } + } + + internal static Dictionary CreateAssemblyScopedLegacyAliasesByDirectory( + Dictionary> aliasesByDirectory) + { + return CreateAssemblyScopedNamesByDirectory(aliasesByDirectory); + } + + internal static Dictionary CreateAssemblyScopedNamesByDirectory( + Dictionary> namesByDirectory) + { + Debug.Assert(namesByDirectory != null, "namesByDirectory must not be null"); + + Dictionary result = new(StringComparer.Ordinal); + foreach (KeyValuePair> namesForDirectory in namesByDirectory) + { + result.Add( + namesForDirectory.Key, + namesForDirectory.Value.OrderBy(name => name, StringComparer.Ordinal).ToArray()); + } + + return result; + } + + internal static string[] GetAssemblyScopedNames( + Dictionary> namesByDirectory, + string assemblyDirectory) + { + Debug.Assert(namesByDirectory != null, "namesByDirectory must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + + if (!namesByDirectory.TryGetValue(assemblyDirectory, out HashSet names)) + { + return Array.Empty(); + } + + return names + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + } + + internal static string[] GetAllAssemblyScopedLegacyToolInfoAliases(MigrationAssemblyUsage assemblyUsage) + { + return assemblyUsage.AssemblyScopedLegacyToolInfoAliasesByDirectory.Values + .SelectMany(aliases => aliases) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs.meta new file mode 100644 index 0000000000..b45eda8f31 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ddba4992803e5441ba03d7bb69eb0ccf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs new file mode 100644 index 0000000000..c8b25e89de --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +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 +{ + /// + /// Builds assembly usage facts for synchronous migration planning. + /// + internal static class ThirdPartyToolMigrationAssemblyUsageAnalyzer + { + internal static MigrationAssemblyUsage FindMigrationAssemblyUsage( + string projectRoot, + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + + List asmdefDirectories = asmdefFilePaths + .Select(path => Path.GetDirectoryName(path) ?? string.Empty) + .Where(path => !string.IsNullOrEmpty(path)) + .OrderByDescending(path => path.Length) + .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> 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); + Dictionary sourceByCSharpFilePath = new(StringComparer.Ordinal); + + foreach (string csharpFilePath in csharpFilePaths) + { + string source = File.ReadAllText(csharpFilePath); + sourceByCSharpFilePath.Add(csharpFilePath, source); + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + AddAssemblyScopedNames( + assemblyDeclaredTypeNamesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + 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); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) + { + assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) + { + assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); + 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) || + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + { + registrarAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) + { + toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) + { + domainMetadataAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + 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); + + 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)) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( + source, + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory))) + { + registrarAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( + source, + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory))) + { + domainReferenceAssemblyDirectories.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); + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + hasLegacyCSharpApi || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || + hasAssemblyScopedCurrentToolContractsUsing, + legacyAssemblyAliases, + assemblyDeclaredTypeNames) || + hasLegacyEditorWindowCaptureUtilitySourceTarget || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames)) + { + 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); + } + + foreach (string domainMetadataAssemblyDirectory in domainMetadataAssemblyDirectories) + { + if (legacyAssemblyDirectories.Contains(domainMetadataAssemblyDirectory)) + { + domainReferenceAssemblyDirectories.Add(domainMetadataAssemblyDirectory); + } + } + + return new MigrationAssemblyUsage( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs.meta new file mode 100644 index 0000000000..1d40d573f8 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 450bfd0ee4cec4160871c0f7a19ab21d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs new file mode 100644 index 0000000000..37b2a40bab --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +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 +{ + /// + /// Builds assembly usage facts for asynchronous migration planning. + /// + internal static class ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer + { + internal static async Task FindMigrationAssemblyUsageAsync( + string projectRoot, + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths, + ThirdPartyToolMigrationSourceFileCache sourceFileCache, + MigrationProgressCounter progressCounter, + CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); + Debug.Assert(progressCounter != null, "progressCounter must not be null"); + + List asmdefDirectories = asmdefFilePaths + .Select(path => Path.GetDirectoryName(path) ?? string.Empty) + .Where(path => !string.IsNullOrEmpty(path)) + .OrderByDescending(path => path.Length) + .ToList(); + List assemblyReferenceDirectories = + await CreateAssemblyReferenceDirectoriesAsync( + asmdefFilePaths, + asmrefFilePaths, + 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> 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); + + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return CreateMigrationAssemblyUsage( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + } + + string source = sourceFileCache.ReadAllText(csharpFilePath); + await progressCounter.ReportProcessedItemAsync(ct); + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + AddAssemblyScopedNames( + assemblyDeclaredTypeNamesByDirectory, + assemblyDirectory, + ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source)); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + 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); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) + { + assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) + { + assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); + 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) || + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + { + registrarAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) + { + toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) + { + domainMetadataAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return CreateMigrationAssemblyUsage( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + } + + 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); + + 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)) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( + source, + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + currentApplicationAssemblyAliases, + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory))) + { + registrarAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( + source, + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory))) + { + domainReferenceAssemblyDirectories.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); + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + hasLegacyCSharpApi || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || + hasAssemblyScopedCurrentToolContractsUsing, + legacyAssemblyAliases, + assemblyDeclaredTypeNames) || + hasLegacyEditorWindowCaptureUtilitySourceTarget || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames)) + { + 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); + } + + foreach (string domainMetadataAssemblyDirectory in domainMetadataAssemblyDirectories) + { + if (legacyAssemblyDirectories.Contains(domainMetadataAssemblyDirectory)) + { + domainReferenceAssemblyDirectories.Add(domainMetadataAssemblyDirectory); + } + } + + return CreateMigrationAssemblyUsage( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs.meta new file mode 100644 index 0000000000..8cead5e7a6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa9512320151c469fb1971df88f73585 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs new file mode 100644 index 0000000000..41d600cbf6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationAttributeRules + { + internal static bool TryMigrateLegacyToolAttributeList( + string attributesSource, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyToolAttribute, + out string migratedAttributes) + { + Debug.Assert(attributesSource != null, "attributesSource must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + string[] attributes = SplitAttributeArguments(attributesSource); + List migratedAttributeItems = new(); + bool changed = false; + foreach (string attribute in attributes) + { + string trimmedAttribute = attribute.Trim(); + if (TryMigrateLegacyToolAttributeEntry( + trimmedAttribute, + legacyNamespaceAliases, + canMigrateBareLegacyToolAttribute, + out string migratedAttribute)) + { + migratedAttributeItems.Add(migratedAttribute); + changed = true; + continue; + } + + migratedAttributeItems.Add(trimmedAttribute); + } + + if (!changed) + { + migratedAttributes = string.Empty; + return false; + } + + migratedAttributes = string.Join(", ", migratedAttributeItems); + return true; + } + + internal static bool TryMigrateLegacyToolAttributeEntry( + string attribute, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyToolAttribute, + out string migratedAttribute) + { + Debug.Assert(attribute != null, "attribute must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + Match match = LegacyToolAttributeEntryRegex.Match(attribute); + if (!match.Success) + { + migratedAttribute = string.Empty; + return false; + } + + bool hasQualifier = match.Groups["qualifier"].Success; + bool hasAlias = match.Groups["alias"].Success; + if (!hasQualifier && !hasAlias && !canMigrateBareLegacyToolAttribute) + { + migratedAttribute = string.Empty; + return false; + } + + if (hasAlias && !legacyNamespaceAliases.Contains(match.Groups["alias"].Value)) + { + migratedAttribute = string.Empty; + return false; + } + + string argumentsSource = match.Groups["arguments"].Value; + string[] migratedArguments = GetMigratedSupportedAttributeArguments(argumentsSource); + string attributeName = hasQualifier || hasAlias + ? $"{CurrentNamespace}.UnityCliLoopTool" + : "UnityCliLoopTool"; + migratedAttribute = migratedArguments.Length == 0 + ? attributeName + : $"{attributeName}({string.Join(", ", migratedArguments)})"; + return true; + } + + internal static string[] GetMigratedSupportedAttributeArguments(string argumentsSource) + { + Debug.Assert(argumentsSource != null, "argumentsSource must not be null"); + + List migratedArguments = new(); + string[] arguments = SplitAttributeArguments(argumentsSource); + foreach (string argument in arguments) + { + string trimmedArgument = argument.Trim(); + if (trimmedArgument.Length == 0) + { + continue; + } + + if (IsNamedAttributeArgument(trimmedArgument, DescriptionAttributeArgumentName)) + { + continue; + } + + if (IsNamedAttributeArgument(trimmedArgument, DisplayDevelopmentOnlyAttributeArgumentName)) + { + migratedArguments.Add(trimmedArgument); + continue; + } + + if (IsNamedAttributeArgument(trimmedArgument, RequiredSecuritySettingAttributeArgumentName)) + { + migratedArguments.Add(trimmedArgument); + } + } + + return migratedArguments.ToArray(); + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs.meta new file mode 100644 index 0000000000..9cbdeae4b9 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 045a4c9db8aa54fef8100990be05d023 +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 new file mode 100644 index 0000000000..13aa7cb0cb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationCSharpRules + { + internal static ThirdPartyToolMigrationContentResult MigrateCSharpSource(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), + hasAssemblyScopedCurrentToolContractsUsing: false, + hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentFirstPartyToolsUsing: false, + legacyAssemblyAliases: Array.Empty(), + legacyAssemblyToolInfoAliases: Array.Empty(), + currentApplicationAssemblyAliases: Array.Empty(), + currentFirstPartyToolsAssemblyAliases: Array.Empty(), + assemblyDeclaredTypeNames: Array.Empty()); + } + + internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegacyAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentApplicationUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] legacyAssemblyToolInfoAliases, + string[] currentApplicationAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); + Debug.Assert( + currentApplicationAssemblyAliases != null, + "currentApplicationAssemblyAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsAssemblyAliases != null, + "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[] 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 hasCurrentToolContractsNamespaceUsage = RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); + bool hasCurrentToolContractsUsingDirective = + RegexMatchesCode(source, CurrentToolContractsUsingRegex); + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + bool canUseCurrentToolContracts = + hasCurrentToolContractsNamespaceUsage || + hasAssemblyScopedCurrentToolContractsUsing; + bool canUseBareCurrentToolContracts = + hasLegacyAssemblySource || + hasLegacyNamespaceUsingDirective || + hasCurrentToolContractsUsingDirective || + hasAssemblyScopedCurrentToolContractsUsing; + bool canMigrateBareLegacyToolAttribute = + hasLegacyAssemblySource || + hasLegacyNamespaceUsage || + legacyNamespaceAliases.Length > 0; + bool canMigrateBareLegacyEditorWindowCaptureUtility = + canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + hasAssemblyScopedCurrentFirstPartyToolsUsing || + hasCurrentFirstPartyToolsNamespaceUsage; + bool canUseBareCurrentFirstPartyTools = + hasAssemblyScopedCurrentFirstPartyToolsUsing || + hasCurrentFirstPartyToolsNamespaceUsage; + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout = + !canUseBareCurrentToolContracts; + bool canMigrateBareLegacyFirstPartyScreenshotApi = + canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + hasCurrentFirstPartyToolsNamespaceUsage; + bool canMigrateBareLegacyApplicationApi = + canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + hasAssemblyScopedCurrentApplicationUsing || + hasCurrentApplicationNamespaceUsage; + bool canMigrateBareLegacyApplicationTypeName = + canMigrateBareLegacyToolAttribute || + canUseCurrentToolContracts || + 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, + legacyNamespaceAliases, + currentApplicationNamespaceAliases, + canMigrateBareLegacyApplicationApi, + assemblyDeclaredTypeNames); + migratedContent = mainThreadSwitcherMigratedContent; + replacementCount += mainThreadSwitcherReplacementCount; + bool hasMainThreadSwitcherMigrationContext = + mainThreadSwitcherReplacementCount > 0 || + ContainsMigratedMainThreadSwitcherSwitchCall(migratedContent); + if (hasMainThreadSwitcherMigrationContext) + { + string[] migratedCalleeMethodNames = Array.Empty(); + while (true) + { + ( + string playerLoopTimingMigratedContent, + int playerLoopTimingReplacementCount, + RemovedLegacyPlayerLoopTimingSignature[] localRemovedTimingSignatures) = + RemoveLegacyPlayerLoopTimingParametersInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyApplicationApi, + migratedCalleeMethodNames); + if (playerLoopTimingReplacementCount == 0) + { + break; + } + + migratedContent = playerLoopTimingMigratedContent; + replacementCount += playerLoopTimingReplacementCount; + removedPlayerLoopTimingSignatures.AddRange(localRemovedTimingSignatures); + (string timingCallerMigratedContent, int timingCallerReplacementCount) = + RemoveLegacyPlayerLoopTimingCallerArgumentsInCode( + migratedContent, + localRemovedTimingSignatures, + legacyNamespaceAliases); + migratedContent = timingCallerMigratedContent; + replacementCount += 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, + canUseBareCurrentFirstPartyTools, + assemblyDeclaredTypeNames); + migratedContent = editorWindowCaptureMigratedContent; + replacementCount += editorWindowCaptureReplacementCount; + migratedContent = ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyFirstPartyScreenshotApi, + assemblyDeclaredTypeNames, + ref replacementCount); + migratedContent = ReplaceLegacyApplicationTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyApplicationTypeName, + assemblyDeclaredTypeNames, + ref replacementCount); + migratedContent = ReplaceLegacyRegistrarAliasesInCode( + migratedContent, + legacyNamespaceAliases, + ref replacementCount); + + if (shouldApplyContractRenames) + { + migratedContent = ReplaceLegacyDomainTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + ref replacementCount); + + migratedContent = ReplaceLegacyContractTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + ref replacementCount); + + foreach (ReplacementRule rule in CSharpReplacementRules) + { + migratedContent = ReplaceRegexInCode( + migratedContent, + rule.PatternRegex, + _ => rule.Replacement, + ref replacementCount); + } + } + + if (shouldApplyRegistrarRenames || shouldApplyDomainMetadataRenames) + { + if (shouldApplyRegistrarRenames) + { + migratedContent = ReplaceUnqualifiedLegacyRegistrarReferencesInCode( + migratedContent, + ref replacementCount); + } + + foreach (ReplacementRule rule in RegistrarReplacementRules) + { + migratedContent = ReplaceRegexInCode( + migratedContent, + rule.PatternRegex, + _ => rule.Replacement, + ref replacementCount); + } + + migratedContent = ReplaceLegacyToolInfoTypeReferencesInCode( + migratedContent, + ref replacementCount); + } + + migratedContent = ReplaceCurrentPublicContractNamespacesInCode( + migratedContent, + ref replacementCount); + + return new ThirdPartyToolMigrationContentResult( + migratedContent, + replacementCount, + removedPlayerLoopTimingSignatures.ToArray()); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs.meta new file mode 100644 index 0000000000..31e61b6b3b --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e4fc1d205d9f49c2b19d4dc57996929 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs new file mode 100644 index 0000000000..0dba9ece6a --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationCodeTextMaskBuilder + { + internal static bool[] CreateCodeCharacters(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool[] codeCharacters = new bool[source.Length]; + for (int i = 0; i < codeCharacters.Length; i++) + { + codeCharacters[i] = true; + } + + int index = 0; + while (index < source.Length) + { + if (IsInterpolatedRawStringStart(source, index)) + { + index = MarkInterpolatedRawStringAsIgnored(codeCharacters, source, index); + continue; + } + + if (StartsWith(source, index, "$@\"") || StartsWith(source, index, "@$\"")) + { + index = MarkInterpolatedVerbatimStringAsIgnored(codeCharacters, source, index); + continue; + } + + if (StartsWith(source, index, "$\"") && !StartsWith(source, index, "$\"\"\"")) + { + index = MarkInterpolatedRegularStringAsIgnored(codeCharacters, source, index); + continue; + } + + int ignoredTextEndIndex = GetIgnoredTextEndIndex(source, index); + if (ignoredTextEndIndex == index) + { + index++; + continue; + } + + MarkRangeAsIgnored(codeCharacters, index, ignoredTextEndIndex); + index = ignoredTextEndIndex; + } + + return codeCharacters; + } + + internal static int GetIgnoredTextEndIndex(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + if (StartsWith(source, startIndex, "//")) + { + return FindLineCommentEndIndex(source, startIndex); + } + + if (StartsWith(source, startIndex, "/*")) + { + return FindBlockCommentEndIndex(source, startIndex); + } + + if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) + { + return FindVerbatimStringEndIndex(source, startIndex + 2); + } + + if (StartsWith(source, startIndex, "@\"")) + { + return FindVerbatimStringEndIndex(source, startIndex + 1); + } + + if (IsInterpolatedRawStringStart(source, startIndex)) + { + int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); + return FindRawStringEndIndex(source, startIndex + dollarCount); + } + + if (StartsWith(source, startIndex, "$\"")) + { + return FindRegularStringEndIndex(source, startIndex + 1); + } + + if (StartsWith(source, startIndex, "\"\"\"")) + { + return FindRawStringEndIndex(source, startIndex); + } + + if (source[startIndex] == '"') + { + return FindRegularStringEndIndex(source, startIndex); + } + + if (source[startIndex] == '\'') + { + return FindCharLiteralEndIndex(source, startIndex); + } + + return startIndex; + } + + internal static int FindLineCommentEndIndex(string source, int startIndex) + { + int index = startIndex; + while (index < source.Length && source[index] != '\n') + { + index++; + } + + return index; + } + + internal static int FindBlockCommentEndIndex(string source, int startIndex) + { + int index = startIndex + 2; + while (index + 1 < source.Length) + { + if (source[index] == '*' && source[index + 1] == '/') + { + return index + 2; + } + + index++; + } + + return source.Length; + } + + internal static int FindRegularStringEndIndex(string source, int quoteIndex) + { + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '"') + { + return index + 1; + } + + index++; + } + + return source.Length; + } + + internal static int FindVerbatimStringEndIndex(string source, int quoteIndex) + { + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] != '"') + { + index++; + continue; + } + + if (index + 1 < source.Length && source[index + 1] == '"') + { + index += 2; + continue; + } + + return index + 1; + } + + return source.Length; + } + + internal static int FindRawStringEndIndex(string source, int quoteIndex) + { + int quoteCount = CountRepeatedCharacter(source, quoteIndex, '"'); + Debug.Assert( + quoteCount >= MinimumRawStringDelimiterQuoteCount, + "quoteIndex must point to a raw string delimiter"); + + int index = quoteIndex + quoteCount; + while (index + quoteCount <= source.Length) + { + if (HasRepeatedCharacterAt(source, index, '"', quoteCount)) + { + return index + quoteCount; + } + + index++; + } + + return source.Length; + } + + internal static int FindCharLiteralEndIndex(string source, int quoteIndex) + { + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '\'') + { + return index + 1; + } + + index++; + } + + return source.Length; + } + + internal static void MarkRangeAsIgnored(bool[] codeCharacters, int startIndex, int endIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + Debug.Assert(endIndex >= startIndex, "endIndex must not be less than startIndex"); + + for (int i = startIndex; i < endIndex && i < codeCharacters.Length; i++) + { + codeCharacters[i] = false; + } + } + + internal static bool IsInterpolatedRawStringStart(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + if (startIndex >= source.Length || source[startIndex] != '$') + { + return false; + } + + int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); + int quoteIndex = startIndex + dollarCount; + return CountRepeatedCharacter(source, quoteIndex, '"') >= MinimumRawStringDelimiterQuoteCount; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs.meta new file mode 100644 index 0000000000..30ac86465d --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56d8406cf16174ebdbd4911e14f67359 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs new file mode 100644 index 0000000000..9460c015bb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs @@ -0,0 +1,350 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationCodeTextMaskInterpolationRules + { + internal static int MarkInterpolatedRawStringAsIgnored( + bool[] codeCharacters, + string source, + int dollarIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(dollarIndex >= 0, "dollarIndex must not be negative"); + + int dollarCount = CountRepeatedCharacter(source, dollarIndex, '$'); + int quoteIndex = dollarIndex + dollarCount; + int quoteCount = CountRepeatedCharacter(source, quoteIndex, '"'); + Debug.Assert(dollarCount > 0, "dollarIndex must point to an interpolated raw string prefix"); + Debug.Assert( + quoteCount >= MinimumRawStringDelimiterQuoteCount, + "quoteIndex must point to a raw string delimiter"); + + int literalStartIndex = dollarIndex; + int index = quoteIndex + quoteCount; + while (index < source.Length) + { + if (HasRepeatedCharacterAt(source, index, '"', quoteCount)) + { + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + quoteCount); + return index + quoteCount; + } + + if (source[index] == '{') + { + int braceCount = CountRepeatedCharacter(source, index, '{'); + if (braceCount < dollarCount) + { + index += braceCount; + continue; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); + index = MarkRawInterpolationHoleNestedTextAsIgnored( + codeCharacters, + source, + index, + dollarCount); + literalStartIndex = index; + continue; + } + + if (source[index] == '}') + { + int braceCount = CountRepeatedCharacter(source, index, '}'); + index += braceCount; + continue; + } + + index++; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); + return source.Length; + } + + internal static int MarkInterpolatedRegularStringAsIgnored( + bool[] codeCharacters, + string source, + int dollarIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(dollarIndex >= 0, "dollarIndex must not be negative"); + + int quoteIndex = dollarIndex + 1; + int literalStartIndex = dollarIndex; + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '{') + { + if (index + 1 < source.Length && source[index + 1] == '{') + { + index += 2; + continue; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); + index = MarkInterpolationHoleNestedTextAsIgnored(codeCharacters, source, index); + literalStartIndex = index; + continue; + } + + if (source[index] == '"') + { + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + 1); + return index + 1; + } + + index++; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); + return source.Length; + } + + internal static int MarkInterpolatedVerbatimStringAsIgnored( + bool[] codeCharacters, + string source, + int prefixIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(prefixIndex >= 0, "prefixIndex must not be negative"); + + int quoteIndex = prefixIndex + 2; + int literalStartIndex = prefixIndex; + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '"') + { + if (index + 1 < source.Length && source[index + 1] == '"') + { + index += 2; + continue; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + 1); + return index + 1; + } + + if (source[index] == '{') + { + if (index + 1 < source.Length && source[index + 1] == '{') + { + index += 2; + continue; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); + index = MarkInterpolationHoleNestedTextAsIgnored(codeCharacters, source, index); + literalStartIndex = index; + continue; + } + + if (source[index] == '}' && index + 1 < source.Length && source[index + 1] == '}') + { + index += 2; + continue; + } + + index++; + } + + MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); + return source.Length; + } + + internal static int MarkInterpolationHoleNestedTextAsIgnored( + bool[] codeCharacters, + string source, + int openBraceIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); + + int nestedBraceDepth = 0; + int index = openBraceIndex + 1; + while (index < source.Length) + { + int ignoredTextEndIndex = MarkIgnoredTextInInterpolationHole( + codeCharacters, + source, + index); + if (ignoredTextEndIndex != index) + { + index = ignoredTextEndIndex; + continue; + } + + if (source[index] == '{') + { + nestedBraceDepth++; + index++; + continue; + } + + if (source[index] == '}') + { + if (nestedBraceDepth == 0) + { + return index + 1; + } + + nestedBraceDepth--; + } + + index++; + } + + return source.Length; + } + + internal static int MarkRawInterpolationHoleNestedTextAsIgnored( + bool[] codeCharacters, + string source, + int openBraceIndex, + int interpolationBraceCount) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); + Debug.Assert(interpolationBraceCount > 0, "interpolationBraceCount must be positive"); + + int nestedBraceDepth = 0; + int index = openBraceIndex + interpolationBraceCount; + while (index < source.Length) + { + int ignoredTextEndIndex = MarkIgnoredTextInInterpolationHole( + codeCharacters, + source, + index); + if (ignoredTextEndIndex != index) + { + index = ignoredTextEndIndex; + continue; + } + + if (source[index] == '{') + { + nestedBraceDepth++; + index++; + continue; + } + + if (source[index] == '}') + { + bool isClosingInterpolation = + nestedBraceDepth == 0 && + HasRepeatedCharacterAt(source, index, '}', interpolationBraceCount); + if (isClosingInterpolation) + { + return index + interpolationBraceCount; + } + + if (nestedBraceDepth > 0) + { + nestedBraceDepth--; + } + } + + index++; + } + + return source.Length; + } + + internal static int MarkIgnoredTextInInterpolationHole( + bool[] codeCharacters, + string source, + int startIndex) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + if (IsInterpolatedRawStringStart(source, startIndex)) + { + return MarkInterpolatedRawStringAsIgnored(codeCharacters, source, startIndex); + } + + if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) + { + return MarkInterpolatedVerbatimStringAsIgnored(codeCharacters, source, startIndex); + } + + if (StartsWith(source, startIndex, "$\"") && !StartsWith(source, startIndex, "$\"\"\"")) + { + return MarkInterpolatedRegularStringAsIgnored(codeCharacters, source, startIndex); + } + + int ignoredTextEndIndex = GetIgnoredTextEndIndex(source, startIndex); + if (ignoredTextEndIndex == startIndex) + { + return startIndex; + } + + MarkRangeAsIgnored(codeCharacters, startIndex, ignoredTextEndIndex); + return ignoredTextEndIndex; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs.meta new file mode 100644 index 0000000000..7f9836bcf9 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e78f8fc5334642d7bc3241772b98a48 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs new file mode 100644 index 0000000000..d1204e2ca9 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationConstructorArgumentRules + { + internal static string[] GetMigratedToolInfoConstructorArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + int namedDescriptionArgumentIndex = FindNamedConstructorArgumentIndex( + arguments, + DescriptionAttributeArgumentName.ToLowerInvariant()); + if (namedDescriptionArgumentIndex >= 0) + { + return RemoveArgumentAt(arguments, namedDescriptionArgumentIndex); + } + + if (arguments.Length == 4) + { + return new[] + { + arguments[0].Trim(), + arguments[2].Trim(), + arguments[3].Trim() + }; + } + + if (arguments.Length == 3) + { + return new[] + { + arguments[0].Trim(), + arguments[2].Trim() + }; + } + + return arguments; + } + + internal static string[] GetMigratedToolSettingsCatalogItemConstructorArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + int namedDescriptionArgumentIndex = FindNamedConstructorArgumentIndex( + arguments, + DescriptionAttributeArgumentName.ToLowerInvariant()); + if (namedDescriptionArgumentIndex >= 0) + { + return RemoveArgumentAt(arguments, namedDescriptionArgumentIndex); + } + + if (arguments.Length == 4) + { + return new[] + { + arguments[0].Trim(), + arguments[2].Trim(), + arguments[3].Trim() + }; + } + + return arguments; + } + + internal static int FindNamedConstructorArgumentIndex(string[] arguments, string argumentName) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); + + for (int i = 0; i < arguments.Length; i++) + { + if (IsNamedConstructorArgument(arguments[i].Trim(), argumentName)) + { + return i; + } + } + + return -1; + } + + internal static bool IsNamedConstructorArgument(string argument, string argumentName) + { + Debug.Assert(argument != null, "argument must not be null"); + Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); + + int colonIndex = argument.IndexOf(':'); + if (colonIndex <= 0) + { + return false; + } + + string possibleArgumentName = argument.Substring(0, colonIndex).Trim(); + return string.Equals(possibleArgumentName, argumentName, StringComparison.Ordinal); + } + + internal static string[] RemoveArgumentAt(string[] arguments, int removeIndex) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(removeIndex >= 0, "removeIndex must not be negative"); + Debug.Assert(removeIndex < arguments.Length, "removeIndex must be within arguments"); + + List migratedArguments = new(); + for (int i = 0; i < arguments.Length; i++) + { + if (i == removeIndex) + { + continue; + } + + migratedArguments.Add(arguments[i].Trim()); + } + + return migratedArguments.ToArray(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs.meta new file mode 100644 index 0000000000..8101c6d02c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62c6c68775f0740c48214edd91e72d36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs new file mode 100644 index 0000000000..e4dfa726eb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAssemblyReferenceResolver; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Propagates removed PlayerLoopTiming signatures across reachable files. + /// + internal static class ThirdPartyToolMigrationCrossFileTimingMigrationPlanner + { + internal static void AddRemovedPlayerLoopTimingSignatures( + Dictionary> + removedSignaturesByAssemblyDirectory, + string assemblyDirectory, + ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature[] removedSignatures) + { + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + Debug.Assert(removedSignatures != null, "removedSignatures must not be null"); + + if (removedSignatures.Length == 0) + { + return; + } + + if (!removedSignaturesByAssemblyDirectory.TryGetValue( + assemblyDirectory, + out List signatures)) + { + signatures = new List(); + removedSignaturesByAssemblyDirectory.Add(assemblyDirectory, signatures); + } + + signatures.AddRange(removedSignatures); + } + + internal static int ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + List csharpFilePaths, + string projectRoot, + MigrationAssemblyUsage assemblyUsage, + List changes, + Dictionary> + removedSignaturesByAssemblyDirectory, + Func readAllText) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(changes != null, "changes must not be null"); + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + Debug.Assert(readAllText != null, "readAllText must not be null"); + + if (removedSignaturesByAssemblyDirectory.Count == 0) + { + return 0; + } + + Dictionary referencedAssemblyDirectoriesByDirectory = + CreateReferencedAssemblyDirectoriesByDirectory(assemblyUsage.AsmdefDirectories); + HashSet asmdefDirectorySet = new(assemblyUsage.AsmdefDirectories, StringComparer.Ordinal); + Dictionary> + activeRemovedSignaturesByAssemblyDirectory = + CloneRemovedPlayerLoopTimingSignatures(removedSignaturesByAssemblyDirectory); + if (!HasRemovedPlayerLoopTimingSignatures(activeRemovedSignaturesByAssemblyDirectory)) + { + return 0; + } + + int replacementCount = 0; + while (HasRemovedPlayerLoopTimingSignatures(activeRemovedSignaturesByAssemblyDirectory)) + { + Dictionary> + nextRemovedSignaturesByAssemblyDirectory = new(StringComparer.Ordinal); + foreach (string csharpFilePath in csharpFilePaths) + { + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + assemblyUsage.AsmdefDirectories, + assemblyUsage.AssemblyReferenceDirectories, + projectRoot); + string originalSource = readAllText(csharpFilePath); + string source = GetPendingMigrationFileContent(csharpFilePath, changes, originalSource); + string[] legacyAssemblyAliases = GetAssemblyScopedNameArray( + assemblyUsage.AssemblyScopedLegacyAliasesByDirectory, + assemblyDirectory); + List activeRemovedSignatures = + asmdefDirectorySet.Contains(assemblyDirectory) + ? GetReachableRemovedPlayerLoopTimingSignatures( + assemblyDirectory, + activeRemovedSignaturesByAssemblyDirectory, + referencedAssemblyDirectoriesByDirectory) + : GetAllRemovedPlayerLoopTimingSignatures( + activeRemovedSignaturesByAssemblyDirectory); + if (activeRemovedSignatures.Count == 0) + { + continue; + } + + ThirdPartyToolMigrationContentResult callerResult = + ThirdPartyToolMigrationRules.RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + source, + originalSource, + activeRemovedSignatures.ToArray(), + legacyAssemblyAliases); + if (!callerResult.Changed) + { + continue; + } + + UpsertMigrationFileChange(changes, csharpFilePath, callerResult.Content); + replacementCount += callerResult.ReplacementCount; + + bool canMigrateBareLegacyPlayerLoopTiming = + CanMigrateBareLegacyPlayerLoopTimingForAssembly( + assemblyUsage, + assemblyDirectory, + originalSource); + ThirdPartyToolMigrationContentResult parameterResult = + ThirdPartyToolMigrationRules.RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + callerResult.Content, + originalSource, + legacyAssemblyAliases, + canMigrateBareLegacyPlayerLoopTiming, + activeRemovedSignatures + .Select(signature => signature.MethodName) + .ToArray()); + if (!parameterResult.Changed) + { + continue; + } + + UpsertMigrationFileChange(changes, csharpFilePath, parameterResult.Content); + replacementCount += parameterResult.ReplacementCount; + AddRemovedPlayerLoopTimingSignatures( + nextRemovedSignaturesByAssemblyDirectory, + assemblyDirectory, + parameterResult.RemovedPlayerLoopTimingSignatures); + } + + activeRemovedSignaturesByAssemblyDirectory = nextRemovedSignaturesByAssemblyDirectory; + } + + return replacementCount; + } + + internal static bool CanMigrateBareLegacyPlayerLoopTimingForAssembly( + MigrationAssemblyUsage assemblyUsage, + string assemblyDirectory, + string source) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + Debug.Assert(source != null, "source must not be null"); + + return assemblyUsage.LegacyAssemblyDirectories.Contains(assemblyDirectory) || + assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + assemblyUsage.AssemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory) || + assemblyUsage.AssemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(source); + } + + internal static bool HasRemovedPlayerLoopTimingSignatures( + Dictionary> + removedSignaturesByAssemblyDirectory) + { + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + + foreach ( + KeyValuePair> + signaturesByAssemblyDirectory in removedSignaturesByAssemblyDirectory) + { + if (signaturesByAssemblyDirectory.Value.Count > 0) + { + return true; + } + } + + return false; + } + + internal static Dictionary> + CloneRemovedPlayerLoopTimingSignatures( + Dictionary> + removedSignaturesByAssemblyDirectory) + { + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + + Dictionary> clone = + new(StringComparer.Ordinal); + foreach ( + KeyValuePair> + signaturesByAssemblyDirectory in removedSignaturesByAssemblyDirectory) + { + clone.Add( + signaturesByAssemblyDirectory.Key, + new List( + signaturesByAssemblyDirectory.Value)); + } + + return clone; + } + + internal static List + GetAllRemovedPlayerLoopTimingSignatures( + Dictionary> + removedSignaturesByAssemblyDirectory) + { + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + + List signatures = new(); + foreach ( + KeyValuePair> + signaturesByAssemblyDirectory in removedSignaturesByAssemblyDirectory) + { + signatures.AddRange(signaturesByAssemblyDirectory.Value); + } + + return signatures; + } + + internal static List + GetReachableRemovedPlayerLoopTimingSignatures( + string assemblyDirectory, + Dictionary> + removedSignaturesByAssemblyDirectory, + Dictionary referencedAssemblyDirectoriesByDirectory) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + Debug.Assert( + referencedAssemblyDirectoriesByDirectory != null, + "referencedAssemblyDirectoriesByDirectory must not be null"); + + List signatures = new(); + AddRemovedPlayerLoopTimingSignaturesForDirectory( + signatures, + removedSignaturesByAssemblyDirectory, + assemblyDirectory); + if (!referencedAssemblyDirectoriesByDirectory.TryGetValue( + assemblyDirectory, + out string[] referencedAssemblyDirectories)) + { + return signatures; + } + + foreach (string referencedAssemblyDirectory in referencedAssemblyDirectories) + { + AddRemovedPlayerLoopTimingSignaturesForDirectory( + signatures, + removedSignaturesByAssemblyDirectory, + referencedAssemblyDirectory); + } + + return signatures; + } + + internal static void AddRemovedPlayerLoopTimingSignaturesForDirectory( + List targetSignatures, + Dictionary> + removedSignaturesByAssemblyDirectory, + string assemblyDirectory) + { + Debug.Assert(targetSignatures != null, "targetSignatures must not be null"); + Debug.Assert( + removedSignaturesByAssemblyDirectory != null, + "removedSignaturesByAssemblyDirectory must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + + if (!removedSignaturesByAssemblyDirectory.TryGetValue( + assemblyDirectory, + out List signatures)) + { + return; + } + + targetSignatures.AddRange(signatures); + } + + internal static string GetPendingMigrationFileContent( + string filePath, + List changes, + string fallbackContent) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(changes != null, "changes must not be null"); + Debug.Assert(fallbackContent != null, "fallbackContent must not be null"); + + foreach (MigrationFileChange change in changes) + { + if (string.Equals(change.FilePath, filePath, StringComparison.Ordinal)) + { + return change.Content; + } + } + + return fallbackContent; + } + + internal static string[] GetAssemblyScopedNameArray( + Dictionary namesByDirectory, + string assemblyDirectory) + { + Debug.Assert(namesByDirectory != null, "namesByDirectory must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + + if (!namesByDirectory.TryGetValue(assemblyDirectory, out string[] names)) + { + return Array.Empty(); + } + + return names; + } + + internal static void UpsertMigrationFileChange( + List changes, + string filePath, + string content) + { + Debug.Assert(changes != null, "changes must not be null"); + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(content != null, "content must not be null"); + + for (int index = 0; index < changes.Count; index++) + { + MigrationFileChange change = changes[index]; + if (!string.Equals(change.FilePath, filePath, StringComparison.Ordinal)) + { + continue; + } + + changes[index] = new MigrationFileChange(filePath, content); + return; + } + + changes.Add(new MigrationFileChange(filePath, content)); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs.meta new file mode 100644 index 0000000000..08ceadfa75 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50b513cc9d1be4be5aa6dc7da7c10185 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs new file mode 100644 index 0000000000..b2c3c27bf2 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationDelayRules + { + internal static (string Content, int ReplacementCount) ReplaceLegacyTimerDelayNamedArgumentsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyTimerDelay) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyTimerDelayInvocationRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyTimerDelayInvocationMatch(match, legacyNamespaceAliases, canMigrateBareLegacyTimerDelay)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + (string[] migratedArguments, bool changed) = + GetTimerDelayArgumentsWithMigratedCancellationTokenName(SplitAttributeArguments(argumentsSource)); + if (!changed) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(match.Value); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static bool IsLegacyTimerDelayInvocationMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyTimerDelay) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); + } + + return match.Groups["timerDelay"].Success && canMigrateBareLegacyTimerDelay; + } + + internal static (string[] Arguments, bool Changed) + GetTimerDelayArgumentsWithMigratedCancellationTokenName(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] migratedArguments = new string[arguments.Length]; + bool changed = false; + for (int i = 0; i < arguments.Length; i++) + { + string argument = arguments[i].Trim(); + string cancellationTokenValue = + GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (cancellationTokenValue == null) + { + migratedArguments[i] = argument; + continue; + } + + migratedArguments[i] = $"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"; + changed = true; + } + + return (migratedArguments, changed); + } + + internal static (string Content, int ReplacementCount) ReplaceLegacyMainThreadSwitcherCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentApplicationNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentApplicationNamespaceAliases != null, + "currentApplicationNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyMainThreadSwitcherSwitchRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyMainThreadSwitcherCallMatch( + source, + match, + legacyNamespaceAliases, + currentApplicationNamespaceAliases, + canMigrateBareLegacyMainThreadSwitcher, + assemblyDeclaredTypeNames)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + (string[] migratedArguments, bool changed) = + GetMigratedMainThreadSwitcherArguments( + SplitAttributeArguments(argumentsSource), + legacyNamespaceAliases); + if (!changed) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(match.Value); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static bool IsLegacyMainThreadSwitcherCallMatch( + string source, + Match match, + string[] legacyNamespaceAliases, + string[] currentApplicationNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentApplicationNamespaceAliases != null, + "currentApplicationNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["currentQualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + string alias = match.Groups["alias"].Value; + return legacyNamespaceAliases.Contains(alias) || + currentApplicationNamespaceAliases.Contains(alias); + } + + return match.Groups["mainThreadSwitcher"].Success && + canMigrateBareLegacyMainThreadSwitcher && + !DeclaresLocalType(source, LegacyMainThreadSwitcherTypeName) && + !assemblyDeclaredTypeNames.Contains(LegacyMainThreadSwitcherTypeName); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs.meta new file mode 100644 index 0000000000..06c4ffdbf1 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3d8a11b0bdc3486e8948f69b1ee381e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs new file mode 100644 index 0000000000..3d96fbc7a3 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationDetectionRules + { + internal static bool ContainsLegacyCSharpApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ContainsLegacyToolMigrationMarker(source); + } + + 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; + } + } + + foreach (TypeReplacementRule rule in DomainTypeReplacementRules) + { + if (ContainsTextFragment(source, rule.LegacyName) || + ContainsTextFragment(source, rule.CurrentName)) + { + return true; + } + } + + foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + if (ContainsTextFragment(source, rule.LegacyName) || + ContainsTextFragment(source, rule.CurrentName)) + { + return true; + } + } + + foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + if (ContainsTextFragment(source, rule.LegacyName) || + ContainsTextFragment(source, rule.CurrentName)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsLegacyMigrationCandidateText(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ContainsTextFragment(source, LegacyNamespace) || + ContainsTextFragment(source, LegacyEditorAssemblyName) || + ContainsTextFragment(source, LegacyRuntimeAssemblyName); + } + + internal static bool ContainsLegacyAsmdefNameReference(string source) + { + Debug.Assert(source != null, "source must not be null"); + + if (!ContainsTextFragment(source, LegacyEditorAssemblyName) && + !ContainsTextFragment(source, LegacyRuntimeAssemblyName)) + { + return false; + } + + JObject asmdef = JObject.Parse(source); + if (asmdef["references"] is not JArray references) + { + return false; + } + + foreach (JToken reference in references) + { + string referenceValue = reference.Value() ?? string.Empty; + if (string.Equals(referenceValue, LegacyEditorAssemblyName, StringComparison.Ordinal) || + string.Equals(referenceValue, LegacyRuntimeAssemblyName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + return RegexMatchesCode(source, LegacyBaseTypeUsageRegex) || + RegexMatchesCode(source, LegacyAssemblyScopedApiUsageRegex) || + ContainsLegacyAssemblyScopedTypeReference(source) || + ContainsLegacyAliasQualifiedAssemblyScopedApi(source, legacyAssemblyAliases) || + ContainsLegacyEditorDelayFrameCall( + source, + legacyAssemblyAliases, + canMigrateBareLegacyEditorDelay: true) || + ContainsLegacyEditorWindowCaptureUtilityCall( + source, + legacyAssemblyAliases, + Array.Empty(), + canMigrateBareLegacyEditorWindowCaptureUtility: true, + GetDeclaredTypeNames(source)) || + ContainsLegacyTimerDelayInvocation( + source, + legacyAssemblyAliases, + canMigrateBareLegacyTimerDelay: true) || + ContainsLegacyApplicationReference( + source, + true, + legacyAssemblyAliases, + Array.Empty(), + GetDeclaredTypeNames(source)) || + ContainsLegacyFirstPartyScreenshotReference( + source, + true, + legacyAssemblyAliases, + Array.Empty()) || + ContainsLegacyToolAttributeList( + source, + legacyAssemblyAliases, + canMigrateBareLegacyToolAttribute: true); + } + + internal static bool ContainsLegacyGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, LegacyGlobalUsingRegex); + } + + internal static bool ContainsCurrentDomainGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); + } + + internal static bool ContainsCurrentToolContractsGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentToolContractsGlobalUsingRegex); + } + + internal static bool ContainsCurrentApplicationGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentApplicationGlobalUsingRegex); + } + + internal static bool ContainsCurrentApplicationNamespaceAlias(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentApplicationNamespaceAliasRegex); + } + + internal static bool ContainsCurrentFirstPartyToolsGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentFirstPartyToolsGlobalUsingRegex); + } + + internal static bool ContainsCurrentFirstPartyToolsNamespaceAlias(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceAliasRegex); + } + + internal static string[] GetLegacyGlobalNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, LegacyGlobalNamespaceAliasRegex, "alias"); + } + + internal static string[] GetLegacyGlobalToolInfoTypeAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, LegacyGlobalToolInfoTypeAliasRegex, "alias"); + } + + internal static string[] GetCurrentApplicationGlobalNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, CurrentApplicationGlobalNamespaceAliasRegex, "alias"); + } + + internal static string[] GetCurrentFirstPartyToolsGlobalNamespaceAliases(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return GetRegexGroupValuesInCode(source, CurrentFirstPartyToolsGlobalNamespaceAliasRegex, "alias"); + } + + internal static string[] GetDeclaredTypeNames(string source) + { + Debug.Assert(source != null, "source must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + HashSet typeNames = new(StringComparer.Ordinal); + MatchCollection matches = TypeDeclarationNameRegex.Matches(source); + foreach (Match match in matches) + { + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + typeNames.Add(match.Groups["name"].Value); + } + + return typeNames + .OrderBy(typeName => typeName, StringComparer.Ordinal) + .ToArray(); + } + + internal static bool ContainsLegacyGlobalToolInfoTypeAlias(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, LegacyGlobalToolInfoTypeAliasRegex); + } + + internal static bool ContainsLegacyTypeAliasReference(string source, string[] aliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (string alias in aliases) + { + Regex aliasRegex = new($@"(? string.Equals( + excludedDirectoryName, + directoryName, + StringComparison.OrdinalIgnoreCase)); + } + + internal static string[] GetExcludedDirectoryNames() + { + string[] names = new string[ExcludedDirectoryNames.Length]; + Array.Copy(ExcludedDirectoryNames, names, ExcludedDirectoryNames.Length); + return names; + } + + internal static bool RegexMatchesCode(string source, Regex regex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(regex != null, "regex must not be null"); + + MatchCollection matches = regex.Matches(source); + if (matches.Count == 0) + { + return false; + } + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index)) + { + return true; + } + } + + return false; + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs.meta new file mode 100644 index 0000000000..1fdfd94044 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2829290fdf8004610851ac75ec83cc3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDomainDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDomainDetectionRules.cs new file mode 100644 index 0000000000..ddfe8c9491 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDomainDetectionRules.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationDomainDetectionRules + { + internal static bool ContainsLegacyAliasQualifiedName(string source, string alias, string typeName) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(alias), "alias must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be null or empty"); + + Regex aliasQualifiedRegex = new( + $@"(?())); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(GetMigratedEditorFrameWaiterInvocationTarget( + match, + shouldQualifyBareToolContractsReferences)); + builder.Append('.'); + builder.Append(CurrentEditorFrameWaiterMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static bool IsLegacyEditorDelayFrameCallMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorDelay) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); + } + + return match.Groups["editorDelay"].Success && canMigrateBareLegacyEditorDelay; + } + + internal static string GetMigratedEditorFrameWaiterInvocationTarget( + Match match, + bool shouldQualifyBareToolContractsReferences) + { + Debug.Assert(match != null, "match must not be null"); + + if (match.Groups["qualifier"].Success) + { + return $"{CurrentNamespace}.{CurrentEditorFrameWaiterTypeName}"; + } + + if (match.Groups["alias"].Success) + { + return $"{match.Groups["alias"].Value}.{CurrentEditorFrameWaiterTypeName}"; + } + + if (shouldQualifyBareToolContractsReferences) + { + return $"{CurrentNamespace}.{CurrentEditorFrameWaiterTypeName}"; + } + + return CurrentEditorFrameWaiterTypeName; + } + + internal static string GetMigratedEditorFrameWaitTimeoutExpression( + Match match, + bool shouldQualifyBareToolContractsReferences, + string[] currentFirstPartyToolsNamespaceAliases) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + if (match.Groups["currentQualifier"].Success) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + if (match.Groups["alias"].Success) + { + string alias = match.Groups["alias"].Value; + if (currentFirstPartyToolsNamespaceAliases.Contains(alias)) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + return $"{alias}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + if (shouldQualifyBareToolContractsReferences) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + return $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + internal static string GetMigratedEditorWindowCaptureUtilityTimeoutExpression( + Match match, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + string[] currentFirstPartyToolsNamespaceAliases) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + if (shouldQualifyBareEditorWindowCaptureUtilityTimeout && + match.Groups["editorWindowCaptureUtility"].Success) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + return GetMigratedEditorFrameWaitTimeoutExpression( + match, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + currentFirstPartyToolsNamespaceAliases); + } + + internal static string[] GetMigratedEditorDelayFrameArguments( + string[] arguments, + string timeoutExpression) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(!string.IsNullOrEmpty(timeoutExpression), "timeoutExpression must not be null or empty"); + + string[] trimmedArguments = arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + if (trimmedArguments.Length > 2) + { + return Array.Empty(); + } + + string frameCountArgument = "1"; + string cancellationTokenArgument = null; + bool hasFrameCountArgument = false; + bool hasCancellationTokenArgument = false; + for (int i = 0; i < trimmedArguments.Length; i++) + { + string argument = trimmedArguments[i]; + string namedFrameCountValue = GetNamedArgumentValueOrNull(argument, "frameCount"); + if (namedFrameCountValue != null) + { + if (hasFrameCountArgument) + { + return Array.Empty(); + } + + frameCountArgument = namedFrameCountValue; + hasFrameCountArgument = true; + continue; + } + + string namedCancellationTokenValue = GetNamedArgumentValueOrNull(argument, "cancellationToken"); + if (namedCancellationTokenValue != null) + { + if (hasCancellationTokenArgument) + { + return Array.Empty(); + } + + cancellationTokenArgument = namedCancellationTokenValue; + hasCancellationTokenArgument = true; + continue; + } + + if (!hasFrameCountArgument) + { + frameCountArgument = argument; + hasFrameCountArgument = true; + continue; + } + + if (!hasCancellationTokenArgument) + { + cancellationTokenArgument = argument; + hasCancellationTokenArgument = true; + continue; + } + + return Array.Empty(); + } + + List migratedArguments = new() + { + frameCountArgument, + timeoutExpression + }; + if (cancellationTokenArgument != null) + { + migratedArguments.Add(cancellationTokenArgument); + } + + return migratedArguments.ToArray(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationEditorDelayRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationEditorDelayRules.cs.meta new file mode 100644 index 0000000000..cfc1095fe5 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationEditorDelayRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1202ac271b37c4ecab88dee87bd2e26c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs new file mode 100644 index 0000000000..6207cae749 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +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 +{ + /// + /// Collects assembly reference requirements during fast migration-target scans. + /// + internal static class ThirdPartyToolMigrationFastAssemblyRequirementCollector + { + internal static void CollectFastAssemblyReferenceRequirements( + string source, + string assemblyDirectory, + bool hasAssemblyScopedCurrentToolContractsNamespaceUsage, + bool hasAssemblyScopedCurrentDomainNamespaceUsage, + bool hasAssemblyScopedCurrentApplicationNamespaceUsage, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames, + HashSet legacyAssemblyDirectories, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet applicationReferenceAssemblyDirectories, + HashSet domainReferenceAssemblyDirectories) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); + Debug.Assert( + currentApplicationAssemblyAliases != null, + "currentApplicationAssemblyAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + Debug.Assert(legacyAssemblyDirectories != null, "legacyAssemblyDirectories 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"); + + if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) + { + legacyAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) + { + toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory) || + hasAssemblyScopedCurrentToolContractsNamespaceUsage || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty(), + currentApplicationAssemblyAliases, + assemblyDeclaredTypeNames) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( + source, + hasAssemblyScopedCurrentApplicationNamespaceUsage, + currentApplicationAssemblyAliases, + assemblyDeclaredTypeNames)) + { + applicationReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( + source, + hasAssemblyScopedCurrentDomainNamespaceUsage)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + } + + internal static async Task CollectFastAssemblyReferenceRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentDomainDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, + Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + HashSet legacyAssemblyDirectories, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet applicationReferenceAssemblyDirectories, + HashSet domainReferenceAssemblyDirectories, + CancellationToken ct) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "assemblyReferenceDirectories must not be null"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert( + assemblyScopedCurrentToolContractsDirectories != null, + "assemblyScopedCurrentToolContractsDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentDomainDirectories != null, + "assemblyScopedCurrentDomainDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationDirectories != null, + "assemblyScopedCurrentApplicationDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationAliasesByDirectory != null, + "assemblyScopedCurrentApplicationAliasesByDirectory must not be null"); + Debug.Assert( + assemblyDeclaredTypeNamesByDirectory != null, + "assemblyDeclaredTypeNamesByDirectory must not be null"); + Debug.Assert(legacyAssemblyDirectories != null, "legacyAssemblyDirectories 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"); + + int inspectedEntryCount = 0; + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return; + } + + string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + CollectFastAssemblyReferenceRequirements( + source, + assemblyDirectory, + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories); + + } + } + + internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentDomainDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, + Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + HashSet legacyAssemblyDirectories, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet applicationReferenceAssemblyDirectories, + HashSet domainReferenceAssemblyDirectories, + CancellationToken ct) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "assemblyReferenceDirectories must not be null"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert( + assemblyScopedCurrentToolContractsDirectories != null, + "assemblyScopedCurrentToolContractsDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentDomainDirectories != null, + "assemblyScopedCurrentDomainDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationDirectories != null, + "assemblyScopedCurrentApplicationDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationAliasesByDirectory != null, + "assemblyScopedCurrentApplicationAliasesByDirectory must not be null"); + Debug.Assert( + assemblyDeclaredTypeNamesByDirectory != null, + "assemblyDeclaredTypeNamesByDirectory must not be null"); + + int inspectedEntryCount = 0; + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return; + } + + string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + if (!assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory)) + { + continue; + } + + CollectFastAssemblyReferenceRequirements( + source, + assemblyDirectory, + true, + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories); + + } + } + + internal static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet assemblyScopedCurrentDomainDirectories, + HashSet domainReferenceAssemblyDirectories, + CancellationToken ct) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "assemblyReferenceDirectories must not be null"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert( + assemblyScopedCurrentDomainDirectories != null, + "assemblyScopedCurrentDomainDirectories must not be null"); + Debug.Assert(domainReferenceAssemblyDirectories != null, "domainReferenceAssemblyDirectories must not be null"); + + int inspectedEntryCount = 0; + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return; + } + + string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + if (assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly(source, true)) + { + domainReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + } + } + + internal static async Task CollectFastFirstPartyScreenshotRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet firstPartyScreenshotReferenceAssemblyDirectories, + CancellationToken ct) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert( + assemblyReferenceDirectories != null, + "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( + assemblyScopedCurrentToolContractsDirectories != null, + "assemblyScopedCurrentToolContractsDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentFirstPartyToolsDirectories != null, + "assemblyScopedCurrentFirstPartyToolsDirectories 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( + firstPartyScreenshotReferenceAssemblyDirectories != null, + "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); + + int inspectedEntryCount = 0; + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return false; + } + + string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + 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 hasCurrentRenderingCaptureSourceTarget = + ThirdPartyToolMigrationRules.ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + if (hasLegacyScreenshotSourceTarget || + 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); + } + + if (hasLegacyScreenshotSourceTarget || hasCurrentRenderingCaptureSourceTarget) + { + return true; + } + + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs.meta new file mode 100644 index 0000000000..a6ba1aaf95 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0ebf0ee8e45c4a52a03a4e8eef32ca9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs new file mode 100644 index 0000000000..e9b4b5c8c6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +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 +{ + /// + /// Performs fast source-level checks before building a complete migration plan. + /// + internal static class ThirdPartyToolMigrationFastSourceTargetDetector + { + internal static bool ContainsFastCSharpMigrationTarget(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source) && + ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source); + } + + internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedLegacyDirectories, + Dictionary> assemblyScopedLegacyAliasesByDirectory, + Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, + Dictionary> assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + Dictionary> assemblyDeclaredTypeNamesByDirectory, + CancellationToken ct) + { + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); + Debug.Assert(assemblyReferenceDirectories != null, "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( + assemblyScopedLegacyDirectories != null, + "assemblyScopedLegacyDirectories must not be null"); + Debug.Assert( + assemblyScopedLegacyAliasesByDirectory != null, + "assemblyScopedLegacyAliasesByDirectory must not be null"); + Debug.Assert( + assemblyScopedLegacyToolInfoAliasesByDirectory != null, + "assemblyScopedLegacyToolInfoAliasesByDirectory must not be null"); + Debug.Assert( + assemblyScopedCurrentToolContractsDirectories != null, + "assemblyScopedCurrentToolContractsDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationDirectories != null, + "assemblyScopedCurrentApplicationDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentFirstPartyToolsDirectories != null, + "assemblyScopedCurrentFirstPartyToolsDirectories must not be null"); + Debug.Assert( + assemblyScopedCurrentApplicationAliasesByDirectory != null, + "assemblyScopedCurrentApplicationAliasesByDirectory must not be null"); + Debug.Assert( + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory != null, + "assemblyScopedCurrentFirstPartyToolsAliasesByDirectory must not be null"); + Debug.Assert( + assemblyDeclaredTypeNamesByDirectory != null, + "assemblyDeclaredTypeNamesByDirectory must not be null"); + + int inspectedEntryCount = 0; + foreach (string csharpFilePath in csharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return false; + } + + string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + string[] assemblyDeclaredTypeNames = + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); + string[] legacyAssemblyAliases = + GetAssemblyScopedNames(assemblyScopedLegacyAliasesByDirectory, assemblyDirectory); + string[] legacyAssemblyToolInfoAliases = + GetAssemblyScopedNames(assemblyScopedLegacyToolInfoAliasesByDirectory, assemblyDirectory); + bool hasLegacyAssemblySource = + (legacyAssemblyDirectories.Contains(assemblyDirectory) || + assemblyScopedLegacyDirectories.Contains(assemblyDirectory)) && + ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi( + source, + legacyAssemblyAliases); + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases, + legacyAssemblyToolInfoAliases, + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames( + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDirectory), + assemblyDeclaredTypeNames); + if (result.Changed) + { + return true; + } + } + + return false; + } + + internal static bool ContainsFastAsmdefMigrationTarget(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return ThirdPartyToolMigrationRules.ContainsLegacyAsmdefNameReference(source); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs.meta new file mode 100644 index 0000000000..db0b63fa64 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da4942a83b20045539ecc5676762d3f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs index 459ec61842..91268f724a 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs @@ -1,66 +1,20 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using io.github.hatayama.UnityCliLoop.Domain; namespace io.github.hatayama.UnityCliLoop.Infrastructure { - /// - /// Caches source text within one preview pass so analysis phases share a single disk read per file. - /// - internal sealed class ThirdPartyToolMigrationSourceFileCache - { - private readonly Func _readAllText; - private readonly Dictionary _sources = new(StringComparer.Ordinal); - - internal ThirdPartyToolMigrationSourceFileCache() - : this(File.ReadAllText) - { - } - - internal ThirdPartyToolMigrationSourceFileCache(Func readAllText) - { - Debug.Assert(readAllText != null, "readAllText must not be null"); - - _readAllText = readAllText ?? throw new ArgumentNullException(nameof(readAllText)); - } - - internal string ReadAllText(string filePath) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - - if (_sources.TryGetValue(filePath, out string source)) - { - return source; - } - - string loadedSource = _readAllText(filePath); - _sources.Add(filePath, loadedSource); - return loadedSource; - } - } - /// /// Scans Unity project files and rewrites V2 custom tool source to the V3 public contract API. /// public sealed class ThirdPartyToolMigrationFileService : IThirdPartyToolMigrationPort { - private const string ImplicitEditorAssemblyDirectoryName = "__UnityCliLoopImplicitEditorAssembly"; - private const string ImplicitRuntimeAssemblyDirectoryName = "__UnityCliLoopImplicitRuntimeAssembly"; - private const string ImplicitFirstPassEditorAssemblyDirectoryName = - "__UnityCliLoopImplicitFirstPassEditorAssembly"; - private const string ImplicitFirstPassRuntimeAssemblyDirectoryName = - "__UnityCliLoopImplicitFirstPassRuntimeAssembly"; - private const int PreviewYieldBatchSize = 32; - private readonly object _previewCacheLock = new(); private bool _hasCachedPreview; private string _cachedPreviewProjectRoot = string.Empty; @@ -76,7 +30,7 @@ public ThirdPartyToolMigrationPreview PreviewMigration(string projectRoot) return cachedPreview; } - MigrationPlan plan = CreateMigrationPlan(normalizedProjectRoot); + MigrationPlan plan = ThirdPartyToolMigrationPlanBuilder.Create(normalizedProjectRoot); ThirdPartyToolMigrationPreview preview = new( plan.ChangedFilePaths.Count, plan.ReplacementCount, @@ -94,7 +48,7 @@ public async Task PreviewMigrationAsync( Debug.Assert(progress != null, "progress must not be null"); string normalizedProjectRoot = NormalizeProjectRoot(projectRoot); - MigrationPlan plan = await CreateMigrationPlanAsync(normalizedProjectRoot, progress, ct); + MigrationPlan plan = await ThirdPartyToolMigrationPlanBuilder.CreateAsync(normalizedProjectRoot, progress, ct); if (ct.IsCancellationRequested) { return new ThirdPartyToolMigrationPreview(0, 0, Array.Empty()); @@ -123,7 +77,7 @@ public async Task HasMigrationTargetsAsync(string projectRoot, Cancellatio return false; } - return await HasMigrationTargetAsync(normalizedProjectRoot, ct); + return await ThirdPartyToolMigrationTargetScanner.HasMigrationTargetAsync(normalizedProjectRoot, ct); } public ThirdPartyToolMigrationResult ApplyMigration(string projectRoot) @@ -132,10 +86,10 @@ public ThirdPartyToolMigrationResult ApplyMigration(string projectRoot) string normalizedProjectRoot = NormalizeProjectRoot(projectRoot); InvalidatePreviewCache(); - MigrationPlan plan = CreateMigrationPlan(normalizedProjectRoot); + MigrationPlan plan = ThirdPartyToolMigrationPlanBuilder.Create(normalizedProjectRoot); foreach (MigrationFileChange change in plan.Changes) { - WriteMigrationFile(change.FilePath, change.Content); + ThirdPartyToolMigrationFileWriter.Write(change.FilePath, change.Content); } return new ThirdPartyToolMigrationResult( @@ -144,343 +98,58 @@ public ThirdPartyToolMigrationResult ApplyMigration(string projectRoot) plan.ChangedFilePaths.ToArray()); } - internal void InvalidatePreviewCache() - { - lock (_previewCacheLock) - { - _hasCachedPreview = false; - _cachedPreviewProjectRoot = string.Empty; - _cachedPreview = default; - } - } - - private static async Task HasMigrationTargetAsync(string projectRoot, CancellationToken ct) + public async Task ApplyMigrationAsync( + string projectRoot, + IProgress progress, + CancellationToken ct) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(progress != null, "progress must not be null"); - ProjectFileInventory inventory = await ProjectFileInventory.CreateAsync( - projectRoot, - new Progress(), - ct); + string normalizedProjectRoot = NormalizeProjectRoot(projectRoot); + InvalidatePreviewCache(); + MigrationPlan plan = await ThirdPartyToolMigrationPlanBuilder.CreateAsync(normalizedProjectRoot, progress, ct); + // A canceled operation must not start mutating files, but an active write batch must finish as one plan. if (ct.IsCancellationRequested) { - return false; - } - - List asmdefDirectories = inventory.AsmdefFilePaths - .Select(path => Path.GetDirectoryName(path) ?? string.Empty) - .Where(path => !string.IsNullOrEmpty(path)) - .OrderByDescending(path => path.Length) - .ToList(); - List assemblyReferenceDirectories = inventory.AsmrefFilePaths.Count == 0 - ? new List() - : CreateAssemblyReferenceDirectories(inventory.AsmdefFilePaths, inventory.AsmrefFilePaths); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); - int inspectedEntryCount = 0; - foreach (string csharpFilePath in inventory.CSharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return false; - } - - string source = File.ReadAllText(csharpFilePath); - if (ContainsFastCSharpMigrationTarget(source)) - { - return true; - } - - if (ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainGlobalUsing(source)) - { - assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); - } - - CollectFastAssemblyReferenceRequirements( - source, - assemblyDirectory, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), - legacyAssemblyDirectories, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } - } - - if (assemblyScopedCurrentDomainDirectories.Count > 0) - { - await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( - inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot, - assemblyScopedCurrentDomainDirectories, - domainReferenceAssemblyDirectories, - ct); - if (ct.IsCancellationRequested) - { - return false; - } + return new ThirdPartyToolMigrationResult(0, 0, Array.Empty()); } - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) + for (int index = 0; index < plan.Changes.Count; index++) { - if (ct.IsCancellationRequested) - { - return false; - } - - string source = File.ReadAllText(asmdefFilePath); - if (ContainsFastAsmdefMigrationTarget(source)) - { - return true; - } - - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) + MigrationFileChange change = plan.Changes[index]; + ThirdPartyToolMigrationFileWriter.Write(change.FilePath, change.Content); + if ((index + 1) % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) { await Task.Yield(); } } - if (toolContractsReferenceAssemblyDirectories.Count == 0 && - applicationReferenceAssemblyDirectories.Count == 0 && - domainReferenceAssemblyDirectories.Count == 0) - { - return false; - } - - MigrationAssemblyUsage assemblyUsage = new( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - new HashSet(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) - { - if (ct.IsCancellationRequested) - { - return false; - } - - if (ContainsAsmdefMigrationTarget(asmdefFilePath, projectRoot, assemblyUsage)) - { - return true; - } - } - - return false; - } - - private static void CollectFastAssemblyReferenceRequirements( - string source, - string assemblyDirectory, - bool hasAssemblyScopedCurrentDomainNamespaceUsage, - HashSet legacyAssemblyDirectories, - HashSet toolContractsReferenceAssemblyDirectories, - HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); - Debug.Assert(legacyAssemblyDirectories != null, "legacyAssemblyDirectories 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"); - - if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) - { - legacyAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - hasAssemblyScopedCurrentDomainNamespaceUsage)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - private static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsync( - List csharpFilePaths, - List asmdefDirectories, - List assemblyReferenceDirectories, - string projectRoot, - HashSet assemblyScopedCurrentDomainDirectories, - HashSet domainReferenceAssemblyDirectories, - CancellationToken ct) - { - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); - Debug.Assert( - assemblyReferenceDirectories != null, - "assemblyReferenceDirectories must not be null"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert( - assemblyScopedCurrentDomainDirectories != null, - "assemblyScopedCurrentDomainDirectories must not be null"); - Debug.Assert(domainReferenceAssemblyDirectories != null, "domainReferenceAssemblyDirectories must not be null"); - - int inspectedEntryCount = 0; - foreach (string csharpFilePath in csharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return; - } - - string source = File.ReadAllText(csharpFilePath); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - if (assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly(source, true)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } - } + return new ThirdPartyToolMigrationResult( + plan.ChangedFilePaths.Count, + plan.ReplacementCount, + plan.ChangedFilePaths.ToArray()); } - private static bool ContainsAsmdefMigrationTarget( - string asmdefFilePath, - string projectRoot, - MigrationAssemblyUsage assemblyUsage) + internal void InvalidatePreviewCache() { - Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - bool hasLegacyCSharpSource; - bool requiresToolContractsReference; - bool requiresApplicationReference; - bool requiresDomainReference; - bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( - asmdefFilePath, - projectRoot, - assemblyUsage, - out hasLegacyCSharpSource, - out requiresToolContractsReference, - out requiresApplicationReference, - out requiresDomainReference); - string source = File.ReadAllText(asmdefFilePath); - if (!hasAssemblyMigrationRequirement && - !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) + lock (_previewCacheLock) { - return false; + _hasCachedPreview = false; + _cachedPreviewProjectRoot = string.Empty; + _cachedPreview = default; } - - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateAsmdefSource( - source, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference); - - return result.Changed; - } - - private static bool TryGetAsmdefMigrationRequirements( - string asmdefFilePath, - string projectRoot, - MigrationAssemblyUsage assemblyUsage, - out bool hasLegacyCSharpSource, - out bool requiresToolContractsReference, - out bool requiresApplicationReference, - out bool requiresDomainReference) - { - Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? projectRoot; - hasLegacyCSharpSource = assemblyUsage.LegacyAssemblyDirectories.Contains(asmdefDirectory); - requiresToolContractsReference = - assemblyUsage.ToolContractsReferenceAssemblyDirectories.Contains(asmdefDirectory); - requiresApplicationReference = - assemblyUsage.ApplicationReferenceAssemblyDirectories.Contains(asmdefDirectory); - requiresDomainReference = - assemblyUsage.DomainReferenceAssemblyDirectories.Contains(asmdefDirectory); - return hasLegacyCSharpSource || - requiresToolContractsReference || - requiresApplicationReference || - requiresDomainReference; - } - - private static bool ContainsFastCSharpMigrationTarget(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source) && - ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source); - } - - private static bool ContainsFastAsmdefMigrationTarget(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return ThirdPartyToolMigrationRules.ContainsLegacyAsmdefNameReference(source); } - private static bool ShouldExcludeFastScanDirectory(string directoryPath) + internal static bool TryReadJsonObjectForMigration( + string filePath, + Func readAllText, + out JObject jsonObject) { - Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); - - string directoryName = Path.GetFileName(directoryPath); - return ThirdPartyToolMigrationRules.IsExcludedDirectoryName(directoryName); + return ThirdPartyToolMigrationAssemblyReferenceResolver.TryReadJsonObjectForMigration( + filePath, + readAllText, + out jsonObject); } private bool TryGetCachedPreview( @@ -515,1613 +184,12 @@ private void StoreCachedPreview(string projectRoot, ThirdPartyToolMigrationPrevi } } - private static MigrationPlan CreateMigrationPlan(string projectRoot) - { - if (!Directory.Exists(projectRoot)) - { - throw new DirectoryNotFoundException(projectRoot); - } - - ProjectFileInventory inventory = ProjectFileInventory.Create(projectRoot); - MigrationAssemblyUsage assemblyUsage = FindMigrationAssemblyUsage( - projectRoot, - inventory.CSharpFilePaths, - inventory.AsmdefFilePaths, - inventory.AsmrefFilePaths); - List changes = new(); - int replacementCount = 0; - string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); - - foreach (string csharpFilePath in inventory.CSharpFilePaths) - { - string source = File.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(); - } - bool hasLegacyAssemblySource = - assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( - source, - hasLegacyAssemblySource, - legacyAssemblyAliases, - legacyAssemblyToolInfoAliases); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); - } - - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) - { - bool hasLegacyCSharpSource; - bool requiresToolContractsReference; - bool requiresApplicationReference; - bool requiresDomainReference; - bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( - asmdefFilePath, - projectRoot, - assemblyUsage, - out hasLegacyCSharpSource, - out requiresToolContractsReference, - out requiresApplicationReference, - out requiresDomainReference); - string source = File.ReadAllText(asmdefFilePath); - if (!hasAssemblyMigrationRequirement && - !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) - { - continue; - } - - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateAsmdefSource( - source, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); - } - - return new MigrationPlan(changes, replacementCount); - } - - private static async Task CreateMigrationPlanAsync( - string projectRoot, - IProgress progress, - CancellationToken ct) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(progress != null, "progress must not be null"); - - if (!Directory.Exists(projectRoot)) - { - throw new DirectoryNotFoundException(projectRoot); - } - - ProjectFileInventory inventory = await ProjectFileInventory.CreateAsync(projectRoot, progress, ct); - if (ct.IsCancellationRequested) - { - return MigrationPlan.Empty; - } - - MigrationProgressCounter progressCounter = new(GetPreviewWorkItemCount(inventory), progress); - ThirdPartyToolMigrationSourceFileCache sourceFileCache = new(); - MigrationAssemblyUsage assemblyUsage = await FindMigrationAssemblyUsageAsync( - projectRoot, - inventory.CSharpFilePaths, - inventory.AsmdefFilePaths, - inventory.AsmrefFilePaths, - sourceFileCache, - progressCounter, - ct); - if (ct.IsCancellationRequested) - { - return MigrationPlan.Empty; - } - - List changes = new(); - int replacementCount = 0; - string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); - - foreach (string csharpFilePath in inventory.CSharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return MigrationPlan.Empty; - } - - string source = sourceFileCache.ReadAllText(csharpFilePath); - await progressCounter.ReportProcessedItemAsync(ct); - 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(); - } - bool hasLegacyAssemblySource = - assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( - source, - hasLegacyAssemblySource, - legacyAssemblyAliases, - legacyAssemblyToolInfoAliases); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); - } - - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) - { - if (ct.IsCancellationRequested) - { - return MigrationPlan.Empty; - } - - bool hasLegacyCSharpSource; - bool requiresToolContractsReference; - bool requiresApplicationReference; - bool requiresDomainReference; - bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( - asmdefFilePath, - projectRoot, - assemblyUsage, - out hasLegacyCSharpSource, - out requiresToolContractsReference, - out requiresApplicationReference, - out requiresDomainReference); - string source = sourceFileCache.ReadAllText(asmdefFilePath); - await progressCounter.ReportProcessedItemAsync(ct); - if (!hasAssemblyMigrationRequirement && - !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) - { - continue; - } - - ThirdPartyToolMigrationContentResult result = - ThirdPartyToolMigrationRules.MigrateAsmdefSource( - source, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference); - if (!result.Changed) - { - continue; - } - - replacementCount += result.ReplacementCount; - changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); - } - - progressCounter.ReportComplete(); - return new MigrationPlan(changes, replacementCount); - } - - private static int GetPreviewWorkItemCount(ProjectFileInventory inventory) - { - Debug.Assert(inventory != null, "inventory must not be null"); - - return (inventory.CSharpFilePaths.Count * 3) + - (inventory.AsmdefFilePaths.Count * 2) + - inventory.AsmrefFilePaths.Count; - } - - private static void WriteMigrationFile(string filePath, string content) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(content != null, "content must not be null"); - - string tempFilePath = CreateUniqueSidecarPath(filePath, ".tmp"); - File.WriteAllText(tempFilePath, content); - if (!File.Exists(filePath)) - { - File.Move(tempFilePath, filePath); - return; - } - - string backupFilePath = CreateUniqueSidecarPath(filePath, ".bak"); - File.Replace(tempFilePath, filePath, backupFilePath); - File.Delete(backupFilePath); - } - - private static string CreateUniqueSidecarPath(string filePath, string extension) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(extension), "extension must not be null or empty"); - - string directory = Path.GetDirectoryName(filePath) ?? string.Empty; - string fileName = Path.GetFileName(filePath); - string sidecarPath = Path.Combine(directory, $"{fileName}.{Guid.NewGuid():N}{extension}"); - while (File.Exists(sidecarPath)) - { - sidecarPath = Path.Combine(directory, $"{fileName}.{Guid.NewGuid():N}{extension}"); - } - - return sidecarPath; - } - - private static string NormalizeProjectRoot(string projectRoot) + internal static string NormalizeProjectRoot(string projectRoot) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); return Path.GetFullPath(projectRoot) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } - - private static MigrationAssemblyUsage FindMigrationAssemblyUsage( - string projectRoot, - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - - List asmdefDirectories = asmdefFilePaths - .Select(path => Path.GetDirectoryName(path) ?? string.Empty) - .Where(path => !string.IsNullOrEmpty(path)) - .OrderByDescending(path => path.Length) - .ToList(); - List assemblyReferenceDirectories = - CreateAssemblyReferenceDirectories(asmdefFilePaths, asmrefFilePaths); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = - 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); - - foreach (string csharpFilePath in csharpFilePaths) - { - string source = File.ReadAllText(csharpFilePath); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - - 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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string csharpFilePath in csharpFilePaths) - { - string source = File.ReadAllText(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(); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainHelperApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory))) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) - { - applicationReferenceAssemblyDirectories.Add(registrarAssemblyDirectory); - } - - foreach (string domainMetadataAssemblyDirectory in domainMetadataAssemblyDirectories) - { - if (legacyAssemblyDirectories.Contains(domainMetadataAssemblyDirectory)) - { - domainReferenceAssemblyDirectories.Add(domainMetadataAssemblyDirectory); - } - } - - return new MigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - private static async Task FindMigrationAssemblyUsageAsync( - string projectRoot, - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths, - ThirdPartyToolMigrationSourceFileCache sourceFileCache, - MigrationProgressCounter progressCounter, - CancellationToken ct) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); - Debug.Assert(progressCounter != null, "progressCounter must not be null"); - - List asmdefDirectories = asmdefFilePaths - .Select(path => Path.GetDirectoryName(path) ?? string.Empty) - .Where(path => !string.IsNullOrEmpty(path)) - .OrderByDescending(path => path.Length) - .ToList(); - List assemblyReferenceDirectories = - await CreateAssemblyReferenceDirectoriesAsync( - asmdefFilePaths, - asmrefFilePaths, - sourceFileCache, - progressCounter, - ct); - HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyAliasesByDirectory = - new(StringComparer.Ordinal); - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = - 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); - - foreach (string csharpFilePath in csharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - string source = sourceFileCache.ReadAllText(csharpFilePath); - await progressCounter.ReportProcessedItemAsync(ct); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - - 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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source)) - { - toolContractsReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainMetadataApi(source)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string csharpFilePath in csharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - 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(); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyDomainHelperApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainMetadataAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - registrarAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory), - legacyAssemblyAliases)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory))) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) - { - applicationReferenceAssemblyDirectories.Add(registrarAssemblyDirectory); - } - - foreach (string domainMetadataAssemblyDirectory in domainMetadataAssemblyDirectories) - { - if (legacyAssemblyDirectories.Contains(domainMetadataAssemblyDirectory)) - { - domainReferenceAssemblyDirectories.Add(domainMetadataAssemblyDirectory); - } - } - - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( - List asmdefDirectories, - List assemblyReferenceDirectories, - HashSet legacyAssemblyDirectories, - HashSet assemblyScopedLegacyDirectories, - Dictionary> assemblyScopedLegacyAliasesByDirectory, - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, - HashSet toolContractsReferenceAssemblyDirectories, - HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) - { - 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( - assemblyScopedLegacyAliasesByDirectory != null, - "assemblyScopedLegacyAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedLegacyToolInfoAliasesByDirectory != null, - "assemblyScopedLegacyToolInfoAliasesByDirectory 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"); - - return new MigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); - } - - private static void AddAssemblyScopedLegacyAliases( - Dictionary> aliasesByDirectory, - string assemblyDirectory, - string[] aliases) - { - Debug.Assert(aliasesByDirectory != null, "aliasesByDirectory must not be null"); - Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); - Debug.Assert(aliases != null, "aliases must not be null"); - - if (aliases.Length == 0) - { - return; - } - - if (!aliasesByDirectory.TryGetValue(assemblyDirectory, out HashSet aliasSet)) - { - aliasSet = new HashSet(StringComparer.Ordinal); - aliasesByDirectory.Add(assemblyDirectory, aliasSet); - } - - foreach (string alias in aliases) - { - aliasSet.Add(alias); - } - } - - private static Dictionary CreateAssemblyScopedLegacyAliasesByDirectory( - Dictionary> aliasesByDirectory) - { - Debug.Assert(aliasesByDirectory != null, "aliasesByDirectory must not be null"); - - Dictionary result = new(StringComparer.Ordinal); - foreach (KeyValuePair> aliasesForDirectory in aliasesByDirectory) - { - result.Add( - aliasesForDirectory.Key, - aliasesForDirectory.Value.OrderBy(alias => alias, StringComparer.Ordinal).ToArray()); - } - - return result; - } - - private static string[] GetAllAssemblyScopedLegacyToolInfoAliases(MigrationAssemblyUsage assemblyUsage) - { - return assemblyUsage.AssemblyScopedLegacyToolInfoAliasesByDirectory.Values - .SelectMany(aliases => aliases) - .Distinct(StringComparer.Ordinal) - .ToArray(); - } - - private static List CreateAssemblyReferenceDirectories( - List asmdefFilePaths, - List asmrefFilePaths) - { - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - - if (asmrefFilePaths.Count == 0) - { - return new List(); - } - - Dictionary asmdefDirectoriesByReference = CreateAsmdefDirectoryMap(asmdefFilePaths); - List assemblyReferenceDirectories = new(); - foreach (string asmrefFilePath in asmrefFilePaths) - { - if (!TryReadJsonObjectFromFile(asmrefFilePath, out JObject asmref)) - { - continue; - } - - string reference = asmref["reference"]?.Value() ?? string.Empty; - if (reference.Length == 0) - { - continue; - } - - if (!asmdefDirectoriesByReference.TryGetValue(reference, out string targetAssemblyDirectory)) - { - continue; - } - - string sourceDirectory = Path.GetDirectoryName(asmrefFilePath) ?? string.Empty; - if (sourceDirectory.Length == 0) - { - continue; - } - - assemblyReferenceDirectories.Add( - new AssemblyReferenceDirectory(sourceDirectory, targetAssemblyDirectory)); - } - - return assemblyReferenceDirectories - .OrderByDescending(assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) - .ToList(); - } - - private static async Task> CreateAssemblyReferenceDirectoriesAsync( - List asmdefFilePaths, - List asmrefFilePaths, - ThirdPartyToolMigrationSourceFileCache sourceFileCache, - MigrationProgressCounter progressCounter, - CancellationToken ct) - { - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); - Debug.Assert(progressCounter != null, "progressCounter must not be null"); - - if (asmrefFilePaths.Count == 0) - { - return new List(); - } - - Dictionary asmdefDirectoriesByReference = - await CreateAsmdefDirectoryMapAsync(asmdefFilePaths, sourceFileCache, progressCounter, ct); - List assemblyReferenceDirectories = new(); - foreach (string asmrefFilePath in asmrefFilePaths) - { - if (ct.IsCancellationRequested) - { - return assemblyReferenceDirectories - .OrderByDescending( - assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) - .ToList(); - } - - if (!TryReadJsonObjectFromCache(asmrefFilePath, sourceFileCache, out JObject asmref)) - { - await progressCounter.ReportProcessedItemAsync(ct); - continue; - } - - await progressCounter.ReportProcessedItemAsync(ct); - string reference = asmref["reference"]?.Value() ?? string.Empty; - if (reference.Length == 0) - { - continue; - } - - if (!asmdefDirectoriesByReference.TryGetValue(reference, out string targetAssemblyDirectory)) - { - continue; - } - - string sourceDirectory = Path.GetDirectoryName(asmrefFilePath) ?? string.Empty; - if (sourceDirectory.Length == 0) - { - continue; - } - - assemblyReferenceDirectories.Add( - new AssemblyReferenceDirectory(sourceDirectory, targetAssemblyDirectory)); - } - - return assemblyReferenceDirectories - .OrderByDescending(assemblyReferenceDirectory => assemblyReferenceDirectory.SourceDirectory.Length) - .ToList(); - } - - private static Dictionary CreateAsmdefDirectoryMap(List asmdefFilePaths) - { - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - - Dictionary asmdefDirectoriesByReference = new(StringComparer.Ordinal); - foreach (string asmdefFilePath in asmdefFilePaths) - { - string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? string.Empty; - if (asmdefDirectory.Length == 0) - { - continue; - } - - if (!TryReadJsonObjectFromFile(asmdefFilePath, out JObject asmdef)) - { - continue; - } - - string assemblyName = asmdef["name"]?.Value() ?? string.Empty; - AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); - AddAsmdefDirectoryReference( - asmdefDirectoriesByReference, - ReadAsmdefGuidReference(asmdefFilePath), - asmdefDirectory); - } - - return asmdefDirectoriesByReference; - } - - private static async Task> CreateAsmdefDirectoryMapAsync( - List asmdefFilePaths, - ThirdPartyToolMigrationSourceFileCache sourceFileCache, - MigrationProgressCounter progressCounter, - CancellationToken ct) - { - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); - Debug.Assert(progressCounter != null, "progressCounter must not be null"); - - Dictionary asmdefDirectoriesByReference = new(StringComparer.Ordinal); - foreach (string asmdefFilePath in asmdefFilePaths) - { - if (ct.IsCancellationRequested) - { - return asmdefDirectoriesByReference; - } - - string asmdefDirectory = Path.GetDirectoryName(asmdefFilePath) ?? string.Empty; - if (asmdefDirectory.Length == 0) - { - await progressCounter.ReportProcessedItemAsync(ct); - continue; - } - - if (!TryReadJsonObjectFromCache(asmdefFilePath, sourceFileCache, out JObject asmdef)) - { - await progressCounter.ReportProcessedItemAsync(ct); - continue; - } - - await progressCounter.ReportProcessedItemAsync(ct); - string assemblyName = asmdef["name"]?.Value() ?? string.Empty; - AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); - AddAsmdefDirectoryReference( - asmdefDirectoriesByReference, - ReadAsmdefGuidReference(asmdefFilePath), - asmdefDirectory); - } - - return asmdefDirectoriesByReference; - } - - private static bool TryReadJsonObjectFromFile(string filePath, out JObject jsonObject) - { - return TryReadJsonObjectForMigration(filePath, File.ReadAllText, out jsonObject); - } - - private static bool TryReadJsonObjectFromCache( - string filePath, - ThirdPartyToolMigrationSourceFileCache sourceFileCache, - out JObject jsonObject) - { - Debug.Assert(sourceFileCache != null, "sourceFileCache must not be null"); - - return TryReadJsonObjectForMigration(filePath, sourceFileCache.ReadAllText, out jsonObject); - } - - internal static bool TryReadJsonObjectForMigration( - string filePath, - Func readAllText, - out JObject jsonObject) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(readAllText != null, "readAllText must not be null"); - - try - { - jsonObject = JObject.Parse(readAllText(filePath)); - return true; - } - catch (Exception ex) when (IsSkippableAssemblyJsonReadException(ex)) - { - UnityEngine.Debug.LogWarning( - $"[UnityCliLoop] Skipping unreadable or malformed assembly JSON at {filePath}: {ex.Message}"); - jsonObject = null; - return false; - } - } - - private static bool IsSkippableAssemblyJsonReadException(Exception ex) - { - Debug.Assert(ex != null, "ex must not be null"); - - return ex is JsonException || - ex is IOException || - ex is UnauthorizedAccessException; - } - - private static void AddAsmdefDirectoryReference( - Dictionary asmdefDirectoriesByReference, - string reference, - string asmdefDirectory) - { - Debug.Assert(asmdefDirectoriesByReference != null, "asmdefDirectoriesByReference must not be null"); - Debug.Assert(reference != null, "reference must not be null"); - Debug.Assert(!string.IsNullOrEmpty(asmdefDirectory), "asmdefDirectory must not be null or empty"); - - if (reference.Length == 0 || asmdefDirectoriesByReference.ContainsKey(reference)) - { - return; - } - - asmdefDirectoriesByReference.Add(reference, asmdefDirectory); - } - - private static string ReadAsmdefGuidReference(string asmdefFilePath) - { - Debug.Assert(!string.IsNullOrEmpty(asmdefFilePath), "asmdefFilePath must not be null or empty"); - - string metaPath = asmdefFilePath + ".meta"; - if (!File.Exists(metaPath)) - { - return string.Empty; - } - - foreach (string line in File.ReadLines(metaPath)) - { - string trimmedLine = line.Trim(); - if (!trimmedLine.StartsWith("guid:", StringComparison.Ordinal)) - { - continue; - } - - string guid = trimmedLine.Substring("guid:".Length).Trim(); - return guid.Length == 0 ? string.Empty : $"GUID:{guid}"; - } - - return string.Empty; - } - - private static string FindNearestAssemblyDirectory( - string csharpFilePath, - List asmdefDirectories, - List assemblyReferenceDirectories, - string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); - Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); - Debug.Assert( - assemblyReferenceDirectories != null, - "assemblyReferenceDirectories must not be null"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - string csharpDirectory = Path.GetDirectoryName(csharpFilePath) ?? string.Empty; - string matchedAssemblyDirectory = string.Empty; - int matchedSourceDirectoryLength = -1; - foreach (string asmdefDirectory in asmdefDirectories) - { - if (!IsSameOrChildPath(csharpDirectory, asmdefDirectory) || - asmdefDirectory.Length <= matchedSourceDirectoryLength) - { - continue; - } - - matchedAssemblyDirectory = asmdefDirectory; - matchedSourceDirectoryLength = asmdefDirectory.Length; - } - - foreach (AssemblyReferenceDirectory assemblyReferenceDirectory in assemblyReferenceDirectories) - { - string sourceDirectory = assemblyReferenceDirectory.SourceDirectory; - if (!IsSameOrChildPath(csharpDirectory, sourceDirectory) || - sourceDirectory.Length <= matchedSourceDirectoryLength) - { - continue; - } - - matchedAssemblyDirectory = assemblyReferenceDirectory.TargetAssemblyDirectory; - matchedSourceDirectoryLength = sourceDirectory.Length; - } - - if (matchedAssemblyDirectory.Length > 0) - { - return matchedAssemblyDirectory; - } - - return GetImplicitAssemblyDirectory(csharpFilePath, projectRoot); - } - - private static string GetImplicitAssemblyDirectory(string csharpFilePath, string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - bool isEditorAssemblyPath = IsEditorAssemblyPath(csharpFilePath, projectRoot); - bool isFirstPassAssemblyPath = IsFirstPassAssemblyPath(csharpFilePath, projectRoot); - string implicitAssemblyDirectoryName = GetImplicitAssemblyDirectoryName( - isEditorAssemblyPath, - isFirstPassAssemblyPath); - return Path.Combine(projectRoot, implicitAssemblyDirectoryName); - } - - private static string GetImplicitAssemblyDirectoryName( - bool isEditorAssemblyPath, - bool isFirstPassAssemblyPath) - { - if (isEditorAssemblyPath && isFirstPassAssemblyPath) - { - return ImplicitFirstPassEditorAssemblyDirectoryName; - } - - if (isFirstPassAssemblyPath) - { - return ImplicitFirstPassRuntimeAssemblyDirectoryName; - } - - return isEditorAssemblyPath - ? ImplicitEditorAssemblyDirectoryName - : ImplicitRuntimeAssemblyDirectoryName; - } - - private static bool IsEditorAssemblyPath(string csharpFilePath, string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - string[] pathSegments = GetRelativePathSegments(csharpFilePath, projectRoot); - return pathSegments.Any( - pathSegment => string.Equals(pathSegment, "Editor", StringComparison.OrdinalIgnoreCase)); - } - - private static bool IsFirstPassAssemblyPath(string csharpFilePath, string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(csharpFilePath), "csharpFilePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - string[] pathSegments = GetRelativePathSegments(csharpFilePath, projectRoot); - if (pathSegments.Length < 2 || - !string.Equals(pathSegments[0], "Assets", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return string.Equals(pathSegments[1], "Plugins", StringComparison.OrdinalIgnoreCase) || - string.Equals(pathSegments[1], "Standard Assets", StringComparison.OrdinalIgnoreCase) || - string.Equals(pathSegments[1], "Pro Standard Assets", StringComparison.OrdinalIgnoreCase); - } - - private static string[] GetRelativePathSegments(string filePath, string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - string relativePath = filePath.StartsWith(projectRoot, StringComparison.Ordinal) - ? filePath.Substring(projectRoot.Length) - : filePath; - char[] separators = - { - Path.DirectorySeparatorChar, - Path.AltDirectorySeparatorChar - }; - return relativePath.Split(separators, StringSplitOptions.RemoveEmptyEntries); - } - - private static bool IsSameOrChildPath(string childPath, string parentPath) - { - Debug.Assert(childPath != null, "childPath must not be null"); - Debug.Assert(parentPath != null, "parentPath must not be null"); - - if (string.Equals(childPath, parentPath, StringComparison.Ordinal)) - { - return true; - } - - string parentWithSeparator = parentPath.TrimEnd(Path.DirectorySeparatorChar) - + Path.DirectorySeparatorChar; - return childPath.StartsWith(parentWithSeparator, StringComparison.Ordinal); - } - - private readonly struct MigrationFileChange - { - public MigrationFileChange(string filePath, string content) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(content != null, "content must not be null"); - - FilePath = filePath; - Content = content ?? string.Empty; - } - - public string FilePath { get; } - public string Content { get; } - } - - private readonly struct MigrationPlan - { - public static MigrationPlan Empty => new(new List(), 0); - - public MigrationPlan(List changes, int replacementCount) - { - Debug.Assert(changes != null, "changes must not be null"); - Debug.Assert(replacementCount >= 0, "replacementCount must not be negative"); - - Changes = changes ?? new List(); - ReplacementCount = replacementCount; - ChangedFilePaths = Changes - .Select(change => change.FilePath) - .OrderBy(path => path, StringComparer.Ordinal) - .ToList(); - } - - public List Changes { get; } - public int ReplacementCount { get; } - public List ChangedFilePaths { get; } - } - - private sealed class MigrationProgressCounter - { - private readonly IProgress _progress; - private readonly int _totalItemCount; - private int _processedItemCount; - - public MigrationProgressCounter( - int totalItemCount, - IProgress progress) - { - Debug.Assert(totalItemCount >= 0, "totalItemCount must not be negative"); - Debug.Assert(progress != null, "progress must not be null"); - - _totalItemCount = totalItemCount; - _progress = progress; - Report(); - } - - public async Task ReportProcessedItemAsync(CancellationToken ct) - { - if (ct.IsCancellationRequested) - { - return; - } - - _processedItemCount++; - Report(); - if (_processedItemCount % PreviewYieldBatchSize != 0) - { - return; - } - - await Task.Yield(); - } - - public void ReportComplete() - { - _processedItemCount = _totalItemCount; - Report(); - } - - private void Report() - { - _progress.Report( - new ThirdPartyToolMigrationProgress( - Math.Min(_processedItemCount, _totalItemCount), - _totalItemCount)); - } - } - - private readonly struct MigrationAssemblyUsage - { - public MigrationAssemblyUsage( - List asmdefDirectories, - List assemblyReferenceDirectories, - HashSet legacyAssemblyDirectories, - HashSet assemblyScopedLegacyDirectories, - Dictionary assemblyScopedLegacyAliasesByDirectory, - Dictionary assemblyScopedLegacyToolInfoAliasesByDirectory, - HashSet toolContractsReferenceAssemblyDirectories, - HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) - { - 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( - assemblyScopedLegacyAliasesByDirectory != null, - "assemblyScopedLegacyAliasesByDirectory must not be null"); - Debug.Assert( - assemblyScopedLegacyToolInfoAliasesByDirectory != null, - "assemblyScopedLegacyToolInfoAliasesByDirectory 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"); - - AsmdefDirectories = asmdefDirectories ?? new List(); - AssemblyReferenceDirectories = assemblyReferenceDirectories ?? new List(); - LegacyAssemblyDirectories = legacyAssemblyDirectories ?? new HashSet(StringComparer.Ordinal); - AssemblyScopedLegacyDirectories = assemblyScopedLegacyDirectories ?? - new HashSet(StringComparer.Ordinal); - AssemblyScopedLegacyAliasesByDirectory = assemblyScopedLegacyAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); - AssemblyScopedLegacyToolInfoAliasesByDirectory = assemblyScopedLegacyToolInfoAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); - ToolContractsReferenceAssemblyDirectories = toolContractsReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); - ApplicationReferenceAssemblyDirectories = applicationReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); - DomainReferenceAssemblyDirectories = domainReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); - } - - public List AsmdefDirectories { get; } - public List AssemblyReferenceDirectories { get; } - public HashSet LegacyAssemblyDirectories { get; } - public HashSet AssemblyScopedLegacyDirectories { get; } - public Dictionary AssemblyScopedLegacyAliasesByDirectory { get; } - public Dictionary AssemblyScopedLegacyToolInfoAliasesByDirectory { get; } - public HashSet ToolContractsReferenceAssemblyDirectories { get; } - public HashSet ApplicationReferenceAssemblyDirectories { get; } - public HashSet DomainReferenceAssemblyDirectories { get; } - } - - private readonly struct AssemblyReferenceDirectory - { - public AssemblyReferenceDirectory(string sourceDirectory, string targetAssemblyDirectory) - { - Debug.Assert(!string.IsNullOrEmpty(sourceDirectory), "sourceDirectory must not be null or empty"); - Debug.Assert( - !string.IsNullOrEmpty(targetAssemblyDirectory), - "targetAssemblyDirectory must not be null or empty"); - - SourceDirectory = sourceDirectory; - TargetAssemblyDirectory = targetAssemblyDirectory; - } - - public string SourceDirectory { get; } - public string TargetAssemblyDirectory { get; } - } - - private sealed class ProjectFileInventory - { - private ProjectFileInventory( - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths) - { - CSharpFilePaths = csharpFilePaths; - AsmdefFilePaths = asmdefFilePaths; - AsmrefFilePaths = asmrefFilePaths; - } - - public List CSharpFilePaths { get; } - public List AsmdefFilePaths { get; } - public List AsmrefFilePaths { get; } - - public static ProjectFileInventory Create(string projectRoot) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - - List csharpFilePaths = new(); - List asmdefFilePaths = new(); - List asmrefFilePaths = new(); - string assetsDirectory = Path.Combine(projectRoot, "Assets"); - if (Directory.Exists(assetsDirectory)) - { - CollectCandidateFiles( - projectRoot, - assetsDirectory, - csharpFilePaths, - asmdefFilePaths, - asmrefFilePaths); - } - - csharpFilePaths.Sort(StringComparer.Ordinal); - asmdefFilePaths.Sort(StringComparer.Ordinal); - asmrefFilePaths.Sort(StringComparer.Ordinal); - return new ProjectFileInventory(csharpFilePaths, asmdefFilePaths, asmrefFilePaths); - } - - public static async Task CreateAsync( - string projectRoot, - IProgress progress, - CancellationToken ct) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(progress != null, "progress must not be null"); - - List csharpFilePaths = new(); - List asmdefFilePaths = new(); - List asmrefFilePaths = new(); - string assetsDirectory = Path.Combine(projectRoot, "Assets"); - if (Directory.Exists(assetsDirectory)) - { - await CollectCandidateFilesAsync( - projectRoot, - assetsDirectory, - csharpFilePaths, - asmdefFilePaths, - asmrefFilePaths, - progress, - ct); - } - - csharpFilePaths.Sort(StringComparer.Ordinal); - asmdefFilePaths.Sort(StringComparer.Ordinal); - asmrefFilePaths.Sort(StringComparer.Ordinal); - return new ProjectFileInventory(csharpFilePaths, asmdefFilePaths, asmrefFilePaths); - } - - private static void CollectCandidateFiles( - string projectRoot, - string directoryPath, - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - - foreach (string filePath in Directory.EnumerateFiles(directoryPath)) - { - string extension = Path.GetExtension(filePath); - if (string.Equals(extension, ".cs", StringComparison.OrdinalIgnoreCase)) - { - csharpFilePaths.Add(filePath); - continue; - } - - if (string.Equals(extension, ".asmdef", StringComparison.OrdinalIgnoreCase)) - { - asmdefFilePaths.Add(filePath); - continue; - } - - if (string.Equals(extension, ".asmref", StringComparison.OrdinalIgnoreCase)) - { - asmrefFilePaths.Add(filePath); - } - } - - foreach (string childDirectoryPath in Directory.EnumerateDirectories(directoryPath)) - { - if (ShouldExcludeDirectory(projectRoot, childDirectoryPath)) - { - continue; - } - - CollectCandidateFiles( - projectRoot, - childDirectoryPath, - csharpFilePaths, - asmdefFilePaths, - asmrefFilePaths); - } - } - - private static async Task CollectCandidateFilesAsync( - string projectRoot, - string assetsDirectory, - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths, - IProgress progress, - CancellationToken ct) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(assetsDirectory), "assetsDirectory must not be null or empty"); - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - Debug.Assert(progress != null, "progress must not be null"); - - Stack pendingDirectories = new(); - pendingDirectories.Push(assetsDirectory); - int inspectedEntryCount = 0; - progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); - - while (pendingDirectories.Count > 0) - { - if (ct.IsCancellationRequested) - { - return; - } - - string directoryPath = pendingDirectories.Pop(); - foreach (string filePath in Directory.EnumerateFiles(directoryPath)) - { - AddCandidateFilePath(filePath, csharpFilePaths, asmdefFilePaths, asmrefFilePaths); - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) - { - progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); - await Task.Yield(); - } - } - - foreach (string childDirectoryPath in Directory.EnumerateDirectories(directoryPath)) - { - if (ShouldExcludeDirectory(projectRoot, childDirectoryPath)) - { - continue; - } - - pendingDirectories.Push(childDirectoryPath); - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) - { - progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); - await Task.Yield(); - } - } - } - } - - private static void AddCandidateFilePath( - string filePath, - List csharpFilePaths, - List asmdefFilePaths, - List asmrefFilePaths) - { - Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); - Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); - Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); - Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); - - string extension = Path.GetExtension(filePath); - if (string.Equals(extension, ".cs", StringComparison.OrdinalIgnoreCase)) - { - csharpFilePaths.Add(filePath); - return; - } - - if (string.Equals(extension, ".asmdef", StringComparison.OrdinalIgnoreCase)) - { - asmdefFilePaths.Add(filePath); - return; - } - - if (string.Equals(extension, ".asmref", StringComparison.OrdinalIgnoreCase)) - { - asmrefFilePaths.Add(filePath); - } - } - - private static bool ShouldExcludeDirectory(string projectRoot, string directoryPath) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); - - if (IsProjectRootPackagesDirectory(projectRoot, directoryPath)) - { - return true; - } - - string directoryName = Path.GetFileName(directoryPath); - return ThirdPartyToolMigrationRules.IsExcludedDirectoryName(directoryName); - } - - private static bool IsProjectRootPackagesDirectory(string projectRoot, string directoryPath) - { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); - - string packagesDirectory = Path.Combine(projectRoot, "Packages"); - string normalizedPackagesDirectory = NormalizeDirectoryPath(packagesDirectory); - string normalizedDirectoryPath = NormalizeDirectoryPath(directoryPath); - return string.Equals( - normalizedDirectoryPath, - normalizedPackagesDirectory, - StringComparison.Ordinal); - } - - private static string NormalizeDirectoryPath(string directoryPath) - { - Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); - - return Path.GetFullPath(directoryPath) - .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - } - } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs new file mode 100644 index 0000000000..2a3bdb7414 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs @@ -0,0 +1,16 @@ +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Holds file-service constants shared by migration scanners and planners. + /// + internal static class ThirdPartyToolMigrationFileServiceConstants + { + internal const string ImplicitEditorAssemblyDirectoryName = "__UnityCliLoopImplicitEditorAssembly"; + internal const string ImplicitRuntimeAssemblyDirectoryName = "__UnityCliLoopImplicitRuntimeAssembly"; + internal const string ImplicitFirstPassEditorAssemblyDirectoryName = + "__UnityCliLoopImplicitFirstPassEditorAssembly"; + internal const string ImplicitFirstPassRuntimeAssemblyDirectoryName = + "__UnityCliLoopImplicitFirstPassRuntimeAssembly"; + internal const int PreviewYieldBatchSize = 32; + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs.meta new file mode 100644 index 0000000000..1471f95691 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b93eef5e338e849a197dbdba5e2f945e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs new file mode 100644 index 0000000000..c332c3654a --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs @@ -0,0 +1,46 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Writes migrated files atomically through temporary sidecar files. + /// + internal static class ThirdPartyToolMigrationFileWriter + { + internal static void Write(string filePath, string content) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(content != null, "content must not be null"); + + string tempFilePath = CreateUniqueSidecarPath(filePath, ".tmp"); + File.WriteAllText(tempFilePath, content); + if (!File.Exists(filePath)) + { + File.Move(tempFilePath, filePath); + return; + } + + string backupFilePath = CreateUniqueSidecarPath(filePath, ".bak"); + File.Replace(tempFilePath, filePath, backupFilePath); + File.Delete(backupFilePath); + } + + internal static string CreateUniqueSidecarPath(string filePath, string extension) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(extension), "extension must not be null or empty"); + + string directory = Path.GetDirectoryName(filePath) ?? string.Empty; + string fileName = Path.GetFileName(filePath); + string sidecarPath = Path.Combine(directory, $"{fileName}.{Guid.NewGuid():N}{extension}"); + while (File.Exists(sidecarPath)) + { + sidecarPath = Path.Combine(directory, $"{fileName}.{Guid.NewGuid():N}{extension}"); + } + + return sidecarPath; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs.meta new file mode 100644 index 0000000000..ae82a301b8 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3041b8ff81724d8f86d9ce1ef642c2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs new file mode 100644 index 0000000000..9a022a9309 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs @@ -0,0 +1,344 @@ +using System.Diagnostics; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Finds literal boundaries inside regular interpolated strings used by migration argument parsing. + /// + internal static class ThirdPartyToolMigrationInterpolatedStringRules + { + internal static int FindRegularInterpolatedStringEndIndex(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + Debug.Assert( + ThirdPartyToolMigrationParsingRules.StartsWith(source, startIndex, "$\""), + "startIndex must point to an interpolated string"); + + int index = startIndex + 2; + int interpolationBraceDepth = 0; + while (index < source.Length) + { + if (interpolationBraceDepth == 0) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '"') + { + return index; + } + + if (source[index] == '{') + { + if (index + 1 < source.Length && source[index + 1] == '{') + { + index += 2; + continue; + } + + interpolationBraceDepth = 1; + } + + index++; + continue; + } + + int skippedLiteralEndIndex = FindSkippedInterpolationLiteralEndIndex(source, index); + if (skippedLiteralEndIndex >= 0) + { + index = skippedLiteralEndIndex + 1; + continue; + } + + if (source[index] == '{') + { + interpolationBraceDepth++; + index++; + continue; + } + + if (source[index] == '}') + { + interpolationBraceDepth--; + index++; + continue; + } + + index++; + } + + return -1; + } + + private static int FindSkippedInterpolationLiteralEndIndex(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + if (ThirdPartyToolMigrationParsingRules.IsRawStringStart(source, startIndex)) + { + return FindRawStringEndIndex(source, startIndex); + } + + if (ThirdPartyToolMigrationParsingRules.StartsWith(source, startIndex, "$\"")) + { + return FindRegularInterpolatedStringEndIndex(source, startIndex); + } + + if (ThirdPartyToolMigrationParsingRules.StartsWith(source, startIndex, "@\"") || + ThirdPartyToolMigrationParsingRules.StartsWith(source, startIndex, "$@\"") || + ThirdPartyToolMigrationParsingRules.StartsWith(source, startIndex, "@$\"")) + { + int quoteIndex = startIndex + ThirdPartyToolMigrationParsingRules.GetStringPrefixLength( + source, + startIndex); + return FindVerbatimStringEndIndex(source, quoteIndex); + } + + if (source[startIndex] == '"') + { + return FindRegularStringEndIndex(source, startIndex); + } + + if (source[startIndex] == '\'') + { + return FindCharLiteralEndIndex(source, startIndex); + } + + return -1; + } + + private static int FindRawStringEndIndex(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int dollarCount = ThirdPartyToolMigrationParsingRules.CountRepeatedCharacter(source, startIndex, '$'); + int quoteIndex = startIndex + dollarCount; + int quoteCount = ThirdPartyToolMigrationParsingRules.CountRepeatedCharacter(source, quoteIndex, '"'); + Debug.Assert( + quoteCount >= ThirdPartyToolMigrationRuleCatalog.MinimumRawStringDelimiterQuoteCount, + "startIndex must point to a raw string delimiter"); + + if (dollarCount > 0) + { + return FindInterpolatedRawStringEndIndex(source, startIndex, dollarCount, quoteCount); + } + + return FindUninterpolatedRawStringEndIndex(source, quoteIndex, quoteCount); + } + + private static int FindInterpolatedRawStringEndIndex( + string source, + int startIndex, + int dollarCount, + int quoteCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + Debug.Assert(dollarCount > 0, "dollarCount must be positive"); + Debug.Assert( + quoteCount >= ThirdPartyToolMigrationRuleCatalog.MinimumRawStringDelimiterQuoteCount, + "quoteCount must describe a raw string delimiter"); + + int index = startIndex + dollarCount + quoteCount; + while (index < source.Length) + { + if (ThirdPartyToolMigrationParsingRules.HasRepeatedCharacterAt( + source, + index, + '"', + quoteCount)) + { + return index + quoteCount - 1; + } + + if (source[index] == '{') + { + int braceCount = ThirdPartyToolMigrationParsingRules.CountRepeatedCharacter(source, index, '{'); + if (braceCount < dollarCount) + { + index += braceCount; + continue; + } + + index = FindRawInterpolationHoleEndIndex(source, index, dollarCount); + continue; + } + + if (source[index] == '}') + { + int braceCount = ThirdPartyToolMigrationParsingRules.CountRepeatedCharacter(source, index, '}'); + index += braceCount; + continue; + } + + index++; + } + + return -1; + } + + private static int FindRawInterpolationHoleEndIndex( + string source, + int openBraceIndex, + int interpolationBraceCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); + Debug.Assert(interpolationBraceCount > 0, "interpolationBraceCount must be positive"); + + int nestedBraceDepth = 0; + int index = openBraceIndex + interpolationBraceCount; + while (index < source.Length) + { + int skippedLiteralEndIndex = FindSkippedInterpolationLiteralEndIndex(source, index); + if (skippedLiteralEndIndex >= 0) + { + index = skippedLiteralEndIndex + 1; + continue; + } + + if (source[index] == '{') + { + nestedBraceDepth++; + index++; + continue; + } + + if (source[index] == '}') + { + bool isClosingInterpolation = + nestedBraceDepth == 0 && + ThirdPartyToolMigrationParsingRules.HasRepeatedCharacterAt( + source, + index, + '}', + interpolationBraceCount); + if (isClosingInterpolation) + { + return index + interpolationBraceCount; + } + + if (nestedBraceDepth > 0) + { + nestedBraceDepth--; + } + } + + index++; + } + + return source.Length; + } + + private static int FindUninterpolatedRawStringEndIndex( + string source, + int quoteIndex, + int quoteCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(quoteIndex >= 0, "quoteIndex must not be negative"); + Debug.Assert( + quoteCount >= ThirdPartyToolMigrationRuleCatalog.MinimumRawStringDelimiterQuoteCount, + "quoteCount must describe a raw string delimiter"); + + int index = quoteIndex + quoteCount; + while (index < source.Length) + { + if (ThirdPartyToolMigrationParsingRules.HasRepeatedCharacterAt( + source, + index, + '"', + quoteCount)) + { + return index + quoteCount - 1; + } + + index++; + } + + return -1; + } + + private static int FindVerbatimStringEndIndex(string source, int quoteIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(quoteIndex >= 0, "quoteIndex must not be negative"); + + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] != '"') + { + index++; + continue; + } + + if (index + 1 < source.Length && source[index + 1] == '"') + { + index += 2; + continue; + } + + return index; + } + + return -1; + } + + private static int FindRegularStringEndIndex(string source, int quoteIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(quoteIndex >= 0, "quoteIndex must not be negative"); + + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '"') + { + return index; + } + + index++; + } + + return -1; + } + + private static int FindCharLiteralEndIndex(string source, int quoteIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(quoteIndex >= 0, "quoteIndex must not be negative"); + + int index = quoteIndex + 1; + while (index < source.Length) + { + if (source[index] == '\\') + { + index += 2; + continue; + } + + if (source[index] == '\'') + { + return index; + } + + index++; + } + + return -1; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs.meta new file mode 100644 index 0000000000..5d13286d0e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0eb172466bc034608858e8c3a543e915 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs new file mode 100644 index 0000000000..a88596b806 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationMetadataConstructorRules + { + internal static string ReplaceLegacyToolInfoConstructorsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyToolInfoConstructor, + bool canMigrateAmbiguousBareLegacyToolInfoConstructor, + string[] legacyAssemblyToolInfoAliases, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + string[] legacyToolInfoTypeAliases = + GetCombinedLegacyToolInfoTypeAliases(source, legacyAssemblyToolInfoAliases); + MatchCollection matches = LegacyToolInfoConstructorRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int localReplacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyToolInfoConstructorMatch( + match, + legacyNamespaceAliases, + legacyToolInfoTypeAliases, + canMigrateBareLegacyToolInfoConstructor)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] arguments = SplitAttributeArguments(argumentsSource); + if (!ShouldMigrateLegacyToolInfoConstructorArguments( + match, + arguments, + canMigrateAmbiguousBareLegacyToolInfoConstructor)) + { + continue; + } + + string[] migratedArguments = GetMigratedToolInfoConstructorArguments(arguments); + if (migratedArguments.Length == arguments.Length) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append($"new {CurrentNamespace}.ToolInfo("); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + sourceCopyIndex = closingParenthesisIndex + 1; + localReplacementCount++; + } + + if (localReplacementCount == 0) + { + return source; + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + replacementCount += localReplacementCount; + return builder.ToString(); + } + + internal static string ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyConstructor, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyToolSettingsCatalogItemConstructorRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int localReplacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyToolSettingsCatalogItemConstructorMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyConstructor)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] arguments = SplitAttributeArguments(argumentsSource); + string[] migratedArguments = GetMigratedToolSettingsCatalogItemConstructorArguments(arguments); + if (migratedArguments.Length == arguments.Length) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append($"new {CurrentNamespace}.ToolSettingsCatalogItem("); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + sourceCopyIndex = closingParenthesisIndex + 1; + localReplacementCount++; + } + + if (localReplacementCount == 0) + { + return source; + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + replacementCount += localReplacementCount; + return builder.ToString(); + } + + internal static bool IsLegacyToolSettingsCatalogItemConstructorMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyConstructor) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); + } + + return match.Groups["toolSettingsCatalogItem"].Success && canMigrateBareLegacyConstructor; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs.meta new file mode 100644 index 0000000000..471b1d7cc3 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce34f77e7deec43e5a74e2c954ce0ef0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs new file mode 100644 index 0000000000..01260d6dfa --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal readonly struct MigrationFileChange + { + public MigrationFileChange(string filePath, string content) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(content != null, "content must not be null"); + + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + } + + public string FilePath { get; } + public string Content { get; } + } + + internal readonly struct MigrationPlan + { + public static MigrationPlan Empty => new(new List(), 0); + + public MigrationPlan(List changes, int replacementCount) + { + Debug.Assert(changes != null, "changes must not be null"); + Debug.Assert(replacementCount >= 0, "replacementCount must not be negative"); + + Changes = changes ?? throw new ArgumentNullException(nameof(changes)); + ReplacementCount = replacementCount; + ChangedFilePaths = Changes + .Select(change => change.FilePath) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + } + + public List Changes { get; } + public int ReplacementCount { get; } + public List ChangedFilePaths { get; } + } + + internal sealed class MigrationProgressCounter + { + private readonly IProgress _progress; + private readonly int _totalItemCount; + private int _processedItemCount; + + public MigrationProgressCounter( + int totalItemCount, + IProgress progress) + { + Debug.Assert(totalItemCount >= 0, "totalItemCount must not be negative"); + Debug.Assert(progress != null, "progress must not be null"); + + _totalItemCount = totalItemCount; + _progress = progress; + Report(); + } + + public async Task ReportProcessedItemAsync(CancellationToken ct) + { + if (ct.IsCancellationRequested) + { + return; + } + + _processedItemCount++; + Report(); + if (_processedItemCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize != 0) + { + return; + } + + await Task.Yield(); + } + + public void ReportComplete() + { + _processedItemCount = _totalItemCount; + Report(); + } + + private void Report() + { + _progress.Report( + new ThirdPartyToolMigrationProgress( + Math.Min(_processedItemCount, _totalItemCount), + _totalItemCount)); + } + } + + internal readonly struct MigrationAssemblyUsage + { + public MigrationAssemblyUsage( + List asmdefDirectories, + List assemblyReferenceDirectories, + HashSet legacyAssemblyDirectories, + HashSet assemblyScopedLegacyDirectories, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + Dictionary assemblyScopedLegacyAliasesByDirectory, + Dictionary assemblyScopedLegacyToolInfoAliasesByDirectory, + Dictionary assemblyScopedCurrentApplicationAliasesByDirectory, + Dictionary assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + Dictionary assemblyDeclaredTypeNamesByDirectory, + HashSet toolContractsReferenceAssemblyDirectories, + HashSet applicationReferenceAssemblyDirectories, + 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( + 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( + 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)); + 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)); + 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)); + } + + public List AsmdefDirectories { get; } + public List AssemblyReferenceDirectories { get; } + public HashSet LegacyAssemblyDirectories { get; } + public HashSet AssemblyScopedLegacyDirectories { get; } + public HashSet AssemblyScopedCurrentToolContractsDirectories { get; } + public HashSet AssemblyScopedCurrentApplicationDirectories { get; } + public HashSet AssemblyScopedCurrentFirstPartyToolsDirectories { get; } + public Dictionary AssemblyScopedLegacyAliasesByDirectory { get; } + public Dictionary AssemblyScopedLegacyToolInfoAliasesByDirectory { get; } + public Dictionary AssemblyScopedCurrentApplicationAliasesByDirectory { get; } + public Dictionary AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory { get; } + public Dictionary AssemblyDeclaredTypeNamesByDirectory { get; } + public HashSet ToolContractsReferenceAssemblyDirectories { get; } + public HashSet ApplicationReferenceAssemblyDirectories { get; } + public HashSet DomainReferenceAssemblyDirectories { get; } + public HashSet FirstPartyScreenshotReferenceAssemblyDirectories { get; } + } + + internal readonly struct AssemblyReferenceDirectory + { + public AssemblyReferenceDirectory(string sourceDirectory, string targetAssemblyDirectory) + { + Debug.Assert(!string.IsNullOrEmpty(sourceDirectory), "sourceDirectory must not be null or empty"); + Debug.Assert( + !string.IsNullOrEmpty(targetAssemblyDirectory), + "targetAssemblyDirectory must not be null or empty"); + + SourceDirectory = sourceDirectory; + TargetAssemblyDirectory = targetAssemblyDirectory; + } + + public string SourceDirectory { get; } + public string TargetAssemblyDirectory { get; } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs.meta new file mode 100644 index 0000000000..34a2ba4d4c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 076409c2de60a4c46a5a66d19cd26143 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs new file mode 100644 index 0000000000..94c7d72a0c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs @@ -0,0 +1,438 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationParsingRules + { + internal static readonly ConditionalWeakTable CodeTextMaskCache = new(); + + internal static bool IsLegacyAssemblyScopedTypeDeclaration(string source, int typeNameIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(typeNameIndex >= 0, "typeNameIndex must not be negative"); + + string previousIdentifier = ReadPreviousIdentifier(source, typeNameIndex); + return string.Equals(previousIdentifier, "class", StringComparison.Ordinal) || + string.Equals(previousIdentifier, "struct", StringComparison.Ordinal) || + string.Equals(previousIdentifier, "interface", StringComparison.Ordinal) || + string.Equals(previousIdentifier, "enum", StringComparison.Ordinal) || + string.Equals(previousIdentifier, "using", StringComparison.Ordinal); + } + + internal static string ReadPreviousIdentifier(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int index = startIndex - 1; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + + int identifierEndIndex = index + 1; + while (index >= 0 && IsIdentifierCharacter(source[index])) + { + index--; + } + + return source.Substring(index + 1, identifierEndIndex - index - 1); + } + + internal static int ReadNextNonWhitespaceIndex(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + for (int index = startIndex; index < source.Length; index++) + { + if (char.IsWhiteSpace(source[index])) + { + continue; + } + + return index; + } + + return source.Length; + } + + internal static char ReadNextNonWhitespaceCharacter(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + for (int index = startIndex; index < source.Length; index++) + { + if (char.IsWhiteSpace(source[index])) + { + continue; + } + + return source[index]; + } + + return '\0'; + } + + internal static char ReadPreviousNonWhitespaceCharacter(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + for (int index = startIndex - 1; index >= 0; index--) + { + if (char.IsWhiteSpace(source[index])) + { + continue; + } + + return source[index]; + } + + return '\0'; + } + + internal static bool IsDeclarationIdentifierTerminator(char value) + { + return value == '{' || + value == ';' || + value == '=' || + value == ')' || + value == ','; + } + + internal static bool PreviousCodeTokenEquals(string source, int endIndex, string token) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(endIndex >= 0, "endIndex must not be negative"); + Debug.Assert(!string.IsNullOrEmpty(token), "token must not be null or empty"); + + int tokenEndIndex = endIndex - 1; + while (tokenEndIndex >= 0 && char.IsWhiteSpace(source[tokenEndIndex])) + { + tokenEndIndex--; + } + + int tokenStartIndex = tokenEndIndex; + while (tokenStartIndex >= 0 && IsIdentifierCharacter(source[tokenStartIndex])) + { + tokenStartIndex--; + } + + int tokenLength = tokenEndIndex - tokenStartIndex; + if (tokenLength <= 0) + { + return false; + } + + string previousToken = source.Substring(tokenStartIndex + 1, tokenLength); + return string.Equals(previousToken, token, StringComparison.Ordinal); + } + + internal static bool PreviousCodeTokenIsArrow(string source, int endIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(endIndex >= 0, "endIndex must not be negative"); + + int tokenEndIndex = endIndex - 1; + while (tokenEndIndex >= 0 && char.IsWhiteSpace(source[tokenEndIndex])) + { + tokenEndIndex--; + } + + return tokenEndIndex > 0 && + source[tokenEndIndex] == '>' && + source[tokenEndIndex - 1] == '='; + } + + internal static bool CanPrecedeDeclarationIdentifier(char value) + { + return IsIdentifierCharacter(value) || + value == ']' || + value == '>'; + } + + internal static bool IsIdentifierCharacter(char value) + { + return char.IsLetterOrDigit(value) || value == '_'; + } + + internal static bool IsIdentifierStartCharacter(char value) + { + return char.IsLetter(value) || value == '_'; + } + + internal static bool ContainsLegacyToolMigrationMarker(string source) + { + Debug.Assert(source != null, "source must not be null"); + + if (RegexMatchesCode(source, LegacyNamespaceRegex)) return true; + + return ContainsLegacyToolAttributeList( + source, + Array.Empty(), + canMigrateBareLegacyToolAttribute: false); + } + + internal static bool ContainsLegacyToolAttributeList( + string source, + string[] legacyAssemblyAliases, + bool canMigrateBareLegacyToolAttribute) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); + int index = 0; + while (index < source.Length) + { + if (source[index] != '[' || !codeTextMask.IsCodeAt(index)) + { + index++; + continue; + } + + int closingBracketIndex = FindAttributeListClosingBracketIndex( + source, + codeTextMask, + index + 1); + if (closingBracketIndex < 0) + { + index++; + continue; + } + + string attributesSource = source.Substring(index + 1, closingBracketIndex - index - 1); + if (TryMigrateLegacyToolAttributeList( + attributesSource, + legacyNamespaceAliases, + canMigrateBareLegacyToolAttribute, + out _)) + { + return true; + } + + index = closingBracketIndex + 1; + } + + return false; + } + + internal static bool StartsWith(string source, int startIndex, string value) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + Debug.Assert(value != null, "value must not be null"); + + if (startIndex + value.Length > source.Length) + { + return false; + } + + return string.CompareOrdinal(source, startIndex, value, 0, value.Length) == 0; + } + + internal static bool ContainsTextFragment(string source, string text) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(text), "text must not be null or empty"); + + return source.IndexOf(text, StringComparison.Ordinal) >= 0; + } + + internal static bool IsRawStringStart(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); + int quoteIndex = startIndex + dollarCount; + return CountRepeatedCharacter(source, quoteIndex, '"') >= MinimumRawStringDelimiterQuoteCount; + } + + internal static int CountRepeatedCharacter(string source, int startIndex, char character) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int index = startIndex; + while (index < source.Length && source[index] == character) + { + index++; + } + + return index - startIndex; + } + + internal static bool HasRepeatedCharacterAt(string source, int startIndex, char character, int count) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + Debug.Assert(count > 0, "count must be positive"); + + if (startIndex + count > source.Length) + { + return false; + } + + for (int i = 0; i < count; i++) + { + if (source[startIndex + i] != character) + { + return false; + } + } + + return true; + } + + internal static int GetStringPrefixLength(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) + { + return 2; + } + + return 1; + } + + internal readonly struct ReplacementRule + { + public ReplacementRule(string pattern, string replacement) + { + Debug.Assert(!string.IsNullOrEmpty(pattern), "pattern must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(replacement), "replacement must not be null or empty"); + + PatternRegex = new Regex(pattern, RegexOptions.Compiled); + Replacement = replacement; + } + + public Regex PatternRegex { get; } + public string Replacement { get; } + } + + internal readonly struct TypeReplacementRule + { + public TypeReplacementRule(string legacyName, string currentName) + { + Debug.Assert(!string.IsNullOrEmpty(legacyName), "legacyName must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(currentName), "currentName must not be null or empty"); + + LegacyName = legacyName; + CurrentName = currentName; + } + + public string LegacyName { get; } + public string CurrentName { get; } + } + + internal readonly struct CodeTextMask + { + private readonly bool[] _codeCharacters; + + private CodeTextMask(bool[] codeCharacters) + { + Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); + + _codeCharacters = codeCharacters; + } + + public static CodeTextMask Create(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return CodeTextMaskCache.GetValue(source, CreateCodeTextMaskHolder).Mask; + } + + internal static CodeTextMask CreateUncached(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool[] codeCharacters = ThirdPartyToolMigrationCodeTextMaskBuilder.CreateCodeCharacters(source); + return new CodeTextMask(codeCharacters); + } + + public bool IsCodeAt(int index) + { + if (index < 0 || index >= _codeCharacters.Length) + { + return false; + } + + return _codeCharacters[index]; + } + + } + + internal sealed class CodeTextMaskHolder + { + public CodeTextMaskHolder(CodeTextMask mask) + { + Mask = mask; + } + + public CodeTextMask Mask { get; } + } + + internal static CodeTextMaskHolder CreateCodeTextMaskHolder(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return new CodeTextMaskHolder(CodeTextMask.CreateUncached(source)); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs.meta new file mode 100644 index 0000000000..326ff97c67 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a558124657647409a8333c3543946c31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs new file mode 100644 index 0000000000..af5c460b70 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs @@ -0,0 +1,74 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Normalizes project file paths before migration assembly ancestry checks. + /// + internal static class ThirdPartyToolMigrationPathRules + { + internal static string[] GetRelativePathSegments(string filePath, string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + string normalizedRoot = NormalizeFullPath(projectRoot); + string normalizedFilePath = Path.GetFullPath(filePath); + string relativePath = Path.GetRelativePath(normalizedRoot, normalizedFilePath); + char[] separators = + { + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + }; + return relativePath.Split(separators, StringSplitOptions.RemoveEmptyEntries); + } + + internal static bool IsSameOrChildPath(string childPath, string parentPath) + { + ThrowIfPathArgumentEmpty(childPath, nameof(childPath)); + ThrowIfPathArgumentEmpty(parentPath, nameof(parentPath)); + + string normalizedChildPath = NormalizeFullPath(childPath); + string normalizedParentPath = NormalizeFullPath(parentPath); + StringComparison pathComparison = GetPathComparison(); + + if (string.Equals(normalizedChildPath, normalizedParentPath, pathComparison)) + { + return true; + } + + string parentWithSeparator = normalizedParentPath + + Path.DirectorySeparatorChar; + return normalizedChildPath.StartsWith(parentWithSeparator, pathComparison); + } + + private static string NormalizeFullPath(string path) + { + Debug.Assert(!string.IsNullOrEmpty(path), "path must not be null or empty"); + + string fullPath = Path.GetFullPath(path); + string trimmedPath = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return trimmedPath.Length == 0 ? fullPath : trimmedPath; + } + + private static void ThrowIfPathArgumentEmpty(string path, string argumentName) + { + Debug.Assert(!string.IsNullOrEmpty(path), $"{argumentName} must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); + + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException($"{argumentName} must not be null or empty", argumentName); + } + } + + private static StringComparison GetPathComparison() + { + return Path.DirectorySeparatorChar == '\\' + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs.meta new file mode 100644 index 0000000000..117415443d --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a64a06fe2141d4a30bf7acdc728faf05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs new file mode 100644 index 0000000000..2e30dcd4b4 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +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.ThirdPartyToolMigrationCrossFileTimingMigrationPlanner; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Builds preview/apply migration plans without mutating project files. + /// + internal static class ThirdPartyToolMigrationPlanBuilder + { + internal static MigrationPlan Create(string projectRoot) + { + if (!Directory.Exists(projectRoot)) + { + throw new DirectoryNotFoundException(projectRoot); + } + + ProjectFileInventory inventory = ProjectFileInventory.Create(projectRoot); + MigrationAssemblyUsage assemblyUsage = ThirdPartyToolMigrationAssemblyUsageAnalyzer.FindMigrationAssemblyUsage( + projectRoot, + inventory.CSharpFilePaths, + inventory.AsmdefFilePaths, + inventory.AsmrefFilePaths); + List changes = new(); + int replacementCount = 0; + string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); + Dictionary> + removedPlayerLoopTimingSignaturesByAssemblyDirectory = new(StringComparer.Ordinal); + + foreach (string csharpFilePath in inventory.CSharpFilePaths) + { + string source = File.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[] 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 hasAssemblyScopedCurrentFirstPartyToolsUsing = + assemblyUsage.AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); + bool hasLegacyAssemblySource = + assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentApplicationUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyAssemblyAliases, + legacyAssemblyToolInfoAliases, + currentApplicationAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + if (!result.Changed) + { + continue; + } + + replacementCount += result.ReplacementCount; + changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); + AddRemovedPlayerLoopTimingSignatures( + removedPlayerLoopTimingSignaturesByAssemblyDirectory, + assemblyDirectory, + result.RemovedPlayerLoopTimingSignatures); + } + + replacementCount += ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + inventory.CSharpFilePaths, + projectRoot, + assemblyUsage, + changes, + removedPlayerLoopTimingSignaturesByAssemblyDirectory, + File.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 = File.ReadAllText(asmdefFilePath); + if (!hasAssemblyMigrationRequirement && + !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) + { + continue; + } + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + if (!result.Changed) + { + continue; + } + + replacementCount += result.ReplacementCount; + changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); + } + + return new MigrationPlan(changes, replacementCount); + } + + internal static async Task CreateAsync( + string projectRoot, + IProgress progress, + CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(progress != null, "progress must not be null"); + + if (!Directory.Exists(projectRoot)) + { + throw new DirectoryNotFoundException(projectRoot); + } + + ProjectFileInventory inventory = await ProjectFileInventory.CreateAsync(projectRoot, progress, ct); + if (ct.IsCancellationRequested) + { + return MigrationPlan.Empty; + } + + MigrationProgressCounter progressCounter = new(GetPreviewWorkItemCount(inventory), progress); + ThirdPartyToolMigrationSourceFileCache sourceFileCache = new(); + MigrationAssemblyUsage assemblyUsage = await ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.FindMigrationAssemblyUsageAsync( + projectRoot, + inventory.CSharpFilePaths, + inventory.AsmdefFilePaths, + inventory.AsmrefFilePaths, + sourceFileCache, + progressCounter, + ct); + if (ct.IsCancellationRequested) + { + return MigrationPlan.Empty; + } + + List changes = new(); + int replacementCount = 0; + string[] legacyToolInfoAliases = GetAllAssemblyScopedLegacyToolInfoAliases(assemblyUsage); + Dictionary> + removedPlayerLoopTimingSignaturesByAssemblyDirectory = new(StringComparer.Ordinal); + + foreach (string csharpFilePath in inventory.CSharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return MigrationPlan.Empty; + } + + string source = sourceFileCache.ReadAllText(csharpFilePath); + await progressCounter.ReportProcessedItemAsync(ct); + 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[] 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 hasAssemblyScopedCurrentFirstPartyToolsUsing = + assemblyUsage.AssemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory); + bool hasLegacyAssemblySource = + assemblyUsage.AssemblyScopedLegacyDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentApplicationUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyAssemblyAliases, + legacyAssemblyToolInfoAliases, + currentApplicationAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); + if (!result.Changed) + { + continue; + } + + replacementCount += result.ReplacementCount; + changes.Add(new MigrationFileChange(csharpFilePath, result.Content)); + AddRemovedPlayerLoopTimingSignatures( + removedPlayerLoopTimingSignaturesByAssemblyDirectory, + assemblyDirectory, + result.RemovedPlayerLoopTimingSignatures); + } + + if (ct.IsCancellationRequested) + { + return MigrationPlan.Empty; + } + + replacementCount += ApplyCrossFilePlayerLoopTimingCallerArgumentMigrations( + inventory.CSharpFilePaths, + projectRoot, + assemblyUsage, + changes, + removedPlayerLoopTimingSignaturesByAssemblyDirectory, + sourceFileCache.ReadAllText); + + foreach (string asmdefFilePath in inventory.AsmdefFilePaths) + { + if (ct.IsCancellationRequested) + { + 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; + } + + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); + if (!result.Changed) + { + continue; + } + + replacementCount += result.ReplacementCount; + changes.Add(new MigrationFileChange(asmdefFilePath, result.Content)); + } + + progressCounter.ReportComplete(); + return new MigrationPlan(changes, replacementCount); + } + + + internal static int GetPreviewWorkItemCount(ProjectFileInventory inventory) + { + Debug.Assert(inventory != null, "inventory must not be null"); + + return (inventory.CSharpFilePaths.Count * 3) + + (inventory.AsmdefFilePaths.Count * 2) + + inventory.AsmrefFilePaths.Count; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs.meta new file mode 100644 index 0000000000..0e0f302f1e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac3c08ad9c47c4aeba312e26be0e4298 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs new file mode 100644 index 0000000000..e59ba29987 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal sealed class ProjectFileInventory + { + private ProjectFileInventory( + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths) + { + CSharpFilePaths = csharpFilePaths; + AsmdefFilePaths = asmdefFilePaths; + AsmrefFilePaths = asmrefFilePaths; + } + + public List CSharpFilePaths { get; } + public List AsmdefFilePaths { get; } + public List AsmrefFilePaths { get; } + + public static ProjectFileInventory Create(string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + List csharpFilePaths = new(); + List asmdefFilePaths = new(); + List asmrefFilePaths = new(); + string assetsDirectory = Path.Combine(projectRoot, "Assets"); + if (Directory.Exists(assetsDirectory)) + { + CollectCandidateFiles( + projectRoot, + assetsDirectory, + csharpFilePaths, + asmdefFilePaths, + asmrefFilePaths); + } + + csharpFilePaths.Sort(StringComparer.Ordinal); + asmdefFilePaths.Sort(StringComparer.Ordinal); + asmrefFilePaths.Sort(StringComparer.Ordinal); + return new ProjectFileInventory(csharpFilePaths, asmdefFilePaths, asmrefFilePaths); + } + + public static async Task CreateAsync( + string projectRoot, + IProgress progress, + CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(progress != null, "progress must not be null"); + + List csharpFilePaths = new(); + List asmdefFilePaths = new(); + List asmrefFilePaths = new(); + string assetsDirectory = Path.Combine(projectRoot, "Assets"); + if (Directory.Exists(assetsDirectory)) + { + await CollectCandidateFilesAsync( + projectRoot, + assetsDirectory, + csharpFilePaths, + asmdefFilePaths, + asmrefFilePaths, + progress, + ct); + } + + csharpFilePaths.Sort(StringComparer.Ordinal); + asmdefFilePaths.Sort(StringComparer.Ordinal); + asmrefFilePaths.Sort(StringComparer.Ordinal); + return new ProjectFileInventory(csharpFilePaths, asmdefFilePaths, asmrefFilePaths); + } + + private static void CollectCandidateFiles( + string projectRoot, + string directoryPath, + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + + foreach (string filePath in Directory.EnumerateFiles(directoryPath)) + { + string extension = Path.GetExtension(filePath); + if (string.Equals(extension, ".cs", StringComparison.OrdinalIgnoreCase)) + { + csharpFilePaths.Add(filePath); + continue; + } + + if (string.Equals(extension, ".asmdef", StringComparison.OrdinalIgnoreCase)) + { + asmdefFilePaths.Add(filePath); + continue; + } + + if (string.Equals(extension, ".asmref", StringComparison.OrdinalIgnoreCase)) + { + asmrefFilePaths.Add(filePath); + } + } + + foreach (string childDirectoryPath in Directory.EnumerateDirectories(directoryPath)) + { + if (ShouldExcludeDirectory(projectRoot, childDirectoryPath)) + { + continue; + } + + CollectCandidateFiles( + projectRoot, + childDirectoryPath, + csharpFilePaths, + asmdefFilePaths, + asmrefFilePaths); + } + } + + private static async Task CollectCandidateFilesAsync( + string projectRoot, + string assetsDirectory, + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths, + IProgress progress, + CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(assetsDirectory), "assetsDirectory must not be null or empty"); + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + Debug.Assert(progress != null, "progress must not be null"); + + Stack pendingDirectories = new(); + pendingDirectories.Push(assetsDirectory); + int inspectedEntryCount = 0; + progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); + + while (pendingDirectories.Count > 0) + { + if (ct.IsCancellationRequested) + { + return; + } + + string directoryPath = pendingDirectories.Pop(); + foreach (string filePath in Directory.EnumerateFiles(directoryPath)) + { + AddCandidateFilePath(filePath, csharpFilePaths, asmdefFilePaths, asmrefFilePaths); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); + await Task.Yield(); + } + } + + foreach (string childDirectoryPath in Directory.EnumerateDirectories(directoryPath)) + { + if (ShouldExcludeDirectory(projectRoot, childDirectoryPath)) + { + continue; + } + + pendingDirectories.Push(childDirectoryPath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + progress.Report(new ThirdPartyToolMigrationProgress(0, 0)); + await Task.Yield(); + } + } + } + } + + private static void AddCandidateFilePath( + string filePath, + List csharpFilePaths, + List asmdefFilePaths, + List asmrefFilePaths) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + Debug.Assert(csharpFilePaths != null, "csharpFilePaths must not be null"); + Debug.Assert(asmdefFilePaths != null, "asmdefFilePaths must not be null"); + Debug.Assert(asmrefFilePaths != null, "asmrefFilePaths must not be null"); + + string extension = Path.GetExtension(filePath); + if (string.Equals(extension, ".cs", StringComparison.OrdinalIgnoreCase)) + { + csharpFilePaths.Add(filePath); + return; + } + + if (string.Equals(extension, ".asmdef", StringComparison.OrdinalIgnoreCase)) + { + asmdefFilePaths.Add(filePath); + return; + } + + if (string.Equals(extension, ".asmref", StringComparison.OrdinalIgnoreCase)) + { + asmrefFilePaths.Add(filePath); + } + } + + private static bool ShouldExcludeDirectory(string projectRoot, string directoryPath) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); + + if (IsProjectRootPackagesDirectory(projectRoot, directoryPath)) + { + return true; + } + + string directoryName = Path.GetFileName(directoryPath); + return ThirdPartyToolMigrationRules.IsExcludedDirectoryName(directoryName); + } + + private static bool IsProjectRootPackagesDirectory(string projectRoot, string directoryPath) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); + + string packagesDirectory = Path.Combine(projectRoot, "Packages"); + string normalizedPackagesDirectory = NormalizeDirectoryPath(packagesDirectory); + string normalizedDirectoryPath = NormalizeDirectoryPath(directoryPath); + return string.Equals( + normalizedDirectoryPath, + normalizedPackagesDirectory, + StringComparison.Ordinal); + } + + private static string NormalizeDirectoryPath(string directoryPath) + { + Debug.Assert(!string.IsNullOrEmpty(directoryPath), "directoryPath must not be null or empty"); + + return Path.GetFullPath(directoryPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs.meta new file mode 100644 index 0000000000..2f1be7a694 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c7ad5b38e4204be3973b8a673f63842 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs new file mode 100644 index 0000000000..9d3f6d9deb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationRegexRewriteRules + { + internal static string ReplaceRegexInCode( + string source, + Regex regex, + Func replacementFactory, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(regex != null, "regex must not be null"); + Debug.Assert(replacementFactory != null, "replacementFactory must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + int localReplacementCount = 0; + string migrated = regex.Replace( + source, + match => + { + if (!codeTextMask.IsCodeAt(match.Index)) + { + return match.Value; + } + + string replacement = replacementFactory(match); + if (string.Equals(match.Value, replacement, StringComparison.Ordinal)) + { + return match.Value; + } + + localReplacementCount++; + return replacement; + }); + replacementCount += localReplacementCount; + return migrated; + } + + internal static string ReplaceLegacyToolAttributesInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyToolAttribute, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + StringBuilder builder = new(source.Length); + int localReplacementCount = 0; + int index = 0; + while (index < source.Length) + { + if (source[index] != '[' || !codeTextMask.IsCodeAt(index)) + { + builder.Append(source[index]); + index++; + continue; + } + + int closingBracketIndex = FindAttributeListClosingBracketIndex( + source, + codeTextMask, + index + 1); + if (closingBracketIndex < 0) + { + builder.Append(source[index]); + index++; + continue; + } + + string attributesSource = source.Substring(index + 1, closingBracketIndex - index - 1); + if (!TryMigrateLegacyToolAttributeList( + attributesSource, + legacyNamespaceAliases, + canMigrateBareLegacyToolAttribute, + out string migratedAttributes)) + { + builder.Append(source, index, closingBracketIndex - index + 1); + index = closingBracketIndex + 1; + continue; + } + + builder.Append('['); + builder.Append(migratedAttributes); + builder.Append(']'); + localReplacementCount++; + index = closingBracketIndex + 1; + } + + replacementCount += localReplacementCount; + return builder.ToString(); + } + + internal static int FindAttributeListClosingBracketIndex( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int nestedBracketDepth = 0; + for (int i = startIndex; i < source.Length; i++) + { + if (!codeTextMask.IsCodeAt(i)) + { + continue; + } + + if (source[i] == '[') + { + nestedBracketDepth++; + continue; + } + + if (source[i] != ']') + { + continue; + } + + if (nestedBracketDepth == 0) + { + return i; + } + + nestedBracketDepth--; + } + + return -1; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs.meta new file mode 100644 index 0000000000..f97eff13bd --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d2336a0f42734d33884c1fb0ecad144 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs new file mode 100644 index 0000000000..1abff40dea --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs @@ -0,0 +1,353 @@ +using System.Text.RegularExpressions; + +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Defines the shared V2-to-V3 migration names, patterns, and replacement tables. + /// + internal static class ThirdPartyToolMigrationRuleCatalog + { + internal const string LegacyNamespace = "io.github.hatayama.uLoopMCP"; + internal const string CurrentNamespace = "io.github.hatayama.UnityCliLoop.ToolContracts"; + internal const string CurrentApplicationNamespace = "io.github.hatayama.UnityCliLoop.Application"; + internal const string CurrentDomainNamespace = "io.github.hatayama.UnityCliLoop.Domain"; + internal const string CurrentFirstPartyToolsNamespace = "io.github.hatayama.UnityCliLoop.FirstPartyTools"; + internal const string LegacyEditorAssemblyName = "uLoopMCP.Editor"; + internal const string LegacyRuntimeAssemblyName = "uLoopMCP.Runtime"; + internal const string CurrentApplicationAssemblyName = "UnityCLILoop.Application"; + internal const string CurrentDomainAssemblyName = "UnityCLILoop.Domain"; + internal const string CurrentFirstPartyToolsScreenshotAssemblyName = + "UnityCLILoop.FirstPartyTools.Screenshot.Editor"; + internal const string CurrentRuntimeAssemblyName = "UnityCLILoop.Runtime"; + internal const string CurrentToolContractsAssemblyName = "UnityCLILoop.ToolContracts"; + internal const string LegacyEditorAssemblyGuidReference = "GUID:214998e563c124e8a88199b2dd1f522d"; + internal const string CurrentApplicationGuidReference = "GUID:214998e563c124e8a88199b2dd1f522d"; + internal const string CurrentDomainGuidReference = "GUID:5c4588558a3624eacbce0f50007cf1eb"; + internal const string CurrentFirstPartyToolsScreenshotGuidReference = + "GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"; + internal const string CurrentRuntimeGuidReference = "GUID:c956a21f824994ef087b6de566690b3d"; + internal const string CurrentToolContractsGuidReference = "GUID:fc3fd32eddbee40e39c2d76dc184957b"; + internal const string DescriptionAttributeArgumentName = "Description"; + internal const string DisplayDevelopmentOnlyAttributeArgumentName = "DisplayDevelopmentOnly"; + internal const string RequiredSecuritySettingAttributeArgumentName = "RequiredSecuritySetting"; + internal const string LegacySecuritySettingsTypeName = "SecuritySettings"; + internal const string CurrentSecuritySettingTypeName = "UnityCliLoopSecuritySetting"; + internal const string LegacyEditorDelayTypeName = "EditorDelay"; + internal const string LegacyEditorDelayMethodName = "DelayFrame"; + internal const string LegacyTimerDelayTypeName = "TimerDelay"; + internal const string LegacyCancellationTokenArgumentName = "cancellationToken"; + internal const string CurrentCancellationTokenArgumentName = "ct"; + internal const string LegacyPlayerLoopTimingTypeName = "PlayerLoopTiming"; + internal const string LegacyTimingArgumentName = "timing"; + internal const string LegacyMainThreadSwitcherTypeName = "MainThreadSwitcher"; + internal const string LegacyMainThreadSwitcherSwitchMethodName = "SwitchToMainThread"; + internal const string CurrentEditorFrameWaiterTypeName = "EditorFrameWaiter"; + internal const string CurrentEditorFrameWaiterMethodName = "WaitFramesOrTimeoutAsync"; + internal const string LegacyEditorWindowCaptureUtilityTypeName = "EditorWindowCaptureUtility"; + internal const string EditorWindowCaptureUtilityCaptureWindowMethodName = "CaptureWindowAsync"; + internal const string EditorWindowCaptureUtilityCaptureGameRenderingMethodName = + "CaptureGameRenderingAsync"; + internal const string EditorWindowCaptureUtilityWindowArgumentName = "window"; + internal const string EditorWindowCaptureUtilityResolutionScaleArgumentName = "resolutionScale"; + internal const string CaptureGameRenderingProjectionTaskVariableName = "__unityCliLoopRenderingTask"; + internal const string CurrentConstantsTypeName = "UnityCliLoopConstants"; + internal const string CurrentEditorFrameWaitTimeoutMemberName = "EDITOR_FRAME_WAIT_TIMEOUT_MS"; + internal const int MinimumRawStringDelimiterQuoteCount = 3; + internal static readonly string[] ExcludedDirectoryNames = + { + ".git", + "Library", + "Temp", + "Logs", + "obj", + "bin", + "Build", + "Builds" + }; + + internal static readonly string LegacyNamespacePattern = + $@"(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex CurrentApplicationGlobalNamespaceAliasRegex = + new( + $@"\bglobal\s+using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex CurrentToolContractsUsingRegex = + new( + $@"\b(?:global\s+)?using\s+(?:global::)?{Regex.Escape(CurrentNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex CurrentFirstPartyToolsGlobalUsingRegex = + new( + $@"\bglobal\s+using\s+(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex CurrentFirstPartyToolsNamespaceAliasRegex = + new( + $@"\b(?:global\s+)?using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex CurrentFirstPartyToolsGlobalNamespaceAliasRegex = + new( + $@"\bglobal\s+using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex LegacyGlobalNamespaceAliasRegex = + new( + $@"\bglobal\s+using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\s*;", + RegexOptions.Compiled); + + internal static readonly Regex LegacyRegistrarRegex = + new(@"\bCustomToolManager\b", RegexOptions.Compiled); + + internal static readonly Regex LegacyQualifiedRegistrarRegex = + new( + $@"(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?" + + @"io\.github\.hatayama\.uLoopMCP\s*;", + RegexOptions.Compiled); + + internal static readonly Regex LegacyToolAttributeEntryRegex = + new( + @"^\s*(?:(?(?:global::)?io\.github\.hatayama\.uLoopMCP\.)|(?[A-Za-z_][A-Za-z0-9_]*)\.)?" + + @"McpTool(?:Attribute)?\s*(?:\((?[\s\S]*)\))?\s*$", + RegexOptions.Compiled); + + internal static readonly Regex LegacyToolInfoConstructorRegex = + new( + $@"new\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.)ToolInfo|(?[A-Za-z_][A-Za-z0-9_]*)\.ToolInfo|(?ToolInfo)|(?[A-Za-z_][A-Za-z0-9_]*))\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyToolInfoTypeAliasRegex = + new( + $@"\busing\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\.ToolInfo\s*;", + RegexOptions.Compiled); + + internal static readonly Regex LegacyGlobalToolInfoTypeAliasRegex = + new( + $@"\bglobal\s+using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\.ToolInfo\s*;", + RegexOptions.Compiled); + + internal static readonly Regex LegacyToolSettingsCatalogItemConstructorRegex = + new( + $@"new\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.)ToolSettingsCatalogItem|(?[A-Za-z_][A-Za-z0-9_]*)\.ToolSettingsCatalogItem|(?ToolSettingsCatalogItem))\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyEditorDelayFrameRegex = + new( + $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorDelayTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorDelayTypeName}|(?{LegacyEditorDelayTypeName}))\s*\.\s*{LegacyEditorDelayMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowRegex = + new( + $@"(?(?:global::)?(?:{Regex.Escape(CurrentFirstPartyToolsNamespace)}|{Regex.Escape(CurrentNamespace)})\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex = + new( + $@"(?(?:global::)?(?:{Regex.Escape(CurrentFirstPartyToolsNamespace)}|{Regex.Escape(CurrentNamespace)})\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex = + new( + $@"(?(?:global::)?(?:{Regex.Escape(CurrentFirstPartyToolsNamespace)}|{Regex.Escape(CurrentNamespace)})\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyEditorWindowCaptureUtilityCaptureGameRenderingInvocationRegex = + new( + $@"(?(?:global::)?(?:{Regex.Escape(CurrentFirstPartyToolsNamespace)}|{Regex.Escape(CurrentNamespace)})\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyTimerDelayInvocationRegex = + new( + $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyTimerDelayTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyTimerDelayTypeName}|(?{LegacyTimerDelayTypeName}))\s*\.\s*(?Wait|WaitThenExecuteOnMainThread)\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex LegacyMainThreadSwitcherSwitchRegex = + new( + $@"(?(?:global::)?(?:{Regex.Escape(CurrentApplicationNamespace)}|{Regex.Escape(CurrentNamespace)})\.){LegacyMainThreadSwitcherTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyMainThreadSwitcherTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyMainThreadSwitcherTypeName}|(?{LegacyMainThreadSwitcherTypeName}))\s*\.\s*{LegacyMainThreadSwitcherSwitchMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex MigratedMainThreadSwitcherSwitchRegex = + new( + $@"(?[^()]*)\)\s*=\s*await\s+(?:(?(?:global::)?(?:{Regex.Escape(CurrentFirstPartyToolsNamespace)}|{Regex.Escape(CurrentNamespace)})\.)|(?[A-Za-z_][A-Za-z0-9_]*)\.)?{LegacyEditorWindowCaptureUtilityTypeName}\.{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + RegexOptions.Compiled); + + internal static readonly Regex TypeDeclarationNameRegex = + new( + @"\b(?:class|struct|interface|enum|record(?:\s+(?:class|struct))?)\s+(?[A-Za-z_][A-Za-z0-9_]*)\b", + RegexOptions.Compiled); + + internal static readonly Regex InterfaceDeclarationNameRegex = + new( + @"\binterface\s+(?[A-Za-z_][A-Za-z0-9_]*)\b", + RegexOptions.Compiled); + + internal static readonly Regex LegacyPlayerLoopTimingDeclarationRegex = + new( + $@"(?m)^[ \t]*(?:(?:private|protected|internal|static|readonly)\s+)*{LegacyPlayerLoopTimingTypeName}\s+(?[A-Za-z_][A-Za-z0-9_]*)(?:\s*=\s*[^;]+)?;\s*(?:\r?\n)?", + RegexOptions.Compiled); + + internal static readonly Regex NamespaceDeclarationRegex = + new( + @"\bnamespace\s+(?[A-Za-z_][A-Za-z0-9_.]*)\s*(?[{;])", + RegexOptions.Compiled); + + internal static readonly TypeReplacementRule[] ToolContractTypeReplacementRules = + { + new("ToolParameterSchemaGenerator", "UnityCliLoopToolParameterSchemaGenerator"), + new("ParameterValidationException", "UnityCliLoopToolParameterValidationException"), + new("Mcp" + "Constants", CurrentConstantsTypeName), + new("McpToolAttribute", "UnityCliLoopToolAttribute"), + new("IUnityTool", "IUnityCliLoopTool"), + new("AbstractUnityTool", "UnityCliLoopTool"), + new("BaseToolSchema", "UnityCliLoopToolSchema"), + new("BaseToolResponse", "UnityCliLoopToolResponse"), + new("SecuritySettings", CurrentSecuritySettingTypeName) + }; + + internal static readonly TypeReplacementRule[] DomainTypeReplacementRules = + { + new("ServiceResult", "ServiceResult"), + new("ToolSettingsCatalogItem", "ToolSettingsCatalogItem") + }; + + internal static readonly TypeReplacementRule[] ApplicationTypeReplacementRules = + { + new("MainThreadSwitcher", "MainThreadSwitcher"), + new("SwitchToMainThreadAwaitable", "SwitchToMainThreadAwaitable") + }; + + internal static readonly TypeReplacementRule[] FirstPartyScreenshotTypeReplacementRules = + { + new("EditorWindowCaptureUtility", "EditorWindowCaptureUtility"), + new("WindowMatchMode", "WindowMatchMode"), + new("CaptureMode", "CaptureMode"), + new("ScreenshotSchema", "ScreenshotSchema"), + new("ScreenshotResponse", "ScreenshotResponse"), + new("ScreenshotInfo", "ScreenshotInfo"), + new("UIElementInfo", "UIElementInfo") + }; + + internal static readonly ReplacementRule[] CSharpReplacementRules = + { + new( + $@"(? internal static class ThirdPartyToolMigrationRules { - internal const string LegacyNamespace = "io.github.hatayama.uLoopMCP"; - internal const string CurrentNamespace = "io.github.hatayama.UnityCliLoop.ToolContracts"; - internal const string CurrentApplicationNamespace = "io.github.hatayama.UnityCliLoop.Application"; - internal const string CurrentDomainNamespace = "io.github.hatayama.UnityCliLoop.Domain"; - internal const string LegacyEditorAssemblyName = "uLoopMCP.Editor"; - internal const string LegacyRuntimeAssemblyName = "uLoopMCP.Runtime"; - private const string CurrentApplicationAssemblyName = "UnityCLILoop.Application"; - private const string CurrentDomainAssemblyName = "UnityCLILoop.Domain"; - private const string CurrentRuntimeAssemblyName = "UnityCLILoop.Runtime"; - private const string CurrentToolContractsAssemblyName = "UnityCLILoop.ToolContracts"; - internal const string LegacyEditorAssemblyGuidReference = "GUID:214998e563c124e8a88199b2dd1f522d"; - internal const string CurrentApplicationGuidReference = "GUID:214998e563c124e8a88199b2dd1f522d"; - internal const string CurrentDomainGuidReference = "GUID:5c4588558a3624eacbce0f50007cf1eb"; - internal const string CurrentRuntimeGuidReference = "GUID:c956a21f824994ef087b6de566690b3d"; - internal const string CurrentToolContractsGuidReference = "GUID:fc3fd32eddbee40e39c2d76dc184957b"; - private const string DescriptionAttributeArgumentName = "Description"; - private const string DisplayDevelopmentOnlyAttributeArgumentName = "DisplayDevelopmentOnly"; - private const string RequiredSecuritySettingAttributeArgumentName = "RequiredSecuritySetting"; - private const string LegacySecuritySettingsTypeName = "SecuritySettings"; - private const string CurrentSecuritySettingTypeName = "UnityCliLoopSecuritySetting"; - private const int MinimumRawStringDelimiterQuoteCount = 3; - private static readonly ConditionalWeakTable CodeTextMaskCache = new(); - - private static readonly string[] ExcludedDirectoryNames = - { - ".git", - "Library", - "Temp", - "Logs", - "obj", - "bin", - "Build", - "Builds" - }; - - private static readonly string LegacyNamespacePattern = - $@"(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\s*;", - RegexOptions.Compiled); - - private static readonly Regex LegacyRegistrarRegex = - new(@"\bCustomToolManager\b", RegexOptions.Compiled); - - private static readonly Regex LegacyQualifiedRegistrarRegex = - new( - $@"(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?" + - @"io\.github\.hatayama\.uLoopMCP\s*;", - RegexOptions.Compiled); - - private static readonly Regex LegacyToolAttributeEntryRegex = - new( - @"^\s*(?:(?(?:global::)?io\.github\.hatayama\.uLoopMCP\.)|(?[A-Za-z_][A-Za-z0-9_]*)\.)?" + - @"McpTool(?:Attribute)?\s*(?:\((?[\s\S]*)\))?\s*$", - RegexOptions.Compiled); - - private static readonly Regex LegacyToolInfoConstructorRegex = - new( - $@"new\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.)ToolInfo|(?[A-Za-z_][A-Za-z0-9_]*)\.ToolInfo|(?ToolInfo)|(?[A-Za-z_][A-Za-z0-9_]*))\s*\(", - RegexOptions.Compiled); - - private static readonly Regex LegacyToolInfoTypeAliasRegex = - new( - $@"\busing\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\.ToolInfo\s*;", - RegexOptions.Compiled); - - private static readonly Regex LegacyGlobalToolInfoTypeAliasRegex = - new( - $@"\bglobal\s+using\s+(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\.ToolInfo\s*;", - RegexOptions.Compiled); - - private static readonly Regex LegacyToolSettingsCatalogItemConstructorRegex = - new( - $@"new\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.)ToolSettingsCatalogItem|(?[A-Za-z_][A-Za-z0-9_]*)\.ToolSettingsCatalogItem|(?ToolSettingsCatalogItem))\s*\(", - RegexOptions.Compiled); - - private static readonly TypeReplacementRule[] ToolContractTypeReplacementRules = - { - new("ToolParameterSchemaGenerator", "UnityCliLoopToolParameterSchemaGenerator"), - new("ParameterValidationException", "UnityCliLoopToolParameterValidationException"), - new("Mcp" + "Constants", "UnityCliLoopConstants"), - new("McpToolAttribute", "UnityCliLoopToolAttribute"), - new("IUnityTool", "IUnityCliLoopTool"), - new("AbstractUnityTool", "UnityCliLoopTool"), - new("BaseToolSchema", "UnityCliLoopToolSchema"), - new("BaseToolResponse", "UnityCliLoopToolResponse"), - new("SecuritySettings", CurrentSecuritySettingTypeName) - }; - - private static readonly TypeReplacementRule[] DomainTypeReplacementRules = - { - new("ServiceResult", "ServiceResult"), - new("ToolSettingsCatalogItem", "ToolSettingsCatalogItem") - }; - - private static readonly ReplacementRule[] CSharpReplacementRules = - { - new( - $@"(?(), - legacyAssemblyToolInfoAliases: Array.Empty()); + return ThirdPartyToolMigrationCSharpRules.MigrateCSharpSource(source); } internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegacyAssembly( string source, bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentApplicationUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, string[] legacyAssemblyAliases, - string[] legacyAssemblyToolInfoAliases) + string[] legacyAssemblyToolInfoAliases, + string[] currentApplicationAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); - - string migratedContent = source; - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - bool hasLegacyNamespaceUsage = RegexMatchesCode(source, LegacyNamespaceRegex); - bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); - bool canMigrateBareLegacyToolAttribute = - hasLegacyAssemblySource || - hasLegacyNamespaceUsage || - legacyNamespaceAliases.Length > 0; - 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; - migratedContent = ReplaceLegacyToolAttributesInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - ref replacementCount); - migratedContent = ReplaceLegacyToolInfoConstructorsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolInfoConstructor, - canMigrateAmbiguousBareLegacyToolInfoConstructor, + return ThirdPartyToolMigrationCSharpRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + hasAssemblyScopedCurrentToolContractsUsing, + hasAssemblyScopedCurrentApplicationUsing, + hasAssemblyScopedCurrentFirstPartyToolsUsing, + legacyAssemblyAliases, legacyAssemblyToolInfoAliases, - ref replacementCount); - migratedContent = ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - ref replacementCount); - migratedContent = ReplaceLegacyRegistrarAliasesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); - - if (shouldApplyContractRenames) - { - migratedContent = ReplaceLegacyDomainTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); - - migratedContent = ReplaceLegacyContractTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - ref replacementCount); - - foreach (ReplacementRule rule in CSharpReplacementRules) - { - migratedContent = ReplaceRegexInCode( - migratedContent, - rule.PatternRegex, - _ => rule.Replacement, - ref replacementCount); - } - } - - if (shouldApplyRegistrarRenames || shouldApplyDomainMetadataRenames) - { - if (shouldApplyRegistrarRenames) - { - migratedContent = ReplaceUnqualifiedLegacyRegistrarReferencesInCode( - migratedContent, - ref replacementCount); - } - - foreach (ReplacementRule rule in RegistrarReplacementRules) - { - migratedContent = ReplaceRegexInCode( - migratedContent, - rule.PatternRegex, - _ => rule.Replacement, - ref replacementCount); - } - - migratedContent = ReplaceLegacyToolInfoTypeReferencesInCode( - migratedContent, - ref replacementCount); - } - - return new ThirdPartyToolMigrationContentResult( - migratedContent, - replacementCount); + currentApplicationAssemblyAliases, + currentFirstPartyToolsAssemblyAliases, + assemblyDeclaredTypeNames); } internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( @@ -311,158 +43,41 @@ internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( bool hasLegacyCSharpSource, bool requiresToolContractsReference, bool requiresApplicationReference, - bool requiresDomainReference) + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) { - Debug.Assert(source != null, "source must not be null"); - - JObject asmdef = JObject.Parse(source); - JToken referencesToken = asmdef["references"]; - JArray references = referencesToken == null ? new JArray() : referencesToken as JArray; - if (referencesToken != null && references == null) - { - return new ThirdPartyToolMigrationContentResult(source, 0); - } - - int replacementCount = 0; - HashSet addedReferences = new(StringComparer.Ordinal); - JArray migratedReferences = new(); - foreach (JToken referenceToken in references) - { - string reference = referenceToken.Value() ?? string.Empty; - string[] migratedReferenceItems = GetMigratedAsmdefReferences( - reference, - hasLegacyCSharpSource, - requiresToolContractsReference, - requiresApplicationReference, - requiresDomainReference); - bool referenceChanged = migratedReferenceItems.Length != 1 || - !string.Equals(migratedReferenceItems[0], reference, StringComparison.Ordinal); - if (referenceChanged) - { - replacementCount++; - } - - foreach (string migratedReference in migratedReferenceItems) - { - string migratedReferenceKey = GetCurrentAsmdefReferenceKey(migratedReference); - if (!addedReferences.Add(migratedReferenceKey)) - { - continue; - } - - migratedReferences.Add(migratedReference); - } - } - - AddRequiredCurrentAsmdefReferences( - migratedReferences, - addedReferences, - hasLegacyCSharpSource || requiresToolContractsReference, + return ThirdPartyToolMigrationAsmdefRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, requiresApplicationReference, requiresDomainReference, - ref replacementCount); - - if (replacementCount == 0) - { - return new ThirdPartyToolMigrationContentResult(source, 0); - } - - asmdef["references"] = migratedReferences; - return new ThirdPartyToolMigrationContentResult( - asmdef.ToString(Formatting.Indented), - replacementCount); + requiresFirstPartyScreenshotReference); } internal static bool ContainsLegacyCSharpApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return ContainsLegacyToolMigrationMarker(source); + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyCSharpApi(source); } 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, "McpTool") || - ContainsTextFragment(source, "CustomToolManager") || - ContainsTextFragment(source, "UnityCliLoopToolRegistrar") || - ContainsTextFragment(source, "ToolInfo")) - { - return true; - } - - foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) - { - if (ContainsTextFragment(source, rule.LegacyName) || - ContainsTextFragment(source, rule.CurrentName)) - { - return true; - } - } - - foreach (TypeReplacementRule rule in DomainTypeReplacementRules) - { - if (ContainsTextFragment(source, rule.LegacyName) || - ContainsTextFragment(source, rule.CurrentName)) - { - return true; - } - } - - return false; + return ThirdPartyToolMigrationDetectionRules.ContainsMigrationCandidateText(source); } internal static bool ContainsLegacyMigrationCandidateText(string source) { - Debug.Assert(source != null, "source must not be null"); - - return ContainsTextFragment(source, LegacyNamespace) || - ContainsTextFragment(source, LegacyEditorAssemblyName) || - ContainsTextFragment(source, LegacyRuntimeAssemblyName); + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyMigrationCandidateText(source); } internal static bool ContainsLegacyAsmdefNameReference(string source) { - Debug.Assert(source != null, "source must not be null"); - - if (!ContainsTextFragment(source, LegacyEditorAssemblyName) && - !ContainsTextFragment(source, LegacyRuntimeAssemblyName)) - { - return false; - } - - JObject asmdef = JObject.Parse(source); - if (asmdef["references"] is not JArray references) - { - return false; - } - - foreach (JToken reference in references) - { - string referenceValue = reference.Value() ?? string.Empty; - if (string.Equals(referenceValue, LegacyEditorAssemblyName, StringComparison.Ordinal) || - string.Equals(referenceValue, LegacyRuntimeAssemblyName, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyAsmdefNameReference(source); } internal static bool ContainsLegacyRegistrarApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return ContainsLegacyRegistrarApiForAssembly( - source, - hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), - legacyAssemblyAliases: Array.Empty()); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyRegistrarApi(source); } internal static bool ContainsLegacyRegistrarApiForAssembly( @@ -470,35 +85,41 @@ internal static bool ContainsLegacyRegistrarApiForAssembly( bool hasLegacyAssemblySource, string[] legacyAssemblyAliases) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - bool canMigrateBareLegacyRegistrar = - hasLegacyAssemblySource || - ContainsLegacyToolMigrationMarker(source) || - legacyNamespaceAliases.Length > 0; - return ContainsLegacyRegistrarReference( - source, - canMigrateBareLegacyRegistrar, - legacyNamespaceAliases); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyRegistrarApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases); } internal static bool ContainsCurrentRegistrarApi(string source) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentRegistrarApi(source); + } + + internal static bool ContainsLegacyApplicationApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyApplicationApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases, currentApplicationAssemblyAliases, assemblyDeclaredTypeNames); + } - return RegexMatchesCode(source, CurrentRegistrarRegex); + internal static bool ContainsCurrentApplicationApi(string source) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentApplicationApi(source); } - internal static bool ContainsRegistrarDomainReturnApi(string source) + internal static bool ContainsCurrentApplicationApiForAssembly( + string source, + bool hasAssemblyScopedCurrentApplicationUsing, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentApplicationApiForAssembly(source, hasAssemblyScopedCurrentApplicationUsing, currentApplicationAssemblyAliases, assemblyDeclaredTypeNames); + } - return ContainsRegistrarDomainReturnApiForAssembly( - source, - hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), - legacyAssemblyAliases: Array.Empty()); + internal static bool ContainsRegistrarDomainReturnApi(string source) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsRegistrarDomainReturnApi(source); } internal static bool ContainsRegistrarDomainReturnApiForAssembly( @@ -506,41 +127,17 @@ internal static bool ContainsRegistrarDomainReturnApiForAssembly( bool hasLegacyAssemblySource, string[] legacyAssemblyAliases) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - if (RegexMatchesCode(source, CurrentRegistrarDomainReturnRegex)) - { - return true; - } - - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - bool canMigrateBareLegacyRegistrar = - hasLegacyAssemblySource || - ContainsLegacyToolMigrationMarker(source) || - legacyNamespaceAliases.Length > 0; - return ContainsLegacyRegistrarDomainReturnReference( - source, - canMigrateBareLegacyRegistrar, - legacyNamespaceAliases); + return ThirdPartyToolMigrationApiDetectionRules.ContainsRegistrarDomainReturnApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases); } internal static bool ContainsCurrentToolContractsApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentToolContractsApi(source); } internal static bool ContainsLegacyDomainMetadataApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, LegacyDomainMetadataRegex) || - ContainsLegacyDomainHelperApiForAssembly( - source, - hasLegacyAssemblySource: ContainsLegacyToolMigrationMarker(source), - legacyAssemblyAliases: Array.Empty()); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyDomainMetadataApi(source); } internal static bool ContainsLegacyDomainHelperApiForAssembly( @@ -548,2548 +145,279 @@ internal static bool ContainsLegacyDomainHelperApiForAssembly( bool hasLegacyAssemblySource, string[] legacyAssemblyAliases) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - return ContainsLegacyDomainHelperReference( - source, - hasLegacyAssemblySource, - legacyNamespaceAliases); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyDomainHelperApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases); } internal static bool ContainsCurrentDomainMetadataApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return ContainsCurrentDomainMetadataApiForAssembly( - source, - hasAssemblyScopedCurrentDomainUsing: false); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentDomainMetadataApi(source); } internal static bool ContainsCurrentDomainMetadataApiForAssembly( string source, bool hasAssemblyScopedCurrentDomainUsing) { - Debug.Assert(source != null, "source must not be null"); - - bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); - bool canUseBareCurrentDomainType = hasAssemblyScopedCurrentDomainUsing || hasCurrentDomainNamespaceUsage; - return RegexMatchesCode(source, CurrentDomainMetadataRegex) || - ContainsCurrentDomainHelperApiForAssembly(source, canUseBareCurrentDomainType) || - (canUseBareCurrentDomainType && RegexMatchesCode(source, LegacyDomainMetadataRegex)); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentDomainMetadataApiForAssembly(source, hasAssemblyScopedCurrentDomainUsing); } - internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] legacyAssemblyAliases) + internal static bool ContainsLegacyFirstPartyScreenshotApiForAssembly( + string source, + bool hasLegacyAssemblySource, + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - return RegexMatchesCode(source, LegacyBaseTypeUsageRegex) || - RegexMatchesCode(source, LegacyAssemblyScopedApiUsageRegex) || - ContainsLegacyAssemblyScopedTypeReference(source) || - ContainsLegacyAliasQualifiedAssemblyScopedApi(source, legacyAssemblyAliases) || - ContainsLegacyToolAttributeList( - source, - legacyAssemblyAliases, - canMigrateBareLegacyToolAttribute: true); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyFirstPartyScreenshotApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases, assemblyDeclaredTypeNames); } - internal static bool ContainsLegacyGlobalUsing(string source) + internal static bool ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, LegacyGlobalUsingRegex); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly(source, hasLegacyAssemblySource, hasAssemblyScopedCurrentToolContractsUsing, hasAssemblyScopedCurrentFirstPartyToolsUsing, legacyAssemblyAliases, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } - internal static bool ContainsCurrentDomainGlobalUsing(string source) + internal static bool ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly(source, hasLegacyAssemblySource, hasAssemblyScopedCurrentToolContractsUsing, hasAssemblyScopedCurrentFirstPartyToolsUsing, legacyAssemblyAliases, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } - internal static string[] GetLegacyGlobalNamespaceAliases(string source) + internal static bool ContainsCurrentFirstPartyScreenshotApi(string source) { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyGlobalNamespaceAliasRegex, "alias"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentFirstPartyScreenshotApi(source); } - internal static string[] GetLegacyGlobalToolInfoTypeAliases(string source) + internal static bool ContainsCurrentFirstPartyScreenshotApiForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyGlobalToolInfoTypeAliasRegex, "alias"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentFirstPartyScreenshotApiForAssembly(source, hasAssemblyScopedCurrentFirstPartyToolsUsing, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } - internal static bool ContainsLegacyGlobalToolInfoTypeAlias(string source) + internal static bool ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, LegacyGlobalToolInfoTypeAliasRegex); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly(source, hasAssemblyScopedCurrentFirstPartyToolsUsing, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } - internal static bool ContainsLegacyTypeAliasReference(string source, string[] aliases) + internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] legacyAssemblyAliases) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(aliases != null, "aliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - foreach (string alias in aliases) - { - Regex aliasRegex = new($@"(? string.Equals( - excludedDirectoryName, - directoryName, - StringComparison.OrdinalIgnoreCase)); + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyGlobalUsing(source); } - internal static string[] GetExcludedDirectoryNames() + internal static bool ContainsCurrentDomainGlobalUsing(string source) { - string[] names = new string[ExcludedDirectoryNames.Length]; - Array.Copy(ExcludedDirectoryNames, names, ExcludedDirectoryNames.Length); - return names; + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentDomainGlobalUsing(source); } - private static string[] GetMigratedAsmdefReferences( - string reference, - bool hasLegacyCSharpSource, - bool requiresToolContractsReference, - bool requiresApplicationReference, - bool requiresDomainReference) + internal static bool ContainsCurrentToolContractsGlobalUsing(string source) { - if (string.Equals(reference, LegacyEditorAssemblyName, StringComparison.Ordinal)) - { - return GetMigratedLegacyEditorReferences(requiresApplicationReference, requiresDomainReference); - } - - if (string.Equals(reference, LegacyRuntimeAssemblyName, StringComparison.Ordinal)) - { - return new[] { CurrentRuntimeGuidReference }; - } - - if (hasLegacyCSharpSource && - string.Equals(reference, LegacyEditorAssemblyGuidReference, StringComparison.Ordinal)) - { - return GetMigratedLegacyEditorReferences(requiresApplicationReference, requiresDomainReference); - } - - return new[] { reference }; + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentToolContractsGlobalUsing(source); } - private static string[] GetMigratedLegacyEditorReferences( - bool requiresApplicationReference, - bool requiresDomainReference) + internal static bool ContainsCurrentApplicationGlobalUsing(string source) { - List references = new() - { - CurrentToolContractsGuidReference - }; - - if (requiresApplicationReference) - { - references.Add(CurrentApplicationGuidReference); - } - - if (requiresDomainReference) - { - references.Add(CurrentDomainGuidReference); - } - - return references.ToArray(); + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentApplicationGlobalUsing(source); } - private static void AddRequiredCurrentAsmdefReferences( - JArray references, - HashSet addedReferences, - bool requiresToolContractsReference, - bool requiresApplicationReference, - bool requiresDomainReference, - ref int replacementCount) + internal static bool ContainsCurrentApplicationNamespaceAlias(string source) { - Debug.Assert(references != null, "references must not be null"); - Debug.Assert(addedReferences != null, "addedReferences must not be null"); - - if (requiresToolContractsReference || requiresApplicationReference || requiresDomainReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentToolContractsGuidReference, - ref replacementCount); - } - - if (requiresApplicationReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentApplicationGuidReference, - ref replacementCount); - } - - if (requiresDomainReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentDomainGuidReference, - ref replacementCount); - } + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentApplicationNamespaceAlias(source); } - private static void AddRequiredCurrentAsmdefReference( - JArray references, - HashSet addedReferences, - string reference, - ref int replacementCount) + internal static bool ContainsCurrentFirstPartyToolsGlobalUsing(string source) { - Debug.Assert(references != null, "references must not be null"); - Debug.Assert(addedReferences != null, "addedReferences must not be null"); - Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); - - string referenceKey = GetCurrentAsmdefReferenceKey(reference); - if (!addedReferences.Add(referenceKey)) - { - return; - } - - references.Add(reference); - replacementCount++; + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentFirstPartyToolsGlobalUsing(source); } - private static string GetCurrentAsmdefReferenceKey(string reference) + internal static bool ContainsCurrentFirstPartyToolsNamespaceAlias(string source) { - Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); - - if (IsCurrentAsmdefReference( - reference, - CurrentRuntimeAssemblyName, - CurrentRuntimeGuidReference)) - { - return CurrentRuntimeAssemblyName; - } - - if (IsCurrentAsmdefReference( - reference, - CurrentToolContractsAssemblyName, - CurrentToolContractsGuidReference)) - { - return CurrentToolContractsAssemblyName; - } - - if (IsCurrentAsmdefReference( - reference, - CurrentApplicationAssemblyName, - CurrentApplicationGuidReference)) - { - return CurrentApplicationAssemblyName; - } + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source); + } - if (IsCurrentAsmdefReference( - reference, - CurrentDomainAssemblyName, - CurrentDomainGuidReference)) - { - return CurrentDomainAssemblyName; - } + internal static string[] GetLegacyGlobalNamespaceAliases(string source) + { + return ThirdPartyToolMigrationDetectionRules.GetLegacyGlobalNamespaceAliases(source); + } - return reference; + internal static string[] GetLegacyGlobalToolInfoTypeAliases(string source) + { + return ThirdPartyToolMigrationDetectionRules.GetLegacyGlobalToolInfoTypeAliases(source); } - private static bool IsCurrentAsmdefReference( - string reference, - string assemblyName, - string guidReference) + internal static string[] GetCurrentApplicationGlobalNamespaceAliases(string source) { - Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(guidReference), "guidReference must not be null or empty"); + return ThirdPartyToolMigrationDetectionRules.GetCurrentApplicationGlobalNamespaceAliases(source); + } - return string.Equals(reference, assemblyName, StringComparison.Ordinal) || - string.Equals(reference, guidReference, StringComparison.Ordinal); + internal static string[] GetCurrentFirstPartyToolsGlobalNamespaceAliases(string source) + { + return ThirdPartyToolMigrationDetectionRules.GetCurrentFirstPartyToolsGlobalNamespaceAliases(source); } - private static bool TryMigrateLegacyToolAttributeList( - string attributesSource, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyToolAttribute, - out string migratedAttributes) + internal static string[] GetDeclaredTypeNames(string source) { - Debug.Assert(attributesSource != null, "attributesSource must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + return ThirdPartyToolMigrationDetectionRules.GetDeclaredTypeNames(source); + } - string[] attributes = SplitAttributeArguments(attributesSource); - List migratedAttributeItems = new(); - bool changed = false; - foreach (string attribute in attributes) - { - string trimmedAttribute = attribute.Trim(); - if (TryMigrateLegacyToolAttributeEntry( - trimmedAttribute, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - out string migratedAttribute)) - { - migratedAttributeItems.Add(migratedAttribute); - changed = true; - continue; - } + internal static bool ContainsLegacyGlobalToolInfoTypeAlias(string source) + { + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyGlobalToolInfoTypeAlias(source); + } - migratedAttributeItems.Add(trimmedAttribute); - } + internal static bool ContainsLegacyTypeAliasReference(string source, string[] aliases) + { + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyTypeAliasReference(source, aliases); + } - if (!changed) - { - migratedAttributes = string.Empty; - return false; - } + internal static bool IsExcludedDirectoryName(string directoryName) + { + return ThirdPartyToolMigrationDetectionRules.IsExcludedDirectoryName(directoryName); + } - migratedAttributes = string.Join(", ", migratedAttributeItems); - return true; + internal static string[] GetExcludedDirectoryNames() + { + return ThirdPartyToolMigrationDetectionRules.GetExcludedDirectoryNames(); } - private static bool TryMigrateLegacyToolAttributeEntry( - string attribute, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyToolAttribute, - out string migratedAttribute) + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + string source, + string originalSource, + RemovedLegacyPlayerLoopTimingSignature[] removedSignatures, + string[] legacyAssemblyAliases) { - Debug.Assert(attribute != null, "attribute must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + return ThirdPartyToolMigrationTimingCallerRules.RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + source, + originalSource, + removedSignatures, + legacyAssemblyAliases); + } - Match match = LegacyToolAttributeEntryRegex.Match(attribute); - if (!match.Success) - { - migratedAttribute = string.Empty; - return false; - } + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + string source, + string originalSource, + string[] legacyAssemblyAliases, + bool canMigrateBareLegacyPlayerLoopTiming, + string[] migratedCalleeMethodNames) + { + return ThirdPartyToolMigrationTimingCallerRules.RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + source, + originalSource, + legacyAssemblyAliases, + canMigrateBareLegacyPlayerLoopTiming, + migratedCalleeMethodNames); + } - bool hasQualifier = match.Groups["qualifier"].Success; - bool hasAlias = match.Groups["alias"].Success; - if (!hasQualifier && !hasAlias && !canMigrateBareLegacyToolAttribute) + internal readonly struct RemovedLegacyPlayerLoopTimingSignature + { + public RemovedLegacyPlayerLoopTimingSignature( + string methodName, + string declaringTypeName, + LegacyPlayerLoopTimingParameterDeclaration[] originalParameters, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters) { - migratedAttribute = string.Empty; - return false; - } + Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName must not be null or empty"); + Debug.Assert(declaringTypeName != null, "declaringTypeName must not be null"); + Debug.Assert(originalParameters != null, "originalParameters must not be null"); + Debug.Assert(removedParameters != null, "removedParameters must not be null"); - if (hasAlias && !legacyNamespaceAliases.Contains(match.Groups["alias"].Value)) - { - migratedAttribute = string.Empty; - return false; + MethodName = methodName; + DeclaringTypeName = declaringTypeName; + OriginalParameters = + originalParameters ?? Array.Empty(); + RemovedParameters = removedParameters ?? Array.Empty(); } - string argumentsSource = match.Groups["arguments"].Value; - string[] migratedArguments = GetMigratedSupportedAttributeArguments(argumentsSource); - string attributeName = hasQualifier || hasAlias - ? $"{CurrentNamespace}.UnityCliLoopTool" - : "UnityCliLoopTool"; - migratedAttribute = migratedArguments.Length == 0 - ? attributeName - : $"{attributeName}({string.Join(", ", migratedArguments)})"; - return true; + public string MethodName { get; } + public string DeclaringTypeName { get; } + public LegacyPlayerLoopTimingParameterDeclaration[] OriginalParameters { get; } + public RemovedLegacyPlayerLoopTimingParameter[] RemovedParameters { get; } } - private static string[] GetMigratedSupportedAttributeArguments(string argumentsSource) + internal readonly struct LegacyPlayerLoopTimingParameterDeclaration { - Debug.Assert(argumentsSource != null, "argumentsSource must not be null"); - - List migratedArguments = new(); - string[] arguments = SplitAttributeArguments(argumentsSource); - foreach (string argument in arguments) + public LegacyPlayerLoopTimingParameterDeclaration( + int index, + string typeName, + string name, + bool hasDefaultValue) { - string trimmedArgument = argument.Trim(); - if (trimmedArgument.Length == 0) - { - continue; - } - - if (IsNamedAttributeArgument(trimmedArgument, DescriptionAttributeArgumentName)) - { - continue; - } - - if (IsNamedAttributeArgument(trimmedArgument, DisplayDevelopmentOnlyAttributeArgumentName)) - { - migratedArguments.Add(trimmedArgument); - continue; - } + Debug.Assert(index >= 0, "index must not be negative"); + Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be null or empty"); + Debug.Assert(!string.IsNullOrEmpty(name), "name must not be null or empty"); - if (IsNamedAttributeArgument(trimmedArgument, RequiredSecuritySettingAttributeArgumentName)) - { - migratedArguments.Add(trimmedArgument); - } + Index = index; + TypeName = typeName; + Name = name; + HasDefaultValue = hasDefaultValue; } - return migratedArguments.ToArray(); + public int Index { get; } + public string TypeName { get; } + public string Name { get; } + public bool HasDefaultValue { get; } } - private static string[] SplitAttributeArguments(string argumentsSource) + internal readonly struct RemovedLegacyPlayerLoopTimingParameter { - 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++) + public RemovedLegacyPlayerLoopTimingParameter(int index, string name) { - char current = argumentsSource[i]; - if (isInRegularString) - { - if (current == '\\') - { - i++; - continue; - } - - if (current == '"') - { - isInRegularString = false; - } - - continue; - } - - if (isInVerbatimString) - { - if (current != '"') - { - continue; - } - - if (i + 1 < argumentsSource.Length && argumentsSource[i + 1] == '"') - { - i++; - continue; - } - - isInVerbatimString = false; - continue; - } - - if (isInRawString) - { - if (HasRepeatedCharacterAt(argumentsSource, i, '"', rawStringQuoteCount)) - { - i += rawStringQuoteCount - 1; - isInRawString = false; - } - - continue; - } - - if (isInCharLiteral) - { - if (current == '\\') - { - i++; - continue; - } - - if (current == '\'') - { - isInCharLiteral = false; - } - - continue; - } - - if (IsRawStringStart(argumentsSource, i)) - { - int dollarCount = CountRepeatedCharacter(argumentsSource, i, '$'); - int quoteIndex = i + dollarCount; - rawStringQuoteCount = CountRepeatedCharacter(argumentsSource, quoteIndex, '"'); - isInRawString = true; - i = quoteIndex + rawStringQuoteCount - 1; - continue; - } + Debug.Assert(index >= 0, "index must not be negative"); + Debug.Assert(!string.IsNullOrEmpty(name), "name must not be null or empty"); - if (StartsWith(argumentsSource, i, "@\"") || - StartsWith(argumentsSource, i, "$@\"") || - StartsWith(argumentsSource, i, "@$\"")) - { - isInVerbatimString = true; - i += GetStringPrefixLength(argumentsSource, i); - continue; - } - - if (StartsWith(argumentsSource, i, "$\"")) - { - isInRegularString = true; - i++; - continue; - } - - if (current == '"') - { - isInRegularString = true; - continue; - } - - if (current == '\'') - { - isInCharLiteral = true; - continue; - } - - if (current == '(' || current == '[' || current == '{') - { - nestingDepth++; - continue; - } - - if (current == ')' || current == ']' || current == '}') - { - nestingDepth = Math.Max(0, nestingDepth - 1); - continue; - } - - if (current != ',' || nestingDepth != 0) - { - continue; - } - - arguments.Add(argumentsSource.Substring(argumentStartIndex, i - argumentStartIndex)); - argumentStartIndex = i + 1; - } - - arguments.Add(argumentsSource.Substring(argumentStartIndex)); - return arguments.ToArray(); - } - - private static string[] GetLegacyNamespaceAliases(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyNamespaceAliasRegex, "alias"); - } - - private static string[] GetLegacyToolInfoTypeAliases(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyToolInfoTypeAliasRegex, "alias"); - } - - private static string[] GetCombinedLegacyNamespaceAliases( - string source, - string[] legacyAssemblyAliases) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - return GetLegacyNamespaceAliases(source) - .Concat(legacyAssemblyAliases) - .Distinct(StringComparer.Ordinal) - .ToArray(); - } - - private static string[] GetCombinedLegacyToolInfoTypeAliases( - string source, - string[] legacyAssemblyToolInfoAliases) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); - - return GetLegacyToolInfoTypeAliases(source) - .Concat(legacyAssemblyToolInfoAliases) - .Distinct(StringComparer.Ordinal) - .ToArray(); - } - - private static string[] GetRegexGroupValuesInCode(string source, Regex regex, string groupName) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(regex != null, "regex must not be null"); - Debug.Assert(!string.IsNullOrEmpty(groupName), "groupName must not be null or empty"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - List values = new(); - MatchCollection matches = regex.Matches(source); - foreach (Match match in matches) - { - if (!codeTextMask.IsCodeAt(match.Index)) - { - continue; - } - - values.Add(match.Groups[groupName].Value); - } - - return values.ToArray(); - } - - private static string ReplaceLegacyRegistrarAliasesInCode( - string source, - string[] aliases, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(aliases != null, "aliases must not be null"); - - string migratedContent = source; - foreach (string alias in aliases) - { - Regex aliasRegistrarRegex = new( - $@"(? $"{CurrentApplicationNamespace}.UnityCliLoopToolRegistrar", - ref replacementCount); - - Regex aliasToolInfoRegex = new( - $@"(? $"{CurrentDomainNamespace}.ToolInfo", - ref replacementCount); - } - - return migratedContent; - } - - private static string ReplaceUnqualifiedLegacyRegistrarReferencesInCode( - string source, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - - Regex unqualifiedRegistrarRegex = - new(@"(? ShouldMigrateLegacyTypeReference(source, "CustomToolManager", match.Index) - ? $"{CurrentApplicationNamespace}.UnityCliLoopToolRegistrar" - : match.Value, - ref replacementCount); - } - - private static string ReplaceLegacyContractTypeNamesInCode( - string source, - string[] aliases, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(aliases != null, "aliases must not be null"); - - string migratedContent = source; - foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) - { - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", - RegexOptions.Compiled); - migratedContent = ReplaceRegexInCode( - migratedContent, - fullyQualifiedRegex, - _ => $"{CurrentNamespace}.{rule.CurrentName}", - ref replacementCount); - - foreach (string alias in aliases) - { - Regex aliasRegex = new( - $@"(? $"{alias}.{rule.CurrentName}", - ref replacementCount); - } - - Regex unqualifiedRegex = new( - $@"(? ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? rule.CurrentName - : match.Value, - ref replacementCount); - } - - return migratedContent; - } - - private static string ReplaceLegacyDomainTypeNamesInCode( - string source, - string[] aliases, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(aliases != null, "aliases must not be null"); - - string migratedContent = source; - foreach (TypeReplacementRule rule in DomainTypeReplacementRules) - { - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", - RegexOptions.Compiled); - migratedContent = ReplaceRegexInCode( - migratedContent, - fullyQualifiedRegex, - _ => $"{CurrentDomainNamespace}.{rule.CurrentName}", - ref replacementCount); - - foreach (string alias in aliases) - { - Regex aliasRegex = new( - $@"(? $"{CurrentDomainNamespace}.{rule.CurrentName}", - ref replacementCount); - } - - Regex unqualifiedRegex = new( - $@"(? ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentDomainNamespace}.{rule.CurrentName}" - : match.Value, - ref replacementCount); - } - - return migratedContent; - } - - private static string ReplaceLegacyToolInfoTypeReferencesInCode(string source, ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - - return ReplaceRegexInCode( - source, - UnqualifiedToolInfoRegex, - match => ShouldMigrateLegacyToolInfoTypeReference(source, match.Index) - ? $"{CurrentDomainNamespace}.ToolInfo" - : match.Value, - ref replacementCount); - } - - private static bool ShouldMigrateLegacyToolInfoTypeReference(string source, int toolInfoIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(toolInfoIndex >= 0, "toolInfoIndex must not be negative"); - - return ShouldMigrateLegacyTypeReference(source, "ToolInfo", toolInfoIndex); - } - - private static bool ShouldMigrateLegacyTypeReference(string source, string typeName, int typeNameIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be empty"); - Debug.Assert(typeNameIndex >= 0, "typeNameIndex must not be negative"); - - if (IsLegacyAssemblyScopedTypeDeclaration(source, typeNameIndex)) - { - return false; - } - - char nextCharacter = ReadNextNonWhitespaceCharacter(source, typeNameIndex + typeName.Length); - char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, typeNameIndex); - if (nextCharacter == '(' && !PreviousCodeTokenEquals(source, typeNameIndex, "new")) - { - return false; - } - - return !IsDeclarationIdentifierTerminator(nextCharacter) || - !CanPrecedeDeclarationIdentifier(previousCharacter); - } - - private static string ReplaceLegacyToolInfoConstructorsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyToolInfoConstructor, - bool canMigrateAmbiguousBareLegacyToolInfoConstructor, - string[] legacyAssemblyToolInfoAliases, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - Debug.Assert(legacyAssemblyToolInfoAliases != null, "legacyAssemblyToolInfoAliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - string[] legacyToolInfoTypeAliases = - GetCombinedLegacyToolInfoTypeAliases(source, legacyAssemblyToolInfoAliases); - MatchCollection matches = LegacyToolInfoConstructorRegex.Matches(source); - StringBuilder builder = new(source.Length); - int sourceCopyIndex = 0; - int localReplacementCount = 0; - foreach (Match match in matches) - { - if (match.Index < sourceCopyIndex || - !codeTextMask.IsCodeAt(match.Index) || - !IsLegacyToolInfoConstructorMatch( - match, - legacyNamespaceAliases, - legacyToolInfoTypeAliases, - canMigrateBareLegacyToolInfoConstructor)) - { - continue; - } - - int openParenthesisIndex = match.Index + match.Length - 1; - int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( - source, - codeTextMask, - openParenthesisIndex); - if (closingParenthesisIndex < 0) - { - continue; - } - - string argumentsSource = source.Substring( - openParenthesisIndex + 1, - closingParenthesisIndex - openParenthesisIndex - 1); - string[] arguments = SplitAttributeArguments(argumentsSource); - if (!ShouldMigrateLegacyToolInfoConstructorArguments( - match, - arguments, - canMigrateAmbiguousBareLegacyToolInfoConstructor)) - { - continue; - } - - string[] migratedArguments = GetMigratedToolInfoConstructorArguments(arguments); - if (migratedArguments.Length == arguments.Length) - { - continue; - } - - builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append($"new {CurrentDomainNamespace}.ToolInfo("); - builder.Append(string.Join(", ", migratedArguments)); - builder.Append(')'); - sourceCopyIndex = closingParenthesisIndex + 1; - localReplacementCount++; - } - - if (localReplacementCount == 0) - { - return source; - } - - builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); - replacementCount += localReplacementCount; - return builder.ToString(); - } - - private static string ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyConstructor, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - MatchCollection matches = LegacyToolSettingsCatalogItemConstructorRegex.Matches(source); - StringBuilder builder = new(source.Length); - int sourceCopyIndex = 0; - int localReplacementCount = 0; - foreach (Match match in matches) - { - if (match.Index < sourceCopyIndex || - !codeTextMask.IsCodeAt(match.Index) || - !IsLegacyToolSettingsCatalogItemConstructorMatch( - match, - legacyNamespaceAliases, - canMigrateBareLegacyConstructor)) - { - continue; - } - - int openParenthesisIndex = match.Index + match.Length - 1; - int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( - source, - codeTextMask, - openParenthesisIndex); - if (closingParenthesisIndex < 0) - { - continue; - } - - string argumentsSource = source.Substring( - openParenthesisIndex + 1, - closingParenthesisIndex - openParenthesisIndex - 1); - string[] arguments = SplitAttributeArguments(argumentsSource); - string[] migratedArguments = GetMigratedToolSettingsCatalogItemConstructorArguments(arguments); - if (migratedArguments.Length == arguments.Length) - { - continue; - } - - builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append($"new {CurrentDomainNamespace}.ToolSettingsCatalogItem("); - builder.Append(string.Join(", ", migratedArguments)); - builder.Append(')'); - sourceCopyIndex = closingParenthesisIndex + 1; - localReplacementCount++; - } - - if (localReplacementCount == 0) - { - return source; - } - - builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); - replacementCount += localReplacementCount; - return builder.ToString(); - } - - private static bool IsLegacyToolSettingsCatalogItemConstructorMatch( - Match match, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyConstructor) - { - Debug.Assert(match != null, "match must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - - if (match.Groups["qualifier"].Success) - { - return true; - } - - if (match.Groups["alias"].Success) - { - return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); - } - - return match.Groups["toolSettingsCatalogItem"].Success && canMigrateBareLegacyConstructor; - } - - private static bool ShouldMigrateLegacyToolInfoConstructorArguments( - Match match, - string[] arguments, - bool canMigrateAmbiguousBareLegacyToolInfoConstructor) - { - Debug.Assert(match != null, "match must not be null"); - Debug.Assert(arguments != null, "arguments must not be null"); - - if (!match.Groups["toolInfo"].Success) - { - return true; - } - - return canMigrateAmbiguousBareLegacyToolInfoConstructor || - HasUnambiguousLegacyToolInfoConstructorArguments(arguments); - } - - private static bool HasUnambiguousLegacyToolInfoConstructorArguments(string[] arguments) - { - Debug.Assert(arguments != null, "arguments must not be null"); - - if (arguments.Length == 4) - { - return true; - } - - if (arguments.Length == 3 && IsLikelyLegacyDescriptionArgument(arguments[1])) - { - return true; - } - - return FindNamedConstructorArgumentIndex( - arguments, - DescriptionAttributeArgumentName.ToLowerInvariant()) >= 0; - } - - private static bool IsLikelyLegacyDescriptionArgument(string argument) - { - Debug.Assert(argument != null, "argument must not be null"); - - string trimmedArgument = argument.Trim(); - return IsStringLiteralArgument(trimmedArgument) || - string.Equals(trimmedArgument, "description", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsStringLiteralArgument(string argument) - { - Debug.Assert(argument != null, "argument must not be null"); - - return argument.StartsWith("\"", StringComparison.Ordinal) || - argument.StartsWith("@\"", StringComparison.Ordinal) || - argument.StartsWith("$\"", StringComparison.Ordinal) || - argument.StartsWith("$@\"", StringComparison.Ordinal) || - argument.StartsWith("@$\"", StringComparison.Ordinal); - } - - private static bool IsLegacyToolInfoConstructorMatch( - Match match, - string[] legacyNamespaceAliases, - string[] legacyToolInfoTypeAliases, - bool canMigrateBareLegacyToolInfoConstructor) - { - Debug.Assert(match != null, "match must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - Debug.Assert(legacyToolInfoTypeAliases != null, "legacyToolInfoTypeAliases must not be null"); - - if (match.Groups["qualifier"].Success) - { - return true; - } - - if (match.Groups["alias"].Success) - { - return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); - } - - if (match.Groups["typeAlias"].Success) - { - return legacyToolInfoTypeAliases.Contains(match.Groups["typeAlias"].Value); - } - - if (match.Groups["toolInfo"].Success) - { - return canMigrateBareLegacyToolInfoConstructor; - } - - return false; - } - - private static string[] GetMigratedToolInfoConstructorArguments(string[] arguments) - { - Debug.Assert(arguments != null, "arguments must not be null"); - - int namedDescriptionArgumentIndex = FindNamedConstructorArgumentIndex( - arguments, - DescriptionAttributeArgumentName.ToLowerInvariant()); - if (namedDescriptionArgumentIndex >= 0) - { - return RemoveArgumentAt(arguments, namedDescriptionArgumentIndex); - } - - if (arguments.Length == 4) - { - return new[] - { - arguments[0].Trim(), - arguments[2].Trim(), - arguments[3].Trim() - }; - } - - if (arguments.Length == 3) - { - return new[] - { - arguments[0].Trim(), - arguments[2].Trim() - }; - } - - return arguments; - } - - private static string[] GetMigratedToolSettingsCatalogItemConstructorArguments(string[] arguments) - { - Debug.Assert(arguments != null, "arguments must not be null"); - - int namedDescriptionArgumentIndex = FindNamedConstructorArgumentIndex( - arguments, - DescriptionAttributeArgumentName.ToLowerInvariant()); - if (namedDescriptionArgumentIndex >= 0) - { - return RemoveArgumentAt(arguments, namedDescriptionArgumentIndex); - } - - if (arguments.Length == 4) - { - return new[] - { - arguments[0].Trim(), - arguments[2].Trim(), - arguments[3].Trim() - }; - } - - return arguments; - } - - private static int FindNamedConstructorArgumentIndex(string[] arguments, string argumentName) - { - Debug.Assert(arguments != null, "arguments must not be null"); - Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); - - for (int i = 0; i < arguments.Length; i++) - { - if (IsNamedConstructorArgument(arguments[i].Trim(), argumentName)) - { - return i; - } - } - - return -1; - } - - private static bool IsNamedConstructorArgument(string argument, string argumentName) - { - Debug.Assert(argument != null, "argument must not be null"); - Debug.Assert(!string.IsNullOrEmpty(argumentName), "argumentName must not be null or empty"); - - int colonIndex = argument.IndexOf(':'); - if (colonIndex <= 0) - { - return false; - } - - string possibleArgumentName = argument.Substring(0, colonIndex).Trim(); - return string.Equals(possibleArgumentName, argumentName, StringComparison.Ordinal); - } - - private static string[] RemoveArgumentAt(string[] arguments, int removeIndex) - { - Debug.Assert(arguments != null, "arguments must not be null"); - Debug.Assert(removeIndex >= 0, "removeIndex must not be negative"); - Debug.Assert(removeIndex < arguments.Length, "removeIndex must be within arguments"); - - List migratedArguments = new(); - for (int i = 0; i < arguments.Length; i++) - { - if (i == removeIndex) - { - continue; - } - - migratedArguments.Add(arguments[i].Trim()); - } - - return migratedArguments.ToArray(); - } - - private static int FindInvocationClosingParenthesisIndex( - string source, - CodeTextMask codeTextMask, - int openParenthesisIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(openParenthesisIndex >= 0, "openParenthesisIndex must not be negative"); - - int nestedParenthesisDepth = 0; - for (int i = openParenthesisIndex + 1; i < source.Length; i++) - { - if (!codeTextMask.IsCodeAt(i)) - { - continue; - } - - if (source[i] == '(') - { - nestedParenthesisDepth++; - continue; - } - - if (source[i] != ')') - { - continue; - } - - if (nestedParenthesisDepth == 0) - { - return i; - } - - nestedParenthesisDepth--; - } - - return -1; - } - - private static bool IsNamedAttributeArgument(string argument, string argumentName) - { - Debug.Assert(!string.IsNullOrWhiteSpace(argument), "argument must not be null or whitespace"); - Debug.Assert(!string.IsNullOrWhiteSpace(argumentName), "argumentName must not be null or whitespace"); - - if (!argument.StartsWith(argumentName, StringComparison.Ordinal)) - { - return false; - } - - for (int i = argumentName.Length; i < argument.Length; i++) - { - char current = argument[i]; - if (char.IsWhiteSpace(current)) - { - continue; - } - - return current == '='; - } - - return false; - } - - private static string ReplaceRegexInCode( - string source, - Regex regex, - Func replacementFactory, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(regex != null, "regex must not be null"); - Debug.Assert(replacementFactory != null, "replacementFactory must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - int localReplacementCount = 0; - string migrated = regex.Replace( - source, - match => - { - if (!codeTextMask.IsCodeAt(match.Index)) - { - return match.Value; - } - - string replacement = replacementFactory(match); - if (string.Equals(match.Value, replacement, StringComparison.Ordinal)) - { - return match.Value; - } - - localReplacementCount++; - return replacement; - }); - replacementCount += localReplacementCount; - return migrated; - } - - private static string ReplaceLegacyToolAttributesInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyToolAttribute, - ref int replacementCount) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - StringBuilder builder = new(source.Length); - int localReplacementCount = 0; - int index = 0; - while (index < source.Length) - { - if (source[index] != '[' || !codeTextMask.IsCodeAt(index)) - { - builder.Append(source[index]); - index++; - continue; - } - - int closingBracketIndex = FindAttributeListClosingBracketIndex( - source, - codeTextMask, - index + 1); - if (closingBracketIndex < 0) - { - builder.Append(source[index]); - index++; - continue; - } - - string attributesSource = source.Substring(index + 1, closingBracketIndex - index - 1); - if (!TryMigrateLegacyToolAttributeList( - attributesSource, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - out string migratedAttributes)) - { - builder.Append(source, index, closingBracketIndex - index + 1); - index = closingBracketIndex + 1; - continue; - } - - builder.Append('['); - builder.Append(migratedAttributes); - builder.Append(']'); - localReplacementCount++; - index = closingBracketIndex + 1; - } - - replacementCount += localReplacementCount; - return builder.ToString(); - } - - private static int FindAttributeListClosingBracketIndex( - string source, - CodeTextMask codeTextMask, - int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - int nestedBracketDepth = 0; - for (int i = startIndex; i < source.Length; i++) - { - if (!codeTextMask.IsCodeAt(i)) - { - continue; - } - - if (source[i] == '[') - { - nestedBracketDepth++; - continue; - } - - if (source[i] != ']') - { - continue; - } - - if (nestedBracketDepth == 0) - { - return i; - } - - nestedBracketDepth--; - } - - return -1; - } - - private static bool RegexMatchesCode(string source, Regex regex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(regex != null, "regex must not be null"); - - MatchCollection matches = regex.Matches(source); - if (matches.Count == 0) - { - return false; - } - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - foreach (Match match in matches) - { - if (codeTextMask.IsCodeAt(match.Index)) - { - return true; - } - } - - return false; - } - - private static bool ContainsLegacyAssemblyScopedTypeReference(string source) - { - Debug.Assert(source != null, "source must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) - { - if (ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) - { - return true; - } - } - - foreach (TypeReplacementRule rule in DomainTypeReplacementRules) - { - if (ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) - { - return true; - } - } - - return ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, "ToolInfo"); - } - - private static bool ContainsLegacyAliasQualifiedAssemblyScopedApi( - string source, - string[] legacyAssemblyAliases) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - foreach (string alias in legacyAssemblyAliases) - { - foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) - { - if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) - { - return true; - } - } - - foreach (TypeReplacementRule rule in DomainTypeReplacementRules) - { - if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) - { - return true; - } - } - - if (ContainsLegacyAliasQualifiedName(source, alias, "ToolInfo") || - ContainsLegacyAliasQualifiedName(source, alias, "CustomToolManager")) - { - return true; - } - } - - return false; - } - - private static bool ContainsLegacyAliasQualifiedName(string source, string alias, string typeName) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(!string.IsNullOrEmpty(alias), "alias must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be null or empty"); - - Regex aliasQualifiedRegex = new( - $@"(?= 0, "typeNameIndex must not be negative"); - - string previousIdentifier = ReadPreviousIdentifier(source, typeNameIndex); - return string.Equals(previousIdentifier, "class", StringComparison.Ordinal) || - string.Equals(previousIdentifier, "struct", StringComparison.Ordinal) || - string.Equals(previousIdentifier, "interface", StringComparison.Ordinal) || - string.Equals(previousIdentifier, "enum", StringComparison.Ordinal) || - string.Equals(previousIdentifier, "using", StringComparison.Ordinal); - } - - private static string ReadPreviousIdentifier(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - int index = startIndex - 1; - while (index >= 0 && char.IsWhiteSpace(source[index])) - { - index--; - } - - int identifierEndIndex = index + 1; - while (index >= 0 && IsIdentifierCharacter(source[index])) - { - index--; - } - - return source.Substring(index + 1, identifierEndIndex - index - 1); - } - - private static char ReadNextNonWhitespaceCharacter(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - for (int index = startIndex; index < source.Length; index++) - { - if (char.IsWhiteSpace(source[index])) - { - continue; - } - - return source[index]; - } - - return '\0'; - } - - private static char ReadPreviousNonWhitespaceCharacter(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - for (int index = startIndex - 1; index >= 0; index--) - { - if (char.IsWhiteSpace(source[index])) - { - continue; - } - - return source[index]; - } - - return '\0'; - } - - private static bool IsDeclarationIdentifierTerminator(char value) - { - return value == '{' || - value == ';' || - value == '=' || - value == ')' || - value == ','; - } - - private static bool PreviousCodeTokenEquals(string source, int endIndex, string token) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(endIndex >= 0, "endIndex must not be negative"); - Debug.Assert(!string.IsNullOrEmpty(token), "token must not be null or empty"); - - int tokenEndIndex = endIndex - 1; - while (tokenEndIndex >= 0 && char.IsWhiteSpace(source[tokenEndIndex])) - { - tokenEndIndex--; - } - - int tokenStartIndex = tokenEndIndex; - while (tokenStartIndex >= 0 && IsIdentifierCharacter(source[tokenStartIndex])) - { - tokenStartIndex--; - } - - int tokenLength = tokenEndIndex - tokenStartIndex; - if (tokenLength <= 0) - { - return false; - } - - string previousToken = source.Substring(tokenStartIndex + 1, tokenLength); - return string.Equals(previousToken, token, StringComparison.Ordinal); - } - - private static bool CanPrecedeDeclarationIdentifier(char value) - { - return IsIdentifierCharacter(value) || - value == ']' || - value == '>'; - } - - private static bool IsIdentifierCharacter(char value) - { - return char.IsLetterOrDigit(value) || value == '_'; - } - - private static bool ContainsLegacyToolMigrationMarker(string source) - { - Debug.Assert(source != null, "source must not be null"); - - if (RegexMatchesCode(source, LegacyNamespaceRegex)) return true; - - return ContainsLegacyToolAttributeList( - source, - Array.Empty(), - canMigrateBareLegacyToolAttribute: false); - } - - private static bool ContainsLegacyToolAttributeList( - string source, - string[] legacyAssemblyAliases, - bool canMigrateBareLegacyToolAttribute) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - int index = 0; - while (index < source.Length) - { - if (source[index] != '[' || !codeTextMask.IsCodeAt(index)) - { - index++; - continue; - } - - int closingBracketIndex = FindAttributeListClosingBracketIndex( - source, - codeTextMask, - index + 1); - if (closingBracketIndex < 0) - { - index++; - continue; - } - - string attributesSource = source.Substring(index + 1, closingBracketIndex - index - 1); - if (TryMigrateLegacyToolAttributeList( - attributesSource, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - out _)) - { - return true; - } - - index = closingBracketIndex + 1; - } - - return false; - } - - private static bool StartsWith(string source, int startIndex, string value) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - Debug.Assert(value != null, "value must not be null"); - - if (startIndex + value.Length > source.Length) - { - return false; - } - - return string.CompareOrdinal(source, startIndex, value, 0, value.Length) == 0; - } - - private static bool ContainsTextFragment(string source, string text) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(!string.IsNullOrEmpty(text), "text must not be null or empty"); - - return source.IndexOf(text, StringComparison.Ordinal) >= 0; - } - - private static bool IsRawStringStart(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); - int quoteIndex = startIndex + dollarCount; - return CountRepeatedCharacter(source, quoteIndex, '"') >= MinimumRawStringDelimiterQuoteCount; - } - - private static int CountRepeatedCharacter(string source, int startIndex, char character) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - int index = startIndex; - while (index < source.Length && source[index] == character) - { - index++; - } - - return index - startIndex; - } - - private static bool HasRepeatedCharacterAt(string source, int startIndex, char character, int count) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - Debug.Assert(count > 0, "count must be positive"); - - if (startIndex + count > source.Length) - { - return false; - } - - for (int i = 0; i < count; i++) - { - if (source[startIndex + i] != character) - { - return false; - } - } - - return true; - } - - private static int GetStringPrefixLength(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) - { - return 2; - } - - return 1; - } - - private readonly struct ReplacementRule - { - public ReplacementRule(string pattern, string replacement) - { - Debug.Assert(!string.IsNullOrEmpty(pattern), "pattern must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(replacement), "replacement must not be null or empty"); - - PatternRegex = new Regex(pattern, RegexOptions.Compiled); - Replacement = replacement; - } - - public Regex PatternRegex { get; } - public string Replacement { get; } - } - - private readonly struct TypeReplacementRule - { - public TypeReplacementRule(string legacyName, string currentName) - { - Debug.Assert(!string.IsNullOrEmpty(legacyName), "legacyName must not be null or empty"); - Debug.Assert(!string.IsNullOrEmpty(currentName), "currentName must not be null or empty"); - - LegacyName = legacyName; - CurrentName = currentName; - } - - public string LegacyName { get; } - public string CurrentName { get; } - } - - private readonly struct CodeTextMask - { - private readonly bool[] _codeCharacters; - - private CodeTextMask(bool[] codeCharacters) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - - _codeCharacters = codeCharacters; - } - - public static CodeTextMask Create(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return CodeTextMaskCache.GetValue(source, CreateCodeTextMaskHolder).Mask; - } - - internal static CodeTextMask CreateUncached(string source) - { - Debug.Assert(source != null, "source must not be null"); - - bool[] codeCharacters = new bool[source.Length]; - for (int i = 0; i < codeCharacters.Length; i++) - { - codeCharacters[i] = true; - } - - int index = 0; - while (index < source.Length) - { - if (IsInterpolatedRawStringStart(source, index)) - { - index = MarkInterpolatedRawStringAsIgnored(codeCharacters, source, index); - continue; - } - - if (StartsWith(source, index, "$@\"") || StartsWith(source, index, "@$\"")) - { - index = MarkInterpolatedVerbatimStringAsIgnored(codeCharacters, source, index); - continue; - } - - if (StartsWith(source, index, "$\"") && !StartsWith(source, index, "$\"\"\"")) - { - index = MarkInterpolatedRegularStringAsIgnored(codeCharacters, source, index); - continue; - } - - int ignoredTextEndIndex = GetIgnoredTextEndIndex(source, index); - if (ignoredTextEndIndex == index) - { - index++; - continue; - } - - MarkRangeAsIgnored(codeCharacters, index, ignoredTextEndIndex); - index = ignoredTextEndIndex; - } - - return new CodeTextMask(codeCharacters); - } - - public bool IsCodeAt(int index) - { - if (index < 0 || index >= _codeCharacters.Length) - { - return false; - } - - return _codeCharacters[index]; - } - - private static int GetIgnoredTextEndIndex(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - if (StartsWith(source, startIndex, "//")) - { - return FindLineCommentEndIndex(source, startIndex); - } - - if (StartsWith(source, startIndex, "/*")) - { - return FindBlockCommentEndIndex(source, startIndex); - } - - if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) - { - return FindVerbatimStringEndIndex(source, startIndex + 2); - } - - if (StartsWith(source, startIndex, "@\"")) - { - return FindVerbatimStringEndIndex(source, startIndex + 1); - } - - if (IsInterpolatedRawStringStart(source, startIndex)) - { - int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); - return FindRawStringEndIndex(source, startIndex + dollarCount); - } - - if (StartsWith(source, startIndex, "$\"")) - { - return FindRegularStringEndIndex(source, startIndex + 1); - } - - if (StartsWith(source, startIndex, "\"\"\"")) - { - return FindRawStringEndIndex(source, startIndex); - } - - if (source[startIndex] == '"') - { - return FindRegularStringEndIndex(source, startIndex); - } - - if (source[startIndex] == '\'') - { - return FindCharLiteralEndIndex(source, startIndex); - } - - return startIndex; - } - - private static int FindLineCommentEndIndex(string source, int startIndex) - { - int index = startIndex; - while (index < source.Length && source[index] != '\n') - { - index++; - } - - return index; - } - - private static int FindBlockCommentEndIndex(string source, int startIndex) - { - int index = startIndex + 2; - while (index + 1 < source.Length) - { - if (source[index] == '*' && source[index + 1] == '/') - { - return index + 2; - } - - index++; - } - - return source.Length; - } - - private static int FindRegularStringEndIndex(string source, int quoteIndex) - { - int index = quoteIndex + 1; - while (index < source.Length) - { - if (source[index] == '\\') - { - index += 2; - continue; - } - - if (source[index] == '"') - { - return index + 1; - } - - index++; - } - - return source.Length; - } - - private static int FindVerbatimStringEndIndex(string source, int quoteIndex) - { - int index = quoteIndex + 1; - while (index < source.Length) - { - if (source[index] != '"') - { - index++; - continue; - } - - if (index + 1 < source.Length && source[index + 1] == '"') - { - index += 2; - continue; - } - - return index + 1; - } - - return source.Length; - } - - private static int FindRawStringEndIndex(string source, int quoteIndex) - { - int quoteCount = CountRepeatedCharacter(source, quoteIndex, '"'); - Debug.Assert( - quoteCount >= MinimumRawStringDelimiterQuoteCount, - "quoteIndex must point to a raw string delimiter"); - - int index = quoteIndex + quoteCount; - while (index + quoteCount <= source.Length) - { - if (HasRepeatedCharacterAt(source, index, '"', quoteCount)) - { - return index + quoteCount; - } - - index++; - } - - return source.Length; - } - - private static int MarkInterpolatedRawStringAsIgnored( - bool[] codeCharacters, - string source, - int dollarIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(dollarIndex >= 0, "dollarIndex must not be negative"); - - int dollarCount = CountRepeatedCharacter(source, dollarIndex, '$'); - int quoteIndex = dollarIndex + dollarCount; - int quoteCount = CountRepeatedCharacter(source, quoteIndex, '"'); - Debug.Assert(dollarCount > 0, "dollarIndex must point to an interpolated raw string prefix"); - Debug.Assert( - quoteCount >= MinimumRawStringDelimiterQuoteCount, - "quoteIndex must point to a raw string delimiter"); - - int literalStartIndex = dollarIndex; - int index = quoteIndex + quoteCount; - while (index < source.Length) - { - if (HasRepeatedCharacterAt(source, index, '"', quoteCount)) - { - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + quoteCount); - return index + quoteCount; - } - - if (source[index] == '{') - { - int braceCount = CountRepeatedCharacter(source, index, '{'); - if (braceCount < dollarCount) - { - index += braceCount; - continue; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); - index = MarkRawInterpolationHoleNestedTextAsIgnored( - codeCharacters, - source, - index, - dollarCount); - literalStartIndex = index; - continue; - } - - if (source[index] == '}') - { - int braceCount = CountRepeatedCharacter(source, index, '}'); - index += braceCount; - continue; - } - - index++; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); - return source.Length; - } - - private static int MarkInterpolatedRegularStringAsIgnored( - bool[] codeCharacters, - string source, - int dollarIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(dollarIndex >= 0, "dollarIndex must not be negative"); - - int quoteIndex = dollarIndex + 1; - int literalStartIndex = dollarIndex; - int index = quoteIndex + 1; - while (index < source.Length) - { - if (source[index] == '\\') - { - index += 2; - continue; - } - - if (source[index] == '{') - { - if (index + 1 < source.Length && source[index + 1] == '{') - { - index += 2; - continue; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); - index = MarkInterpolationHoleNestedTextAsIgnored(codeCharacters, source, index); - literalStartIndex = index; - continue; - } - - if (source[index] == '"') - { - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + 1); - return index + 1; - } - - index++; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); - return source.Length; - } - - private static int MarkInterpolatedVerbatimStringAsIgnored( - bool[] codeCharacters, - string source, - int prefixIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(prefixIndex >= 0, "prefixIndex must not be negative"); - - int quoteIndex = prefixIndex + 2; - int literalStartIndex = prefixIndex; - int index = quoteIndex + 1; - while (index < source.Length) - { - if (source[index] == '"') - { - if (index + 1 < source.Length && source[index + 1] == '"') - { - index += 2; - continue; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index + 1); - return index + 1; - } - - if (source[index] == '{') - { - if (index + 1 < source.Length && source[index + 1] == '{') - { - index += 2; - continue; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, index); - index = MarkInterpolationHoleNestedTextAsIgnored(codeCharacters, source, index); - literalStartIndex = index; - continue; - } - - if (source[index] == '}' && index + 1 < source.Length && source[index + 1] == '}') - { - index += 2; - continue; - } - - index++; - } - - MarkRangeAsIgnored(codeCharacters, literalStartIndex, source.Length); - return source.Length; - } - - private static int MarkInterpolationHoleNestedTextAsIgnored( - bool[] codeCharacters, - string source, - int openBraceIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); - - int nestedBraceDepth = 0; - int index = openBraceIndex + 1; - while (index < source.Length) - { - int ignoredTextEndIndex = MarkIgnoredTextInInterpolationHole( - codeCharacters, - source, - index); - if (ignoredTextEndIndex != index) - { - index = ignoredTextEndIndex; - continue; - } - - if (source[index] == '{') - { - nestedBraceDepth++; - index++; - continue; - } - - if (source[index] == '}') - { - if (nestedBraceDepth == 0) - { - return index + 1; - } - - nestedBraceDepth--; - } - - index++; - } - - return source.Length; - } - - private static int MarkRawInterpolationHoleNestedTextAsIgnored( - bool[] codeCharacters, - string source, - int openBraceIndex, - int interpolationBraceCount) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); - Debug.Assert(interpolationBraceCount > 0, "interpolationBraceCount must be positive"); - - int nestedBraceDepth = 0; - int index = openBraceIndex + interpolationBraceCount; - while (index < source.Length) - { - int ignoredTextEndIndex = MarkIgnoredTextInInterpolationHole( - codeCharacters, - source, - index); - if (ignoredTextEndIndex != index) - { - index = ignoredTextEndIndex; - continue; - } - - if (source[index] == '{') - { - nestedBraceDepth++; - index++; - continue; - } - - if (source[index] == '}') - { - bool isClosingInterpolation = - nestedBraceDepth == 0 && - HasRepeatedCharacterAt(source, index, '}', interpolationBraceCount); - if (isClosingInterpolation) - { - return index + interpolationBraceCount; - } - - if (nestedBraceDepth > 0) - { - nestedBraceDepth--; - } - } - - index++; - } - - return source.Length; - } - - private static int MarkIgnoredTextInInterpolationHole( - bool[] codeCharacters, - string source, - int startIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - if (IsInterpolatedRawStringStart(source, startIndex)) - { - return MarkInterpolatedRawStringAsIgnored(codeCharacters, source, startIndex); - } - - if (StartsWith(source, startIndex, "$@\"") || StartsWith(source, startIndex, "@$\"")) - { - return MarkInterpolatedVerbatimStringAsIgnored(codeCharacters, source, startIndex); - } - - if (StartsWith(source, startIndex, "$\"") && !StartsWith(source, startIndex, "$\"\"\"")) - { - return MarkInterpolatedRegularStringAsIgnored(codeCharacters, source, startIndex); - } - - int ignoredTextEndIndex = GetIgnoredTextEndIndex(source, startIndex); - if (ignoredTextEndIndex == startIndex) - { - return startIndex; - } - - MarkRangeAsIgnored(codeCharacters, startIndex, ignoredTextEndIndex); - return ignoredTextEndIndex; - } - - private static int FindCharLiteralEndIndex(string source, int quoteIndex) - { - int index = quoteIndex + 1; - while (index < source.Length) - { - if (source[index] == '\\') - { - index += 2; - continue; - } - - if (source[index] == '\'') - { - return index + 1; - } - - index++; - } - - return source.Length; - } - - private static void MarkRangeAsIgnored(bool[] codeCharacters, int startIndex, int endIndex) - { - Debug.Assert(codeCharacters != null, "codeCharacters must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - Debug.Assert(endIndex >= startIndex, "endIndex must not be less than startIndex"); - - for (int i = startIndex; i < endIndex && i < codeCharacters.Length; i++) - { - codeCharacters[i] = false; - } - } - - private static bool IsInterpolatedRawStringStart(string source, int startIndex) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - if (startIndex >= source.Length || source[startIndex] != '$') - { - return false; - } - - int dollarCount = CountRepeatedCharacter(source, startIndex, '$'); - int quoteIndex = startIndex + dollarCount; - return CountRepeatedCharacter(source, quoteIndex, '"') >= MinimumRawStringDelimiterQuoteCount; - } - - private static int CountRepeatedCharacter(string source, int startIndex, char character) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - - int index = startIndex; - while (index < source.Length && source[index] == character) - { - index++; - } - - return index - startIndex; - } - - private static bool HasRepeatedCharacterAt(string source, int startIndex, char character, int count) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(startIndex >= 0, "startIndex must not be negative"); - Debug.Assert(count > 0, "count must be positive"); - - if (startIndex + count > source.Length) - { - return false; - } - - for (int i = 0; i < count; i++) - { - if (source[startIndex + i] != character) - { - return false; - } - } - - return true; - } - } - - private sealed class CodeTextMaskHolder - { - public CodeTextMaskHolder(CodeTextMask mask) - { - Mask = mask; + Index = index; + Name = name; } - public CodeTextMask Mask { get; } - } - - private static CodeTextMaskHolder CreateCodeTextMaskHolder(string source) - { - Debug.Assert(source != null, "source must not be null"); - - return new CodeTextMaskHolder(CodeTextMask.CreateUncached(source)); + public int Index { get; } + public string Name { get; } } } internal readonly struct ThirdPartyToolMigrationContentResult { - public ThirdPartyToolMigrationContentResult(string content, int replacementCount) + public ThirdPartyToolMigrationContentResult( + string content, + int replacementCount, + ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature[] removedPlayerLoopTimingSignatures) { Debug.Assert(content != null, "content must not be null"); Debug.Assert(replacementCount >= 0, "replacementCount must not be negative"); + Debug.Assert( + removedPlayerLoopTimingSignatures != null, + "removedPlayerLoopTimingSignatures must not be null"); Content = content ?? string.Empty; ReplacementCount = replacementCount; + RemovedPlayerLoopTimingSignatures = + removedPlayerLoopTimingSignatures ?? + Array.Empty(); } public string Content { get; } public int ReplacementCount { get; } + public ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature[] RemovedPlayerLoopTimingSignatures { get; } public bool Changed => ReplacementCount > 0; } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs new file mode 100644 index 0000000000..aa36956185 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs @@ -0,0 +1,398 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationScreenshotArgumentRules + { + internal static string[] GetMigratedEditorWindowCaptureUtilityArguments( + string[] arguments, + string timeoutExpression) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(!string.IsNullOrEmpty(timeoutExpression), "timeoutExpression must not be null or empty"); + + string[] trimmedArguments = GetTrimmedInvocationArguments(arguments); + if (trimmedArguments.Length != 3) + { + return Array.Empty(); + } + + string[] orderedArguments = GetOrderedEditorWindowCaptureUtilityArguments(trimmedArguments); + if (orderedArguments.Length == 0) + { + return Array.Empty(); + } + + return new[] + { + orderedArguments[0], + orderedArguments[1], + timeoutExpression, + orderedArguments[2] + }; + } + + internal static string[] GetMigratedEditorWindowCaptureUtilityGameRenderingArguments( + string[] arguments, + string timeoutExpression) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(!string.IsNullOrEmpty(timeoutExpression), "timeoutExpression must not be null or empty"); + + string[] trimmedArguments = GetTrimmedInvocationArguments(arguments); + if (trimmedArguments.Length == 3) + { + return Array.Empty(); + } + + if (trimmedArguments.Length != 2) + { + return Array.Empty(); + } + + string[] orderedArguments = GetOrderedEditorWindowCaptureUtilityGameRenderingArguments(trimmedArguments); + if (orderedArguments.Length == 0) + { + return Array.Empty(); + } + + return new[] + { + orderedArguments[0], + timeoutExpression, + orderedArguments[1] + }; + } + + internal static string[] GetTrimmedInvocationArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + return arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + } + + internal static string[] GetOrderedEditorWindowCaptureUtilityArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] orderedArguments = new string[3]; + int nextPosition = 0; + foreach (string argument in arguments) + { + (int position, string namedArgument) = GetEditorWindowCaptureUtilityNamedArgument(argument); + if (position >= 0) + { + if (!TryAssignEditorWindowCaptureUtilityArgument(orderedArguments, position, namedArgument)) + { + return Array.Empty(); + } + + continue; + } + + nextPosition = GetNextUnassignedEditorWindowCaptureUtilityArgumentPosition( + orderedArguments, + nextPosition); + if (nextPosition >= orderedArguments.Length) + { + return Array.Empty(); + } + + orderedArguments[nextPosition] = nextPosition == 2 + ? GetArgumentWithMigratedCancellationTokenName(argument) + : argument; + nextPosition++; + } + + return orderedArguments.Any(argument => argument == null) + ? Array.Empty() + : orderedArguments; + } + + internal static string[] GetOrderedEditorWindowCaptureUtilityGameRenderingArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] orderedArguments = new string[2]; + int nextPosition = 0; + foreach (string argument in arguments) + { + (int position, string namedArgument) = + GetEditorWindowCaptureUtilityGameRenderingNamedArgument(argument); + if (position >= 0) + { + if (!TryAssignEditorWindowCaptureUtilityArgument(orderedArguments, position, namedArgument)) + { + return Array.Empty(); + } + + continue; + } + + nextPosition = GetNextUnassignedEditorWindowCaptureUtilityArgumentPosition( + orderedArguments, + nextPosition); + if (nextPosition >= orderedArguments.Length) + { + return Array.Empty(); + } + + orderedArguments[nextPosition] = nextPosition == 1 + ? GetArgumentWithMigratedCancellationTokenName(argument) + : argument; + nextPosition++; + } + + return orderedArguments.Any(argument => argument == null) + ? Array.Empty() + : orderedArguments; + } + + internal static (int Position, string Argument) GetEditorWindowCaptureUtilityNamedArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + string windowValue = + GetNamedArgumentValueOrNull(argument, EditorWindowCaptureUtilityWindowArgumentName); + if (windowValue != null) + { + return (0, windowValue); + } + + string resolutionScaleValue = + GetNamedArgumentValueOrNull(argument, EditorWindowCaptureUtilityResolutionScaleArgumentName); + if (resolutionScaleValue != null) + { + return (1, resolutionScaleValue); + } + + return GetEditorWindowCaptureUtilityCancellationTokenNamedArgument(argument, 2); + } + + internal static (int Position, string Argument) + GetEditorWindowCaptureUtilityGameRenderingNamedArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + string resolutionScaleValue = + GetNamedArgumentValueOrNull(argument, EditorWindowCaptureUtilityResolutionScaleArgumentName); + if (resolutionScaleValue != null) + { + return (0, resolutionScaleValue); + } + + return GetEditorWindowCaptureUtilityCancellationTokenNamedArgument(argument, 1); + } + + internal static (int Position, string Argument) + GetEditorWindowCaptureUtilityCancellationTokenNamedArgument(string argument, int position) + { + Debug.Assert(argument != null, "argument must not be null"); + Debug.Assert(position >= 0, "position must not be negative"); + + string legacyCancellationTokenValue = + GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (legacyCancellationTokenValue != null) + { + return (position, $"{CurrentCancellationTokenArgumentName}: {legacyCancellationTokenValue}"); + } + + string currentCancellationTokenValue = + GetNamedArgumentValueOrNull(argument, CurrentCancellationTokenArgumentName); + if (currentCancellationTokenValue != null) + { + return (position, $"{CurrentCancellationTokenArgumentName}: {currentCancellationTokenValue}"); + } + + return (-1, string.Empty); + } + + internal static bool TryAssignEditorWindowCaptureUtilityArgument( + string[] orderedArguments, + int position, + string argument) + { + Debug.Assert(orderedArguments != null, "orderedArguments must not be null"); + Debug.Assert(position >= 0, "position must not be negative"); + Debug.Assert(argument != null, "argument must not be null"); + + if (position >= orderedArguments.Length || orderedArguments[position] != null) + { + return false; + } + + orderedArguments[position] = argument; + return true; + } + + internal static int GetNextUnassignedEditorWindowCaptureUtilityArgumentPosition( + string[] orderedArguments, + int startPosition) + { + Debug.Assert(orderedArguments != null, "orderedArguments must not be null"); + Debug.Assert(startPosition >= 0, "startPosition must not be negative"); + + int position = startPosition; + while (position < orderedArguments.Length && orderedArguments[position] != null) + { + position++; + } + + return position; + } + + internal static string GetArgumentWithMigratedCancellationTokenName(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + string cancellationTokenValue = GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (cancellationTokenValue == null) + { + return argument; + } + + return $"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"; + } + + internal static bool ShouldMigrateLegacyToolInfoConstructorArguments( + Match match, + string[] arguments, + bool canMigrateAmbiguousBareLegacyToolInfoConstructor) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(arguments != null, "arguments must not be null"); + + if (!match.Groups["toolInfo"].Success) + { + return true; + } + + return canMigrateAmbiguousBareLegacyToolInfoConstructor || + HasUnambiguousLegacyToolInfoConstructorArguments(arguments); + } + + internal static bool HasUnambiguousLegacyToolInfoConstructorArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + if (arguments.Length == 4) + { + return true; + } + + if (arguments.Length == 3 && IsLikelyLegacyDescriptionArgument(arguments[1])) + { + return true; + } + + return FindNamedConstructorArgumentIndex( + arguments, + DescriptionAttributeArgumentName.ToLowerInvariant()) >= 0; + } + + internal static bool IsLikelyLegacyDescriptionArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + string trimmedArgument = argument.Trim(); + return IsStringLiteralArgument(trimmedArgument) || + string.Equals(trimmedArgument, "description", StringComparison.OrdinalIgnoreCase); + } + + internal static bool IsStringLiteralArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + return argument.StartsWith("\"", StringComparison.Ordinal) || + argument.StartsWith("@\"", StringComparison.Ordinal) || + argument.StartsWith("$\"", StringComparison.Ordinal) || + argument.StartsWith("$@\"", StringComparison.Ordinal) || + argument.StartsWith("@$\"", StringComparison.Ordinal); + } + + internal static bool IsLegacyToolInfoConstructorMatch( + Match match, + string[] legacyNamespaceAliases, + string[] legacyToolInfoTypeAliases, + bool canMigrateBareLegacyToolInfoConstructor) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert(legacyToolInfoTypeAliases != null, "legacyToolInfoTypeAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); + } + + if (match.Groups["typeAlias"].Success) + { + return legacyToolInfoTypeAliases.Contains(match.Groups["typeAlias"].Value); + } + + if (match.Groups["toolInfo"].Success) + { + return canMigrateBareLegacyToolInfoConstructor; + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs.meta new file mode 100644 index 0000000000..7422cb9b3c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6081eb6d16848488da85cae9c19029f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs new file mode 100644 index 0000000000..e344b412ab --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationScreenshotDeconstructionRules + { + internal static void AppendCaptureGameRenderingLegacyTupleProjection(StringBuilder builder) + { + Debug.Assert(builder != null, "builder must not be null"); + + builder.Append(".ContinueWith("); + builder.Append(CaptureGameRenderingProjectionTaskVariableName); + builder.Append(" => ("); + builder.Append(CaptureGameRenderingProjectionTaskVariableName); + builder.Append(".GetAwaiter().GetResult().texture, "); + builder.Append(CaptureGameRenderingProjectionTaskVariableName); + builder.Append(".GetAwaiter().GetResult().yOffset))"); + } + + internal static string AddDiscardToCaptureGameRenderingDeconstructionsInCode( + string source, + bool canUseBareCurrentFirstPartyTools, + string[] currentFirstPartyToolsNamespaceAliases, + string[] assemblyDeclaredTypeNames, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = CurrentCaptureGameRenderingDeconstructionRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int localReplacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || !codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + if (!IsCurrentFirstPartyCaptureGameRenderingDeconstruction( + source, + match, + canUseBareCurrentFirstPartyTools, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex >= 0 && + IsCaptureGameRenderingLegacyTupleProjection(source, closingParenthesisIndex + 1)) + { + continue; + } + + Group itemsGroup = match.Groups["items"]; + string[] items = SplitAttributeArguments(itemsGroup.Value) + .Select(item => item.Trim()) + .Where(item => item.Length > 0) + .ToArray(); + if (items.Length != 2) + { + continue; + } + + builder.Append(source, sourceCopyIndex, itemsGroup.Index - sourceCopyIndex); + builder.Append(itemsGroup.Value); + builder.Append(", _"); + sourceCopyIndex = itemsGroup.Index + itemsGroup.Length; + localReplacementCount++; + } + + if (localReplacementCount == 0) + { + return source; + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + replacementCount += localReplacementCount; + return builder.ToString(); + } + + internal static bool ContainsCurrentCaptureGameRenderingDeconstructionMigration( + string source, + bool canUseBareCurrentFirstPartyTools, + string[] currentFirstPartyToolsNamespaceAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = CurrentCaptureGameRenderingDeconstructionRegex.Matches(source); + foreach (Match match in matches) + { + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + if (!IsCurrentFirstPartyCaptureGameRenderingDeconstruction( + source, + match, + canUseBareCurrentFirstPartyTools, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex >= 0 && + IsCaptureGameRenderingLegacyTupleProjection(source, closingParenthesisIndex + 1)) + { + continue; + } + + Group itemsGroup = match.Groups["items"]; + string[] items = SplitAttributeArguments(itemsGroup.Value) + .Select(item => item.Trim()) + .Where(item => item.Length > 0) + .ToArray(); + if (items.Length == 2) + { + return true; + } + } + + return false; + } + + internal static bool IsCurrentFirstPartyCaptureGameRenderingDeconstruction( + string source, + Match match, + bool canUseBareCurrentFirstPartyTools, + string[] currentFirstPartyToolsNamespaceAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(match != null, "match must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + return currentFirstPartyToolsNamespaceAliases.Contains(match.Groups["alias"].Value); + } + + bool hasProtectedTypeDeclaration = + DeclaresLocalType(source, LegacyEditorWindowCaptureUtilityTypeName) || + assemblyDeclaredTypeNames.Contains(LegacyEditorWindowCaptureUtilityTypeName); + return canUseBareCurrentFirstPartyTools && !hasProtectedTypeDeclaration; + } + + internal static bool IsCaptureGameRenderingLegacyTupleProjection(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int nextCodeIndex = ReadNextNonWhitespaceIndex(source, startIndex); + return nextCodeIndex < source.Length && + StartsWith(source, nextCodeIndex, ".ContinueWith"); + } + + internal static bool IsLegacyEditorWindowCaptureUtilityCallMatch( + Match match, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(match != null, "match must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + if (match.Groups["qualifier"].Success) + { + return true; + } + + if (match.Groups["currentQualifier"].Success) + { + return true; + } + + if (match.Groups["alias"].Success) + { + string alias = match.Groups["alias"].Value; + return legacyNamespaceAliases.Contains(alias) || + currentFirstPartyToolsNamespaceAliases.Contains(alias); + } + + return match.Groups["editorWindowCaptureUtility"].Success && + canMigrateBareLegacyEditorWindowCaptureUtility && + !hasProtectedBareEditorWindowCaptureUtility; + } + + internal static bool HasProtectedEditorWindowCaptureUtilityDeclaration( + string source, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + return DeclaresLocalType(source, LegacyEditorWindowCaptureUtilityTypeName) || + assemblyDeclaredTypeNames.Contains(LegacyEditorWindowCaptureUtilityTypeName); + } + + internal static bool ShouldExtractCaptureWindowTexture( + string source, + CodeTextMask codeTextMask, + int awaitIndex, + int expressionEndIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(awaitIndex >= 0, "awaitIndex must not be negative"); + Debug.Assert(expressionEndIndex >= 0, "expressionEndIndex must not be negative"); + + int nextCodeIndex = ReadNextNonWhitespaceIndex(source, expressionEndIndex); + if (nextCodeIndex >= source.Length || source[nextCodeIndex] != ';' || !codeTextMask.IsCodeAt(nextCodeIndex)) + { + return true; + } + + if (PreviousCodeTokenEquals(source, awaitIndex, "return")) + { + return true; + } + + if (PreviousCodeTokenIsArrow(source, awaitIndex)) + { + return true; + } + + char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, awaitIndex); + return previousCharacter == '='; + } + + internal static (string ConfigureAwaitSuffix, int EndIndex) ReadOptionalConfigureAwaitSuffix( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int dotIndex = startIndex; + while (dotIndex < source.Length && char.IsWhiteSpace(source[dotIndex])) + { + dotIndex++; + } + + if (dotIndex >= source.Length || source[dotIndex] != '.' || !codeTextMask.IsCodeAt(dotIndex)) + { + return (string.Empty, startIndex); + } + + const string configureAwaitMemberName = "ConfigureAwait"; + int memberNameIndex = dotIndex + 1; + if (memberNameIndex + configureAwaitMemberName.Length > source.Length) + { + return (string.Empty, startIndex); + } + + string memberName = source.Substring(memberNameIndex, configureAwaitMemberName.Length); + if (!string.Equals(memberName, configureAwaitMemberName, StringComparison.Ordinal)) + { + return (string.Empty, startIndex); + } + + int openParenthesisIndex = memberNameIndex + configureAwaitMemberName.Length; + while (openParenthesisIndex < source.Length && char.IsWhiteSpace(source[openParenthesisIndex])) + { + openParenthesisIndex++; + } + + if (openParenthesisIndex >= source.Length || source[openParenthesisIndex] != '(') + { + return (string.Empty, startIndex); + } + + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + return (string.Empty, startIndex); + } + + return ( + source.Substring(startIndex, closingParenthesisIndex - startIndex + 1), + closingParenthesisIndex + 1); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs.meta new file mode 100644 index 0000000000..b9fcd08daa --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 500fa5fa8644044059ea36597c2d8feb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs new file mode 100644 index 0000000000..a8a339da3f --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationScreenshotDetectionRules + { + internal static bool ContainsLegacyEditorWindowCaptureUtilityCall( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + bool hasProtectedBareEditorWindowCaptureUtility = + HasProtectedEditorWindowCaptureUtilityDeclaration(source, assemblyDeclaredTypeNames); + if (ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureWindowRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + return true; + } + + if (ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + return true; + } + + if (ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + return true; + } + + return ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureGameRenderingInvocationRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility); + } + + internal static bool ContainsLegacyEditorWindowCaptureUtilityCall( + string source, + CodeTextMask codeTextMask, + Regex regex, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(regex != null, "regex must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + MatchCollection matches = regex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + return true; + } + } + + return false; + } + + internal static bool CanMigrateBareLegacyEditorWindowCaptureUtilityForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyNamespaceAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + return hasLegacyAssemblySource || + RegexMatchesCode(source, LegacyNamespaceRegex) || + legacyNamespaceAliases.Length > 0 || + RegexMatchesCode(source, CurrentToolContractsNamespaceRegex) || + hasAssemblyScopedCurrentToolContractsUsing || + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex) || + hasAssemblyScopedCurrentFirstPartyToolsUsing; + } + + internal static bool ContainsLegacyEditorWindowCaptureUtilityMigration( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + string[] assemblyDeclaredTypeNames, + bool requiresTimeoutArgumentMigration) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + bool hasProtectedBareEditorWindowCaptureUtility = + HasProtectedEditorWindowCaptureUtilityDeclaration(source, assemblyDeclaredTypeNames); + if (ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureWindowRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility, + isGameRenderingCapture: false, + requiresTimeoutArgumentMigration)) + { + return true; + } + + if (ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility, + isGameRenderingCapture: false, + requiresTimeoutArgumentMigration)) + { + return true; + } + + if (ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility, + isGameRenderingCapture: true, + requiresTimeoutArgumentMigration)) + { + return true; + } + + return ContainsLegacyEditorWindowCaptureUtilityMigration( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureGameRenderingInvocationRegex, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility, + isGameRenderingCapture: true, + requiresTimeoutArgumentMigration); + } + + internal static bool ContainsLegacyEditorWindowCaptureUtilityMigration( + string source, + CodeTextMask codeTextMask, + Regex regex, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool hasProtectedBareEditorWindowCaptureUtility, + bool isGameRenderingCapture, + bool requiresTimeoutArgumentMigration) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(regex != null, "regex must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + MatchCollection matches = regex.Matches(source); + foreach (Match match in matches) + { + if (!codeTextMask.IsCodeAt(match.Index) || + !IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] arguments = SplitAttributeArguments(argumentsSource); + string[] migratedArguments = isGameRenderingCapture + ? GetMigratedEditorWindowCaptureUtilityGameRenderingArguments(arguments, "timeout") + : GetMigratedEditorWindowCaptureUtilityArguments(arguments, "timeout"); + if (migratedArguments.Length == 0) + { + continue; + } + + if (!requiresTimeoutArgumentMigration) + { + return true; + } + + string[] trimmedArguments = GetTrimmedInvocationArguments(arguments); + if (!isGameRenderingCapture || trimmedArguments.Length == 2) + { + return true; + } + } + + return false; + } + internal static bool ContainsLegacyFirstPartyScreenshotReference( + string source, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + string[] aliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(source, rule.LegacyName) || + assemblyDeclaredTypeNames.Contains(rule.LegacyName); + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + foreach (string alias in aliases) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) + { + return true; + } + } + + if (canMigrateBareLegacyFirstPartyScreenshotApi && + !hasProtectedTypeDeclaration && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsCurrentFirstPartyScreenshotReference( + string source, + bool canUseBareCurrentFirstPartyScreenshotType, + string[] currentFirstPartyToolsNamespaceAliases, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(source, rule.CurrentName) || + assemblyDeclaredTypeNames.Contains(rule.CurrentName); + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + foreach (string alias in currentFirstPartyToolsNamespaceAliases) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.CurrentName)) + { + return true; + } + } + + if (canUseBareCurrentFirstPartyScreenshotType && + !hasProtectedTypeDeclaration && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) + { + return true; + } + } + + return false; + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs.meta new file mode 100644 index 0000000000..c9188291cf --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72b7631050cd84d848a71498b4794ca0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs new file mode 100644 index 0000000000..c12d652b05 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs @@ -0,0 +1,466 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationScreenshotRules + { + internal static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool canUseBareCurrentFirstPartyTools, + string[] assemblyDeclaredTypeNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + (string captureWindowContent, int captureWindowReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( + source, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + HasProtectedEditorWindowCaptureUtilityDeclaration(source, assemblyDeclaredTypeNames)); + (string captureWindowTaskContent, int captureWindowTaskReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( + captureWindowContent, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + HasProtectedEditorWindowCaptureUtilityDeclaration(captureWindowContent, assemblyDeclaredTypeNames)); + (string captureGameRenderingContent, int captureGameRenderingReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( + captureWindowTaskContent, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + HasProtectedEditorWindowCaptureUtilityDeclaration( + captureWindowTaskContent, + assemblyDeclaredTypeNames)); + (string captureGameRenderingTaskContent, int captureGameRenderingTaskReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingTaskCallsInCode( + captureGameRenderingContent, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + HasProtectedEditorWindowCaptureUtilityDeclaration( + captureGameRenderingContent, + assemblyDeclaredTypeNames)); + int deconstructionReplacementCount = 0; + string migratedContent = AddDiscardToCaptureGameRenderingDeconstructionsInCode( + captureGameRenderingTaskContent, + canUseBareCurrentFirstPartyTools, + currentFirstPartyToolsNamespaceAliases, + assemblyDeclaredTypeNames, + ref deconstructionReplacementCount); + return ( + migratedContent, + captureWindowReplacementCount + + captureWindowTaskReplacementCount + + captureGameRenderingReplacementCount + + captureGameRenderingTaskReplacementCount + + deconstructionReplacementCount); + } + + internal static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyEditorWindowCaptureUtilityCaptureWindowRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityArguments( + SplitAttributeArguments(argumentsSource), + GetMigratedEditorWindowCaptureUtilityTimeoutExpression( + match, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + currentFirstPartyToolsNamespaceAliases)); + if (migratedArguments.Length == 0) + { + continue; + } + + (string configureAwaitSuffix, int replacementEndIndex) = + ReadOptionalConfigureAwaitSuffix(source, codeTextMask, closingParenthesisIndex + 1); + bool shouldExtractTexture = ShouldExtractCaptureWindowTexture( + source, + codeTextMask, + match.Index, + replacementEndIndex); + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(shouldExtractTexture ? "(await " : "await "); + builder.Append(CurrentNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + builder.Append(configureAwaitSuffix); + if (shouldExtractTexture) + { + builder.Append(").texture"); + } + + sourceCopyIndex = replacementEndIndex; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + PreviousCodeTokenEquals(source, match.Index, "await") || + !IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityArguments( + SplitAttributeArguments(argumentsSource), + GetMigratedEditorWindowCaptureUtilityTimeoutExpression( + match, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + currentFirstPartyToolsNamespaceAliases)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(CurrentNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(").ContinueWith(__unityCliLoopCaptureTask => "); + builder.Append("__unityCliLoopCaptureTask.GetAwaiter().GetResult().texture)"); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityGameRenderingArguments( + SplitAttributeArguments(argumentsSource), + GetMigratedEditorWindowCaptureUtilityTimeoutExpression( + match, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + currentFirstPartyToolsNamespaceAliases)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append("await "); + builder.Append(CurrentNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureGameRenderingMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + AppendCaptureGameRenderingLegacyTupleProjection(builder); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + internal static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingTaskCallsInCode( + string source, + string[] legacyNamespaceAliases, + string[] currentFirstPartyToolsNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility, + bool shouldQualifyBareEditorWindowCaptureUtilityTimeout, + bool hasProtectedBareEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert( + currentFirstPartyToolsNamespaceAliases != null, + "currentFirstPartyToolsNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyEditorWindowCaptureUtilityCaptureGameRenderingInvocationRegex.Matches(source); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + PreviousCodeTokenEquals(source, match.Index, "await") || + !IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + currentFirstPartyToolsNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility, + hasProtectedBareEditorWindowCaptureUtility)) + { + continue; + } + + int openParenthesisIndex = match.Index + match.Length - 1; + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + openParenthesisIndex); + if (closingParenthesisIndex < 0) + { + continue; + } + + string argumentsSource = source.Substring( + openParenthesisIndex + 1, + closingParenthesisIndex - openParenthesisIndex - 1); + string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityGameRenderingArguments( + SplitAttributeArguments(argumentsSource), + GetMigratedEditorWindowCaptureUtilityTimeoutExpression( + match, + shouldQualifyBareEditorWindowCaptureUtilityTimeout, + currentFirstPartyToolsNamespaceAliases)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(CurrentNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureGameRenderingMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(')'); + AppendCaptureGameRenderingLegacyTupleProjection(builder); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs.meta new file mode 100644 index 0000000000..c7c04f162c --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cc638419c3a74f71bd6fd6ad88878b7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs new file mode 100644 index 0000000000..d145f2bf3d --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Caches source text within one preview pass so analysis phases share a single disk read per file. + /// + internal sealed class ThirdPartyToolMigrationSourceFileCache + { + private readonly Func _readAllText; + private readonly Dictionary _sources = new(StringComparer.Ordinal); + + internal ThirdPartyToolMigrationSourceFileCache() + : this(File.ReadAllText) + { + } + + internal ThirdPartyToolMigrationSourceFileCache(Func readAllText) + { + Debug.Assert(readAllText != null, "readAllText must not be null"); + + _readAllText = readAllText ?? throw new ArgumentNullException(nameof(readAllText)); + } + + internal string ReadAllText(string filePath) + { + Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty"); + + if (_sources.TryGetValue(filePath, out string source)) + { + return source; + } + + string loadedSource = _readAllText(filePath); + _sources.Add(filePath, loadedSource); + return loadedSource; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs.meta new file mode 100644 index 0000000000..c8452f3a58 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33e858fae6dd44183a74a995d87d429b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs new file mode 100644 index 0000000000..3d676f3233 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +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; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Scans a project cheaply to decide whether migration work exists. + /// + internal static class ThirdPartyToolMigrationTargetScanner + { + internal static async Task HasMigrationTargetAsync(string projectRoot, CancellationToken ct) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty"); + + ProjectFileInventory inventory = await ProjectFileInventory.CreateAsync( + projectRoot, + new Progress(), + ct); + if (ct.IsCancellationRequested) + { + return false; + } + + List asmdefDirectories = inventory.AsmdefFilePaths + .Select(path => Path.GetDirectoryName(path) ?? string.Empty) + .Where(path => !string.IsNullOrEmpty(path)) + .OrderByDescending(path => path.Length) + .ToList(); + 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> 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); + int inspectedEntryCount = 0; + foreach (string csharpFilePath in inventory.CSharpFilePaths) + { + if (ct.IsCancellationRequested) + { + return false; + } + + string source = File.ReadAllText(csharpFilePath); + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + string[] declaredTypeNames = ThirdPartyToolMigrationRules.GetDeclaredTypeNames(source); + AddAssemblyScopedNames( + assemblyDeclaredTypeNamesByDirectory, + assemblyDirectory, + declaredTypeNames); + if (ContainsFastCSharpMigrationTarget(source)) + { + return true; + } + + if (ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + 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); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentToolContractsGlobalUsing(source)) + { + assemblyScopedCurrentToolContractsDirectories.Add(assemblyDirectory); + } + + if (ThirdPartyToolMigrationRules.ContainsCurrentApplicationGlobalUsing(source)) + { + assemblyScopedCurrentApplicationDirectories.Add(assemblyDirectory); + 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)); + } + } + + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + + bool hasCSharpSourceTarget = await ContainsFastCSharpSourceMigrationTargetAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedLegacyToolInfoAliasesByDirectory, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasCSharpSourceTarget) + { + return true; + } + + await CollectFastAssemblyReferenceRequirementsAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentDomainDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (assemblyScopedCurrentToolContractsDirectories.Count > 0) + { + await CollectFastAssemblyScopedCurrentToolContractsRequirementsAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentDomainDirectories, + assemblyScopedCurrentApplicationDirectories, + assemblyScopedCurrentApplicationAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + } + + if (assemblyScopedCurrentDomainDirectories.Count > 0) + { + await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + assemblyScopedCurrentDomainDirectories, + domainReferenceAssemblyDirectories, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + } + + bool hasFirstPartyScreenshotSourceTarget = await CollectFastFirstPartyScreenshotRequirementsAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + legacyAssemblyDirectories, + assemblyScopedCurrentToolContractsDirectories, + assemblyScopedCurrentFirstPartyToolsDirectories, + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDeclaredTypeNamesByDirectory, + toolContractsReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + + if (hasFirstPartyScreenshotSourceTarget) + { + return true; + } + + foreach (string asmdefFilePath in inventory.AsmdefFilePaths) + { + if (ct.IsCancellationRequested) + { + return false; + } + + string source = File.ReadAllText(asmdefFilePath); + if (ContainsFastAsmdefMigrationTarget(source)) + { + 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; + } + + MigrationAssemblyUsage assemblyUsage = new( + asmdefDirectories, + assemblyReferenceDirectories, + legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), + CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); + + foreach (string asmdefFilePath in inventory.AsmdefFilePaths) + { + if (ct.IsCancellationRequested) + { + return false; + } + + if (ContainsAsmdefMigrationTarget(asmdefFilePath, projectRoot, assemblyUsage)) + { + return true; + } + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs.meta new file mode 100644 index 0000000000..45ef0cc2a0 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b381f0234535e4124a6ce21563a82ca2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs new file mode 100644 index 0000000000..56b31068e6 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingArgumentRules + { + internal static (string[] Arguments, bool Changed) RemoveLegacyPlayerLoopTimingCallerArguments( + string[] arguments, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters, + string[] legacyNamespaceAliases) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(removedParameters != null, "removedParameters must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + List migratedArguments = new(); + bool changed = false; + int argumentIndex = 0; + foreach (string argument in arguments) + { + string trimmedArgument = argument.Trim(); + if (trimmedArgument.Length == 0) + { + continue; + } + + if (ShouldRemoveLegacyPlayerLoopTimingCallerArgument( + trimmedArgument, + argumentIndex, + removedParameters, + legacyNamespaceAliases)) + { + changed = true; + argumentIndex++; + continue; + } + + migratedArguments.Add(trimmedArgument); + argumentIndex++; + } + + return (migratedArguments.ToArray(), changed); + } + + internal static bool ShouldRemoveLegacyPlayerLoopTimingCallerArgument( + string argument, + int argumentIndex, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters, + string[] legacyNamespaceAliases) + { + Debug.Assert(!string.IsNullOrWhiteSpace(argument), "argument must not be null or whitespace"); + Debug.Assert(argumentIndex >= 0, "argumentIndex must not be negative"); + Debug.Assert(removedParameters != null, "removedParameters must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + foreach (RemovedLegacyPlayerLoopTimingParameter removedParameter in removedParameters) + { + string namedArgumentValue = GetNamedArgumentValueOrNull(argument, removedParameter.Name); + if (namedArgumentValue != null) + { + return IsLegacyPlayerLoopTimingCallerArgument(namedArgumentValue, legacyNamespaceAliases); + } + + if (argumentIndex == removedParameter.Index && + IsLegacyPlayerLoopTimingCallerArgument(argument, legacyNamespaceAliases)) + { + return true; + } + } + + return false; + } + + internal static bool IsLegacyPlayerLoopTimingCallerArgument(string argument, string[] legacyNamespaceAliases) + { + Debug.Assert(argument != null, "argument must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + return IsLegacyPlayerLoopTimingArgument(argument, legacyNamespaceAliases) || + IsLegacyMainThreadSwitcherSingleTimingArgument(argument); + } + + internal static (bool IsMatch, string ParameterName) ReadLegacyPlayerLoopTimingParameter( + string parameter, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyPlayerLoopTiming) + { + Debug.Assert(!string.IsNullOrWhiteSpace(parameter), "parameter must not be null or whitespace"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + (bool qualifiedIsMatch, string qualifiedParameterName) = + MatchesLegacyPlayerLoopTimingParameter( + parameter, + $@"(?:global::)?{Regex.Escape(LegacyNamespace)}\.{LegacyPlayerLoopTimingTypeName}"); + if (qualifiedIsMatch) + { + return (true, qualifiedParameterName); + } + + foreach (string alias in legacyNamespaceAliases) + { + (bool aliasIsMatch, string aliasParameterName) = + MatchesLegacyPlayerLoopTimingParameter( + parameter, + $@"{Regex.Escape(alias)}\.{LegacyPlayerLoopTimingTypeName}"); + if (aliasIsMatch) + { + return (true, aliasParameterName); + } + } + + if (canMigrateBareLegacyPlayerLoopTiming) + { + return MatchesLegacyPlayerLoopTimingParameter(parameter, LegacyPlayerLoopTimingTypeName); + } + + return (false, string.Empty); + } + + internal static (bool IsMatch, string ParameterName) MatchesLegacyPlayerLoopTimingParameter( + string parameter, + string typePattern) + { + Debug.Assert(!string.IsNullOrWhiteSpace(parameter), "parameter must not be null or whitespace"); + Debug.Assert(!string.IsNullOrWhiteSpace(typePattern), "typePattern must not be null or whitespace"); + + Regex parameterRegex = new( + $@"^(?:\[[^\]]+\]\s*)*(?:(?:this|in|ref|out|params)\s+)*(?:{typePattern})\??\s+(?[A-Za-z_][A-Za-z0-9_]*)(?:\s*=.+)?$", + RegexOptions.Compiled); + Match match = parameterRegex.Match(parameter); + if (!match.Success) + { + return (false, string.Empty); + } + + return (true, match.Groups["name"].Value); + } + + internal static bool ContainsIdentifierInCode(string source, string identifier) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier must not be null or empty"); + + Regex identifierRegex = new($@"\b{Regex.Escape(identifier)}\b", RegexOptions.Compiled); + return RegexMatchesCode(source, identifierRegex); + } + + internal static (string[] Arguments, bool Changed) GetMigratedMainThreadSwitcherArguments( + string[] arguments, + string[] legacyNamespaceAliases) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + string[] trimmedArguments = arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + List migratedArguments = new(); + bool changed = false; + for (int index = 0; index < trimmedArguments.Length; index++) + { + string argument = trimmedArguments[index]; + string cancellationTokenValue = + GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (cancellationTokenValue != null) + { + migratedArguments.Add($"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"); + changed = true; + continue; + } + + string timingValue = GetNamedArgumentValueOrNull(argument, LegacyTimingArgumentName); + if (timingValue != null) + { + changed = true; + continue; + } + + if (ReadNamedArgumentName(argument).Length > 0) + { + migratedArguments.Add(argument); + continue; + } + + if (IsLegacyPlayerLoopTimingArgument(argument, legacyNamespaceAliases) || + IsLegacyMainThreadSwitcherPositionalTimingArgument(trimmedArguments, index)) + { + changed = true; + continue; + } + + migratedArguments.Add(argument); + } + + return (migratedArguments.ToArray(), changed); + } + + internal static bool IsLegacyMainThreadSwitcherPositionalTimingArgument( + string[] arguments, + int argumentIndex) + { + Debug.Assert(arguments != null, "arguments must not be null"); + Debug.Assert(argumentIndex >= 0, "argumentIndex must not be negative"); + + if (argumentIndex != 0) + { + return false; + } + + if (ReadNamedArgumentName(arguments[argumentIndex]).Length > 0) + { + return false; + } + + if (arguments.Length == 2) + { + return true; + } + + return arguments.Length == 1 && + IsLegacyMainThreadSwitcherSingleTimingArgument(arguments[0]); + } + + internal static bool IsLegacyMainThreadSwitcherSingleTimingArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + string trimmedArgument = argument.Trim(); + if (IsLikelyCancellationTokenArgument(trimmedArgument)) + { + return false; + } + + bool containsTimingName = + trimmedArgument.IndexOf("Timing", StringComparison.OrdinalIgnoreCase) >= 0; + bool containsPlayerLoopName = + trimmedArgument.IndexOf("PlayerLoop", StringComparison.OrdinalIgnoreCase) >= 0; + bool endsWithLoopName = + trimmedArgument.EndsWith("loop", StringComparison.OrdinalIgnoreCase) || + trimmedArgument.EndsWith(".loop", StringComparison.OrdinalIgnoreCase); + return string.Equals(trimmedArgument, LegacyTimingArgumentName, StringComparison.Ordinal) || + string.Equals(trimmedArgument, "default", StringComparison.Ordinal) || + string.Equals(trimmedArgument, $"default({LegacyPlayerLoopTimingTypeName})", StringComparison.Ordinal) || + containsTimingName || + containsPlayerLoopName || + endsWithLoopName; + } + + internal static bool IsLikelyCancellationTokenArgument(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + return string.Equals(argument, CurrentCancellationTokenArgumentName, StringComparison.Ordinal) || + string.Equals(argument, LegacyCancellationTokenArgumentName, StringComparison.Ordinal) || + string.Equals(argument, "token", StringComparison.OrdinalIgnoreCase) || + argument.EndsWith("Token", StringComparison.Ordinal) || + argument.IndexOf("CancellationToken", StringComparison.Ordinal) >= 0; + } + + internal static bool IsIdentifierLikeExpression(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + if (argument.Length == 0) + { + return false; + } + + if (!IsIdentifierStartCharacter(argument[0])) + { + return false; + } + + for (int i = 1; i < argument.Length; i++) + { + if (!IsIdentifierCharacter(argument[i])) + { + return false; + } + } + + return true; + } + + internal static bool IsLegacyPlayerLoopTimingArgument(string argument, string[] legacyNamespaceAliases) + { + Debug.Assert(argument != null, "argument must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + string trimmedArgument = argument.Trim(); + if (trimmedArgument.StartsWith($"{LegacyPlayerLoopTimingTypeName}.", StringComparison.Ordinal) || + trimmedArgument.StartsWith( + $"global::{LegacyNamespace}.{LegacyPlayerLoopTimingTypeName}.", + StringComparison.Ordinal) || + trimmedArgument.StartsWith( + $"{LegacyNamespace}.{LegacyPlayerLoopTimingTypeName}.", + StringComparison.Ordinal)) + { + return true; + } + + foreach (string alias in legacyNamespaceAliases) + { + if (trimmedArgument.StartsWith( + $"{alias}.{LegacyPlayerLoopTimingTypeName}.", + StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs.meta new file mode 100644 index 0000000000..b83eb45483 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 915e3e92c7d7249269ddc3ed6af80d1a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCallerRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCallerRules.cs new file mode 100644 index 0000000000..6f2b318293 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCallerRules.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingCallerRules + { + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + string source, + string originalSource, + RemovedLegacyPlayerLoopTimingSignature[] removedSignatures, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(originalSource != null, "originalSource must not be null"); + Debug.Assert(removedSignatures != null, "removedSignatures must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases( + originalSource, + legacyAssemblyAliases); + (string migratedContent, int replacementCount) = RemoveLegacyPlayerLoopTimingCallerArgumentsInCode( + source, + removedSignatures, + legacyNamespaceAliases); + return new ThirdPartyToolMigrationContentResult( + migratedContent, + replacementCount, + Array.Empty()); + } + + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + string source, + string originalSource, + string[] legacyAssemblyAliases, + bool canMigrateBareLegacyPlayerLoopTiming, + string[] migratedCalleeMethodNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(originalSource != null, "originalSource must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + Debug.Assert(migratedCalleeMethodNames != null, "migratedCalleeMethodNames must not be null"); + + string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases( + originalSource, + legacyAssemblyAliases); + ( + string migratedContent, + int replacementCount, + RemovedLegacyPlayerLoopTimingSignature[] removedSignatures) = + RemoveLegacyPlayerLoopTimingParametersInCode( + source, + legacyNamespaceAliases, + canMigrateBareLegacyPlayerLoopTiming, + migratedCalleeMethodNames); + return new ThirdPartyToolMigrationContentResult( + migratedContent, + replacementCount, + removedSignatures); + } + + internal static (string Content, int ReplacementCount) RemoveLegacyPlayerLoopTimingCallerArgumentsInCode( + string source, + RemovedLegacyPlayerLoopTimingSignature[] removedSignatures, + string[] legacyNamespaceAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(removedSignatures != null, "removedSignatures must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + string migratedContent = source; + int replacementCount = 0; + foreach (RemovedLegacyPlayerLoopTimingSignature removedSignature in removedSignatures) + { + (string signatureMigratedContent, int signatureReplacementCount) = + RemoveLegacyPlayerLoopTimingCallerArgumentsForMethodInCode( + migratedContent, + removedSignature, + legacyNamespaceAliases); + migratedContent = signatureMigratedContent; + replacementCount += signatureReplacementCount; + } + + return (migratedContent, replacementCount); + } + + internal static (string Content, int ReplacementCount) + RemoveLegacyPlayerLoopTimingCallerArgumentsForMethodInCode( + string source, + RemovedLegacyPlayerLoopTimingSignature removedSignature, + string[] legacyNamespaceAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(removedSignature.MethodName), "MethodName must not be null or empty"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + Regex invocationRegex = new( + $@"(?= 0, "declarationStartIndex must not be negative"); + + int scanIndex = declarationStartIndex - 1; + int removalStartIndex = declarationStartIndex; + while (true) + { + scanIndex = SkipWhitespaceBackward(source, scanIndex); + if (scanIndex < 0 || source[scanIndex] != ']' || !codeTextMask.IsCodeAt(scanIndex)) + { + return removalStartIndex; + } + + int openingBracketIndex = FindOpeningAttributeBracket(source, scanIndex, codeTextMask); + if (openingBracketIndex < 0 || !HasOnlyWhitespaceBeforeIndexOnLine(source, openingBracketIndex)) + { + return removalStartIndex; + } + + removalStartIndex = GetLineStartIndex(source, openingBracketIndex); + scanIndex = removalStartIndex - 1; + } + } + + internal static int SkipWhitespaceBackward(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + + int index = startIndex; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + + return index; + } + + internal static int FindOpeningAttributeBracket( + string source, + int closingBracketIndex, + CodeTextMask codeTextMask) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(closingBracketIndex >= 0, "closingBracketIndex must not be negative"); + + int depth = 0; + for (int index = closingBracketIndex; index >= 0; index--) + { + if (!codeTextMask.IsCodeAt(index)) + { + continue; + } + + char character = source[index]; + if (character == ']') + { + depth++; + continue; + } + + if (character != '[') + { + continue; + } + + depth--; + if (depth == 0) + { + return index; + } + } + + return -1; + } + + internal static bool HasOnlyWhitespaceBeforeIndexOnLine(string source, int index) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(index >= 0, "index must not be negative"); + + int lineStartIndex = GetLineStartIndex(source, index); + for (int currentIndex = lineStartIndex; currentIndex < index; currentIndex++) + { + if (!char.IsWhiteSpace(source[currentIndex])) + { + return false; + } + } + + return true; + } + + internal static int GetLineStartIndex(string source, int index) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(index >= 0, "index must not be negative"); + + int previousNewLineIndex = source.LastIndexOf('\n', index); + return previousNewLineIndex < 0 ? 0 : previousNewLineIndex + 1; + } + + internal static LegacyPlayerLoopTimingParameterDeclaration[] ReadLegacyPlayerLoopTimingParameterDeclarations( + string[] parameters) + { + Debug.Assert(parameters != null, "parameters must not be null"); + + List declarations = new(); + int parameterIndex = 0; + foreach (string parameter in parameters) + { + string trimmedParameter = parameter.Trim(); + if (trimmedParameter.Length == 0) + { + continue; + } + + (string typeName, string parameterName, bool hasDefaultValue) = + ReadParameterTypeNameAndDefaultState(trimmedParameter); + if (typeName.Length > 0 && parameterName.Length > 0) + { + declarations.Add( + new LegacyPlayerLoopTimingParameterDeclaration( + parameterIndex, + typeName, + parameterName, + hasDefaultValue)); + } + + parameterIndex++; + } + + return declarations.ToArray(); + } + + internal static (string TypeName, string ParameterName, bool HasDefaultValue) + ReadParameterTypeNameAndDefaultState(string parameter) + { + Debug.Assert(!string.IsNullOrWhiteSpace(parameter), "parameter must not be null or whitespace"); + + Regex parameterRegex = new( + @"^(?:\[[^\]]+\]\s*)*(?:(?:this|in|ref|out|params)\s+)*(?.+?)\s+(?[A-Za-z_][A-Za-z0-9_]*)(?\s*=.+)?$", + RegexOptions.Compiled); + Match match = parameterRegex.Match(parameter); + if (!match.Success) + { + return (string.Empty, string.Empty, false); + } + + return (match.Groups["type"].Value.Trim(), match.Groups["name"].Value, match.Groups["default"].Success); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCleanupRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCleanupRules.cs.meta new file mode 100644 index 0000000000..912fe959f7 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCleanupRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bbf63ece3753496491207e80f39be47 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs new file mode 100644 index 0000000000..6d507859d0 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingDeclarationRules + { + internal static ( + string Content, + int ReplacementCount, + RemovedLegacyPlayerLoopTimingSignature[] RemovedSignatures) + RemoveLegacyPlayerLoopTimingParametersInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyPlayerLoopTiming, + string[] migratedCalleeMethodNames) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert(migratedCalleeMethodNames != null, "migratedCalleeMethodNames must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + StringBuilder builder = new(source.Length); + List removedSignatures = new(); + int sourceCopyIndex = 0; + int replacementCount = 0; + for (int i = 0; i < source.Length; i++) + { + if (i < sourceCopyIndex || + source[i] != '(' || + !codeTextMask.IsCodeAt(i) || + !CanContainMethodParameterList(source, i)) + { + continue; + } + + string methodName = ReadMethodNameBeforeParameterList(source, i); + if (!IsMethodDeclarationParameterListName(methodName)) + { + continue; + } + + if (IsConstructorDeclarationParameterList(source, codeTextMask, i, methodName)) + { + continue; + } + + int closingParenthesisIndex = FindInvocationClosingParenthesisIndex( + source, + codeTextMask, + i); + if (closingParenthesisIndex < 0) + { + continue; + } + + string parametersSource = source.Substring( + i + 1, + closingParenthesisIndex - i - 1); + (int usageStartIndex, int usageEndIndex) = FindMethodImplementationUsageRange( + source, + codeTextMask, + closingParenthesisIndex + 1); + if (usageStartIndex < 0) + { + continue; + } + + string methodUsageSource = source.Substring( + usageStartIndex, + usageEndIndex - usageStartIndex); + string[] parameters = SplitAttributeArguments(parametersSource); + if (IsContractBoundMethodDeclaration( + source, + codeTextMask, + methodName, + i, + parametersSource)) + { + continue; + } + + ( + string[] migratedParameters, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters) = + RemoveLegacyPlayerLoopTimingParameters( + parameters, + legacyNamespaceAliases, + canMigrateBareLegacyPlayerLoopTiming, + migratedCalleeMethodNames, + methodUsageSource); + if (removedParameters.Length == 0) + { + continue; + } + + if (methodName.Length > 0) + { + removedSignatures.Add( + new RemovedLegacyPlayerLoopTimingSignature( + methodName, + ReadContainingTypeName(source, codeTextMask, i), + ReadLegacyPlayerLoopTimingParameterDeclarations(parameters), + removedParameters)); + } + + builder.Append(source, sourceCopyIndex, i + 1 - sourceCopyIndex); + builder.Append(string.Join(", ", migratedParameters)); + sourceCopyIndex = closingParenthesisIndex; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0, Array.Empty()); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount, removedSignatures.ToArray()); + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs.meta new file mode 100644 index 0000000000..91f4a4f972 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9649e1234eb244dda5e4766dbbf5162 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs new file mode 100644 index 0000000000..a53d8932b1 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingInvocationRules + { + internal static int FindInvocationOpenParenthesisIndex( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int index = ReadNextNonWhitespaceIndex(source, startIndex); + if (index < source.Length && source[index] == '<' && codeTextMask.IsCodeAt(index)) + { + int closeAngleIndex = FindGenericArgumentListEndIndex(source, codeTextMask, index); + if (closeAngleIndex < 0) + { + return -1; + } + + index = ReadNextNonWhitespaceIndex(source, closeAngleIndex + 1); + } + + if (index < source.Length && source[index] == '(' && codeTextMask.IsCodeAt(index)) + { + return index; + } + + return -1; + } + + internal static int FindGenericArgumentListEndIndex( + string source, + CodeTextMask codeTextMask, + int openAngleIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openAngleIndex >= 0, "openAngleIndex must not be negative"); + + int nestingDepth = 0; + for (int index = openAngleIndex; index < source.Length; index++) + { + if (!codeTextMask.IsCodeAt(index)) + { + continue; + } + + if (source[index] == '<') + { + nestingDepth++; + continue; + } + + if (source[index] != '>') + { + continue; + } + + nestingDepth--; + if (nestingDepth == 0) + { + return index; + } + } + + return -1; + } + + internal static bool ShouldMigrateLegacyPlayerLoopTimingCaller( + string source, + CodeTextMask codeTextMask, + int methodNameIndex, + string[] arguments, + RemovedLegacyPlayerLoopTimingSignature removedSignature) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameIndex >= 0, "methodNameIndex must not be negative"); + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] trimmedArguments = GetTrimmedInvocationArguments(arguments); + if (trimmedArguments.Length > removedSignature.OriginalParameters.Length) + { + return false; + } + + if (!DoesPlayerLoopTimingCallerTargetRemovedSignature( + source, + codeTextMask, + methodNameIndex, + removedSignature)) + { + return false; + } + + return AreRemainingPlayerLoopTimingCallerArgumentsCompatible(trimmedArguments, removedSignature); + } + + internal static bool DoesPlayerLoopTimingCallerTargetRemovedSignature( + string source, + CodeTextMask codeTextMask, + int methodNameIndex, + RemovedLegacyPlayerLoopTimingSignature removedSignature) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameIndex >= 0, "methodNameIndex must not be negative"); + + if (removedSignature.DeclaringTypeName.Length == 0) + { + return false; + } + + string targetExpression = ReadMemberTargetExpressionBeforeMethodName(source, methodNameIndex); + if (targetExpression.Length == 0) + { + string containingTypeName = ReadContainingTypeName(source, codeTextMask, methodNameIndex); + return string.Equals( + containingTypeName, + removedSignature.DeclaringTypeName, + StringComparison.Ordinal); + } + + if (string.Equals(targetExpression, "this", StringComparison.Ordinal) || + string.Equals(targetExpression, "base", StringComparison.Ordinal)) + { + string containingTypeName = ReadContainingTypeName(source, codeTextMask, methodNameIndex); + return string.Equals( + containingTypeName, + removedSignature.DeclaringTypeName, + StringComparison.Ordinal); + } + + if (IsQualifiedMemberTargetExpression(targetExpression)) + { + if (IsExactTypeNameReference(targetExpression, removedSignature.DeclaringTypeName)) + { + return true; + } + + if (!IsInstanceMemberTargetExpression(targetExpression)) + { + string namespaceQualifiedTargetExpression = QualifyRelativeTypeName( + targetExpression, + ReadNamespaceName(source, codeTextMask, methodNameIndex)); + return IsExactTypeNameReference( + namespaceQualifiedTargetExpression, + removedSignature.DeclaringTypeName); + } + + string memberIdentifier = ReadLastMemberIdentifier(targetExpression); + if (memberIdentifier.Length == 0) + { + return false; + } + + return ContainsIdentifierTypeNameReference( + source, + memberIdentifier, + methodNameIndex, + removedSignature.DeclaringTypeName); + } + + if (IsTypeNameReference(targetExpression, removedSignature.DeclaringTypeName)) + { + return true; + } + + string targetIdentifierTypeName = QualifyTypeName( + targetExpression, + ReadNamespaceName(source, codeTextMask, methodNameIndex)); + if (IsTypeNameReference(targetIdentifierTypeName, removedSignature.DeclaringTypeName)) + { + return true; + } + + return ContainsIdentifierTypeNameReference( + source, + targetExpression, + methodNameIndex, + removedSignature.DeclaringTypeName); + } + + internal static bool AreRemainingPlayerLoopTimingCallerArgumentsCompatible( + string[] arguments, + RemovedLegacyPlayerLoopTimingSignature removedSignature) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + foreach (LegacyPlayerLoopTimingParameterDeclaration parameter in removedSignature.OriginalParameters) + { + if (IsRemovedPlayerLoopTimingParameter(parameter.Index, removedSignature.RemovedParameters)) + { + continue; + } + + string argument = ReadCallerArgumentForParameter(arguments, parameter); + if (argument.Length == 0) + { + if (!parameter.HasDefaultValue) + { + return false; + } + + continue; + } + + if (IsCancellationTokenParameter(parameter.TypeName) && + !IsLikelyCancellationTokenArgument(argument)) + { + return false; + } + } + + return true; + } + + internal static bool IsRemovedPlayerLoopTimingParameter( + int parameterIndex, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters) + { + Debug.Assert(parameterIndex >= 0, "parameterIndex must not be negative"); + Debug.Assert(removedParameters != null, "removedParameters must not be null"); + + foreach (RemovedLegacyPlayerLoopTimingParameter removedParameter in removedParameters) + { + if (removedParameter.Index == parameterIndex) + { + return true; + } + } + + return false; + } + + internal static string ReadCallerArgumentForParameter( + string[] arguments, + LegacyPlayerLoopTimingParameterDeclaration parameter) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + foreach (string argument in arguments) + { + string namedArgumentValue = GetNamedArgumentValueOrNull(argument, parameter.Name); + if (namedArgumentValue != null) + { + return namedArgumentValue; + } + } + + if (parameter.Index >= arguments.Length) + { + return string.Empty; + } + + string positionalArgument = arguments[parameter.Index]; + string positionalArgumentName = ReadNamedArgumentName(positionalArgument); + if (positionalArgumentName.Length > 0 && + !string.Equals(positionalArgumentName, parameter.Name, StringComparison.Ordinal)) + { + return string.Empty; + } + + return positionalArgument; + } + + internal static string ReadNamedArgumentName(string argument) + { + Debug.Assert(argument != null, "argument must not be null"); + + int colonIndex = FindNamedArgumentColonIndex(argument); + if (colonIndex <= 0) + { + return string.Empty; + } + + string possibleArgumentName = argument.Substring(0, colonIndex).Trim(); + return IsIdentifierLikeExpression(possibleArgumentName) ? possibleArgumentName : string.Empty; + } + + internal static bool IsCancellationTokenParameter(string typeName) + { + Debug.Assert(typeName != null, "typeName must not be null"); + + return typeName.EndsWith("CancellationToken", StringComparison.Ordinal) || + typeName.IndexOf(".CancellationToken", StringComparison.Ordinal) >= 0; + } + + internal static string ReadMemberTargetExpressionBeforeMethodName(string source, int methodNameIndex) + { + 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--; + } + + if (index < 0 || source[index] != '.') + { + return string.Empty; + } + + index--; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + + if (index >= 0 && (source[index] == '?' || source[index] == '!')) + { + index--; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + } + + int expressionEndIndex = index + 1; + while (index >= 0) + { + if (IsIdentifierCharacter(source[index]) || source[index] == '.') + { + index--; + continue; + } + + if (source[index] == ':' && index > 0 && source[index - 1] == ':') + { + index -= 2; + continue; + } + + break; + } + + return source.Substring(index + 1, expressionEndIndex - index - 1).Trim(); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs.meta new file mode 100644 index 0000000000..33e5dabeae --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc99272f664bf466fb546f47876cfa2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs new file mode 100644 index 0000000000..60b672af5d --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingMethodBodyRules + { + internal static (int StartIndex, int EndIndex) FindMethodImplementationUsageRange( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + for (int i = startIndex; i < source.Length; i++) + { + if (!codeTextMask.IsCodeAt(i)) + { + continue; + } + + if (source[i] == '{') + { + int blockEndIndex = FindBlockClosingBraceIndex(source, codeTextMask, i); + if (blockEndIndex < 0) + { + return (-1, -1); + } + + return (i + 1, blockEndIndex); + } + + if (source[i] == '=' && + i + 1 < source.Length && + source[i + 1] == '>' && + codeTextMask.IsCodeAt(i + 1)) + { + int expressionEndIndex = FindExpressionBodiedMemberSemicolonIndex( + source, + codeTextMask, + i + 2); + if (expressionEndIndex < 0) + { + return (-1, -1); + } + + return (i + 2, expressionEndIndex); + } + + if (source[i] == ';' || source[i] == '=') + { + return (-1, -1); + } + } + + return (-1, -1); + } + + internal static int FindBlockClosingBraceIndex( + string source, + CodeTextMask codeTextMask, + int openBraceIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openBraceIndex >= 0, "openBraceIndex must not be negative"); + + int nestedBraceDepth = 0; + for (int i = openBraceIndex + 1; i < source.Length; i++) + { + if (!codeTextMask.IsCodeAt(i)) + { + continue; + } + + if (source[i] == '{') + { + nestedBraceDepth++; + continue; + } + + if (source[i] != '}') + { + continue; + } + + if (nestedBraceDepth == 0) + { + return i; + } + + nestedBraceDepth--; + } + + return -1; + } + + internal static int FindExpressionBodiedMemberSemicolonIndex( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int nestedParenthesisDepth = 0; + int nestedBracketDepth = 0; + int nestedBraceDepth = 0; + for (int i = startIndex; i < source.Length; i++) + { + if (!codeTextMask.IsCodeAt(i)) + { + continue; + } + + char current = source[i]; + if (current == '(') + { + nestedParenthesisDepth++; + continue; + } + + if (current == ')') + { + nestedParenthesisDepth--; + continue; + } + + if (current == '[') + { + nestedBracketDepth++; + continue; + } + + if (current == ']') + { + nestedBracketDepth--; + continue; + } + + if (current == '{') + { + nestedBraceDepth++; + continue; + } + + if (current == '}') + { + nestedBraceDepth--; + continue; + } + + if (current == ';' && + nestedParenthesisDepth == 0 && + nestedBracketDepth == 0 && + nestedBraceDepth == 0) + { + return i; + } + } + + return -1; + } + + internal static ( + string[] Parameters, + RemovedLegacyPlayerLoopTimingParameter[] RemovedParameters) + RemoveLegacyPlayerLoopTimingParameters( + string[] parameters, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyPlayerLoopTiming, + string[] migratedCalleeMethodNames, + string methodBody) + { + Debug.Assert(parameters != null, "parameters must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + Debug.Assert(migratedCalleeMethodNames != null, "migratedCalleeMethodNames must not be null"); + Debug.Assert(methodBody != null, "methodBody must not be null"); + + bool canRemoveTimingParameter = + CanRemoveLegacyPlayerLoopTimingParameterFromMethod(methodBody, migratedCalleeMethodNames); + List migratedParameters = new(); + List removedParameters = new(); + int parameterIndex = 0; + foreach (string parameter in parameters) + { + string trimmedParameter = parameter.Trim(); + if (trimmedParameter.Length == 0) + { + continue; + } + + (bool isLegacyPlayerLoopTimingParameter, string parameterName) = + ReadLegacyPlayerLoopTimingParameter( + trimmedParameter, + legacyNamespaceAliases, + canMigrateBareLegacyPlayerLoopTiming); + if (isLegacyPlayerLoopTimingParameter && + canRemoveTimingParameter && + !ContainsIdentifierInCode(methodBody, parameterName)) + { + removedParameters.Add( + new RemovedLegacyPlayerLoopTimingParameter( + parameterIndex, + parameterName)); + parameterIndex++; + continue; + } + + migratedParameters.Add(trimmedParameter); + parameterIndex++; + } + + return (migratedParameters.ToArray(), removedParameters.ToArray()); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs.meta new file mode 100644 index 0000000000..143cf29eff --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68be938de8fcd4364bb4f1fd7aa2dd3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs new file mode 100644 index 0000000000..6f8cdd717a --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingMethodDeclarationRules + { + internal static bool IsMethodDeclarationParameterListName(string methodName) + { + Debug.Assert(methodName != null, "methodName must not be null"); + + if (methodName.Length == 0) + { + return false; + } + + return !string.Equals(methodName, "async", StringComparison.Ordinal) && + !string.Equals(methodName, "delegate", StringComparison.Ordinal) && + !string.Equals(methodName, "if", StringComparison.Ordinal) && + !string.Equals(methodName, "for", StringComparison.Ordinal) && + !string.Equals(methodName, "foreach", StringComparison.Ordinal) && + !string.Equals(methodName, "while", StringComparison.Ordinal) && + !string.Equals(methodName, "switch", StringComparison.Ordinal) && + !string.Equals(methodName, "using", StringComparison.Ordinal) && + !string.Equals(methodName, "lock", StringComparison.Ordinal) && + !string.Equals(methodName, "catch", StringComparison.Ordinal); + } + + internal static bool IsConstructorDeclarationParameterList( + string source, + CodeTextMask codeTextMask, + int parameterListStartIndex, + string methodName) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(parameterListStartIndex >= 0, "parameterListStartIndex must not be negative"); + Debug.Assert(methodName != null, "methodName must not be null"); + + string containingTypeName = ReadContainingTypeName(source, codeTextMask, parameterListStartIndex); + if (containingTypeName.Length == 0) + { + return false; + } + + return string.Equals( + GetUnqualifiedTypeName(containingTypeName), + methodName, + StringComparison.Ordinal); + } + + internal static bool IsContractBoundMethodDeclaration( + string source, + CodeTextMask codeTextMask, + string methodName, + int parameterListStartIndex, + string parametersSource) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodName != null, "methodName must not be null"); + Debug.Assert(parameterListStartIndex >= 0, "parameterListStartIndex must not be negative"); + Debug.Assert(parametersSource != null, "parametersSource must not be null"); + + int methodNameStartIndex = ReadMethodNameStartIndexBeforeParameterList( + source, + parameterListStartIndex); + if (methodNameStartIndex < 0) + { + return false; + } + + if (IsVirtualOrOverrideMethodDeclaration(source, methodNameStartIndex)) + { + return true; + } + + if (IsExplicitInterfaceMethodDeclaration(source, methodNameStartIndex)) + { + return true; + } + + if (IsPossibleExternalContractMethodDeclaration(source, codeTextMask, methodNameStartIndex)) + { + return true; + } + + return ContainsInterfaceMethodContract( + source, + codeTextMask, + methodName, + parametersSource); + } + + internal static int ReadMethodNameStartIndexBeforeParameterList( + string source, + int parameterListStartIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(parameterListStartIndex >= 0, "parameterListStartIndex must not be negative"); + + int methodNameEndIndex = parameterListStartIndex - 1; + while (methodNameEndIndex >= 0 && char.IsWhiteSpace(source[methodNameEndIndex])) + { + methodNameEndIndex--; + } + + int methodNameStartIndex = methodNameEndIndex; + while (methodNameStartIndex >= 0 && IsIdentifierCharacter(source[methodNameStartIndex])) + { + methodNameStartIndex--; + } + + int candidateStartIndex = methodNameStartIndex + 1; + return candidateStartIndex <= methodNameEndIndex ? candidateStartIndex : -1; + } + + internal static bool IsVirtualOrOverrideMethodDeclaration(string source, int methodNameStartIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameStartIndex >= 0, "methodNameStartIndex must not be negative"); + + int lineStartIndex = GetLineStartIndex(source, methodNameStartIndex); + string declarationPrefix = source.Substring( + lineStartIndex, + methodNameStartIndex - lineStartIndex); + return ContainsIdentifierInCode(declarationPrefix, "override") || + ContainsIdentifierInCode(declarationPrefix, "virtual"); + } + + internal static bool IsExplicitInterfaceMethodDeclaration(string source, int methodNameStartIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameStartIndex >= 0, "methodNameStartIndex must not be negative"); + + int previousIndex = methodNameStartIndex - 1; + while (previousIndex >= 0 && char.IsWhiteSpace(source[previousIndex])) + { + previousIndex--; + } + + return previousIndex >= 0 && source[previousIndex] == '.'; + } + + internal static bool IsPossibleExternalContractMethodDeclaration( + string source, + CodeTextMask codeTextMask, + int methodNameStartIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameStartIndex >= 0, "methodNameStartIndex must not be negative"); + + if (!IsPublicMethodDeclaration(source, methodNameStartIndex)) + { + return false; + } + + return IsInsideTypeWithBaseList(source, codeTextMask, methodNameStartIndex); + } + + internal static bool IsPublicMethodDeclaration(string source, int methodNameStartIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(methodNameStartIndex >= 0, "methodNameStartIndex must not be negative"); + + int lineStartIndex = GetLineStartIndex(source, methodNameStartIndex); + string declarationPrefix = source.Substring( + lineStartIndex, + methodNameStartIndex - lineStartIndex); + return ContainsIdentifierInCode(declarationPrefix, "public"); + } + + internal static bool IsInsideTypeWithBaseList( + string source, + CodeTextMask codeTextMask, + int memberIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(memberIndex >= 0, "memberIndex must not be negative"); + + MatchCollection matches = TypeDeclarationNameRegex.Matches(source); + foreach (Match match in matches) + { + if (match.Index >= memberIndex || !codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + int openBraceIndex = FindTypeBodyOpenBraceIndex(source, codeTextMask, match.Index + match.Length); + if (openBraceIndex < 0 || openBraceIndex >= memberIndex) + { + continue; + } + + int closingBraceIndex = FindBlockClosingBraceIndex(source, codeTextMask, openBraceIndex); + if (closingBraceIndex < memberIndex) + { + continue; + } + + string typeDeclarationHeader = source.Substring( + match.Index, + openBraceIndex - match.Index); + if (typeDeclarationHeader.IndexOf(':') >= 0) + { + return true; + } + } + + return false; + } + + internal static bool ContainsInterfaceMethodContract( + string source, + CodeTextMask codeTextMask, + string methodName, + string parametersSource) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName must not be null or empty"); + Debug.Assert(parametersSource != null, "parametersSource must not be null"); + + int parameterCount = CountParameters(parametersSource); + MatchCollection interfaceMatches = InterfaceDeclarationNameRegex.Matches(source); + foreach (Match interfaceMatch in interfaceMatches) + { + if (!codeTextMask.IsCodeAt(interfaceMatch.Index)) + { + continue; + } + + int openBraceIndex = FindTypeBodyOpenBraceIndex( + source, + codeTextMask, + interfaceMatch.Index + interfaceMatch.Length); + if (openBraceIndex < 0) + { + continue; + } + + int closingBraceIndex = FindBlockClosingBraceIndex(source, codeTextMask, openBraceIndex); + if (closingBraceIndex <= openBraceIndex) + { + continue; + } + + if (ContainsInterfaceMethodContractInBody( + source, + codeTextMask, + methodName, + parameterCount, + openBraceIndex + 1, + closingBraceIndex)) + { + return true; + } + } + + return false; + } + + internal static bool ContainsInterfaceMethodContractInBody( + string source, + CodeTextMask codeTextMask, + string methodName, + int parameterCount, + int interfaceBodyStartIndex, + int interfaceBodyEndIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName must not be null or empty"); + Debug.Assert(parameterCount >= 0, "parameterCount must not be negative"); + Debug.Assert(interfaceBodyStartIndex >= 0, "interfaceBodyStartIndex must not be negative"); + Debug.Assert(interfaceBodyEndIndex >= interfaceBodyStartIndex, "interfaceBodyEndIndex must be valid"); + + Regex methodRegex = new($@"(? interfaceBodyEndIndex) + { + continue; + } + + string interfaceParametersSource = source.Substring( + parameterListStartIndex + 1, + closingParenthesisIndex - parameterListStartIndex - 1); + if (CountParameters(interfaceParametersSource) == parameterCount) + { + return true; + } + } + + return false; + } + + internal static int CountParameters(string parametersSource) + { + Debug.Assert(parametersSource != null, "parametersSource must not be null"); + + return SplitAttributeArguments(parametersSource) + .Select(parameter => parameter.Trim()) + .Count(parameter => parameter.Length > 0); + } + + internal static bool CanContainMethodParameterList(string source, int openParenthesisIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openParenthesisIndex >= 0, "openParenthesisIndex must not be negative"); + + char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, openParenthesisIndex); + return IsIdentifierCharacter(previousCharacter) || previousCharacter == '>'; + } + + internal static string ReadMethodNameBeforeParameterList(string source, int openParenthesisIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(openParenthesisIndex >= 0, "openParenthesisIndex must not be negative"); + + int index = openParenthesisIndex - 1; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + + if (index >= 0 && source[index] == '>') + { + int genericStartIndex = FindGenericArgumentListStartIndex(source, index); + if (genericStartIndex < 0) + { + return string.Empty; + } + + index = genericStartIndex - 1; + while (index >= 0 && char.IsWhiteSpace(source[index])) + { + index--; + } + } + + int identifierEndIndex = index + 1; + while (index >= 0 && IsIdentifierCharacter(source[index])) + { + index--; + } + + return source.Substring(index + 1, identifierEndIndex - index - 1); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs.meta new file mode 100644 index 0000000000..61684a771e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d27a7656dc0840a6a3b17864748396d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs new file mode 100644 index 0000000000..22dc0e792e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingTypeNameRules + { + internal static string[] ReadImportedNamespaceNames( + string source, + CodeTextMask codeTextMask, + int beforeIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(beforeIndex >= 0, "beforeIndex must not be negative"); + + Regex usingNamespaceRegex = new( + @"^\s*using\s+(?!static\b)(?(?:global::)?[A-Za-z_][A-Za-z0-9_.]*)\s*;", + RegexOptions.Compiled | RegexOptions.Multiline); + MatchCollection matches = usingNamespaceRegex.Matches(source); + List namespaces = new(); + foreach (Match match in matches) + { + if (match.Index >= beforeIndex || !codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + AddTypeNameCandidate(namespaces, match.Groups["namespace"].Value); + } + + return namespaces.ToArray(); + } + + internal static bool IsInstanceMemberTargetExpression(string targetExpression) + { + Debug.Assert(targetExpression != null, "targetExpression must not be null"); + + return targetExpression.StartsWith("this.", StringComparison.Ordinal) || + targetExpression.StartsWith("base.", StringComparison.Ordinal); + } + + internal static string ReadLastMemberIdentifier(string targetExpression) + { + Debug.Assert(targetExpression != null, "targetExpression must not be null"); + + int lastDotIndex = targetExpression.LastIndexOf('.'); + if (lastDotIndex < 0 || lastDotIndex >= targetExpression.Length - 1) + { + return string.Empty; + } + + return targetExpression.Substring(lastDotIndex + 1); + } + + internal static string ReadVarInitializerTypeName(string source, int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + int index = ReadNextNonWhitespaceIndex(source, startIndex); + if (index >= source.Length || source[index] != '=') + { + return string.Empty; + } + + index = ReadNextNonWhitespaceIndex(source, index + 1); + const string newKeyword = "new"; + if (!StartsWith(source, index, newKeyword)) + { + return string.Empty; + } + + int typeStartIndex = ReadNextNonWhitespaceIndex(source, index + newKeyword.Length); + int typeEndIndex = typeStartIndex; + int genericDepth = 0; + while (typeEndIndex < source.Length) + { + char current = source[typeEndIndex]; + if (current == '<') + { + genericDepth++; + typeEndIndex++; + continue; + } + + if (current == '>') + { + genericDepth--; + typeEndIndex++; + continue; + } + + if (genericDepth == 0 && + (char.IsWhiteSpace(current) || current == '(' || current == '{' || current == '[')) + { + break; + } + + typeEndIndex++; + } + + if (typeEndIndex <= typeStartIndex) + { + return string.Empty; + } + + return source.Substring(typeStartIndex, typeEndIndex - typeStartIndex); + } + + internal static bool IsQualifiedMemberTargetExpression(string targetExpression) + { + Debug.Assert(targetExpression != null, "targetExpression must not be null"); + + return targetExpression.StartsWith("global::", StringComparison.Ordinal) || + targetExpression.IndexOf('.') >= 0; + } + + internal static bool IsExactTypeNameReference(string candidateTypeName, string expectedTypeName) + { + Debug.Assert(candidateTypeName != null, "candidateTypeName must not be null"); + Debug.Assert(expectedTypeName != null, "expectedTypeName must not be null"); + + if (candidateTypeName.Length == 0 || expectedTypeName.Length == 0) + { + return false; + } + + return string.Equals( + NormalizeTypeNameForComparison(candidateTypeName), + NormalizeTypeNameForComparison(expectedTypeName), + StringComparison.Ordinal); + } + + internal static bool IsTypeNameReference(string candidateTypeName, string expectedTypeName) + { + Debug.Assert(candidateTypeName != null, "candidateTypeName must not be null"); + Debug.Assert(expectedTypeName != null, "expectedTypeName must not be null"); + + if (candidateTypeName.Length == 0 || expectedTypeName.Length == 0) + { + return false; + } + + string normalizedCandidateTypeName = NormalizeTypeNameForComparison(candidateTypeName); + string normalizedExpectedTypeName = NormalizeTypeNameForComparison(expectedTypeName); + if (normalizedExpectedTypeName.IndexOf('.') >= 0) + { + return string.Equals( + normalizedCandidateTypeName, + normalizedExpectedTypeName, + StringComparison.Ordinal); + } + + return string.Equals( + GetUnqualifiedTypeName(normalizedCandidateTypeName), + normalizedExpectedTypeName, + StringComparison.Ordinal); + } + + internal static string NormalizeTypeNameForComparison(string typeName) + { + Debug.Assert(typeName != null, "typeName must not be null"); + + string normalizedTypeName = typeName.StartsWith("global::", StringComparison.Ordinal) + ? typeName.Substring("global::".Length) + : typeName; + int genericStartIndex = normalizedTypeName.IndexOf('<'); + return genericStartIndex >= 0 + ? normalizedTypeName.Substring(0, genericStartIndex) + : normalizedTypeName; + } + + internal static string GetUnqualifiedTypeName(string typeName) + { + Debug.Assert(typeName != null, "typeName must not be null"); + + int lastDotIndex = typeName.LastIndexOf('.'); + return lastDotIndex >= 0 + ? typeName.Substring(lastDotIndex + 1) + : typeName; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs.meta new file mode 100644 index 0000000000..2df3979d1e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a99fb69c1fea472c9278ba39cf3f1c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs new file mode 100644 index 0000000000..0fdb4d0739 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingTypeResolutionRules + { + internal static bool ContainsIdentifierTypeNameReference( + string source, + string identifier, + int beforeIndex, + string expectedTypeName) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier must not be null or empty"); + Debug.Assert(beforeIndex >= 0, "beforeIndex must not be negative"); + Debug.Assert(expectedTypeName != null, "expectedTypeName must not be null"); + + string[] typeNameCandidates = ReadIdentifierTypeNameCandidates(source, identifier, beforeIndex); + foreach (string typeNameCandidate in typeNameCandidates) + { + if (IsTypeNameReference(typeNameCandidate, expectedTypeName)) + { + return true; + } + } + + return false; + } + + internal static string[] ReadIdentifierTypeNameCandidates( + string source, + string identifier, + int beforeIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier must not be null or empty"); + Debug.Assert(beforeIndex >= 0, "beforeIndex must not be negative"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + (int typeBodyStartIndex, int typeBodyEndIndex) = + ReadInnermostContainingTypeBodyRange(source, codeTextMask, beforeIndex); + int searchOffset = typeBodyStartIndex < 0 + ? 0 + : ReadActiveTypeMemberStartIndex(source, codeTextMask, typeBodyStartIndex, beforeIndex); + string searchSource = source.Substring(searchOffset, beforeIndex - searchOffset); + Regex declarationRegex = new( + $@"(?(?:global::)?[A-Za-z_][A-Za-z0-9_.]*(?:<[^;(){{}}]*>)?)(?:\s*\?)?\s+{Regex.Escape(identifier)}\b(?=\s*(?:=>|[=,);{{]|$))", + RegexOptions.Compiled); + MatchCollection matches = declarationRegex.Matches(searchSource); + string declaredTypeName = + ReadLastIdentifierTypeNameFromMatches( + searchSource, + searchOffset, + codeTextMask, + matches, + false); + + if (declaredTypeName.Length == 0) + { + if (typeBodyStartIndex < 0) + { + return Array.Empty(); + } + + searchOffset = typeBodyStartIndex; + searchSource = source.Substring( + typeBodyStartIndex, + typeBodyEndIndex - typeBodyStartIndex); + matches = declarationRegex.Matches(searchSource); + declaredTypeName = + ReadLastIdentifierTypeNameFromMatches( + searchSource, + searchOffset, + codeTextMask, + matches, + true); + if (declaredTypeName.Length == 0) + { + return Array.Empty(); + } + } + + if (IsQualifiedMemberTargetExpression(declaredTypeName)) + { + return new[] { declaredTypeName }; + } + + List typeNameCandidates = new(); + AddTypeNameCandidate( + typeNameCandidates, + QualifyTypeName( + declaredTypeName, + ReadNamespaceName(source, codeTextMask, beforeIndex))); + AddTypeNameCandidate(typeNameCandidates, declaredTypeName); + string[] importedNamespaces = ReadImportedNamespaceNames(source, codeTextMask, beforeIndex); + foreach (string importedNamespace in importedNamespaces) + { + AddTypeNameCandidate(typeNameCandidates, $"{importedNamespace}.{declaredTypeName}"); + } + + return typeNameCandidates.ToArray(); + } + + internal static string ReadLastIdentifierTypeNameFromMatches( + string searchSource, + int searchOffset, + CodeTextMask codeTextMask, + MatchCollection matches, + bool requireTopLevelTypeMember) + { + Debug.Assert(searchSource != null, "searchSource must not be null"); + Debug.Assert(searchOffset >= 0, "searchOffset must not be negative"); + Debug.Assert(matches != null, "matches must not be null"); + + string declaredTypeName = string.Empty; + foreach (Match match in matches) + { + int sourceMatchIndex = searchOffset + match.Index; + if (!codeTextMask.IsCodeAt(sourceMatchIndex)) + { + continue; + } + + if (requireTopLevelTypeMember && + !IsTopLevelTypeMemberMatch(searchSource, searchOffset, codeTextMask, match.Index)) + { + continue; + } + + string matchTypeName = match.Groups["type"].Value; + if (string.Equals(matchTypeName, "var", StringComparison.Ordinal)) + { + matchTypeName = ReadVarInitializerTypeName(searchSource, match.Index + match.Length); + } + + if (matchTypeName.Length > 0) + { + declaredTypeName = matchTypeName; + } + } + + return declaredTypeName; + } + + internal static int ReadActiveTypeMemberStartIndex( + string source, + CodeTextMask codeTextMask, + int typeBodyStartIndex, + int memberIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(typeBodyStartIndex >= 0, "typeBodyStartIndex must not be negative"); + Debug.Assert(memberIndex >= typeBodyStartIndex, "memberIndex must not precede the type body"); + + int activeMemberStartIndex = typeBodyStartIndex; + int blockDepth = 0; + for (int index = typeBodyStartIndex; index < memberIndex; index++) + { + if (!codeTextMask.IsCodeAt(index)) + { + continue; + } + + if (source[index] == '{') + { + blockDepth++; + continue; + } + + if (source[index] == '}') + { + blockDepth--; + if (blockDepth == 0) + { + activeMemberStartIndex = index + 1; + } + + continue; + } + + if (blockDepth == 0 && source[index] == ';') + { + activeMemberStartIndex = index + 1; + } + } + + return activeMemberStartIndex; + } + + internal static bool IsTopLevelTypeMemberMatch( + string searchSource, + int searchOffset, + CodeTextMask codeTextMask, + int matchIndex) + { + Debug.Assert(searchSource != null, "searchSource must not be null"); + Debug.Assert(searchOffset >= 0, "searchOffset must not be negative"); + Debug.Assert(matchIndex >= 0, "matchIndex must not be negative"); + + int blockDepth = 0; + int parenthesisDepth = 0; + for (int index = 0; index < matchIndex; index++) + { + int sourceIndex = searchOffset + index; + if (!codeTextMask.IsCodeAt(sourceIndex)) + { + continue; + } + + if (searchSource[index] == '{') + { + blockDepth++; + continue; + } + + if (searchSource[index] == '}') + { + blockDepth--; + continue; + } + + if (blockDepth != 0) + { + continue; + } + + if (searchSource[index] == '(') + { + parenthesisDepth++; + continue; + } + + if (searchSource[index] == ')') + { + parenthesisDepth--; + } + } + + return blockDepth == 0 && parenthesisDepth == 0; + } + + internal static (int StartIndex, int EndIndex) ReadInnermostContainingTypeBodyRange( + string source, + CodeTextMask codeTextMask, + int memberIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(memberIndex >= 0, "memberIndex must not be negative"); + + int startIndex = -1; + int endIndex = -1; + MatchCollection matches = TypeDeclarationNameRegex.Matches(source); + foreach (Match match in matches) + { + if (match.Index >= memberIndex) + { + break; + } + + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + int openBraceIndex = FindTypeBodyOpenBraceIndex(source, codeTextMask, match.Index + match.Length); + if (openBraceIndex < 0 || openBraceIndex >= memberIndex) + { + continue; + } + + int closingBraceIndex = FindBlockClosingBraceIndex(source, codeTextMask, openBraceIndex); + if (closingBraceIndex < memberIndex) + { + continue; + } + + startIndex = openBraceIndex + 1; + endIndex = closingBraceIndex; + } + + return (startIndex, endIndex); + } + + internal static void AddTypeNameCandidate(List typeNameCandidates, string typeName) + { + Debug.Assert(typeNameCandidates != null, "typeNameCandidates must not be null"); + Debug.Assert(typeName != null, "typeName must not be null"); + + if (typeName.Length == 0 || typeNameCandidates.Contains(typeName)) + { + return; + } + + typeNameCandidates.Add(typeName); + } + + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs.meta new file mode 100644 index 0000000000..447dd853eb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 06734679e40704bd19a66ca7fead123f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs new file mode 100644 index 0000000000..ebb008e380 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTimingTypeScopeRules + { + internal static int FindGenericArgumentListStartIndex(string source, int closeAngleIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(closeAngleIndex >= 0, "closeAngleIndex must not be negative"); + + int nestingDepth = 0; + for (int index = closeAngleIndex; index >= 0; index--) + { + if (source[index] == '>') + { + nestingDepth++; + continue; + } + + if (source[index] != '<') + { + continue; + } + + nestingDepth--; + if (nestingDepth == 0) + { + return index; + } + } + + return -1; + } + + internal static string ReadContainingTypeName( + string source, + CodeTextMask codeTextMask, + int memberIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(memberIndex >= 0, "memberIndex must not be negative"); + + List containingTypeNames = new(); + MatchCollection matches = TypeDeclarationNameRegex.Matches(source); + foreach (Match match in matches) + { + if (match.Index >= memberIndex) + { + break; + } + + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + int openBraceIndex = FindTypeBodyOpenBraceIndex(source, codeTextMask, match.Index + match.Length); + if (openBraceIndex < 0 || openBraceIndex >= memberIndex) + { + continue; + } + + int closingBraceIndex = FindBlockClosingBraceIndex(source, codeTextMask, openBraceIndex); + if (closingBraceIndex < memberIndex) + { + continue; + } + + containingTypeNames.Add(match.Groups["name"].Value); + } + + if (containingTypeNames.Count == 0) + { + return string.Empty; + } + + return QualifyNestedTypeName( + string.Join(".", containingTypeNames), + ReadNamespaceName(source, codeTextMask, memberIndex)); + } + + internal static string QualifyNestedTypeName(string nestedTypeName, string namespaceName) + { + Debug.Assert(!string.IsNullOrEmpty(nestedTypeName), "nestedTypeName must not be null or empty"); + Debug.Assert(namespaceName != null, "namespaceName must not be null"); + + return namespaceName.Length == 0 + ? nestedTypeName + : $"{namespaceName}.{nestedTypeName}"; + } + + internal static string QualifyRelativeTypeName(string typeName, string namespaceName) + { + Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be null or empty"); + Debug.Assert(namespaceName != null, "namespaceName must not be null"); + + if (namespaceName.Length == 0 || typeName.StartsWith("global::", StringComparison.Ordinal)) + { + return typeName; + } + + return $"{namespaceName}.{typeName}"; + } + + internal static string ReadNamespaceName( + string source, + CodeTextMask codeTextMask, + int memberIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(memberIndex >= 0, "memberIndex must not be negative"); + + string namespaceName = string.Empty; + MatchCollection matches = NamespaceDeclarationRegex.Matches(source); + foreach (Match match in matches) + { + if (match.Index >= memberIndex) + { + break; + } + + if (!codeTextMask.IsCodeAt(match.Index)) + { + continue; + } + + string candidateNamespaceName = match.Groups["name"].Value; + string terminator = match.Groups["terminator"].Value; + if (string.Equals(terminator, ";", StringComparison.Ordinal)) + { + namespaceName = candidateNamespaceName; + continue; + } + + int openBraceIndex = match.Groups["terminator"].Index; + int closingBraceIndex = FindBlockClosingBraceIndex(source, codeTextMask, openBraceIndex); + if (closingBraceIndex >= memberIndex) + { + namespaceName = candidateNamespaceName; + } + } + + return namespaceName; + } + + internal static string QualifyTypeName(string typeName, string namespaceName) + { + Debug.Assert(typeName != null, "typeName must not be null"); + Debug.Assert(namespaceName != null, "namespaceName must not be null"); + + if (typeName.Length == 0 || + namespaceName.Length == 0 || + typeName.StartsWith("global::", StringComparison.Ordinal) || + typeName.IndexOf('.') >= 0) + { + return typeName; + } + + return $"{namespaceName}.{typeName}"; + } + + internal static int FindTypeBodyOpenBraceIndex( + string source, + CodeTextMask codeTextMask, + int startIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(startIndex >= 0, "startIndex must not be negative"); + + for (int index = startIndex; index < source.Length; index++) + { + if (!codeTextMask.IsCodeAt(index)) + { + continue; + } + + if (source[index] == '{') + { + return index; + } + + if (source[index] == ';') + { + return -1; + } + } + + return -1; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs.meta new file mode 100644 index 0000000000..b9e23c0808 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c259d9e2f8bb4aa9bd123e206e4657e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs new file mode 100644 index 0000000000..35adcbde00 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTypeReplacementRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationToolContractDetectionRules + { + internal static bool ContainsLegacyAssemblyScopedTypeReference(string source) + { + Debug.Assert(source != null, "source must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) + { + if (ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + foreach (TypeReplacementRule rule in DomainTypeReplacementRules) + { + if (ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + return ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, "ToolInfo"); + } + + internal static bool ContainsLegacyAliasQualifiedAssemblyScopedApi( + string source, + string[] legacyAssemblyAliases) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + + foreach (string alias in legacyAssemblyAliases) + { + foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) + { + return true; + } + } + + foreach (TypeReplacementRule rule in DomainTypeReplacementRules) + { + if (ContainsLegacyAliasQualifiedName(source, alias, rule.LegacyName)) + { + return true; + } + } + + if (ContainsLegacyAliasQualifiedName(source, alias, "ToolInfo") || + ContainsLegacyAliasQualifiedName(source, alias, "CustomToolManager")) + { + return true; + } + } + + return false; + } + + internal static bool ContainsLegacyEditorDelayFrameCall( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorDelay) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + MatchCollection matches = LegacyEditorDelayFrameRegex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyEditorDelayFrameCallMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyEditorDelay)) + { + return true; + } + } + + return false; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs.meta new file mode 100644 index 0000000000..b139a5d295 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fcf585b2bd0314fee9c4647c2ba68636 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs new file mode 100644 index 0000000000..2822d09711 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using CodeTextMask = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.CodeTextMask; +using ReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.ReplacementRule; +using TypeReplacementRule = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules.TypeReplacementRule; +using LegacyPlayerLoopTimingParameterDeclaration = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.LegacyPlayerLoopTimingParameterDeclaration; +using RemovedLegacyPlayerLoopTimingParameter = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingParameter; +using RemovedLegacyPlayerLoopTimingSignature = io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRules.RemovedLegacyPlayerLoopTimingSignature; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAliasRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApiDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationApplicationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAsmdefRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationAttributeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCSharpRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskBuilder; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationCodeTextMaskInterpolationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationConstructorArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationDomainDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationEditorDelayRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationMetadataConstructorRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationParsingRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRegexRewriteRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationRuleCatalog; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDeconstructionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotDetectionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationScreenshotRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingArgumentRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCallerRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingCleanupRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingInvocationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodBodyRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingMethodDeclarationRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeNameRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeResolutionRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationTimingTypeScopeRules; +using static io.github.hatayama.UnityCliLoop.Infrastructure.ThirdPartyToolMigrationToolContractDetectionRules; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + internal static class ThirdPartyToolMigrationTypeReplacementRules + { + internal static string ReplaceLegacyRegistrarAliasesInCode( + string source, + string[] aliases, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + + string migratedContent = source; + foreach (string alias in aliases) + { + Regex aliasRegistrarRegex = new( + $@"(? $"{CurrentNamespace}.UnityCliLoopToolRegistrar", + ref replacementCount); + + Regex aliasToolInfoRegex = new( + $@"(? $"{CurrentNamespace}.ToolInfo", + ref replacementCount); + } + + return migratedContent; + } + + internal static string ReplaceCurrentPublicContractNamespacesInCode(string source, ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + + string migratedContent = ReplaceRegexInCode( + source, + CurrentApplicationNamespaceRegex, + _ => CurrentNamespace, + ref replacementCount); + migratedContent = ReplaceRegexInCode( + migratedContent, + CurrentDomainNamespaceRegex, + _ => CurrentNamespace, + ref replacementCount); + migratedContent = ReplaceRegexInCode( + migratedContent, + CurrentFirstPartyToolsNamespaceRegex, + _ => CurrentNamespace, + ref replacementCount); + + return migratedContent; + } + + internal static string ReplaceUnqualifiedLegacyRegistrarReferencesInCode( + string source, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + + Regex unqualifiedRegistrarRegex = + new(@"(? ShouldMigrateLegacyTypeReference(source, "CustomToolManager", match.Index) + ? $"{CurrentNamespace}.UnityCliLoopToolRegistrar" + : match.Value, + ref replacementCount); + } + + internal static string ReplaceLegacyContractTypeNamesInCode( + string source, + string[] aliases, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + + string migratedContent = source; + foreach (TypeReplacementRule rule in ToolContractTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{alias}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? rule.CurrentName + : match.Value, + ref replacementCount); + } + + return migratedContent; + } + + internal static string ReplaceLegacyDomainTypeNamesInCode( + string source, + string[] aliases, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + + string migratedContent = source; + foreach (TypeReplacementRule rule in DomainTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentNamespace}.{rule.CurrentName}" + : match.Value, + ref replacementCount); + } + + return migratedContent; + } + + internal static string ReplaceLegacyApplicationTypeNamesInCode( + string source, + string[] aliases, + bool canMigrateBareLegacyApplicationApi, + string[] assemblyDeclaredTypeNames, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string migratedContent = source; + foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(migratedContent, rule.LegacyName) || + assemblyDeclaredTypeNames.Contains(rule.LegacyName); + + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyApplicationApi && + !hasProtectedTypeDeclaration && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentNamespace}.{rule.CurrentName}" + : match.Value, + ref replacementCount); + } + + return migratedContent; + } + + internal static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( + string source, + string[] aliases, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + string[] assemblyDeclaredTypeNames, + ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(aliases != null, "aliases must not be null"); + Debug.Assert(assemblyDeclaredTypeNames != null, "assemblyDeclaredTypeNames must not be null"); + + string migratedContent = source; + foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + bool hasProtectedTypeDeclaration = DeclaresLocalType(migratedContent, rule.LegacyName) || + assemblyDeclaredTypeNames.Contains(rule.LegacyName); + + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyFirstPartyScreenshotApi && + !hasProtectedTypeDeclaration && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentNamespace}.{rule.CurrentName}" + : match.Value, + ref replacementCount); + } + + return migratedContent; + } + + internal static string ReplaceLegacyToolInfoTypeReferencesInCode(string source, ref int replacementCount) + { + Debug.Assert(source != null, "source must not be null"); + + return ReplaceRegexInCode( + source, + UnqualifiedToolInfoRegex, + match => ShouldMigrateLegacyToolInfoTypeReference(source, match.Index) + ? $"{CurrentNamespace}.ToolInfo" + : match.Value, + ref replacementCount); + } + + internal static bool ShouldMigrateLegacyToolInfoTypeReference(string source, int toolInfoIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(toolInfoIndex >= 0, "toolInfoIndex must not be negative"); + + return ShouldMigrateLegacyTypeReference(source, "ToolInfo", toolInfoIndex); + } + + internal static bool ShouldMigrateLegacyTypeReference(string source, string typeName, int typeNameIndex) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be empty"); + Debug.Assert(typeNameIndex >= 0, "typeNameIndex must not be negative"); + + if (IsLegacyAssemblyScopedTypeDeclaration(source, typeNameIndex)) + { + return false; + } + + char nextCharacter = ReadNextNonWhitespaceCharacter(source, typeNameIndex + typeName.Length); + char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, typeNameIndex); + if (nextCharacter == '(' && !PreviousCodeTokenEquals(source, typeNameIndex, "new")) + { + return false; + } + + return !IsDeclarationIdentifierTerminator(nextCharacter) || + !CanPrecedeDeclarationIdentifier(previousCharacter); + } + } +} diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs.meta b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs.meta new file mode 100644 index 0000000000..7931a59eb5 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d928592bf36944aaa3e643c3bce6b20 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Threading/EditorMainThreadDispatcher.cs b/Packages/src/Editor/Infrastructure/Threading/EditorMainThreadDispatcher.cs index 746634336d..0eaada5db7 100644 --- a/Packages/src/Editor/Infrastructure/Threading/EditorMainThreadDispatcher.cs +++ b/Packages/src/Editor/Infrastructure/Threading/EditorMainThreadDispatcher.cs @@ -3,8 +3,8 @@ using System.Threading; using UnityEditor; -using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.InternalAPIBridge; +using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Infrastructure { diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs new file mode 100644 index 0000000000..af2b98f034 --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Throttles background migration progress and applies fresh updates on Unity's main thread. + /// + internal sealed class ThirdPartyToolMigrationProgressReporter + : IProgress + { + private readonly CancellationToken _ct; + private readonly Func _hasActiveOperation; + private readonly Action _applyProgress; + private long _lastReportTimestamp; + + internal ThirdPartyToolMigrationProgressReporter( + CancellationToken ct, + Func hasActiveOperation, + Action applyProgress) + { + Debug.Assert(hasActiveOperation != null, "hasActiveOperation must not be null"); + Debug.Assert(applyProgress != null, "applyProgress must not be null"); + + _ct = ct; + _hasActiveOperation = hasActiveOperation + ?? throw new ArgumentNullException(nameof(hasActiveOperation)); + _applyProgress = applyProgress + ?? throw new ArgumentNullException(nameof(applyProgress)); + } + + public void Report(ThirdPartyToolMigrationProgress value) + { + if (_ct.IsCancellationRequested) + { + return; + } + + long currentTimestamp = Stopwatch.GetTimestamp(); + if (!ThirdPartyToolMigrationWizardStateRules.ShouldReportMigrationProgress( + _lastReportTimestamp, + currentTimestamp, + value, + Stopwatch.Frequency, + ThirdPartyToolMigrationWizardStateRules.MigrationProgressUiUpdateIntervalMilliseconds)) + { + return; + } + + _lastReportTimestamp = currentTimestamp; + _ = ReportAsync(value, _ct); + } + + private async Task ReportAsync(ThirdPartyToolMigrationProgress value, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(); + if (!ThirdPartyToolMigrationWizardStateRules.ShouldApplyMigrationProgress( + ct.IsCancellationRequested, + _hasActiveOperation(ct))) + { + return; + } + + _applyProgress(value); + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs.meta b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs.meta new file mode 100644 index 0000000000..b9651a493f --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cdc47e5f1a36b4715ba3e6c9addaa2fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs new file mode 100644 index 0000000000..e9cceb98ef --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs @@ -0,0 +1,77 @@ +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Holds pure state-transition rules for the third-party tool migration wizard. + /// + internal static class ThirdPartyToolMigrationWizardStateRules + { + internal const int MigrationProgressUiUpdateIntervalMilliseconds = 100; + + internal static bool ShouldStartInitialRefresh( + bool shouldRefreshAfterCreateGui, + bool shouldAutoScanThirdPartyToolMigration) + { + return shouldRefreshAfterCreateGui && shouldAutoScanThirdPartyToolMigration; + } + + internal static bool ShouldReportMigrationProgress( + long lastReportTimestamp, + long currentTimestamp, + ThirdPartyToolMigrationProgress progress, + long stopwatchFrequency, + int updateIntervalMilliseconds) + { + Debug.Assert(currentTimestamp >= 0, "currentTimestamp must not be negative"); + Debug.Assert(stopwatchFrequency > 0, "stopwatchFrequency must be positive"); + Debug.Assert(updateIntervalMilliseconds >= 0, "updateIntervalMilliseconds must not be negative"); + + if (lastReportTimestamp == 0) + { + return true; + } + + if (progress.TotalItemCount > 0 && progress.ProcessedItemCount >= progress.TotalItemCount) + { + return true; + } + + long elapsedTicks = currentTimestamp - lastReportTimestamp; + long requiredTicks = stopwatchFrequency * updateIntervalMilliseconds / 1000; + return elapsedTicks >= requiredTicks; + } + + internal static bool ShouldApplyMigrationProgress( + bool isCancellationRequested, + bool hasActiveOperation) + { + return !isCancellationRequested && hasActiveOperation; + } + + internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult result) + { + Debug.Assert(result.FileCount >= 0, "result file count must not be negative"); + + return false; + } + + internal static bool ShouldFinishMigrationOnMainThread( + bool isCancellationRequested, + ThirdPartyToolMigrationResult result) + { + Debug.Assert(result.FileCount >= 0, "result file count must not be negative"); + + return !isCancellationRequested || result.Changed; + } + + internal static bool ShouldRefreshAfterInterruptedMigration( + bool isMigrationCompletionPending, + bool isCancellationRequested) + { + return isMigrationCompletionPending && !isCancellationRequested; + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs.meta b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs.meta new file mode 100644 index 0000000000..f5e6fc9dde --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7abc83e21804465a90483df3740bce1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs new file mode 100644 index 0000000000..808980708a --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs @@ -0,0 +1,68 @@ +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Creates user-facing copy for the third-party tool migration wizard. + /// + internal static class ThirdPartyToolMigrationWizardText + { + internal const string MigrationNotCheckedText = "Migration status has not been checked."; + internal const string NoMigrationTargetsText = "No V3 custom tool migration is needed."; + private const string MigrationCheckingText = "Scanning project for V3 custom tool migration..."; + private const string MigrationApplyingText = "Migrating project files for V3 custom tools..."; + private const string MigrationButtonReadyText = "Migrate"; + private const string MigrationButtonMigratingText = "Migrating..."; + private const string MigrationButtonNoTargetsText = "Nothing to migrate"; + private const string MigrationButtonCheckRequiredText = "Check required"; + + internal static string GetMigrationStatusText(int fileCount) + { + Debug.Assert(fileCount >= 0, "fileCount must not be negative"); + + string noun = fileCount == 1 ? "file" : "files"; + string verb = fileCount == 1 ? "needs" : "need"; + string subject = fileCount == 1 ? "this file still uses" : "these files still use"; + string objectPronoun = fileCount == 1 ? "it" : "them"; + + return $"{fileCount} {noun} {verb} V3 custom tool migration.\n" + + $"The Unity Console is showing errors because {subject} the old custom tool API.\n\n" + + $"Click Migrate to update {objectPronoun} automatically. " + + "The errors should disappear after migration."; + } + + internal static string GetMigrationProgressText( + ThirdPartyToolMigrationProgress progress, + bool isMigrating) + { + string statusText = isMigrating ? MigrationApplyingText : MigrationCheckingText; + if (progress.TotalItemCount <= 0) + { + return statusText; + } + + return $"{statusText}\n" + + $"{progress.ProcessedItemCount}/{progress.TotalItemCount} steps complete."; + } + + internal static string GetMigrationButtonText( + bool isMigrating, + bool hasMigrationTargets, + bool hasCheckedMigrationStatus) + { + if (!hasCheckedMigrationStatus) + { + return MigrationButtonCheckRequiredText; + } + + if (isMigrating) + { + return MigrationButtonMigratingText; + } + + return hasMigrationTargets ? MigrationButtonReadyText : MigrationButtonNoTargetsText; + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs.meta b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs.meta new file mode 100644 index 0000000000..2bbe7950aa --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b5d24a90d8414c2e955a2f2c437f2b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs new file mode 100644 index 0000000000..8934381f76 --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs @@ -0,0 +1,239 @@ +using System; + +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Owns UI Toolkit elements and visual states for the third-party tool migration wizard. + /// + internal sealed class ThirdPartyToolMigrationWizardView + { + private const string USS_RELATIVE_PATH = "Editor/Presentation/Setup/SetupWizardWindow.uss"; + + private readonly Label _migrationStatusLabel; + private readonly ProgressBar _migrationProgressBar; + private readonly Button _migrateButton; + private readonly Button _refreshButton; + private readonly Button _closeButton; + + private ThirdPartyToolMigrationWizardView( + ScrollView mainScrollView, + Label migrationStatusLabel, + ProgressBar migrationProgressBar, + Button migrateButton, + Button refreshButton, + Button closeButton) + { + Debug.Assert(mainScrollView != null, "mainScrollView must not be null"); + Debug.Assert(migrationStatusLabel != null, "migrationStatusLabel must not be null"); + Debug.Assert(migrationProgressBar != null, "migrationProgressBar must not be null"); + Debug.Assert(migrateButton != null, "migrateButton must not be null"); + Debug.Assert(refreshButton != null, "refreshButton must not be null"); + Debug.Assert(closeButton != null, "closeButton must not be null"); + + MainScrollView = mainScrollView; + _migrationStatusLabel = migrationStatusLabel; + _migrationProgressBar = migrationProgressBar; + _migrateButton = migrateButton; + _refreshButton = refreshButton; + _closeButton = closeButton; + } + + internal ScrollView MainScrollView { get; } + + internal static ThirdPartyToolMigrationWizardView Create( + VisualElement root, + Action refresh, + Action migrate, + Action close) + { + Debug.Assert(root != null, "root must not be null"); + Debug.Assert(refresh != null, "refresh must not be null"); + Debug.Assert(migrate != null, "migrate must not be null"); + Debug.Assert(close != null, "close must not be null"); + + string ussPath = $"{UnityCliLoopConstants.PackageAssetPath}/{USS_RELATIVE_PATH}"; + StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath(ussPath); + Debug.Assert(styleSheet != null, $"USS not found at {ussPath}"); + root.styleSheets.Add(styleSheet); + + ScrollView mainScrollView = CreateMainScrollView(root); + VisualElement content = CreateMigrationSection(mainScrollView); + Label migrationStatusLabel = CreateStatusLabel(content); + ProgressBar migrationProgressBar = CreateProgressBar(content); + Button migrateButton = CreateMigrateButton(content); + (Button refreshButton, Button closeButton) = CreateFooter(mainScrollView); + + ThirdPartyToolMigrationWizardView view = new ThirdPartyToolMigrationWizardView( + mainScrollView, + migrationStatusLabel, + migrationProgressBar, + migrateButton, + refreshButton, + closeButton); + view.BindEvents(refresh, migrate, close); + return view; + } + + internal void ShowNotCheckedState(bool isMigrating) + { + _migrationStatusLabel.text = ThirdPartyToolMigrationWizardText.MigrationNotCheckedText; + ViewDataBinder.SetVisible(_migrationProgressBar, false); + _migrateButton.SetEnabled(false); + _migrateButton.text = ThirdPartyToolMigrationWizardText.GetMigrationButtonText( + isMigrating, + false, + false); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); + } + + internal void ShowMigrationTargetsState(int fileCount, bool isMigrating) + { + _migrationStatusLabel.text = ThirdPartyToolMigrationWizardText.GetMigrationStatusText(fileCount); + ViewDataBinder.SetVisible(_migrationProgressBar, false); + _migrateButton.SetEnabled(!isMigrating); + _migrateButton.text = ThirdPartyToolMigrationWizardText.GetMigrationButtonText( + isMigrating, + true, + true); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); + } + + internal void ShowNoMigrationTargetsState(bool isMigrating) + { + _migrationStatusLabel.text = ThirdPartyToolMigrationWizardText.NoMigrationTargetsText; + ViewDataBinder.SetVisible(_migrationProgressBar, false); + _migrateButton.SetEnabled(false); + _migrateButton.text = ThirdPartyToolMigrationWizardText.GetMigrationButtonText( + isMigrating, + false, + true); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); + } + + internal void ShowCheckingState(ThirdPartyToolMigrationProgress progress, bool isMigrating) + { + _migrationStatusLabel.text = ThirdPartyToolMigrationWizardText.GetMigrationProgressText( + progress, + isMigrating); + ViewDataBinder.SetVisible(_migrationProgressBar, true); + UpdateMigrationProgressBar(progress); + _migrateButton.SetEnabled(false); + _migrateButton.text = ThirdPartyToolMigrationWizardText.GetMigrationButtonText( + isMigrating, + true, + true); + _refreshButton.SetEnabled(false); + _closeButton.SetEnabled(!isMigrating); + } + + private static ScrollView CreateMainScrollView(VisualElement root) + { + ScrollView mainScrollView = new ScrollView(); + mainScrollView.AddToClassList("setup-main-container"); + root.Add(mainScrollView); + return mainScrollView; + } + + private static VisualElement CreateMigrationSection(ScrollView mainScrollView) + { + VisualElement migrationSection = new VisualElement(); + migrationSection.AddToClassList("setup-step"); + migrationSection.AddToClassList("setup-step--migration-alert"); + mainScrollView.Add(migrationSection); + + Label titleLabel = new Label("Custom Tool Migration"); + titleLabel.AddToClassList("setup-step__title"); + migrationSection.Add(titleLabel); + + VisualElement content = new VisualElement(); + content.AddToClassList("setup-step__content"); + migrationSection.Add(content); + return content; + } + + private static Label CreateStatusLabel(VisualElement content) + { + Label migrationStatusLabel = new Label(); + migrationStatusLabel.AddToClassList("setup-step__status-label"); + migrationStatusLabel.AddToClassList("setup-step__status-label--standalone"); + content.Add(migrationStatusLabel); + return migrationStatusLabel; + } + + private static ProgressBar CreateProgressBar(VisualElement content) + { + ProgressBar migrationProgressBar = new ProgressBar(); + migrationProgressBar.AddToClassList("setup-progress-bar"); + content.Add(migrationProgressBar); + return migrationProgressBar; + } + + private static Button CreateMigrateButton(VisualElement content) + { + VisualElement migrationButtonRow = new VisualElement(); + migrationButtonRow.AddToClassList("setup-step__button-row"); + content.Add(migrationButtonRow); + + Button migrateButton = new Button(); + migrateButton.text = ThirdPartyToolMigrationWizardText.GetMigrationButtonText(false, false, false); + migrateButton.AddToClassList("setup-button"); + migrationButtonRow.Add(migrateButton); + return migrateButton; + } + + private static (Button refreshButton, Button closeButton) CreateFooter(ScrollView mainScrollView) + { + VisualElement footer = new VisualElement(); + footer.AddToClassList("setup-footer"); + mainScrollView.Add(footer); + + VisualElement footerButtonRow = new VisualElement(); + footerButtonRow.AddToClassList("setup-footer__button-row"); + footer.Add(footerButtonRow); + + Button refreshButton = new Button(); + refreshButton.text = "Check"; + refreshButton.AddToClassList("setup-button"); + refreshButton.AddToClassList("setup-button--primary"); + footerButtonRow.Add(refreshButton); + + Button closeButton = new Button(); + closeButton.text = "Close"; + closeButton.AddToClassList("setup-button"); + footerButtonRow.Add(closeButton); + + Label reopenHintLabel = new Label( + "You can close this wizard and reopen it later from\n" + + "Window > Unity CLI Loop > Custom Tool Migration."); + reopenHintLabel.AddToClassList("setup-footer__hint-label"); + footer.Add(reopenHintLabel); + return (refreshButton, closeButton); + } + + private void BindEvents(Action refresh, Action migrate, Action close) + { + _refreshButton.clicked += refresh; + _migrateButton.clicked += migrate; + _closeButton.clicked += close; + } + + private void UpdateMigrationProgressBar(ThirdPartyToolMigrationProgress progress) + { + int totalItemCount = Mathf.Max(progress.TotalItemCount, 1); + int processedItemCount = Mathf.Clamp(progress.ProcessedItemCount, 0, totalItemCount); + _migrationProgressBar.lowValue = 0; + _migrationProgressBar.highValue = totalItemCount; + _migrationProgressBar.value = processedItemCount; + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs.meta b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs.meta new file mode 100644 index 0000000000..915754f8e6 --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce99b0fcbbbdc4ba49cc277d42f29fc2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index 3c84225cda..358d4cc990 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -3,7 +3,6 @@ using UnityEditor; using UnityEngine; -using UnityEngine.UIElements; using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; @@ -17,34 +16,20 @@ namespace io.github.hatayama.UnityCliLoop.Presentation public class ThirdPartyToolMigrationWizardWindow : EditorWindow { private const string WindowTitle = "Unity CLI Loop Migration"; - private const string USS_RELATIVE_PATH = "Editor/Presentation/Setup/SetupWizardWindow.uss"; - private const string MigrationNotCheckedText = "Migration status has not been checked."; - private const string MigrationCheckingText = "Scanning project for V3 custom tool migration..."; - private const string NoMigrationTargetsText = "No V3 custom tool migration is needed."; - private const string MigrationButtonReadyText = "Migrate"; - private const string MigrationButtonMigratingText = "Migrating..."; - private const string MigrationButtonNoTargetsText = "Nothing to migrate"; - private const string MigrationButtonCheckRequiredText = "Check required"; - private const int MigrationProgressUiUpdateIntervalMilliseconds = 100; - private static readonly Vector2 InitialWindowSize = new(360f, 220f); - private static readonly Vector2 MinimumWindowSize = new(360f, 120f); + private static readonly Vector2 InitialWindowSize = + ThirdPartyToolMigrationWizardWindowResizer.InitialWindowSize; + private static readonly Vector2 MinimumWindowSize = + ThirdPartyToolMigrationWizardWindowResizer.MinimumWindowSize; private static UnityCliLoopEditorSessionStateService RegisteredSessionStateService; - private ScrollView _mainScrollView; - private VisualElement _migrationSection; - private Label _migrationStatusLabel; - private ProgressBar _migrationProgressBar; - private Button _migrateButton; - private Button _refreshButton; - private Button _closeButton; - - private bool _isMigrating; - private bool _isApplyingContentSize; [SerializeField] private bool _shouldRefreshAfterCreateGui; - private IVisualElementScheduledItem _resizeScheduledItem; - private CancellationTokenSource _migrationPreviewCts; + + private bool _isMigrating; + private CancellationTokenSource _migrationOperationCts; private ThirdPartyToolMigrationUseCase _thirdPartyToolMigrationUseCase; + private ThirdPartyToolMigrationWizardView _view; + private ThirdPartyToolMigrationWizardWindowResizer _resizer; internal static void InitializeEditorServices(UnityCliLoopEditorSessionStateService sessionStateService) { @@ -69,7 +54,9 @@ internal static bool ShouldStartInitialRefresh( bool shouldRefreshAfterCreateGui, bool shouldAutoScanThirdPartyToolMigration) { - return shouldRefreshAfterCreateGui && shouldAutoScanThirdPartyToolMigration; + return ThirdPartyToolMigrationWizardStateRules.ShouldStartInitialRefresh( + shouldRefreshAfterCreateGui, + shouldAutoScanThirdPartyToolMigration); } internal static void PrepareForOpen( @@ -89,45 +76,27 @@ internal static void PrepareForOpen( internal static Rect CreateCenteredRect(Rect bounds, Vector2 size) { - Vector2 centeredPosition = bounds.center - (size * 0.5f); - return new Rect(centeredPosition, size); + return ThirdPartyToolMigrationWizardWindowResizer.CreateCenteredRect(bounds, size); } internal static Rect WithContentHeight(Rect currentRect, float contentHeight, Vector2 frameSize) { - Debug.Assert(contentHeight >= 0f, "contentHeight must not be negative"); - - float measuredHeight = contentHeight + frameSize.y; - Vector2 targetSize = new( - MinimumWindowSize.x, - Mathf.Max(measuredHeight, MinimumWindowSize.y)); - return CreateCenteredRect(currentRect, targetSize); + return ThirdPartyToolMigrationWizardWindowResizer.WithContentHeight( + currentRect, + contentHeight, + frameSize); } internal static string GetMigrationStatusText(int fileCount) { - Debug.Assert(fileCount >= 0, "fileCount must not be negative"); - - string noun = fileCount == 1 ? "file" : "files"; - string verb = fileCount == 1 ? "needs" : "need"; - string subject = fileCount == 1 ? "this file still uses" : "these files still use"; - string objectPronoun = fileCount == 1 ? "it" : "them"; - - return $"{fileCount} {noun} {verb} V3 custom tool migration.\n" + - $"The Unity Console is showing errors because {subject} the old custom tool API.\n\n" + - $"Click Migrate to update {objectPronoun} automatically. " + - "The errors should disappear after migration."; + return ThirdPartyToolMigrationWizardText.GetMigrationStatusText(fileCount); } - internal static string GetMigrationProgressText(ThirdPartyToolMigrationProgress progress) + internal static string GetMigrationProgressText( + ThirdPartyToolMigrationProgress progress, + bool isMigrating) { - if (progress.TotalItemCount <= 0) - { - return MigrationCheckingText; - } - - return $"{MigrationCheckingText}\n" + - $"{progress.ProcessedItemCount}/{progress.TotalItemCount} checks complete."; + return ThirdPartyToolMigrationWizardText.GetMigrationProgressText(progress, isMigrating); } internal static string GetMigrationButtonText( @@ -135,17 +104,62 @@ internal static string GetMigrationButtonText( bool hasMigrationTargets, bool hasCheckedMigrationStatus) { - if (!hasCheckedMigrationStatus) - { - return MigrationButtonCheckRequiredText; - } + return ThirdPartyToolMigrationWizardText.GetMigrationButtonText( + isMigrating, + hasMigrationTargets, + hasCheckedMigrationStatus); + } - if (isMigrating) - { - return MigrationButtonMigratingText; - } + internal static bool HasFiniteSize(Vector2 size) + { + return ThirdPartyToolMigrationWizardWindowResizer.HasFiniteSize(size); + } - return hasMigrationTargets ? MigrationButtonReadyText : MigrationButtonNoTargetsText; + internal static bool ShouldReportMigrationProgress( + long lastReportTimestamp, + long currentTimestamp, + ThirdPartyToolMigrationProgress progress, + long stopwatchFrequency, + int updateIntervalMilliseconds) + { + return ThirdPartyToolMigrationWizardStateRules.ShouldReportMigrationProgress( + lastReportTimestamp, + currentTimestamp, + progress, + stopwatchFrequency, + updateIntervalMilliseconds); + } + + internal static bool ShouldApplyMigrationProgress( + bool isCancellationRequested, + bool hasActiveOperation) + { + return ThirdPartyToolMigrationWizardStateRules.ShouldApplyMigrationProgress( + isCancellationRequested, + hasActiveOperation); + } + + internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult result) + { + return ThirdPartyToolMigrationWizardStateRules.ShouldRefreshAfterMigration(result); + } + + internal static bool ShouldFinishMigrationOnMainThread( + bool isCancellationRequested, + ThirdPartyToolMigrationResult result) + { + return ThirdPartyToolMigrationWizardStateRules.ShouldFinishMigrationOnMainThread( + isCancellationRequested, + result); + } + + internal static bool ShouldRefreshAfterInterruptedMigration( + bool isMigrationCompletionPending, + bool isCancellationRequested) + { + return ThirdPartyToolMigrationWizardStateRules.ShouldRefreshAfterInterruptedMigration( + isMigrationCompletionPending, + isCancellationRequested); } private static void ShowWindowInternal(bool shouldRefreshAfterCreateGui) @@ -166,12 +180,48 @@ private static void ShowWindowInternal(bool shouldRefreshAfterCreateGui) window.ScheduleResizeToContent(); } + private static UnityCliLoopEditorSessionStateService GetSessionStateService() + { + if (RegisteredSessionStateService == null) + { + throw new System.InvalidOperationException( + "Migration Wizard session-state service is not initialized."); + } + + return RegisteredSessionStateService; + } + + private static void FocusExistingWindow(bool shouldRefreshAfterCreateGui) + { + ThirdPartyToolMigrationWizardWindow[] windows = + Resources.FindObjectsOfTypeAll(); + if (windows.Length == 0) + { + return; + } + + ThirdPartyToolMigrationWizardWindow window = windows[0]; + window.Focus(); + if (!shouldRefreshAfterCreateGui) + { + return; + } + + window._shouldRefreshAfterCreateGui = true; + window.TryStartInitialRefresh(); + } + private void CreateGUI() { InitializeApplicationServices(); - BuildLayout(); - BindEvents(); - BindSizeUpdates(); + _view = ThirdPartyToolMigrationWizardView.Create( + rootVisualElement, + RefreshUI, + HandleMigrateThirdPartyTools, + Close); + _resizer = new ThirdPartyToolMigrationWizardWindowResizer(this, _view.MainScrollView); + _resizer.BindSizeUpdates(); + bool shouldStartInitialRefresh = ConsumeShouldStartInitialRefresh(); ShowInitialState(shouldStartInitialRefresh); ScheduleInitialRefresh(shouldStartInitialRefresh); @@ -185,96 +235,8 @@ private void InitializeApplicationServices() private void OnDisable() { - _resizeScheduledItem?.Pause(); - CancelMigrationPreview(); - } - - private void BuildLayout() - { - string ussPath = $"{UnityCliLoopConstants.PackageAssetPath}/{USS_RELATIVE_PATH}"; - StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath(ussPath); - Debug.Assert(styleSheet != null, $"USS not found at {ussPath}"); - rootVisualElement.styleSheets.Add(styleSheet); - - _mainScrollView = new ScrollView(); - _mainScrollView.AddToClassList("setup-main-container"); - rootVisualElement.Add(_mainScrollView); - - _migrationSection = new VisualElement(); - _migrationSection.AddToClassList("setup-step"); - _migrationSection.AddToClassList("setup-step--migration-alert"); - _mainScrollView.Add(_migrationSection); - - Label titleLabel = new("Custom Tool Migration"); - titleLabel.AddToClassList("setup-step__title"); - _migrationSection.Add(titleLabel); - - VisualElement content = new(); - content.AddToClassList("setup-step__content"); - _migrationSection.Add(content); - - _migrationStatusLabel = new Label(); - _migrationStatusLabel.AddToClassList("setup-step__status-label"); - _migrationStatusLabel.AddToClassList("setup-step__status-label--standalone"); - content.Add(_migrationStatusLabel); - - _migrationProgressBar = new ProgressBar(); - _migrationProgressBar.AddToClassList("setup-progress-bar"); - content.Add(_migrationProgressBar); - - VisualElement migrationButtonRow = new(); - migrationButtonRow.AddToClassList("setup-step__button-row"); - content.Add(migrationButtonRow); - - _migrateButton = new Button(); - _migrateButton.text = GetMigrationButtonText(false, false, false); - _migrateButton.AddToClassList("setup-button"); - migrationButtonRow.Add(_migrateButton); - - VisualElement footer = new(); - footer.AddToClassList("setup-footer"); - _mainScrollView.Add(footer); - - VisualElement footerButtonRow = new(); - footerButtonRow.AddToClassList("setup-footer__button-row"); - footer.Add(footerButtonRow); - - _refreshButton = new Button(); - _refreshButton.text = "Check"; - _refreshButton.AddToClassList("setup-button"); - _refreshButton.AddToClassList("setup-button--primary"); - footerButtonRow.Add(_refreshButton); - - _closeButton = new Button(); - _closeButton.text = "Close"; - _closeButton.AddToClassList("setup-button"); - footerButtonRow.Add(_closeButton); - - Label reopenHintLabel = new( - "You can close this wizard and reopen it later from\n" + - "Window > Unity CLI Loop > Custom Tool Migration."); - reopenHintLabel.AddToClassList("setup-footer__hint-label"); - footer.Add(reopenHintLabel); - } - - private void BindEvents() - { - _refreshButton.clicked += RefreshUI; - _migrateButton.clicked += HandleMigrateThirdPartyTools; - _closeButton.clicked += Close; - } - - private void BindSizeUpdates() - { - rootVisualElement.RegisterCallback(_ => - { - if (_isApplyingContentSize) - { - return; - } - - ScheduleResizeToContent(); - }); + _resizer?.Pause(); + CancelMigrationOperation(); } private void ShowInitialState(bool shouldStartInitialRefresh) @@ -300,13 +262,12 @@ private void ScheduleInitialRefresh(bool shouldStartInitialRefresh) private async void RefreshUI() { - CancellationToken ct = BeginMigrationPreview(); + CancellationToken ct = BeginMigrationOperation(); ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); await Task.Yield(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); - System.IProgress progress = - new ThirdPartyToolMigrationPreviewProgress(this, ct); + System.IProgress progress = CreateProgressReporter(ct); ThirdPartyToolMigrationPreview preview = await Task.Run(async () => await _thirdPartyToolMigrationUseCase.PreviewMigrationAsync(projectRoot, progress, ct)); @@ -321,7 +282,7 @@ await Task.Run(async () => return; } - CompleteMigrationPreview(ct); + CompleteMigrationOperation(ct); if (!preview.HasTargets) { @@ -332,199 +293,128 @@ await Task.Run(async () => ShowMigrationTargetsState(preview.FileCount); } - private void HandleMigrateThirdPartyTools() + private async void HandleMigrateThirdPartyTools() { + CancellationToken ct = BeginMigrationOperation(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + ThirdPartyToolMigrationResult result = default; + bool isMigrationCompletionPending = true; _isMigrating = true; ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); + await Task.Yield(); try { - ThirdPartyToolMigrationResult result = - _thirdPartyToolMigrationUseCase.ApplyMigration(projectRoot); + System.IProgress progress = CreateProgressReporter(ct); + result = await Task.Run(async () => + await _thirdPartyToolMigrationUseCase.ApplyMigrationAsync(projectRoot, progress, ct)); + if (!ShouldFinishMigrationOnMainThread(ct.IsCancellationRequested, result)) + { + return; + } + + await MainThreadSwitcher.SwitchToMainThread(); + if (!ShouldFinishMigrationOnMainThread(ct.IsCancellationRequested, result)) + { + return; + } + + CompleteMigrationOperation(ct); if (result.Changed) { + if (!ct.IsCancellationRequested) + { + ShowCheckingState(new ThirdPartyToolMigrationProgress(1, 1)); + await Task.Yield(); + } + AssetDatabase.Refresh(); } + + isMigrationCompletionPending = false; } finally { _isMigrating = false; - RefreshUI(); + bool shouldRefreshAfterInterruptedMigration = ShouldRefreshAfterInterruptedMigration( + isMigrationCompletionPending, + ct.IsCancellationRequested); + CompleteMigrationOperation(ct); + if (shouldRefreshAfterInterruptedMigration) + { + RefreshUI(); + } } - } - - private void ShowNotCheckedState() - { - _migrationStatusLabel.text = MigrationNotCheckedText; - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, false, false); - ScheduleResizeToContent(); - } - - private void ShowMigrationTargetsState(int fileCount) - { - _migrationStatusLabel.text = GetMigrationStatusText(fileCount); - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(!_isMigrating); - _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); - ScheduleResizeToContent(); - } - - private void ShowNoMigrationTargetsState() - { - _migrationStatusLabel.text = NoMigrationTargetsText; - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, false, true); - ScheduleResizeToContent(); - } - - private void ShowCheckingState(ThirdPartyToolMigrationProgress progress) - { - _migrationStatusLabel.text = GetMigrationProgressText(progress); - ViewDataBinder.SetVisible(_migrationProgressBar, true); - UpdateMigrationProgressBar(progress); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); - ScheduleResizeToContent(); - } - - private void ScheduleResizeToContent() - { - _resizeScheduledItem?.Pause(); - _resizeScheduledItem = rootVisualElement.schedule.Execute(ResizeToContent).StartingIn(0); - } - private void ResizeToContent() - { - if (_mainScrollView == null) return; - if (rootVisualElement.layout.width <= 0f || rootVisualElement.layout.height <= 0f) return; - - float contentHeight = MeasurePreferredContentHeight(_mainScrollView, _mainScrollView.contentContainer); - if (!IsFinite(contentHeight)) return; - if (contentHeight <= 0f) return; - - Vector2 frameSize = position.size - rootVisualElement.layout.size; - if (!HasFiniteSize(frameSize)) return; - Rect targetRect = WithContentHeight(position, contentHeight, frameSize); - if (!HasFiniteSize(targetRect.size)) return; - if (Approximately(position.size, targetRect.size)) + if (ct.IsCancellationRequested) { - minSize = targetRect.size; - maxSize = targetRect.size; return; } - _isApplyingContentSize = true; - minSize = targetRect.size; - maxSize = targetRect.size; - position = targetRect; - _isApplyingContentSize = false; - } - - private static float MeasurePreferredContentHeight(VisualElement mainContainer, VisualElement contentContainer) - { - float maxBottom = 0f; - foreach (VisualElement child in contentContainer.Children()) + if (ShouldRefreshAfterMigration(result)) { - if (!child.visible) continue; - if (!HasFiniteRect(child.worldBound)) continue; - float bottom = child.worldBound.yMax - contentContainer.worldBound.yMin; - if (!IsFinite(bottom)) continue; - maxBottom = Mathf.Max(maxBottom, bottom); + RefreshUI(); + return; } - float height = - mainContainer.resolvedStyle.paddingTop - + maxBottom - + mainContainer.resolvedStyle.paddingBottom; - return IsFinite(height) ? Mathf.Ceil(height) : 0f; + ShowNoMigrationTargetsState(); } - private static bool Approximately(Vector2 left, Vector2 right) + private System.IProgress CreateProgressReporter(CancellationToken ct) { - const float Tolerance = 0.5f; - return Mathf.Abs(left.x - right.x) < Tolerance && Mathf.Abs(left.y - right.y) < Tolerance; + return new ThirdPartyToolMigrationProgressReporter( + ct, + IsMigrationOperationActive, + ShowCheckingState); } - internal static bool HasFiniteSize(Vector2 size) + private void ShowNotCheckedState() { - return IsFinite(size.x) && IsFinite(size.y); + _view.ShowNotCheckedState(_isMigrating); + ScheduleResizeToContent(); } - private static bool HasFiniteRect(Rect rect) + private void ShowMigrationTargetsState(int fileCount) { - return IsFinite(rect.xMin) - && IsFinite(rect.xMax) - && IsFinite(rect.yMin) - && IsFinite(rect.yMax); + _view.ShowMigrationTargetsState(fileCount, _isMigrating); + ScheduleResizeToContent(); } - private static bool IsFinite(float value) + private void ShowNoMigrationTargetsState() { - return !float.IsNaN(value) && !float.IsInfinity(value); + _view.ShowNoMigrationTargetsState(_isMigrating); + ScheduleResizeToContent(); } - private void UpdateMigrationProgressBar(ThirdPartyToolMigrationProgress progress) + private void ShowCheckingState(ThirdPartyToolMigrationProgress progress) { - int totalItemCount = Mathf.Max(progress.TotalItemCount, 1); - int processedItemCount = Mathf.Clamp(progress.ProcessedItemCount, 0, totalItemCount); - _migrationProgressBar.lowValue = 0; - _migrationProgressBar.highValue = totalItemCount; - _migrationProgressBar.value = processedItemCount; + _view.ShowCheckingState(progress, _isMigrating); + ScheduleResizeToContent(); } - private CancellationToken BeginMigrationPreview() + private void ScheduleResizeToContent() { - CancelMigrationPreview(); - CancellationTokenSource cts = new(); - _migrationPreviewCts = cts; - return cts.Token; + _resizer?.ScheduleResizeToContent(); } - private void CancelMigrationPreview() + private CancellationToken BeginMigrationOperation() { - if (_migrationPreviewCts == null) - { - return; - } - - _migrationPreviewCts.Cancel(); - _migrationPreviewCts.Dispose(); - _migrationPreviewCts = null; - } - - private static UnityCliLoopEditorSessionStateService GetSessionStateService() - { - if (RegisteredSessionStateService == null) - { - throw new System.InvalidOperationException( - "Migration Wizard session-state service is not initialized."); - } - - return RegisteredSessionStateService; + CancelMigrationOperation(); + CancellationTokenSource cts = new CancellationTokenSource(); + _migrationOperationCts = cts; + return cts.Token; } - private static void FocusExistingWindow(bool shouldRefreshAfterCreateGui) + private void CancelMigrationOperation() { - ThirdPartyToolMigrationWizardWindow[] windows = - Resources.FindObjectsOfTypeAll(); - if (windows.Length == 0) + if (_migrationOperationCts == null) { return; } - ThirdPartyToolMigrationWizardWindow window = windows[0]; - window.Focus(); - if (!shouldRefreshAfterCreateGui) - { - return; - } - - window._shouldRefreshAfterCreateGui = true; - window.TryStartInitialRefresh(); + _migrationOperationCts.Cancel(); + _migrationOperationCts.Dispose(); + _migrationOperationCts = null; } private bool ConsumeShouldStartInitialRefresh() @@ -552,106 +442,20 @@ private void TryStartInitialRefresh() rootVisualElement.schedule.Execute(RefreshUI).StartingIn(0); } - internal static bool ShouldReportMigrationProgress( - long lastReportTimestamp, - long currentTimestamp, - ThirdPartyToolMigrationProgress progress, - long stopwatchFrequency, - int updateIntervalMilliseconds) + private void CompleteMigrationOperation(CancellationToken ct) { - Debug.Assert(currentTimestamp >= 0, "currentTimestamp must not be negative"); - Debug.Assert(stopwatchFrequency > 0, "stopwatchFrequency must be positive"); - Debug.Assert(updateIntervalMilliseconds >= 0, "updateIntervalMilliseconds must not be negative"); - - if (lastReportTimestamp == 0) - { - return true; - } - - if (progress.TotalItemCount > 0 && progress.ProcessedItemCount >= progress.TotalItemCount) - { - return true; - } - - long elapsedTicks = currentTimestamp - lastReportTimestamp; - long requiredTicks = stopwatchFrequency * updateIntervalMilliseconds / 1000; - return elapsedTicks >= requiredTicks; - } - - internal static bool ShouldApplyMigrationProgress( - bool isCancellationRequested, - bool hasActivePreview) - { - return !isCancellationRequested && hasActivePreview; - } - - private void CompleteMigrationPreview(CancellationToken ct) - { - if (!IsMigrationPreviewActive(ct)) + if (!IsMigrationOperationActive(ct)) { return; } - _migrationPreviewCts.Dispose(); - _migrationPreviewCts = null; - } - - private bool IsMigrationPreviewActive(CancellationToken ct) - { - return _migrationPreviewCts != null && _migrationPreviewCts.Token.Equals(ct); + _migrationOperationCts.Dispose(); + _migrationOperationCts = null; } - private sealed class ThirdPartyToolMigrationPreviewProgress - : System.IProgress + private bool IsMigrationOperationActive(CancellationToken ct) { - private readonly ThirdPartyToolMigrationWizardWindow _window; - private readonly CancellationToken _ct; - private long _lastReportTimestamp; - - public ThirdPartyToolMigrationPreviewProgress( - ThirdPartyToolMigrationWizardWindow window, - CancellationToken ct) - { - Debug.Assert(window != null, "window must not be null"); - - _window = window; - _ct = ct; - } - - public void Report(ThirdPartyToolMigrationProgress value) - { - if (_ct.IsCancellationRequested) - { - return; - } - - long currentTimestamp = System.Diagnostics.Stopwatch.GetTimestamp(); - if (!ShouldReportMigrationProgress( - _lastReportTimestamp, - currentTimestamp, - value, - System.Diagnostics.Stopwatch.Frequency, - MigrationProgressUiUpdateIntervalMilliseconds)) - { - return; - } - - _lastReportTimestamp = currentTimestamp; - _ = ReportAsync(value, _ct); - } - - private async Task ReportAsync(ThirdPartyToolMigrationProgress value, CancellationToken ct) - { - await MainThreadSwitcher.SwitchToMainThread(); - if (!ShouldApplyMigrationProgress( - ct.IsCancellationRequested, - _window.IsMigrationPreviewActive(ct))) - { - return; - } - - _window.ShowCheckingState(value); - } + return _migrationOperationCts != null && _migrationOperationCts.Token.Equals(ct); } } } diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs new file mode 100644 index 0000000000..10c155e52c --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs @@ -0,0 +1,171 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Measures UI Toolkit content and resizes the migration wizard window around it. + /// + internal sealed class ThirdPartyToolMigrationWizardWindowResizer + { + internal static readonly Vector2 InitialWindowSize = new Vector2(360f, 220f); + internal static readonly Vector2 MinimumWindowSize = new Vector2(360f, 120f); + + private readonly EditorWindow _window; + private readonly VisualElement _root; + private readonly ScrollView _mainScrollView; + private bool _isApplyingContentSize; + private IVisualElementScheduledItem _resizeScheduledItem; + + internal ThirdPartyToolMigrationWizardWindowResizer(EditorWindow window, ScrollView mainScrollView) + { + Debug.Assert(window != null, "window must not be null"); + Debug.Assert(mainScrollView != null, "mainScrollView must not be null"); + + _window = window ?? throw new System.ArgumentNullException(nameof(window)); + _root = window.rootVisualElement; + _mainScrollView = mainScrollView ?? throw new System.ArgumentNullException(nameof(mainScrollView)); + } + + internal static Rect CreateCenteredRect(Rect bounds, Vector2 size) + { + Vector2 centeredPosition = bounds.center - (size * 0.5f); + return new Rect(centeredPosition, size); + } + + internal static Rect WithContentHeight(Rect currentRect, float contentHeight, Vector2 frameSize) + { + Debug.Assert(contentHeight >= 0f, "contentHeight must not be negative"); + + float measuredHeight = contentHeight + frameSize.y; + Vector2 targetSize = new Vector2( + MinimumWindowSize.x, + Mathf.Max(measuredHeight, MinimumWindowSize.y)); + return CreateCenteredRect(currentRect, targetSize); + } + + internal static bool HasFiniteSize(Vector2 size) + { + return IsFinite(size.x) && IsFinite(size.y); + } + + internal void BindSizeUpdates() + { + _root.RegisterCallback(HandleGeometryChanged); + } + + internal void ScheduleResizeToContent() + { + _resizeScheduledItem?.Pause(); + _resizeScheduledItem = _root.schedule.Execute(ResizeToContent).StartingIn(0); + } + + internal void Pause() + { + _resizeScheduledItem?.Pause(); + } + + private static float MeasurePreferredContentHeight( + VisualElement mainContainer, + VisualElement contentContainer) + { + float maxBottom = 0f; + foreach (VisualElement child in contentContainer.Children()) + { + if (!child.visible) + { + continue; + } + + if (!HasFiniteRect(child.worldBound)) + { + continue; + } + + float bottom = child.worldBound.yMax - contentContainer.worldBound.yMin; + if (!IsFinite(bottom)) + { + continue; + } + + maxBottom = Mathf.Max(maxBottom, bottom); + } + + float height = + mainContainer.resolvedStyle.paddingTop + + maxBottom + + mainContainer.resolvedStyle.paddingBottom; + return IsFinite(height) ? Mathf.Ceil(height) : 0f; + } + + private static bool Approximately(Vector2 left, Vector2 right) + { + const float Tolerance = 0.5f; + return Mathf.Abs(left.x - right.x) < Tolerance && Mathf.Abs(left.y - right.y) < Tolerance; + } + + private static bool HasFiniteRect(Rect rect) + { + return IsFinite(rect.xMin) + && IsFinite(rect.xMax) + && IsFinite(rect.yMin) + && IsFinite(rect.yMax); + } + + private static bool IsFinite(float value) + { + return !float.IsNaN(value) && !float.IsInfinity(value); + } + + private void HandleGeometryChanged(GeometryChangedEvent evt) + { + if (_isApplyingContentSize) + { + return; + } + + ScheduleResizeToContent(); + } + + private void ResizeToContent() + { + if (_root.layout.width <= 0f || _root.layout.height <= 0f) + { + return; + } + + float contentHeight = + MeasurePreferredContentHeight(_mainScrollView, _mainScrollView.contentContainer); + if (!IsFinite(contentHeight) || contentHeight <= 0f) + { + return; + } + + Vector2 frameSize = _window.position.size - _root.layout.size; + if (!HasFiniteSize(frameSize)) + { + return; + } + + Rect targetRect = WithContentHeight(_window.position, contentHeight, frameSize); + if (!HasFiniteSize(targetRect.size)) + { + return; + } + + if (Approximately(_window.position.size, targetRect.size)) + { + _window.minSize = targetRect.size; + _window.maxSize = targetRect.size; + return; + } + + _isApplyingContentSize = true; + _window.minSize = targetRect.size; + _window.maxSize = targetRect.size; + _window.position = targetRect; + _isApplyingContentSize = false; + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs.meta b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs.meta new file mode 100644 index 0000000000..995d98a33c --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e3b2a15050f640beb5dfcda1968c441 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ToolContracts/AssemblyInfo.cs b/Packages/src/Editor/ToolContracts/AssemblyInfo.cs new file mode 100644 index 0000000000..c746ba81eb --- /dev/null +++ b/Packages/src/Editor/ToolContracts/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UnityCLILoop.CompositionRoot.Editor")] +[assembly: InternalsVisibleTo("UnityCLILoop.Application")] +[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.Screenshot.Editor")] +[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor")] +[assembly: InternalsVisibleTo("UnityCLILoop.Tests.PlayMode")] +[assembly: InternalsVisibleTo("UnityCLILoop.Dev")] diff --git a/Packages/src/Editor/ToolContracts/AssemblyInfo.cs.meta b/Packages/src/Editor/ToolContracts/AssemblyInfo.cs.meta new file mode 100644 index 0000000000..934fed2b1b --- /dev/null +++ b/Packages/src/Editor/ToolContracts/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd5af7090c08f4524a0fc00f96305aa1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs b/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs new file mode 100644 index 0000000000..6286aa4511 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs @@ -0,0 +1,87 @@ +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.ToolContracts +{ + /// + /// Public facade for editor window and Game View capture helpers used by custom tools. + /// + public interface IEditorWindowCaptureService + { + EditorWindow[] FindWindowsByName(string windowName, WindowMatchMode matchMode); + + Task<(Texture2D? texture, bool timedOut)> CaptureWindowAsync( + EditorWindow window, + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct); + + string[] GetOpenWindowNames(); + + Task<(Texture2D? texture, int yOffset, bool timedOut)> CaptureGameRenderingAsync( + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct); + } + + /// + /// Static capture entrypoint kept in the public tool contract assembly. + /// + public static class EditorWindowCaptureUtility + { + private static IEditorWindowCaptureService? ServiceValue; + + internal static void RegisterService(IEditorWindowCaptureService service) + { + System.Diagnostics.Debug.Assert(service != null, "service must not be null"); + + ServiceValue = service ?? throw new ArgumentNullException(nameof(service)); + } + + public static EditorWindow[] FindWindowsByName( + string windowName, + WindowMatchMode matchMode = WindowMatchMode.exact) + { + return Service.FindWindowsByName(windowName, matchMode); + } + + public static Task<(Texture2D? texture, bool timedOut)> CaptureWindowAsync( + EditorWindow window, + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct) + { + return Service.CaptureWindowAsync(window, resolutionScale, timeoutMilliseconds, ct); + } + + public static string[] GetOpenWindowNames() + { + return Service.GetOpenWindowNames(); + } + + public static Task<(Texture2D? texture, int yOffset, bool timedOut)> CaptureGameRenderingAsync( + float resolutionScale, + int timeoutMilliseconds, + CancellationToken ct) + { + return Service.CaptureGameRenderingAsync(resolutionScale, timeoutMilliseconds, ct); + } + + private static IEditorWindowCaptureService Service + { + get + { + if (ServiceValue == null) + { + throw new InvalidOperationException("Unity CLI Loop editor window capture service is not registered."); + } + + return ServiceValue; + } + } + } +} diff --git a/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs.meta b/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs.meta new file mode 100644 index 0000000000..d789b3700f --- /dev/null +++ b/Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3500b314692c4c48a7e02e5492f476a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Application/MainThreadSwitcher.cs b/Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs similarity index 99% rename from Packages/src/Editor/Application/MainThreadSwitcher.cs rename to Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs index ea8c63f6d5..cb5e530820 100644 --- a/Packages/src/Editor/Application/MainThreadSwitcher.cs +++ b/Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; using System.Threading; -namespace io.github.hatayama.UnityCliLoop.Application +namespace io.github.hatayama.UnityCliLoop.ToolContracts { // Port for dispatching continuations onto Unity's main thread. /// diff --git a/Packages/src/Editor/Application/MainThreadSwitcher.cs.meta b/Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs.meta similarity index 83% rename from Packages/src/Editor/Application/MainThreadSwitcher.cs.meta rename to Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs.meta index 624bd9a787..91b9a49b7f 100644 --- a/Packages/src/Editor/Application/MainThreadSwitcher.cs.meta +++ b/Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1fb8b3db33a9146cbaf7a70e84ef1ba7 +guid: 7f216860c663f4d31a31bf339dbb5d7c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs b/Packages/src/Editor/ToolContracts/ScreenshotResponse.cs similarity index 96% rename from Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs rename to Packages/src/Editor/ToolContracts/ScreenshotResponse.cs index 0b1dc78131..bffc107dcf 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs +++ b/Packages/src/Editor/ToolContracts/ScreenshotResponse.cs @@ -4,7 +4,7 @@ using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +namespace io.github.hatayama.UnityCliLoop.ToolContracts { /// /// Describes Screenshot information collected by the owning workflow. diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs.meta b/Packages/src/Editor/ToolContracts/ScreenshotResponse.cs.meta similarity index 83% rename from Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs.meta rename to Packages/src/Editor/ToolContracts/ScreenshotResponse.cs.meta index e6c14a6c9d..6d241721f9 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs.meta +++ b/Packages/src/Editor/ToolContracts/ScreenshotResponse.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9c8241beb9ee24fb79942b75fbee8524 +guid: caf418fbbe0764422835e333dc8908e6 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs b/Packages/src/Editor/ToolContracts/ScreenshotSchema.cs similarity index 92% rename from Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs rename to Packages/src/Editor/ToolContracts/ScreenshotSchema.cs index a61bc64663..e71d6cfe48 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs +++ b/Packages/src/Editor/ToolContracts/ScreenshotSchema.cs @@ -1,7 +1,7 @@ using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +namespace io.github.hatayama.UnityCliLoop.ToolContracts { /// /// Describes the parameters accepted by the Screenshot tool. diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs.meta b/Packages/src/Editor/ToolContracts/ScreenshotSchema.cs.meta similarity index 83% rename from Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs.meta rename to Packages/src/Editor/ToolContracts/ScreenshotSchema.cs.meta index 12e752a82e..ba93fb0dde 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotSchema.cs.meta +++ b/Packages/src/Editor/ToolContracts/ScreenshotSchema.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5d23b680fbac84139986f03aabaca0b5 +guid: 246fcb22b0f864f57bb90409874ef4ef MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/ToolContracts/ServiceResult.cs b/Packages/src/Editor/ToolContracts/ServiceResult.cs new file mode 100644 index 0000000000..297c681387 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/ServiceResult.cs @@ -0,0 +1,25 @@ +namespace io.github.hatayama.UnityCliLoop.ToolContracts +{ + /// + /// Represents a platform operation result exposed through public tool contracts. + /// + public class ServiceResult + { + public bool Success { get; } + + public T Data { get; } + + public string ErrorMessage { get; } + + public ServiceResult(bool success, T data = default, string errorMessage = null) + { + Success = success; + Data = data; + ErrorMessage = errorMessage; + } + + public static ServiceResult SuccessResult(T data) => new(true, data); + + public static ServiceResult FailureResult(string errorMessage) => new(false, default, errorMessage); + } +} diff --git a/Packages/src/Editor/Domain/ServiceResult.cs.meta b/Packages/src/Editor/ToolContracts/ServiceResult.cs.meta similarity index 83% rename from Packages/src/Editor/Domain/ServiceResult.cs.meta rename to Packages/src/Editor/ToolContracts/ServiceResult.cs.meta index 16d3bef3c4..fa536b4b19 100644 --- a/Packages/src/Editor/Domain/ServiceResult.cs.meta +++ b/Packages/src/Editor/ToolContracts/ServiceResult.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: de2101885e97d4499889ddbb6bb6e358 +guid: 707e5093ea4504ef5bdfa5d692f62ce9 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/ToolContracts/ToolInfo.cs b/Packages/src/Editor/ToolContracts/ToolInfo.cs new file mode 100644 index 0000000000..5f002528c0 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/ToolInfo.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace io.github.hatayama.UnityCliLoop.ToolContracts +{ + /// + /// Catalog DTO returned by the CLI tool-details command. + /// + public class ToolInfo + { + [JsonProperty("name")] public string Name { get; } + + [JsonProperty("parameterSchema")] public ToolParameterSchema ParameterSchema { get; } + + [JsonProperty("displayDevelopmentOnly")] public bool DisplayDevelopmentOnly { get; } + + public ToolInfo(string name, ToolParameterSchema parameterSchema, bool displayDevelopmentOnly = false) + { + Name = name; + ParameterSchema = parameterSchema; + DisplayDevelopmentOnly = displayDevelopmentOnly; + } + } +} diff --git a/Packages/src/Editor/ToolContracts/ToolInfo.cs.meta b/Packages/src/Editor/ToolContracts/ToolInfo.cs.meta new file mode 100644 index 0000000000..4fb7d4a430 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/ToolInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13aa12b7813ab47f19b0c7499dbbef5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs b/Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs similarity index 75% rename from Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs rename to Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs index 281409eb3f..1aaec057c2 100644 --- a/Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs +++ b/Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs @@ -1,5 +1,8 @@ -namespace io.github.hatayama.UnityCliLoop.Domain +namespace io.github.hatayama.UnityCliLoop.ToolContracts { + /// + /// Describes a tool entry that can appear in tool settings. + /// public class ToolSettingsCatalogItem { public readonly string Name; diff --git a/Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs.meta b/Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs.meta similarity index 83% rename from Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs.meta rename to Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs.meta index 6c0f3a983b..1e6ccfdcb6 100644 --- a/Packages/src/Editor/Domain/ToolSettingsCatalogItem.cs.meta +++ b/Packages/src/Editor/ToolContracts/ToolSettingsCatalogItem.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a7567b31f0be746808ff45569ee93fd2 +guid: 02fb4bd4b06824de7ba38c6ff34baaa0 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs b/Packages/src/Editor/ToolContracts/UIElementInfo.cs similarity index 92% rename from Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs rename to Packages/src/Editor/ToolContracts/UIElementInfo.cs index e6cd5b6951..8f04573979 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs +++ b/Packages/src/Editor/ToolContracts/UIElementInfo.cs @@ -1,5 +1,5 @@ -namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +namespace io.github.hatayama.UnityCliLoop.ToolContracts { /// /// Describes UI Element information collected by the owning workflow. diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs.meta b/Packages/src/Editor/ToolContracts/UIElementInfo.cs.meta similarity index 83% rename from Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs.meta rename to Packages/src/Editor/ToolContracts/UIElementInfo.cs.meta index 8ec045c606..9337ed79fa 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UIElementInfo.cs.meta +++ b/Packages/src/Editor/ToolContracts/UIElementInfo.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 35ace17262f7d4dc2b01041a3bdbe33a +guid: ab01fdda42dbe4021a3d3aacf2ae262d MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs similarity index 97% rename from Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs rename to Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs index d8562024b1..fd04c2df78 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +namespace io.github.hatayama.UnityCliLoop.ToolContracts { /// /// Defines the Unity CLI Loop Screenshot operations required by the owning workflow. diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs.meta b/Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs.meta similarity index 83% rename from Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs.meta rename to Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs.meta index afd1878395..8cb1d6c950 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs.meta +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopScreenshotTypes.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c3d13ec51c7a74a29ae5a1e17fd7a166 +guid: 250677647ef8747f8b2ca46257e1c74b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs new file mode 100644 index 0000000000..6f9597e7d7 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace io.github.hatayama.UnityCliLoop.ToolContracts +{ + /// + /// Defines public custom tool registration operations for extension code. + /// + public interface IUnityCliLoopToolRegistrar + { + void RegisterCustomTool(IUnityCliLoopTool tool); + void UnregisterCustomTool(string toolName); + ToolInfo[] GetRegisteredCustomTools(); + bool IsCustomToolRegistered(string toolName); + Task ExecuteToolAsync(string toolName, JToken paramsToken, CancellationToken ct); + string GetDebugInfo(); + void NotifyToolChanges(); + } + + /// + /// Public facade used by custom tools to register with Unity CLI Loop. + /// + public static class UnityCliLoopToolRegistrar + { + private static IUnityCliLoopToolRegistrar ServiceValue; + + internal static void RegisterService(IUnityCliLoopToolRegistrar service) + { + Debug.Assert(service != null, "service must not be null"); + + ServiceValue = service ?? throw new ArgumentNullException(nameof(service)); + } + + private static IUnityCliLoopToolRegistrar Service + { + get + { + if (ServiceValue == null) + { + throw new InvalidOperationException("Unity CLI Loop tool registrar service is not registered."); + } + + return ServiceValue; + } + } + + public static void RegisterCustomTool(IUnityCliLoopTool tool) + { + Service.RegisterCustomTool(tool); + } + + public static void UnregisterCustomTool(string toolName) + { + Service.UnregisterCustomTool(toolName); + } + + public static ToolInfo[] GetRegisteredCustomTools() + { + return Service.GetRegisteredCustomTools(); + } + + public static bool IsCustomToolRegistered(string toolName) + { + return Service.IsCustomToolRegistered(toolName); + } + + public static Task ExecuteToolAsync( + string toolName, + JToken paramsToken, + CancellationToken ct) + { + return Service.ExecuteToolAsync(toolName, paramsToken, ct); + } + + public static string GetDebugInfo() + { + return Service.GetDebugInfo(); + } + + public static void NotifyToolChanges() + { + Service.NotifyToolChanges(); + } + } +} diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs.meta b/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs.meta new file mode 100644 index 0000000000..69cf734e12 --- /dev/null +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8880a9c8cf5df490a888e6392d6e578e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ToolContracts/VibeLogger.cs b/Packages/src/Editor/ToolContracts/VibeLogger.cs index e713103e40..f742f1f10e 100644 --- a/Packages/src/Editor/ToolContracts/VibeLogger.cs +++ b/Packages/src/Editor/ToolContracts/VibeLogger.cs @@ -431,8 +431,7 @@ public static class VibeLogger { private static readonly VibeLoggerService ServiceValue = new VibeLoggerService(); - [InitializeOnLoadMethod] - private static void InitializeForEditorLoad() + public static void InitializeForEditorStartup() { ServiceValue.InitializeForEditorLoad(); }