code-smells
v0.2.11
Published
Code smell detector for React/TypeScript repos. Thin wrapper over code-pushup with curated ESLint rules, research-backed signals (Tornhill temporal coupling, Nagappan team ownership), and monorepo support.
Maintainers
Readme
code-smells
A curated, research-backed static-analysis tool for React/TypeScript repos. Thin wrapper over code-pushup with opinionated defaults — hook overload, inline props, fan-out coupling, hidden temporal coupling (Tornhill), cross-team ownership (Nagappan 2007), and more. Zero setup against any TS/React repo.
cd into the repo you want to scan, then run:
npx code-smellsReports land in ./reports/report.{json,md}. Requires Node.js 18+ and git.
Or install globally
npm install -g code-smellsWorth doing if you run the tool more than occasionally:
- Faster startup — skips npx's package-resolution step (a few seconds saved per run; adds up in CI or across a sweep of many repos)
- Offline after first install — npx requires network to check the registry each run unless you pin
@x.y.zexactly - Pinned version you control — no surprise patch-version bumps the next time you run
- Shell tab completion works out of the box;
which code-smellsresolves - No
npxprefix — justcode-smellsfrom anywhere in your shell
What you get
Eight categories scored 0-100, composed from 50+ audits across 11 plugins:
| Category | Signal |
|---|---|
| Component Health | Body size, cognitive complexity, hook overload, multi-component files |
| Render Performance | Inline props in JSX, unstable useSelector returns |
| Coupling | Import fan-out, domain-boundary violations (opt-in), hidden temporal coupling |
| Type Safety | TypeScript compiler diagnostics + type-coverage (inferred-any) |
| Security & Dependencies | npm audit vulnerabilities + outdated prod deps |
| Accessibility | jsx-a11y curated subset (alt text, ARIA, focus management) |
| Test Quality | testing-library antipatterns + function/branch/line coverage |
| Maintainability | Duplicated code, churn, bug-fix density, author + team dispersion, dead code |
Categories auto-adapt to target capabilities — no tsconfig skips Type Safety, no lockfile skips Security, no lcov skips the coverage portion of Test Quality.
Output looks like this:
● max-lines-per-function 47 violations
● react-perf/jsx-no-new-function-as-prop 89 violations
● sonarjs/cognitive-complexity 15 violations
● code-smells/hook-count 12 violations
● temporal-coupling/hidden-coupling 8 pairs (max 100% co-change)
● team-ownership/cross-team-churn 0 files
● jsx-a11y/alt-text 3 violations
● testing-library/no-container 2 violations
● ... 40 more audits ...
┌───────────────────────────┬─────────┬──────────┐
│ Category │ Score │ Audits │
├───────────────────────────┼─────────┼──────────┤
│ Component Health │ 0 │ 5 │
│ Render Performance │ 43 │ 4 │
│ Coupling │ 97 │ 3 │
│ Type Safety │ 73 │ 5 │
│ Security & Dependencies │ 100 │ 3 │
│ Accessibility │ 92 │ 11 │
│ Test Quality │ 78 │ 11 │
│ Maintainability │ 89 │ 11 │
└───────────────────────────┴─────────┴──────────┘Why this vs. running ESLint directly
ESLint gives you rule-by-rule violation counts. This gives you:
- Category scores rolling 40+ audits into 6 top-level dials you can trend over time
- Research-backed signals (temporal coupling, team ownership) that no ESLint rule covers — these predict defects better than any static-analysis metric in the Nagappan 2007 study
- Monorepo support out of the box — auto-detects workspaces, expands tsconfig project references, gracefully degrades when plugins don't apply to a sub-workspace
- CI ratchet via
code-pushup compare --fail-on=regression— fail PRs that worsen the score without cargo-culting absolute thresholds - Thin by design — every audit is an existing tool (ESLint rule, maintained CLI, npm package), curated and weighted. No custom analysis engine to outgrow.
Usage
Single-package repo
From inside the repo:
npx code-smellsDefaults: src/**/*.{ts,tsx} source glob, src/ as the dependency-cruiser entry. Override either via env var when needed:
CP_PATTERNS='src/js/**/*.{ts,tsx}' CP_ENTRY='src/js' npx code-smellsMonorepo (yarn / pnpm / npm workspaces)
Auto-detects plugins/<ws>/src, libs/<ws>/src, packages/<ws>/src layouts. From the monorepo root:
CP_PATTERNS='{plugins,libs,packages}/*/src/**/*.{ts,tsx}' npx code-smellsIf the root tsconfig.json uses project references, they're expanded automatically into per-workspace tsconfigs for the TypeScript plugin.
Running from elsewhere (CI, scripts)
If you can't cd first, point CP_TARGET at the repo root:
CP_TARGET=/path/to/repo npx code-smellsEnv vars
| Var | Default | Purpose |
|---|---|---|
| CP_TARGET | current directory | Override the repo under analysis. Useful in CI / wrappers where you can't cd first. |
| CP_PATTERNS | src/**/*.{ts,tsx} (auto-detects workspaces in monorepos) | Glob for source files (ESLint, jsdocs, type-coverage) |
| CP_ENTRY | src (auto-detects workspaces in monorepos) | dependency-cruiser entry point(s); comma-separated list |
| CP_TSCONFIG | auto-detected from references | Explicit tsconfig path(s), comma-separated |
| CP_COVERAGE_LCOV | coverage/lcov.info if present | Path to lcov file for the coverage audit |
| CP_ENABLE_FORMATJS | off | Set =true to enable formatjs/no-literal-string-in-jsx (react-intl repos) |
| CP_OUTPUT_DIR | OS cache dir | Where reports land. Set =./reports to keep them inside the target repo. |
| CP_OPEN | off | Set =md or =json to auto-open the report after the run (macOS open, Linux xdg-open, Windows start). |
Custom ESLint rules
Ships 4 rules under the code-smells/ plugin namespace, each filling a gap where no community rule does the job:
| Rule | Flags |
|---|---|
| hook-count | Components with more than N total hook calls (default 10) |
| use-effect-count | Components with more than N useEffect calls (default 3) |
| unstable-selector-returns | useSelector with inline object-literal return — unstable reference, re-renders every time |
| domain-boundaries | Opt-in. Files referencing N+ distinct domain buckets. Bring your own { token: bucket } map — see the rule source for examples |
CI
Drop examples/workflows/code-smells.yml into your repo's .github/workflows/. It seeds a baseline on main, compares PRs against it, posts a single sticky summary comment, and fails on category regressions.
Full setup guide in docs/ci-integration.md. Three Google Tricorder principles applied: delta over absolute thresholds, summary over per-line annotations, PR-diff-only reporting.
Architecture
code-smells/
├── code-pushup.config.mjs # 11 plugins + 8 categories
├── eslint.target-rules.mjs # Opinionated ESLint config applied to target source
├── eslint-rules/ # Custom ESLint rules (~180 lines total)
├── plugins/ # Thin adapters over existing tools
│ ├── eslint.plugin.mjs # Programmatic ESLint — drives all rule-backed audits
│ ├── coupling.plugin.mjs # dependency-cruiser (fan-out)
│ ├── duplication.plugin.mjs # jscpd (duplicated lines)
│ ├── knip.plugin.mjs # knip (dead code)
│ ├── type-coverage.plugin.mjs # type-coverage (inferred-any)
│ ├── churn.plugin.mjs # git log — file-change frequency
│ ├── bug-fix-density.plugin.mjs # git log — fix commit density
│ ├── author-dispersion.plugin.mjs # git log — individual author dispersion
│ ├── temporal-coupling.plugin.mjs # git log + dep-cruiser — Tornhill hidden coupling
│ └── team-ownership.plugin.mjs # git log + CODEOWNERS — Nagappan cross-team defect predictor
├── examples/workflows/ # Drop-in GitHub Actions template
├── docs/
│ ├── ci-integration.md
│ └── decisions/ # ADRs
├── VISION.md # Boundaries + research footing
└── TASKS.md # RoadmapWhy thin is the design
Per VISION.md: value is curation + defaults + plumbing, not analysis logic. Every audit is an existing tool (ESLint rule, maintained CLI, npm package) wired into code-pushup's audit/category model. Justified custom code:
- ~180 lines total for the 4 custom ESLint rules — each a gap no community rule fills
- ~190 lines for
temporal-coupling(no maintained Node tool; code-maat is JVM-based) - ~180 lines for
team-ownership(wraps codeowners-utils with Nagappan cross-team-commit aggregation) - ~100 lines each for the thin plugin wrappers (
coupling,duplication,churn,bug-fix-density,author-dispersion,knip,type-coverage)
Every custom file's JSDoc header explains why that specific wrapper exists — the gap it fills, what alternative was ruled out, what constraints shaped it.
Decisions (ADRs)
- ADR-0001 — use
eslint-plugin-jsx-a11yfor accessibility; do not adopt@code-pushup/axe-plugin.
Development
git clone https://github.com/fyodoriv/code-smells && cd code-smells
npm install
node ./bin/code-smells.mjs # run against cwd (the tool's own repo)
npm test # vitest (200+ unit tests)
npm run test:coverage # v8 coverage, thresholds: 95% stmts/funcs/lines, 90% branches
npm run format # prettierCI runs tests + coverage on every push / PR via .github/workflows/ci.yml.
Coverage thresholds live in vitest.config.mjs — the build fails if any
metric drops below target.
Releases are automated via release-please — conventional commits on main accumulate into a release PR; when it merges, a GitHub release fires and .github/workflows/publish.yml publishes to npm with provenance. To bootstrap: add an NPM_TOKEN repo secret with an npm automation/granular token scoped to the code-smells package.
Follow-up work lives in TASKS.md. Highlights: tune-weights (calibrate category weights against curated hotspots), bundle-size-plugin, stylelint-plugin, evaluate-sonarcloud.
