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

@omermohideen/react-crap

v1.5.0

Published

Change Risk Anti-Patterns (CRAP) metric for React TypeScript projects

Downloads

1,392

Readme

react-crap

npm license

Compute the CRAP (Change Risk Anti-Patterns) metric for React TypeScript projects.

CRAP combines cyclomatic complexity and test coverage into a single number that is high when code is both hard to understand and poorly tested — i.e. where bugs love to hide. The metric was introduced by Savoia & Evans in 2007 and was originally implemented for Java (Crap4j) and .NET (NDepend). react-crap brings it to the TypeScript / React ecosystem.

CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)

A few properties worth internalizing before you use the output:

  • A trivial function (CC=1, 100% covered) scores exactly 1.0. That's the lower bound.
  • At 100% coverage the quadratic term collapses and CRAP equals CC. When you see matching values in those two columns, that function is fully covered — tests are capping the damage, but the complexity itself remains. It's a good sign, not a bug.
  • Above CC ≈ 30 no amount of coverage keeps you under the default threshold of 30. That's not a bug in the formula — it's the formula saying "this function is too big to certify as clean, regardless of tests."

Install

Via npx (no install):

npx @omermohideen/react-crap --lcov coverage/lcov.info --path src

Via npm (global):

npm install -g @omermohideen/react-crap

Via npm (local dev dependency):

npm install --save-dev @omermohideen/react-crap

Quick start

# 1. Generate an LCOV coverage report.
npx vitest run --coverage

# 2. Score every function.
npx react-crap --lcov coverage/lcov.info --path src

# 3. Gate CI on the threshold.
npx react-crap --lcov coverage/lcov.info --fail-above

# 4. Whole-workspace analysis (monorepos).
npx react-crap --lcov coverage/lcov.info --path . --workspace --top 20

# 5. Quick aggregate summary (no table).
npx react-crap --lcov coverage/lcov.info --summary

# 6. Watch mode during local development.
npx react-crap --lcov coverage/lcov.info --path src --watch --verbose

# 7. Analyze only uncommitted changed files.
npx react-crap --lcov coverage/lcov.info --path src --changed

# 8. Generate an HTML report.
npx react-crap --lcov coverage/lcov.info --path src --format html --output crap-report.html

# 9. Generate a stable JSON baseline (sorted by file/name for readable diffs).
npx react-crap --lcov coverage/lcov.info --format json --sort file --output baseline.json

Example output:

┌───┬───────┬────┬───────────────────┬──────────┬───────────────┐
│   │  CRAP │ CC │ Coverage          │ Function │ Location      │
╞═══╪═══════╪════╪═══════════════════╪══════════╪═══════════════╡
│ ✗ │ 156.0 │ 12 │ ░░░░░░░░░░   0.0% │ crappy   │ src/lib.ts:24 │
│ ▲ │   6.7 │  4 │ ████░░░░░░  44.4% │ moderate │ src/lib.ts:12 │
│ ✓ │   1.0 │  1 │ ██████████ 100.0% │ trivial  │ src/lib.ts:8  │
└───┴───────┴────┴───────────────────┴──────────┴───────────────┘
✗ 1/3 function(s) exceed CRAP threshold 30.

Flags

