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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/11842-automate-a-review-finding-as-a-lint-or-t → main 6 commits fork 1 thread · 1 unresolved

  • Merged. mason/11842-automate-a-review-finding-as-a-lint-or-t was merged into main as 07e3b86.

Review

Sign in to review this change.
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 });

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.

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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs