Skip to content

Repository files navigation

sadpath

sadpath eslint-plugin-sadpath CI license

The linter that checks you handled the sad path, not just the happy path.

Every async boundary in a UI can be loading, error, empty, offline, unauthorized, loadingMore… but developers — and AI codegen especially — render only success and ship the rest as blank screens, infinite spinners and white-screen crashes.

Other tools lint how you call your data (@tanstack/eslint-plugin-query), or render the states for you if you adopt their component. sadpath is the diagnosis: it statically audits arbitrary components — including the ones your agent wrote — and tells you which states you forgot.

✗ ProjectList  examples/bad.tsx:3
  ✗ useQuery → manque loading, error, empty
  State coverage  0%

Packages

Package What it is
sadpath-core The analysis engine (AST + adapters). Framework-agnostic.
eslint-plugin-sadpath The main product: ESLint rules, in your editor and CI.
sadpath CLI: a state coverage report + badge over your repo.
sadpath-states Headless Skeleton / EmptyState / ErrorState — the fixes sadpath scaffolds.

Install

npm i -D eslint-plugin-sadpath   # ESLint plugin (editor + CI)
npm i -D sadpath                 # CLI: npx sadpath

Quickstart

# no install needed — audit any folder
npx sadpath src

# or, from a clone of this repo:
pnpm install && pnpm build
node packages/cli/dist/index.js examples

ESLint

// eslint.config.js
import sadpath from 'eslint-plugin-sadpath';

export default [sadpath.configs.recommended];

CLI

npx sadpath src           # report per file + overall state coverage
npx sadpath src --min 90  # exit 1 if coverage < 90 % (CI gate)
npx sadpath src --json    # machine-readable

How it works

For each data-hook boundary (useQuery, useSWR, custom hooks you declare), sadpath computes the set of states the component actually handles and compares it to the set it requires. The difference is the finding, and the ratio across the repo is your state coverage.

Detection is type-aware static analysis — no backend, no runtime, no telemetry.

Configuration

Optional. Drop a sadpath.config.ts at your project root — the CLI auto-detects it (or pass --config <path>), and the ESLint rule takes the same shape as options.

import { defineConfig } from 'sadpath-core';

export default defineConfig({
  require: ['loading', 'error', 'empty'],
  customHooks: {
    useUser: { data: 'user', loading: 'isLoading', error: 'error' },
  },
});

Fix (codemod)

sadpath can scaffold the missing branches for you — a deterministic AST codemod, no AI:

sadpath src --fix   # rewrites files in place

(Or via ESLint: eslint --fix, and the editor quick-fix.)

// after `sadpath --fix`
const query = useQuery(/* … */);
if (query.isPending) return null; // TODO(sadpath): loading state
if (query.isError) return null; // TODO(sadpath): error state
if (query.data.length === 0) return null; // TODO(sadpath): empty state
return <ul>{query.data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;

It scaffolds the structure (return null + TODO) — you fill in the UI. Safe by design: only queries in a block-bodied function ending in return, idempotent, and mutations / ambiguous shapes are left untouched.

Badge

Write a self-contained SVG coverage badge (no shields.io, no network) and regenerate it in CI:

sadpath src --badge              # writes ./sadpath-badge.svg
sadpath src --badge docs/cov.svg # custom path
![state coverage](./sadpath-badge.svg)

CI / GitHub code scanning

Export findings as SARIF and upload them — they appear as annotations right on the PR diff and in the repo's Security tab:

sadpath src --sarif            # writes ./sadpath.sarif
sadpath src --sarif out.sarif  # custom path

Copy examples/github-workflow.yml to .github/workflows/sadpath.yml — it runs sadpath and uploads the SARIF via github/codeql-action/upload-sarif. (Needs permissions: security-events: write.)

Status — v0.6.0

Working today: React + react-query & SWR, queries and mutations (useMutation / useSWRMutation → pending + error), both const { data } = useQuery() and const q = useQuery() (member-access) patterns, !data loading guards, the exhaustive-async-states rule (with autofix), collection-aware empty detection, sadpath.config.ts loading, CLI coverage report, --fix codemod, --badge, and --sarif for code scanning.

Roadmap: SWR/Apollo adapters · --badge + SARIF · sadpath fix codemod · mutations · unauthorized/offline · Vue & Svelte.

Honest limitations

  • Empty is required only when the data is used as a collection, and counted as handled via a .length guard or a rendered <Empty…/>; full type-awareness via the type-checker is on the roadmap.
  • Cross-file error boundaries aren't resolved yet — handling is checked locally.
  • Escape hatch coming: // sadpath-ignore-next.

License

MIT © Geekles007

About

The linter that checks you handled the sad path — requires loading/error/empty states in your components. ESLint plugin + CLI state-coverage. No backend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages