Files
5chan/docs/agent-runs/codebase-audit-2026-04-23/04-dead-code-duplication.md
T
Tommaso CasaburiandGitHub 5dc5408a15 fix(codebase audit): preserve cleanup without regressions
Fix codebase audit regressions while preserving UI/UX behavior and adding review-driven hardening.
2026-04-24 15:48:07 +07:00

20 KiB

Dead Code & Duplication Audit

Summary

The codebase is clean of obvious dead code. There are no TODO/FIXME/HACK/XXX markers in any .ts/.tsx file under src/ (outside the skipped src/generated/ and src/e2e/ trees), no @deprecated tags, no commented-out code blocks, and no if (false) / unreachable branches. Knip's output is short and all its findings verify as real, though two are "tested-but-not-used" orphans rather than truly dead symbols.

The genuine signal is concentrated in two places:

  1. A small handful of exports that are only referenced from their own unit tests (archive barrel re-export, useAccountCommunitiesWithMetadata, isArchiveView/isSettingsView/isNotFoundView, isDirectMediaUrl, getPreferredOrder/getRandomOrder, getCatalogPostHeightEstimate/getCatalogRowHeightEstimate). Safe to either delete, make non-exported, or wire up a real consumer.
  2. Duplicated logic, not duplicated files. The big offenders are post-desktop.tsx / post-mobile.tsx (they share PendingModerationActions, a moderation-options block repeated again in mod-queue.tsx, a manual isAccountMod computation that already has a hook, and the .eth/.sol display-name switch), plus the useEditCommentPrivileges hook carrying an unused postCid parameter.

Outside those, there is one stale duplicate in package.json (cross-env listed in both dependencies and devDependencies) and one stale type shim (declare module 'react-draggable' for a package that is no longer installed). Total impact is measured in a handful of files — there is no bit-rot backlog here, just a cleanup pass.

knip Findings

yarn knip and yarn knip:full produced identical, short output (13 reportable lines, none --no-exit-code would have suppressed):

