perf(core): cache dead-code results so unchanged rescans skip the analysis#1053
Conversation
…lysis Dead-code re-analyzed the whole import graph every run (~9s of a warm sentry rescan). A stat-fingerprint over the analyzed file set, manifests, tsconfigs, and the analyzer version now keys a single-entry result cache; a hit replays the diagnostics without spawning the worker, and only complete successful passes are stored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
commit: |
| const ANALYZED_FILE_EXTENSIONS = new Set([ | ||
| ".ts", | ||
| ".tsx", | ||
| ".js", | ||
| ".jsx", | ||
| ".mts", | ||
| ".mjs", | ||
| ".cts", | ||
| ".cjs", | ||
| ".mdx", | ||
| ".astro", | ||
| ".graphql", | ||
| ".gql", | ||
| ".css", | ||
| ".scss", | ||
| ".vue", | ||
| ".svelte", | ||
| ]); | ||
|
|
||
| // Non-source files deslop's entry/workspace/dependency analysis reads: | ||
| // manifests (workspace layout, dependency declarations, knip/expo config), | ||
| // lockfiles (the proxy for installed `node_modules` metadata — deslop reads | ||
| // installed packages' bin/peer fields, which only change through an install | ||
| // that rewrites the lockfile), and `.gitignore` files at every level (deslop | ||
| // consults git's ignore state for its walk). | ||
| const ANALYZED_MANIFEST_NAMES = new Set([ | ||
| "package.json", | ||
| "knip.json", | ||
| "app.json", | ||
| "pnpm-workspace.yaml", | ||
| "lerna.json", | ||
| "pnpm-lock.yaml", | ||
| "package-lock.json", | ||
| "yarn.lock", | ||
| "bun.lock", | ||
| "bun.lockb", | ||
| "deno.lock", | ||
| ".gitignore", | ||
| ]); |
There was a problem hiding this comment.
🚩 Cache fingerprint file set must stay in sync with deslop's actual file walk
The ANALYZED_FILE_EXTENSIONS set at packages/core/src/dead-code/dead-code-result-cache.ts:50-67 and ANALYZED_MANIFEST_NAMES at lines 75-88 define which files contribute to the cache key. If deslop adds support for new file extensions (e.g., .elm, .res) or starts reading additional manifest files, the cache won't invalidate on changes to those files — silently replaying stale diagnostics. There's no automated mechanism to keep these sets synchronized with deslop's DEFAULT_EXTENSIONS or its workspace/dependency resolution inputs. This is an accepted maintenance burden (the comment on line 48-49 documents the intent), but a deslop version bump that adds extensions would need a corresponding update here or a DEAD_CODE_CACHE_SCHEMA_VERSION bump to force invalidation.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Addressed in #1056: the extension and manifest lists are now imported from a new dependency-free deslop-js/analyzed-inputs subpath whose constants deslop's own readers consume — the copies can no longer drift (and indeed were already missing ng-package.json and pnpm-workspace.yml).
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 e741684. Configure here.
| files: collectAnalyzedFileFingerprints(input.rootDirectory), | ||
| }), | ||
| ) | ||
| .digest("hex"); |
There was a problem hiding this comment.
Cache key omits react-doctor version
Medium Severity
The dead-code result fingerprint includes deslopVersion and DEAD_CODE_CACHE_SCHEMA_VERSION but not the installed @react-doctor/core version. Cached entries store fully materialized diagnostics after checkDeadCode post-processing (for example REACT_DOCTOR_TOOLCHAIN_PACKAGES filtering and message text). After upgrading react-doctor with an unchanged source tree, the key can still match and replay stale dead-code findings while the rest of the scan uses new logic.
Reviewed by Cursor Bugbot for commit e741684. Configure here.
There was a problem hiding this comment.
Addressed in #1056: CORE_PACKAGE_VERSION (build-time-injected, mirroring the CLI's VERSION mechanism) now joins the cache key, so a react-doctor upgrade invalidates post-processed cached diagnostics even on an unchanged tree.
…ist drift (#1056) Addresses the two review findings on #1053: the fingerprint's extension and manifest lists are now imported from a new dependency-free deslop-js/analyzed-inputs subpath (single source of truth with the analyzer's own readers — the hand-copies were already missing ng-package.json and pnpm-workspace.yml), and the core package version joins the key so an upgrade invalidates post-processed cached diagnostics. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>


Why
Dead-code analysis re-derives the whole import graph from scratch on every scan — measured at ~9s of a ~19.8s warm rescan on getsentry/sentry (8,899 TS/TSX files), the single largest uncached component. The whole-repo scan cache covers the nothing-changed case, but any miss (dirty tree today, any config/flag difference) pays the full pass again.
Before:
After:
What changed
dead-code-result-cache.ts): dead-code reachability is a whole-project property, so one entry keyed by a fingerprint over everything the analysis reads — sorted (path, mtime, size) stats of all deslop-parsed extensions, the manifests it consults (package.json/knip/expo/workspace files, all lockfiles as the node_modules proxy,.gitignoreat every level), tsconfig/jsconfig anywhere (extends chains), resolved entry/ignore patterns, and the deslop-js version + schema constant. Additions/deletions invalidate via the hashed path list; the mtime+size blind spot is the documentedparse-source-fileprecedent.shouldStoreScanPayloadphilosophy). Reads are schema-validated and fail open.checkDeadCodeAPI level (direct callers/tests keep fresh semantics); theDeadCodeservice enables it via a newDeadCodeResultCacheEnabledReference, disabled byREACT_DOCTOR_NO_CACHEor granularREACT_DOCTOR_NO_DEAD_CODE_CACHE.lintCacheHitRatioprecedent exactly:deadCode.cacheHitthreaded outsideCachedScanPayload(null on whole-repo hits and when no cache ran) — no scan-cache schema bump.DeadCodeOverlapsemantics untouched.Test plan
packages/core: 1158 passed (baseline 1145; +13 new — key stability/sensitivity for touch/add/delete/manifest/tsconfig/config, hit-skips-worker spy, store-barred-on-failure, NO_CACHE bypass);packages/react-doctor: 2124 / 26 unchanged. Root typecheck + lint + smoke:json-report clean.🤖 Generated with Claude Code
Note
Low Risk
Performance-only caching with fail-open reads, schema validation, stat-based invalidation, and no storage on failed passes; wrong replays are unlikely and env vars allow disabling.
Overview
Warm rescans can skip dead-code analysis when nothing the pass reads has changed. A new single-entry on-disk cache stores the last complete dead-code diagnostic set, keyed by a fingerprint of the analyzed source tree (path/mtime/size stats), manifests, lockfiles, tsconfigs, knip/entry/ignore config, and
deslop-jsversion. A hit returns cached diagnostics without spawning the heavy analysis worker; a miss runs analysis and persists the result only on success (crashes/timeouts/aborts never write).Caching is on by default for normal scans via
DeadCodeResultCacheEnabled, with opt-out throughREACT_DOCTOR_NO_CACHEorREACT_DOCTOR_NO_DEAD_CODE_CACHE. DirectcheckDeadCodecallers still default cache off so tests keep fresh semantics. Telemetry addsdeadCode.cacheHit(andInspectResult.deadCodeCacheHit), aligned with the lint cache pattern; lint failure clears the dead-code cache signal so outcomes do not leak.Reviewed by Cursor Bugbot for commit e741684. Bugbot is set up for automated code reviews on this repo. Configure here.