From 31ce7a8fd192ff8e78fefc11b946a845d8b16767 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 18:00:17 +0900 Subject: [PATCH 01/13] Migrate legacy EditorDelay frame waits Rewrite V2 EditorDelay.DelayFrame calls to the V3 EditorFrameWaiter API so migrated custom tools no longer keep an unresolved EditorDelay symbol. Cover both direct legacy namespace usage and files that rely on assembly-scoped legacy global usings. --- .../ThirdPartyToolMigrationRulesTests.cs | 69 +++++ .../ThirdPartyToolMigrationRules.cs | 272 +++++++++++++++++- 2 files changed, 340 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 2a484b15a5..52cce61725 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -278,6 +278,62 @@ public void MigrateCSharpSource_WhenLegacyMcpConstantsAreUsed_RewritesConstantsT 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 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, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: 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_WhenLegacyRegistrarMetadataIsUsed_RewritesDomainMetadataType() { @@ -1147,6 +1203,19 @@ 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 MigrateCSharpSource_WhenLegacyNamespaceLikeTextExists_KeepsContent() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index 104a063930..bd3fa28b7f 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -36,6 +36,12 @@ internal static class ThirdPartyToolMigrationRules private const string RequiredSecuritySettingAttributeArgumentName = "RequiredSecuritySetting"; private const string LegacySecuritySettingsTypeName = "SecuritySettings"; private const string CurrentSecuritySettingTypeName = "UnityCliLoopSecuritySetting"; + private const string LegacyEditorDelayTypeName = "EditorDelay"; + private const string LegacyEditorDelayMethodName = "DelayFrame"; + private const string CurrentEditorFrameWaiterTypeName = "EditorFrameWaiter"; + private const string CurrentEditorFrameWaiterMethodName = "WaitFramesOrTimeoutAsync"; + private const string CurrentConstantsTypeName = "UnityCliLoopConstants"; + private const string CurrentEditorFrameWaitTimeoutMemberName = "EDITOR_FRAME_WAIT_TIMEOUT_MS"; private const int MinimumRawStringDelimiterQuoteCount = 3; private static readonly ConditionalWeakTable CodeTextMaskCache = new(); @@ -158,11 +164,16 @@ internal static class ThirdPartyToolMigrationRules $@"new\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.)ToolSettingsCatalogItem|(?[A-Za-z_][A-Za-z0-9_]*)\.ToolSettingsCatalogItem|(?ToolSettingsCatalogItem))\s*\(", RegexOptions.Compiled); + private 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); + private static readonly TypeReplacementRule[] ToolContractTypeReplacementRules = { new("ToolParameterSchemaGenerator", "UnityCliLoopToolParameterSchemaGenerator"), new("ParameterValidationException", "UnityCliLoopToolParameterValidationException"), - new("Mcp" + "Constants", "UnityCliLoopConstants"), + new("Mcp" + "Constants", CurrentConstantsTypeName), new("McpToolAttribute", "UnityCliLoopToolAttribute"), new("IUnityTool", "IUnityCliLoopTool"), new("AbstractUnityTool", "UnityCliLoopTool"), @@ -251,6 +262,13 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac legacyNamespaceAliases, canMigrateBareLegacyToolAttribute, ref replacementCount); + (string editorDelayMigratedContent, int editorDelayReplacementCount) = + ReplaceLegacyEditorDelayFrameCallsInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyToolAttribute); + migratedContent = editorDelayMigratedContent; + replacementCount += editorDelayReplacementCount; migratedContent = ReplaceLegacyRegistrarAliasesInCode( migratedContent, legacyNamespaceAliases, @@ -390,6 +408,7 @@ internal static bool ContainsMigrationCandidateText(string source) ContainsTextFragment(source, CurrentDomainNamespace) || ContainsTextFragment(source, "McpTool") || ContainsTextFragment(source, "CustomToolManager") || + ContainsTextFragment(source, LegacyEditorDelayTypeName) || ContainsTextFragment(source, "UnityCliLoopToolRegistrar") || ContainsTextFragment(source, "ToolInfo")) { @@ -589,6 +608,10 @@ internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] leg RegexMatchesCode(source, LegacyAssemblyScopedApiUsageRegex) || ContainsLegacyAssemblyScopedTypeReference(source) || ContainsLegacyAliasQualifiedAssemblyScopedApi(source, legacyAssemblyAliases) || + ContainsLegacyEditorDelayFrameCall( + source, + legacyAssemblyAliases, + canMigrateBareLegacyEditorDelay: true) || ContainsLegacyToolAttributeList( source, legacyAssemblyAliases, @@ -1493,6 +1516,228 @@ private static bool IsLegacyToolSettingsCatalogItemConstructorMatch( return match.Groups["toolSettingsCatalogItem"].Success && canMigrateBareLegacyConstructor; } + private static (string Content, int ReplacementCount) ReplaceLegacyEditorDelayFrameCallsInCode( + 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); + StringBuilder builder = new(source.Length); + int sourceCopyIndex = 0; + int replacementCount = 0; + foreach (Match match in matches) + { + if (match.Index < sourceCopyIndex || + !codeTextMask.IsCodeAt(match.Index) || + !IsLegacyEditorDelayFrameCallMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyEditorDelay)) + { + 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 = GetMigratedEditorDelayFrameArguments( + arguments, + GetMigratedEditorFrameWaitTimeoutExpression(match)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(GetMigratedEditorFrameWaiterInvocationTarget(match)); + 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); + } + + private 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; + } + + private static string GetMigratedEditorFrameWaiterInvocationTarget(Match match) + { + 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}"; + } + + return CurrentEditorFrameWaiterTypeName; + } + + private static string GetMigratedEditorFrameWaitTimeoutExpression(Match match) + { + Debug.Assert(match != null, "match must not be null"); + + if (match.Groups["qualifier"].Success) + { + return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + if (match.Groups["alias"].Success) + { + return $"{match.Groups["alias"].Value}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + return $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; + } + + private 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(); + } + + private 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 = argument.IndexOf(':'); + 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; + } + private static bool ShouldMigrateLegacyToolInfoConstructorArguments( Match match, string[] arguments, @@ -1964,6 +2209,31 @@ private static bool ContainsLegacyAliasQualifiedAssemblyScopedApi( return false; } + private 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; + } + private static bool ContainsLegacyAliasQualifiedName(string source, string alias, string typeName) { Debug.Assert(source != null, "source must not be null"); From 2434bf76f4739311b61b7af984381d113a07c1ba Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 18:03:21 +0900 Subject: [PATCH 02/13] Stop post-migration auto scan Avoid starting another migration preview immediately after files were rewritten. The wizard now shows the completed no-targets state after a successful migration and only refreshes when the apply step made no changes. --- .../ThirdPartyToolMigrationWizardWindowTests.cs | 15 +++++++++++++++ .../Setup/ThirdPartyToolMigrationWizardWindow.cs | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index 99acbedcb0..7169f0a919 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -124,6 +124,21 @@ public void ShouldApplyMigrationProgress_ReturnsExpectedValue( Assert.That(shouldApply, Is.EqualTo(expected)); } + [TestCase(1, false)] + [TestCase(0, true)] + public void ShouldRefreshAfterMigration_ReturnsExpectedValue( + int migratedFileCount, + bool expected) + { + // 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.EqualTo(expected)); + } + [Test] public void PrepareForOpen_PopulatesWindowStateBeforeShowing() { diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index 3c84225cda..644408b2b5 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -335,13 +335,13 @@ await Task.Run(async () => private void HandleMigrateThirdPartyTools() { string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + ThirdPartyToolMigrationResult result = default; _isMigrating = true; ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); try { - ThirdPartyToolMigrationResult result = - _thirdPartyToolMigrationUseCase.ApplyMigration(projectRoot); + result = _thirdPartyToolMigrationUseCase.ApplyMigration(projectRoot); if (result.Changed) { AssetDatabase.Refresh(); @@ -350,8 +350,15 @@ private void HandleMigrateThirdPartyTools() finally { _isMigrating = false; + } + + if (ShouldRefreshAfterMigration(result)) + { RefreshUI(); + return; } + + ShowNoMigrationTargetsState(); } private void ShowNotCheckedState() @@ -585,6 +592,11 @@ internal static bool ShouldApplyMigrationProgress( return !isCancellationRequested && hasActivePreview; } + internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult result) + { + return !result.Changed; + } + private void CompleteMigrationPreview(CancellationToken ct) { if (!IsMigrationPreviewActive(ct)) From 17de2033419075ed94768c125bd3ed072233c689 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 18:29:11 +0900 Subject: [PATCH 03/13] Migrate legacy editor window capture calls Rewrite V2 EditorWindowCaptureUtility.CaptureWindowAsync calls to the V3 screenshot utility signature so partially migrated custom tools do not keep an unresolved helper reference. Cover both legacy-source and partially migrated ToolContracts-source cases. --- .../ThirdPartyToolMigrationRulesTests.cs | 98 +++++++++++ .../ThirdPartyToolMigrationRules.cs | 161 ++++++++++++++++++ 2 files changed, 259 insertions(+) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 52cce61725..7914ab24ae 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -334,6 +334,90 @@ public async Task CaptureAsync(CancellationToken ct) 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.FirstPartyTools.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, + legacyAssemblyAliases: System.Array.Empty(), + legacyAssemblyToolInfoAliases: System.Array.Empty()); + + Assert.That(result.Changed, Is.True); + Assert.That(result.Content, Does.Contain( + "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.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_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.FirstPartyTools.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_WhenLegacyRegistrarMetadataIsUsed_RewritesDomainMetadataType() { @@ -1216,6 +1300,20 @@ public void ContainsLegacyAssemblyScopedApi_WhenLegacyEditorDelayIsUsed_ReturnsT 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() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index bd3fa28b7f..d8c3f12ae6 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -20,6 +20,7 @@ internal static class ThirdPartyToolMigrationRules 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"; private const string CurrentApplicationAssemblyName = "UnityCLILoop.Application"; @@ -40,6 +41,8 @@ internal static class ThirdPartyToolMigrationRules private const string LegacyEditorDelayMethodName = "DelayFrame"; private const string CurrentEditorFrameWaiterTypeName = "EditorFrameWaiter"; private const string CurrentEditorFrameWaiterMethodName = "WaitFramesOrTimeoutAsync"; + private const string LegacyEditorWindowCaptureUtilityTypeName = "EditorWindowCaptureUtility"; + private const string EditorWindowCaptureUtilityCaptureWindowMethodName = "CaptureWindowAsync"; private const string CurrentConstantsTypeName = "UnityCliLoopConstants"; private const string CurrentEditorFrameWaitTimeoutMemberName = "EDITOR_FRAME_WAIT_TIMEOUT_MS"; private const int MinimumRawStringDelimiterQuoteCount = 3; @@ -169,6 +172,11 @@ internal static class ThirdPartyToolMigrationRules $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorDelayTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorDelayTypeName}|(?{LegacyEditorDelayTypeName}))\s*\.\s*{LegacyEditorDelayMethodName}\s*\(", RegexOptions.Compiled); + private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowRegex = + new( + $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + RegexOptions.Compiled); + private static readonly TypeReplacementRule[] ToolContractTypeReplacementRules = { new("ToolParameterSchemaGenerator", "UnityCliLoopToolParameterSchemaGenerator"), @@ -229,10 +237,14 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); bool hasLegacyNamespaceUsage = RegexMatchesCode(source, LegacyNamespaceRegex); bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); + bool hasCurrentToolContractsNamespaceUsage = RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); bool canMigrateBareLegacyToolAttribute = hasLegacyAssemblySource || hasLegacyNamespaceUsage || legacyNamespaceAliases.Length > 0; + bool canMigrateBareLegacyEditorWindowCaptureUtility = + canMigrateBareLegacyToolAttribute || + hasCurrentToolContractsNamespaceUsage; bool canMigrateBareLegacyToolInfoConstructor = canMigrateBareLegacyToolAttribute; bool canMigrateAmbiguousBareLegacyToolInfoConstructor = @@ -269,6 +281,13 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac canMigrateBareLegacyToolAttribute); migratedContent = editorDelayMigratedContent; replacementCount += editorDelayReplacementCount; + (string editorWindowCaptureMigratedContent, int editorWindowCaptureReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility); + migratedContent = editorWindowCaptureMigratedContent; + replacementCount += editorWindowCaptureReplacementCount; migratedContent = ReplaceLegacyRegistrarAliasesInCode( migratedContent, legacyNamespaceAliases, @@ -409,6 +428,7 @@ internal static bool ContainsMigrationCandidateText(string source) ContainsTextFragment(source, "McpTool") || ContainsTextFragment(source, "CustomToolManager") || ContainsTextFragment(source, LegacyEditorDelayTypeName) || + ContainsTextFragment(source, LegacyEditorWindowCaptureUtilityTypeName) || ContainsTextFragment(source, "UnityCliLoopToolRegistrar") || ContainsTextFragment(source, "ToolInfo")) { @@ -612,6 +632,10 @@ internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] leg source, legacyAssemblyAliases, canMigrateBareLegacyEditorDelay: true) || + ContainsLegacyEditorWindowCaptureUtilityCall( + source, + legacyAssemblyAliases, + canMigrateBareLegacyEditorWindowCaptureUtility: true) || ContainsLegacyToolAttributeList( source, legacyAssemblyAliases, @@ -1738,6 +1762,118 @@ private static string GetNamedArgumentValueOrNull(string argument, string argume return value.Length == 0 ? null : value; } + private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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 = 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, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + 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)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append("(await "); + builder.Append(CurrentFirstPartyToolsNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(")).texture"); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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["editorWindowCaptureUtility"].Success && + canMigrateBareLegacyEditorWindowCaptureUtility; + } + + private static string[] GetMigratedEditorWindowCaptureUtilityArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] trimmedArguments = arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + if (trimmedArguments.Length != 3) + { + return Array.Empty(); + } + + return new[] + { + trimmedArguments[0], + trimmedArguments[1], + $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", + trimmedArguments[2] + }; + } + private static bool ShouldMigrateLegacyToolInfoConstructorArguments( Match match, string[] arguments, @@ -2234,6 +2370,31 @@ private static bool ContainsLegacyEditorDelayFrameCall( return false; } + private static bool ContainsLegacyEditorWindowCaptureUtilityCall( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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 = LegacyEditorWindowCaptureUtilityCaptureWindowRegex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyEditorWindowCaptureUtilityCallMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + return true; + } + } + + return false; + } + private static bool ContainsLegacyAliasQualifiedName(string source, string alias, string typeName) { Debug.Assert(source != null, "source must not be null"); From 25b3f6509758e2a21ade603881db6d00dec8ef7f Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 19:06:44 +0900 Subject: [PATCH 04/13] Fix V3 custom tool migration coverage Handle public custom tool APIs that moved out of the legacy V2 editor assembly, including screenshot helpers, main-thread switching, timer delay arguments, and ToolInfo metadata. Keep migration target assemblies reachable from predefined Editor assemblies and prevent the migration wizard from immediately starting another scan after a migration run. --- .../Editor/OnionAssemblyDependencyTests.cs | 5 +- ...ThirdPartyToolMigrationFileServiceTests.cs | 197 +++ .../ThirdPartyToolMigrationRulesTests.cs | 163 ++- ...hirdPartyToolMigrationWizardWindowTests.cs | 10 +- .../UnityCLILoop.Application.asmdef | 2 +- .../UnityCliLoopApplicationRegistration.cs | 1 + .../Editor/Domain/UnityCLILoop.Domain.asmdef | 2 +- .../DomainReloadDisableScopeRecovery.cs | 3 +- .../FirstPartyToolsEditorStartup.cs | 1 + ...p.FirstPartyTools.Screenshot.Editor.asmdef | 2 +- ...UnityCLILoop.FirstPartyTools.Editor.asmdef | 1 + .../ThirdPartyToolMigrationFileService.cs | 259 +++- .../ThirdPartyToolMigrationRules.cs | 1083 ++++++++++++++++- .../ThirdPartyToolMigrationWizardWindow.cs | 4 +- .../src/Editor/ToolContracts/VibeLogger.cs | 3 +- 15 files changed, 1640 insertions(+), 96 deletions(-) diff --git a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs index 6851e8f3e5..3dba5518a1 100644 --- a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs +++ b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs @@ -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..cbeeaf2c1a 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -906,6 +906,161 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitContra } } + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitScreenshotHelpersAndAsmdef() + { + // Verifies that split files using old screenshot helpers receive the required V3 first-party assembly reference. + 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, "ScreenshotHelper.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + File.WriteAllText(globalUsingPath, "global using io.github.hatayama.uLoopMCP;"); + File.WriteAllText(helperPath, @"using UnityEditor; + +public sealed class ScreenshotHelper +{ + public EditorWindow[] FindWindows() => EditorWindowCaptureUtility.FindWindowsByName(""Game"", WindowMatchMode.exact); +}"); + 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.FirstPartyTools.EditorWindowCaptureUtility.FindWindowsByName")); + Assert.That(File.ReadAllText(helperPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.WindowMatchMode.exact")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyScreenshotCapture_AddsScreenshotAsmdefReference() + { + // Verifies that partially migrated files finish screenshot helper migration and add the missing assembly reference. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "CaptureTool.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.ToolContracts; + +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 CaptureSchema : UnityCliLoopToolSchema +{ + public EditorWindow Window { get; set; } +} + +public sealed class CaptureResponse : UnityCliLoopToolResponse +{ + public bool Success { get; } + public CaptureResponse(bool success) { Success = success; } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyMainThreadSwitcher_AddsApplicationReference() + { + // Verifies that partially migrated files finish main-thread switcher migration and add the Application asmdef reference. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string toolPath = Path.Combine(toolDirectory, "MainThreadTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + 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:fc3fd32eddbee40e39c2d76dc184957b"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + + Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(File.ReadAllText(toolPath), Does.Contain( + "io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(File.ReadAllText(toolPath), Does.Not.Contain("PlayerLoopTiming")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + [Test] public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles() { @@ -1422,6 +1577,48 @@ public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSpl } } + [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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedSource, Does.Not.Contain("await EditorWindowCaptureUtility.CaptureWindowAsync")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + [Test] public void ApplyMigration_WhenNoAsmdefEditorAssemblyUsesGlobalLegacyUsing_KeepsRuntimeFiles() { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 7914ab24ae..4994541520 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -418,6 +418,136 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Content, Does.Not.Contain("return await EditorWindowCaptureUtility.CaptureWindowAsync")); } + [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.FirstPartyTools.WindowMatchMode MatchMode")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.WindowMatchMode.contains")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.CaptureMode CaptureMode")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.CaptureMode.rendering")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotInfo()")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.FindWindowsByName")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.GetOpenWindowNames")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsUsed_RewritesSignatureAndDiscard() + { + // Verifies that old rendering capture deconstruction keeps compiling after the V3 timeout result. + 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")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)")); + Assert.That(result.Content, Does.Not.Contain("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); + 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.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.Application.SwitchToMainThreadAwaitable awaitable = io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread();")); + Assert.That(result.Content, Does.Contain( + "bool isMainThread = io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.IsMainThread;")); + Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); + Assert.That(result.Content, Does.Not.Contain("cancellationToken:")); + } + [Test] public void MigrateCSharpSource_WhenLegacyRegistrarMetadataIsUsed_RewritesDomainMetadataType() { @@ -1344,7 +1474,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")); @@ -1369,7 +1500,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")); @@ -1394,7 +1526,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)); @@ -1417,7 +1550,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")); @@ -1438,7 +1572,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"": [")); @@ -1462,7 +1597,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")); @@ -1487,7 +1623,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)); @@ -1511,7 +1648,8 @@ 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")); @@ -1536,7 +1674,8 @@ 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")); @@ -1559,7 +1698,8 @@ 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")); @@ -1586,7 +1726,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 7169f0a919..1b9bf10e5d 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -124,11 +124,9 @@ public void ShouldApplyMigrationProgress_ReturnsExpectedValue( Assert.That(shouldApply, Is.EqualTo(expected)); } - [TestCase(1, false)] - [TestCase(0, true)] - public void ShouldRefreshAfterMigration_ReturnsExpectedValue( - int migratedFileCount, - bool 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 = @@ -136,7 +134,7 @@ public void ShouldRefreshAfterMigration_ReturnsExpectedValue( bool shouldRefresh = ThirdPartyToolMigrationWizardWindow.ShouldRefreshAfterMigration(result); - Assert.That(shouldRefresh, Is.EqualTo(expected)); + Assert.That(shouldRefresh, Is.False); } [Test] diff --git a/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef b/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef index c8ecdcc581..1f6b39f50d 100644 --- a/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef +++ b/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef @@ -15,7 +15,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": false, + "autoReferenced": true, "defineConstraints": [], "versionDefines": [ { diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs index dac57ea5a5..40c9142e70 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs @@ -12,6 +12,7 @@ internal sealed class UnityCliLoopApplicationRegistration { internal UnityCliLoopApplicationServices Register() { + VibeLogger.InitializeForEditorStartup(); ToolSettingsRepository toolSettingsRepository = new(); ToolSettingsService toolSettingsService = new(toolSettingsRepository); UnityCliLoopEditorSettingsRepository editorSettingsRepository = new(); diff --git a/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef b/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef index 39b5c41f13..328843c917 100644 --- a/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef +++ b/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef @@ -11,7 +11,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": false, + "autoReferenced": true, "defineConstraints": [], "versionDefines": [ { 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..ba03663728 100644 --- a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs @@ -8,6 +8,7 @@ public static class FirstPartyToolsEditorStartup { public static void Initialize() { + DomainReloadDisableScopeRecovery.RestoreForEditorStartup(); ExternalSceneChangeTracker.Initialize(); ControlPlayModeEditorStartup.Initialize(); ExecuteDynamicCodeEditorStartup.Initialize(); diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef index 55c4576d25..0f6d7f6352 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef @@ -14,7 +14,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": false, + "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false 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/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs index 459ec61842..d42460b901 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs @@ -177,9 +177,11 @@ private static async Task HasMigrationTargetAsync(string projectRoot, Canc : CreateAssemblyReferenceDirectories(inventory.AsmdefFilePaths, inventory.AsmrefFilePaths); HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); + HashSet firstPartyScreenshotReferenceAssemblyDirectories = new(StringComparer.Ordinal); int inspectedEntryCount = 0; foreach (string csharpFilePath in inventory.CSharpFilePaths) { @@ -206,14 +208,21 @@ private static async Task HasMigrationTargetAsync(string projectRoot, Canc assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); } + if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) + { + assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); + } + CollectFastAssemblyReferenceRequirements( source, assemblyDirectory, assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), legacyAssemblyDirectories, toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } inspectedEntryCount++; @@ -239,6 +248,22 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( } } + if (assemblyScopedCurrentFirstPartyToolsDirectories.Count > 0) + { + await CollectFastAssemblyScopedCurrentFirstPartyToolsRequirementsAsync( + inventory.CSharpFilePaths, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot, + assemblyScopedCurrentFirstPartyToolsDirectories, + firstPartyScreenshotReferenceAssemblyDirectories, + ct); + if (ct.IsCancellationRequested) + { + return false; + } + } + foreach (string asmdefFilePath in inventory.AsmdefFilePaths) { if (ct.IsCancellationRequested) @@ -261,7 +286,8 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( if (toolContractsReferenceAssemblyDirectories.Count == 0 && applicationReferenceAssemblyDirectories.Count == 0 && - domainReferenceAssemblyDirectories.Count == 0) + domainReferenceAssemblyDirectories.Count == 0 && + firstPartyScreenshotReferenceAssemblyDirectories.Count == 0) { return false; } @@ -275,7 +301,8 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( new Dictionary(StringComparer.Ordinal), toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); foreach (string asmdefFilePath in inventory.AsmdefFilePaths) { @@ -297,10 +324,12 @@ private static void CollectFastAssemblyReferenceRequirements( string source, string assemblyDirectory, bool hasAssemblyScopedCurrentDomainNamespaceUsage, + bool hasAssemblyScopedCurrentFirstPartyToolsNamespaceUsage, HashSet legacyAssemblyDirectories, HashSet toolContractsReferenceAssemblyDirectories, HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) + HashSet domainReferenceAssemblyDirectories, + HashSet firstPartyScreenshotReferenceAssemblyDirectories) { Debug.Assert(source != null, "source must not be null"); Debug.Assert(!string.IsNullOrEmpty(assemblyDirectory), "assemblyDirectory must not be null or empty"); @@ -312,6 +341,9 @@ private static void CollectFastAssemblyReferenceRequirements( 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"); if (ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source)) { @@ -324,7 +356,13 @@ private static void CollectFastAssemblyReferenceRequirements( } if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(source)) { applicationReferenceAssemblyDirectories.Add(assemblyDirectory); } @@ -341,6 +379,19 @@ private static void CollectFastAssemblyReferenceRequirements( { domainReferenceAssemblyDirectories.Add(assemblyDirectory); } + + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + legacyAssemblyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + hasAssemblyScopedCurrentFirstPartyToolsNamespaceUsage)) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } } private static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsync( @@ -396,6 +447,61 @@ private static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsyn } } + private static async Task CollectFastAssemblyScopedCurrentFirstPartyToolsRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet assemblyScopedCurrentFirstPartyToolsDirectories, + 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( + assemblyScopedCurrentFirstPartyToolsDirectories != null, + "assemblyScopedCurrentFirstPartyToolsDirectories 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; + } + + string source = File.ReadAllText(csharpFilePath); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + if (assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly(source, true)) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } + + inspectedEntryCount++; + if (inspectedEntryCount % PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + } + private static bool ContainsAsmdefMigrationTarget( string asmdefFilePath, string projectRoot, @@ -408,6 +514,7 @@ private static bool ContainsAsmdefMigrationTarget( bool requiresToolContractsReference; bool requiresApplicationReference; bool requiresDomainReference; + bool requiresFirstPartyScreenshotReference; bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( asmdefFilePath, projectRoot, @@ -415,7 +522,8 @@ private static bool ContainsAsmdefMigrationTarget( out hasLegacyCSharpSource, out requiresToolContractsReference, out requiresApplicationReference, - out requiresDomainReference); + out requiresDomainReference, + out requiresFirstPartyScreenshotReference); string source = File.ReadAllText(asmdefFilePath); if (!hasAssemblyMigrationRequirement && !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) @@ -429,7 +537,8 @@ private static bool ContainsAsmdefMigrationTarget( hasLegacyCSharpSource, requiresToolContractsReference, requiresApplicationReference, - requiresDomainReference); + requiresDomainReference, + requiresFirstPartyScreenshotReference); return result.Changed; } @@ -441,7 +550,8 @@ private static bool TryGetAsmdefMigrationRequirements( out bool hasLegacyCSharpSource, out bool requiresToolContractsReference, out bool requiresApplicationReference, - out bool requiresDomainReference) + 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"); @@ -454,10 +564,13 @@ private static bool TryGetAsmdefMigrationRequirements( assemblyUsage.ApplicationReferenceAssemblyDirectories.Contains(asmdefDirectory); requiresDomainReference = assemblyUsage.DomainReferenceAssemblyDirectories.Contains(asmdefDirectory); + requiresFirstPartyScreenshotReference = + assemblyUsage.FirstPartyScreenshotReferenceAssemblyDirectories.Contains(asmdefDirectory); return hasLegacyCSharpSource || requiresToolContractsReference || requiresApplicationReference || - requiresDomainReference; + requiresDomainReference || + requiresFirstPartyScreenshotReference; } private static bool ContainsFastCSharpMigrationTarget(string source) @@ -584,6 +697,7 @@ private static MigrationPlan CreateMigrationPlan(string projectRoot) bool requiresToolContractsReference; bool requiresApplicationReference; bool requiresDomainReference; + bool requiresFirstPartyScreenshotReference; bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( asmdefFilePath, projectRoot, @@ -591,7 +705,8 @@ private static MigrationPlan CreateMigrationPlan(string projectRoot) out hasLegacyCSharpSource, out requiresToolContractsReference, out requiresApplicationReference, - out requiresDomainReference); + out requiresDomainReference, + out requiresFirstPartyScreenshotReference); string source = File.ReadAllText(asmdefFilePath); if (!hasAssemblyMigrationRequirement && !ThirdPartyToolMigrationRules.ContainsLegacyMigrationCandidateText(source)) @@ -605,7 +720,8 @@ private static MigrationPlan CreateMigrationPlan(string projectRoot) hasLegacyCSharpSource, requiresToolContractsReference, requiresApplicationReference, - requiresDomainReference); + requiresDomainReference, + requiresFirstPartyScreenshotReference); if (!result.Changed) { continue; @@ -719,6 +835,7 @@ private static async Task CreateMigrationPlanAsync( bool requiresToolContractsReference; bool requiresApplicationReference; bool requiresDomainReference; + bool requiresFirstPartyScreenshotReference; bool hasAssemblyMigrationRequirement = TryGetAsmdefMigrationRequirements( asmdefFilePath, projectRoot, @@ -726,7 +843,8 @@ private static async Task CreateMigrationPlanAsync( out hasLegacyCSharpSource, out requiresToolContractsReference, out requiresApplicationReference, - out requiresDomainReference); + out requiresDomainReference, + out requiresFirstPartyScreenshotReference); string source = sourceFileCache.ReadAllText(asmdefFilePath); await progressCounter.ReportProcessedItemAsync(ct); if (!hasAssemblyMigrationRequirement && @@ -741,7 +859,8 @@ private static async Task CreateMigrationPlanAsync( hasLegacyCSharpSource, requiresToolContractsReference, requiresApplicationReference, - requiresDomainReference); + requiresDomainReference, + requiresFirstPartyScreenshotReference); if (!result.Changed) { continue; @@ -827,6 +946,7 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); Dictionary> assemblyScopedLegacyAliasesByDirectory = new(StringComparer.Ordinal); Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = @@ -836,6 +956,7 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( 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) { @@ -878,8 +999,19 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); } + if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) + { + assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); + } + if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(source)) { registrarAssemblyDirectories.Add(assemblyDirectory); } @@ -903,6 +1035,16 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( { domainReferenceAssemblyDirectories.Add(assemblyDirectory); } + + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source)) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } } foreach (string csharpFilePath in csharpFilePaths) @@ -939,6 +1081,11 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( source, assemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), legacyAssemblyAliases)) { registrarAssemblyDirectories.Add(assemblyDirectory); @@ -958,6 +1105,18 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( { domainReferenceAssemblyDirectories.Add(assemblyDirectory); } + + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + legacyAssemblyAliases) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory))) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } } foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) @@ -982,7 +1141,8 @@ private static MigrationAssemblyUsage FindMigrationAssemblyUsage( CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } private static async Task FindMigrationAssemblyUsageAsync( @@ -1016,6 +1176,7 @@ await CreateAssemblyReferenceDirectoriesAsync( HashSet legacyAssemblyDirectories = new(StringComparer.Ordinal); HashSet assemblyScopedLegacyDirectories = new(StringComparer.Ordinal); HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); Dictionary> assemblyScopedLegacyAliasesByDirectory = new(StringComparer.Ordinal); Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory = @@ -1025,6 +1186,7 @@ await CreateAssemblyReferenceDirectoriesAsync( 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) { @@ -1039,7 +1201,8 @@ await CreateAssemblyReferenceDirectoriesAsync( assemblyScopedLegacyToolInfoAliasesByDirectory, toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } string source = sourceFileCache.ReadAllText(csharpFilePath); @@ -1082,8 +1245,19 @@ await CreateAssemblyReferenceDirectoriesAsync( assemblyScopedCurrentDomainDirectories.Add(assemblyDirectory); } + if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) + { + assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); + } + if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source)) + ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(source)) { registrarAssemblyDirectories.Add(assemblyDirectory); } @@ -1107,6 +1281,16 @@ await CreateAssemblyReferenceDirectoriesAsync( { domainReferenceAssemblyDirectories.Add(assemblyDirectory); } + + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + Array.Empty()) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source)) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } } foreach (string csharpFilePath in csharpFilePaths) @@ -1122,7 +1306,8 @@ await CreateAssemblyReferenceDirectoriesAsync( assemblyScopedLegacyToolInfoAliasesByDirectory, toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } string source = sourceFileCache.ReadAllText(csharpFilePath); @@ -1158,6 +1343,11 @@ await CreateAssemblyReferenceDirectoriesAsync( if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApiForAssembly( source, assemblyScopedLegacyDirectories.Contains(assemblyDirectory), + legacyAssemblyAliases) || + ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), legacyAssemblyAliases)) { registrarAssemblyDirectories.Add(assemblyDirectory); @@ -1177,6 +1367,18 @@ await CreateAssemblyReferenceDirectoriesAsync( { domainReferenceAssemblyDirectories.Add(assemblyDirectory); } + + if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || + ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), + legacyAssemblyAliases) || + ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory))) + { + firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); + } } foreach (string registrarAssemblyDirectory in registrarAssemblyDirectories) @@ -1201,7 +1403,8 @@ await CreateAssemblyReferenceDirectoriesAsync( assemblyScopedLegacyToolInfoAliasesByDirectory, toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( @@ -1213,7 +1416,8 @@ private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, HashSet toolContractsReferenceAssemblyDirectories, HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) + HashSet domainReferenceAssemblyDirectories, + HashSet firstPartyScreenshotReferenceAssemblyDirectories) { Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); Debug.Assert( @@ -1238,6 +1442,9 @@ private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( Debug.Assert( domainReferenceAssemblyDirectories != null, "domainReferenceAssemblyDirectories must not be null"); + Debug.Assert( + firstPartyScreenshotReferenceAssemblyDirectories != null, + "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); return new MigrationAssemblyUsage( asmdefDirectories, @@ -1248,7 +1455,8 @@ private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), toolContractsReferenceAssemblyDirectories, applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories); + domainReferenceAssemblyDirectories, + firstPartyScreenshotReferenceAssemblyDirectories); } private static void AddAssemblyScopedLegacyAliases( @@ -1808,7 +2016,8 @@ public MigrationAssemblyUsage( Dictionary assemblyScopedLegacyToolInfoAliasesByDirectory, HashSet toolContractsReferenceAssemblyDirectories, HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories) + HashSet domainReferenceAssemblyDirectories, + HashSet firstPartyScreenshotReferenceAssemblyDirectories) { Debug.Assert(asmdefDirectories != null, "asmdefDirectories must not be null"); Debug.Assert( @@ -1833,6 +2042,9 @@ public MigrationAssemblyUsage( Debug.Assert( domainReferenceAssemblyDirectories != null, "domainReferenceAssemblyDirectories must not be null"); + Debug.Assert( + firstPartyScreenshotReferenceAssemblyDirectories != null, + "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); AsmdefDirectories = asmdefDirectories ?? new List(); AssemblyReferenceDirectories = assemblyReferenceDirectories ?? new List(); @@ -1849,6 +2061,8 @@ public MigrationAssemblyUsage( new HashSet(StringComparer.Ordinal); DomainReferenceAssemblyDirectories = domainReferenceAssemblyDirectories ?? new HashSet(StringComparer.Ordinal); + FirstPartyScreenshotReferenceAssemblyDirectories = firstPartyScreenshotReferenceAssemblyDirectories ?? + new HashSet(StringComparer.Ordinal); } public List AsmdefDirectories { get; } @@ -1860,6 +2074,7 @@ public MigrationAssemblyUsage( public HashSet ToolContractsReferenceAssemblyDirectories { get; } public HashSet ApplicationReferenceAssemblyDirectories { get; } public HashSet DomainReferenceAssemblyDirectories { get; } + public HashSet FirstPartyScreenshotReferenceAssemblyDirectories { get; } } private readonly struct AssemblyReferenceDirectory diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index d8c3f12ae6..236e3d04b9 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -25,11 +25,15 @@ internal static class ThirdPartyToolMigrationRules internal const string LegacyRuntimeAssemblyName = "uLoopMCP.Runtime"; private const string CurrentApplicationAssemblyName = "UnityCLILoop.Application"; private const string CurrentDomainAssemblyName = "UnityCLILoop.Domain"; + private const string CurrentFirstPartyToolsScreenshotAssemblyName = + "UnityCLILoop.FirstPartyTools.Screenshot.Editor"; 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 CurrentFirstPartyToolsScreenshotGuidReference = + "GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"; internal const string CurrentRuntimeGuidReference = "GUID:c956a21f824994ef087b6de566690b3d"; internal const string CurrentToolContractsGuidReference = "GUID:fc3fd32eddbee40e39c2d76dc184957b"; private const string DescriptionAttributeArgumentName = "Description"; @@ -39,10 +43,19 @@ internal static class ThirdPartyToolMigrationRules private const string CurrentSecuritySettingTypeName = "UnityCliLoopSecuritySetting"; private const string LegacyEditorDelayTypeName = "EditorDelay"; private const string LegacyEditorDelayMethodName = "DelayFrame"; + private const string LegacyTimerDelayTypeName = "TimerDelay"; + private const string LegacyCancellationTokenArgumentName = "cancellationToken"; + private const string CurrentCancellationTokenArgumentName = "ct"; + private const string LegacyPlayerLoopTimingTypeName = "PlayerLoopTiming"; + private const string LegacyTimingArgumentName = "timing"; + private const string LegacyMainThreadSwitcherTypeName = "MainThreadSwitcher"; + private const string LegacyMainThreadSwitcherSwitchMethodName = "SwitchToMainThread"; private const string CurrentEditorFrameWaiterTypeName = "EditorFrameWaiter"; private const string CurrentEditorFrameWaiterMethodName = "WaitFramesOrTimeoutAsync"; private const string LegacyEditorWindowCaptureUtilityTypeName = "EditorWindowCaptureUtility"; private const string EditorWindowCaptureUtilityCaptureWindowMethodName = "CaptureWindowAsync"; + private const string EditorWindowCaptureUtilityCaptureGameRenderingMethodName = + "CaptureGameRenderingAsync"; private const string CurrentConstantsTypeName = "UnityCliLoopConstants"; private const string CurrentEditorFrameWaitTimeoutMemberName = "EDITOR_FRAME_WAIT_TIMEOUT_MS"; private const int MinimumRawStringDelimiterQuoteCount = 3; @@ -76,6 +89,16 @@ internal static class ThirdPartyToolMigrationRules $@"(?[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?{Regex.Escape(LegacyNamespace)}\s*;", @@ -177,6 +205,26 @@ internal static class ThirdPartyToolMigrationRules $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", RegexOptions.Compiled); + private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex = + new( + $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + RegexOptions.Compiled); + + private 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); + + private static readonly Regex LegacyMainThreadSwitcherSwitchRegex = + new( + $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyMainThreadSwitcherTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyMainThreadSwitcherTypeName}|(?{LegacyMainThreadSwitcherTypeName}))\s*\.\s*{LegacyMainThreadSwitcherSwitchMethodName}\s*\(", + RegexOptions.Compiled); + + private static readonly Regex CurrentCaptureGameRenderingDeconstructionRegex = + new( + $@"\((?[^()]*)\)\s*=\s*await\s+{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.{LegacyEditorWindowCaptureUtilityTypeName}\.{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + RegexOptions.Compiled); + private static readonly TypeReplacementRule[] ToolContractTypeReplacementRules = { new("ToolParameterSchemaGenerator", "UnityCliLoopToolParameterSchemaGenerator"), @@ -196,6 +244,23 @@ internal static class ThirdPartyToolMigrationRules new("ToolSettingsCatalogItem", "ToolSettingsCatalogItem") }; + private static readonly TypeReplacementRule[] ApplicationTypeReplacementRules = + { + new("MainThreadSwitcher", "MainThreadSwitcher"), + new("SwitchToMainThreadAwaitable", "SwitchToMainThreadAwaitable") + }; + + private 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") + }; + private static readonly ReplacementRule[] CSharpReplacementRules = { new( @@ -236,8 +301,11 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac string migratedContent = source; string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); bool hasLegacyNamespaceUsage = RegexMatchesCode(source, LegacyNamespaceRegex); + bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); bool hasCurrentToolContractsNamespaceUsage = RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); bool canMigrateBareLegacyToolAttribute = hasLegacyAssemblySource || hasLegacyNamespaceUsage || @@ -245,6 +313,14 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac bool canMigrateBareLegacyEditorWindowCaptureUtility = canMigrateBareLegacyToolAttribute || hasCurrentToolContractsNamespaceUsage; + bool canMigrateBareLegacyFirstPartyScreenshotApi = + canMigrateBareLegacyToolAttribute || + hasCurrentToolContractsNamespaceUsage || + hasCurrentFirstPartyToolsNamespaceUsage; + bool canMigrateBareLegacyApplicationApi = + canMigrateBareLegacyToolAttribute || + hasCurrentToolContractsNamespaceUsage || + hasCurrentApplicationNamespaceUsage; bool canMigrateBareLegacyToolInfoConstructor = canMigrateBareLegacyToolAttribute; bool canMigrateAmbiguousBareLegacyToolInfoConstructor = @@ -281,6 +357,20 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac canMigrateBareLegacyToolAttribute); migratedContent = editorDelayMigratedContent; replacementCount += editorDelayReplacementCount; + (string timerDelayMigratedContent, int timerDelayReplacementCount) = + ReplaceLegacyTimerDelayNamedArgumentsInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyToolAttribute || hasCurrentToolContractsNamespaceUsage); + migratedContent = timerDelayMigratedContent; + replacementCount += timerDelayReplacementCount; + (string mainThreadSwitcherMigratedContent, int mainThreadSwitcherReplacementCount) = + ReplaceLegacyMainThreadSwitcherCallsInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyApplicationApi); + migratedContent = mainThreadSwitcherMigratedContent; + replacementCount += mainThreadSwitcherReplacementCount; (string editorWindowCaptureMigratedContent, int editorWindowCaptureReplacementCount) = ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( migratedContent, @@ -288,6 +378,16 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac canMigrateBareLegacyEditorWindowCaptureUtility); migratedContent = editorWindowCaptureMigratedContent; replacementCount += editorWindowCaptureReplacementCount; + migratedContent = ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyFirstPartyScreenshotApi, + ref replacementCount); + migratedContent = ReplaceLegacyApplicationTypeNamesInCode( + migratedContent, + legacyNamespaceAliases, + canMigrateBareLegacyApplicationApi, + ref replacementCount); migratedContent = ReplaceLegacyRegistrarAliasesInCode( migratedContent, legacyNamespaceAliases, @@ -348,7 +448,8 @@ 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"); @@ -371,7 +472,8 @@ internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( hasLegacyCSharpSource, requiresToolContractsReference, requiresApplicationReference, - requiresDomainReference); + requiresDomainReference, + requiresFirstPartyScreenshotReference); bool referenceChanged = migratedReferenceItems.Length != 1 || !string.Equals(migratedReferenceItems[0], reference, StringComparison.Ordinal); if (referenceChanged) @@ -397,6 +499,7 @@ internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( hasLegacyCSharpSource || requiresToolContractsReference, requiresApplicationReference, requiresDomainReference, + requiresFirstPartyScreenshotReference, ref replacementCount); if (replacementCount == 0) @@ -425,9 +528,13 @@ internal static bool ContainsMigrationCandidateText(string source) 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")) @@ -453,6 +560,24 @@ internal static bool ContainsMigrationCandidateText(string source) } } + 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; } @@ -530,6 +655,29 @@ internal static bool ContainsCurrentRegistrarApi(string source) return RegexMatchesCode(source, CurrentRegistrarRegex); } + internal static bool ContainsLegacyApplicationApiForAssembly( + 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 ContainsLegacyApplicationReference( + source, + hasLegacyAssemblySource, + legacyNamespaceAliases); + } + + internal static bool ContainsCurrentApplicationApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); + return ContainsCurrentApplicationReference(source, hasCurrentApplicationNamespaceUsage); + } + internal static bool ContainsRegistrarDomainReturnApi(string source) { Debug.Assert(source != null, "source must not be null"); @@ -619,6 +767,48 @@ internal static bool ContainsCurrentDomainMetadataApiForAssembly( (canUseBareCurrentDomainType && RegexMatchesCode(source, LegacyDomainMetadataRegex)); } + internal static bool ContainsLegacyFirstPartyScreenshotApiForAssembly( + 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 ContainsLegacyFirstPartyScreenshotReference( + source, + hasLegacyAssemblySource, + legacyNamespaceAliases); + } + + internal static bool ContainsCurrentFirstPartyScreenshotApi(string source) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + return ContainsCurrentFirstPartyScreenshotApiForAssembly( + source, + hasCurrentFirstPartyToolsNamespaceUsage); + } + + internal static bool ContainsCurrentFirstPartyScreenshotApiForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing) + { + Debug.Assert(source != null, "source must not be null"); + + bool hasCurrentFirstPartyToolsNamespaceUsage = + RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); + bool canUseBareCurrentFirstPartyScreenshotType = + hasAssemblyScopedCurrentFirstPartyToolsUsing || + hasCurrentFirstPartyToolsNamespaceUsage; + return ContainsCurrentFirstPartyScreenshotReference( + source, + canUseBareCurrentFirstPartyScreenshotType); + } + internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] legacyAssemblyAliases) { Debug.Assert(source != null, "source must not be null"); @@ -636,6 +826,18 @@ internal static bool ContainsLegacyAssemblyScopedApi(string source, string[] leg source, legacyAssemblyAliases, canMigrateBareLegacyEditorWindowCaptureUtility: true) || + ContainsLegacyTimerDelayInvocation( + source, + legacyAssemblyAliases, + canMigrateBareLegacyTimerDelay: true) || + ContainsLegacyApplicationReference( + source, + true, + legacyAssemblyAliases) || + ContainsLegacyFirstPartyScreenshotReference( + source, + true, + legacyAssemblyAliases) || ContainsLegacyToolAttributeList( source, legacyAssemblyAliases, @@ -656,6 +858,13 @@ internal static bool ContainsCurrentDomainGlobalUsing(string source) return RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); } + internal static bool ContainsCurrentFirstPartyToolsGlobalUsing(string source) + { + Debug.Assert(source != null, "source must not be null"); + + return RegexMatchesCode(source, CurrentFirstPartyToolsGlobalUsingRegex); + } + internal static string[] GetLegacyGlobalNamespaceAliases(string source) { Debug.Assert(source != null, "source must not be null"); @@ -722,11 +931,15 @@ private static string[] GetMigratedAsmdefReferences( bool hasLegacyCSharpSource, bool requiresToolContractsReference, bool requiresApplicationReference, - bool requiresDomainReference) + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) { if (string.Equals(reference, LegacyEditorAssemblyName, StringComparison.Ordinal)) { - return GetMigratedLegacyEditorReferences(requiresApplicationReference, requiresDomainReference); + return GetMigratedLegacyEditorReferences( + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); } if (string.Equals(reference, LegacyRuntimeAssemblyName, StringComparison.Ordinal)) @@ -737,7 +950,10 @@ private static string[] GetMigratedAsmdefReferences( if (hasLegacyCSharpSource && string.Equals(reference, LegacyEditorAssemblyGuidReference, StringComparison.Ordinal)) { - return GetMigratedLegacyEditorReferences(requiresApplicationReference, requiresDomainReference); + return GetMigratedLegacyEditorReferences( + requiresApplicationReference, + requiresDomainReference, + requiresFirstPartyScreenshotReference); } return new[] { reference }; @@ -745,7 +961,8 @@ private static string[] GetMigratedAsmdefReferences( private static string[] GetMigratedLegacyEditorReferences( bool requiresApplicationReference, - bool requiresDomainReference) + bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference) { List references = new() { @@ -762,6 +979,11 @@ private static string[] GetMigratedLegacyEditorReferences( references.Add(CurrentDomainGuidReference); } + if (requiresFirstPartyScreenshotReference) + { + references.Add(CurrentFirstPartyToolsScreenshotGuidReference); + } + return references.ToArray(); } @@ -771,6 +993,7 @@ private static void AddRequiredCurrentAsmdefReferences( bool requiresToolContractsReference, bool requiresApplicationReference, bool requiresDomainReference, + bool requiresFirstPartyScreenshotReference, ref int replacementCount) { Debug.Assert(references != null, "references must not be null"); @@ -802,6 +1025,15 @@ private static void AddRequiredCurrentAsmdefReferences( CurrentDomainGuidReference, ref replacementCount); } + + if (requiresFirstPartyScreenshotReference) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentFirstPartyToolsScreenshotGuidReference, + ref replacementCount); + } } private static void AddRequiredCurrentAsmdefReference( @@ -860,6 +1092,14 @@ private static string GetCurrentAsmdefReferenceKey(string reference) return CurrentDomainAssemblyName; } + if (IsCurrentAsmdefReference( + reference, + CurrentFirstPartyToolsScreenshotAssemblyName, + CurrentFirstPartyToolsScreenshotGuidReference)) + { + return CurrentFirstPartyToolsScreenshotAssemblyName; + } + return reference; } @@ -1334,6 +1574,104 @@ private static string ReplaceLegacyDomainTypeNamesInCode( return migratedContent; } + private static string ReplaceLegacyApplicationTypeNamesInCode( + string source, + string[] aliases, + bool canMigrateBareLegacyApplicationApi, + 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 ApplicationTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentApplicationNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentApplicationNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyApplicationApi && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentApplicationNamespace}.{rule.CurrentName}" + : match.Value, + ref replacementCount); + } + + return migratedContent; + } + + private static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( + string source, + string[] aliases, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + 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 FirstPartyScreenshotTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + fullyQualifiedRegex, + _ => $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyFirstPartyScreenshotApi && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentFirstPartyToolsNamespace}.{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"); @@ -1762,16 +2100,16 @@ private static string GetNamedArgumentValueOrNull(string argument, string argume return value.Length == 0 ? null : value; } - private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + private static (string Content, int ReplacementCount) ReplaceLegacyTimerDelayNamedArgumentsInCode( string source, string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) + 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 = LegacyEditorWindowCaptureUtilityCaptureWindowRegex.Matches(source); + MatchCollection matches = LegacyTimerDelayInvocationRegex.Matches(source); StringBuilder builder = new(source.Length); int sourceCopyIndex = 0; int replacementCount = 0; @@ -1779,10 +2117,7 @@ private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowC { if (match.Index < sourceCopyIndex || !codeTextMask.IsCodeAt(match.Index) || - !IsLegacyEditorWindowCaptureUtilityCallMatch( - match, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility)) + !IsLegacyTimerDelayInvocationMatch(match, legacyNamespaceAliases, canMigrateBareLegacyTimerDelay)) { continue; } @@ -1800,23 +2135,17 @@ private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowC string argumentsSource = source.Substring( openParenthesisIndex + 1, closingParenthesisIndex - openParenthesisIndex - 1); - string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityArguments( - SplitAttributeArguments(argumentsSource)); - if (migratedArguments.Length == 0) + (string[] migratedArguments, bool changed) = + GetTimerDelayArgumentsWithMigratedCancellationTokenName(SplitAttributeArguments(argumentsSource)); + if (!changed) { continue; } builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append("(await "); - builder.Append(CurrentFirstPartyToolsNamespace); - builder.Append('.'); - builder.Append(LegacyEditorWindowCaptureUtilityTypeName); - builder.Append('.'); - builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); - builder.Append('('); + builder.Append(match.Value); builder.Append(string.Join(", ", migratedArguments)); - builder.Append(")).texture"); + builder.Append(')'); sourceCopyIndex = closingParenthesisIndex + 1; replacementCount++; } @@ -1830,10 +2159,10 @@ private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowC return (builder.ToString(), replacementCount); } - private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( + private static bool IsLegacyTimerDelayInvocationMatch( Match match, string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) + bool canMigrateBareLegacyTimerDelay) { Debug.Assert(match != null, "match must not be null"); Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); @@ -1848,37 +2177,480 @@ private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( return legacyNamespaceAliases.Contains(match.Groups["alias"].Value); } - return match.Groups["editorWindowCaptureUtility"].Success && - canMigrateBareLegacyEditorWindowCaptureUtility; + return match.Groups["timerDelay"].Success && canMigrateBareLegacyTimerDelay; } - private static string[] GetMigratedEditorWindowCaptureUtilityArguments(string[] arguments) + private static (string[] Arguments, bool Changed) + GetTimerDelayArgumentsWithMigratedCancellationTokenName(string[] arguments) { Debug.Assert(arguments != null, "arguments must not be null"); - string[] trimmedArguments = arguments - .Select(argument => argument.Trim()) - .Where(argument => argument.Length > 0) - .ToArray(); - if (trimmedArguments.Length != 3) + string[] migratedArguments = new string[arguments.Length]; + bool changed = false; + for (int i = 0; i < arguments.Length; i++) { - return Array.Empty(); + string argument = arguments[i].Trim(); + string cancellationTokenValue = + GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (cancellationTokenValue == null) + { + migratedArguments[i] = argument; + continue; + } + + migratedArguments[i] = $"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"; + changed = true; } - return new[] - { - trimmedArguments[0], - trimmedArguments[1], - $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", - trimmedArguments[2] - }; + return (migratedArguments, changed); } - private static bool ShouldMigrateLegacyToolInfoConstructorArguments( - Match match, - string[] arguments, - bool canMigrateAmbiguousBareLegacyToolInfoConstructor) - { + private static (string Content, int ReplacementCount) ReplaceLegacyMainThreadSwitcherCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher) + { + 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 = 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( + match, + legacyNamespaceAliases, + canMigrateBareLegacyMainThreadSwitcher)) + { + 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); + } + + private static bool IsLegacyMainThreadSwitcherCallMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher) + { + 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["mainThreadSwitcher"].Success && canMigrateBareLegacyMainThreadSwitcher; + } + + private 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; + foreach (string argument in trimmedArguments) + { + string timingValue = GetNamedArgumentValueOrNull(argument, LegacyTimingArgumentName); + if (timingValue != null) + { + if (IsLegacyPlayerLoopTimingArgument(timingValue, legacyNamespaceAliases)) + { + changed = true; + continue; + } + } + + if (IsLegacyPlayerLoopTimingArgument(argument, legacyNamespaceAliases)) + { + changed = true; + continue; + } + + string cancellationTokenValue = + GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); + if (cancellationTokenValue != null) + { + migratedArguments.Add($"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"); + changed = true; + continue; + } + + migratedArguments.Add(argument); + } + + return (migratedArguments.ToArray(), changed); + } + + private 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; + } + + private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); + + (string captureWindowContent, int captureWindowReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( + source, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility); + (string captureGameRenderingContent, int captureGameRenderingReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( + captureWindowContent, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility); + int deconstructionReplacementCount = 0; + string migratedContent = AddDiscardToCaptureGameRenderingDeconstructionsInCode( + captureGameRenderingContent, + ref deconstructionReplacementCount); + return ( + migratedContent, + captureWindowReplacementCount + captureGameRenderingReplacementCount + deconstructionReplacementCount); + } + + private static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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 = 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, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + 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)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append("(await "); + builder.Append(CurrentFirstPartyToolsNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); + builder.Append('('); + builder.Append(string.Join(", ", migratedArguments)); + builder.Append(")).texture"); + sourceCopyIndex = closingParenthesisIndex + 1; + replacementCount++; + } + + if (replacementCount == 0) + { + return (source, 0); + } + + builder.Append(source, sourceCopyIndex, source.Length - sourceCopyIndex); + return (builder.ToString(), replacementCount); + } + + private static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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 = 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, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + 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)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append("await "); + builder.Append(CurrentFirstPartyToolsNamespace); + builder.Append('.'); + builder.Append(LegacyEditorWindowCaptureUtilityTypeName); + builder.Append('.'); + builder.Append(EditorWindowCaptureUtilityCaptureGameRenderingMethodName); + 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); + } + + private static string AddDiscardToCaptureGameRenderingDeconstructionsInCode( + string source, + ref int replacementCount) + { + Debug.Assert(source != null, "source 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; + } + + 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(); + } + + private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( + Match match, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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["editorWindowCaptureUtility"].Success && + canMigrateBareLegacyEditorWindowCaptureUtility; + } + + private static string[] GetMigratedEditorWindowCaptureUtilityArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] trimmedArguments = arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + if (trimmedArguments.Length != 3) + { + return Array.Empty(); + } + + return new[] + { + trimmedArguments[0], + trimmedArguments[1], + $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", + trimmedArguments[2] + }; + } + + private static string[] GetMigratedEditorWindowCaptureUtilityGameRenderingArguments(string[] arguments) + { + Debug.Assert(arguments != null, "arguments must not be null"); + + string[] trimmedArguments = arguments + .Select(argument => argument.Trim()) + .Where(argument => argument.Length > 0) + .ToArray(); + if (trimmedArguments.Length == 3) + { + return trimmedArguments; + } + + if (trimmedArguments.Length != 2) + { + return Array.Empty(); + } + + return new[] + { + trimmedArguments[0], + $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", + trimmedArguments[1] + }; + } + + 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"); @@ -2379,7 +3151,36 @@ private static bool ContainsLegacyEditorWindowCaptureUtilityCall( Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); CodeTextMask codeTextMask = CodeTextMask.Create(source); - MatchCollection matches = LegacyEditorWindowCaptureUtilityCaptureWindowRegex.Matches(source); + if (ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureWindowRegex, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + return true; + } + + return ContainsLegacyEditorWindowCaptureUtilityCall( + source, + codeTextMask, + LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility); + } + + private static bool ContainsLegacyEditorWindowCaptureUtilityCall( + string source, + CodeTextMask codeTextMask, + Regex regex, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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"); + + MatchCollection matches = regex.Matches(source); foreach (Match match in matches) { if (codeTextMask.IsCodeAt(match.Index) && @@ -2395,6 +3196,192 @@ private static bool ContainsLegacyEditorWindowCaptureUtilityCall( return false; } + private 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; + } + + private static bool ContainsLegacyApplicationReference( + string source, + bool canMigrateBareLegacyApplicationApi, + 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 (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + 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 && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + if (ContainsLegacyMainThreadSwitcherSwitchCall( + source, + aliases, + canMigrateBareLegacyApplicationApi)) + { + return true; + } + + return false; + } + + private static bool ContainsLegacyMainThreadSwitcherSwitchCall( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyMainThreadSwitcher) + { + 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 = LegacyMainThreadSwitcherSwitchRegex.Matches(source); + foreach (Match match in matches) + { + if (codeTextMask.IsCodeAt(match.Index) && + IsLegacyMainThreadSwitcherCallMatch( + match, + legacyNamespaceAliases, + canMigrateBareLegacyMainThreadSwitcher)) + { + return true; + } + } + + return false; + } + + private static bool ContainsCurrentApplicationReference( + string source, + bool canUseBareCurrentApplicationType) + { + Debug.Assert(source != null, "source must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + if (canUseBareCurrentApplicationType && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) + { + return true; + } + } + + return false; + } + + private static bool ContainsLegacyFirstPartyScreenshotReference( + string source, + bool canMigrateBareLegacyFirstPartyScreenshotApi, + 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 (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + 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 && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) + { + return true; + } + } + + return false; + } + + private static bool ContainsCurrentFirstPartyScreenshotReference( + string source, + bool canUseBareCurrentFirstPartyScreenshotType) + { + Debug.Assert(source != null, "source must not be null"); + + CodeTextMask codeTextMask = CodeTextMask.Create(source); + foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) + { + Regex fullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", + RegexOptions.Compiled); + if (RegexMatchesCode(source, fullyQualifiedRegex)) + { + return true; + } + + if (canUseBareCurrentFirstPartyScreenshotType && + ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) + { + return true; + } + } + + return false; + } + private static bool ContainsLegacyAliasQualifiedName(string source, string alias, string typeName) { Debug.Assert(source != null, "source must not be null"); diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index 644408b2b5..9cb7209b52 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -594,7 +594,9 @@ internal static bool ShouldApplyMigrationProgress( internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult result) { - return !result.Changed; + Debug.Assert(result.FileCount >= 0, "result file count must not be negative"); + + return false; } private void CompleteMigrationPreview(CancellationToken ct) 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(); } From 10c599157b01298ae6988a6b8ab0a14d70f7967f Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 19:14:08 +0900 Subject: [PATCH 05/13] Run custom tool migration asynchronously Move the migration button path onto the async migration port so project scanning and file writes no longer block the Unity editor thread. Reuse the wizard progress bar during apply, prevent duplicate actions while migration is running, and keep AssetDatabase.Refresh on the main thread after writes finish. --- ...ThirdPartyToolMigrationFileServiceTests.cs | 65 ++++++++++++ ...hirdPartyToolMigrationWizardWindowTests.cs | 27 ++++- .../ThirdPartyToolMigrationUseCase.cs | 11 +++ .../Domain/ThirdPartyToolMigrationData.cs | 6 +- .../ThirdPartyToolMigrationFileService.cs | 37 +++++++ .../ThirdPartyToolMigrationWizardWindow.cs | 99 +++++++++++++------ 6 files changed, 208 insertions(+), 37 deletions(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index cbeeaf2c1a..a6015cf200 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -2588,6 +2588,71 @@ public sealed class HelloResponse : BaseToolResponse } } + [Test] + public async Task ApplyMigrationAsync_WhenProjectContainsManyFiles_ReportsProgressAndWritesChanges() + { + // 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; + +[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"" + ] +}"); + + List reports = new(); + RecordingMigrationProgress progress = new(reports); + ThirdPartyToolMigrationFileService service = new(); + + 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 + { + Directory.Delete(projectRoot, recursive: true); + } + } + private static string CreateProjectRoot() { string projectRoot = Path.Combine( diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index 1b9bf10e5d..e79fdc9023 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,13 +130,13 @@ 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)); } 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/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/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs index d42460b901..442262f9ab 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileService.cs @@ -144,6 +144,43 @@ public ThirdPartyToolMigrationResult ApplyMigration(string projectRoot) plan.ChangedFilePaths.ToArray()); } + 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"); + + string normalizedProjectRoot = NormalizeProjectRoot(projectRoot); + InvalidatePreviewCache(); + MigrationPlan plan = await CreateMigrationPlanAsync(normalizedProjectRoot, progress, ct); + if (ct.IsCancellationRequested) + { + return new ThirdPartyToolMigrationResult(0, 0, Array.Empty()); + } + + for (int index = 0; index < plan.Changes.Count; index++) + { + if (ct.IsCancellationRequested) + { + return new ThirdPartyToolMigrationResult(0, 0, Array.Empty()); + } + + MigrationFileChange change = plan.Changes[index]; + WriteMigrationFile(change.FilePath, change.Content); + if ((index + 1) % PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + + return new ThirdPartyToolMigrationResult( + plan.ChangedFilePaths.Count, + plan.ReplacementCount, + plan.ChangedFilePaths.ToArray()); + } + internal void InvalidatePreviewCache() { lock (_previewCacheLock) diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index 9cb7209b52..aeaa9483fb 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -20,6 +20,7 @@ public class ThirdPartyToolMigrationWizardWindow : EditorWindow 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 MigrationApplyingText = "Migrating project files for V3 custom tools..."; private const string NoMigrationTargetsText = "No V3 custom tool migration is needed."; private const string MigrationButtonReadyText = "Migrate"; private const string MigrationButtonMigratingText = "Migrating..."; @@ -43,7 +44,7 @@ public class ThirdPartyToolMigrationWizardWindow : EditorWindow [SerializeField] private bool _shouldRefreshAfterCreateGui; private IVisualElementScheduledItem _resizeScheduledItem; - private CancellationTokenSource _migrationPreviewCts; + private CancellationTokenSource _migrationOperationCts; private ThirdPartyToolMigrationUseCase _thirdPartyToolMigrationUseCase; internal static void InitializeEditorServices(UnityCliLoopEditorSessionStateService sessionStateService) @@ -119,15 +120,18 @@ internal static string GetMigrationStatusText(int fileCount) "The errors should disappear after migration."; } - internal static string GetMigrationProgressText(ThirdPartyToolMigrationProgress progress) + internal static string GetMigrationProgressText( + ThirdPartyToolMigrationProgress progress, + bool isMigrating) { + string statusText = isMigrating ? MigrationApplyingText : MigrationCheckingText; if (progress.TotalItemCount <= 0) { - return MigrationCheckingText; + return statusText; } - return $"{MigrationCheckingText}\n" + - $"{progress.ProcessedItemCount}/{progress.TotalItemCount} checks complete."; + return $"{statusText}\n" + + $"{progress.ProcessedItemCount}/{progress.TotalItemCount} steps complete."; } internal static string GetMigrationButtonText( @@ -186,7 +190,7 @@ private void InitializeApplicationServices() private void OnDisable() { _resizeScheduledItem?.Pause(); - CancelMigrationPreview(); + CancelMigrationOperation(); } private void BuildLayout() @@ -300,13 +304,13 @@ 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); + new ThirdPartyToolMigrationProgressReporter(this, ct); ThirdPartyToolMigrationPreview preview = await Task.Run(async () => await _thirdPartyToolMigrationUseCase.PreviewMigrationAsync(projectRoot, progress, ct)); @@ -321,7 +325,7 @@ await Task.Run(async () => return; } - CompleteMigrationPreview(ct); + CompleteMigrationOperation(ct); if (!preview.HasTargets) { @@ -332,24 +336,49 @@ await Task.Run(async () => ShowMigrationTargetsState(preview.FileCount); } - private void HandleMigrateThirdPartyTools() + private async void HandleMigrateThirdPartyTools() { + CancellationToken ct = BeginMigrationOperation(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); ThirdPartyToolMigrationResult result = default; _isMigrating = true; ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); + await Task.Yield(); try { - result = _thirdPartyToolMigrationUseCase.ApplyMigration(projectRoot); + System.IProgress progress = + new ThirdPartyToolMigrationProgressReporter(this, ct); + result = await Task.Run(async () => + await _thirdPartyToolMigrationUseCase.ApplyMigrationAsync(projectRoot, progress, ct)); + if (ct.IsCancellationRequested) + { + return; + } + + await MainThreadSwitcher.SwitchToMainThread(); + if (ct.IsCancellationRequested) + { + return; + } + + CompleteMigrationOperation(ct); if (result.Changed) { + ShowCheckingState(new ThirdPartyToolMigrationProgress(1, 1)); + await Task.Yield(); AssetDatabase.Refresh(); } } finally { _isMigrating = false; + CompleteMigrationOperation(ct); + } + + if (ct.IsCancellationRequested) + { + return; } if (ShouldRefreshAfterMigration(result)) @@ -367,6 +396,8 @@ private void ShowNotCheckedState() ViewDataBinder.SetVisible(_migrationProgressBar, false); _migrateButton.SetEnabled(false); _migrateButton.text = GetMigrationButtonText(_isMigrating, false, false); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); ScheduleResizeToContent(); } @@ -376,6 +407,8 @@ private void ShowMigrationTargetsState(int fileCount) ViewDataBinder.SetVisible(_migrationProgressBar, false); _migrateButton.SetEnabled(!_isMigrating); _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); ScheduleResizeToContent(); } @@ -385,16 +418,20 @@ private void ShowNoMigrationTargetsState() ViewDataBinder.SetVisible(_migrationProgressBar, false); _migrateButton.SetEnabled(false); _migrateButton.text = GetMigrationButtonText(_isMigrating, false, true); + _refreshButton.SetEnabled(true); + _closeButton.SetEnabled(true); ScheduleResizeToContent(); } private void ShowCheckingState(ThirdPartyToolMigrationProgress progress) { - _migrationStatusLabel.text = GetMigrationProgressText(progress); + _migrationStatusLabel.text = GetMigrationProgressText(progress, _isMigrating); ViewDataBinder.SetVisible(_migrationProgressBar, true); UpdateMigrationProgressBar(progress); _migrateButton.SetEnabled(false); _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); + _refreshButton.SetEnabled(false); + _closeButton.SetEnabled(!_isMigrating); ScheduleResizeToContent(); } @@ -483,24 +520,24 @@ private void UpdateMigrationProgressBar(ThirdPartyToolMigrationProgress progress _migrationProgressBar.value = processedItemCount; } - private CancellationToken BeginMigrationPreview() + private CancellationToken BeginMigrationOperation() { - CancelMigrationPreview(); + CancelMigrationOperation(); CancellationTokenSource cts = new(); - _migrationPreviewCts = cts; + _migrationOperationCts = cts; return cts.Token; } - private void CancelMigrationPreview() + private void CancelMigrationOperation() { - if (_migrationPreviewCts == null) + if (_migrationOperationCts == null) { return; } - _migrationPreviewCts.Cancel(); - _migrationPreviewCts.Dispose(); - _migrationPreviewCts = null; + _migrationOperationCts.Cancel(); + _migrationOperationCts.Dispose(); + _migrationOperationCts = null; } private static UnityCliLoopEditorSessionStateService GetSessionStateService() @@ -587,9 +624,9 @@ internal static bool ShouldReportMigrationProgress( internal static bool ShouldApplyMigrationProgress( bool isCancellationRequested, - bool hasActivePreview) + bool hasActiveOperation) { - return !isCancellationRequested && hasActivePreview; + return !isCancellationRequested && hasActiveOperation; } internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult result) @@ -599,30 +636,30 @@ internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult r return false; } - private void CompleteMigrationPreview(CancellationToken ct) + private void CompleteMigrationOperation(CancellationToken ct) { - if (!IsMigrationPreviewActive(ct)) + if (!IsMigrationOperationActive(ct)) { return; } - _migrationPreviewCts.Dispose(); - _migrationPreviewCts = null; + _migrationOperationCts.Dispose(); + _migrationOperationCts = null; } - private bool IsMigrationPreviewActive(CancellationToken ct) + private bool IsMigrationOperationActive(CancellationToken ct) { - return _migrationPreviewCts != null && _migrationPreviewCts.Token.Equals(ct); + return _migrationOperationCts != null && _migrationOperationCts.Token.Equals(ct); } - private sealed class ThirdPartyToolMigrationPreviewProgress + private sealed class ThirdPartyToolMigrationProgressReporter : System.IProgress { private readonly ThirdPartyToolMigrationWizardWindow _window; private readonly CancellationToken _ct; private long _lastReportTimestamp; - public ThirdPartyToolMigrationPreviewProgress( + public ThirdPartyToolMigrationProgressReporter( ThirdPartyToolMigrationWizardWindow window, CancellationToken ct) { @@ -659,7 +696,7 @@ private async Task ReportAsync(ThirdPartyToolMigrationProgress value, Cancellati await MainThreadSwitcher.SwitchToMainThread(); if (!ShouldApplyMigrationProgress( ct.IsCancellationRequested, - _window.IsMigrationPreviewActive(ct))) + _window.IsMigrationOperationActive(ct))) { return; } From fd070d8fe63c1919abda1a55651cbdc6f5b249fb Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 22:46:15 +0900 Subject: [PATCH 06/13] Repair local UIElementInfo migration conflicts Prefer project-declared DTO names over first-party screenshot DTO rewrites during custom tool migration. Repair already-qualified local UIElementInfo references so rerunning migration fixes files broken by earlier passes. --- ...ThirdPartyToolMigrationFileServiceTests.cs | 58 ++++++++++ .../ThirdPartyToolMigrationRulesTests.cs | 102 ++++++++++++++++++ .../ThirdPartyToolMigrationRules.cs | 31 +++++- 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index a6015cf200..04aaa52837 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -453,6 +453,64 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } } + [Test] + public void ApplyMigration_WhenLocalUIElementInfoWasAlreadyQualified_RepairsSource() + { + // Verifies that project-wide migration repairs local DTO references qualified by an older migration pass. + 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 io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class CurrentElementTool : UnityCliLoopTool +{ + private List Classify() + { + List elements = new(); + elements.Add(CreateElementInfo()); + return elements; + } + + private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateElementInfo() + { + 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 Classify()")); + Assert.That(migratedSource, Does.Contain("private UIElementInfo CreateElementInfo()")); + Assert.That(migratedSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + [Test] public void ApplyMigration_WhenCurrentRegistrarExistsWithToolContractsAsmdefReference_AddsApplicationReference() { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 4994541520..f2a872fb79 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -457,6 +457,108 @@ public sealed class ScreenshotHelper "io.github.hatayama.UnityCliLoop.FirstPartyTools.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.FirstPartyTools.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.FirstPartyTools.UIElementInfo")); + } + + [Test] + public void MigrateCSharpSource_WhenLocalUIElementInfoWasAlreadyQualified_RepairsLocalReferences() + { + // Verifies that rerunning migration repairs local DTO references qualified by an older migration pass. + string source = @"using System.Collections.Generic; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +public sealed class LocalTool : UnityCliLoopTool +{ + private List Classify() + { + List elements = new(); + elements.Add(CreateElementInfo()); + return elements; + } + + private io.github.hatayama.UnityCliLoop.FirstPartyTools.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("private List Classify()")); + Assert.That(result.Content, Does.Contain("List elements = new();")); + Assert.That(result.Content, Does.Contain("private UIElementInfo CreateElementInfo()")); + Assert.That(result.Content, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + } + [Test] public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsUsed_RewritesSignatureAndDiscard() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index 236e3d04b9..16cf989477 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -1635,6 +1635,19 @@ private static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( string migratedContent = source; foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) { + bool hasLocalTypeDeclaration = DeclaresLocalType(migratedContent, rule.LegacyName); + if (hasLocalTypeDeclaration) + { + Regex currentFullyQualifiedRegex = new( + $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", + RegexOptions.Compiled); + migratedContent = ReplaceRegexInCode( + migratedContent, + currentFullyQualifiedRegex, + _ => rule.CurrentName, + ref replacementCount); + } + Regex fullyQualifiedRegex = new( $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", RegexOptions.Compiled); @@ -1663,6 +1676,7 @@ private static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( migratedContent, unqualifiedRegex, match => canMigrateBareLegacyFirstPartyScreenshotApi && + !hasLocalTypeDeclaration && ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) ? $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}" : match.Value, @@ -3329,6 +3343,7 @@ private static bool ContainsLegacyFirstPartyScreenshotReference( CodeTextMask codeTextMask = CodeTextMask.Create(source); foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) { + bool hasLocalTypeDeclaration = DeclaresLocalType(source, rule.LegacyName); Regex fullyQualifiedRegex = new( $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", RegexOptions.Compiled); @@ -3346,6 +3361,7 @@ private static bool ContainsLegacyFirstPartyScreenshotReference( } if (canMigrateBareLegacyFirstPartyScreenshotApi && + !hasLocalTypeDeclaration && ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) { return true; @@ -3364,15 +3380,17 @@ private static bool ContainsCurrentFirstPartyScreenshotReference( CodeTextMask codeTextMask = CodeTextMask.Create(source); foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) { + bool hasLocalTypeDeclaration = DeclaresLocalType(source, rule.CurrentName); Regex fullyQualifiedRegex = new( $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", RegexOptions.Compiled); - if (RegexMatchesCode(source, fullyQualifiedRegex)) + if (!hasLocalTypeDeclaration && RegexMatchesCode(source, fullyQualifiedRegex)) { return true; } if (canUseBareCurrentFirstPartyScreenshotType && + !hasLocalTypeDeclaration && ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) { return true; @@ -3591,6 +3609,17 @@ private static bool ContainsLegacyAssemblyScopedTypeName( return false; } + private static bool DeclaresLocalType(string source, string typeName) + { + Debug.Assert(source != null, "source must not be null"); + Debug.Assert(!string.IsNullOrEmpty(typeName), "typeName must not be null or empty"); + + Regex localTypeDeclarationRegex = new( + $@"\b(?:class|struct|interface|enum|record(?:\s+(?:class|struct))?)\s+{Regex.Escape(typeName)}\b", + RegexOptions.Compiled); + return RegexMatchesCode(source, localTypeDeclarationRegex); + } + private static bool IsLegacyAssemblyScopedTypeDeclaration(string source, int typeNameIndex) { Debug.Assert(source != null, "source must not be null"); From 91da3b1d1781ba68bca983dc0bf95167a5fdd5a3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 23:11:15 +0900 Subject: [PATCH 07/13] Fix screenshot capture migration edge cases Keep migrated screenshot capture calls compilable for named cancellation tokens, qualified legacy calls, and ConfigureAwait suffixes. --- .../ThirdPartyToolMigrationRulesTests.cs | 97 +++++++++++++++ .../ThirdPartyToolMigrationRules.cs | 110 ++++++++++++++++-- 2 files changed, 196 insertions(+), 11 deletions(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index f2a872fb79..7b97993ee5 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -418,6 +418,103 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Content, Does.Not.Contain("return await 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_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.FirstPartyTools.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_WhenLegacyScreenshotHelpersAreUsed_RewritesFirstPartyReferences() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index 16cf989477..adbaa75d41 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -2447,12 +2447,16 @@ private static (string Content, int ReplacementCount) openParenthesisIndex + 1, closingParenthesisIndex - openParenthesisIndex - 1); string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityArguments( - SplitAttributeArguments(argumentsSource)); + SplitAttributeArguments(argumentsSource), + GetMigratedEditorFrameWaitTimeoutExpression(match)); if (migratedArguments.Length == 0) { continue; } + (string configureAwaitSuffix, int replacementEndIndex) = + ReadOptionalConfigureAwaitSuffix(source, codeTextMask, closingParenthesisIndex + 1); + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); builder.Append("(await "); builder.Append(CurrentFirstPartyToolsNamespace); @@ -2462,8 +2466,10 @@ private static (string Content, int ReplacementCount) builder.Append(EditorWindowCaptureUtilityCaptureWindowMethodName); builder.Append('('); builder.Append(string.Join(", ", migratedArguments)); - builder.Append(")).texture"); - sourceCopyIndex = closingParenthesisIndex + 1; + builder.Append(')'); + builder.Append(configureAwaitSuffix); + builder.Append(").texture"); + sourceCopyIndex = replacementEndIndex; replacementCount++; } @@ -2516,7 +2522,8 @@ private static (string Content, int ReplacementCount) openParenthesisIndex + 1, closingParenthesisIndex - openParenthesisIndex - 1); string[] migratedArguments = GetMigratedEditorWindowCaptureUtilityGameRenderingArguments( - SplitAttributeArguments(argumentsSource)); + SplitAttributeArguments(argumentsSource), + GetMigratedEditorFrameWaitTimeoutExpression(match)); if (migratedArguments.Length == 0) { continue; @@ -2612,9 +2619,69 @@ private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( canMigrateBareLegacyEditorWindowCaptureUtility; } - private static string[] GetMigratedEditorWindowCaptureUtilityArguments(string[] arguments) + private 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); + } + + private 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 = arguments .Select(argument => argument.Trim()) @@ -2629,14 +2696,17 @@ private static string[] GetMigratedEditorWindowCaptureUtilityArguments(string[] { trimmedArguments[0], trimmedArguments[1], - $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", - trimmedArguments[2] + timeoutExpression, + GetArgumentWithMigratedCancellationTokenName(trimmedArguments[2]) }; } - private static string[] GetMigratedEditorWindowCaptureUtilityGameRenderingArguments(string[] arguments) + private 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 = arguments .Select(argument => argument.Trim()) @@ -2644,7 +2714,12 @@ private static string[] GetMigratedEditorWindowCaptureUtilityGameRenderingArgume .ToArray(); if (trimmedArguments.Length == 3) { - return trimmedArguments; + return new[] + { + trimmedArguments[0], + trimmedArguments[1], + GetArgumentWithMigratedCancellationTokenName(trimmedArguments[2]) + }; } if (trimmedArguments.Length != 2) @@ -2655,11 +2730,24 @@ private static string[] GetMigratedEditorWindowCaptureUtilityGameRenderingArgume return new[] { trimmedArguments[0], - $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}", - trimmedArguments[1] + timeoutExpression, + GetArgumentWithMigratedCancellationTokenName(trimmedArguments[1]) }; } + private 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}"; + } + private static bool ShouldMigrateLegacyToolInfoConstructorArguments( Match match, string[] arguments, From 904d3405702e5b431830350795eee307a86ae3e4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 19 Jun 2026 23:22:49 +0900 Subject: [PATCH 08/13] Handle capture window task migration forms Keep ignored awaited CaptureWindowAsync statements valid and map non-awaited legacy capture tasks back to texture task results. --- .../ThirdPartyToolMigrationRulesTests.cs | 57 +++++++ .../ThirdPartyToolMigrationRules.cs | 144 +++++++++++++++++- 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 7b97993ee5..6489d6347d 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -515,6 +515,63 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.FirstPartyTools.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.FirstPartyTools.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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); + } + [Test] public void MigrateCSharpSource_WhenLegacyScreenshotHelpersAreUsed_RewritesFirstPartyReferences() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs index adbaa75d41..cced8b2293 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRules.cs @@ -205,6 +205,11 @@ internal static class ThirdPartyToolMigrationRules $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", RegexOptions.Compiled); + private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex = + new( + $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + RegexOptions.Compiled); + private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex = new( $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", @@ -2393,9 +2398,14 @@ private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowC source, legacyNamespaceAliases, canMigrateBareLegacyEditorWindowCaptureUtility); + (string captureWindowTaskContent, int captureWindowTaskReplacementCount) = + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( + captureWindowContent, + legacyNamespaceAliases, + canMigrateBareLegacyEditorWindowCaptureUtility); (string captureGameRenderingContent, int captureGameRenderingReplacementCount) = ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( - captureWindowContent, + captureWindowTaskContent, legacyNamespaceAliases, canMigrateBareLegacyEditorWindowCaptureUtility); int deconstructionReplacementCount = 0; @@ -2404,7 +2414,10 @@ private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowC ref deconstructionReplacementCount); return ( migratedContent, - captureWindowReplacementCount + captureGameRenderingReplacementCount + deconstructionReplacementCount); + captureWindowReplacementCount + + captureWindowTaskReplacementCount + + captureGameRenderingReplacementCount + + deconstructionReplacementCount); } private static (string Content, int ReplacementCount) @@ -2456,9 +2469,14 @@ private static (string Content, int ReplacementCount) (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("(await "); + builder.Append(shouldExtractTexture ? "(await " : "await "); builder.Append(CurrentFirstPartyToolsNamespace); builder.Append('.'); builder.Append(LegacyEditorWindowCaptureUtilityTypeName); @@ -2468,7 +2486,11 @@ private static (string Content, int ReplacementCount) builder.Append(string.Join(", ", migratedArguments)); builder.Append(')'); builder.Append(configureAwaitSuffix); - builder.Append(").texture"); + if (shouldExtractTexture) + { + builder.Append(").texture"); + } + sourceCopyIndex = replacementEndIndex; replacementCount++; } @@ -2482,6 +2504,77 @@ private static (string Content, int ReplacementCount) return (builder.ToString(), replacementCount); } + private static (string Content, int ReplacementCount) + ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( + string source, + string[] legacyNamespaceAliases, + bool canMigrateBareLegacyEditorWindowCaptureUtility) + { + 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 = 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, + canMigrateBareLegacyEditorWindowCaptureUtility)) + { + 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), + GetMigratedEditorFrameWaitTimeoutExpression(match)); + if (migratedArguments.Length == 0) + { + continue; + } + + builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); + builder.Append(CurrentFirstPartyToolsNamespace); + 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); + } + private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( string source, @@ -2619,6 +2712,31 @@ private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( canMigrateBareLegacyEditorWindowCaptureUtility; } + private 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; + } + + char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, awaitIndex); + return previousCharacter == '='; + } + private static (string ConfigureAwaitSuffix, int EndIndex) ReadOptionalConfigureAwaitSuffix( string source, CodeTextMask codeTextMask, @@ -3741,6 +3859,24 @@ private static string ReadPreviousIdentifier(string source, int startIndex) return source.Substring(index + 1, identifierEndIndex - index - 1); } + private 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; + } + private static char ReadNextNonWhitespaceCharacter(string source, int startIndex) { Debug.Assert(source != null, "source must not be null"); From fb0c7e98d5e5e160c691797b671db6aa29e8a1a6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 20 Jun 2026 09:20:45 +0900 Subject: [PATCH 09/13] Refactor third-party migration responsibilities Split the migration rule and file-service logic by responsibility so each extracted class stays within a reviewable size while preserving the migration behavior covered by the existing upgrade tests. --- ...ThirdPartyToolMigrationFileServiceTests.cs | 2690 ++++++++-- .../ThirdPartyToolMigrationRulesTests.cs | 2105 +++++++- ...hirdPartyToolMigrationWizardWindowTests.cs | 36 + .../ThirdPartyToolMigrationAliasRules.cs | 162 + .../ThirdPartyToolMigrationAliasRules.cs.meta | 11 + ...hirdPartyToolMigrationApiDetectionRules.cs | 396 ++ ...artyToolMigrationApiDetectionRules.cs.meta | 11 + ...yToolMigrationApplicationDetectionRules.cs | 217 + ...MigrationApplicationDetectionRules.cs.meta | 11 + .../ThirdPartyToolMigrationArgumentRules.cs | 302 ++ ...irdPartyToolMigrationArgumentRules.cs.meta | 11 + ...ationAsmdefMigrationRequirementResolver.cs | 82 + ...AsmdefMigrationRequirementResolver.cs.meta | 11 + .../ThirdPartyToolMigrationAsmdefRules.cs | 323 ++ ...ThirdPartyToolMigrationAsmdefRules.cs.meta | 11 + ...yToolMigrationAssemblyReferenceResolver.cs | 497 ++ ...MigrationAssemblyReferenceResolver.cs.meta | 11 + ...PartyToolMigrationAssemblyScopedNameMap.cs | 176 + ...ToolMigrationAssemblyScopedNameMap.cs.meta | 11 + ...PartyToolMigrationAssemblyUsageAnalyzer.cs | 318 ++ ...ToolMigrationAssemblyUsageAnalyzer.cs.meta | 11 + ...ToolMigrationAssemblyUsageAsyncAnalyzer.cs | 374 ++ ...igrationAssemblyUsageAsyncAnalyzer.cs.meta | 11 + .../ThirdPartyToolMigrationAttributeRules.cs | 171 + ...rdPartyToolMigrationAttributeRules.cs.meta | 11 + .../ThirdPartyToolMigrationCSharpRules.cs | 324 ++ ...ThirdPartyToolMigrationCSharpRules.cs.meta | 11 + ...rdPartyToolMigrationCodeTextMaskBuilder.cs | 297 + ...tyToolMigrationCodeTextMaskBuilder.cs.meta | 11 + ...MigrationCodeTextMaskInterpolationRules.cs | 350 ++ ...tionCodeTextMaskInterpolationRules.cs.meta | 11 + ...tyToolMigrationConstructorArgumentRules.cs | 166 + ...lMigrationConstructorArgumentRules.cs.meta | 11 + ...igrationCrossFileTimingMigrationPlanner.cs | 353 ++ ...ionCrossFileTimingMigrationPlanner.cs.meta | 11 + .../ThirdPartyToolMigrationDelayRules.cs | 273 + .../ThirdPartyToolMigrationDelayRules.cs.meta | 11 + .../ThirdPartyToolMigrationDetectionRules.cs | 372 ++ ...rdPartyToolMigrationDetectionRules.cs.meta | 11 + ...dPartyToolMigrationDomainDetectionRules.cs | 273 + ...yToolMigrationDomainDetectionRules.cs.meta | 11 + ...ThirdPartyToolMigrationEditorDelayRules.cs | 314 ++ ...PartyToolMigrationEditorDelayRules.cs.meta | 11 + ...grationFastAssemblyRequirementCollector.cs | 429 ++ ...onFastAssemblyRequirementCollector.cs.meta | 11 + ...tyToolMigrationFastSourceTargetDetector.cs | 126 + ...lMigrationFastSourceTargetDetector.cs.meta | 11 + .../ThirdPartyToolMigrationFileService.cs | 2220 +------- ...dPartyToolMigrationFileServiceConstants.cs | 16 + ...yToolMigrationFileServiceConstants.cs.meta | 11 + .../ThirdPartyToolMigrationFileWriter.cs | 46 + .../ThirdPartyToolMigrationFileWriter.cs.meta | 11 + ...tyToolMigrationMetadataConstructorRules.cs | 219 + ...lMigrationMetadataConstructorRules.cs.meta | 11 + .../ThirdPartyToolMigrationModels.cs | 231 + .../ThirdPartyToolMigrationModels.cs.meta | 11 + .../ThirdPartyToolMigrationParsingRules.cs | 473 ++ ...hirdPartyToolMigrationParsingRules.cs.meta | 11 + .../ThirdPartyToolMigrationPlanBuilder.cs | 377 ++ ...ThirdPartyToolMigrationPlanBuilder.cs.meta | 11 + ...dPartyToolMigrationProjectFileInventory.cs | 256 + ...yToolMigrationProjectFileInventory.cs.meta | 11 + ...hirdPartyToolMigrationRegexRewriteRules.cs | 184 + ...artyToolMigrationRegexRewriteRules.cs.meta | 11 + .../ThirdPartyToolMigrationRuleCatalog.cs | 350 ++ ...ThirdPartyToolMigrationRuleCatalog.cs.meta | 11 + .../ThirdPartyToolMigrationRules.cs | 4759 +---------------- ...rtyToolMigrationScreenshotArgumentRules.cs | 398 ++ ...olMigrationScreenshotArgumentRules.cs.meta | 11 + ...lMigrationScreenshotDeconstructionRules.cs | 371 ++ ...ationScreenshotDeconstructionRules.cs.meta | 11 + ...tyToolMigrationScreenshotDetectionRules.cs | 399 ++ ...lMigrationScreenshotDetectionRules.cs.meta | 11 + .../ThirdPartyToolMigrationScreenshotRules.cs | 466 ++ ...dPartyToolMigrationScreenshotRules.cs.meta | 11 + .../ThirdPartyToolMigrationSourceFileCache.cs | 42 + ...dPartyToolMigrationSourceFileCache.cs.meta | 11 + .../ThirdPartyToolMigrationTargetScanner.cs | 299 ++ ...irdPartyToolMigrationTargetScanner.cs.meta | 11 + ...rdPartyToolMigrationTimingArgumentRules.cs | 365 ++ ...tyToolMigrationTimingArgumentRules.cs.meta | 11 + ...hirdPartyToolMigrationTimingCallerRules.cs | 218 + ...artyToolMigrationTimingCallerRules.cs.meta | 11 + ...irdPartyToolMigrationTimingCleanupRules.cs | 295 + ...rtyToolMigrationTimingCleanupRules.cs.meta | 11 + ...artyToolMigrationTimingDeclarationRules.cs | 171 + ...oolMigrationTimingDeclarationRules.cs.meta | 11 + ...PartyToolMigrationTimingInvocationRules.cs | 391 ++ ...ToolMigrationTimingInvocationRules.cs.meta | 11 + ...PartyToolMigrationTimingMethodBodyRules.cs | 265 + ...ToolMigrationTimingMethodBodyRules.cs.meta | 11 + ...olMigrationTimingMethodDeclarationRules.cs | 410 ++ ...rationTimingMethodDeclarationRules.cs.meta | 11 + ...rdPartyToolMigrationTimingTypeNameRules.cs | 231 + ...tyToolMigrationTimingTypeNameRules.cs.meta | 11 + ...yToolMigrationTimingTypeResolutionRules.cs | 347 ++ ...MigrationTimingTypeResolutionRules.cs.meta | 11 + ...dPartyToolMigrationTimingTypeScopeRules.cs | 241 + ...yToolMigrationTimingTypeScopeRules.cs.meta | 11 + ...ToolMigrationToolContractDetectionRules.cs | 140 + ...igrationToolContractDetectionRules.cs.meta | 11 + ...dPartyToolMigrationTypeReplacementRules.cs | 353 ++ ...yToolMigrationTypeReplacementRules.cs.meta | 11 + .../ThirdPartyToolMigrationWizardWindow.cs | 38 +- 104 files changed, 18988 insertions(+), 7248 deletions(-) create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAliasRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApiDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationApplicationDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMigrationRequirementResolver.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyScopedNameMap.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAsyncAnalyzer.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAttributeRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskBuilder.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCodeTextMaskInterpolationRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationConstructorArgumentRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCrossFileTimingMigrationPlanner.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDelayRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDomainDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationDomainDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationEditorDelayRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationEditorDelayRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileServiceConstants.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFileWriter.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPlanBuilder.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationProjectFileInventory.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRegexRewriteRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotArgumentRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDeconstructionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationScreenshotRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationSourceFileCache.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingArgumentRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCallerRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCallerRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCleanupRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingCleanupRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingDeclarationRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingInvocationRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodBodyRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingMethodDeclarationRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeNameRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeResolutionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTimingTypeScopeRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationToolContractDetectionRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs.meta diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index 04aaa52837..73d4681610 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -454,9 +454,9 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public void ApplyMigration_WhenLocalUIElementInfoWasAlreadyQualified_RepairsSource() + public void ApplyMigration_WhenLocalUIElementInfoWasAlreadyQualified_PreservesExplicitFirstPartyReference() { - // Verifies that project-wide migration repairs local DTO references qualified by an older migration pass. + // Verifies that project-wide migration keeps explicit first-party DTO references. string projectRoot = CreateProjectRoot(); try { @@ -464,18 +464,32 @@ public void ApplyMigration_WhenLocalUIElementInfoWasAlreadyQualified_RepairsSour 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 { - private List Classify() + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + private List BuildFirstPartyElements() { List elements = new(); - elements.Add(CreateElementInfo()); + elements.Add(CreateFirstPartyElement()); return elements; } - private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateElementInfo() + private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo(); + } + + private UIElementInfo CreateProjectElement() { return new UIElementInfo(); } @@ -500,10 +514,59 @@ public sealed class UIElementInfo Assert.That(preview.FileCount, Is.EqualTo(1)); Assert.That(result.FileCount, Is.EqualTo(1)); - Assert.That(migratedSource, Does.Contain("private List Classify()")); - Assert.That(migratedSource, Does.Contain("private UIElementInfo CreateElementInfo()")); - Assert.That(migratedSource, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + Assert.That(migratedSource, Does.Contain( + "private List BuildFirstPartyElements()")); + Assert.That(migratedSource, Does.Contain( + "private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement()")); + Assert.That(migratedSource, Does.Contain( + "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + Assert.That(migratedSource, Does.Contain("private UIElementInfo CreateProjectElement()")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenQualifiedFirstPartyDtoSharesLocalName_AddsScreenshotReference() + { + // Verifies that unambiguous first-party DTO references still add their 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.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.FirstPartyTools.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:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1120,34 +1183,42 @@ public sealed class MainThreadResponse : UnityCliLoopToolResponse } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles() + public void ApplyMigration_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsApplicationReference() { - // Verifies that assembly-level migration does not rename unrelated project types. + // Verifies that regular legacy usings add the Application 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 unrelatedPath = Path.Combine(toolDirectory, "BaseToolSchema.cs"); + string toolPath = Path.Combine(toolDirectory, "MainThreadTool.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(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); + string migratedSource = File.ReadAllText(toolPath); + string migratedAsmdef = File.ReadAllText(asmdefPath); - 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")); + 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1156,48 +1227,40 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles } [Test] - public void ApplyMigration_WhenAssemblyUsesFileScopedLegacyUsing_KeepsUnrelatedBareTypes() + public void ApplyMigration_WhenLegacyUsingUsesBareFirstPartyScreenshotDto_AddsScreenshotReference() { - // Verifies that file-scoped imports do not grant legacy type context to sibling files. + // Verifies that regular legacy usings that migrate screenshot DTOs also repair asmdef references. 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, "ElementTool.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] -public sealed class HelloTool : AbstractUnityTool -{ -} - -public sealed class HelloSchema : BaseToolSchema -{ -} - -public sealed class HelloResponse : BaseToolResponse +public sealed class ElementTool { + public UIElementInfo CreateElement() + { + return new UIElementInfo(); + } }"); - File.WriteAllText(unrelatedPath, unrelatedSource); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""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(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")); + 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.FirstPartyTools.UIElementInfo")); + Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1206,34 +1269,42 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSplitContractFiles() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_AddsApplicationReference() { - // Verifies that collection-shaped legacy type references migrate with their assembly. + // Verifies that split files relying on a current Application global using get the required asmdef reference. 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 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( - listPath, - "public sealed class ToolList { public System.Collections.Generic.List Tools; }"); + 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(listPath), Does.Contain( - "System.Collections.Generic.List")); + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(File.ReadAllText(mainThreadPath), Does.Contain( + "await MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1242,21 +1313,28 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSpli } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomainHelpers() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_RewritesBareMainThreadSwitcherTiming() { - // Verifies that split Domain helper files receive source and asmdef migration through global usings. + // 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 helperPath = Path.Combine(toolDirectory, "ToolHelper.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( - helperPath, - "public static class ToolHelper { public static ServiceResult Create() => null; }"); + 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"": [ @@ -1266,12 +1344,12 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomain ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(mainThreadPath); - 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")); + 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 { @@ -1280,21 +1358,28 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomain } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturnContractTypes() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalAlias_RewritesQualifiedMainThreadSwitcherTiming() { - // Verifies that helper return types relying on global legacy usings migrate with their assembly. + // 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 io.github.hatayama.uLoopMCP;"); - File.WriteAllText( - responseFactoryPath, - "public static class ResponseFactory { public static 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"": [ @@ -1304,10 +1389,12 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturn ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + string migratedSource = File.ReadAllText(mainThreadPath); - 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")); + Assert.That(result.FilePaths.Contains(mainThreadPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "await App.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); } finally { @@ -1316,34 +1403,49 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturn } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareToolAttributes() + public void ApplyMigration_WhenLegacyTimingWrapperCallerIsSplit_RewritesCallerArguments() { - // Verifies that attribute-only files relying on global legacy usings migrate with their assembly. + // 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 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"" - ] + 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 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); + } }"); 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(toolAttributePath), Does.Contain("[UnityCliLoopTool]")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + 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 { @@ -1352,37 +1454,63 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareTo } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedFiles() + public void ApplyMigration_WhenLegacyTimingWrapperCallerIsInAnotherAssembly_RewritesCallerArguments() { - // Verifies that global namespace aliases provide legacy 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 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, @"{ + 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(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")); + 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 { @@ -1391,84 +1519,96 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQ } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedContractTypes() + public void ApplyMigration_WhenUnrelatedAssemblyHasSameTimingWrapper_KeepsUnreachableCallerArguments() { - // Verifies that alias-qualified legacy contract types migrate in sibling files. + // Verifies that cross-file timing rewrites do not cross asmdef boundaries without a reference. string projectRoot = CreateProjectRoot(); try { + 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); - 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"" - ] -}"); + 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; - ThirdPartyToolMigrationFileService service = new(); - ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); +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; - 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); - } - } +public sealed class MainThreadCaller +{ + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) + { + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText(unrelatedWrapperPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; - [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 sealed class MainThreadWrapper +{ + public Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) + { + return Task.CompletedTask; + } +}"); + File.WriteAllText(unrelatedCallerPath, @"using System.Threading; +using System.Threading.Tasks; +using io.github.hatayama.uLoopMCP; -public static class ToolMetadataProvider +public sealed class MainThreadCaller { - public static LegacyToolInfo Create(ToolParameterSchema schema) + public Task CallAsync(MainThreadWrapper wrapper, CancellationToken ct) { - return new LegacyToolInfo(""hello"", ""description"", schema); + return wrapper.RunAsync(PlayerLoopTiming.Update, ct); } }"); - File.WriteAllText(asmdefPath, @"{ + 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(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")); + 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 { @@ -1477,36 +1617,67 @@ public static LegacyToolInfo Create(ToolParameterSchema schema) } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrelatedBareTypes() + public void ApplyMigration_WhenCrossFileTimingWrapperCallChain_RevisitsStaleOuterParameter() { - // Verifies that ToolInfo type aliases do not grant full legacy namespace context to sibling files. + // Verifies that cross-file wrapper rewrites also remove newly stale timing parameters. 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"" - ] + 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; + +public sealed class InnerWrapper +{ + public async Task InnerAsync(PlayerLoopTiming loop, CancellationToken ct) + { + await MainThreadSwitcher.SwitchToMainThread(loop, ct); + } +}"); + 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(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(globalUsingPath), Does.Contain( - "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.Domain.ToolInfo;")); - Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + 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 { @@ -1515,42 +1686,66 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrela } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderAsmref_RewritesReferencedAsmdef() + public void ApplyMigration_WhenAssemblyUsesCurrentToolContractsGlobalUsing_RewritesSplitPartialHelpers() { - // Verifies that asmref folders mark the referenced asmdef as the migrated assembly. + // Verifies that split files relying on a current ToolContracts global using finish partial migration. 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 toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + 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; -[McpTool] -public sealed class HelloTool : AbstractUnityTool +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(asmrefPath, @"{ - ""reference"": ""VendorTools.Editor"" }"); File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""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(toolPath), Does.Contain("UnityCliLoopTool")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.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:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1559,48 +1754,44 @@ public sealed class HelloTool : AbstractUnityTool } [Test] - public void ApplyMigration_WhenNestedAsmrefOverridesAncestorAsmdef_RewritesReferencedAsmdef() + public void ApplyMigration_WhenCurrentFirstPartyToolsCaptureNeedsTimeout_AddsToolContractsReference() { - // Verifies that nested asmref folders use their referenced assembly instead of an ancestor asmdef. + // Verifies that migrated capture timeout constants have a direct ToolContracts asmdef reference. 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 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; -[McpTool] -public sealed class HelloTool : AbstractUnityTool +public sealed class CurrentScreenshotTool { + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } }"); - File.WriteAllText(asmrefPath, @"{ - ""reference"": ""VendorTools.Editor"" -}"); - File.WriteAllText(targetAsmdefPath, @"{ + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" - ] + ""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(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")); + 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.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1609,25 +1800,48 @@ public sealed class HelloTool : AbstractUnityTool } [Test] - public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSplitContractFiles() + public void ApplyMigration_WhenAssemblyUsesCurrentFirstPartyToolsGlobalUsing_RewritesSplitCapture() { - // Verifies that predefined assemblies get the same assembly-level migration as asmdef assemblies. + // Verifies that split files relying on a current FirstPartyTools global using finish capture 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, "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(2)); - Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(schemaPath), Does.Contain("UnityCliLoopToolSchema")); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.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.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1636,40 +1850,50 @@ public void ApplyMigration_WhenNoAsmdefAssemblyUsesGlobalLegacyUsing_RewritesSpl } [Test] - public void ApplyMigration_WhenNoAsmdefCurrentToolContractsFileUsesLegacyScreenshotCapture_RewritesSource() + public void ApplyMigration_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_RewritesQualifiedSplitCapture() { - // Verifies that predefined editor assemblies finish partially migrated screenshot helpers. + // Verifies that split files relying on a current FirstPartyTools global alias finish capture migration. string projectRoot = CreateProjectRoot(); try { - string toolDirectory = Path.Combine(projectRoot, "Assets", "Editor", "CustomTools"); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); Directory.CreateDirectory(toolDirectory); - string toolPath = Path.Combine(toolDirectory, "CaptureTool.cs"); + 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 Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;"); 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 sealed class CurrentScreenshotTool { public async Task CaptureAsync(EditorWindow window, CancellationToken ct) { - Texture2D texture = await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); - return texture; + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } }"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" + ] +}"); 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(result.FilePaths.Contains(toolPath), Is.True); Assert.That(migratedSource, Does.Contain( "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); - Assert.That(migratedSource, Does.Not.Contain("await 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 { @@ -1678,28 +1902,31 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken } [Test] - public void ApplyMigration_WhenNoAsmdefEditorAssemblyUsesGlobalLegacyUsing_KeepsRuntimeFiles() + public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_AddsScreenshotReference() { - // Verifies that predefined editor migration does not rewrite the separate runtime assembly. + // Verifies that a current FirstPartyTools global using by itself still gets the defining 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); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + 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"": [] +}"); ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); + 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.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( + "global using io.github.hatayama.UnityCliLoop.FirstPartyTools;")); } finally { @@ -1708,29 +1935,31 @@ public void ApplyMigration_WhenNoAsmdefEditorAssemblyUsesGlobalLegacyUsing_Keeps } [Test] - public void ApplyMigration_WhenNoAsmdefFirstPassRuntimeUsesGlobalLegacyUsing_KeepsRegularRuntimeFiles() + public void ApplyMigration_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_AddsScreenshotReference() { - // Verifies that Unity predefined firstpass runtime scripts do not migrate regular runtime siblings. + // Verifies that a current FirstPartyTools namespace alias by itself still gets the defining asmdef reference. 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); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + 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 = service.ApplyMigration(projectRoot); + 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.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(aliasUsingPath), Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;")); } finally { @@ -1739,29 +1968,34 @@ public void ApplyMigration_WhenNoAsmdefFirstPassRuntimeUsesGlobalLegacyUsing_Kee } [Test] - public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_KeepsRegularEditorFiles() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_KeepsUnrelatedFiles() { - // Verifies that Unity predefined firstpass editor scripts do not migrate regular editor siblings. + // Verifies that assembly-level migration does not rename unrelated project types. 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; }"; + 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(editorPath, editorSource); + 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(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(globalUsingPath), Does.Contain("io.github.hatayama.UnityCliLoop.ToolContracts")); - Assert.That(File.ReadAllText(editorPath), Is.EqualTo(editorSource)); + Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); } finally { @@ -1770,25 +2004,107 @@ public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_Keep } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsingAndSplitManualRegistration_AddsApplicationReference() + public void ApplyMigration_WhenAssemblyUsesFileScopedLegacyUsing_KeepsUnrelatedBareTypes() { - // Verifies that manual registration files relying on assembly-level legacy detection get required refs. + // Verifies that file-scoped imports do not grant legacy type context to sibling files. + 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 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] +public sealed class HelloTool : AbstractUnityTool +{ +} + +public sealed class HelloSchema : BaseToolSchema +{ +} + +public sealed class HelloResponse : BaseToolResponse +{ +}"); + 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(toolPath), Does.Contain("UnityCliLoopTool")); + Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesGenericSplitContractFiles() + { + // Verifies that collection-shaped legacy type references 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 registrationPath = Path.Combine(toolDirectory, "ManualToolRegistration.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(registrationPath, @"public static class ManualToolRegistration -{ - public static void Register(IUnityTool tool) - { - CustomToolManager.RegisterCustomTool(tool); - } + File.WriteAllText( + listPath, + "public sealed class ToolList { public System.Collections.Generic.List Tools; }"); + 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(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"": [ @@ -1800,11 +2116,10 @@ public static void Register(IUnityTool tool) ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); 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(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:214998e563c124e8a88199b2dd1f522d")); - Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:5c4588558a3624eacbce0f50007cf1eb")); } finally { @@ -1813,32 +2128,34 @@ public static void Register(IUnityTool tool) } [Test] - public void PreviewMigration_WhenUnrelatedCustomToolManagerExists_KeepsAsmdef() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitReturnContractTypes() { - // Verifies that project-owned type names do not trigger migration dependencies by themselves. + // 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 sourcePath = Path.Combine(toolDirectory, "CustomToolManager.cs"); + string globalUsingPath = Path.Combine(toolDirectory, "GlobalUsings.cs"); + string responseFactoryPath = Path.Combine(toolDirectory, "ResponseFactory.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.uLoopMCP;"); + File.WriteAllText( + responseFactoryPath, + "public static class ResponseFactory { public static BaseToolResponse Create() => null; }"); + File.WriteAllText(asmdefPath, @"{ ""name"": ""VendorTools.Editor"", - ""references"": [] -}"; - File.WriteAllText(sourcePath, source); - File.WriteAllText(asmdefPath, asmdefSource); + ""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(0)); - Assert.That(File.ReadAllText(sourcePath), Is.EqualTo(source)); - Assert.That(File.ReadAllText(asmdefPath), Is.EqualTo(asmdefSource)); + 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 { @@ -1847,33 +2164,34 @@ public void PreviewMigration_WhenUnrelatedCustomToolManagerExists_KeepsAsmdef() } [Test] - public void ApplyMigration_WhenUserSidecarFilesExist_PreservesSidecarFiles() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitBareToolAttributes() { - // Verifies that project-wide source rewrites do not treat user sidecars as migration scratch files. + // 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 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 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"" + ] }"); - 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")); + 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 { @@ -1882,22 +2200,1007 @@ public sealed class HelloTool : AbstractUnityTool } [Test] - public void PreviewMigration_WhenLegacyToolExistsUnderExcludedDirectory_IgnoresFile() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyAlias_RewritesSplitAliasQualifiedFiles() { - // Verifies that generated Unity folders are not scanned for third-party tools. + // Verifies that global namespace aliases provide legacy context to sibling files. string projectRoot = CreateProjectRoot(); try { - string generatedDirectory = Path.Combine(projectRoot, "Library"); - Directory.CreateDirectory(generatedDirectory); + 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( - Path.Combine(generatedDirectory, "GeneratedTool.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class GeneratedTool {}"); + 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(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); + 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.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")); + } + 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.Domain.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(2)); + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotSchema")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotResponse")); + Assert.That(migratedToolSource, Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + Assert.That(File.ReadAllText(dtoPath), Does.Not.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools")); + Assert.That(File.ReadAllText(asmdefPath), Does.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.FirstPartyTools.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_AddsApplicationReference() + { + // Verifies that manual registration files relying on assembly-level legacy detection get required 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.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")); + } + 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_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(preview.HasTargets, Is.False); + Assert.That(hasTargets, Is.True); } finally { @@ -1906,22 +3209,43 @@ public void PreviewMigration_WhenLegacyToolExistsUnderExcludedDirectory_IgnoresF } [Test] - public void PreviewMigration_WhenLegacyToolExistsOutsideAssets_IgnoresFile() + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentApplicationGlobalAlias_ReturnsTrue() { - // Verifies that repository tooling outside Unity source roots is not migrated. + // Verifies that preview detection treats current Application global aliases as assembly-scoped. string projectRoot = CreateProjectRoot(); try { - string toolsDirectory = Path.Combine(projectRoot, "tools"); - Directory.CreateDirectory(toolsDirectory); + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); File.WriteAllText( - Path.Combine(toolsDirectory, "LegacyToolFixture.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class LegacyToolFixture {}"); + 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(); - ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); - Assert.That(preview.HasTargets, Is.False); + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); } finally { @@ -1930,43 +3254,46 @@ public void PreviewMigration_WhenLegacyToolExistsOutsideAssets_IgnoresFile() } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderAssetsPackagesDirectory_RewritesAssetsFiles() + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_ReturnsTrue() { - // Verifies that only Unity's project-root Packages directory is excluded from migration scans. + // Verifies that preview detection treats current FirstPartyTools global aliases as assembly-scoped. 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 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( + 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 HelloResponse : BaseToolResponse +public sealed class CurrentScreenshotTool { + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } }"); - File.WriteAllText(asmdefPath, @"{ + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" ] }"); 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")); + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); } finally { @@ -1975,51 +3302,52 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public void ApplyMigration_WhenLegacyToolExistsUnderPackagesDirectory_KeepsPackageFiles() + public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentToolContractsGlobalUsingAndSplitPartialHelpers_ReturnsTrue() { - // Verifies that Package Manager and embedded package contents are not rewritten in place. + // Verifies that startup detection treats current ToolContracts global usings as assembly-scoped. 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; + 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; -[McpTool] -public sealed class PackageTool : AbstractUnityTool +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 PackageToolSchema : BaseToolSchema +public sealed class PartialSchema : UnityCliLoopToolSchema { } -public sealed class PackageToolResponse : BaseToolResponse +public sealed class PartialResponse : UnityCliLoopToolResponse { -}"; - string asmdefSource = @"{ - ""name"": ""LegacyPackageTool.Editor"", +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"), + @"{ + ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] -}"; - 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)); + bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); + + Assert.That(hasTargets, Is.True); } finally { @@ -2028,17 +3356,38 @@ public sealed class PackageToolResponse : BaseToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderAssets_ReturnsTrue() + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsGlobalUsingOnlyNeedsHelperRewrite_ReturnsTrue() { - // Verifies that startup detection can find V2 custom tools without building a migration preview. + // 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, "HelloTool.cs"), - "using io.github.hatayama.uLoopMCP; [McpTool] public sealed class HelloTool {}"); + 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(); @@ -2053,20 +3402,48 @@ public async Task HasMigrationTargetsAsync_WhenLegacyToolExistsUnderAssets_Retur } [Test] - public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets_ReturnsTrue() + public async Task HasMigrationTargetsAsync_WhenAssemblyDeclaresMainThreadSwitcherForCurrentToolContractsFile_ReturnsFalse() { - // Verifies that startup detection catches old assembly names without relying on source files. + // 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"": [ - ""uLoopMCP.Editor"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] }"); @@ -2074,7 +3451,7 @@ public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(hasTargets, Is.False); } finally { @@ -2083,28 +3460,30 @@ public async Task HasMigrationTargetsAsync_WhenLegacyAsmdefNameExistsUnderAssets } [Test] - public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsExistsWithLegacyAsmdefGuid_ReturnsTrue() + public async Task HasMigrationTargetsAsync_WhenCurrentSplitScreenshotDtosDoNotUseScreenshotApis_ReturnsFalse() { - // Verifies that startup detection catches partially migrated tools that still need asmdef repair. + // 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, "CurrentHelloTool.cs"), + Path.Combine(toolDirectory, "ATool.cs"), @"using io.github.hatayama.UnityCliLoop.ToolContracts; -[UnityCliLoopTool] -public sealed class CurrentHelloTool : UnityCliLoopTool +public sealed class CurrentScreenshotTool : UnityCliLoopTool { -} +}"); + File.WriteAllText( + Path.Combine(toolDirectory, "ZDtos.cs"), + @"using io.github.hatayama.UnityCliLoop.ToolContracts; -public sealed class HelloSchema : UnityCliLoopToolSchema +public sealed class ScreenshotSchema : UnityCliLoopToolSchema { } -public sealed class HelloResponse : UnityCliLoopToolResponse +public sealed class ScreenshotResponse : UnityCliLoopToolResponse { }"); File.WriteAllText( @@ -2112,7 +3491,7 @@ public sealed class HelloResponse : UnityCliLoopToolResponse @"{ ""name"": ""VendorTools.Editor"", ""references"": [ - ""GUID:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"" ] }"); @@ -2120,7 +3499,7 @@ public sealed class HelloResponse : UnityCliLoopToolResponse bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - Assert.That(hasTargets, Is.True); + Assert.That(hasTargets, Is.False); } finally { @@ -2129,40 +3508,41 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenUnrelatedAsmdefJsonIsMalformed_StillDetectsAsmdefRepair() + public async Task HasMigrationTargetsAsync_WhenCurrentRenderingCaptureNeedsDiscard_ReturnsTrue() { - // Verifies that unrelated malformed asmdefs do not block startup detection for repairable tools. + // Verifies that startup detection reports current rendering capture deconstruction rewrites. 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 -{ -} + 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 HelloResponse : UnityCliLoopToolResponse +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:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" ] }"); @@ -2179,32 +3559,89 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenAssemblyUsesCurrentDomainGlobalUsingAndSplitMetadata_ReturnsTrue() + public async Task HasMigrationTargetsAsync_WhenCurrentToolContractsFileUsesLegacyScreenshotCaptureWithoutAsmdef_ReturnsTrue() { - // Verifies that startup detection treats current Domain global usings as assembly-scoped. + // 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, "GlobalUsings.cs"), - "global using io.github.hatayama.UnityCliLoop.Domain;"); + 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, "ToolMetadataProvider.cs"), - @"public static class ToolMetadataProvider + 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 static ToolInfo[] GetTools() + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) { - return new ToolInfo[0]; + 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:214998e563c124e8a88199b2dd1f522d"" + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" ] }"); @@ -2711,6 +4148,331 @@ public sealed class HelloResponse : BaseToolResponse } } + [Test] + public async Task ApplyMigrationAsync_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsApplicationReference() + { + // Verifies that async migration also adds Application 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, "MainThreadTool.cs"); + string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); + 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"": [] +}"); + + ThirdPartyToolMigrationFileService service = new(); + 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task ApplyMigrationAsync_WhenAssemblyUsesCurrentApplicationGlobalAlias_RewritesQualifiedMainThreadSwitcherTiming() + { + // Verifies that async migration uses current Application global aliases from sibling files. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + 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 MainThreadUsage +{ + public async Task RunAsync(CancellationToken ct) + { + await App.MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:214998e563c124e8a88199b2dd1f522d"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedSource = File.ReadAllText(mainThreadPath); + + Assert.That(result.FilePaths.Contains(mainThreadPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "await App.MainThreadSwitcher.SwitchToMainThread(ct);")); + Assert.That(migratedSource, Does.Not.Contain("PlayerLoopTiming")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_AddsScreenshotReference() + { + // Verifies that async migration adds the screenshot asmdef reference for current FirstPartyTools global using. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + 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"": [] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedAsmdef = File.ReadAllText(asmdefPath); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + } + finally + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task ApplyMigrationAsync_WhenAssemblyUsesCurrentFirstPartyToolsGlobalAlias_RewritesQualifiedSplitCapture() + { + // 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 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 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 async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await Fpt.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } +}"); + File.WriteAllText(asmdefPath, @"{ + ""name"": ""VendorTools.Editor"", + ""references"": [ + ""GUID:fc3fd32eddbee40e39c2d76dc184957b"", + ""GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"" + ] +}"); + + ThirdPartyToolMigrationFileService service = new(); + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync( + projectRoot, + new Progress(), + CancellationToken.None); + string migratedSource = File.ReadAllText(toolPath); + + Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FilePaths.Contains(toolPath), Is.True); + Assert.That(migratedSource, Does.Contain( + "io.github.hatayama.UnityCliLoop.FirstPartyTools.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 + { + Directory.Delete(projectRoot, recursive: true); + } + } + + [Test] + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_AddsScreenshotReference() + { + // Verifies that async migration adds the screenshot asmdef reference for current FirstPartyTools alias using. + string projectRoot = CreateProjectRoot(); + try + { + string toolDirectory = Path.Combine(projectRoot, "Assets", "VendorTools"); + Directory.CreateDirectory(toolDirectory); + 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(1)); + Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); + Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(aliasUsingPath), Does.Contain( + "using Fpt = io.github.hatayama.UnityCliLoop.FirstPartyTools;")); + } + 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; + +[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"" + ] +}"); + + using CancellationTokenSource cts = new(); + CancelOnCompleteMigrationProgress progress = new(cts); + ThirdPartyToolMigrationFileService service = new(); + + ThirdPartyToolMigrationResult result = + await service.ApplyMigrationAsync(projectRoot, progress, cts.Token); + + 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 + { + Directory.Delete(projectRoot, recursive: true); + } + } + private static string CreateProjectRoot() { string projectRoot = Path.Combine( @@ -2740,5 +4502,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 6489d6347d..2f4daf5a21 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -305,6 +305,39 @@ public async Task CaptureAsync(CancellationToken ct) 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() { @@ -324,8 +357,14 @@ public async Task CaptureAsync(CancellationToken ct) 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( @@ -382,8 +421,14 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken 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( @@ -391,6 +436,90 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken 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.FirstPartyTools.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.FirstPartyTools.EditorWindowCaptureUtility")); + } + [Test] public void MigrateCSharpSource_WhenCurrentToolContractsFileHasLegacyEditorWindowCaptureUtility_RewritesCaptureCall() { @@ -418,6 +547,122 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken 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.FirstPartyTools.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.FirstPartyTools.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.FirstPartyTools.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 still 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( + "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + Assert.That(result.Content, Does.Contain( + "=> (await io.github.hatayama.UnityCliLoop.FirstPartyTools.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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + } + [Test] public void MigrateCSharpSource_WhenLegacyCaptureUsesNamedCancellationToken_RewritesArgumentName() { @@ -454,6 +699,48 @@ public async Task CaptureRenderingAsync(CancellationToken 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() { @@ -669,22 +956,36 @@ public sealed class UIElementInfo } [Test] - public void MigrateCSharpSource_WhenLocalUIElementInfoWasAlreadyQualified_RepairsLocalReferences() + public void MigrateCSharpSource_WhenFileDeclaresLocalUIElementInfoAndUsesExplicitFirstPartyInfo_PreservesExplicitReference() { - // Verifies that rerunning migration repairs local DTO references qualified by an older migration pass. + // 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 { - private List Classify() + public async Task CaptureAsync(EditorWindow window, CancellationToken ct) + { + return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + } + + private List BuildFirstPartyElements() { List elements = new(); - elements.Add(CreateElementInfo()); + elements.Add(CreateFirstPartyElement()); return elements; } - private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateElementInfo() + private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + { + return new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo(); + } + + private UIElementInfo CreateProjectElement() { return new UIElementInfo(); } @@ -706,17 +1007,21 @@ public sealed class UIElementInfo ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("private List Classify()")); - Assert.That(result.Content, Does.Contain("List elements = new();")); - Assert.That(result.Content, Does.Contain("private UIElementInfo CreateElementInfo()")); - Assert.That(result.Content, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + 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.FirstPartyTools.UIElementInfo CreateFirstPartyElement()")); + Assert.That(result.Content, Does.Contain( + "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + Assert.That(result.Content, Does.Contain("private UIElementInfo CreateProjectElement()")); } [Test] - public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsUsed_RewritesSignatureAndDiscard() + public void MigrateCSharpSource_WhenLegacyGameRenderingCaptureIsUsed_ProjectsLegacyTuple() { - // Verifies that old rendering capture deconstruction keeps compiling after the V3 timeout result. + // Verifies that old rendering capture deconstruction keeps the legacy two-item tuple shape. string source = @"using System.Threading; using System.Threading.Tasks; using UnityEngine; @@ -736,55 +1041,237 @@ public async Task CaptureAsync(CancellationToken ct) ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("(texture, yOffset, _) = await")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)")); + "(texture, yOffset) = await io.github.hatayama.UnityCliLoop.FirstPartyTools.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_WhenLegacyTimerDelayNamedCancellationTokenIsUsed_RewritesArgumentName() + public void MigrateCSharpSource_WhenCurrentGameRenderingCaptureIsUsed_KeepsCurrentTuple() { - // Verifies that TimerDelay named arguments keep compiling after the V3 cancellation token rename. + // Verifies that already-current rendering captures keep the V3 three-item tuple shape. string source = @"using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.uLoopMCP; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; -public sealed class DelayTool +public sealed class RenderingCapture { - public async Task RunAsync(CancellationToken ct) + public async Task CaptureAsync(CancellationToken ct) { - await TimerDelay.Wait(10, cancellationToken: ct); - await TimerDelay.WaitThenExecuteOnMainThread(10, () => {}, 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.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:")); + "(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_WhenLegacyMainThreadSwitcherIsUsed_RewritesApplicationReferences() + public void MigrateCSharpSource_WhenCurrentBareGameRenderingCaptureUsesTwoItemDeconstruction_AddsDiscard() { - // Verifies that main-thread switch helpers moved to Application keep compiling after timing arguments were removed. + // Verifies that bare current rendering captures deconstruct the V3 timeout result. string source = @"using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.uLoopMCP; +using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; -public sealed class MainThreadTool +public sealed class RenderingCapture { - public async Task RunAsync(CancellationToken ct) + public async Task CaptureAsync(CancellationToken ct) { - await MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update, ct); - await MainThreadSwitcher.SwitchToMainThread(timing: PlayerLoopTiming.PostLateUpdate, cancellationToken: ct); - SwitchToMainThreadAwaitable awaitable = MainThreadSwitcher.SwitchToMainThread(PlayerLoopTiming.Update); - bool isMainThread = MainThreadSwitcher.IsMainThread; + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync")); + Assert.That(result.Content, Does.Not.Contain("(texture, yOffset) = await")); + } + + [Test] + public void MigrateCSharpSource_WhenCurrentRenderingCaptureUsesFirstPartyAlias_AddsDiscard() + { + // Verifies that current FirstPartyTools aliases still receive the V3 rendering timeout 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( + "(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.FirstPartyTools.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.FirstPartyTools.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.FirstPartyTools.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; } }"; @@ -800,8 +1287,1538 @@ public async Task RunAsync(CancellationToken ct) "io.github.hatayama.UnityCliLoop.Application.SwitchToMainThreadAwaitable awaitable = io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread();")); Assert.That(result.Content, Does.Contain( "bool isMainThread = io.github.hatayama.UnityCliLoop.Application.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 namespace aliases also 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( + "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.Application.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.Application.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.Application.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.Application.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.Application.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("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_WhenLegacyTimingWrapperCallChain_RevisitsStaleOuterParameter() + { + // 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; + +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.Application.SwitchToMainThreadAwaitable Switch() =>")); + Assert.That(result.Content, Does.Contain( + "io.github.hatayama.UnityCliLoop.Application.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( + "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_WhenUnrelatedLocalTimingParameterIsStillUsed_PreservesParameter() + { + // 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("public string Format(PlayerLoopTiming loop)")); + Assert.That(result.Content, Does.Contain("return loop.ToString();")); + } + + [Test] + public void MigrateCSharpSource_WhenUnrelatedTimingParameterIsUnused_PreservesSignature() + { + // 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("public void Configure(PlayerLoopTiming loop)")); + } + + [Test] + public void MigrateCSharpSource_WhenUnrelatedTimingParameterMentionsSwitcherInText_PreservesSignature() + { + // 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("public void Configure(PlayerLoopTiming loop)")); + Assert.That(result.Content, Does.Contain("string label = \"SwitchToMainThread\";")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingOverrideUsesSwitcher_PreservesSignature() + { + // 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( + "public override async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingInterfaceImplementationUsesSwitcher_PreservesSignature() + { + // 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; + +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( + "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingVirtualMethodUsesSwitcher_PreservesSignature() + { + // 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 class MainThreadTool +{ + public virtual 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( + "public virtual async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + } + + [Test] + public void MigrateCSharpSource_WhenLegacyTimingExternalInterfaceImplementationUsesSwitcher_PreservesSignature() + { + // 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( + "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); + Assert.That(result.Content, Does.Contain( + "await io.github.hatayama.UnityCliLoop.Application.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] @@ -1621,8 +3638,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")); @@ -1639,8 +3662,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( diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index e79fdc9023..41e22cbf79 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -154,6 +154,42 @@ public void ShouldRefreshAfterMigration_WhenMigrationCompletes_ReturnsFalse(int 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/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..eedf40ecbb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs @@ -0,0 +1,302 @@ +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, "$\"")) + { + 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/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..c255bdf739 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs @@ -0,0 +1,323 @@ +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 + }; + + if (requiresApplicationReference) + { + references.Add(CurrentApplicationGuidReference); + } + + if (requiresDomainReference) + { + references.Add(CurrentDomainGuidReference); + } + + if (requiresFirstPartyScreenshotReference) + { + references.Add(CurrentFirstPartyToolsScreenshotGuidReference); + } + + 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) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentToolContractsGuidReference, + ref replacementCount); + } + + if (requiresApplicationReference) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentApplicationGuidReference, + ref replacementCount); + } + + if (requiresDomainReference) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentDomainGuidReference, + ref replacementCount); + } + + if (requiresFirstPartyScreenshotReference) + { + AddRequiredCurrentAsmdefReference( + references, + addedReferences, + CurrentFirstPartyToolsScreenshotGuidReference, + 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..efab5b36df --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs @@ -0,0 +1,497 @@ +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, + ReadAsmdefGuidReference(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, + ReadAsmdefGuidReference(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 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; + } + + 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) + { + 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); + } + + internal 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); + } + } +} 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..fb31f133b2 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs @@ -0,0 +1,318 @@ +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); + + foreach (string csharpFilePath in csharpFilePaths) + { + string source = File.ReadAllText(csharpFilePath); + 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 = 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(); + } + 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..9b4ef97069 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs @@ -0,0 +1,324 @@ +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); + } + + 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..502db682ee --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs @@ -0,0 +1,429 @@ +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, + 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, + hasAssemblyScopedCurrentApplicationUsing: false, + 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, + 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( + 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); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + CollectFastAssemblyReferenceRequirements( + source, + assemblyDirectory, + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories); + + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + } + + internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirementsAsync( + List csharpFilePaths, + List asmdefDirectories, + List assemblyReferenceDirectories, + string projectRoot, + HashSet assemblyScopedCurrentToolContractsDirectories, + HashSet assemblyScopedCurrentDomainDirectories, + 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( + 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); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + if (!assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory)) + { + continue; + } + + CollectFastAssemblyReferenceRequirements( + source, + assemblyDirectory, + true, + assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), + legacyAssemblyDirectories, + toolContractsReferenceAssemblyDirectories, + applicationReferenceAssemblyDirectories, + domainReferenceAssemblyDirectories); + + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + } + + 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); + 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 % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + } + + 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); + 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; + } + + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + + 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..f76d7e4ebf --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs @@ -0,0 +1,126 @@ +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 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( + 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); + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) + { + continue; + } + + string assemblyDirectory = FindNearestAssemblyDirectory( + csharpFilePath, + asmdefDirectories, + assemblyReferenceDirectories, + projectRoot); + string[] assemblyDeclaredTypeNames = + GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); + bool hasLegacyAssemblySource = + legacyAssemblyDirectories.Contains(assemblyDirectory) && + ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi( + source, + Array.Empty()); + ThirdPartyToolMigrationContentResult result = + ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( + source, + hasLegacyAssemblySource, + assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), + Array.Empty(), + Array.Empty(), + GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), + GetAssemblyScopedNames( + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, + assemblyDirectory), + assemblyDeclaredTypeNames); + if (result.Changed) + { + return true; + } + + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + } + + 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 442262f9ab..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( @@ -154,7 +108,8 @@ public async Task ApplyMigrationAsync( string normalizedProjectRoot = NormalizeProjectRoot(projectRoot); InvalidatePreviewCache(); - MigrationPlan plan = await CreateMigrationPlanAsync(normalizedProjectRoot, progress, ct); + 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 new ThirdPartyToolMigrationResult(0, 0, Array.Empty()); @@ -162,14 +117,9 @@ public async Task ApplyMigrationAsync( for (int index = 0; index < plan.Changes.Count; index++) { - if (ct.IsCancellationRequested) - { - return new ThirdPartyToolMigrationResult(0, 0, Array.Empty()); - } - MigrationFileChange change = plan.Changes[index]; - WriteMigrationFile(change.FilePath, change.Content); - if ((index + 1) % PreviewYieldBatchSize == 0) + ThirdPartyToolMigrationFileWriter.Write(change.FilePath, change.Content); + if ((index + 1) % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) { await Task.Yield(); } @@ -191,446 +141,15 @@ internal void InvalidatePreviewCache() } } - private 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 assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = new(StringComparer.Ordinal); - HashSet toolContractsReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet applicationReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet domainReferenceAssemblyDirectories = new(StringComparer.Ordinal); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = 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); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - } - - CollectFastAssemblyReferenceRequirements( - source, - assemblyDirectory, - assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - legacyAssemblyDirectories, - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); - } - - 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; - } - } - - if (assemblyScopedCurrentFirstPartyToolsDirectories.Count > 0) - { - await CollectFastAssemblyScopedCurrentFirstPartyToolsRequirementsAsync( - inventory.CSharpFilePaths, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot, - assemblyScopedCurrentFirstPartyToolsDirectories, - firstPartyScreenshotReferenceAssemblyDirectories, - ct); - if (ct.IsCancellationRequested) - { - return false; - } - } - - foreach (string asmdefFilePath in inventory.AsmdefFilePaths) - { - if (ct.IsCancellationRequested) - { - return false; - } - - string source = File.ReadAllText(asmdefFilePath); - if (ContainsFastAsmdefMigrationTarget(source)) - { - return true; - } - - inspectedEntryCount++; - if (inspectedEntryCount % 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, - new HashSet(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); - - 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, - bool hasAssemblyScopedCurrentFirstPartyToolsNamespaceUsage, - HashSet legacyAssemblyDirectories, - HashSet toolContractsReferenceAssemblyDirectories, - HashSet applicationReferenceAssemblyDirectories, - HashSet domainReferenceAssemblyDirectories, - HashSet firstPartyScreenshotReferenceAssemblyDirectories) - { - 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"); - Debug.Assert( - firstPartyScreenshotReferenceAssemblyDirectories != null, - "firstPartyScreenshotReferenceAssemblyDirectories 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) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(source)) - { - applicationReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsRegistrarDomainReturnApi(source)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentDomainMetadataApiForAssembly( - source, - hasAssemblyScopedCurrentDomainNamespaceUsage)) - { - domainReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - legacyAssemblyDirectories.Contains(assemblyDirectory) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - hasAssemblyScopedCurrentFirstPartyToolsNamespaceUsage)) - { - firstPartyScreenshotReferenceAssemblyDirectories.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(); - } - } - } - - private static async Task CollectFastAssemblyScopedCurrentFirstPartyToolsRequirementsAsync( - List csharpFilePaths, - List asmdefDirectories, - List assemblyReferenceDirectories, - string projectRoot, - HashSet assemblyScopedCurrentFirstPartyToolsDirectories, - 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( - assemblyScopedCurrentFirstPartyToolsDirectories != null, - "assemblyScopedCurrentFirstPartyToolsDirectories 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; - } - - string source = File.ReadAllText(csharpFilePath); - if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) - { - continue; - } - - string assemblyDirectory = FindNearestAssemblyDirectory( - csharpFilePath, - asmdefDirectories, - assemblyReferenceDirectories, - projectRoot); - if (assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory) && - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly(source, true)) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - - inspectedEntryCount++; - if (inspectedEntryCount % PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } - } - } - - private 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; - } - - private 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; - } - - 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( @@ -665,1715 +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 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); - } - - 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 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); - } - - 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); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = 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); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = 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.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( - source, - ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source)) - { - firstPartyScreenshotReferenceAssemblyDirectories.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) || - ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - 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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - legacyAssemblyAliases) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory))) - { - firstPartyScreenshotReferenceAssemblyDirectories.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, - firstPartyScreenshotReferenceAssemblyDirectories); - } - - 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); - HashSet assemblyScopedCurrentFirstPartyToolsDirectories = 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); - HashSet firstPartyScreenshotReferenceAssemblyDirectories = new(StringComparer.Ordinal); - - foreach (string csharpFilePath in csharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - 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); - - 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.ContainsCurrentFirstPartyToolsGlobalUsing(source)) - { - assemblyScopedCurrentFirstPartyToolsDirectories.Add(assemblyDirectory); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentRegistrarApi(source) || - ThirdPartyToolMigrationRules.ContainsLegacyApplicationApiForAssembly( - source, - ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentApplicationApi(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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - ThirdPartyToolMigrationRules.ContainsLegacyCSharpApi(source) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - Array.Empty()) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApi(source)) - { - firstPartyScreenshotReferenceAssemblyDirectories.Add(assemblyDirectory); - } - } - - foreach (string csharpFilePath in csharpFilePaths) - { - if (ct.IsCancellationRequested) - { - return CreateMigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - assemblyScopedLegacyAliasesByDirectory, - assemblyScopedLegacyToolInfoAliasesByDirectory, - 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(); - } - - 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) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - 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); - } - - if (ThirdPartyToolMigrationRules.ContainsLegacyFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedLegacyDirectories.Contains(assemblyDirectory) || - ThirdPartyToolMigrationRules.ContainsCurrentToolContractsApi(source), - legacyAssemblyAliases) || - ThirdPartyToolMigrationRules.ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory))) - { - firstPartyScreenshotReferenceAssemblyDirectories.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, - firstPartyScreenshotReferenceAssemblyDirectories); - } - - private static MigrationAssemblyUsage CreateMigrationAssemblyUsage( - List asmdefDirectories, - List assemblyReferenceDirectories, - HashSet legacyAssemblyDirectories, - HashSet assemblyScopedLegacyDirectories, - Dictionary> assemblyScopedLegacyAliasesByDirectory, - Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, - 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( - 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"); - Debug.Assert( - firstPartyScreenshotReferenceAssemblyDirectories != null, - "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); - - return new MigrationAssemblyUsage( - asmdefDirectories, - assemblyReferenceDirectories, - legacyAssemblyDirectories, - assemblyScopedLegacyDirectories, - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), - CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), - toolContractsReferenceAssemblyDirectories, - applicationReferenceAssemblyDirectories, - domainReferenceAssemblyDirectories, - firstPartyScreenshotReferenceAssemblyDirectories); - } - - 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, - 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( - 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"); - Debug.Assert( - firstPartyScreenshotReferenceAssemblyDirectories != null, - "firstPartyScreenshotReferenceAssemblyDirectories 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); - FirstPartyScreenshotReferenceAssemblyDirectories = firstPartyScreenshotReferenceAssemblyDirectories ?? - 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; } - public HashSet FirstPartyScreenshotReferenceAssemblyDirectories { 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/ThirdPartyToolMigrationMetadataConstructorRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs new file mode 100644 index 0000000000..b307a5d301 --- /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 {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(); + } + + 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 {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(); + } + + 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..2380694200 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs @@ -0,0 +1,231 @@ +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; + Content = content ?? string.Empty; + } + + 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 ?? 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; } + } + + 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 ?? new List(); + AssemblyReferenceDirectories = assemblyReferenceDirectories ?? new List(); + LegacyAssemblyDirectories = legacyAssemblyDirectories ?? new HashSet(StringComparer.Ordinal); + AssemblyScopedLegacyDirectories = assemblyScopedLegacyDirectories ?? + new HashSet(StringComparer.Ordinal); + AssemblyScopedCurrentToolContractsDirectories = assemblyScopedCurrentToolContractsDirectories ?? + new HashSet(StringComparer.Ordinal); + AssemblyScopedCurrentApplicationDirectories = assemblyScopedCurrentApplicationDirectories ?? + new HashSet(StringComparer.Ordinal); + AssemblyScopedCurrentFirstPartyToolsDirectories = assemblyScopedCurrentFirstPartyToolsDirectories ?? + new HashSet(StringComparer.Ordinal); + AssemblyScopedLegacyAliasesByDirectory = assemblyScopedLegacyAliasesByDirectory ?? + new Dictionary(StringComparer.Ordinal); + AssemblyScopedLegacyToolInfoAliasesByDirectory = assemblyScopedLegacyToolInfoAliasesByDirectory ?? + new Dictionary(StringComparer.Ordinal); + AssemblyScopedCurrentApplicationAliasesByDirectory = + assemblyScopedCurrentApplicationAliasesByDirectory ?? + new Dictionary(StringComparer.Ordinal); + AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory = + assemblyScopedCurrentFirstPartyToolsAliasesByDirectory ?? + new Dictionary(StringComparer.Ordinal); + AssemblyDeclaredTypeNamesByDirectory = assemblyDeclaredTypeNamesByDirectory ?? + new Dictionary(StringComparer.Ordinal); + ToolContractsReferenceAssemblyDirectories = toolContractsReferenceAssemblyDirectories ?? + new HashSet(StringComparer.Ordinal); + ApplicationReferenceAssemblyDirectories = applicationReferenceAssemblyDirectories ?? + new HashSet(StringComparer.Ordinal); + DomainReferenceAssemblyDirectories = domainReferenceAssemblyDirectories ?? + new HashSet(StringComparer.Ordinal); + FirstPartyScreenshotReferenceAssemblyDirectories = firstPartyScreenshotReferenceAssemblyDirectories ?? + new HashSet(StringComparer.Ordinal); + } + + 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..b929563233 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs @@ -0,0 +1,473 @@ +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 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 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/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..8be895d68f --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs @@ -0,0 +1,350 @@ +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)}\.){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)}\.){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)}\.){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)}\.){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)}\.){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)}\.)|(?[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 CurrentFirstPartyToolsNamespace = "io.github.hatayama.UnityCliLoop.FirstPartyTools"; - 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 CurrentFirstPartyToolsScreenshotAssemblyName = - "UnityCLILoop.FirstPartyTools.Screenshot.Editor"; - 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 CurrentFirstPartyToolsScreenshotGuidReference = - "GUID:a0bdbd2c5705643fbb9aef9fac8fd46a"; - 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 string LegacyEditorDelayTypeName = "EditorDelay"; - private const string LegacyEditorDelayMethodName = "DelayFrame"; - private const string LegacyTimerDelayTypeName = "TimerDelay"; - private const string LegacyCancellationTokenArgumentName = "cancellationToken"; - private const string CurrentCancellationTokenArgumentName = "ct"; - private const string LegacyPlayerLoopTimingTypeName = "PlayerLoopTiming"; - private const string LegacyTimingArgumentName = "timing"; - private const string LegacyMainThreadSwitcherTypeName = "MainThreadSwitcher"; - private const string LegacyMainThreadSwitcherSwitchMethodName = "SwitchToMainThread"; - private const string CurrentEditorFrameWaiterTypeName = "EditorFrameWaiter"; - private const string CurrentEditorFrameWaiterMethodName = "WaitFramesOrTimeoutAsync"; - private const string LegacyEditorWindowCaptureUtilityTypeName = "EditorWindowCaptureUtility"; - private const string EditorWindowCaptureUtilityCaptureWindowMethodName = "CaptureWindowAsync"; - private const string EditorWindowCaptureUtilityCaptureGameRenderingMethodName = - "CaptureGameRenderingAsync"; - private const string CurrentConstantsTypeName = "UnityCliLoopConstants"; - private const string CurrentEditorFrameWaitTimeoutMemberName = "EDITOR_FRAME_WAIT_TIMEOUT_MS"; - 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 Regex LegacyEditorDelayFrameRegex = - new( - $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorDelayTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorDelayTypeName}|(?{LegacyEditorDelayTypeName}))\s*\.\s*{LegacyEditorDelayMethodName}\s*\(", - RegexOptions.Compiled); - - private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowRegex = - new( - $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", - RegexOptions.Compiled); - - private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureWindowInvocationRegex = - new( - $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", - RegexOptions.Compiled); - - private static readonly Regex LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex = - new( - $@"await\s+(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", - RegexOptions.Compiled); - - private 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); - - private static readonly Regex LegacyMainThreadSwitcherSwitchRegex = - new( - $@"(?:(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyMainThreadSwitcherTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyMainThreadSwitcherTypeName}|(?{LegacyMainThreadSwitcherTypeName}))\s*\.\s*{LegacyMainThreadSwitcherSwitchMethodName}\s*\(", - RegexOptions.Compiled); - - private static readonly Regex CurrentCaptureGameRenderingDeconstructionRegex = - new( - $@"\((?[^()]*)\)\s*=\s*await\s+{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.{LegacyEditorWindowCaptureUtilityTypeName}\.{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", - RegexOptions.Compiled); - - private 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) - }; - - private static readonly TypeReplacementRule[] DomainTypeReplacementRules = - { - new("ServiceResult", "ServiceResult"), - new("ToolSettingsCatalogItem", "ToolSettingsCatalogItem") - }; - - private static readonly TypeReplacementRule[] ApplicationTypeReplacementRules = - { - new("MainThreadSwitcher", "MainThreadSwitcher"), - new("SwitchToMainThreadAwaitable", "SwitchToMainThreadAwaitable") - }; - - private 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") - }; - - 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 hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); - bool hasCurrentDomainNamespaceUsage = RegexMatchesCode(source, CurrentDomainNamespaceRegex); - bool hasCurrentToolContractsNamespaceUsage = RegexMatchesCode(source, CurrentToolContractsNamespaceRegex); - bool hasCurrentFirstPartyToolsNamespaceUsage = - RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); - bool canMigrateBareLegacyToolAttribute = - hasLegacyAssemblySource || - hasLegacyNamespaceUsage || - legacyNamespaceAliases.Length > 0; - bool canMigrateBareLegacyEditorWindowCaptureUtility = - canMigrateBareLegacyToolAttribute || - hasCurrentToolContractsNamespaceUsage; - bool canMigrateBareLegacyFirstPartyScreenshotApi = - canMigrateBareLegacyToolAttribute || - hasCurrentToolContractsNamespaceUsage || - hasCurrentFirstPartyToolsNamespaceUsage; - bool canMigrateBareLegacyApplicationApi = - canMigrateBareLegacyToolAttribute || - hasCurrentToolContractsNamespaceUsage || - 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; - 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); - (string editorDelayMigratedContent, int editorDelayReplacementCount) = - ReplaceLegacyEditorDelayFrameCallsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute); - migratedContent = editorDelayMigratedContent; - replacementCount += editorDelayReplacementCount; - (string timerDelayMigratedContent, int timerDelayReplacementCount) = - ReplaceLegacyTimerDelayNamedArgumentsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute || hasCurrentToolContractsNamespaceUsage); - migratedContent = timerDelayMigratedContent; - replacementCount += timerDelayReplacementCount; - (string mainThreadSwitcherMigratedContent, int mainThreadSwitcherReplacementCount) = - ReplaceLegacyMainThreadSwitcherCallsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyApplicationApi); - migratedContent = mainThreadSwitcherMigratedContent; - replacementCount += mainThreadSwitcherReplacementCount; - (string editorWindowCaptureMigratedContent, int editorWindowCaptureReplacementCount) = - ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility); - migratedContent = editorWindowCaptureMigratedContent; - replacementCount += editorWindowCaptureReplacementCount; - migratedContent = ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyFirstPartyScreenshotApi, - ref replacementCount); - migratedContent = ReplaceLegacyApplicationTypeNamesInCode( - migratedContent, - legacyNamespaceAliases, - canMigrateBareLegacyApplicationApi, - 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( @@ -456,182 +46,38 @@ internal static ThirdPartyToolMigrationContentResult MigrateAsmdefSource( 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, - 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, + return ThirdPartyToolMigrationAsmdefRules.MigrateAsmdefSource( + source, + hasLegacyCSharpSource, + requiresToolContractsReference, requiresApplicationReference, requiresDomainReference, - requiresFirstPartyScreenshotReference, - 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, 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; + 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( @@ -639,58 +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 RegexMatchesCode(source, CurrentRegistrarRegex); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentRegistrarApi(source); } internal static bool ContainsLegacyApplicationApiForAssembly( string source, bool hasLegacyAssemblySource, - string[] legacyAssemblyAliases) + string[] legacyAssemblyAliases, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); - - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - return ContainsLegacyApplicationReference( - source, - hasLegacyAssemblySource, - legacyNamespaceAliases); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyApplicationApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases, currentApplicationAssemblyAliases, assemblyDeclaredTypeNames); } internal static bool ContainsCurrentApplicationApi(string source) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentApplicationApi(source); + } - bool hasCurrentApplicationNamespaceUsage = RegexMatchesCode(source, CurrentApplicationNamespaceRegex); - return ContainsCurrentApplicationReference(source, hasCurrentApplicationNamespaceUsage); + internal static bool ContainsCurrentApplicationApiForAssembly( + string source, + bool hasAssemblyScopedCurrentApplicationUsing, + string[] currentApplicationAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentApplicationApiForAssembly(source, hasAssemblyScopedCurrentApplicationUsing, currentApplicationAssemblyAliases, 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()); + return ThirdPartyToolMigrationApiDetectionRules.ContainsRegistrarDomainReturnApi(source); } internal static bool ContainsRegistrarDomainReturnApiForAssembly( @@ -698,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( @@ -740,4027 +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 ContainsLegacyFirstPartyScreenshotApiForAssembly( string source, bool hasLegacyAssemblySource, - string[] legacyAssemblyAliases) + string[] legacyAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyAssemblyAliases != null, "legacyAssemblyAliases must not be null"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyFirstPartyScreenshotApiForAssembly(source, hasLegacyAssemblySource, legacyAssemblyAliases, assemblyDeclaredTypeNames); + } - string[] legacyNamespaceAliases = GetCombinedLegacyNamespaceAliases(source, legacyAssemblyAliases); - return ContainsLegacyFirstPartyScreenshotReference( - source, - hasLegacyAssemblySource, - legacyNamespaceAliases); + internal static bool ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly( + string source, + bool hasLegacyAssemblySource, + bool hasAssemblyScopedCurrentToolContractsUsing, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] legacyAssemblyAliases, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyEditorWindowCaptureUtilityMigrationForAssembly(source, hasLegacyAssemblySource, hasAssemblyScopedCurrentToolContractsUsing, hasAssemblyScopedCurrentFirstPartyToolsUsing, legacyAssemblyAliases, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } - internal static bool ContainsCurrentFirstPartyScreenshotApi(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 ThirdPartyToolMigrationApiDetectionRules.ContainsLegacyEditorWindowCaptureUtilityTimeoutMigrationForAssembly(source, hasLegacyAssemblySource, hasAssemblyScopedCurrentToolContractsUsing, hasAssemblyScopedCurrentFirstPartyToolsUsing, legacyAssemblyAliases, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); + } - bool hasCurrentFirstPartyToolsNamespaceUsage = - RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); - return ContainsCurrentFirstPartyScreenshotApiForAssembly( - source, - hasCurrentFirstPartyToolsNamespaceUsage); + internal static bool ContainsCurrentFirstPartyScreenshotApi(string source) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentFirstPartyScreenshotApi(source); } internal static bool ContainsCurrentFirstPartyScreenshotApiForAssembly( string source, - bool hasAssemblyScopedCurrentFirstPartyToolsUsing) + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentFirstPartyScreenshotApiForAssembly(source, hasAssemblyScopedCurrentFirstPartyToolsUsing, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); + } - bool hasCurrentFirstPartyToolsNamespaceUsage = - RegexMatchesCode(source, CurrentFirstPartyToolsNamespaceRegex); - bool canUseBareCurrentFirstPartyScreenshotType = - hasAssemblyScopedCurrentFirstPartyToolsUsing || - hasCurrentFirstPartyToolsNamespaceUsage; - return ContainsCurrentFirstPartyScreenshotReference( - source, - canUseBareCurrentFirstPartyScreenshotType); + internal static bool ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly( + string source, + bool hasAssemblyScopedCurrentFirstPartyToolsUsing, + string[] currentFirstPartyToolsAssemblyAliases, + string[] assemblyDeclaredTypeNames) + { + return ThirdPartyToolMigrationApiDetectionRules.ContainsCurrentCaptureGameRenderingDeconstructionMigrationForAssembly(source, hasAssemblyScopedCurrentFirstPartyToolsUsing, currentFirstPartyToolsAssemblyAliases, assemblyDeclaredTypeNames); } 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, - canMigrateBareLegacyEditorWindowCaptureUtility: true) || - ContainsLegacyTimerDelayInvocation( - source, - legacyAssemblyAliases, - canMigrateBareLegacyTimerDelay: true) || - ContainsLegacyApplicationReference( - source, - true, - legacyAssemblyAliases) || - ContainsLegacyFirstPartyScreenshotReference( - source, - true, - legacyAssemblyAliases) || - ContainsLegacyToolAttributeList( - source, - legacyAssemblyAliases, - canMigrateBareLegacyToolAttribute: true); + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyAssemblyScopedApi(source, legacyAssemblyAliases); } internal static bool ContainsLegacyGlobalUsing(string source) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, LegacyGlobalUsingRegex); + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyGlobalUsing(source); } internal static bool ContainsCurrentDomainGlobalUsing(string source) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentDomainGlobalUsing(source); + } + + internal static bool ContainsCurrentToolContractsGlobalUsing(string source) + { + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentToolContractsGlobalUsing(source); + } - return RegexMatchesCode(source, CurrentDomainGlobalUsingRegex); + internal static bool ContainsCurrentApplicationGlobalUsing(string source) + { + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentApplicationGlobalUsing(source); + } + + internal static bool ContainsCurrentApplicationNamespaceAlias(string source) + { + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentApplicationNamespaceAlias(source); } internal static bool ContainsCurrentFirstPartyToolsGlobalUsing(string source) { - Debug.Assert(source != null, "source must not be null"); + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentFirstPartyToolsGlobalUsing(source); + } - return RegexMatchesCode(source, CurrentFirstPartyToolsGlobalUsingRegex); + internal static bool ContainsCurrentFirstPartyToolsNamespaceAlias(string source) + { + return ThirdPartyToolMigrationDetectionRules.ContainsCurrentFirstPartyToolsNamespaceAlias(source); } internal static string[] GetLegacyGlobalNamespaceAliases(string source) { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyGlobalNamespaceAliasRegex, "alias"); + return ThirdPartyToolMigrationDetectionRules.GetLegacyGlobalNamespaceAliases(source); } internal static string[] GetLegacyGlobalToolInfoTypeAliases(string source) { - Debug.Assert(source != null, "source must not be null"); - - return GetRegexGroupValuesInCode(source, LegacyGlobalToolInfoTypeAliasRegex, "alias"); + return ThirdPartyToolMigrationDetectionRules.GetLegacyGlobalToolInfoTypeAliases(source); } - internal static bool ContainsLegacyGlobalToolInfoTypeAlias(string source) + internal static string[] GetCurrentApplicationGlobalNamespaceAliases(string source) { - Debug.Assert(source != null, "source must not be null"); - - return RegexMatchesCode(source, LegacyGlobalToolInfoTypeAliasRegex); + return ThirdPartyToolMigrationDetectionRules.GetCurrentApplicationGlobalNamespaceAliases(source); } - internal static bool ContainsLegacyTypeAliasReference(string source, string[] aliases) + internal static string[] GetCurrentFirstPartyToolsGlobalNamespaceAliases(string source) { - 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.GetDeclaredTypeNames(source); } - internal static string[] GetExcludedDirectoryNames() + internal static bool ContainsLegacyGlobalToolInfoTypeAlias(string source) { - string[] names = new string[ExcludedDirectoryNames.Length]; - Array.Copy(ExcludedDirectoryNames, names, ExcludedDirectoryNames.Length); - return names; + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyGlobalToolInfoTypeAlias(source); } - private static string[] GetMigratedAsmdefReferences( - string reference, - bool hasLegacyCSharpSource, - bool requiresToolContractsReference, - bool requiresApplicationReference, - bool requiresDomainReference, - bool requiresFirstPartyScreenshotReference) + internal static bool ContainsLegacyTypeAliasReference(string source, string[] aliases) { - 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 }; + return ThirdPartyToolMigrationDetectionRules.ContainsLegacyTypeAliasReference(source, aliases); } - private static string[] GetMigratedLegacyEditorReferences( - bool requiresApplicationReference, - bool requiresDomainReference, - bool requiresFirstPartyScreenshotReference) + internal static bool IsExcludedDirectoryName(string directoryName) { - List references = new() - { - CurrentToolContractsGuidReference - }; - - if (requiresApplicationReference) - { - references.Add(CurrentApplicationGuidReference); - } - - if (requiresDomainReference) - { - references.Add(CurrentDomainGuidReference); - } - - if (requiresFirstPartyScreenshotReference) - { - references.Add(CurrentFirstPartyToolsScreenshotGuidReference); - } - - return references.ToArray(); + return ThirdPartyToolMigrationDetectionRules.IsExcludedDirectoryName(directoryName); } - private static void AddRequiredCurrentAsmdefReferences( - JArray references, - HashSet addedReferences, - bool requiresToolContractsReference, - bool requiresApplicationReference, - bool requiresDomainReference, - bool requiresFirstPartyScreenshotReference, - ref int replacementCount) + internal static string[] GetExcludedDirectoryNames() { - 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); - } - - if (requiresFirstPartyScreenshotReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentFirstPartyToolsScreenshotGuidReference, - ref replacementCount); - } + return ThirdPartyToolMigrationDetectionRules.GetExcludedDirectoryNames(); } - private static void AddRequiredCurrentAsmdefReference( - JArray references, - HashSet addedReferences, - string reference, - ref int replacementCount) + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + string source, + string originalSource, + RemovedLegacyPlayerLoopTimingSignature[] removedSignatures, + string[] legacyAssemblyAliases) { - 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 ThirdPartyToolMigrationTimingCallerRules.RemoveLegacyPlayerLoopTimingCallerArgumentsForLegacyAssembly( + source, + originalSource, + removedSignatures, + legacyAssemblyAliases); } - private static string GetCurrentAsmdefReferenceKey(string reference) + internal static ThirdPartyToolMigrationContentResult RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + string source, + string originalSource, + string[] legacyAssemblyAliases, + bool canMigrateBareLegacyPlayerLoopTiming, + string[] migratedCalleeMethodNames) { - Debug.Assert(!string.IsNullOrEmpty(reference), "reference must not be null or empty"); + return ThirdPartyToolMigrationTimingCallerRules.RemoveLegacyPlayerLoopTimingParametersForLegacyAssembly( + source, + originalSource, + legacyAssemblyAliases, + canMigrateBareLegacyPlayerLoopTiming, + migratedCalleeMethodNames); + } - if (IsCurrentAsmdefReference( - reference, - CurrentRuntimeAssemblyName, - CurrentRuntimeGuidReference)) + internal readonly struct RemovedLegacyPlayerLoopTimingSignature + { + public RemovedLegacyPlayerLoopTimingSignature( + string methodName, + string declaringTypeName, + LegacyPlayerLoopTimingParameterDeclaration[] originalParameters, + RemovedLegacyPlayerLoopTimingParameter[] removedParameters) { - return CurrentRuntimeAssemblyName; - } + 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 (IsCurrentAsmdefReference( - reference, - CurrentToolContractsAssemblyName, - CurrentToolContractsGuidReference)) - { - return CurrentToolContractsAssemblyName; + MethodName = methodName; + DeclaringTypeName = declaringTypeName; + OriginalParameters = + originalParameters ?? Array.Empty(); + RemovedParameters = removedParameters ?? Array.Empty(); } - if (IsCurrentAsmdefReference( - reference, - CurrentApplicationAssemblyName, - CurrentApplicationGuidReference)) - { - return CurrentApplicationAssemblyName; - } + public string MethodName { get; } + public string DeclaringTypeName { get; } + public LegacyPlayerLoopTimingParameterDeclaration[] OriginalParameters { get; } + public RemovedLegacyPlayerLoopTimingParameter[] RemovedParameters { get; } + } - if (IsCurrentAsmdefReference( - reference, - CurrentDomainAssemblyName, - CurrentDomainGuidReference)) + internal readonly struct LegacyPlayerLoopTimingParameterDeclaration + { + public LegacyPlayerLoopTimingParameterDeclaration( + int index, + string typeName, + string name, + bool hasDefaultValue) { - return CurrentDomainAssemblyName; - } + 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 (IsCurrentAsmdefReference( - reference, - CurrentFirstPartyToolsScreenshotAssemblyName, - CurrentFirstPartyToolsScreenshotGuidReference)) - { - return CurrentFirstPartyToolsScreenshotAssemblyName; + Index = index; + TypeName = typeName; + Name = name; + HasDefaultValue = hasDefaultValue; } - return reference; - } - - private 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); + public int Index { get; } + public string TypeName { get; } + public string Name { get; } + public bool HasDefaultValue { get; } } - private static bool TryMigrateLegacyToolAttributeList( - string attributesSource, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyToolAttribute, - out string migratedAttributes) + internal readonly struct RemovedLegacyPlayerLoopTimingParameter { - 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) + public RemovedLegacyPlayerLoopTimingParameter(int index, string name) { - string trimmedAttribute = attribute.Trim(); - if (TryMigrateLegacyToolAttributeEntry( - trimmedAttribute, - legacyNamespaceAliases, - canMigrateBareLegacyToolAttribute, - out string migratedAttribute)) - { - migratedAttributeItems.Add(migratedAttribute); - changed = true; - continue; - } + Debug.Assert(index >= 0, "index must not be negative"); + Debug.Assert(!string.IsNullOrEmpty(name), "name must not be null or empty"); - migratedAttributeItems.Add(trimmedAttribute); + Index = index; + Name = name; } - if (!changed) - { - migratedAttributes = string.Empty; - return false; - } - - migratedAttributes = string.Join(", ", migratedAttributeItems); - return true; - } - - private 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; - } - - private 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(); - } - - private 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, "$\"")) - { - 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 ReplaceLegacyApplicationTypeNamesInCode( - string source, - string[] aliases, - bool canMigrateBareLegacyApplicationApi, - 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 ApplicationTypeReplacementRules) - { - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", - RegexOptions.Compiled); - migratedContent = ReplaceRegexInCode( - migratedContent, - fullyQualifiedRegex, - _ => $"{CurrentApplicationNamespace}.{rule.CurrentName}", - ref replacementCount); - - foreach (string alias in aliases) - { - Regex aliasRegex = new( - $@"(? $"{CurrentApplicationNamespace}.{rule.CurrentName}", - ref replacementCount); - } - - Regex unqualifiedRegex = new( - $@"(? canMigrateBareLegacyApplicationApi && - ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentApplicationNamespace}.{rule.CurrentName}" - : match.Value, - ref replacementCount); - } - - return migratedContent; - } - - private static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( - string source, - string[] aliases, - bool canMigrateBareLegacyFirstPartyScreenshotApi, - 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 FirstPartyScreenshotTypeReplacementRules) - { - bool hasLocalTypeDeclaration = DeclaresLocalType(migratedContent, rule.LegacyName); - if (hasLocalTypeDeclaration) - { - Regex currentFullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", - RegexOptions.Compiled); - migratedContent = ReplaceRegexInCode( - migratedContent, - currentFullyQualifiedRegex, - _ => rule.CurrentName, - ref replacementCount); - } - - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(LegacyNamespace)}\.){Regex.Escape(rule.LegacyName)}\b", - RegexOptions.Compiled); - migratedContent = ReplaceRegexInCode( - migratedContent, - fullyQualifiedRegex, - _ => $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", - ref replacementCount); - - foreach (string alias in aliases) - { - Regex aliasRegex = new( - $@"(? $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", - ref replacementCount); - } - - Regex unqualifiedRegex = new( - $@"(? canMigrateBareLegacyFirstPartyScreenshotApi && - !hasLocalTypeDeclaration && - ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentFirstPartyToolsNamespace}.{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 (string Content, int ReplacementCount) ReplaceLegacyEditorDelayFrameCallsInCode( - 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); - StringBuilder builder = new(source.Length); - int sourceCopyIndex = 0; - int replacementCount = 0; - foreach (Match match in matches) - { - if (match.Index < sourceCopyIndex || - !codeTextMask.IsCodeAt(match.Index) || - !IsLegacyEditorDelayFrameCallMatch( - match, - legacyNamespaceAliases, - canMigrateBareLegacyEditorDelay)) - { - 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 = GetMigratedEditorDelayFrameArguments( - arguments, - GetMigratedEditorFrameWaitTimeoutExpression(match)); - if (migratedArguments.Length == 0) - { - continue; - } - - builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append(GetMigratedEditorFrameWaiterInvocationTarget(match)); - 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); - } - - private 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; - } - - private static string GetMigratedEditorFrameWaiterInvocationTarget(Match match) - { - 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}"; - } - - return CurrentEditorFrameWaiterTypeName; - } - - private static string GetMigratedEditorFrameWaitTimeoutExpression(Match match) - { - Debug.Assert(match != null, "match must not be null"); - - if (match.Groups["qualifier"].Success) - { - return $"{CurrentNamespace}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; - } - - if (match.Groups["alias"].Success) - { - return $"{match.Groups["alias"].Value}.{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; - } - - return $"{CurrentConstantsTypeName}.{CurrentEditorFrameWaitTimeoutMemberName}"; - } - - private 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(); - } - - private 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 = argument.IndexOf(':'); - 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; - } - - private 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); - } - - private 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; - } - - private 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); - } - - private static (string Content, int ReplacementCount) ReplaceLegacyMainThreadSwitcherCallsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyMainThreadSwitcher) - { - 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 = 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( - match, - legacyNamespaceAliases, - canMigrateBareLegacyMainThreadSwitcher)) - { - 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); - } - - private static bool IsLegacyMainThreadSwitcherCallMatch( - Match match, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyMainThreadSwitcher) - { - 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["mainThreadSwitcher"].Success && canMigrateBareLegacyMainThreadSwitcher; - } - - private 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; - foreach (string argument in trimmedArguments) - { - string timingValue = GetNamedArgumentValueOrNull(argument, LegacyTimingArgumentName); - if (timingValue != null) - { - if (IsLegacyPlayerLoopTimingArgument(timingValue, legacyNamespaceAliases)) - { - changed = true; - continue; - } - } - - if (IsLegacyPlayerLoopTimingArgument(argument, legacyNamespaceAliases)) - { - changed = true; - continue; - } - - string cancellationTokenValue = - GetNamedArgumentValueOrNull(argument, LegacyCancellationTokenArgumentName); - if (cancellationTokenValue != null) - { - migratedArguments.Add($"{CurrentCancellationTokenArgumentName}: {cancellationTokenValue}"); - changed = true; - continue; - } - - migratedArguments.Add(argument); - } - - return (migratedArguments.ToArray(), changed); - } - - private 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; - } - - private static (string Content, int ReplacementCount) ReplaceLegacyEditorWindowCaptureUtilityCallsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - - (string captureWindowContent, int captureWindowReplacementCount) = - ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( - source, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility); - (string captureWindowTaskContent, int captureWindowTaskReplacementCount) = - ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( - captureWindowContent, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility); - (string captureGameRenderingContent, int captureGameRenderingReplacementCount) = - ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( - captureWindowTaskContent, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility); - int deconstructionReplacementCount = 0; - string migratedContent = AddDiscardToCaptureGameRenderingDeconstructionsInCode( - captureGameRenderingContent, - ref deconstructionReplacementCount); - return ( - migratedContent, - captureWindowReplacementCount + - captureWindowTaskReplacementCount + - captureGameRenderingReplacementCount + - deconstructionReplacementCount); - } - - private static (string Content, int ReplacementCount) - ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowCallsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - 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 = 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, - canMigrateBareLegacyEditorWindowCaptureUtility)) - { - 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), - GetMigratedEditorFrameWaitTimeoutExpression(match)); - 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(CurrentFirstPartyToolsNamespace); - 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); - } - - private static (string Content, int ReplacementCount) - ReplaceLegacyEditorWindowCaptureUtilityCaptureWindowTaskCallsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - 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 = 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, - canMigrateBareLegacyEditorWindowCaptureUtility)) - { - 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), - GetMigratedEditorFrameWaitTimeoutExpression(match)); - if (migratedArguments.Length == 0) - { - continue; - } - - builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append(CurrentFirstPartyToolsNamespace); - 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); - } - - private static (string Content, int ReplacementCount) - ReplaceLegacyEditorWindowCaptureUtilityCaptureGameRenderingCallsInCode( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - 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 = 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, - canMigrateBareLegacyEditorWindowCaptureUtility)) - { - 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), - GetMigratedEditorFrameWaitTimeoutExpression(match)); - if (migratedArguments.Length == 0) - { - continue; - } - - builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append("await "); - builder.Append(CurrentFirstPartyToolsNamespace); - builder.Append('.'); - builder.Append(LegacyEditorWindowCaptureUtilityTypeName); - builder.Append('.'); - builder.Append(EditorWindowCaptureUtilityCaptureGameRenderingMethodName); - 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); - } - - private static string AddDiscardToCaptureGameRenderingDeconstructionsInCode( - string source, - ref int replacementCount) - { - Debug.Assert(source != null, "source 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; - } - - 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(); - } - - private static bool IsLegacyEditorWindowCaptureUtilityCallMatch( - Match match, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - 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["editorWindowCaptureUtility"].Success && - canMigrateBareLegacyEditorWindowCaptureUtility; - } - - private 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; - } - - char previousCharacter = ReadPreviousNonWhitespaceCharacter(source, awaitIndex); - return previousCharacter == '='; - } - - private 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); - } - - private 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 = arguments - .Select(argument => argument.Trim()) - .Where(argument => argument.Length > 0) - .ToArray(); - if (trimmedArguments.Length != 3) - { - return Array.Empty(); - } - - return new[] - { - trimmedArguments[0], - trimmedArguments[1], - timeoutExpression, - GetArgumentWithMigratedCancellationTokenName(trimmedArguments[2]) - }; - } - - private 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 = arguments - .Select(argument => argument.Trim()) - .Where(argument => argument.Length > 0) - .ToArray(); - if (trimmedArguments.Length == 3) - { - return new[] - { - trimmedArguments[0], - trimmedArguments[1], - GetArgumentWithMigratedCancellationTokenName(trimmedArguments[2]) - }; - } - - if (trimmedArguments.Length != 2) - { - return Array.Empty(); - } - - return new[] - { - trimmedArguments[0], - timeoutExpression, - GetArgumentWithMigratedCancellationTokenName(trimmedArguments[1]) - }; - } - - private 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}"; - } - - 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 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; - } - - private static bool ContainsLegacyEditorWindowCaptureUtilityCall( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - Debug.Assert(source != null, "source must not be null"); - Debug.Assert(legacyNamespaceAliases != null, "legacyNamespaceAliases must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - if (ContainsLegacyEditorWindowCaptureUtilityCall( - source, - codeTextMask, - LegacyEditorWindowCaptureUtilityCaptureWindowRegex, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility)) - { - return true; - } - - return ContainsLegacyEditorWindowCaptureUtilityCall( - source, - codeTextMask, - LegacyEditorWindowCaptureUtilityCaptureGameRenderingRegex, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility); - } - - private static bool ContainsLegacyEditorWindowCaptureUtilityCall( - string source, - CodeTextMask codeTextMask, - Regex regex, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyEditorWindowCaptureUtility) - { - 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"); - - MatchCollection matches = regex.Matches(source); - foreach (Match match in matches) - { - if (codeTextMask.IsCodeAt(match.Index) && - IsLegacyEditorWindowCaptureUtilityCallMatch( - match, - legacyNamespaceAliases, - canMigrateBareLegacyEditorWindowCaptureUtility)) - { - return true; - } - } - - return false; - } - - private 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; - } - - private static bool ContainsLegacyApplicationReference( - string source, - bool canMigrateBareLegacyApplicationApi, - 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 (TypeReplacementRule rule in ApplicationTypeReplacementRules) - { - 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 && - ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) - { - return true; - } - } - - if (ContainsLegacyMainThreadSwitcherSwitchCall( - source, - aliases, - canMigrateBareLegacyApplicationApi)) - { - return true; - } - - return false; - } - - private static bool ContainsLegacyMainThreadSwitcherSwitchCall( - string source, - string[] legacyNamespaceAliases, - bool canMigrateBareLegacyMainThreadSwitcher) - { - 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 = LegacyMainThreadSwitcherSwitchRegex.Matches(source); - foreach (Match match in matches) - { - if (codeTextMask.IsCodeAt(match.Index) && - IsLegacyMainThreadSwitcherCallMatch( - match, - legacyNamespaceAliases, - canMigrateBareLegacyMainThreadSwitcher)) - { - return true; - } - } - - return false; - } - - private static bool ContainsCurrentApplicationReference( - string source, - bool canUseBareCurrentApplicationType) - { - Debug.Assert(source != null, "source must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - foreach (TypeReplacementRule rule in ApplicationTypeReplacementRules) - { - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", - RegexOptions.Compiled); - if (RegexMatchesCode(source, fullyQualifiedRegex)) - { - return true; - } - - if (canUseBareCurrentApplicationType && - ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) - { - return true; - } - } - - return false; - } - - private static bool ContainsLegacyFirstPartyScreenshotReference( - string source, - bool canMigrateBareLegacyFirstPartyScreenshotApi, - 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 (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) - { - bool hasLocalTypeDeclaration = DeclaresLocalType(source, 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 && - !hasLocalTypeDeclaration && - ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.LegacyName)) - { - return true; - } - } - - return false; - } - - private static bool ContainsCurrentFirstPartyScreenshotReference( - string source, - bool canUseBareCurrentFirstPartyScreenshotType) - { - Debug.Assert(source != null, "source must not be null"); - - CodeTextMask codeTextMask = CodeTextMask.Create(source); - foreach (TypeReplacementRule rule in FirstPartyScreenshotTypeReplacementRules) - { - bool hasLocalTypeDeclaration = DeclaresLocalType(source, rule.CurrentName); - Regex fullyQualifiedRegex = new( - $@"(?:(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){Regex.Escape(rule.CurrentName)}\b", - RegexOptions.Compiled); - if (!hasLocalTypeDeclaration && RegexMatchesCode(source, fullyQualifiedRegex)) - { - return true; - } - - if (canUseBareCurrentFirstPartyScreenshotType && - !hasLocalTypeDeclaration && - ContainsLegacyAssemblyScopedTypeName(source, codeTextMask, rule.CurrentName)) - { - 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 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; - } - - 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; - } - - 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..d5688edb68 --- /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(CurrentFirstPartyToolsNamespace); + 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(CurrentFirstPartyToolsNamespace); + 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(CurrentFirstPartyToolsNamespace); + 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(CurrentFirstPartyToolsNamespace); + 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..9ce73d5a51 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs @@ -0,0 +1,299 @@ +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 assemblyScopedCurrentToolContractsDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentDomainDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentApplicationDirectories = new(StringComparer.Ordinal); + HashSet assemblyScopedCurrentFirstPartyToolsDirectories = 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.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, + 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, + 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, + 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, + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + new Dictionary(StringComparer.Ordinal), + new Dictionary(StringComparer.Ordinal), + 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..d8344f531e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTypeReplacementRules.cs @@ -0,0 +1,353 @@ +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( + $@"(? $"{CurrentApplicationNamespace}.UnityCliLoopToolRegistrar", + ref replacementCount); + + Regex aliasToolInfoRegex = new( + $@"(? $"{CurrentDomainNamespace}.ToolInfo", + 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) + ? $"{CurrentApplicationNamespace}.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, + _ => $"{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; + } + + 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, + _ => $"{CurrentApplicationNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentApplicationNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyApplicationApi && + !hasProtectedTypeDeclaration && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentApplicationNamespace}.{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, + _ => $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + ref replacementCount); + + foreach (string alias in aliases) + { + Regex aliasRegex = new( + $@"(? $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + ref replacementCount); + } + + Regex unqualifiedRegex = new( + $@"(? canMigrateBareLegacyFirstPartyScreenshotApi && + !hasProtectedTypeDeclaration && + ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) + ? $"{CurrentFirstPartyToolsNamespace}.{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) + ? $"{CurrentDomainNamespace}.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/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index aeaa9483fb..ee26956934 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -341,6 +341,7 @@ 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(); @@ -351,13 +352,13 @@ private async void HandleMigrateThirdPartyTools() new ThirdPartyToolMigrationProgressReporter(this, ct); result = await Task.Run(async () => await _thirdPartyToolMigrationUseCase.ApplyMigrationAsync(projectRoot, progress, ct)); - if (ct.IsCancellationRequested) + if (!ShouldFinishMigrationOnMainThread(ct.IsCancellationRequested, result)) { return; } await MainThreadSwitcher.SwitchToMainThread(); - if (ct.IsCancellationRequested) + if (!ShouldFinishMigrationOnMainThread(ct.IsCancellationRequested, result)) { return; } @@ -365,15 +366,28 @@ private async void HandleMigrateThirdPartyTools() CompleteMigrationOperation(ct); if (result.Changed) { - ShowCheckingState(new ThirdPartyToolMigrationProgress(1, 1)); - await Task.Yield(); + if (!ct.IsCancellationRequested) + { + ShowCheckingState(new ThirdPartyToolMigrationProgress(1, 1)); + await Task.Yield(); + } + AssetDatabase.Refresh(); } + + isMigrationCompletionPending = false; } finally { _isMigrating = false; + bool shouldRefreshAfterInterruptedMigration = ShouldRefreshAfterInterruptedMigration( + isMigrationCompletionPending, + ct.IsCancellationRequested); CompleteMigrationOperation(ct); + if (shouldRefreshAfterInterruptedMigration) + { + RefreshUI(); + } } if (ct.IsCancellationRequested) @@ -636,6 +650,22 @@ internal static bool ShouldRefreshAfterMigration(ThirdPartyToolMigrationResult r 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; + } + private void CompleteMigrationOperation(CancellationToken ct) { if (!IsMigrationOperationActive(ct)) From 56f51e4117447cef08d38a0b8275f0714af7f5a4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 20 Jun 2026 11:00:47 +0900 Subject: [PATCH 10/13] Expose migrated tool APIs through ToolContracts Move extension-facing registrar, main-thread, metadata, and screenshot contracts into the public ToolContracts assembly so third-party tools no longer need Application, Domain, or FirstPartyTools auto references. Update migration rules to rewrite current internal namespaces to ToolContracts and add only the ToolContracts asmdef reference for migrated tools. --- .../CaptureTest/EditorWindowCaptureTest.cs | 1 - .../CompileCheckWindow/CompileEditorWindow.cs | 1 - .../JsonRpcProcessorCliVersionGateTests.cs | 49 +-- .../Tests/Editor/MainThreadSwitcherTests.cs | 2 +- ...ThirdPartyToolMigrationFileServiceTests.cs | 328 ++++++++++-------- .../ThirdPartyToolMigrationRulesTests.cs | 218 ++++++------ .../Tests/Editor/UIElementAnnotatorTests.cs | 1 + .../Editor/UnityCliLoopToolRegistryTests.cs | 17 +- .../UnityCLILoop.Application.asmdef | 2 +- .../UnityCliLoopServerStartupService.cs | 1 + .../Application/UnityCliLoopToolRegistrar.cs | 2 +- .../UnityCliLoopServerShutdownUseCase.cs | 1 + .../UnityCliLoopApplicationRegistration.cs | 5 +- Packages/src/Editor/Domain/ServiceResult.cs | 51 --- .../Editor/Domain/UnityCLILoop.Domain.asmdef | 2 +- .../Editor/Domain/UnityCliLoopToolRegistry.cs | 21 -- .../FirstPartyToolsEditorStartup.cs | 1 + .../Screenshot/EditorWindowCaptureService.cs | 62 ++++ .../EditorWindowCaptureService.cs.meta | 11 + .../Screenshot/UIElementAnnotator.cs | 2 + ...p.FirstPartyTools.Screenshot.Editor.asmdef | 2 +- .../Api/GetToolDetailsBridgeCommand.cs | 4 +- .../Infrastructure/Api/UnityApiHandler.cs | 5 +- .../Server/UnityCliLoopServerController.cs | 5 +- .../ThirdPartyToolMigrationAsmdefRules.cs | 47 +-- .../ThirdPartyToolMigrationCSharpRules.cs | 4 + ...tyToolMigrationMetadataConstructorRules.cs | 4 +- .../ThirdPartyToolMigrationRuleCatalog.cs | 23 +- .../ThirdPartyToolMigrationScreenshotRules.cs | 8 +- ...dPartyToolMigrationTypeReplacementRules.cs | 49 ++- .../Threading/EditorMainThreadDispatcher.cs | 2 +- .../src/Editor/ToolContracts/AssemblyInfo.cs | 8 + .../Editor/ToolContracts/AssemblyInfo.cs.meta | 11 + .../EditorWindowCaptureUtility.cs | 87 +++++ .../EditorWindowCaptureUtility.cs.meta | 11 + .../MainThreadSwitcher.cs | 2 +- .../MainThreadSwitcher.cs.meta | 2 +- .../ScreenshotResponse.cs | 2 +- .../ScreenshotResponse.cs.meta | 2 +- .../ScreenshotSchema.cs | 2 +- .../ScreenshotSchema.cs.meta | 2 +- .../src/Editor/ToolContracts/ServiceResult.cs | 25 ++ .../ServiceResult.cs.meta | 2 +- Packages/src/Editor/ToolContracts/ToolInfo.cs | 23 ++ .../src/Editor/ToolContracts/ToolInfo.cs.meta | 11 + .../ToolSettingsCatalogItem.cs | 5 +- .../ToolSettingsCatalogItem.cs.meta | 2 +- .../UIElementInfo.cs | 2 +- .../UIElementInfo.cs.meta | 2 +- .../UnityCliLoopScreenshotTypes.cs | 2 +- .../UnityCliLoopScreenshotTypes.cs.meta | 2 +- .../UnityCliLoopToolRegistrar.cs | 88 +++++ .../UnityCliLoopToolRegistrar.cs.meta | 11 + 53 files changed, 786 insertions(+), 449 deletions(-) delete mode 100644 Packages/src/Editor/Domain/ServiceResult.cs create mode 100644 Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs create mode 100644 Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureService.cs.meta create mode 100644 Packages/src/Editor/ToolContracts/AssemblyInfo.cs create mode 100644 Packages/src/Editor/ToolContracts/AssemblyInfo.cs.meta create mode 100644 Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs create mode 100644 Packages/src/Editor/ToolContracts/EditorWindowCaptureUtility.cs.meta rename Packages/src/Editor/{Application => ToolContracts}/MainThreadSwitcher.cs (99%) rename Packages/src/Editor/{Application => ToolContracts}/MainThreadSwitcher.cs.meta (83%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/ScreenshotResponse.cs (96%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/ScreenshotResponse.cs.meta (83%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/ScreenshotSchema.cs (92%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/ScreenshotSchema.cs.meta (83%) create mode 100644 Packages/src/Editor/ToolContracts/ServiceResult.cs rename Packages/src/Editor/{Domain => ToolContracts}/ServiceResult.cs.meta (83%) create mode 100644 Packages/src/Editor/ToolContracts/ToolInfo.cs create mode 100644 Packages/src/Editor/ToolContracts/ToolInfo.cs.meta rename Packages/src/Editor/{Domain => ToolContracts}/ToolSettingsCatalogItem.cs (75%) rename Packages/src/Editor/{Domain => ToolContracts}/ToolSettingsCatalogItem.cs.meta (83%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/UIElementInfo.cs (92%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/UIElementInfo.cs.meta (83%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/UnityCliLoopScreenshotTypes.cs (97%) rename Packages/src/Editor/{FirstPartyTools/Screenshot => ToolContracts}/UnityCliLoopScreenshotTypes.cs.meta (83%) create mode 100644 Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs create mode 100644 Packages/src/Editor/ToolContracts/UnityCliLoopToolRegistrar.cs.meta 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/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index 73d4681610..0c52f79494 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -189,9 +189,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 +220,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 +232,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 +276,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 +291,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 +321,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 +367,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 +396,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")); @@ -477,16 +482,16 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } - private List BuildFirstPartyElements() + private List BuildFirstPartyElements() { - List elements = new(); + List elements = new(); elements.Add(CreateFirstPartyElement()); return elements; } - private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() { - return new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo(); + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); } private UIElementInfo CreateProjectElement() @@ -515,11 +520,11 @@ public sealed class UIElementInfo Assert.That(preview.FileCount, Is.EqualTo(1)); Assert.That(result.FileCount, Is.EqualTo(1)); Assert.That(migratedSource, Does.Contain( - "private List BuildFirstPartyElements()")); + "private List BuildFirstPartyElements()")); Assert.That(migratedSource, Does.Contain( - "private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement()")); + "private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement()")); Assert.That(migratedSource, Does.Contain( - "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); Assert.That(migratedSource, Does.Contain("private UIElementInfo CreateProjectElement()")); } finally @@ -529,9 +534,9 @@ public sealed class UIElementInfo } [Test] - public void ApplyMigration_WhenQualifiedFirstPartyDtoSharesLocalName_AddsScreenshotReference() + public void ApplyMigration_WhenQualifiedFirstPartyDtoSharesLocalName_UsesToolContractsReference() { - // Verifies that unambiguous first-party DTO references still add their asmdef dependency. + // Verifies that unambiguous first-party DTO references add the public ToolContracts asmdef dependency. string projectRoot = CreateProjectRoot(); try { @@ -541,9 +546,9 @@ public void ApplyMigration_WhenQualifiedFirstPartyDtoSharesLocalName_AddsScreens string asmdefPath = Path.Combine(toolDirectory, "VendorTools.Editor.asmdef"); string toolSource = @"public sealed class CurrentElementTool { - private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() { - return new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo(); + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); } private UIElementInfo CreateProjectElement() @@ -566,7 +571,8 @@ public sealed class UIElementInfo Assert.That(result.FileCount, Is.EqualTo(1)); Assert.That(File.ReadAllText(toolPath), Is.EqualTo(toolSource)); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -575,9 +581,9 @@ public sealed class UIElementInfo } [Test] - public void ApplyMigration_WhenCurrentRegistrarExistsWithToolContractsAsmdefReference_AddsApplicationReference() + 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 { @@ -607,9 +613,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 @@ -619,9 +628,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 { @@ -650,10 +659,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 { @@ -662,7 +671,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(); @@ -693,9 +702,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 @@ -705,9 +714,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 { @@ -741,11 +750,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 @@ -755,9 +764,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 { @@ -785,10 +794,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 @@ -798,9 +808,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 { @@ -829,11 +839,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 @@ -1030,7 +1041,7 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitContra [Test] public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitScreenshotHelpersAndAsmdef() { - // Verifies that split files using old screenshot helpers receive the required V3 first-party assembly reference. + // Verifies that split files using old screenshot helpers receive the required ToolContracts assembly reference. string projectRoot = CreateProjectRoot(); try { @@ -1058,11 +1069,11 @@ public sealed class ScreenshotHelper Assert.That(result.FileCount, Is.EqualTo(3)); Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.FindWindowsByName")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.FindWindowsByName")); Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.WindowMatchMode.exact")); + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode.exact")); Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1071,9 +1082,9 @@ public sealed class ScreenshotHelper } [Test] - public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyScreenshotCapture_AddsScreenshotAsmdefReference() + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyScreenshotCapture_KeepsToolContractsReference() { - // Verifies that partially migrated files finish screenshot helper migration and add the missing assembly reference. + // Verifies that partially migrated files finish screenshot helper migration using ToolContracts only. string projectRoot = CreateProjectRoot(); try { @@ -1118,10 +1129,11 @@ public sealed class CaptureResponse : UnityCliLoopToolResponse ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FileCount, Is.EqualTo(1)); Assert.That(File.ReadAllText(toolPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + "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 { @@ -1130,9 +1142,9 @@ public sealed class CaptureResponse : UnityCliLoopToolResponse } [Test] - public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyMainThreadSwitcher_AddsApplicationReference() + public void ApplyMigration_WhenCurrentToolContractsFileUsesLegacyMainThreadSwitcher_KeepsToolContractsReference() { - // Verifies that partially migrated files finish main-thread switcher migration and add the Application asmdef reference. + // Verifies that partially migrated files finish main-thread switcher migration using ToolContracts only. string projectRoot = CreateProjectRoot(); try { @@ -1170,11 +1182,12 @@ public sealed class MainThreadResponse : UnityCliLoopToolResponse ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); - Assert.That(result.FileCount, Is.EqualTo(2)); + Assert.That(result.FileCount, Is.EqualTo(1)); Assert.That(File.ReadAllText(toolPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + "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:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1183,9 +1196,9 @@ public sealed class MainThreadResponse : UnityCliLoopToolResponse } [Test] - public void ApplyMigration_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsApplicationReference() + public void ApplyMigration_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsToolContractsReference() { - // Verifies that regular legacy usings add the Application asmdef reference for migrated main-thread calls. + // Verifies that regular legacy usings add the ToolContracts asmdef reference for migrated main-thread calls. string projectRoot = CreateProjectRoot(); try { @@ -1217,8 +1230,9 @@ public async Task RunAsync(CancellationToken ct) 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); - Assert.That(migratedAsmdef, Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1227,9 +1241,9 @@ public async Task RunAsync(CancellationToken ct) } [Test] - public void ApplyMigration_WhenLegacyUsingUsesBareFirstPartyScreenshotDto_AddsScreenshotReference() + public void ApplyMigration_WhenLegacyUsingUsesBareFirstPartyScreenshotDto_AddsToolContractsReference() { - // Verifies that regular legacy usings that migrate screenshot DTOs also repair asmdef references. + // Verifies that regular legacy usings that migrate screenshot DTOs repair asmdef references. string projectRoot = CreateProjectRoot(); try { @@ -1259,8 +1273,9 @@ public UIElementInfo CreateElement() 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.FirstPartyTools.UIElementInfo")); - Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1269,9 +1284,9 @@ public UIElementInfo CreateElement() } [Test] - public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_AddsApplicationReference() + public void ApplyMigration_WhenAssemblyUsesCurrentApplicationGlobalUsing_RewritesApplicationNamespace() { - // Verifies that split files relying on a current Application global using get the required asmdef reference. + // Verifies that split files relying on a current Application global using move to ToolContracts. string projectRoot = CreateProjectRoot(); try { @@ -1302,9 +1317,12 @@ public async Task RunAsync(CancellationToken ct) ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); 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:214998e563c124e8a88199b2dd1f522d")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -1391,7 +1409,10 @@ public async Task RunAsync(CancellationToken ct) ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); 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")); @@ -1733,19 +1754,20 @@ public sealed class PartialResponse : UnityCliLoopToolResponse ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); string migratedSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(2)); + 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); Assert.That(migratedSource, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "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:214998e563c124e8a88199b2dd1f522d")); - Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + 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 { @@ -1791,7 +1813,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken 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.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1834,14 +1856,16 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken string migratedSource = File.ReadAllText(toolPath); string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(2)); + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "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.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -1886,10 +1910,13 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); string migratedSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "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( @@ -1902,9 +1929,9 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken } [Test] - public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_AddsScreenshotReference() + public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that a current FirstPartyTools global using by itself still gets the defining asmdef reference. + // Verifies that a current FirstPartyTools global using by itself moves to ToolContracts. string projectRoot = CreateProjectRoot(); try { @@ -1922,11 +1949,13 @@ public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReferen ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); - Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + 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.FirstPartyTools;")); + "global using io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { @@ -1935,9 +1964,9 @@ public void ApplyMigration_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReferen } [Test] - public void ApplyMigration_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_AddsScreenshotReference() + public void ApplyMigration_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that a current FirstPartyTools namespace alias by itself still gets the defining asmdef reference. + // Verifies that a current FirstPartyTools namespace alias by itself moves to ToolContracts. string projectRoot = CreateProjectRoot(); try { @@ -1955,11 +1984,13 @@ public void ApplyMigration_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_Add ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); - Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + 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.FirstPartyTools;")); + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { @@ -2117,9 +2148,9 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsing_RewritesSplitDomain Assert.That(result.FileCount, Is.EqualTo(3)); Assert.That(File.ReadAllText(helperPath), Does.Contain( - "io.github.hatayama.UnityCliLoop.Domain.ServiceResult Create")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ServiceResult Create")); 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")); } finally { @@ -2313,9 +2344,9 @@ public static LegacyToolInfo Create(ToolParameterSchema schema) Assert.That(result.FileCount, Is.EqualTo(3)); Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( - "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.Domain.ToolInfo;")); + "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo;")); Assert.That(File.ReadAllText(metadataPath), Does.Contain( - "new io.github.hatayama.UnityCliLoop.Domain.ToolInfo(\"hello\", schema)")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo(\"hello\", schema)")); Assert.That(File.ReadAllText(metadataPath), Does.Not.Contain("\"description\", schema")); } finally @@ -2353,7 +2384,7 @@ public void ApplyMigration_WhenAssemblyUsesGlobalLegacyToolInfoAlias_KeepsUnrela Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(File.ReadAllText(globalUsingPath), Does.Contain( - "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.Domain.ToolInfo;")); + "global using LegacyToolInfo = io.github.hatayama.UnityCliLoop.ToolContracts.ToolInfo;")); Assert.That(File.ReadAllText(unrelatedPath), Is.EqualTo(unrelatedSource)); } finally @@ -2512,21 +2543,22 @@ public sealed class UIElementInfo ThirdPartyToolMigrationResult result = service.ApplyMigration(projectRoot); string migratedToolSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(2)); + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); Assert.That(migratedToolSource, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotSchema")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotSchema")); Assert.That(migratedToolSource, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotResponse")); + "io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotResponse")); Assert.That(migratedToolSource, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo")); + "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:a0bdbd2c5705643fbb9aef9fac8fd46a")); + Assert.That(File.ReadAllText(asmdefPath), Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(File.ReadAllText(asmdefPath), Does.Not.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); } finally { @@ -2592,9 +2624,9 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.FileCount, Is.EqualTo(1)); Assert.That(migratedSource, Does.Contain( - "UnityCliLoopTool")); + "UnityCliLoopTool")); Assert.That(migratedSource, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); Assert.That(migratedSource, Does.Not.Contain("await EditorWindowCaptureUtility.CaptureWindowAsync")); } finally @@ -2696,9 +2728,9 @@ public void ApplyMigration_WhenNoAsmdefFirstPassEditorUsesGlobalLegacyUsing_Keep } [Test] - public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsingAndSplitManualRegistration_AddsApplicationReference() + public void ApplyMigration_WhenAssemblyUsesGlobalLegacyUsingAndSplitManualRegistration_AddsToolContractsReference() { - // Verifies that manual registration files relying on assembly-level legacy detection get required refs. + // Verifies that manual registration files relying on assembly-level legacy detection get ToolContracts refs. string projectRoot = CreateProjectRoot(); try { @@ -2727,9 +2759,9 @@ public static void Register(IUnityTool tool) Assert.That(result.FileCount, Is.EqualTo(3)); Assert.That(File.ReadAllText(registrationPath), 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 @@ -3658,9 +3690,9 @@ public sealed class ScreenshotResponse : UnityCliLoopToolResponse } [Test] - public async Task HasMigrationTargetsAsync_WhenCurrentApplicationGuidReferenceExists_ReturnsFalse() + public async Task HasMigrationTargetsAsync_WhenCurrentApplicationGuidReferenceExists_ReturnsTrue() { - // Verifies that current V3 Application references do not trigger the startup migration prompt. + // Verifies that current V3 Application namespace usage still triggers source migration. string projectRoot = CreateProjectRoot(); try { @@ -3692,7 +3724,7 @@ public static void Register(IUnityCliLoopTool tool) bool hasTargets = await service.HasMigrationTargetsAsync(projectRoot, CancellationToken.None); - Assert.That(hasTargets, Is.False); + Assert.That(hasTargets, Is.True); } finally { @@ -3977,9 +4009,9 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } [Test] - public void PreviewMigration_WhenCurrentApplicationGuidReferenceExists_KeepsAsmdef() + public void PreviewMigration_WhenCurrentApplicationGuidReferenceExists_ReportsSourceMigration() { - // Verifies that V3 Application references are not reported as legacy asmdef migration. + // Verifies that V3 Application namespace usage is reported as source migration. string projectRoot = CreateProjectRoot(); try { @@ -4010,7 +4042,10 @@ public static void Register(IUnityCliLoopTool tool) ThirdPartyToolMigrationFileService service = new(); ThirdPartyToolMigrationPreview preview = service.PreviewMigration(projectRoot); - Assert.That(preview.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)); } @@ -4149,9 +4184,9 @@ public sealed class HelloResponse : BaseToolResponse } [Test] - public async Task ApplyMigrationAsync_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsApplicationReference() + public async Task ApplyMigrationAsync_WhenFileScopedLegacyUsingUsesMainThreadSwitcher_AddsToolContractsReference() { - // Verifies that async migration also adds Application asmdef references for regular legacy usings. + // Verifies that async migration also adds ToolContracts asmdef references for regular legacy usings. string projectRoot = CreateProjectRoot(); try { @@ -4187,8 +4222,9 @@ await service.ApplyMigrationAsync( 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.Application.MainThreadSwitcher.SwitchToMainThread(ct)")); - Assert.That(migratedAsmdef, Does.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct)")); + Assert.That(migratedAsmdef, Does.Contain("GUID:fc3fd32eddbee40e39c2d76dc184957b")); + Assert.That(migratedAsmdef, Does.Not.Contain("GUID:214998e563c124e8a88199b2dd1f522d")); } finally { @@ -4234,7 +4270,10 @@ await service.ApplyMigrationAsync( 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")); @@ -4246,9 +4285,9 @@ await service.ApplyMigrationAsync( } [Test] - public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_AddsScreenshotReference() + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsGlobalUsingOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that async migration adds the screenshot asmdef reference for current FirstPartyTools global using. + // Verifies that async migration rewrites current FirstPartyTools global using to ToolContracts. string projectRoot = CreateProjectRoot(); try { @@ -4270,9 +4309,13 @@ await service.ApplyMigrationAsync( CancellationToken.None); string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); - Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + 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 { @@ -4321,10 +4364,13 @@ await service.ApplyMigrationAsync( CancellationToken.None); string migratedSource = File.ReadAllText(toolPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + 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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "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( @@ -4337,9 +4383,9 @@ await service.ApplyMigrationAsync( } [Test] - public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_AddsScreenshotReference() + public async Task ApplyMigrationAsync_WhenCurrentFirstPartyToolsAliasOnlyNeedsReference_RewritesFirstPartyNamespace() { - // Verifies that async migration adds the screenshot asmdef reference for current FirstPartyTools alias using. + // Verifies that async migration rewrites current FirstPartyTools alias using to ToolContracts. string projectRoot = CreateProjectRoot(); try { @@ -4361,11 +4407,13 @@ await service.ApplyMigrationAsync( CancellationToken.None); string migratedAsmdef = File.ReadAllText(asmdefPath); - Assert.That(result.FileCount, Is.EqualTo(1)); + Assert.That(result.FileCount, Is.EqualTo(2)); Assert.That(result.FilePaths.Contains(asmdefPath), Is.True); - Assert.That(migratedAsmdef, Does.Contain("GUID:a0bdbd2c5705643fbb9aef9fac8fd46a")); + 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.FirstPartyTools;")); + "using Fpt = io.github.hatayama.UnityCliLoop.ToolContracts;")); } finally { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 2f4daf5a21..7a9ec23628 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -396,7 +396,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -432,7 +432,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -468,7 +468,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -517,7 +517,7 @@ public static Task CaptureWindowAsync( Assert.That(result.Content, Does.Contain( "return EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); Assert.That(result.Content, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility")); } [Test] @@ -543,7 +543,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -570,7 +570,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -588,7 +588,7 @@ public sealed class ScreenshotTool { public async Task CaptureAsync(EditorWindow window, CancellationToken ct) { - return await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); + return await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } }"; @@ -597,7 +597,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -605,7 +605,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken [Test] public void MigrateCSharpSource_WhenCurrentFirstPartyToolsAliasCaptureLacksTimeout_RewritesCaptureCall() { - // Verifies that current FirstPartyTools namespace aliases still receive the V3 timeout argument. + // 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; @@ -625,7 +625,9 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, io.github.hatayama.UnityCliLoop.ToolContracts.UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "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")); } @@ -656,11 +658,11 @@ public Func> CreateCapture() Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "=>\n (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "=>\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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct)).texture;")); + "=> (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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "=> await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync")); } [Test] @@ -798,7 +800,7 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return (await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ConfigureAwait(false)).texture;")); + "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")); } @@ -825,9 +827,9 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + "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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ConfigureAwait(false);")); + "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;")); } @@ -854,9 +856,9 @@ public Task CaptureAsync(EditorWindow window, CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopCaptureTask => __unityCliLoopCaptureTask.GetAwaiter().GetResult().texture)")); + "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.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); + "return io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct);")); } [Test] @@ -881,21 +883,21 @@ public sealed class ScreenshotHelper Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.WindowMatchMode MatchMode")); + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode MatchMode")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.WindowMatchMode.contains")); + "io.github.hatayama.UnityCliLoop.ToolContracts.WindowMatchMode.contains")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.CaptureMode CaptureMode")); + "io.github.hatayama.UnityCliLoop.ToolContracts.CaptureMode CaptureMode")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.CaptureMode.rendering")); + "io.github.hatayama.UnityCliLoop.ToolContracts.CaptureMode.rendering")); Assert.That(result.Content, Does.Contain( - "new io.github.hatayama.UnityCliLoop.FirstPartyTools.ScreenshotInfo()")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.ScreenshotInfo()")); Assert.That(result.Content, Does.Contain( - "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.FindWindowsByName")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.FindWindowsByName")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.GetOpenWindowNames")); + "io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.GetOpenWindowNames")); } [Test] @@ -948,11 +950,11 @@ public sealed class UIElementInfo Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureWindowAsync")); + "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.FirstPartyTools.UIElementInfo")); + "io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo")); } [Test] @@ -973,16 +975,16 @@ public async Task CaptureAsync(EditorWindow window, CancellationToken return await EditorWindowCaptureUtility.CaptureWindowAsync(window, 1.0f, ct); } - private List BuildFirstPartyElements() + private List BuildFirstPartyElements() { - List elements = new(); + List elements = new(); elements.Add(CreateFirstPartyElement()); return elements; } - private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement() + private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement() { - return new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo(); + return new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo(); } private UIElementInfo CreateProjectElement() @@ -1008,13 +1010,13 @@ public sealed class UIElementInfo Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "private List BuildFirstPartyElements()")); + "private List BuildFirstPartyElements()")); Assert.That(result.Content, Does.Contain( - "List elements = new();")); + "List elements = new();")); Assert.That(result.Content, Does.Contain( - "private io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo CreateFirstPartyElement()")); + "private io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo CreateFirstPartyElement()")); Assert.That(result.Content, Does.Contain( - "new io.github.hatayama.UnityCliLoop.FirstPartyTools.UIElementInfo()")); + "new io.github.hatayama.UnityCliLoop.ToolContracts.UIElementInfo()")); Assert.That(result.Content, Does.Contain("private UIElementInfo CreateProjectElement()")); } @@ -1042,7 +1044,7 @@ public async Task CaptureAsync(CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "(texture, yOffset) = await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset))")); + "(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)")); } @@ -1102,14 +1104,14 @@ public async Task CaptureAsync(CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "(texture, yOffset, _) = await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync")); + "(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 still receive the V3 rendering timeout discard. + // 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; @@ -1129,6 +1131,8 @@ public async Task CaptureAsync(CancellationToken ct) 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")); } @@ -1193,9 +1197,9 @@ public sealed class RenderingCapture Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return await io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset));")); + "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.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); + "return await io.github.hatayama.UnityCliLoop.ToolContracts.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct);")); } [Test] @@ -1220,7 +1224,7 @@ public sealed class RenderingCapture Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "return io.github.hatayama.UnityCliLoop.FirstPartyTools.EditorWindowCaptureUtility.CaptureGameRenderingAsync(1.0f, UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS, ct).ContinueWith(__unityCliLoopRenderingTask => (__unityCliLoopRenderingTask.GetAwaiter().GetResult().texture, __unityCliLoopRenderingTask.GetAwaiter().GetResult().yOffset));")); + "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);")); } @@ -1280,13 +1284,13 @@ public async Task RunAsync(CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.SwitchToMainThreadAwaitable awaitable = io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread();")); + "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.Application.MainThreadSwitcher.IsMainThread;")); + "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)")); @@ -1298,7 +1302,7 @@ public async Task RunAsync(CancellationToken ct) [Test] public void MigrateCSharpSource_WhenCurrentApplicationAliasSwitcherHasLegacyTiming_RemovesTimingArgument() { - // Verifies that partially migrated Application namespace aliases also receive the V3 argument shape. + // 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; @@ -1315,6 +1319,8 @@ public async Task RunAsync(CancellationToken ct) 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")); @@ -1342,7 +1348,7 @@ await MainThreadSwitcher.SwitchToMainThread( Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "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::")); } @@ -1368,7 +1374,7 @@ public async Task RunAsync(CancellationToken ct) Assert.That(result.Changed, Is.True); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct: ct);")); + "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:")); @@ -1395,7 +1401,7 @@ public async Task RunAsync(PlayerLoopTiming loop) 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.Application.MainThreadSwitcher.SwitchToMainThread();")); + "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")); } @@ -1560,7 +1566,7 @@ public static Task SwitchToMainThread(PlayerLoopTiming loop) Assert.That(result.Content, Does.Contain("return MainThreadSwitcher.SwitchToMainThread(loop);")); Assert.That(result.Content, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher")); } [Test] @@ -1593,7 +1599,7 @@ public Task RunAsync(PlayerLoopTiming loop) Assert.That(result.Content, Does.Contain("return MainThreadSwitcher.SwitchToMainThread(loop);")); Assert.That(result.Content, Does.Not.Contain( - "io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher")); } [Test] @@ -2541,9 +2547,9 @@ public SwitchToMainThreadAwaitable Switch(PlayerLoopTiming loop) => ThirdPartyToolMigrationRules.MigrateCSharpSource(source); Assert.That(result.Changed, Is.True); - Assert.That(result.Content, Does.Contain("public io.github.hatayama.UnityCliLoop.Application.SwitchToMainThreadAwaitable Switch() =>")); + Assert.That(result.Content, Does.Contain("public io.github.hatayama.UnityCliLoop.ToolContracts.SwitchToMainThreadAwaitable Switch() =>")); Assert.That(result.Content, Does.Contain( - "io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread()")); + "io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread()")); Assert.That(result.Content, Does.Not.Contain("PlayerLoopTiming")); } @@ -2699,7 +2705,7 @@ public override async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) Assert.That(result.Content, Does.Contain( "public override async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] @@ -2730,7 +2736,7 @@ public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) Assert.That(result.Content, Does.Contain( "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] @@ -2756,7 +2762,7 @@ public virtual async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) 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.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] @@ -2782,7 +2788,7 @@ public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct) Assert.That(result.Content, Does.Contain( "public async Task RunAsync(PlayerLoopTiming loop, CancellationToken ct)")); Assert.That(result.Content, Does.Contain( - "await io.github.hatayama.UnityCliLoop.Application.MainThreadSwitcher.SwitchToMainThread(ct);")); + "await io.github.hatayama.UnityCliLoop.ToolContracts.MainThreadSwitcher.SwitchToMainThread(ct);")); } [Test] @@ -2839,9 +2845,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] @@ -2868,15 +2874,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] @@ -2898,7 +2904,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")); } @@ -2926,11 +2932,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")); } @@ -2954,7 +2960,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")); } @@ -2977,7 +2983,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")); } @@ -3000,7 +3006,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")); } @@ -3024,7 +3030,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")); } @@ -3073,7 +3079,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] @@ -3581,7 +3587,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] @@ -3673,7 +3679,7 @@ public void MigrateCSharpSourceForLegacyAssembly_WhenFileReliesOnGlobalUsing_Rew 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] @@ -3917,9 +3923,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"": [ @@ -3938,14 +3944,14 @@ public void MigrateAsmdefSource_WhenManualRegistrationIsUsed_KeepsApplicationRef 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"": [ @@ -3964,14 +3970,14 @@ public void MigrateAsmdefSource_WhenDomainMetadataIsUsed_AddsDomainReference() 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"": [] @@ -3988,7 +3994,7 @@ public void MigrateAsmdefSource_WhenCurrentDomainMetadataRequiresDomainReference 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")); } 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/UnityCLILoop.Application.asmdef b/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef index 1f6b39f50d..c8ecdcc581 100644 --- a/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef +++ b/Packages/src/Editor/Application/UnityCLILoop.Application.asmdef @@ -15,7 +15,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": true, + "autoReferenced": false, "defineConstraints": [], "versionDefines": [ { 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/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 40c9142e70..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 { @@ -27,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/UnityCLILoop.Domain.asmdef b/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef index 328843c917..39b5c41f13 100644 --- a/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef +++ b/Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef @@ -11,7 +11,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": true, + "autoReferenced": false, "defineConstraints": [], "versionDefines": [ { 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/FirstPartyToolsEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs index ba03663728..0420ae88cd 100644 --- a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs @@ -13,6 +13,7 @@ public static void 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/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef index 0f6d7f6352..55c4576d25 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef @@ -14,7 +14,7 @@ "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": true, + "autoReferenced": false, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false 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/ThirdPartyToolMigrationAsmdefRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs index c255bdf739..e7b6cf4527 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefRules.cs @@ -96,21 +96,6 @@ internal static string[] GetMigratedLegacyEditorReferences( CurrentToolContractsGuidReference }; - if (requiresApplicationReference) - { - references.Add(CurrentApplicationGuidReference); - } - - if (requiresDomainReference) - { - references.Add(CurrentDomainGuidReference); - } - - if (requiresFirstPartyScreenshotReference) - { - references.Add(CurrentFirstPartyToolsScreenshotGuidReference); - } - return references.ToArray(); } @@ -126,7 +111,10 @@ internal static void AddRequiredCurrentAsmdefReferences( Debug.Assert(references != null, "references must not be null"); Debug.Assert(addedReferences != null, "addedReferences must not be null"); - if (requiresToolContractsReference || requiresApplicationReference || requiresDomainReference) + if (requiresToolContractsReference || + requiresApplicationReference || + requiresDomainReference || + requiresFirstPartyScreenshotReference) { AddRequiredCurrentAsmdefReference( references, @@ -134,33 +122,6 @@ internal static void AddRequiredCurrentAsmdefReferences( CurrentToolContractsGuidReference, ref replacementCount); } - - if (requiresApplicationReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentApplicationGuidReference, - ref replacementCount); - } - - if (requiresDomainReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentDomainGuidReference, - ref replacementCount); - } - - if (requiresFirstPartyScreenshotReference) - { - AddRequiredCurrentAsmdefReference( - references, - addedReferences, - CurrentFirstPartyToolsScreenshotGuidReference, - ref replacementCount); - } } internal static void AddRequiredCurrentAsmdefReference( diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs index 9b4ef97069..13aa7cb0cb 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationCSharpRules.cs @@ -315,6 +315,10 @@ internal static ThirdPartyToolMigrationContentResult MigrateCSharpSourceForLegac ref replacementCount); } + migratedContent = ReplaceCurrentPublicContractNamespacesInCode( + migratedContent, + ref replacementCount); + return new ThirdPartyToolMigrationContentResult( migratedContent, replacementCount, diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs index b307a5d301..a88596b806 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationMetadataConstructorRules.cs @@ -114,7 +114,7 @@ internal static string ReplaceLegacyToolInfoConstructorsInCode( } builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append($"new {CurrentDomainNamespace}.ToolInfo("); + builder.Append($"new {CurrentNamespace}.ToolInfo("); builder.Append(string.Join(", ", migratedArguments)); builder.Append(')'); sourceCopyIndex = closingParenthesisIndex + 1; @@ -178,7 +178,7 @@ internal static string ReplaceLegacyToolSettingsCatalogItemConstructorsInCode( } builder.Append(source, sourceCopyIndex, match.Index - sourceCopyIndex); - builder.Append($"new {CurrentDomainNamespace}.ToolSettingsCatalogItem("); + builder.Append($"new {CurrentNamespace}.ToolSettingsCatalogItem("); builder.Append(string.Join(", ", migratedArguments)); builder.Append(')'); sourceCopyIndex = closingParenthesisIndex + 1; diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs index 8be895d68f..1abff40dea 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationRuleCatalog.cs @@ -96,7 +96,7 @@ internal static class ThirdPartyToolMigrationRuleCatalog internal static readonly Regex CurrentDomainMetadataRegex = new( - $@"(?(?:global::)?{Regex.Escape(CurrentFirstPartyToolsNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + $@"(?(?: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)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureWindowMethodName}\s*\(", + $@"(?(?: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)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + $@"(?(?: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)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyEditorWindowCaptureUtilityTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyEditorWindowCaptureUtilityTypeName}|(?{LegacyEditorWindowCaptureUtilityTypeName}))\s*\.\s*{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + $@"(?(?: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 = @@ -262,17 +262,17 @@ internal static class ThirdPartyToolMigrationRuleCatalog internal static readonly Regex LegacyMainThreadSwitcherSwitchRegex = new( - $@"(?(?:global::)?{Regex.Escape(CurrentApplicationNamespace)}\.){LegacyMainThreadSwitcherTypeName}|(?(?:global::)?{Regex.Escape(LegacyNamespace)}\.){LegacyMainThreadSwitcherTypeName}|(?[A-Za-z_][A-Za-z0-9_]*)\.{LegacyMainThreadSwitcherTypeName}|(?{LegacyMainThreadSwitcherTypeName}))\s*\.\s*{LegacyMainThreadSwitcherSwitchMethodName}\s*\(", + $@"(?(?: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)}\.)|(?[A-Za-z_][A-Za-z0-9_]*)\.)?{LegacyEditorWindowCaptureUtilityTypeName}\.{EditorWindowCaptureUtilityCaptureGameRenderingMethodName}\s*\(", + $@"\((?[^()]*)\)\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 = @@ -335,13 +335,16 @@ internal static class ThirdPartyToolMigrationRuleCatalog { new( $@"(? $"{CurrentApplicationNamespace}.UnityCliLoopToolRegistrar", + _ => $"{CurrentNamespace}.UnityCliLoopToolRegistrar", ref replacementCount); Regex aliasToolInfoRegex = new( @@ -79,13 +79,36 @@ internal static string ReplaceLegacyRegistrarAliasesInCode( migratedContent = ReplaceRegexInCode( migratedContent, aliasToolInfoRegex, - _ => $"{CurrentDomainNamespace}.ToolInfo", + _ => $"{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) @@ -98,7 +121,7 @@ internal static string ReplaceUnqualifiedLegacyRegistrarReferencesInCode( source, unqualifiedRegistrarRegex, match => ShouldMigrateLegacyTypeReference(source, "CustomToolManager", match.Index) - ? $"{CurrentApplicationNamespace}.UnityCliLoopToolRegistrar" + ? $"{CurrentNamespace}.UnityCliLoopToolRegistrar" : match.Value, ref replacementCount); } @@ -167,7 +190,7 @@ internal static string ReplaceLegacyDomainTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, fullyQualifiedRegex, - _ => $"{CurrentDomainNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); foreach (string alias in aliases) @@ -178,7 +201,7 @@ internal static string ReplaceLegacyDomainTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, aliasRegex, - _ => $"{CurrentDomainNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); } @@ -189,7 +212,7 @@ internal static string ReplaceLegacyDomainTypeNamesInCode( migratedContent, unqualifiedRegex, match => ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentDomainNamespace}.{rule.CurrentName}" + ? $"{CurrentNamespace}.{rule.CurrentName}" : match.Value, ref replacementCount); } @@ -220,7 +243,7 @@ internal static string ReplaceLegacyApplicationTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, fullyQualifiedRegex, - _ => $"{CurrentApplicationNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); foreach (string alias in aliases) @@ -231,7 +254,7 @@ internal static string ReplaceLegacyApplicationTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, aliasRegex, - _ => $"{CurrentApplicationNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); } @@ -244,7 +267,7 @@ internal static string ReplaceLegacyApplicationTypeNamesInCode( match => canMigrateBareLegacyApplicationApi && !hasProtectedTypeDeclaration && ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentApplicationNamespace}.{rule.CurrentName}" + ? $"{CurrentNamespace}.{rule.CurrentName}" : match.Value, ref replacementCount); } @@ -275,7 +298,7 @@ internal static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, fullyQualifiedRegex, - _ => $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); foreach (string alias in aliases) @@ -286,7 +309,7 @@ internal static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( migratedContent = ReplaceRegexInCode( migratedContent, aliasRegex, - _ => $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}", + _ => $"{CurrentNamespace}.{rule.CurrentName}", ref replacementCount); } @@ -299,7 +322,7 @@ internal static string ReplaceLegacyFirstPartyScreenshotTypeNamesInCode( match => canMigrateBareLegacyFirstPartyScreenshotApi && !hasProtectedTypeDeclaration && ShouldMigrateLegacyTypeReference(migratedContent, rule.LegacyName, match.Index) - ? $"{CurrentFirstPartyToolsNamespace}.{rule.CurrentName}" + ? $"{CurrentNamespace}.{rule.CurrentName}" : match.Value, ref replacementCount); } @@ -315,7 +338,7 @@ internal static string ReplaceLegacyToolInfoTypeReferencesInCode(string source, source, UnqualifiedToolInfoRegex, match => ShouldMigrateLegacyToolInfoTypeReference(source, match.Index) - ? $"{CurrentDomainNamespace}.ToolInfo" + ? $"{CurrentNamespace}.ToolInfo" : match.Value, ref replacementCount); } 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/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: From 1aa21f72bf934a619b467789b4841f2a6e226ecf Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 20 Jun 2026 11:10:49 +0900 Subject: [PATCH 11/13] Split third-party migration wizard responsibilities Extract text, state rules, view rendering, resize measurement, and progress reporting from the migration EditorWindow so the window coordinates lifecycle flow instead of owning every UI concern. Update onion assembly expectations for public ToolContracts types that were moved as part of the v3 migration surface. --- .../Editor/OnionAssemblyDependencyTests.cs | 16 +- ...ThirdPartyToolMigrationProgressReporter.cs | 72 +++ ...PartyToolMigrationProgressReporter.cs.meta | 11 + ...ThirdPartyToolMigrationWizardStateRules.cs | 77 +++ ...PartyToolMigrationWizardStateRules.cs.meta | 11 + .../ThirdPartyToolMigrationWizardText.cs | 68 +++ .../ThirdPartyToolMigrationWizardText.cs.meta | 11 + .../ThirdPartyToolMigrationWizardView.cs | 239 ++++++++ .../ThirdPartyToolMigrationWizardView.cs.meta | 11 + .../ThirdPartyToolMigrationWizardWindow.cs | 533 +++++------------- ...rdPartyToolMigrationWizardWindowResizer.cs | 176 ++++++ ...tyToolMigrationWizardWindowResizer.cs.meta | 11 + 12 files changed, 823 insertions(+), 413 deletions(-) create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationProgressReporter.cs.meta create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardStateRules.cs.meta create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs.meta create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardView.cs.meta create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs create mode 100644 Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs.meta diff --git a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs index 3dba5518a1..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] 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 ee26956934..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,35 +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 MigrationApplyingText = "Migrating project files for V3 custom tools..."; - 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 bool _isMigrating; private CancellationTokenSource _migrationOperationCts; private ThirdPartyToolMigrationUseCase _thirdPartyToolMigrationUseCase; + private ThirdPartyToolMigrationWizardView _view; + private ThirdPartyToolMigrationWizardWindowResizer _resizer; internal static void InitializeEditorServices(UnityCliLoopEditorSessionStateService sessionStateService) { @@ -70,7 +54,9 @@ internal static bool ShouldStartInitialRefresh( bool shouldRefreshAfterCreateGui, bool shouldAutoScanThirdPartyToolMigration) { - return shouldRefreshAfterCreateGui && shouldAutoScanThirdPartyToolMigration; + return ThirdPartyToolMigrationWizardStateRules.ShouldStartInitialRefresh( + shouldRefreshAfterCreateGui, + shouldAutoScanThirdPartyToolMigration); } internal static void PrepareForOpen( @@ -90,48 +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, bool isMigrating) { - string statusText = isMigrating ? MigrationApplyingText : MigrationCheckingText; - if (progress.TotalItemCount <= 0) - { - return statusText; - } - - return $"{statusText}\n" + - $"{progress.ProcessedItemCount}/{progress.TotalItemCount} steps complete."; + return ThirdPartyToolMigrationWizardText.GetMigrationProgressText(progress, isMigrating); } internal static string GetMigrationButtonText( @@ -139,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); + } + + 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); + } - return hasMigrationTargets ? MigrationButtonReadyText : MigrationButtonNoTargetsText; + 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) @@ -170,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); @@ -189,98 +235,10 @@ private void InitializeApplicationServices() private void OnDisable() { - _resizeScheduledItem?.Pause(); + _resizer?.Pause(); CancelMigrationOperation(); } - 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(); - }); - } - private void ShowInitialState(bool shouldStartInitialRefresh) { if (shouldStartInitialRefresh) @@ -309,8 +267,7 @@ private async void RefreshUI() await Task.Yield(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); - System.IProgress progress = - new ThirdPartyToolMigrationProgressReporter(this, ct); + System.IProgress progress = CreateProgressReporter(ct); ThirdPartyToolMigrationPreview preview = await Task.Run(async () => await _thirdPartyToolMigrationUseCase.PreviewMigrationAsync(projectRoot, progress, ct)); @@ -348,8 +305,7 @@ private async void HandleMigrateThirdPartyTools() try { - System.IProgress progress = - new ThirdPartyToolMigrationProgressReporter(this, ct); + System.IProgress progress = CreateProgressReporter(ct); result = await Task.Run(async () => await _thirdPartyToolMigrationUseCase.ApplyMigrationAsync(projectRoot, progress, ct)); if (!ShouldFinishMigrationOnMainThread(ct.IsCancellationRequested, result)) @@ -404,140 +360,47 @@ private async void HandleMigrateThirdPartyTools() ShowNoMigrationTargetsState(); } + private System.IProgress CreateProgressReporter(CancellationToken ct) + { + return new ThirdPartyToolMigrationProgressReporter( + ct, + IsMigrationOperationActive, + ShowCheckingState); + } + private void ShowNotCheckedState() { - _migrationStatusLabel.text = MigrationNotCheckedText; - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, false, false); - _refreshButton.SetEnabled(true); - _closeButton.SetEnabled(true); + _view.ShowNotCheckedState(_isMigrating); ScheduleResizeToContent(); } private void ShowMigrationTargetsState(int fileCount) { - _migrationStatusLabel.text = GetMigrationStatusText(fileCount); - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(!_isMigrating); - _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); - _refreshButton.SetEnabled(true); - _closeButton.SetEnabled(true); + _view.ShowMigrationTargetsState(fileCount, _isMigrating); ScheduleResizeToContent(); } private void ShowNoMigrationTargetsState() { - _migrationStatusLabel.text = NoMigrationTargetsText; - ViewDataBinder.SetVisible(_migrationProgressBar, false); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, false, true); - _refreshButton.SetEnabled(true); - _closeButton.SetEnabled(true); + _view.ShowNoMigrationTargetsState(_isMigrating); ScheduleResizeToContent(); } private void ShowCheckingState(ThirdPartyToolMigrationProgress progress) { - _migrationStatusLabel.text = GetMigrationProgressText(progress, _isMigrating); - ViewDataBinder.SetVisible(_migrationProgressBar, true); - UpdateMigrationProgressBar(progress); - _migrateButton.SetEnabled(false); - _migrateButton.text = GetMigrationButtonText(_isMigrating, true, true); - _refreshButton.SetEnabled(false); - _closeButton.SetEnabled(!_isMigrating); + _view.ShowCheckingState(progress, _isMigrating); 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)) - { - 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 (!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; - } - - internal static bool HasFiniteSize(Vector2 size) - { - return IsFinite(size.x) && IsFinite(size.y); - } - - 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 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; + _resizer?.ScheduleResizeToContent(); } private CancellationToken BeginMigrationOperation() { CancelMigrationOperation(); - CancellationTokenSource cts = new(); + CancellationTokenSource cts = new CancellationTokenSource(); _migrationOperationCts = cts; return cts.Token; } @@ -554,37 +417,6 @@ private void CancelMigrationOperation() _migrationOperationCts = null; } - 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 bool ConsumeShouldStartInitialRefresh() { if (!_shouldRefreshAfterCreateGui) @@ -610,62 +442,6 @@ private void TryStartInitialRefresh() rootVisualElement.schedule.Execute(RefreshUI).StartingIn(0); } - 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; - } - private void CompleteMigrationOperation(CancellationToken ct) { if (!IsMigrationOperationActive(ct)) @@ -681,58 +457,5 @@ private bool IsMigrationOperationActive(CancellationToken ct) { return _migrationOperationCts != null && _migrationOperationCts.Token.Equals(ct); } - - private sealed class ThirdPartyToolMigrationProgressReporter - : System.IProgress - { - private readonly ThirdPartyToolMigrationWizardWindow _window; - private readonly CancellationToken _ct; - private long _lastReportTimestamp; - - public ThirdPartyToolMigrationProgressReporter( - 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.IsMigrationOperationActive(ct))) - { - return; - } - - _window.ShowCheckingState(value); - } - } } } diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs new file mode 100644 index 0000000000..6009c05ecd --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs @@ -0,0 +1,176 @@ +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; + _root = window.rootVisualElement; + _mainScrollView = 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 (_mainScrollView == null) + { + return; + } + + 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: From cb2c76c44dc6371ffff6ab716dbcf70f5339ed04 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 20 Jun 2026 11:38:03 +0900 Subject: [PATCH 12/13] Fix migration review findings Tighten migration parsing and fast-scan behavior so split global usings, interpolated attribute arguments, normalized paths, unreadable asmdef meta files, and fail-fast model contracts behave consistently during V3 migration checks. Keep larger sync/async analyzer consolidation out of this pass because those flows currently differ in cancellation, progress, and cache ownership. --- ...ThirdPartyToolMigrationFileServiceTests.cs | 90 ++++++++ .../ThirdPartyToolMigrationRulesTests.cs | 17 ++ .../ThirdPartyToolMigrationArgumentRules.cs | 11 + ...dPartyToolMigrationAsmdefMetaGuidReader.cs | 65 ++++++ ...yToolMigrationAsmdefMetaGuidReader.cs.meta | 11 + ...yToolMigrationAssemblyReferenceResolver.cs | 61 ++--- ...PartyToolMigrationAssemblyUsageAnalyzer.cs | 4 +- ...grationFastAssemblyRequirementCollector.cs | 57 +++-- ...tyToolMigrationFastSourceTargetDetector.cs | 37 ++- ...rtyToolMigrationInterpolatedStringRules.cs | 217 ++++++++++++++++++ ...olMigrationInterpolatedStringRules.cs.meta | 11 + .../ThirdPartyToolMigrationModels.cs | 41 ++-- .../ThirdPartyToolMigrationParsingRules.cs | 35 --- .../ThirdPartyToolMigrationPathRules.cs | 63 +++++ .../ThirdPartyToolMigrationPathRules.cs.meta | 11 + .../ThirdPartyToolMigrationTargetScanner.cs | 39 +++- ...rdPartyToolMigrationWizardWindowResizer.cs | 9 +- 17 files changed, 637 insertions(+), 142 deletions(-) create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAsmdefMetaGuidReader.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs create mode 100644 Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs.meta diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index 0c52f79494..e56271cbdc 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -96,6 +96,59 @@ 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 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() { @@ -3086,6 +3139,43 @@ public sealed class HelloResponse : UnityCliLoopToolResponse } } + [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() { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index 7a9ec23628..da99dfcf22 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -109,6 +109,23 @@ 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 MigrateCSharpSource_WhenLegacyToolAttributeHasSecurityArgument_RewritesSecurityArgument() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs index eedf40ecbb..26d4de24c2 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationArgumentRules.cs @@ -149,6 +149,16 @@ internal static string[] SplitAttributeArguments(string argumentsSource) if (StartsWith(argumentsSource, i, "$\"")) { + int interpolatedStringEndIndex = + ThirdPartyToolMigrationInterpolatedStringRules.FindRegularInterpolatedStringEndIndex( + argumentsSource, + i); + if (interpolatedStringEndIndex >= 0) + { + i = interpolatedStringEndIndex; + continue; + } + isInRegularString = true; i++; continue; @@ -190,6 +200,7 @@ internal static string[] SplitAttributeArguments(string argumentsSource) 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"); 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/ThirdPartyToolMigrationAssemblyReferenceResolver.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs index efab5b36df..62ebc5ff59 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyReferenceResolver.cs @@ -217,7 +217,7 @@ internal static Dictionary CreateAsmdefDirectoryMap(List AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); AddAsmdefDirectoryReference( asmdefDirectoriesByReference, - ReadAsmdefGuidReference(asmdefFilePath), + ReadAsmdefGuidReferenceFromAsmdefPath(asmdefFilePath), asmdefDirectory); } @@ -260,7 +260,7 @@ internal static async Task> CreateAsmdefDirectoryMapA AddAsmdefDirectoryReference(asmdefDirectoriesByReference, assemblyName, asmdefDirectory); AddAsmdefDirectoryReference( asmdefDirectoriesByReference, - ReadAsmdefGuidReference(asmdefFilePath), + ReadAsmdefGuidReferenceFromAsmdefPath(asmdefFilePath), asmdefDirectory); } @@ -330,29 +330,19 @@ internal static void AddAsmdefDirectoryReference( asmdefDirectoriesByReference.Add(reference, asmdefDirectory); } - internal static string ReadAsmdefGuidReference(string asmdefFilePath) + 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; - } - - 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 ThirdPartyToolMigrationAsmdefMetaGuidReader.ReadAsmdefGuidReferenceFromAsmdefPath( + asmdefFilePath); + } - return string.Empty; + internal static string ReadAsmdefGuidReferenceFromMetaFile( + string metaPath, + Func> readLines) + { + return ThirdPartyToolMigrationAsmdefMetaGuidReader.ReadAsmdefGuidReferenceFromMetaFile( + metaPath, + readLines); } internal static string FindNearestAssemblyDirectory( @@ -465,33 +455,12 @@ internal static bool IsFirstPassAssemblyPath(string csharpFilePath, string proje 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 relativePath = filePath.StartsWith(projectRoot, StringComparison.Ordinal) - ? filePath.Substring(projectRoot.Length) - : filePath; - char[] separators = - { - Path.DirectorySeparatorChar, - Path.AltDirectorySeparatorChar - }; - return relativePath.Split(separators, StringSplitOptions.RemoveEmptyEntries); + return ThirdPartyToolMigrationPathRules.GetRelativePathSegments(filePath, projectRoot); } internal 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); + return ThirdPartyToolMigrationPathRules.IsSameOrChildPath(childPath, parentPath); } } } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs index fb31f133b2..c8b25e89de 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationAssemblyUsageAnalyzer.cs @@ -54,10 +54,12 @@ internal static MigrationAssemblyUsage FindMigrationAssemblyUsage( 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, @@ -163,7 +165,7 @@ internal static MigrationAssemblyUsage FindMigrationAssemblyUsage( foreach (string csharpFilePath in csharpFilePaths) { - string source = File.ReadAllText(csharpFilePath); + string source = sourceByCSharpFilePath[csharpFilePath]; if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs index 502db682ee..6207cae749 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastAssemblyRequirementCollector.cs @@ -20,6 +20,7 @@ internal static void CollectFastAssemblyReferenceRequirements( string assemblyDirectory, bool hasAssemblyScopedCurrentToolContractsNamespaceUsage, bool hasAssemblyScopedCurrentDomainNamespaceUsage, + bool hasAssemblyScopedCurrentApplicationNamespaceUsage, string[] currentApplicationAssemblyAliases, string[] assemblyDeclaredTypeNames, HashSet legacyAssemblyDirectories, @@ -64,7 +65,7 @@ internal static void CollectFastAssemblyReferenceRequirements( assemblyDeclaredTypeNames) || ThirdPartyToolMigrationRules.ContainsCurrentApplicationApiForAssembly( source, - hasAssemblyScopedCurrentApplicationUsing: false, + hasAssemblyScopedCurrentApplicationNamespaceUsage, currentApplicationAssemblyAliases, assemblyDeclaredTypeNames)) { @@ -92,6 +93,7 @@ internal static async Task CollectFastAssemblyReferenceRequirementsAsync( string projectRoot, HashSet assemblyScopedCurrentToolContractsDirectories, HashSet assemblyScopedCurrentDomainDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, Dictionary> assemblyDeclaredTypeNamesByDirectory, HashSet legacyAssemblyDirectories, @@ -112,6 +114,9 @@ internal static async Task CollectFastAssemblyReferenceRequirementsAsync( 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"); @@ -136,6 +141,12 @@ internal static async Task CollectFastAssemblyReferenceRequirementsAsync( } string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; @@ -151,6 +162,7 @@ internal static async Task CollectFastAssemblyReferenceRequirementsAsync( assemblyDirectory, assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), legacyAssemblyDirectories, @@ -158,11 +170,6 @@ internal static async Task CollectFastAssemblyReferenceRequirementsAsync( applicationReferenceAssemblyDirectories, domainReferenceAssemblyDirectories); - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } } } @@ -173,6 +180,7 @@ internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirem string projectRoot, HashSet assemblyScopedCurrentToolContractsDirectories, HashSet assemblyScopedCurrentDomainDirectories, + HashSet assemblyScopedCurrentApplicationDirectories, Dictionary> assemblyScopedCurrentApplicationAliasesByDirectory, Dictionary> assemblyDeclaredTypeNamesByDirectory, HashSet legacyAssemblyDirectories, @@ -193,6 +201,9 @@ internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirem 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"); @@ -209,6 +220,12 @@ internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirem } string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; @@ -229,6 +246,7 @@ internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirem assemblyDirectory, true, assemblyScopedCurrentDomainDirectories.Contains(assemblyDirectory), + assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory), legacyAssemblyDirectories, @@ -236,11 +254,6 @@ internal static async Task CollectFastAssemblyScopedCurrentToolContractsRequirem applicationReferenceAssemblyDirectories, domainReferenceAssemblyDirectories); - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } } } @@ -273,6 +286,12 @@ internal static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsy } string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; @@ -289,11 +308,6 @@ internal static async Task CollectFastAssemblyScopedCurrentDomainRequirementsAsy domainReferenceAssemblyDirectories.Add(assemblyDirectory); } - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } } } @@ -346,6 +360,12 @@ internal static async Task CollectFastFirstPartyScreenshotRequirementsAsyn } string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; @@ -416,11 +436,6 @@ internal static async Task CollectFastFirstPartyScreenshotRequirementsAsyn return true; } - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } } return false; diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs index f76d7e4ebf..e9b4b5c8c6 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationFastSourceTargetDetector.cs @@ -29,6 +29,9 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( List assemblyReferenceDirectories, string projectRoot, HashSet legacyAssemblyDirectories, + HashSet assemblyScopedLegacyDirectories, + Dictionary> assemblyScopedLegacyAliasesByDirectory, + Dictionary> assemblyScopedLegacyToolInfoAliasesByDirectory, HashSet assemblyScopedCurrentToolContractsDirectories, HashSet assemblyScopedCurrentApplicationDirectories, HashSet assemblyScopedCurrentFirstPartyToolsDirectories, @@ -42,6 +45,15 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( 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"); @@ -70,6 +82,12 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( } string source = File.ReadAllText(csharpFilePath); + inspectedEntryCount++; + if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) + { + await Task.Yield(); + } + if (!ThirdPartyToolMigrationRules.ContainsMigrationCandidateText(source)) { continue; @@ -82,11 +100,16 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( projectRoot); string[] assemblyDeclaredTypeNames = GetAssemblyScopedNames(assemblyDeclaredTypeNamesByDirectory, assemblyDirectory); + string[] legacyAssemblyAliases = + GetAssemblyScopedNames(assemblyScopedLegacyAliasesByDirectory, assemblyDirectory); + string[] legacyAssemblyToolInfoAliases = + GetAssemblyScopedNames(assemblyScopedLegacyToolInfoAliasesByDirectory, assemblyDirectory); bool hasLegacyAssemblySource = - legacyAssemblyDirectories.Contains(assemblyDirectory) && + (legacyAssemblyDirectories.Contains(assemblyDirectory) || + assemblyScopedLegacyDirectories.Contains(assemblyDirectory)) && ThirdPartyToolMigrationRules.ContainsLegacyAssemblyScopedApi( source, - Array.Empty()); + legacyAssemblyAliases); ThirdPartyToolMigrationContentResult result = ThirdPartyToolMigrationRules.MigrateCSharpSourceForLegacyAssembly( source, @@ -94,8 +117,8 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( assemblyScopedCurrentToolContractsDirectories.Contains(assemblyDirectory), assemblyScopedCurrentApplicationDirectories.Contains(assemblyDirectory), assemblyScopedCurrentFirstPartyToolsDirectories.Contains(assemblyDirectory), - Array.Empty(), - Array.Empty(), + legacyAssemblyAliases, + legacyAssemblyToolInfoAliases, GetAssemblyScopedNames(assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDirectory), GetAssemblyScopedNames( assemblyScopedCurrentFirstPartyToolsAliasesByDirectory, @@ -105,12 +128,6 @@ internal static async Task ContainsFastCSharpSourceMigrationTargetAsync( { return true; } - - inspectedEntryCount++; - if (inspectedEntryCount % ThirdPartyToolMigrationFileServiceConstants.PreviewYieldBatchSize == 0) - { - await Task.Yield(); - } } return false; diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs new file mode 100644 index 0000000000..e2cb2d32c2 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs @@ -0,0 +1,217 @@ +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, '"'); + 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/ThirdPartyToolMigrationModels.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs index 2380694200..01260d6dfa 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationModels.cs @@ -16,8 +16,8 @@ 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; + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + Content = content ?? throw new ArgumentNullException(nameof(content)); } public string FilePath { get; } @@ -33,7 +33,7 @@ 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(); + Changes = changes ?? throw new ArgumentNullException(nameof(changes)); ReplacementCount = replacementCount; ChangedFilePaths = Changes .Select(change => change.FilePath) @@ -161,37 +161,40 @@ public MigrationAssemblyUsage( firstPartyScreenshotReferenceAssemblyDirectories != null, "firstPartyScreenshotReferenceAssemblyDirectories must not be null"); - AsmdefDirectories = asmdefDirectories ?? new List(); - AssemblyReferenceDirectories = assemblyReferenceDirectories ?? new List(); - LegacyAssemblyDirectories = legacyAssemblyDirectories ?? new HashSet(StringComparer.Ordinal); + AsmdefDirectories = asmdefDirectories ?? + throw new ArgumentNullException(nameof(asmdefDirectories)); + AssemblyReferenceDirectories = assemblyReferenceDirectories ?? + throw new ArgumentNullException(nameof(assemblyReferenceDirectories)); + LegacyAssemblyDirectories = legacyAssemblyDirectories ?? + throw new ArgumentNullException(nameof(legacyAssemblyDirectories)); AssemblyScopedLegacyDirectories = assemblyScopedLegacyDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedLegacyDirectories)); AssemblyScopedCurrentToolContractsDirectories = assemblyScopedCurrentToolContractsDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedCurrentToolContractsDirectories)); AssemblyScopedCurrentApplicationDirectories = assemblyScopedCurrentApplicationDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedCurrentApplicationDirectories)); AssemblyScopedCurrentFirstPartyToolsDirectories = assemblyScopedCurrentFirstPartyToolsDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedCurrentFirstPartyToolsDirectories)); AssemblyScopedLegacyAliasesByDirectory = assemblyScopedLegacyAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedLegacyAliasesByDirectory)); AssemblyScopedLegacyToolInfoAliasesByDirectory = assemblyScopedLegacyToolInfoAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedLegacyToolInfoAliasesByDirectory)); AssemblyScopedCurrentApplicationAliasesByDirectory = assemblyScopedCurrentApplicationAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedCurrentApplicationAliasesByDirectory)); AssemblyScopedCurrentFirstPartyToolsAliasesByDirectory = assemblyScopedCurrentFirstPartyToolsAliasesByDirectory ?? - new Dictionary(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory)); AssemblyDeclaredTypeNamesByDirectory = assemblyDeclaredTypeNamesByDirectory ?? - new Dictionary(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(assemblyDeclaredTypeNamesByDirectory)); ToolContractsReferenceAssemblyDirectories = toolContractsReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(toolContractsReferenceAssemblyDirectories)); ApplicationReferenceAssemblyDirectories = applicationReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(applicationReferenceAssemblyDirectories)); DomainReferenceAssemblyDirectories = domainReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(domainReferenceAssemblyDirectories)); FirstPartyScreenshotReferenceAssemblyDirectories = firstPartyScreenshotReferenceAssemblyDirectories ?? - new HashSet(StringComparer.Ordinal); + throw new ArgumentNullException(nameof(firstPartyScreenshotReferenceAssemblyDirectories)); } public List AsmdefDirectories { get; } diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs index b929563233..94c7d72a0c 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationParsingRules.cs @@ -416,41 +416,6 @@ public bool IsCodeAt(int index) return _codeCharacters[index]; } - 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 sealed class CodeTextMaskHolder diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs new file mode 100644 index 0000000000..7eed0dc546 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs @@ -0,0 +1,63 @@ +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) + { + Debug.Assert(childPath != null, "childPath must not be null"); + Debug.Assert(parentPath != null, "parentPath must not be null"); + + 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(path != null, "path must not be null"); + + string fullPath = Path.GetFullPath(path); + string trimmedPath = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return trimmedPath.Length == 0 ? fullPath : trimmedPath; + } + + 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/ThirdPartyToolMigrationTargetScanner.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs index 9ce73d5a51..3d676f3233 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationTargetScanner.cs @@ -43,10 +43,15 @@ internal static async Task HasMigrationTargetAsync(string projectRoot, Can ? 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 = @@ -83,6 +88,29 @@ internal static async Task HasMigrationTargetAsync(string projectRoot, Can 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); @@ -137,6 +165,9 @@ internal static async Task HasMigrationTargetAsync(string projectRoot, Can assemblyReferenceDirectories, projectRoot, legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, + assemblyScopedLegacyAliasesByDirectory, + assemblyScopedLegacyToolInfoAliasesByDirectory, assemblyScopedCurrentToolContractsDirectories, assemblyScopedCurrentApplicationDirectories, assemblyScopedCurrentFirstPartyToolsDirectories, @@ -161,6 +192,7 @@ await CollectFastAssemblyReferenceRequirementsAsync( projectRoot, assemblyScopedCurrentToolContractsDirectories, assemblyScopedCurrentDomainDirectories, + assemblyScopedCurrentApplicationDirectories, assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDeclaredTypeNamesByDirectory, legacyAssemblyDirectories, @@ -182,6 +214,7 @@ await CollectFastAssemblyScopedCurrentToolContractsRequirementsAsync( projectRoot, assemblyScopedCurrentToolContractsDirectories, assemblyScopedCurrentDomainDirectories, + assemblyScopedCurrentApplicationDirectories, assemblyScopedCurrentApplicationAliasesByDirectory, assemblyDeclaredTypeNamesByDirectory, legacyAssemblyDirectories, @@ -266,12 +299,12 @@ await CollectFastAssemblyScopedCurrentDomainRequirementsAsync( asmdefDirectories, assemblyReferenceDirectories, legacyAssemblyDirectories, + assemblyScopedLegacyDirectories, new HashSet(StringComparer.Ordinal), new HashSet(StringComparer.Ordinal), new HashSet(StringComparer.Ordinal), - new HashSet(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), - new Dictionary(StringComparer.Ordinal), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyAliasesByDirectory), + CreateAssemblyScopedLegacyAliasesByDirectory(assemblyScopedLegacyToolInfoAliasesByDirectory), CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentApplicationAliasesByDirectory), CreateAssemblyScopedNamesByDirectory(assemblyScopedCurrentFirstPartyToolsAliasesByDirectory), CreateAssemblyScopedNamesByDirectory(assemblyDeclaredTypeNamesByDirectory), diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs index 6009c05ecd..10c155e52c 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindowResizer.cs @@ -23,9 +23,9 @@ internal ThirdPartyToolMigrationWizardWindowResizer(EditorWindow window, ScrollV Debug.Assert(window != null, "window must not be null"); Debug.Assert(mainScrollView != null, "mainScrollView must not be null"); - _window = window; + _window = window ?? throw new System.ArgumentNullException(nameof(window)); _root = window.rootVisualElement; - _mainScrollView = mainScrollView; + _mainScrollView = mainScrollView ?? throw new System.ArgumentNullException(nameof(mainScrollView)); } internal static Rect CreateCenteredRect(Rect bounds, Vector2 size) @@ -130,11 +130,6 @@ private void HandleGeometryChanged(GeometryChangedEvent evt) private void ResizeToContent() { - if (_mainScrollView == null) - { - return; - } - if (_root.layout.width <= 0f || _root.layout.height <= 0f) { return; From b0de187ad9d2c0583e156475f6d87a7333443f11 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 20 Jun 2026 11:51:41 +0900 Subject: [PATCH 13/13] Fix migration literal parsing review issues Handle interpolated raw string holes while scanning nested literals so raw delimiters inside hole expressions do not close the outer string early. Also fail fast on empty path ancestry arguments before path normalization. --- ...ThirdPartyToolMigrationFileServiceTests.cs | 15 +++ .../ThirdPartyToolMigrationRulesTests.cs | 14 ++ ...rtyToolMigrationInterpolatedStringRules.cs | 127 ++++++++++++++++++ .../ThirdPartyToolMigrationPathRules.cs | 17 ++- 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs index e56271cbdc..828f9d7644 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationFileServiceTests.cs @@ -137,6 +137,21 @@ public void IsSameOrChildPath_WhenChildContainsParentSegments_NormalizesBeforeCo 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() { diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs index da99dfcf22..840d363c32 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationRulesTests.cs @@ -126,6 +126,20 @@ public void MigrateCSharpSource_WhenLegacyToolDescriptionInterpolatesCommaExpres 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() { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs index e2cb2d32c2..9a022a9309 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationInterpolatedStringRules.cs @@ -120,6 +120,133 @@ private static int FindRawStringEndIndex(string source, int startIndex) 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) { diff --git a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs index 7eed0dc546..af5c460b70 100644 --- a/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs +++ b/Packages/src/Editor/Infrastructure/ThirdPartyToolMigration/ThirdPartyToolMigrationPathRules.cs @@ -27,8 +27,8 @@ internal static string[] GetRelativePathSegments(string filePath, string project internal 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"); + ThrowIfPathArgumentEmpty(childPath, nameof(childPath)); + ThrowIfPathArgumentEmpty(parentPath, nameof(parentPath)); string normalizedChildPath = NormalizeFullPath(childPath); string normalizedParentPath = NormalizeFullPath(parentPath); @@ -46,13 +46,24 @@ internal static bool IsSameOrChildPath(string childPath, string parentPath) private static string NormalizeFullPath(string path) { - Debug.Assert(path != null, "path must not be null"); + 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 == '\\'