npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rot-scan

v0.3.0

Published

Language-agnostic dead code analyzer powered by tree-sitter

Readme

rot-scan

Language-agnostic code-rot scanner powered by tree-sitter. No AI, no cloud, no language-specific toolchain on your machine. JS, TS, TSX, Python.

Install

Run without installing:

npx -y rot-scan

Or install globally:

npm install -g rot-scan
rot-scan

Node 20+. No auth, no postinstall step — the 4 tree-sitter wasm grammars ship inside the package.

Use with coding agents

rot-scan ships an MCP server — works in Claude Code, Cursor, and Windsurf natively:

{
  "mcpServers": {
    "rot-scan": {
      "command": "npx",
      "args": ["-y", "rot-scan", "mcp"]
    }
  }
}

Drop this into .claude/settings.json / .cursor/mcp.json / ~/.codeium/windsurf/mcp_config.json. See docs/agents/ for per-agent details.

Your agent can then ask for a health score (rot_scan_summary), list findings (rot_scan_run), or fetch rule docs (rot-scan://rules/{name}). Every finding carries an explicit suggestedAction (remove-export, delete-file, remove-dependency, etc.) so agents can drive cleanups directly.

Unlike Knip's MCP (JS/TS only), rot-scan's MCP handles JS + TS + TSX + Python in one server with zero config.

Usage

# Score the current directory
rot-scan

# Score specific paths
rot-scan src/ packages/

# CI gate — fail if health score drops below 80
rot-scan --min-score 80

# JSON output
rot-scan --format json

# Scope to a language subset
rot-scan --lang ts,tsx src/

# Disable a rule for this run
rot-scan --rule no-unused-locals=off

Every run renders a health score banner:

72 / 100  Good
██████████████░░░░░░
47 findings across 183 files in 2.1s

src/services/auth.ts
  unused-export:23:14  Exported "authKeys" is used inside this file but
                       imported by nothing — remove the `export` keyword.
...

Grades: Healthy 90+, Good 75-89, Warning 50-74, Poor 25-49, Critical <25.

Exit codes: 0 pass, 1 findings or score below --min-score, 2 internal error (unparseable files).

Flags

rot-scan [paths...] [options]

Positionals:
  paths                       One or more directories or files. Default: cwd.

Rules & detection:
  --rule NAME=SEVERITY        Per-rule override, repeatable. SEVERITY is error|warn|off.
  --detect <list>             [legacy] Comma-separated rule kinds: exports,files,locals,unreachable.
  --no-string-refs            Disable the dynamic-dispatch heuristic.
  --no-path-refs              Disable the cross-language path-literal heuristic.

Scope:
  --lang <list>               Restrict to languages: js,ts,tsx,py (or full names).
  --ignore <glob>             Extra ignore pattern, repeatable.
  --exclude-tests             Skip common test-file conventions.
  --entry-points <files>      Comma-separated entry points in addition to auto-detect.

Score & CI:
  --min-score <n>             Fail if health score < n (0-100).
  --no-score                  Hide the score banner.

Output:
  --format <fmt>              text | json (default text).

Config:
  --config <path>             Path to .rot-scanrc.json.
  --no-ignore-comments        Disable `// rot-scan:ignore` comment support.

Config

Drop .rot-scanrc.json at the repo root:

{
  "presets": ["vite", "nestjs"],
  "entryPoints": ["src/index.ts", "scripts/cli.ts"],
  "ignore": ["src/generated/**"],
  "ignoreSymbols": ["publicAPI"],
  "dynamicPatterns": ["**/routes/**"],
  "rules": {
    "no-unused-exports": "error",
    "no-unused-locals": "off"
  },
  "heuristics": { "stringRefs": true, "pathRefs": true }
}

Rules (see docs/rules/ for each):

| Rule | Category | Weight | What it flags | |------|----------|-------:|---------------| | no-unused-exports | correctness | ×1 | Exported symbols with no importers | | no-unused-files | correctness | ×2 | Orphan files (no importers, not entry points) | | no-unused-locals | suspicious | ×0.5 | File-local declarations never referenced | | no-unreachable-code | correctness | ×2 | Statements after return/throw/break/continue | | no-unused-deps | correctness | ×1 | package.json entries never imported |

Presets expand into entryPoints + dynamicPatterns for common frameworks: nextjs, vite, remix, nestjs, storybook, vitest, drizzle, tsup, sveltekit, astro, hono, solid, nuxt.

Inline ignores:

// rot-scan:ignore
export function registeredByDecorator() {}

// rot-scan:ignore-file   (put at top of file to skip it entirely)

Limitations

rot-scan reads your source code statically — it never runs anything. That's fast and safe, but a few patterns are genuinely invisible to a tool that only reads. Here's what to watch for and how to work around each one.

Calling functions by string name

const handlers = { handleClick, handleSubmit };
handlers[eventName]();   // which one gets called? depends on runtime

rot-scan can't follow handlers[eventName]() to a specific function, so handleClick and handleSubmit look unused. As a safety net, the tool scans string literals in your code — so if the string "handleClick" appears anywhere, the function stays marked as used. But if the name is assembled at runtime ("handle" + event), it's invisible.

What to do: list the file in dynamicPatterns (stops the tool from flagging its exports), or list specific names in ignoreSymbols.

Code stripped by your bundler

if (process.env.NODE_ENV !== "production") {
  debugOnlyThing();   // bundlers delete this in prod
}

rot-scan reads your source, not your built bundle. Code inside dev-only branches looks alive to the tool even though your production build has it removed. The symmetric case: something only called inside a dev branch looks "used," but really isn't in prod.

What to do: if you want a "what's dead in production" report, point rot-scan at your build output (e.g. dist/) instead of src/.

Framework files loaded by filename

Some frameworks load files by convention, not by import:

  • Rails autoloads UsersController from users_controller.rb
  • Next.js makes every pages/foo.tsx a route
  • NestJS collects controllers/services by decorator at app startup

rot-scan doesn't see these files as imported, so they look orphaned.

What to do: add a preset — nextjs, nestjs, remix, vite, rails, storybook, vitest, drizzle, tsup. Each one teaches rot-scan about that framework's conventions. For custom conventions, extend entryPoints + dynamicPatterns yourself.

Scripts invoked across languages

spawn("python", ["scripts/train.py"]);

rot-scan analyzes each language's imports separately. It won't know that a JS file invokes a Python script. As a fallback, it looks at string literals — if the exact path "scripts/train.py" appears, the Python file stays marked as used. But runtime-assembled paths ("scripts/" + name + ".py") are invisible.

What to do: add the target script to entryPoints.

Shadowed variables in nested scopes

If you reuse a variable name in nested scopes (an outer x and an inner x), rot-scan's scope tracker counts inner uses as also referring to the outer x. The inner use "leaks" into the outer scope's count. Usually harmless; in pathological cases it may mask an unused outer variable.

What to do: if you hit it, file a fixture and we'll extend the scope walker to handle shadowing correctly.

Default exports

export default function greet() {}

The no-unused-exports rule skips export default entirely. Matching a default export to its importer reliably requires tracking each file's default binding through renames (import foo from "./x" is really importing default), which needs type-level info that tree-sitter queries don't provide.

What to do: nothing — these just won't be flagged. If default-only dead code is a concern, convert the exports to named form.

Side-effect-only imports

import "./init";   // runs the file for its side effects, no binding

rot-scan currently only tracks imports that have a named or namespace binding. A bare side-effect import doesn't mark the target as imported, so it may get flagged as an orphan file.

What to do: add the file to entryPoints.