| Flag | Default | Purpose | |------|---------|---------| | --lcov <FILE> | coverage/lcov.info | LCOV file from your test runner (Vitest, Jest, etc.). | | --path <DIR> | src | Root to walk for .ts / .tsx files (respects .gitignore). | | --threshold <N> | 30 | Score above which a function is flagged. | | --min <SCORE> | — | Hide entries below this CRAP score. | | --max <SCORE> | — | Hide entries above this CRAP score. | | --top <N> | — | Show only the N worst offenders. | | --only-failures | — | Only show functions exceeding the threshold. | | --missing {pessimistic,optimistic,skip} | pessimistic | How to score a function with no coverage data. | | --exclude <GLOB> | — | Skip files matching this pattern (repeatable). ** crosses directories. | | --allow <GLOB> | — | Suppress matching functions (repeatable). An entry containing / or ** is a path glob and matches the file the function is in (e.g. src/generated/**); otherwise it matches the function name and * is a wildcard (e.g. use*). | | --format {human,json,github,markdown,html,pr-comment,sarif} | human | Output format. json emits a versioned envelope (see JSON output schema below). github emits ::warning annotations. markdown emits a GFM table (exhaustive). html emits a self-contained styled HTML page. pr-comment is the opinionated PR-bot variant: hides unchanged rows, caps each section, collapses non-critical info into <details> blocks. sarif emits SARIF 2.1.0 JSON for upload to GitHub Code Scanning, VS Code, and other static-analysis tooling (see SARIF output below). | | --summary | off | Print only aggregate stats (total, crappy count, worst offender) — no per-function table. In --workspace mode this becomes the per-package summary plus the aggregate line. | | --workspace | off | Analyze all workspace packages (discovered via package.json workspaces or pnpm-workspace.yaml). Ignores --path. Adds a Per-package summary table to human and markdown output, and a package field to JSON entries. | | --verbose | off | Print step-by-step progress to stderr (file discovery, analysis progress, merge/scoring steps). | | --watch | off | Re-run automatically when source files or LCOV change. Uses 1-second polling. Press Ctrl+C to stop. | | --changed | off | Only analyze uncommitted .ts/.tsx files (modified, staged, and untracked). Useful for pre-commit checks and local iteration. CLI-only; not supported in config. | | --fail-above | off | Exit 1 if any function exceeds --threshold. | | --baseline <FILE> | — | JSON from a previous --format json run. Enables delta mode (shows Δ column). Functions that moved between files (same name, body unchanged) are detected and reported as Moved rather than as separate New + Removed entries; renderers show ← <previous_file> next to the new location. | | --fail-regression | off | Exit 1 if any function's score increased since --baseline. Moved (pure relocation, no score change) is not a regression. | | --epsilon <VALUE> | 0.01 | Tolerance for the regression detector. Score deltas with absolute value at or below this count as Unchanged. Set to 0.0 to flag every increase, or higher to tolerate noisy coverage. Must be non-negative. | | --sort <fields> | crap | Comma-separated display sort fields. crap (default) shows highest-risk first. file (or name) sorts by filename. path sorts by full file path. function sorts by function name. line sorts by line number. cc/cyclomatic sorts by complexity descending. coverage sorts by coverage descending. Combine fields like file,function or function,path. --top always selects the worst offenders first regardless of sort order. | | --jobs <N> | host CPUs | Cap parallel source-file analysis at N threads. Useful in memory-constrained CI/Docker environments. Must be a positive integer. | | --output <FILE> | — | Write output to FILE instead of stdout (useful for saving JSON baselines). | | --no-color | — | Disable colored output. |

Filtering order

Flags are applied in this order:

  1. --min — filter out low CRAP scores
  2. --max — filter out high CRAP scores
  3. --only-failures — keep only functions above threshold
  4. --top — slice to N worst remaining

Configuration file

Any flag can be set persistently in .react-crap.json at the project root (or any parent directory — the tool walks up until it finds one). CLI flags always take precedence.

{
  "threshold": 30,
  "top": 10,
  "min": 10,
  "max": 5000,
  "onlyFailures": false,
  "missing": "pessimistic",
  "exclude": ["**/*.test.ts", "**/*.test.tsx"],
  "allow": ["src/generated/**"],
  "failAbove": true,
  "workspace": false,
  "verbose": false,
  "sort": "crap"
}

All keys are optional. Unknown keys are rejected to catch typos.

Inline annotations

You can control individual functions directly in your source code with leading comments:

// react-crap-ignore
export function legacyHelper() {
  // This function will be excluded from analysis entirely.
}

// @crap-threshold 50
export function parser(input: string) {
  // This function is allowed a higher threshold (50 instead of the global default).
}
  • // react-crap-ignore — excludes the next function from analysis
  • // @crap-threshold N — overrides the global threshold for the next function

Context-aware function naming

Anonymous arrow functions and function expressions are resolved from their surrounding context, so you never see generic <anonymous> spam:

| Pattern | Displayed name | |---------|----------------| | return () => {} | handleAuthErrors return | | useEffect(() => {}) | useEffect callback | | dedupePromise(() => {}) | dedupePromise callback | | <Sheet>{() => ...}</Sheet> | Sheet child | | (async () => {})() | useEffect callback IIFE | | const dropSpec = () => () => {} | dropSpec nested |

If a name cannot be resolved, the tool walks up the AST to the nearest named parent function to provide useful context.

Caching

Complexity analysis results are cached in .react-crap-cache.json (created next to .react-crap.json). Only files whose content has changed are re-analyzed. This makes repeated runs near-instant on large codebases. The cache is automatically invalidated when the file hash changes.

JSON output schema

--format json produces a versioned envelope with a $schema URL pointing at the published JSON Schema. Consumers can validate output offline or generate types directly from the schema.

| Variant | Schema | |---------|--------| | Absolute (no --baseline) | schemas/report-v1.json | | Delta (with --baseline) | schemas/delta-v2.json |

// react-crap --format json
{
  "$schema": "https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/report-v1.json",
  "version": "0.1.0",
  "entries": [
    {
      "file": "src/lib.ts",
      "function": "doThing",
      "line": 12,
      "cyclomatic": 4,
      "coverage": 75.0,
      "crap": 5.6,
      "package": "my-pkg"
    }
  ]
}

// react-crap --format json --baseline baseline.json
{
  "$schema": "https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/delta-v2.json",
  "version": "0.1.0",
  "entries": [],
  "removed": []
}

--baseline only reads files in this envelope shape; bare-array baselines from older runs must be regenerated.

SARIF output

--format sarif emits a SARIF 2.1.0 JSON document — the format consumed by GitHub Code Scanning, VS Code, and most static-analysis tooling.

  • Each crappy function (entry above --threshold) becomes one result with level: "warning" and a physical location pointing at the function's start line.
  • Functions below the threshold are not included.
  • An empty result set still produces a valid SARIF document with the full runs[0].tool.driver envelope.
  • --baseline is rejected with --format sarif; SARIF describes findings, not deltas. Use --format json for delta output.

Design

The tool has six orthogonal modules. Each is testable in isolation; the join between them has its own integration test.

  vitest --coverage                  typescript
  (LCOV file)                      (TS AST)
        │                              │
        ▼                              ▼
  ┌───────────┐                 ┌────────────┐
  │ coverage  │                 │ complexity │
  │  module   │                 │   module   │
  └─────┬─────┘                 └──────┬─────┘
        │                              │
        └──────────┬───────────────────┘
                   ▼
             ┌──────────┐
             │  merge   │  ← path normalization lives here
             └─────┬────┘
                   ▼
             ┌──────────┐     ┌───────┐
             │  score   │ ──▶ │ delta │  ← baseline comparison (optional)
             └─────┬────┘     └───────┘
                   ▼
             ┌──────────┐
             │  report  │  ← human / JSON / GitHub / Markdown
             └──────────┘

The path-matching problem

This is where silent failures happen. Complexity analysis produces absolute paths (whatever was passed to the walker). LCOV files contain whatever the coverage tool decided to write:

  1. Absolute paths — /home/alice/project/src/foo.ts
  2. Project-relative paths — src/foo.ts
  3. Workspace-relative paths in a monorepo — packages/core/src/foo.ts
  4. Paths with ./ or ../ components

A naïve Map<string, _> lookup silently returns None for 100% of files when the two don't agree, and every function reports as 0% covered. react-crap handles this with a two-level index:

  • Absolute coverage paths → direct canonical-path hash lookup.
  • Relative coverage paths → suffix match on path components (not bytes — /foo/bar.ts must not match oofoo/bar.ts).

Relative paths are never canonicalized against the process's CWD, which would otherwise silently bind them to whatever file happened to exist under the tool's working directory.

The --missing policy

Some functions have complexity data but no coverage data — the coverage tool didn't instrument them, or they were excluded via test files, or the coverage run was scoped to a subset of the workspace. Three policies:

  • pessimistic (default): treat as 0% covered. Surfaces unmapped code as a red flag. Correct for CI gates.
  • optimistic: treat as 100% covered. Useful during local development when you're iterating on a specific module.
  • skip: drop the row entirely.

Integrating with CI

Absolute threshold gate

- run: npx vitest run --coverage
- run: npx react-crap --lcov coverage/lcov.info --fail-above --threshold 30

Regression gate (recommended for teams)

Save a baseline on master, then fail on any PR that makes a score go up. This works regardless of the absolute threshold and catches regressions as they are introduced, not weeks later.

# On master branch — upload baseline as a CI artifact
- run: npx vitest run --coverage
- run: npx react-crap --lcov coverage/lcov.info --format json --sort file --output baseline.json
- uses: actions/upload-artifact@v4
  with:
    name: crap-baseline
    path: baseline.json

# On pull requests — download baseline and compare
- uses: actions/download-artifact@v4
  with:
    name: crap-baseline
    path: baseline
- run: npx vitest run --coverage
- run: npx react-crap --lcov coverage/lcov.info --baseline baseline/baseline.json --fail-regression

GitHub Code Scanning (SARIF)

Upload --format sarif output to surface crappy functions in the repository's Security → Code scanning tab. The job needs security-events: write.

self_score:
  permissions:
    security-events: write
  steps:
    - run: npx vitest run --coverage
    - run: npx react-crap --lcov coverage/lcov.info --format sarif --output crap.sarif
    - uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: crap.sarif
        category: react-crap

PR comment bot

--format pr-comment produces a sticky comment that surfaces regressions and new functions in the primary table and tucks improvements / removed functions / above-threshold hot-spots into collapsed <details> blocks. A hidden marker (<!-- react-crap-report -->) lets the script update an existing comment instead of posting duplicates. The job needs pull-requests: write.

self_score:
  permissions:
    pull-requests: write
  steps:
    # ...generate coverage and download baseline as above...

    - name: Generate PR comment
      if: github.event_name == 'pull_request'
      run: |
        npx react-crap \
          --lcov coverage/lcov.info \
          --baseline baseline.json \
          --format pr-comment \
          --output crap-comment.md

    - name: Post or update PR comment
      if: github.event_name == 'pull_request'
      uses: actions/github-script@v7
      with:
        script: |
          const fs = require('fs');
          const body = fs.readFileSync('crap-comment.md', 'utf8');
          const marker = '<!-- react-crap-report -->';
          const { data: comments } = await github.rest.issues.listComments({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.issue.number,
          });
          const existing = comments.find(c => c.body.startsWith(marker));
          const args = {
            owner: context.repo.owner,
            repo: context.repo.repo,
            body,
          };
          if (existing) {
            await github.rest.issues.updateComment({ ...args, comment_id: existing.id });
          } else {
            await github.rest.issues.createComment({ ...args, issue_number: context.issue.number });
          }

What this tool is not

  • It is not a replacement for engineering judgment.
  • It does not understand your business domain.
  • It does not prove that your tests are good.

Coverage can execute a line without asserting the right behavior. A function can be fully covered and still poorly tested.

So the CRAP score should not be treated as absolute truth. It is a signal — a useful one.

The best use of the tool is to ask better questions:

  • Why is this function so complex?
  • Is this complexity essential or accidental?
  • Do the tests cover the important branches?
  • Can we split this into smaller pieces?
  • Should this logic be modeled more explicitly?

Good tools do not replace thinking. They make thinking easier to focus.

Prior art and references

Contributing

See CONTRIBUTING.md for the commit convention, development setup, and release process.

License

MIT