Knip category Finding Verification
Unused files src/views/archive/index.ts confirmed. The file is a barrel export { default } from './archive';. src/app.tsx:39 imports ./views/archive/archive directly, unlike every other view under src/views/ which all go through their index.ts.
Unused devDependencies cross-env in package.json:148 confirmed. cross-env is legitimately used in build, build-vercel, analyze-bundle, and both electron:start* scripts, but it is declared twice — once at package.json:25 (dependencies) and once at package.json:148 (devDependencies). The devDependencies copy is redundant.
Unused exports getCommunityIdentifier in src/hooks/use-community-identifiers.ts:7 confirmed. Only consumed by the sibling getCommunityIdentifiers and useCommunityIdentifier in the same file. Not re-exported externally.
Unused exports getCommunityIdentifiers in src/hooks/use-community-identifiers.ts:39 confirmed. Only consumed by the sibling useCommunityIdentifiers in the same file. A same-named local getCommunityIdentifiers exists in src/lib/utils/post-page-resolution.ts:67 (different signature) — knip is not confused, the module-level export really is internal-only.
Unused exports REPLY_HEIGHT_DATA_ATTRIBUTE in src/lib/utils/pretext-height-estimates.ts:71 confirmed. Used once inside the same file at line 853 as a DOM selector. No external consumer.
Unused exports getCatalogPostHeightEstimate in src/lib/utils/pretext-height-estimates.ts:746 confirmed. Only called internally at line 789 from getCatalogRowHeightEstimate.
Unused exports getCatalogRowHeightEstimate in src/lib/utils/pretext-height-estimates.ts:780 confirmed. Only called internally at line 804 from getCatalogRowHeightEstimates.
Unused exported types ArchiveRouteRenderOptions in src/views/archive/__tests__/helpers.ts:10 confirmed. The helper renderArchiveRoute is called with object-literal args at 5 sites in archive.test.tsx; none of the test files name the type. The export keyword on the type alias is surplus.
Configuration hints android/app/src/main/assets/public/** still in ignoreFiles likely-false-positive (harness-specific). This path is listed as a deliberate skip for Android-bundled build output. Knip is offering to remove it because the directory doesn't currently exist in the checkout; leave it pinned unless the Android workflow has been retired.
Configuration hints babel-plugin-react-compiler still in ignoreDependencies likely-false-positive (build tooling). It is loaded by Vite config as a string id and knip cannot trace it; the ignore entry is intentional.

Two categories knip did not surface but that were checked by hand and are real: test-only orphans (useAccountCommunitiesWithMetadata, isDirectMediaUrl, etc.) and type-stub drift (declare module 'react-draggable'). Knip skips the first because tests import them, and the second because it only inspects JS/TS imports, not ambient .d.ts declarations for absent packages.

Findings

Unused Exports

  • src/views/archive/index.ts:1 — One-line barrel export { default } from './archive' that nobody imports; src/app.tsx uses ./views/archive/archive directly. Action: either delete index.ts and leave the direct import, or flip app.tsx to import Archive from './views/archive' (the convention used by every other view). The second is the smaller-surface fix. confirmed.
  • src/hooks/use-community-identifiers.ts:7,39getCommunityIdentifier and getCommunityIdentifiers are exported but only used by the React-facing useCommunityIdentifier / useCommunityIdentifiers in the same file. Action: drop the export keyword on both; keep them as module-private helpers. confirmed.
  • src/lib/utils/pretext-height-estimates.ts:71REPLY_HEIGHT_DATA_ATTRIBUTE is only used inside the same file (line 853). Action: drop the export. confirmed.
  • src/lib/utils/pretext-height-estimates.ts:746,780getCatalogPostHeightEstimate and getCatalogRowHeightEstimate are exported but only used within the file by getCatalogRowHeightEstimate / getCatalogRowHeightEstimates respectively. Action: drop the export on both. Only getCatalogRowHeightEstimates (line 799) needs to remain public. confirmed.
  • src/views/archive/tests/helpers.ts:10export type ArchiveRouteRenderOptions is never referenced by name; callers pass object literals. Action: drop the export keyword. confirmed.
  • src/hooks/use-account-communities-with-metadata.ts:5useAccountCommunitiesWithMetadata has zero production consumers; only src/hooks/__tests__/selector-hooks.test.tsx:6 imports it. Action: delete the hook and its test cases, or wire it into the one plausible consumer (BoardsBar's "my communities" section currently uses useDirectories instead). Knip didn't flag this because the test keeps the import alive. confirmed.
  • src/lib/media-hosting/direct-url.ts:5isDirectMediaUrl and the DIRECT_MEDIA_EXTENSIONS list have no non-test consumer. The extension set is also a subset of KNOWN_IMAGE_EXTENSIONS/KNOWN_VIDEO_EXTENSIONS in src/lib/utils/media-utils.ts:77-78, so the logic duplicates the classifier used by getLinkMediaInfo. Action: delete src/lib/media-hosting/direct-url.ts and its test, or rewrite it as a thin re-export of a shared classifyUrl() helper backed by the media-utils extension lists. confirmed.
  • src/lib/media-hosting/provider-order.ts:5,10getPreferredOrder and getRandomOrder are exported only to be exercised by __tests__/provider-order.test.ts. getPreferredOrder has no internal consumer either; getRandomOrder is called once at line 34 by the public getProviderOrder. Action: drop both export keywords and rewrite the tests against getProviderOrder with runtime+mode combinations that exercise the preferred and random branches. confirmed.
  • src/lib/utils/view-utils.ts:85,93,72isArchiveView, isNotFoundView, and isSettingsView are only used internally (isNotFoundView composes the other two) and by __tests__/view-utils.test.ts. No component/route imports them. Action: if NotFoundView routing is still useful, wire isNotFoundView into src/app.tsx's route-resolution logic (it currently duplicates the negation inline). Otherwise drop the export on all three and delete the tests. confirmed.
  • src/hooks/use-author-privileges.ts:11 — The destructured return includes accountAuthorRole but no external consumer reads it (the two callers in edit-menu and post-menu-mobile destructure only isAccountMod/isAccountCommentAuthor/isCommentAuthorMod/commentAuthorRole). Action: drop accountAuthorRole from the returned object. confirmed.
  • src/hooks/use-author-privileges.ts:5-9 — The AuthorPrivilegesProps interface declares postCid?: string but the hook body never reads it; edit-menu/edit-menu.tsx:52 passes a postCid in anyway. Action: delete the postCid field from the interface and the postCid, from the destructuring at the edit-menu call site. Dead parameter. confirmed.

Dead Code

  • src/modules.d.ts:8declare module 'react-draggable' type stub for a package that is not installed (node_modules/react-draggable does not exist, no react-draggable entry in package.json, zero from 'react-draggable' imports under src/). Action: delete the line. confirmed.
  • src/modules.d.ts:6declare module 'lodash' is a permissive any-typed stub, but @types/lodash is installed (package.json declares it at the top level and node_modules/@types/lodash/package.json exists). The stub silently broadens types the installed package would otherwise provide. Action: delete the stub. If tsgo complains about a specific submodule import (e.g. lodash/capitalize), the correct fix is to import from lodash or add a typed helper, not to re-shadow with any. confirmed.
  • package.json:148cross-env duplicated in devDependencies (it is already a direct dependencies entry at line 25 at the same version). Action: remove the devDependencies line so the package surfaces in exactly one group. confirmed.

Nothing else in this category turned up: no TODO/FIXME/HACK/XXX markers, no commented-out logic, no @deprecated tags, no if (false) / unreachable default: branches, no empty .module.css files.

Duplication Candidates

  • src/components/post-desktop/post-desktop.tsx:99-209 vs src/components/post-mobile/post-mobile.tsx:115-210 vs src/views/mod-queue/mod-queue.tsx:209-281 — The usePublishCommentModeration(approve) + usePublishCommentModeration(reject) pair is set up three times with the same onChallenge/onChallengeVerification/onError trio and the same handleApprove/handleReject double-confirm wrapper. Post-desktop additionally wraps it in a PendingModerationActions subcomponent, which post-mobile redoes inline inside PostInfoAndMedia. Shared abstraction: useApproveRejectModeration({ cid, communityAddress, post }) in src/hooks/, returning { approve, reject, approveState, approveError, rejectState, rejectError, handleApprove, handleReject }. Callers render the UI. The hook owns the two usePublishCommentModeration calls, the shared onChallenge/onError, and the window.confirm(t('double_confirm')) guard. This would delete roughly 90 lines across the three call sites.
  • src/components/post-desktop/post-desktop.tsx:253-254 + 109-112 vs src/components/post-mobile/post-mobile.tsx:108-109 + 124-127 — Both files manually compute const accountRole = roles?.[accountAddress]?.role; const isAccountMod = accountRole === 'admin' || accountRole === 'owner' || accountRole === 'moderator';. The exact same isAccountMod logic already lives in src/hooks/use-author-privileges.ts (which is already consumed by edit-menu and post-menu-mobile). Shared abstraction: call useAuthorPrivileges({ commentAuthorAddress: address, communityAddress }) in both post components instead, destructuring isAccountMod. Delete the manual ternary. Bonus: the post components would also pick up useCommunityField-level selector memoization instead of reading roles off a prop.
  • src/components/boards-bar/boards-bar.tsx:195, src/components/board-header/board-header.tsx:98, src/components/post-desktop/post-desktop.tsx:642,857, src/components/post-mobile/post-mobile.tsx:85, src/views/mod-queue/mod-queue.tsx:639 — Six sites inline the check address.endsWith('.eth') || address.endsWith('.sol') ? address : getShortAddress(address) to decide whether a community address should be displayed verbatim or truncated. Two sites (post-desktop, post-mobile) do it twice. Shared abstraction: formatCommunityAddressForDisplay(address) in src/lib/utils/string-utils.ts (or a tiny is-domain-like-address predicate used by the existing getShortAddress fallback). When the TLD list grows (the community shortlist already includes .sol; IPNS keys are a separate branch), you update it once.
  • src/components/post-desktop/post-desktop.tsx:72-78 vs src/components/post-mobile/post-mobile.tsx:62-67 — Identical RepliesFooter component (hasMore ? <div className={styles.stateString}><LoadingEllipsis string={loadingString}/></div> : null) redefined in each file. styles resolves to the same views/post/post.module.css in both. Shared abstraction: move RepliesFooter into src/components/ (e.g. src/components/post-desktop/replies-footer.tsx shared via a barrel) or directly inline it under a single src/components/post-thread-parts/.
  • src/components/post-desktop/post-desktop.tsx:80-97 vs src/components/post-mobile/post-mobile.tsx:69-70 — Both files keep a module-level const lastVirtuosoStates: { [key: string]: StateSnapshot } = {} plus (desktop-only) a useShowOmittedReplies Zustand store. Desktop also defines an equivalent setShowOmittedReplies store inline. Shared abstraction: lift the scroll-snapshot map into a shared src/lib/utils/virtuoso-scroll-memory.ts helper (saveSnapshot(key, state) / getSnapshot(key)) so both files consume the same keyed cache. This also avoids two per-instance module caches holding onto (possibly stale) snapshots. The useShowOmittedReplies store belongs in src/stores/use-show-omitted-replies-store.ts since desktop and mobile are mutually exclusive at runtime anyway.
  • src/components/settings-modal/crypto-wallets-setting/crypto-wallets-setting.tsx:65 — Uses navigator.clipboard.writeText(messageToSign) directly, bypassing copyToClipboard from src/lib/utils/clipboard-utils.ts that every other copy site (error-display, post-menu-desktop, post-menu-mobile, url-utils) routes through. clipboard-utils handles the Electron IPC bridge + fallback that this site silently skips. Action: replace with await copyToClipboard(messageToSign) and keep the failure alert that sits around it.
  • src/lib/media-hosting/direct-url.ts:2 vs src/lib/utils/media-utils.ts:77-78DIRECT_MEDIA_EXTENSIONS (with leading dots: .jpg, .jpeg, .png, ...) is a subset of KNOWN_IMAGE_EXTENSIONS + KNOWN_VIDEO_EXTENSIONS (without leading dots: jpg, jpeg, png, ...). Two independently-maintained lists with different shapes, same intent. Shared abstraction: one canonical list in media-utils.ts plus a classifyUrlAsDirectMedia(url) function that the rare caller needs. Resolves the direct-url.ts file's dead-code status at the same time.

TODO/FIXME Backlog

Empty. grep -rn -i 'TODO\|FIXME\|HACK\|XXX' under src/ (excluding src/generated/ and src/e2e/) returns zero markers in any .ts, .tsx, .js, .jsx, or .css file. The only hit is a narrative comment in src/app.tsx:126 containing the literal substring "xxx" ("/thread/xxx") — not a marker. Nothing to triage.

Legacy Code

  • src/modules.d.ts:8 (react-draggable) — Already flagged under Dead Code. This is the only remaining ambient type stub for a package that isn't installed. The two siblings in the file (*.module.css, react-router-hash-link) are still needed: @types/react-router-hash-link is not published on DefinitelyTyped, and CSS-modules typing is a Vite-compatible pattern.
  • src/modules.d.ts:6 (declare module 'lodash') — Already flagged under Dead Code. @types/lodash@4.17.24 is installed in devDependencies and provides full typings; the ambient declare module 'lodash' shadows them with implicit any. Pure legacy residue.
  • src/lib/utils/view-utils.ts:81,34,93,85,72 — Several predicate signatures declare params: ParamsType but don't read from it (isSubscriptionsView, and the bodies of isCatalogView/isSettingsView only read boardIdentifier / commentCid / accountCommentIndex). isSubscriptionsView in particular has 12 call sites all passing params needlessly. This is light code smell, not dead code — trimming is optional. Action (optional): drop the unused params from isSubscriptionsView; leave the others (they do read from params).
  • src/data/5chan-blotter.json (not opened here, flagged for completeness) — The blotter data file is the source of truth for the blotter UI; not legacy even though it's auto-generated. Left in place.

No @deprecated JSDoc tags, no commented-out code blocks, no feature-flag branches that no longer toggle were found. The codebase has been cleaned recently; the backlog is genuinely thin.

Top 5 Actions

  1. Delete the react-draggable type stub and the duplicate cross-env devDep. Two-line net removal in src/modules.d.ts:8 and package.json:148, zero risk, and it clears the only pieces of stale manifest/stub drift.
  2. Un-export internal helpers flagged by knip. Drop the export keyword on getCommunityIdentifier/getCommunityIdentifiers (src/hooks/use-community-identifiers.ts:7,39), REPLY_HEIGHT_DATA_ATTRIBUTE (src/lib/utils/pretext-height-estimates.ts:71), getCatalogPostHeightEstimate / getCatalogRowHeightEstimate (pretext-height-estimates.ts:746,780), and ArchiveRouteRenderOptions (src/views/archive/__tests__/helpers.ts:10). Also drop the export on getPreferredOrder/getRandomOrder (src/lib/media-hosting/provider-order.ts:5,10) and rewrite their tests to go through getProviderOrder. Shrinks the public API surface by ~7 symbols without changing behavior.
  3. Extract useApproveRejectModeration({ cid, communityAddress, post }) and collapse the three copies. Replaces the usePublishCommentModeration(approve) + usePublishCommentModeration(reject) + handleApprove/handleReject blocks in post-desktop.tsx:102-159, post-mobile.tsx:115-210, and mod-queue.tsx:209-281. Roughly 90 LOC deleted, the onChallenge/onChallengeVerification/onError error-log strings stop drifting, and the window.confirm(t('double_confirm')) gate has one owner.
  4. Make the post components reuse useAuthorPrivileges and delete the postCid ghost parameter. Replace the manual isAccountMod ternaries in post-desktop.tsx:253-254 and post-mobile.tsx:108-109 with a useAuthorPrivileges({ commentAuthorAddress, communityAddress }) call. Simultaneously drop postCid from AuthorPrivilegesProps in src/hooks/use-author-privileges.ts:5-9 and from the caller in edit-menu.tsx:52. Removes two duplicated logic sites and kills an unused parameter.
  5. Delete or wire up the four test-only orphans. Either delete (preferred if there is no plan to use them) or land a production consumer for: src/hooks/use-account-communities-with-metadata.ts (plus its test in selector-hooks.test.tsx), src/lib/media-hosting/direct-url.ts (plus its test; merge into media-utils.ts's extension lists if a use case returns), isArchiveView/isNotFoundView/isSettingsView in src/lib/utils/view-utils.ts (plus their tests; or wire isNotFoundView into app.tsx as the canonical not-found predicate), and src/views/archive/index.ts (either delete, or flip app.tsx:39 to import through it like every other view).