dead-code-audit-frontend
Audit dead code in frontend/src for Svelte frontend code. Triggers on: requests like 'find dead code', 'remove unused code', 'cleanup unused', 'what can be refactored', deeper inspection after a large
SKILL.md.claude/skills/dead-code-audit-frontend/SKILL.md
GitHubSkill: Dead code audit for frontend
When to run
- User asks for "code cleanup", "dead code", "unused", "tidy up"
- After a large refactor (e.g., extracting helpers, removing a feature) — leaves orphans
- Before merging a long-running branch
- Periodically to surface debt
Audit catalyst list
For each, run matching greps:
Imports
grep for `^import .* from` per file, list imports, then grep each named import's symbol usage within that file (must appear ≥ 1 time outside the import line).
Props in components
For each $props() destructure, confirm each prop is read in the markup or script. Unused prop = dead because consumers can stop passing it.
Recently removed feature can leave props behind — Common example: a variant prop whose default branch was removed, leaving an unconditional template.
$state
let x = $state(...) — must appear in {x} or {... x ...} or be read inside another $derived / function used by template.
$derived
const x = $derived(...) — must appear as {x} in template (or be read by another $derived / function used in template).
Functions declared in <script>
For each function foo() code path that's not exported: trace call graph from template. If unreachable: dead.
Exports from .ts files
For each export const foo, export function bar, export type X: grep the entire frontend/src for usage. If zero uses: dead export.
Types / interfaces
For each export type X in types/: confirm at least one .svelte or .ts file imports it. If only the declaration file references it, it's dead.
Anti-pattern: $derived(() => expr) returning a function
This is the function form of derived in Svelte 5 — it evaluates to the arrow function, not the value. Most of the time the user wanted $derived(expr) (eager). Verify each $derived(...) body that returns a value; if the body is () => { ... return value; } and the call site does title() as if it were a value, that's a bug masquerading as dead-code-shaped behavior. Convert to $derived.by(() => { return value; }) or inline the expression.
Inline event handlers that could be bind: (subject to AGENTS.md)
Grep oninput=\{(e) => and onchange=\{(e) =>. Each handler should either (a) be a named function with side effects, (b) trigger state mutation that requires a callback pattern (per AGENTS.md bug #4, when parent state is string | undefined), or (c) be a candidate to become bind:value. Beware: in this repo, bind:value is forbidden for form components (see .opencode/AGENTS.md). Don't suggest bind: on form components.
Output format
Always deliver report before executing. Use this format:
## Dead code report
### File: <path>
| Lines | What | Why dead | Suggested fix |
|---|---|---|---|
| X-Y | `<code>` | No call site outside itself | Delete lines X-Y |
Group by:
- Safe to delete (no behavior change, no API impact)
- Refactor suggestion (e.g.,
$derivedanti-pattern) - Risky (state used internally for caching, load guards) — recommend asking user before removing
Verification before execution
For every dead code candidate, before deleting:
- Grep the entire
frontend/srcfor any reference (functions, props, exports) - Read related files to confirm call graph
- Skip "internal but functional" code — e.g.,
loadedflag insettings.svelte.tsthat guardsload(force)dedupe. Not dead. - Skip "redundant duplicate" code — e.g.,
galleryCategoriesApiduplicatesgalleryApi.categories(). Mention but don't delete (replacing it changes call sites, refactor by preference).
Execution protocol
After report:
- Ask the user which categories to apply (safe / anti-pattern / risky)
- Execute edits in batch
- Run
svelte-check(see verify-svelte-check skill) after each batch - Report final state
Pitfalls
- Don't delete "internal state that sets only itself" without checking that internal state isn't read indirectly (e.g., the
loadedguard readsthis.loadedlater). - Don't rely on
svelte-checkalone. It surfaces unused locals and imports, but doesn't catch unused exports, props, or$derived. - Inline handlers are not dead code just because they "could be
bind:". They may exist for AGENTS.md compliance. Cross-check.opencode/AGENTS.mdbefore suggesting bind. - Custom exports in
.svelte.jsfiles (toastStore.info()etc) may be intentional public API for future. If unsure, ask user before removing.