An open API service indexing awesome lists of open source software.

https://github.com/acoyfellow/deadlint

Find dead cross-boundary methods (Workers DurableObject / RpcTarget / Agent) and structural clones in TypeScript. The linter layer that sits above knip/oxlint.
https://github.com/acoyfellow/deadlint

cloudflare-workers dead-code developer-tools duplicate-code durable-objects lint static-analysis typescript

Last synced: 8 days ago
JSON representation

Find dead cross-boundary methods (Workers DurableObject / RpcTarget / Agent) and structural clones in TypeScript. The linter layer that sits above knip/oxlint.

Awesome Lists containing this project

README

          


three skulls and ritual ornaments, block-printed in oxblood ink on cream paper

deadlint

Ruthlessly Eliminate the Dead.

---

Find dead public methods on Cloudflare Workers `DurableObject` /
`WorkerEntrypoint` / `WorkflowEntrypoint` / `RpcTarget` / `Agent` subclasses,
and structural clones across your TypeScript codebase.

## The gap

| Tool | Stops at | Result |
| ---------------------------- | ----------------------- | ------------------------------------------------- |
| `tsc` (`noUnusedLocals`) | function boundary | unused vars only |
| `oxlint` / `biome` / `eslint`| file/class boundary | every `export` is treated as live |
| `knip` / `ts-prune` | module-export boundary | every public class member is treated as the API |
| **deadlint** | RPC stub / clone | this is the layer that was missing |

In a Workers / Agents codebase, every public method on a DO is — to a static
analyzer — an entry point. Anyone with a stub could call it, so nothing dares
flag it. Real codebases accumulate dead RPC methods for years and no linter
will tell you.

deadlint walks the call graph plus a targeted token scan and tells you which
ones are actually unreachable.

## What it found, on real repos

```text
$ npx deadlint ./artifacts
deadlint report — ./artifacts

Dead RPC methods (1)
───────────────────────────
GitServer.streamingUploadPack (extends DurableObject)
apps/worker/src/git-server.ts:299

Clones — engine: inline (3)
───────────────────────────
1.00 27L readBlob ≈ readTree
src/capabilities/repo.ts:135
src/capabilities/repo.ts:167
1.00 27L readBlob ≈ readCommit
src/capabilities/repo.ts:135
src/capabilities/repo.ts:199
1.00 27L readTree ≈ readCommit
src/capabilities/repo.ts:167
src/capabilities/repo.ts:199

4 findings
```

`streamingUploadPack` was a wrapper around `streamingUploadPackWithEventType`
that nothing called for months. `readBlob` / `readTree` / `readCommit` were
27-line copy-pastes of each other. No other linter saw any of it.

## Install and run

```bash
# one-off
npx deadlint /path/to/your/repo

# global
npm i -g deadlint
deadlint /path/to/your/repo
```

The path needs a `tsconfig.json` somewhere — at the root, in `apps/worker/`,
or anywhere within the first 3 directory levels. Multi-package monorepos
without a root config (a `packages/api/tsconfig.json` + `packages/web/tsconfig.json`
layout) are auto-discovered and scanned per-project.

```bash
deadlint ./repo # full scan (default)
deadlint ./repo --check dead-rpc # just the dead methods
deadlint ./repo --check clones # just the clones
deadlint ./repo --clones-engine both # similarity-ts + inline engine
deadlint ./repo --json > findings.json # machine-readable
deadlint --help # all flags
```

Exit `0` = clean, `1` = findings, `2` = misconfig.

## What it actually checks

**Dead RPC methods.** For every public method on a class extending
`DurableObject`, `WorkerEntrypoint`, `WorkflowEntrypoint`, `RpcTarget`,
or `Agent`, deadlint looks for callers across three signals:

1. The TypeScript language service (precise — but blind to JSRPC stubs).
2. A token scan for `.method(` / `["method"](` direct dispatch.
3. A token scan for `.call("method", …)` string-key dispatch — the
Agents SDK pattern frontend code uses to reach DO methods through
the WebSocket proxy.

Patterns 2 and 3 are scanned across both TypeScript files and companion
files (`.svelte`, `.vue`, `.astro`, `.tsx`, `.jsx`) so frontend call
sites that aren't compiled by your tsconfig are still seen. A method is
flagged dead **only when all three signals turn up zero**. Common
stdlib names (`map`, `then`, `set`, …) are excluded from the token
scans to avoid coincidental keep-alives. Workers/Agents runtime hooks
(`fetch`, `alarm`, `onConnect`, …) are allow-listed.

Override the boundary class list with `--bases Foo,Bar`.

**Structural clones.** Two engines, run independently, results merged.

`similarity` (default) shells out to
[`similarity-ts`](https://github.com/mizchi/similarity) — a Rust binary
using oxc-parser and TSED. Higher precision, biased toward larger
functions. Install once: `cargo install similarity-ts`.

`inline` is built into deadlint, ~150 lines of `ts-morph`. Each function
body is normalized to a `SyntaxKind`-only token sequence (identifiers
and literals erased, so renamed copies match exactly). Findings are
emitted on identical shapes (`1.00`) or 5-gram Jaccard similarity above
`--clone-threshold` (default `0.85`). No external dependencies.

Use `--clones-engine both` to run them side by side. They find largely
non-overlapping pairs.

## Always on, every repo

```bash
deadlint --install-hook
```

Sets `git config --global core.hooksPath ~/.config/git/hooks` and writes
a `pre-push` script that runs `deadlint . --check dead-rpc` before every
push. Covers every repo on your machine — public, private, GitHub, GitLab.
The hook silently no-ops on non-TypeScript repos.

```bash
deadlint --hook-status # is it installed?
deadlint --uninstall-hook # remove it (only if we wrote it)
git push --no-verify # bypass once
```

The installer refuses to clobber a pre-existing `pre-push` hook unless
you pass `--force`. The uninstaller refuses to remove anything that
isn't deadlint-managed. You can't accidentally lose work.

## What it won't catch

- Fully dynamic RPC: `stub[methodFromConfig]()` where the method name is
computed at runtime. None of the signals can see it.
- Cross-repo dead code. If your callers live in a different repository,
deadlint sees nothing.
- HTTP routes dispatched by URL path rather than method name. Add the
router class to `--bases` if appropriate.
- Behavioral clones with different control flow. The inline engine is
shape-based; the similarity engine helps but isn't magic.

Build-output directories (`dist`, `build`, `.svelte-kit`, `.next`, etc.)
are excluded by default — they generate ~100% clone matches against the
source they were built from. Pass `--exclude` to replace the list, or
`--also-exclude` to extend it. Note: `lib` is **not** in the default
list because SvelteKit and many other frameworks use `src/lib/` for
source code; pass `--also-exclude lib` if your project emits to it.

Findings are meant for human review. The tool biases toward false
negatives — it would rather miss a dead method than wrongly flag a live one.

## License

[MIT](./LICENSE)