diff --git a/.github/workflows/dead-code.yml b/.github/workflows/dead-code.yml new file mode 100644 index 000000000..c25e83ad4 --- /dev/null +++ b/.github/workflows/dead-code.yml @@ -0,0 +1,47 @@ +name: Dead Code + +on: + pull_request: + branches: [ main, v3-beta ] + paths: + - 'Packages/src/**/*.cs' + - 'tools/UnityCliLoop.DeadCodeScanner/**' + - 'tests/UnityCliLoop.DeadCodeScanner.Tests/**' + - 'scripts/check-dead-code.sh' + - '.github/workflows/dead-code.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + dead-code: + name: Dead Code Gate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 + with: + dotnet-version: 10.0.x + + - name: Test dead code scanner + run: dotnet test tests/UnityCliLoop.DeadCodeScanner.Tests/UnityCliLoop.DeadCodeScanner.Tests.csproj --configuration Release + + - name: Scan for dead code + run: > + scripts/check-dead-code.sh + --root . + --scope public + --include-types true + --include-members true + --include-locals true + --include-test-only true + --include-kept false + --format table + --fail-on high-confidence diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeForegroundWarmupRunner.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeForegroundWarmupRunner.cs index 6ae35352a..53f711efb 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeForegroundWarmupRunner.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeForegroundWarmupRunner.cs @@ -32,30 +32,6 @@ internal static async Task RunForegroundSequenceAsync( return true; } - internal static async Task TryRunBackgroundSequenceAsync( - IDynamicCodeExecutionRuntime runtime, - bool yieldToForegroundRequests, - CancellationToken ct) - { - System.Diagnostics.Debug.Assert(runtime != null, "runtime must not be null"); - - // Why: background probes must match the foreground sequence so whichever path succeeds - // first marks the same execution shape as ready. - foreach (string warmupCode in ExecuteDynamicCodeReadinessProbe.CreateReturnStringProbeCodes()) - { - DynamicCodeExecutionRequest request = CreateRequest( - warmupCode, - yieldToForegroundRequests); - (bool entered, ExecutionResult result) = await runtime.TryExecuteIfIdleAsync(request, ct).ConfigureAwait(false); - if (!entered || !result.Success) - { - return false; - } - } - - return true; - } - private static DynamicCodeExecutionRequest CreateRequest( string code, bool yieldToForegroundRequests) diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/ExecuteDynamicCodeReadinessProbe.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/ExecuteDynamicCodeReadinessProbe.cs index 1649dafd7..224772392 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/ExecuteDynamicCodeReadinessProbe.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/ExecuteDynamicCodeReadinessProbe.cs @@ -4,11 +4,6 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools // otherwise one path can look ready while the user's first return-string execution is still cold. public static class ExecuteDynamicCodeReadinessProbe { - public static string CreatePrimaryReturnStringProbeCode() - { - return DynamicCodeForegroundWarmupSnippets.ReturnStringShapes[0]; - } - public static string[] CreateReturnStringProbeCodes() { string[] source = DynamicCodeForegroundWarmupSnippets.ReturnStringShapes; diff --git a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs index 549fa3457..1054d0309 100644 --- a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs @@ -36,12 +36,5 @@ public static void ResetServerScopedServicesBeforeDomainReload() { ExecuteDynamicCodeEditorStartup.ResetServerScopedServicesBeforeDomainReload(); } - - public static string CreateExecuteDynamicCodeReadinessProbeCode() - { - // Why: composition root can only depend on the bundled-tool facade assembly, - // so the dynamic-code assembly keeps ownership of the actual probe source shape. - return ExecuteDynamicCodeReadinessProbe.CreatePrimaryReturnStringProbeCode(); - } } } diff --git a/docs/dead-code-scanner.md b/docs/dead-code-scanner.md index 2a146fd7d..200960273 100644 --- a/docs/dead-code-scanner.md +++ b/docs/dead-code-scanner.md @@ -17,6 +17,18 @@ For a broader member/local-variable pass, run: dotnet run --project tools/UnityCliLoop.DeadCodeScanner -- --scope public --include-types true --include-members true --include-locals true --include-test-only true --include-kept false --format table ``` +## CI gate + +`.github/workflows/dead-code.yml` runs automatically on pull requests that +target `main` or `v3-beta` and touch `Packages/src/**/*.cs`, the scanner +itself, its tests, `scripts/check-dead-code.sh`, or the workflow file. + +The gate uses `--fail-on high-confidence`, so CI fails only for +`Unused`, `UnusedPrivateMember`, and `UnusedLocal`. + +`PublicCandidate` and `TestOnly` do not fail CI. Those findings need +manual review of non-C# references and cannot be decided mechanically. + ## Interpreting the output Interpret scanner output conservatively: diff --git a/tests/UnityCliLoop.DeadCodeScanner.Tests/CommandLineOptionsTests.cs b/tests/UnityCliLoop.DeadCodeScanner.Tests/CommandLineOptionsTests.cs new file mode 100644 index 000000000..23b21d925 --- /dev/null +++ b/tests/UnityCliLoop.DeadCodeScanner.Tests/CommandLineOptionsTests.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using UnityCliLoop.DeadCodeScanner; + +namespace UnityCliLoop.DeadCodeScanner.Tests +{ + [TestFixture] + public sealed class CommandLineOptionsTests + { + // Verifies that --fail-on high-confidence enables the CI fail flag and --fail-on none leaves it off. + [Test] + public void Parse_WhenFailOnIsHighConfidenceOrNone_ShouldSetFailOnHighConfidenceFlag() + { + ScanOptions highConfidenceOptions = CommandLineOptions.Parse(new[] + { + "--fail-on", + "high-confidence" + }); + ScanOptions noneOptions = CommandLineOptions.Parse(new[] + { + "--fail-on", + "none" + }); + + Assert.That(highConfidenceOptions.FailOnHighConfidence, Is.True); + Assert.That(noneOptions.FailOnHighConfidence, Is.False); + } + } +} diff --git a/tests/UnityCliLoop.DeadCodeScanner.Tests/DeadCodeScannerTests.cs b/tests/UnityCliLoop.DeadCodeScanner.Tests/DeadCodeScannerTests.cs index b9f14cd46..67750a1a5 100644 --- a/tests/UnityCliLoop.DeadCodeScanner.Tests/DeadCodeScannerTests.cs +++ b/tests/UnityCliLoop.DeadCodeScanner.Tests/DeadCodeScannerTests.cs @@ -107,6 +107,19 @@ public async Task ScanAsync_WhenProductionSymbolIsOnlyUsedByAssets_ShouldReportT && issue.FullName.Contains("TestOnlyFactory", StringComparison.Ordinal)), Is.True); } + // Verifies that default-scope unused private findings are treated as high-confidence deletion candidates for the CI gate. + [Test] + public async Task ScanAsync_WhenUsingDefaultPrivateScope_ShouldReportHighConfidenceDeletionCandidates() + { + DeadCodeScanner scanner = new(); + ScanOptions options = ScanOptions.Default(_rootPath); + + System.Collections.Generic.IReadOnlyList issues = + await scanner.ScanAsync(options, CancellationToken.None); + + Assert.That(issues.Any(issue => issue.IsHighConfidenceDeletionCandidate()), Is.True); + } + // Verifies that Unity or reflection entry points can be reported separately when requested. [Test] public async Task ScanAsync_WhenIncludingKeptSymbols_ShouldReportUnityToolAsKept()