@omermohideen/react-crap 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +400 -0
  3. package/dist/bin/react-crap.d.ts +3 -0
  4. package/dist/bin/react-crap.d.ts.map +1 -0
  5. package/dist/bin/react-crap.js +67 -0
  6. package/dist/bin/react-crap.js.map +1 -0
  7. package/dist/src/cache.d.ts +13 -0
  8. package/dist/src/cache.d.ts.map +1 -0
  9. package/dist/src/cache.js +33 -0
  10. package/dist/src/cache.js.map +1 -0
  11. package/dist/src/complexity.d.ts +11 -0
  12. package/dist/src/complexity.d.ts.map +1 -0
  13. package/dist/src/complexity.js +279 -0
  14. package/dist/src/complexity.js.map +1 -0
  15. package/dist/src/config.d.ts +17 -0
  16. package/dist/src/config.d.ts.map +1 -0
  17. package/dist/src/config.js +96 -0
  18. package/dist/src/config.js.map +1 -0
  19. package/dist/src/coverage.d.ts +14 -0
  20. package/dist/src/coverage.d.ts.map +1 -0
  21. package/dist/src/coverage.js +62 -0
  22. package/dist/src/coverage.js.map +1 -0
  23. package/dist/src/delta.d.ts +26 -0
  24. package/dist/src/delta.d.ts.map +1 -0
  25. package/dist/src/delta.js +102 -0
  26. package/dist/src/delta.js.map +1 -0
  27. package/dist/src/index.d.ts +26 -0
  28. package/dist/src/index.d.ts.map +1 -0
  29. package/dist/src/index.js +388 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/merge.d.ts +20 -0
  32. package/dist/src/merge.d.ts.map +1 -0
  33. package/dist/src/merge.js +83 -0
  34. package/dist/src/merge.js.map +1 -0
  35. package/dist/src/report/github.d.ts +3 -0
  36. package/dist/src/report/github.d.ts.map +1 -0
  37. package/dist/src/report/github.js +14 -0
  38. package/dist/src/report/github.js.map +1 -0
  39. package/dist/src/report/html.d.ts +3 -0
  40. package/dist/src/report/html.d.ts.map +1 -0
  41. package/dist/src/report/html.js +75 -0
  42. package/dist/src/report/html.js.map +1 -0
  43. package/dist/src/report/human.d.ts +10 -0
  44. package/dist/src/report/human.d.ts.map +1 -0
  45. package/dist/src/report/human.js +126 -0
  46. package/dist/src/report/human.js.map +1 -0
  47. package/dist/src/report/json.d.ts +5 -0
  48. package/dist/src/report/json.d.ts.map +1 -0
  49. package/dist/src/report/json.js +20 -0
  50. package/dist/src/report/json.js.map +1 -0
  51. package/dist/src/report/markdown.d.ts +3 -0
  52. package/dist/src/report/markdown.d.ts.map +1 -0
  53. package/dist/src/report/markdown.js +16 -0
  54. package/dist/src/report/markdown.js.map +1 -0
  55. package/dist/src/report/pr-comment.d.ts +3 -0
  56. package/dist/src/report/pr-comment.d.ts.map +1 -0
  57. package/dist/src/report/pr-comment.js +62 -0
  58. package/dist/src/report/pr-comment.js.map +1 -0
  59. package/dist/src/report/sarif.d.ts +3 -0
  60. package/dist/src/report/sarif.d.ts.map +1 -0
  61. package/dist/src/report/sarif.js +50 -0
  62. package/dist/src/report/sarif.js.map +1 -0
  63. package/dist/src/score.d.ts +29 -0
  64. package/dist/src/score.d.ts.map +1 -0
  65. package/dist/src/score.js +52 -0
  66. package/dist/src/score.js.map +1 -0
  67. package/dist/src/walker.d.ts +7 -0
  68. package/dist/src/walker.d.ts.map +1 -0
  69. package/dist/src/walker.js +75 -0
  70. package/dist/src/walker.js.map +1 -0
  71. package/package.json +72 -0
  72. package/schemas/delta-v2.json +41 -0
  73. package/schemas/report-v1.json +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Omer Mohideen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # react-crap
2
+
3
+ [![npm](https://img.shields.io/npm/v/@omermohideen/react-crap?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/package/@omermohideen/react-crap)
4
+ [![license](https://img.shields.io/badge/license-MIT-2563eb?style=for-the-badge)](LICENSE)
5
+
6
+ Compute the **CRAP** (Change Risk Anti-Patterns) metric for React TypeScript projects.
7
+
8
+ 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.
9
+
10
+ ```
11
+ CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)
12
+ ```
13
+
14
+ A few properties worth internalizing before you use the output:
15
+
16
+ - A trivial function (CC=1, 100% covered) scores exactly 1.0. That's the lower bound.
17
+ - 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.
18
+ - 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."
19
+
20
+ ## Install
21
+
22
+ **Via `npx`** (no install):
23
+
24
+ ```bash
25
+ npx @omermohideen/react-crap --lcov coverage/lcov.info --path src
26
+ ```
27
+
28
+ **Via `npm`** (global):
29
+
30
+ ```bash
31
+ npm install -g @omermohideen/react-crap
32
+ ```
33
+
34
+ **Via `npm`** (local dev dependency):
35
+
36
+ ```bash
37
+ npm install --save-dev @omermohideen/react-crap
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```bash
43
+ # 1. Generate an LCOV coverage report.
44
+ npx vitest run --coverage
45
+
46
+ # 2. Score every function.
47
+ npx react-crap --lcov coverage/lcov.info --path src
48
+
49
+ # 3. Gate CI on the threshold.
50
+ npx react-crap --lcov coverage/lcov.info --fail-above
51
+
52
+ # 4. Whole-workspace analysis (monorepos).
53
+ npx react-crap --lcov coverage/lcov.info --path . --workspace --top 20
54
+
55
+ # 5. Quick aggregate summary (no table).
56
+ npx react-crap --lcov coverage/lcov.info --summary
57
+
58
+ # 6. Watch mode during local development.
59
+ npx react-crap --lcov coverage/lcov.info --path src --watch --verbose
60
+
61
+ # 7. Generate an HTML report.
62
+ npx react-crap --lcov coverage/lcov.info --path src --format html --output crap-report.html
63
+ ```
64
+
65
+ Example output:
66
+
67
+ ```
68
+ ┌───┬───────┬────┬───────────────────┬──────────┬───────────────┐
69
+ │ │ CRAP │ CC │ Coverage │ Function │ Location │
70
+ ╞═══╪═══════╪════╪═══════════════════╪══════════╪═══════════════╡
71
+ │ ✗ │ 156.0 │ 12 │ ░░░░░░░░░░ 0.0% │ crappy │ src/lib.ts:24 │
72
+ │ ▲ │ 6.7 │ 4 │ ████░░░░░░ 44.4% │ moderate │ src/lib.ts:12 │
73
+ │ ✓ │ 1.0 │ 1 │ ██████████ 100.0% │ trivial │ src/lib.ts:8 │
74
+ └───┴───────┴────┴───────────────────┴──────────┴───────────────┘
75
+ ✗ 1/3 function(s) exceed CRAP threshold 30.
76
+ ```
77
+
78
+ ## Flags
79
+
80
+ | Flag | Default | Purpose |
81
+ |------|---------|---------|
82
+ | `--lcov <FILE>` | `coverage/lcov.info` | LCOV file from your test runner (Vitest, Jest, etc.). |
83
+ | `--path <DIR>` | `src` | Root to walk for `.ts` / `.tsx` files (respects `.gitignore`). |
84
+ | `--threshold <N>` | `30` | Score above which a function is flagged. |
85
+ | `--min <SCORE>` | — | Hide entries **below** this CRAP score. |
86
+ | `--max <SCORE>` | — | Hide entries **above** this CRAP score. |
87
+ | `--top <N>` | — | Show only the **N worst** offenders. |
88
+ | `--only-failures` | — | Only show functions **exceeding** the threshold. |
89
+ | `--missing {pessimistic,optimistic,skip}` | `pessimistic` | How to score a function with no coverage data. |
90
+ | `--exclude <GLOB>` | — | Skip files matching this pattern (repeatable). `**` crosses directories. |
91
+ | `--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*`). |
92
+ | `--format {human,json,github,markdown,html,pr-comment,sarif}` | `human` | Output format. `json` emits a versioned envelope (see [JSON output schema](#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](#sarif-output) below). |
93
+ | `--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. |
94
+ | `--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. |
95
+ | `--verbose` | off | Print step-by-step progress to stderr (file discovery, analysis progress, merge/scoring steps). |
96
+ | `--watch` | off | Re-run automatically when source files or LCOV change. Uses 1-second polling. Press Ctrl+C to stop. |
97
+ | `--fail-above` | off | Exit 1 if any function exceeds `--threshold`. |
98
+ | `--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. |
99
+ | `--fail-regression` | off | Exit 1 if any function's score increased since `--baseline`. `Moved` (pure relocation, no score change) is not a regression. |
100
+ | `--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. |
101
+ | `--jobs <N>` | host CPUs | Cap parallel source-file analysis at `N` threads. Useful in memory-constrained CI/Docker environments. Must be a positive integer. |
102
+ | `--output <FILE>` | — | Write output to FILE instead of stdout (useful for saving JSON baselines). |
103
+ | `--no-color` | — | Disable colored output. |
104
+
105
+ ### Filtering order
106
+
107
+ Flags are applied in this order:
108
+
109
+ 1. `--min` — filter out low CRAP scores
110
+ 2. `--max` — filter out high CRAP scores
111
+ 3. `--only-failures` — keep only functions above threshold
112
+ 4. `--top` — slice to N worst remaining
113
+
114
+ ## Configuration file
115
+
116
+ 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.
117
+
118
+ ```json
119
+ {
120
+ "threshold": 30,
121
+ "top": 10,
122
+ "min": 10,
123
+ "max": 5000,
124
+ "onlyFailures": false,
125
+ "missing": "pessimistic",
126
+ "exclude": ["**/*.test.ts", "**/*.test.tsx"],
127
+ "allow": ["src/generated/**"],
128
+ "failAbove": true,
129
+ "workspace": false,
130
+ "verbose": false
131
+ }
132
+ ```
133
+
134
+ All keys are optional. Unknown keys are rejected to catch typos.
135
+
136
+ ## Inline annotations
137
+
138
+ You can control individual functions directly in your source code with leading comments:
139
+
140
+ ```typescript
141
+ // react-crap-ignore
142
+ export function legacyHelper() {
143
+ // This function will be excluded from analysis entirely.
144
+ }
145
+
146
+ // @crap-threshold 50
147
+ export function parser(input: string) {
148
+ // This function is allowed a higher threshold (50 instead of the global default).
149
+ }
150
+ ```
151
+
152
+ - `// react-crap-ignore` — excludes the next function from analysis
153
+ - `// @crap-threshold N` — overrides the global threshold for the next function
154
+
155
+ ## Context-aware function naming
156
+
157
+ Anonymous arrow functions and function expressions are resolved from their surrounding context, so you never see generic `<anonymous>` spam:
158
+
159
+ | Pattern | Displayed name |
160
+ |---------|----------------|
161
+ | `return () => {}` | `handleAuthErrors return` |
162
+ | `useEffect(() => {})` | `useEffect callback` |
163
+ | `dedupePromise(() => {})` | `dedupePromise callback` |
164
+ | `<Sheet>{() => ...}</Sheet>` | `Sheet child` |
165
+ | `(async () => {})()` | `useEffect callback IIFE` |
166
+ | `const dropSpec = () => () => {}` | `dropSpec nested` |
167
+
168
+ If a name cannot be resolved, the tool walks up the AST to the nearest named parent function to provide useful context.
169
+
170
+ ## Caching
171
+
172
+ 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.
173
+
174
+ ## JSON output schema
175
+
176
+ `--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.
177
+
178
+ | Variant | Schema |
179
+ |---------|--------|
180
+ | Absolute (no `--baseline`) | [`schemas/report-v1.json`](https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/report-v1.json) |
181
+ | Delta (with `--baseline`) | [`schemas/delta-v2.json`](https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/delta-v2.json) |
182
+
183
+ ```json
184
+ // react-crap --format json
185
+ {
186
+ "$schema": "https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/report-v1.json",
187
+ "version": "0.1.0",
188
+ "entries": [
189
+ {
190
+ "file": "src/lib.ts",
191
+ "function": "doThing",
192
+ "line": 12,
193
+ "cyclomatic": 4,
194
+ "coverage": 75.0,
195
+ "crap": 5.6,
196
+ "package": "my-pkg"
197
+ }
198
+ ]
199
+ }
200
+
201
+ // react-crap --format json --baseline baseline.json
202
+ {
203
+ "$schema": "https://raw.githubusercontent.com/OmerMohideen/react-crap/master/schemas/delta-v2.json",
204
+ "version": "0.1.0",
205
+ "entries": [],
206
+ "removed": []
207
+ }
208
+ ```
209
+
210
+ `--baseline` only reads files in this envelope shape; bare-array baselines from older runs must be regenerated.
211
+
212
+ ## SARIF output
213
+
214
+ `--format sarif` emits a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) JSON document — the format consumed by GitHub Code Scanning, VS Code, and most static-analysis tooling.
215
+
216
+ - Each crappy function (entry above `--threshold`) becomes one `result` with `level: "warning"` and a physical location pointing at the function's start line.
217
+ - Functions below the threshold are not included.
218
+ - An empty result set still produces a valid SARIF document with the full `runs[0].tool.driver` envelope.
219
+ - `--baseline` is rejected with `--format sarif`; SARIF describes findings, not deltas. Use `--format json` for delta output.
220
+
221
+ ## Design
222
+
223
+ The tool has six orthogonal modules. Each is testable in isolation; the join between them has its own integration test.
224
+
225
+ ```
226
+ vitest --coverage typescript
227
+ (LCOV file) (TS AST)
228
+ │ │
229
+ ▼ ▼
230
+ ┌───────────┐ ┌────────────┐
231
+ │ coverage │ │ complexity │
232
+ │ module │ │ module │
233
+ └─────┬─────┘ └──────┬─────┘
234
+ │ │
235
+ └──────────┬───────────────────┘
236
+
237
+ ┌──────────┐
238
+ │ merge │ ← path normalization lives here
239
+ └─────┬────┘
240
+
241
+ ┌──────────┐ ┌───────┐
242
+ │ score │ ──▶ │ delta │ ← baseline comparison (optional)
243
+ └─────┬────┘ └───────┘
244
+
245
+ ┌──────────┐
246
+ │ report │ ← human / JSON / GitHub / Markdown
247
+ └──────────┘
248
+ ```
249
+
250
+ ### The path-matching problem
251
+
252
+ 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:
253
+
254
+ 1. Absolute paths — `/home/alice/project/src/foo.ts`
255
+ 2. Project-relative paths — `src/foo.ts`
256
+ 3. Workspace-relative paths in a monorepo — `packages/core/src/foo.ts`
257
+ 4. Paths with `./` or `../` components
258
+
259
+ 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:
260
+
261
+ - Absolute coverage paths → direct canonical-path hash lookup.
262
+ - Relative coverage paths → suffix match on path components (not bytes — `/foo/bar.ts` must not match `oofoo/bar.ts`).
263
+
264
+ 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.
265
+
266
+ ### The `--missing` policy
267
+
268
+ 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:
269
+
270
+ - **pessimistic** (default): treat as 0% covered. Surfaces unmapped code as a red flag. Correct for CI gates.
271
+ - **optimistic**: treat as 100% covered. Useful during local development when you're iterating on a specific module.
272
+ - **skip**: drop the row entirely.
273
+
274
+ ## Integrating with CI
275
+
276
+ ### Absolute threshold gate
277
+
278
+ ```yaml
279
+ - run: npx vitest run --coverage
280
+ - run: npx react-crap --lcov coverage/lcov.info --fail-above --threshold 30
281
+ ```
282
+
283
+ ### Regression gate (recommended for teams)
284
+
285
+ 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.
286
+
287
+ ```yaml
288
+ # On master branch — upload baseline as a CI artifact
289
+ - run: npx vitest run --coverage
290
+ - run: npx react-crap --lcov coverage/lcov.info --format json --output baseline.json
291
+ - uses: actions/upload-artifact@v4
292
+ with:
293
+ name: crap-baseline
294
+ path: baseline.json
295
+
296
+ # On pull requests — download baseline and compare
297
+ - uses: actions/download-artifact@v4
298
+ with:
299
+ name: crap-baseline
300
+ path: baseline
301
+ - run: npx vitest run --coverage
302
+ - run: npx react-crap --lcov coverage/lcov.info --baseline baseline/baseline.json --fail-regression
303
+ ```
304
+
305
+ ### GitHub Code Scanning (SARIF)
306
+
307
+ Upload `--format sarif` output to surface crappy functions in the repository's **Security → Code scanning** tab. The job needs `security-events: write`.
308
+
309
+ ```yaml
310
+ self_score:
311
+ permissions:
312
+ security-events: write
313
+ steps:
314
+ - run: npx vitest run --coverage
315
+ - run: npx react-crap --lcov coverage/lcov.info --format sarif --output crap.sarif
316
+ - uses: github/codeql-action/upload-sarif@v3
317
+ with:
318
+ sarif_file: crap.sarif
319
+ category: react-crap
320
+ ```
321
+
322
+ ### PR comment bot
323
+
324
+ `--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`.
325
+
326
+ ```yaml
327
+ self_score:
328
+ permissions:
329
+ pull-requests: write
330
+ steps:
331
+ # ...generate coverage and download baseline as above...
332
+
333
+ - name: Generate PR comment
334
+ if: github.event_name == 'pull_request'
335
+ run: |
336
+ npx react-crap \
337
+ --lcov coverage/lcov.info \
338
+ --baseline baseline.json \
339
+ --format pr-comment \
340
+ --output crap-comment.md
341
+
342
+ - name: Post or update PR comment
343
+ if: github.event_name == 'pull_request'
344
+ uses: actions/github-script@v7
345
+ with:
346
+ script: |
347
+ const fs = require('fs');
348
+ const body = fs.readFileSync('crap-comment.md', 'utf8');
349
+ const marker = '<!-- react-crap-report -->';
350
+ const { data: comments } = await github.rest.issues.listComments({
351
+ owner: context.repo.owner,
352
+ repo: context.repo.repo,
353
+ issue_number: context.issue.number,
354
+ });
355
+ const existing = comments.find(c => c.body.startsWith(marker));
356
+ const args = {
357
+ owner: context.repo.owner,
358
+ repo: context.repo.repo,
359
+ body,
360
+ };
361
+ if (existing) {
362
+ await github.rest.issues.updateComment({ ...args, comment_id: existing.id });
363
+ } else {
364
+ await github.rest.issues.createComment({ ...args, issue_number: context.issue.number });
365
+ }
366
+ ```
367
+
368
+ ## What this tool is not
369
+
370
+ - It is not a replacement for engineering judgment.
371
+ - It does not understand your business domain.
372
+ - It does not prove that your tests are good.
373
+
374
+ Coverage can execute a line without asserting the right behavior. A function can be fully covered and still poorly tested.
375
+
376
+ So the CRAP score should not be treated as absolute truth. It is a signal — a useful one.
377
+
378
+ The best use of the tool is to ask better questions:
379
+
380
+ - Why is this function so complex?
381
+ - Is this complexity essential or accidental?
382
+ - Do the tests cover the important branches?
383
+ - Can we split this into smaller pieces?
384
+ - Should this logic be modeled more explicitly?
385
+
386
+ Good tools do not replace thinking. They make thinking easier to focus.
387
+
388
+ ## Prior art and references
389
+
390
+ - [Savoia, A. & Evans, B. (2007). *The CRAP Metric.*](https://www.artima.com/weblogs/viewpost.jsp?thread=210575)
391
+ - [Crap4j](http://www.crap4j.org/) — the original Java implementation.
392
+ - [cargo-crap](https://github.com/minikin/cargo-crap) — Rust implementation of the CRAP metric for Cargo projects by [minikin](https://minikin.me/blog/cargo-crap).
393
+
394
+ ## Contributing
395
+
396
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the commit convention, development setup, and release process.
397
+
398
+ ## License
399
+
400
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=react-crap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-crap.d.ts","sourceRoot":"","sources":["../../bin/react-crap.ts"],"names":[],"mappings":""}
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const index_js_1 = require("../src/index.js");
6
+ function collect(value, previous) {
7
+ return previous.concat([value]);
8
+ }
9
+ const program = new commander_1.Command();
10
+ program
11
+ .name("react-crap")
12
+ .description("Change Risk Anti-Patterns (CRAP) metric for React TypeScript projects")
13
+ .version("0.1.0");
14
+ program
15
+ .option("--lcov <file>", "LCOV coverage report file")
16
+ .option("--path <dir>", "Root directory to analyze", "src")
17
+ .option("--threshold <n>", "CRAP score threshold", "30")
18
+ .option("--min <score>", "Hide entries below this CRAP score")
19
+ .option("--max <score>", "Hide entries above this CRAP score")
20
+ .option("--top <n>", "Show only the N worst offenders")
21
+ .option("--only-failures", "Only show functions exceeding the CRAP threshold")
22
+ .option("--missing <policy>", "Missing coverage policy: pessimistic, optimistic, skip", "pessimistic")
23
+ .option("--exclude <glob>", "Skip files matching pattern (repeatable)", collect, [])
24
+ .option("--allow <glob>", "Suppress matching functions (repeatable)", collect, [])
25
+ .option("--format <fmt>", "Output format: human, json, github, markdown, pr-comment, sarif", "human")
26
+ .option("--summary", "Print only aggregate stats")
27
+ .option("--fail-above", "Exit 1 if any function exceeds threshold")
28
+ .option("--baseline <file>", "JSON baseline for delta comparison")
29
+ .option("--fail-regression", "Exit 1 if any score increased since baseline")
30
+ .option("--epsilon <value>", "Regression detector tolerance", "0.01")
31
+ .option("--jobs <n>", "Cap parallel analysis threads")
32
+ .option("--workspace", "Analyze all workspace packages")
33
+ .option("--verbose", "Print detailed progress information")
34
+ .option("--watch", "Re-run automatically when files change")
35
+ .option("--no-color", "Disable colored output")
36
+ .option("--output <file>", "Write output to file instead of stdout");
37
+ program.parse();
38
+ const options = program.opts();
39
+ const noColor = options.noColor || !!process.env.NO_COLOR;
40
+ (0, index_js_1.run)({
41
+ lcov: options.lcov,
42
+ path: options.path,
43
+ threshold: parseFloat(options.threshold),
44
+ min: options.min ? parseFloat(options.min) : undefined,
45
+ max: options.max ? parseFloat(options.max) : undefined,
46
+ top: options.top ? parseInt(options.top, 10) : undefined,
47
+ onlyFailures: options.onlyFailures ?? false,
48
+ missing: options.missing,
49
+ exclude: options.exclude,
50
+ allow: options.allow,
51
+ format: options.format,
52
+ summary: options.summary ?? false,
53
+ failAbove: options.failAbove ?? false,
54
+ baseline: options.baseline,
55
+ failRegression: options.failRegression ?? false,
56
+ epsilon: parseFloat(options.epsilon),
57
+ jobs: options.jobs ? parseInt(options.jobs, 10) : undefined,
58
+ workspace: options.workspace ?? false,
59
+ verbose: options.verbose ?? false,
60
+ watch: options.watch ?? false,
61
+ output: options.output,
62
+ noColor,
63
+ }).catch((err) => {
64
+ console.error(err instanceof Error ? err.message : String(err));
65
+ process.exit(1);
66
+ });
67
+ //# sourceMappingURL=react-crap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-crap.js","sourceRoot":"","sources":["../../bin/react-crap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { ComplexityEntry } from "./complexity.js";
2
+ export interface CacheEntry {
3
+ hash: string;
4
+ functions: ComplexityEntry[];
5
+ }
6
+ export interface Cache {
7
+ version: string;
8
+ files: Record<string, CacheEntry>;
9
+ }
10
+ export declare function loadCache(projectPath: string): Cache;
11
+ export declare function saveCache(projectPath: string, cache: Cache): void;
12
+ export declare function hashFile(content: string): string;
13
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAClC;AAKD,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAYpD;AAED,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAGjE;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhD"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadCache = loadCache;
4
+ exports.saveCache = saveCache;
5
+ exports.hashFile = hashFile;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const CACHE_FILE = ".react-crap-cache.json";
10
+ const CACHE_VERSION = "1";
11
+ function loadCache(projectPath) {
12
+ const path = (0, node_path_1.resolve)(projectPath, CACHE_FILE);
13
+ if ((0, node_fs_1.existsSync)(path)) {
14
+ try {
15
+ const raw = (0, node_fs_1.readFileSync)(path, "utf-8");
16
+ const parsed = JSON.parse(raw);
17
+ if (parsed.version === CACHE_VERSION)
18
+ return parsed;
19
+ }
20
+ catch {
21
+ // invalid cache, ignore
22
+ }
23
+ }
24
+ return { version: CACHE_VERSION, files: {} };
25
+ }
26
+ function saveCache(projectPath, cache) {
27
+ const path = (0, node_path_1.resolve)(projectPath, CACHE_FILE);
28
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(cache, null, 2), "utf-8");
29
+ }
30
+ function hashFile(content) {
31
+ return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex").slice(0, 16);
32
+ }
33
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":";;AAkBA,8BAYC;AAED,8BAGC;AAED,4BAEC;AAvCD,6CAAyC;AACzC,qCAAkE;AAClE,yCAAoC;AAapC,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAC5C,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B,SAAgB,SAAS,CAAC,WAAmB;IAC5C,MAAM,IAAI,GAAG,IAAA,mBAAO,EAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAC9C,IAAI,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAU,CAAC;YACxC,IAAI,MAAM,CAAC,OAAO,KAAK,aAAa;gBAAE,OAAO,MAAM,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACR,wBAAwB;QACzB,CAAC;IACF,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAC9C,CAAC;AAED,SAAgB,SAAS,CAAC,WAAmB,EAAE,KAAY;IAC1D,MAAM,IAAI,GAAG,IAAA,mBAAO,EAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAC9C,IAAA,uBAAa,EAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC;AAED,SAAgB,QAAQ,CAAC,OAAe;IACvC,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxE,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface ComplexityEntry {
2
+ file: string;
3
+ function: string;
4
+ line: number;
5
+ endLine: number;
6
+ cyclomatic: number;
7
+ bodyHash: string;
8
+ threshold?: number;
9
+ }
10
+ export declare function analyzeComplexity(files: string[], tsPath?: string, jobs?: number, onProgress?: (current: number, total: number, file: string) => void): Promise<ComplexityEntry[]>;
11
+ //# sourceMappingURL=complexity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../../src/complexity.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,iBAAiB,CACtC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GACjE,OAAO,CAAC,eAAe,EAAE,CAAC,CAqB5B"}