diff --git a/.fallowrc.json b/.fallowrc.json index ebe4f4947..f4d2221b9 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -87,6 +87,34 @@ { "file": "src/cloud-webdriver.ts", "exports": ["CLOUD_WEBDRIVER_PROVIDERS"] + }, + { + "file": "src/daemon/handlers/lease.ts", + "exports": ["handleLeaseCommands"] + }, + { + "file": "src/daemon/handlers/session.ts", + "exports": ["handleSessionCommands"] + }, + { + "file": "src/daemon/handlers/snapshot.ts", + "exports": ["handleSnapshotCommands"] + }, + { + "file": "src/daemon/handlers/react-native.ts", + "exports": ["handleReactNativeCommands"] + }, + { + "file": "src/daemon/handlers/record-trace.ts", + "exports": ["handleRecordTraceCommands"] + }, + { + "file": "src/daemon/handlers/find.ts", + "exports": ["handleFindCommands"] + }, + { + "file": "src/daemon/handlers/interaction.ts", + "exports": ["handleInteractionCommands"] } ], "usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52d9747b6..4fc9e936d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,9 @@ jobs: FALLOW_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} run: pnpm check:fallow --base "$FALLOW_BASE" + - name: Check for production-unused exports + run: pnpm check:production-exports + coverage: # Runs the full unit + provider-integration suites under coverage with # thresholds, so a separate unit-tests job would rerun the same tests. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c4c351f6..e9222fdba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,23 @@ intentionally accepting a finding. - `pnpm fallow:all` — full-tree summary, includes grandfathered legacy findings - `pnpm fallow:baseline` — regenerate baselines (only to intentionally accept a finding) +Code quality (production exports): `pnpm check:production-exports` runs Fallow's native +production graph, which excludes test/story/dev files, and fails when a new export has no +production consumer. This includes the test-only-export bug class that shipped in #1199's first +revision, while also catching exports that are unreachable from every graph. Fallow's +`ignoreExportsUsedInFile` option in the gate's inherited config keeps exports with a real +same-file consumer out of this report without weakening the general Fallow audit. The checked-in +native baseline lives at `fallow-baselines/production-unused-exports.json`. + +Fix a finding by wiring the export into production or removing the unnecessary export/code. For +an intentional test seam, explain why in a source comment and put +`// fallow-ignore-next-line unused-export` directly above the export. Run +`pnpm check:production-exports:baseline` only for a deliberate reviewed baseline migration or to +remove stale entries; additions accept new production-unreachable exports and should be rare. +Production usage reached only through dynamic property access remains invisible to a static +import graph, so register those exports in `.fallowrc.json` `ignoreExports` instead (as with the +daemon route handlers loaded through `typeof import()`). + Optional device selectors for tests: - `ANDROID_DEVICE=Pixel_9_Pro_XL` or `ANDROID_SERIAL=emulator-5554` diff --git a/fallow-baselines/production-unused-exports.json b/fallow-baselines/production-unused-exports.json new file mode 100644 index 000000000..0185cf3eb --- /dev/null +++ b/fallow-baselines/production-unused-exports.json @@ -0,0 +1,130 @@ +{ + "unused_files": [], + "unused_exports": [ + "src/cli/parser/args.ts:parseArgs", + "src/cli/parser/command-suggestions.ts:listCommandAliasSuggestionEntries", + "src/cloud-webdriver/aws-device-farm.ts:getAwsDeviceFarmWebDriverCapabilities", + "src/cloud-webdriver/aws-device-farm.ts:createAwsDeviceFarmWebDriverRuntime", + "src/cloud-webdriver/browserstack.ts:getBrowserStackWebDriverCapabilities", + "src/cloud-webdriver/browserstack.ts:createBrowserStackWebDriverRuntime", + "src/commands/capture/index.ts:alertCliReader", + "src/commands/capture/index.ts:alertDaemonWriter", + "src/commands/capture/index.ts:diffCliReader", + "src/commands/capture/index.ts:screenshotCliReader", + "src/commands/capture/index.ts:screenshotDaemonWriter", + "src/commands/capture/index.ts:settingsCliReader", + "src/commands/capture/index.ts:settingsDaemonWriter", + "src/commands/capture/index.ts:snapshotCliReader", + "src/commands/capture/index.ts:waitCliReader", + "src/commands/capture/index.ts:waitDaemonWriter", + "src/commands/command-metadata.ts:listCommandMetadataNames", + "src/commands/command-surface.ts:listExecutableCommandNames", + "src/commands/index.ts:ref", + "src/commands/index.ts:selector", + "src/commands/index.ts:commands", + "src/commands/interaction/runtime/selector-read.ts:selector", + "src/commands/interaction/runtime/selector-read.ts:ref", + "src/core/command-descriptor/registry.ts:listDescriptorCatalogCommandNames", + "src/core/command-descriptor/registry.ts:listDescriptorDispatchCommandNames", + "src/core/command-descriptor/registry.ts:listCapabilityCheckedCommandNames", + "src/core/command-descriptor/registry.ts:listCommandResponseDataTransforms", + "src/core/dispatch.ts:listRegisteredDispatchCommandNames", + "src/core/platform-plugin/plugin.ts:registeredPlatforms", + "src/core/scroll-gesture.ts:buildSwipeGesturePlan", + "src/daemon/app-log.ts:APP_LOG_PID_FILENAME", + "src/daemon/app-log.ts:cleanupStaleAppLogProcesses", + "src/daemon/app-log.ts:assertAndroidPackageArgSafe", + "src/daemon/app-log.ts:buildAppleLogPredicate", + "src/daemon/app-log.ts:buildIosDeviceConsoleLaunchArgs", + "src/daemon/app-log.ts:buildIosSimulatorLogStreamArgs", + "src/daemon/client/daemon-client.ts:computeDaemonCodeSignature", + "src/daemon/client/daemon-client.ts:downloadRemoteArtifact", + "src/daemon/client/daemon-client.ts:cleanupFailedDaemonStartupMetadata", + "src/daemon/client/daemon-client.ts:resolveDaemonStartupHint", + "src/daemon/client/daemon-client.ts:canConnectSocket", + "src/daemon/client/daemon-client.ts:resolveDaemonRequestTimeoutMs", + "src/daemon/client/daemon-client.ts:shouldResetDaemonAfterRequestTimeout", + "src/daemon/handlers/find.ts:parseFindArgs", + "src/daemon/handlers/snapshot-capture.ts:buildSnapshotVisibility", + "src/daemon/lease-context.ts:buildLeaseDiagnosticsContext", + "src/daemon/runtime-hints.ts:resolveRuntimeTransportHints", + "src/kernel/contracts.ts:daemonCommandRequestSchema", + "src/kernel/contracts.ts:leaseAllocateSchema", + "src/kernel/contracts.ts:leaseHeartbeatSchema", + "src/kernel/contracts.ts:leaseReleaseSchema", + "src/kernel/device.ts:isPlatform", + "src/platforms/android/multitouch-helper.ts:resetAndroidMultiTouchHelperInstallCache", + "src/platforms/android/perf-frame.ts:parseAndroidFramePerfSample", + "src/platforms/android/perf.ts:parseAndroidFramePerfSample", + "src/platforms/android/snapshot-helper-artifact.ts:prepareAndroidSnapshotHelperArtifactFromManifestUrl", + "src/platforms/android/snapshot-helper-capture.ts:parseAndroidSnapshotHelperXml", + "src/platforms/android/snapshot-helper-install.ts:resetAndroidSnapshotHelperInstallCache", + "src/platforms/android/snapshot-helper-session.ts:resetAndroidSnapshotHelperSessions", + "src/platforms/android/snapshot-helper.ts:prepareAndroidSnapshotHelperArtifactFromManifestUrl", + "src/platforms/android/snapshot-helper.ts:verifyAndroidSnapshotHelperArtifact", + "src/platforms/android/snapshot-helper.ts:parseAndroidSnapshotHelperOutput", + "src/platforms/android/snapshot-helper.ts:parseAndroidSnapshotHelperXml", + "src/platforms/android/snapshot-helper.ts:resetAndroidSnapshotHelperSessions", + "src/platforms/android/snapshot-helper.ts:resolveAndroidSnapshotHelperSessionRequestTimeoutMs", + "src/platforms/android/snapshot-helper.ts:resetAndroidSnapshotHelperInstallCache", + "src/platforms/apple/core/app-resolution.ts:maybeResolveIosDevicectlHint", + "src/platforms/apple/core/apps.ts:shouldFallbackToRunnerForIosScreenshot", + "src/platforms/apple/core/apps.ts:shouldRetryIosSimulatorScreenshot", + "src/platforms/apple/core/devices.ts:createLocalAppleToolProvider", + "src/platforms/apple/core/devices.ts:withAppleToolProvider", + "src/platforms/apple/core/runner/runner-artifact.ts:ensureXctestrun", + "src/platforms/apple/core/runner/runner-client.ts:isRetryableRunnerError", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerEarlyExitHint", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerBuildFailureHint", + "src/platforms/apple/core/runner/runner-client.ts:shouldRetryRunnerConnectError", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerDestination", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerBuildDestination", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerMaxConcurrentDestinationsFlag", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerSigningBuildSettings", + "src/platforms/apple/core/runner/runner-client.ts:resolveRunnerBundleBuildSettings", + "src/platforms/apple/core/runner/runner-client.ts:assertSafeDerivedCleanup", + "src/platforms/apple/core/runner/runner-recycle-ledger.ts:resetRunnerRecycleLedgerForTests", + "src/platforms/apple/core/runner/runner-transport.ts:clearDeviceTunnelIpCache", + "src/platforms/apple/core/runner/runner-xctestrun.ts:ensureXctestrun", + "src/platforms/apple/core/runner/runner-xctestrun.ts:findXctestrun", + "src/platforms/apple/core/runner/runner-xctestrun.ts:scoreXctestrunCandidate", + "src/platforms/apple/core/runner/runner-xctestrun.ts:xctestrunReferencesProjectRoot", + "src/platforms/apple/core/runner/runner-xctestrun.ts:acquireRunnerXctestrunCacheLock", + "src/platforms/apple/core/runner/runner-xctestrun.ts:resolveRunnerCacheMetadataPath", + "src/platforms/apple/core/runner/runner-xctestrun.ts:shouldDeleteRunnerDerivedRootEntry", + "src/platforms/apple/core/runner/runner-xctestrun.ts:writeRunnerCacheMetadata", + "src/platforms/apple/core/runner/runner-xctestrun.ts:resolveRunnerPerformanceBuildSettings", + "src/platforms/apple/core/runner/runner-xctestrun.ts:resolveRunnerSandboxBuildArgs", + "src/platforms/apple/core/runner/runner-xctestrun.ts:resolveXcodebuildSimulatorDeviceSetPath", + "src/platforms/apple/core/screenshot.ts:prepareSimulatorStatusBarForScreenshot", + "src/platforms/install-source.ts:ARCHIVE_EXTENSIONS", + "src/platforms/linux/linux-env.ts:resetInputToolCache", + "src/provider-device-runtime.ts:setActiveProviderDeviceRuntimes", + "src/remote/remote-config.ts:resolveRemoteConfigPath", + "src/utils/ttl-memo.ts:resetAllProcessMemosForTests" + ], + "unused_types": [], + "private_type_leaks": [], + "unused_dependencies": [], + "unused_dev_dependencies": [], + "circular_dependencies": [], + "re_export_cycles": [], + "unused_optional_dependencies": [], + "unused_enum_members": [], + "unused_class_members": [], + "unresolved_imports": [], + "unlisted_dependencies": [], + "duplicate_exports": [], + "type_only_dependencies": [], + "test_only_dependencies": [], + "boundary_violations": [], + "boundary_coverage_violations": [], + "boundary_call_violations": [], + "policy_violations": [], + "stale_suppressions": [], + "unused_catalog_entries": [], + "empty_catalog_groups": [], + "unresolved_catalog_references": [], + "unused_dependency_overrides": [], + "misconfigured_dependency_overrides": [] +} diff --git a/fallow-production-exports.json b/fallow-production-exports.json new file mode 100644 index 000000000..e8617e8bb --- /dev/null +++ b/fallow-production-exports.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", + "extends": ["./.fallowrc.json"], + "ignoreExportsUsedInFile": true, + "rules": { + "unused-exports": "warn" + } +} diff --git a/package.json b/package.json index 8f5e2bf80..c099c5cf1 100644 --- a/package.json +++ b/package.json @@ -114,11 +114,13 @@ "check:fallow": "fallow audit", "check:layering": "node --experimental-strip-types --test scripts/layering/model.test.ts && node --experimental-strip-types scripts/layering/check.ts", "check:layering:baseline": "node --experimental-strip-types scripts/layering/check.ts --update-baseline", + "check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --baseline fallow-baselines/production-unused-exports.json --fail-on-issues", + "check:production-exports:baseline": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --save-baseline fallow-baselines/production-unused-exports.json --summary", "check:quick": "pnpm lint && pnpm typecheck", "sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs", "check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check", "version": "node scripts/sync-mcp-metadata.mjs && git add server.json", - "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:mcp-metadata && pnpm build", + "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build", "check:unit": "pnpm test:unit && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:apple-runner:npm && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm",