feat(complexity): add react-doctor complexity command (CFG-based cyclomatic + cognitive metrics, --diff, --json) - #1089
feat(complexity): add react-doctor complexity command (CFG-based cyclomatic + cognitive metrics, --diff, --json)#1089aidenybai wants to merge 8 commits into
react-doctor complexity command (CFG-based cyclomatic + cognitive metrics, --diff, --json)#1089Conversation
…clomatic + cognitive metrics, --diff, --json) Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Original prompt from Aiden Bai
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
commit: |
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Manual end-to-end test results —
|
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Bugbot findings addressed (commit 815b5d2)Thanks Bugbot — all 5 were valid and are fixed, each with a regression test:
Tests: added regressions in |
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aad0956. Configure here.
| resolvedDirectory, | ||
| headAnalysis.functions, | ||
| baseAnalysis.functions, | ||
| sortMetric, |
There was a problem hiding this comment.
Deleted files omit removed functions
Medium Severity
--diff only enumerates current-tree source paths and materializes those paths at the base ref, so functions in files removed on HEAD never appear as removed deltas and removedCount can stay zero.
Reviewed by Cursor Bugbot for commit aad0956. Configure here.
Test results — change complexity
|
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Test results — output redesign + 0–1 scoreBuilt the CLI locally and ran
caveatsThe red case is a synthetic fixture just to hit the ≥0.75 band (not part of the PR). Colors captured by rendering real ANSI output to HTML (piping strips ANSI); escape codes 32/33/31 also verified directly.Devin session: https://app.devin.ai/sessions/a2505c42baa240ef8c83852305ec7ba4 |


Summary
Adds a new standalone
react-doctor complexitysubcommand that measures function complexity from an actual control-flow graph, ranked worst-first, with a base-ref diff mode and JSON output. It is intentionally a separate command — it does not touch the default scan, the 0–100 score, or theJsonReportschema, so no existing contract changes.The metrics are grounded in the standard literature:
E − N + 2P(edges, nodes, weakly-connected components) over the blocks reachable from the function entry — not by counting keywords.decisionPointscross-check, andmaxNestingDepth. Functions are React-aware classified (component / hook / method / arrow / function / module).Terminal output redesign + headline 0–1 score (latest)
The command output was reworked to reuse React Doctor's own visual language instead of ad-hoc formatting, and now leads with a single 0–1 complexity score (higher = more complex/riskier; red when high, green when low).
colorizeByScore(fed the inverted health value100·(1 − score)so simple = green, complex = red), rendered with a new focusedbuild-meter-barutil (█/░), abuildSectionDivider, and the branded 2-space layout.0.6·min(cyc/50,1) + 0.4·min(cog/50,1), aggregated to a codebase score0.5·mean + 0.5·maxover real functions (the<module>aggregate is excluded from the score and from the rendered tables, but stays in JSON).changeComplexityScoreis mapped into[0,1]via a saturating1 − e^(−score/K)(K=80):normalizedChangeComplexityScore.cyc/cog/nest/function/location; diff:Δcyc/Δcog/essential/bloat/function/location), short headers, location never truncated on the right (left-elided sofile:linestays visible). A⚠bloat callout fires only when a changed function wrote many lines for ~0 structural change (pure churn).JSON is additive (no
schemaVersionbump):ComplexitySummary.complexityScoreandComplexityDiffSummary.normalizedChangeComplexityScore. Terminal↔JSON parity holds. New score constants live incli/utils/constants.ts(SCREAMING_SNAKE_CASE). Telemetry gains ascoreBandattribute (simple|moderate|complex|very complex) on the existingcomplexity.command_invokedmetric.Change complexity (diff mode)
Diff mode also reports a change-complexity decomposition (Hassan-style change entropy + AST essential-change + bloat ratio + structural-risk Δ): essential change, raw line churn, bloat ratio (lines touched ÷ essential change — the "100 lines for a tiny change" smell), nesting delta, and entropy.
Reuse (no new CFG)
The engine reuses the plugin's existing per-function CFG (
analyzeControlFlow) and AST parsing (parseSourceFile);--diffreuses the same base-ref file materialization the baseline comparison uses; file enumeration reuseslistSourceFilesfrom core. New public exports fromoxlint-plugin-react-doctor:analyzeComplexity,parseSourceFile, and theFileComplexity/FunctionComplexitytypes.Shape
--diff <ref>materializes the analyzed files at<ref>(relative refs likeHEAD~1/HEAD^are rev-parsed to a SHA first), re-runs the same analysis on base + head, joins functions by qualified key (same-named class methods stay distinct), and reports per-function Δ plus added/removed. If base materialization fails it degrades to a head-only report with a note; a valid base ref with no matching files reports all functions asadded.Notes for reviewers
complexity.command_invokedwithmode: full|diffandscoreBand, following the existing anonymizedrecord-metricpath.--helpepilog,README.md, andwebsite/public/llms.txtupdated. Changeset added (patch:react-doctor,oxlint-plugin-react-doctor).Testing
pnpm typecheck,pnpm lint(only pre-existing fixture warnings), and the complexity unit/e2e tests pass.--jsonparity against fixtures; score lands in[0,1]with correct bands and no column wrap at ~80 cols.Link to Devin session: https://app.devin.ai/sessions/a2505c42baa240ef8c83852305ec7ba4
Requested by: @aidenybai