DeadCodeView.svelte 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <script lang="ts">
  2. /**
  3. * Dead code — symbols nothing in this repository reaches, grouped by file
  4. * (design spec §3.11).
  5. *
  6. * The screen is a list and a disclaimer, deliberately in that order and
  7. * deliberately inseparable. The caveat line sits above the rows and never
  8. * goes away, because the claim behind every row is "no static reference in
  9. * the index" and not "unused": reflection, a framework registry and a
  10. * template can all reach code the graph cannot follow. Underneath, every
  11. * reason a candidate was left off is printed with its count — a list of
  12. * twenty drawn from two and a half thousand candidates means something very
  13. * different from a list of twenty drawn from twenty-one.
  14. *
  15. * The one switch is "including exported". Off (the default) the list is only
  16. * symbols nothing outside this repository could import either; on, it widens
  17. * to symbols the index has no way to check, and says so. It travels in the
  18. * URL like the map's shape does, so a link reopens the same list.
  19. *
  20. * The other half of this task lives on the Map: a module nothing depends on
  21. * says so in its own count line. See `lib/map-model.ts`.
  22. */
  23. import KindGlyph from '../components/KindGlyph.svelte';
  24. import { fetchDeadCode, ApiFailure, type WireDeadCode, type WireDeadCodeRow } from '../lib/api';
  25. import { deadHref, fileHref, navigate, symbolHref } from '../lib/navigation';
  26. import { live } from '../lib/live.svelte';
  27. import {
  28. DEAD_CODE_CAVEAT,
  29. deadCodeHeadline,
  30. deadCodeRowMeta,
  31. deadCodeScale,
  32. emptyMessage,
  33. exclusionPhrases,
  34. groupMeta,
  35. } from '../lib/deadcode-model';
  36. interface Props {
  37. /** Include symbols something outside the index could import. */
  38. exported?: boolean;
  39. }
  40. let { exported = false }: Props = $props();
  41. let payload = $state<WireDeadCode | null>(null);
  42. let failure = $state<string | null>(null);
  43. let loading = $state(true);
  44. $effect(() => {
  45. const includeExported = exported;
  46. // The index moving invalidates every row: a symbol is on this list because
  47. // of what the graph does NOT contain, which is exactly what a sync changes.
  48. void live.indexTick;
  49. const controller = new AbortController();
  50. loading = true;
  51. failure = null;
  52. fetchDeadCode({ includeExported }, controller.signal)
  53. .then((next) => {
  54. payload = next;
  55. loading = false;
  56. })
  57. .catch((error: unknown) => {
  58. if (controller.signal.aborted) return;
  59. failure = error instanceof ApiFailure ? error.message : 'The list could not be read.';
  60. loading = false;
  61. });
  62. return () => controller.abort();
  63. });
  64. let headline = $derived(deadCodeHeadline(payload));
  65. let scale = $derived(deadCodeScale(payload));
  66. let phrases = $derived(exclusionPhrases(payload));
  67. function open(row: WireDeadCodeRow): void {
  68. navigate(symbolHref(row.id, { line: row.line }));
  69. }
  70. </script>
  71. <div class="scroll">
  72. <div class="head">
  73. <h2>Dead code</h2>
  74. <p>
  75. Symbols no import, call or reference in this index reaches, largest first. Everything below
  76. is what the graph can see; the notes under the list are what it cannot.
  77. </p>
  78. </div>
  79. <div class="bar">
  80. <p class="caveat">{DEAD_CODE_CAVEAT}</p>
  81. <a
  82. class="toggle"
  83. class:on={exported}
  84. href={deadHref({ exported: !exported })}
  85. title={exported
  86. ? 'Back to symbols nothing outside this repository could import either'
  87. : 'Also list symbols something outside this repository could import — the index cannot check those'}
  88. onclick={(event) => {
  89. event.preventDefault();
  90. navigate(deadHref({ exported: !exported }));
  91. }}>{exported ? 'Including exported' : 'Internal only'}</a
  92. >
  93. </div>
  94. {#if failure}
  95. <p class="state">Could not read the list — {failure}</p>
  96. {:else if loading && payload === null}
  97. <p class="state">Reading the graph…</p>
  98. {:else if payload}
  99. {#if exported}
  100. <p class="warn">
  101. Exported symbols are on this list. Nothing in this repository references them, but anything
  102. outside it can — a published package, another service, a script. Read each one before you
  103. believe it.
  104. </p>
  105. {/if}
  106. {#if payload.groups.length === 0}
  107. <p class="state">{emptyMessage(payload)}</p>
  108. {:else}
  109. <p class="headline">{headline}</p>
  110. <div class="groups">
  111. {#each payload.groups as group (group.file)}
  112. <div class="filegroup" class:gen={group.generated}>
  113. <div class="fpath">
  114. <a href={fileHref(group.file)} title={group.file}>{group.file}</a>
  115. <b>{groupMeta(group)}</b>
  116. </div>
  117. {#each group.rows as row (row.id)}
  118. <div class="row">
  119. <KindGlyph kind={row.kind} />
  120. <div class="body">
  121. <div class="line">
  122. <button
  123. type="button"
  124. class="nm"
  125. title={row.qualifiedName}
  126. data-dead-row={row.id}
  127. onclick={() => open(row)}>{row.name}</button
  128. >
  129. <a class="ln" href={fileHref(group.file, { source: true, line: row.line })}
  130. >{row.file}:{row.line}</a
  131. >
  132. {#if row.exported}<span class="chip">exported</span>{/if}
  133. </div>
  134. <div class="meta">{deadCodeRowMeta(row)}</div>
  135. {#if row.members.items.length > 0}
  136. <div class="members">
  137. {#each row.members.items as member (member.id)}
  138. <a class="member" href={symbolHref(member.id)}>{member.name}</a>
  139. {/each}
  140. {#if row.members.truncated}
  141. <span class="member more"
  142. >+{row.members.total - row.members.shown} more</span
  143. >
  144. {/if}
  145. </div>
  146. {/if}
  147. </div>
  148. </div>
  149. {/each}
  150. </div>
  151. {/each}
  152. </div>
  153. {/if}
  154. <div class="notes">
  155. <p>{scale}</p>
  156. {#if phrases.length > 0}
  157. <ul>
  158. {#each phrases as phrase (phrase)}
  159. <li>{phrase}</li>
  160. {/each}
  161. </ul>
  162. {/if}
  163. {#if !payload.corroborated}
  164. <p>
  165. The rows were not checked against the text of the files that can reach them, so a
  166. reference the extractor did not record would not have been caught.
  167. </p>
  168. {/if}
  169. {#if payload.bounded}
  170. <p>
  171. The scan stopped at its cap — this index holds more unreferenced symbols than were
  172. considered.
  173. </p>
  174. {/if}
  175. {#if payload.rows.truncated}
  176. <p>
  177. Showing {payload.rows.shown} of {payload.rows.total} — the rest are in the index, not on
  178. this list.
  179. </p>
  180. {/if}
  181. </div>
  182. {/if}
  183. </div>
  184. <style>
  185. .scroll {
  186. height: 100%;
  187. overflow: auto;
  188. }
  189. .head {
  190. max-width: 760px;
  191. padding: 26px 40px 6px;
  192. }
  193. .head h2 {
  194. margin: 0 0 6px;
  195. font-size: 20px;
  196. font-weight: 600;
  197. letter-spacing: -0.01em;
  198. }
  199. .head p {
  200. margin: 0;
  201. color: var(--ink-2);
  202. font-size: 13px;
  203. line-height: 1.45;
  204. }
  205. .bar {
  206. display: flex;
  207. align-items: baseline;
  208. justify-content: space-between;
  209. gap: 16px;
  210. max-width: 760px;
  211. margin: 14px 40px 0;
  212. padding: 6px 0;
  213. border-top: 1px solid var(--rule-soft);
  214. border-bottom: 1px solid var(--rule-soft);
  215. }
  216. /* The caveat is never dismissible and never collapsed: it is the difference
  217. between "nothing references this" and "nobody uses this". */
  218. .caveat {
  219. margin: 0;
  220. color: var(--ink-3);
  221. font-size: 11.5px;
  222. line-height: 1.4;
  223. }
  224. .toggle {
  225. flex: none;
  226. padding: 1px 6px;
  227. border: 1px solid var(--rule-soft);
  228. color: var(--ink-2);
  229. font: 11px var(--mono);
  230. text-decoration: none;
  231. }
  232. .toggle:hover {
  233. border-color: var(--ink);
  234. color: var(--ink);
  235. }
  236. .toggle.on {
  237. border-color: var(--accent-line);
  238. background: var(--accent-soft);
  239. color: var(--accent);
  240. }
  241. .warn {
  242. max-width: 760px;
  243. margin: 12px 40px 0;
  244. padding: 8px 12px;
  245. border: 1px solid var(--accent-line);
  246. background: var(--accent-soft);
  247. color: var(--ink-2);
  248. font-size: 11.5px;
  249. line-height: 1.45;
  250. }
  251. .headline {
  252. max-width: 760px;
  253. margin: 14px 40px 0;
  254. color: var(--ink-2);
  255. font-size: 12.5px;
  256. }
  257. .state {
  258. max-width: 760px;
  259. padding: 16px 40px 40px;
  260. color: var(--ink-3);
  261. font-size: 12.5px;
  262. line-height: 1.5;
  263. }
  264. .groups {
  265. max-width: 760px;
  266. margin: 8px 40px 0;
  267. border: 1px solid var(--rule-soft);
  268. }
  269. .filegroup {
  270. padding: 10px 14px 6px;
  271. border-bottom: 1px solid var(--rule-faint);
  272. }
  273. .filegroup:last-child {
  274. border-bottom: 0;
  275. }
  276. .fpath {
  277. display: flex;
  278. justify-content: space-between;
  279. gap: 8px;
  280. margin-bottom: 4px;
  281. color: var(--ink-3);
  282. font: 11px var(--mono);
  283. }
  284. .fpath a {
  285. overflow: hidden;
  286. text-overflow: ellipsis;
  287. white-space: nowrap;
  288. }
  289. .fpath a:hover {
  290. color: var(--ink);
  291. text-decoration: underline;
  292. }
  293. .fpath b {
  294. flex: none;
  295. color: var(--ink-2);
  296. font-weight: 500;
  297. }
  298. /* Generated code recedes wherever it appears (design spec §2.6). */
  299. .filegroup.gen .fpath,
  300. .filegroup.gen .fpath b,
  301. .filegroup.gen .nm,
  302. .filegroup.gen .meta {
  303. color: var(--ink-4);
  304. }
  305. .row {
  306. display: grid;
  307. grid-template-columns: 16px 1fr;
  308. gap: 8px;
  309. align-items: start;
  310. margin: 0 -6px;
  311. padding: 5px 6px 5px 4px;
  312. border: 1px solid transparent;
  313. }
  314. .row:hover {
  315. background: var(--press);
  316. }
  317. .body {
  318. min-width: 0;
  319. }
  320. .line {
  321. display: flex;
  322. align-items: baseline;
  323. gap: 8px;
  324. }
  325. .nm {
  326. overflow: hidden;
  327. min-width: 0;
  328. color: var(--ink);
  329. font: 12.5px var(--mono);
  330. text-align: left;
  331. text-overflow: ellipsis;
  332. white-space: nowrap;
  333. cursor: pointer;
  334. }
  335. .ln {
  336. flex: none;
  337. color: var(--ink-3);
  338. font: 11px var(--mono);
  339. text-decoration: none;
  340. }
  341. .ln:hover {
  342. color: var(--accent);
  343. text-decoration: underline;
  344. }
  345. .chip {
  346. flex: none;
  347. padding: 0 4px;
  348. border: 1px solid var(--accent-line);
  349. background: var(--accent-soft);
  350. color: var(--accent);
  351. font: 11px var(--mono);
  352. }
  353. .meta {
  354. margin-top: 1px;
  355. overflow: hidden;
  356. color: var(--ink-3);
  357. font-size: 11px;
  358. text-overflow: ellipsis;
  359. white-space: nowrap;
  360. }
  361. .members {
  362. display: flex;
  363. flex-wrap: wrap;
  364. gap: 4px 8px;
  365. margin-top: 3px;
  366. }
  367. .member {
  368. color: var(--ink-3);
  369. font: 11px var(--mono);
  370. text-decoration: none;
  371. }
  372. .member:hover {
  373. color: var(--accent);
  374. text-decoration: underline;
  375. }
  376. .member.more {
  377. color: var(--ink-4);
  378. }
  379. .notes {
  380. max-width: 760px;
  381. margin: 14px 40px 48px;
  382. color: var(--ink-3);
  383. font-size: 11.5px;
  384. line-height: 1.5;
  385. }
  386. .notes p {
  387. margin: 0 0 6px;
  388. }
  389. .notes ul {
  390. margin: 0;
  391. padding-left: 16px;
  392. }
  393. </style>