Public: anyone can read this; the crew can change it.
Automate a review finding as a lint or test: **Dead code.** `clearState` is never called; remove it. #11850
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/11842-automate-a-review-finding-as-a-lint-or-t → main 6 commits fork 1 thread · 1 unresolved
3 files
changed +303
−0
scripts/unused-exports-allow.json added +204
| @@ -0,0 +1,204 @@ | ||
| 1 | [ | |
| 2 | "src/apps/check-memory.ts usePodMemoryFile", | |
| 3 | "src/apps/dns_provider.ts EDITABLE_TYPES", | |
| 4 | "src/apps/egress.ts resetEgressTotals", | |
| 5 | "src/apps/egress_refused.ts flushRefused", | |
| 6 | "src/apps/manifest.ts declaresApp", | |
| 7 | "src/apps/relay_wait.ts clearRelayWaits", | |
| 8 | "src/apps/toolbar.ts TOOLBAR_PATH", | |
| 9 | "src/arcade/host.ts arcadeOn", | |
| 10 | "src/billing/orders.ts bySession", | |
| 11 | "src/billing/orders.ts recentOrders", | |
| 12 | "src/billing/sync.ts setStripeFetch", | |
| 13 | "src/boards/model_fill.ts useModel", | |
| 14 | "src/checks/doctor.ts isDoctorHold", | |
| 15 | "src/checks/order_dependent.ts orderDependent", | |
| 16 | "src/checks/queue.ts gateDeadlineForTest", | |
| 17 | "src/checks/queue.ts gateRuns", | |
| 18 | "src/checks/scans.ts SCAN_BUDGET_SECONDS", | |
| 19 | "src/checks/scans.ts scanSeconds", | |
| 20 | "src/checks/service.ts provideChecksForTest", | |
| 21 | "src/contract/audit.ts identitiesOf", | |
| 22 | "src/core/accounts.ts edgeOnlyAccounts", | |
| 23 | "src/core/capacity.ts provideCpusForTest", | |
| 24 | "src/core/chapters.ts forgetFrozenItems", | |
| 25 | "src/core/credit.ts clearCreditCache", | |
| 26 | "src/core/device_grants.ts appIdOfSlug", | |
| 27 | "src/core/device_grants.ts grantedSlugs", | |
| 28 | "src/core/feed_chips.ts noChips", | |
| 29 | "src/core/fence.ts heldIn", | |
| 30 | "src/core/idempotency.ts clearInFlight", | |
| 31 | "src/core/idempotency.ts resetPruneClock", | |
| 32 | "src/core/issue_teams.ts teamsAnswered", | |
| 33 | "src/core/issues.ts countIssues", | |
| 34 | "src/core/issues.ts issueRefKey", | |
| 35 | "src/core/issues.ts searchIssuesVisibleTo", | |
| 36 | "src/core/looks.ts isLookKind", | |
| 37 | "src/core/meter.ts PLACEMENTS", | |
| 38 | "src/core/oidc.ts rotateKeys", | |
| 39 | "src/core/oidc.ts verifyIdToken", | |
| 40 | "src/core/parent_releases.ts refreshIdle", | |
| 41 | "src/core/pulls.ts mergeReadingsSettled", | |
| 42 | "src/core/pulls.ts pullsForIssues", | |
| 43 | "src/core/roles.ts rolesTable", | |
| 44 | "src/core/route_timing.ts readTiming", | |
| 45 | "src/core/sealed.ts sealedById", | |
| 46 | "src/core/search.ts grepCount", | |
| 47 | "src/core/search.ts grepRepo", | |
| 48 | "src/core/sessions.ts sessionUser", | |
| 49 | "src/core/starts.ts startOfAccount", | |
| 50 | "src/core/subscriptions.ts listAllGrants", | |
| 51 | "src/core/team-metrics.ts keptFigures", | |
| 52 | "src/core/timers.ts pendingTimers", | |
| 53 | "src/core/transfer.ts transfersOf", | |
| 54 | "src/core/usage.ts USAGE_BY", | |
| 55 | "src/core/usage.ts USAGE_OUTCOMES", | |
| 56 | "src/core/webhooks.ts deliveryBusy", | |
| 57 | "src/crew/ceiling_stagger.ts forgetStagger", | |
| 58 | "src/crew/conversation_confirm.ts CONFIRM_TTL_MS", | |
| 59 | "src/crew/first_look.ts useLookModel", | |
| 60 | "src/crew/hot_files.ts rememberPullFiles", | |
| 61 | "src/crew/hot_history.ts forgetHeat", | |
| 62 | "src/crew/hot_history.ts sampleTimes", | |
| 63 | "src/crew/models.ts modelLabel", | |
| 64 | "src/crew/running.ts runningAgentRuns", | |
| 65 | "src/crew/sandbox_cause.ts skipReason", | |
| 66 | "src/crew/week_read.ts forgetWeek", | |
| 67 | "src/data/mask.ts isSecretColumn", | |
| 68 | "src/data/query.ts queriesRunning", | |
| 69 | "src/design/qr.ts QR_MAX_BYTES", | |
| 70 | "src/display/items.ts itemsOfKeys", | |
| 71 | "src/display/items.ts keepUnnamed", | |
| 72 | "src/display/known.ts withoutDisplay", | |
| 73 | "src/display/launch.ts coverUrl", | |
| 74 | "src/display/launch.ts downloadUrl", | |
| 75 | "src/display/launch.ts playUrl", | |
| 76 | "src/display/launch.ts shotsUrl", | |
| 77 | "src/display/selections.ts SHOWCASED", | |
| 78 | "src/display/selections.ts displaySelections", | |
| 79 | "src/display/selections.ts selectionsHolding", | |
| 80 | "src/efficiency/installs.ts resetInstalls", | |
| 81 | "src/feed/chips.ts chipOfKind", | |
| 82 | "src/feed/likes.ts ledgerCount", | |
| 83 | "src/flow/alarm_drift.ts alarmDrift", | |
| 84 | "src/flow/alarm_drift.ts clearDriftCache", | |
| 85 | "src/flow/handovers.ts handoversSays", | |
| 86 | "src/flow/keeper.ts listAttempts", | |
| 87 | "src/flow/kpi_words.ts trafficWords", | |
| 88 | "src/flow/reach.ts NO_REACH", | |
| 89 | "src/flow/shared_red.ts sharedRedSays", | |
| 90 | "src/git/coauthors.ts creditsOf", | |
| 91 | "src/git/diff-memory.ts diffMemoryCounts", | |
| 92 | "src/git/diff-memory.ts forgetDiffs", | |
| 93 | "src/git/diff-memory.ts mergeBaseCount", | |
| 94 | "src/git/local.ts aheadBehind", | |
| 95 | "src/git/local.ts isClean", | |
| 96 | "src/git/local.ts isRepo", | |
| 97 | "src/git/local.ts statusLetters", | |
| 98 | "src/git/merge.ts setMergeTreeAcceptsMergeBaseForTest", | |
| 99 | "src/git/path_listing.ts keptListings", | |
| 100 | "src/git/read.ts closeAllReaders", | |
| 101 | "src/git/read.ts readerPoolSize", | |
| 102 | "src/git/read.ts sweepReaderPool", | |
| 103 | "src/http/crawl.ts crawlAgentCount", | |
| 104 | "src/http/crawl.ts setCrawlClock", | |
| 105 | "src/http/mcp.ts toolNames", | |
| 106 | "src/http/mcp.ts toolRegistry", | |
| 107 | "src/http/mcp_table.ts tableIn", | |
| 108 | "src/http/script_errors.ts SCRIPT_ERROR_PATH", | |
| 109 | "src/http/sse.ts resetSseDroppedCount", | |
| 110 | "src/http/sse.ts sseDroppedCount", | |
| 111 | "src/http/strangers.ts heldChanged", | |
| 112 | "src/http/strangers.ts setStrangerClock", | |
| 113 | "src/http/strangers.ts setStrangerLimits", | |
| 114 | "src/http/stripe_hook.ts refusedCount", | |
| 115 | "src/http/stripe_hook.ts setStripeHookCaps", | |
| 116 | "src/kernel/destroy_reach.ts unitsOf", | |
| 117 | "src/kernel/disk_reading.ts provideDiskForTest", | |
| 118 | "src/kernel/log.ts clearErrors", | |
| 119 | "src/kernel/migrate.ts notAdditive", | |
| 120 | "src/kernel/protection.ts resetProtection", | |
| 121 | "src/kernel/result.ts andThen", | |
| 122 | "src/kernel/result.ts trapAsync", | |
| 123 | "src/kernel/result.ts unwrapOr", | |
| 124 | "src/looks/main_looks.ts mainLook", | |
| 125 | "src/machines/shell.ts liveSessions", | |
| 126 | "src/maintenance/standby.ts forgetStandby", | |
| 127 | "src/origins/ahead.ts aheadBusy", | |
| 128 | "src/origins/git-mirror.ts mirrorOutboundForTest", | |
| 129 | "src/origins/git-mirror.ts provideMirrorOutboundForTest", | |
| 130 | "src/origins/jobs.ts listJobs", | |
| 131 | "src/origins/jobs.ts resetJobs", | |
| 132 | "src/origins/milestones.ts dueOf", | |
| 133 | "src/origins/oauth.ts pruneFlows", | |
| 134 | "src/origins/providers/index.ts forgetProviders", | |
| 135 | "src/origins/sync.ts syncsSettled", | |
| 136 | "src/packs/catalog.ts packNamed", | |
| 137 | "src/perf/budgets.ts EXPLORE_READ_BUDGET", | |
| 138 | "src/perf/page_budget_read.ts pageBudgetRows", | |
| 139 | "src/perf/views_store.ts pendingViews", | |
| 140 | "src/release/behind.ts readNewest", | |
| 141 | "src/release/journey.ts walkJourney", | |
| 142 | "src/release/landings.ts forgetLive", | |
| 143 | "src/release/landings.ts keepLive", | |
| 144 | "src/runs/log.ts runLogListenerCount", | |
| 145 | "src/runs/log.ts runLogTapCount", | |
| 146 | "src/runs/stoppable.ts runningRun", | |
| 147 | "src/spawn/child-apps.ts childRuntime", | |
| 148 | "src/spawn/clients.ts useChildFetch", | |
| 149 | "src/spawn/door.ts usePlacement", | |
| 150 | "src/spawn/door.ts useProbe", | |
| 151 | "src/spawn/included_credit.ts worstCaseDailyUsd", | |
| 152 | "src/spawn/parent_credential.ts keepParentCredential", | |
| 153 | "src/spawn/pod.ts renderPod", | |
| 154 | "src/spawn/rate.ts resetAnonSpawns", | |
| 155 | "src/spawn/reach.ts useReachByDefault", | |
| 156 | "src/spawn/usage.ts forgetSelfCpu", | |
| 157 | "src/story/records.ts isKnownRecord", | |
| 158 | "src/story/records.ts parseRecords", | |
| 159 | "src/system/flow_cache.ts forgetFlow", | |
| 160 | "src/system/live.ts useLive", | |
| 161 | "src/system/telemetry_reads.ts resetTelemetryReads", | |
| 162 | "src/tui/gallery.ts galleryFrame", | |
| 163 | "src/tui/local/common.ts localBusy", | |
| 164 | "src/tui/local/common.ts localIdle", | |
| 165 | "src/tui/screens/common.ts COMMENT_MARK", | |
| 166 | "src/tui/screens/external.ts forgetEditor", | |
| 167 | "src/tui/term/ansi.ts ERASE_LINE_RIGHT", | |
| 168 | "src/tui/term/style.ts withAttr", | |
| 169 | "src/tui/term/style.ts withBg", | |
| 170 | "src/tui/term/style.ts withFg", | |
| 171 | "src/tui/term/style.ts withoutAttr", | |
| 172 | "src/tui/term/terminal.ts diffScreens", | |
| 173 | "src/tui/ui/editor.ts STATUS_BOLD", | |
| 174 | "src/tui/ui/editor.ts expandLine", | |
| 175 | "src/tui/ui/icons.ts iconNames", | |
| 176 | "src/tui/ui/image.ts clearImage", | |
| 177 | "src/tui/ui/markdown.ts lineText", | |
| 178 | "src/web/chrome.ts stickyAction", | |
| 179 | "src/web/code_rows_pool.ts workersForTest", | |
| 180 | "src/web/diff-cache.ts clearDiffCache", | |
| 181 | "src/web/go_pages.ts goPagesFor", | |
| 182 | "src/web/head-reads.ts clearHeadReads", | |
| 183 | "src/web/map_cache.ts clearMapCache", | |
| 184 | "src/web/map_cache.ts mapCacheStats", | |
| 185 | "src/web/map_cache.ts planBuildCount", | |
| 186 | "src/web/markdown.ts markdownMemoStats", | |
| 187 | "src/web/markdown.ts markdownWorkDone", | |
| 188 | "src/web/og/raster.ts useLoader", | |
| 189 | "src/web/pages/flow-waiting.ts forgetUnproposed", | |
| 190 | "src/web/pages/repo.ts clearReadmeCache", | |
| 191 | "src/web/pages/site.ts signupAllowed", | |
| 192 | "src/web/pages/tree_cache.ts clearTreeCache", | |
| 193 | "src/web/pinned.ts deployToHref", | |
| 194 | "src/web/render/kinds.ts KNOWN_EXTENSIONS", | |
| 195 | "src/web/script_keys.ts SCRIPT_KEYS", | |
| 196 | "src/web/share_target.ts forgetShares", | |
| 197 | "src/web/space_view.ts GLIDE_MS", | |
| 198 | "src/web/studio/home_cache.ts cachedVitals", | |
| 199 | "src/web/studio/home_cache.ts setHomeClock", | |
| 200 | "src/web/studio/home_last.ts forgetHome", | |
| 201 | "src/web/studio/review_evidence.ts rememberStat", | |
| 202 | "src/web/studio/shelf.ts productionFactsForRepos", | |
| 203 | "src/web/sw.ts swSource" | |
| 204 | ] |
scripts/unused-exports.ts added +53
| @@ -0,0 +1,53 @@ | ||
| 1 | /** | |
| 2 | * Dead code (#11842): an exported function, const or class under `src/` whose | |
| 3 | * name appears nowhere else in `src/`, `scripts/` or `bench/` outside tests. | |
| 4 | * Reviewers kept writing "X is never called; remove it" (#9639, #11255, | |
| 5 | * #11809, #10611); this says it for them. A test that alone calls it does not | |
| 6 | * count: done means reachable (docs/VISION.md). | |
| 7 | * | |
| 8 | * The match is by name across the whole non-test corpus, so a name used | |
| 9 | * anywhere (even in its own file) is alive; that errs toward silence, never | |
| 10 | * toward a false accusation. `scripts/unused-exports-allow.json` lists | |
| 11 | * "path name" pairs main carries today; `--write` prunes the ones that are | |
| 12 | * no longer dead (the test only reports those, so pulls do not conflict). | |
| 13 | */ | |
| 14 | ||
| 15 | import { readFileSync, writeFileSync } from "node:fs"; | |
| 16 | import { join } from "node:path"; | |
| 17 | ||
| 18 | const DECL = /^export\s+(?:async\s+)?(?:function\*?|const|let|class)\s+([\w$]+)/gm; | |
| 19 | const WORD = /[A-Za-z_$][\w$]*/g; | |
| 20 | ||
| 21 | /** Non-test sources, by path: what counts as a caller. */ | |
| 22 | function corpus(root: string): Map<string, string> { | |
| 23 | const files = [...new Bun.Glob("{src,scripts,bench}/**/*.ts").scanSync({ cwd: root })].filter( | |
| 24 | (f) => !f.endsWith(".test.ts") && !f.endsWith(".d.ts"), | |
| 25 | ); | |
| 26 | return new Map(files.map((f) => [f, readFileSync(join(root, f), "utf8")])); | |
| 27 | } | |
| 28 | ||
| 29 | /** `"path name"` for each src/ export nothing but its declaration mentions, sorted. */ | |
| 30 | export function unusedExports(root: string): string[] { | |
| 31 | const text = corpus(root); | |
| 32 | const counts = new Map<string, number>(); | |
| 33 | for (const t of text.values()) for (const m of t.matchAll(WORD)) counts.set(m[0], (counts.get(m[0]) ?? 0) + 1); | |
| 34 | const dead: string[] = []; | |
| 35 | for (const [file, t] of text) { | |
| 36 | if (!file.startsWith("src/")) continue; | |
| 37 | for (const m of t.matchAll(DECL)) if ((counts.get(m[1]!) ?? 0) < 2) dead.push(`${file} ${m[1]}`); | |
| 38 | } | |
| 39 | return dead.sort(); | |
| 40 | } | |
| 41 | ||
| 42 | if (import.meta.main) { | |
| 43 | const list = unusedExports(new URL("..", import.meta.url).pathname); | |
| 44 | if (process.argv.includes("--write")) { | |
| 45 | // Prune only: entries still dead stay, so a new dead export is never silently allowed. | |
| 46 | const file = new URL("./unused-exports-allow.json", import.meta.url).pathname; | |
| 47 | const kept = (JSON.parse(readFileSync(file, "utf8")) as string[]).filter((e) => list.includes(e)); | |
| 48 | writeFileSync(file, JSON.stringify(kept, null, 2) + "\n"); | |
| 49 | console.log(`kept ${kept.length}`); | |
| 50 | } else | |
| 51 | if (process.argv.includes("--json")) console.log(JSON.stringify(list, null, 2)); | |
| 52 | else for (const d of list) console.log(d); | |
| 53 | } |
test/unused-exports.test.ts added +46
| @@ -0,0 +1,46 @@ | ||
| 1 | /** | |
| 2 | * Dead code (#11842): an export under src/ that nothing calls fails here | |
| 3 | * unless `scripts/unused-exports-allow.json` lists it, and a listed export | |
| 4 | * that has gained a caller or been deleted fails too, so the list only shrinks. | |
| 5 | */ | |
| 6 | ||
| 7 | import { expect, test } from "bun:test"; | |
| 8 | import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; | |
| 9 | import { tmpdir } from "node:os"; | |
| 10 | import { dirname, join } from "node:path"; | |
| 11 | import { unusedExports } from "../scripts/unused-exports.ts"; | |
| 12 | import allowed from "../scripts/unused-exports-allow.json"; | |
| 13 | ||
| 14 | const found = unusedExports(new URL("..", import.meta.url).pathname); | |
| 15 | ||
| 16 | test("dead code: every export under src/ has a caller, or is allowed", () => { | |
| 17 | const fresh = found.filter((f) => !allowed.includes(f)); | |
| 18 | expect( | |
| 19 | fresh, | |
| 20 | `Dead code: nothing calls these exports; remove them or use them (path name); only if truly intended, add them to scripts/unused-exports-allow.json: ${fresh.join(", ")}`, | |
| 21 | ).toEqual([]); | |
| 22 | }); | |
| 23 | ||
| 24 | test("an allowed export that is now used or gone is reported, not failed", () => { | |
| 25 | // Non-blocking on purpose: a pull that gives a listed export a caller, or deletes it, | |
| 26 | // must not have to edit the shared list, or concurrent pulls conflict on it. | |
| 27 | const stale = allowed.filter((f) => !found.includes(f)); | |
| 28 | if (stale.length) console.warn(`no longer dead; \`bun scripts/unused-exports.ts --write\` prunes: ${stale.join(", ")}`); | |
| 29 | expect(Array.isArray(stale)).toBe(true); | |
| 30 | }); | |
| 31 | ||
| 32 | test("unusedExports() flags a name mentioned only at its declaration; tests do not count", () => { | |
| 33 | const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), "unused-exports-")); | |
| 34 | const put = (path: string, text: string) => { | |
| 35 | mkdirSync(dirname(join(dir, path)), { recursive: true }); | |
| 36 | writeFileSync(join(dir, path), text); | |
| 37 | }; | |
| 38 | try { | |
| 39 | put("src/a.ts", "export function clearState() {}\nexport const used = 1;\nexport async function onlyTested() {}\n"); | |
| 40 | put("src/b.ts", 'import { used } from "./a.ts";\nexport const x = used;\n'); | |
| 41 | put("test/a.test.ts", "onlyTested(); clearState();\n"); | |
| 42 | expect(unusedExports(dir)).toEqual(["src/a.ts clearState", "src/a.ts onlyTested", "src/b.ts x"]); | |
| 43 | } finally { | |
| 44 | rmSync(dir, { recursive: true, force: true }); | |
| 45 | } | |
| 46 | }); |
new above
offline
This check makes every pull that uses or deletes a listed export edit the shared allow JSON, which conflicts across concurrent pulls. Do not fail on stale entries, or add a --write that regenerates the list and name it in the message.