refactor(dead-export-finder): pure functional pipes, zero mutation#46
Conversation
…onal pipes Replace imperative loops, mutable arrays/maps/sets, and push-based accumulation with Effect pipe syntax, Array module utilities, HashSet for lookups, and small named composed functions throughout. - export-parser: flatMap over AST nodes with per-type extractors - import-parser: recursive collectSymbols returns ReadonlyArray - export-graph: 5 composed stages replace 150-line analyzeSync - reporter: Array.groupBy + Order.string replace mutable Map/sort - file-scanner: pure pattern building, single ignore construction - workspace-detector: chained detection strategies via Effect.catchTag - index.ts: scanWorkspace/parseAllFiles/analyzeAndReport pipeline Zero test changes. All 42 tests pass. Build clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
TL;DR — Large but mechanical refactor of dead-export-finder replacing all imperative mutation (push, mutable Map/Set, let reassignment) with pure functional pipes via Effect's Array, HashSet, Option, and Order modules. Service topology and public API are unchanged.
Key changes
- Design doc for the refactor — Adds
docs/superpowers/specs/2026-05-12-dead-export-finder-pure-refactor-design.mddocumenting the plan, constraints, and file-by-file strategy. export-parser.ts— Inline switch/case withsymbols.push()replaced byArr.flatMap(extractExportsFromNode)with per-type extractor functions (extractNamedDeclaration,extractDefaultDeclaration,extractAllDeclaration,extractCjsExports).lineFromOffsetsimplified tosource.slice(0, offset).split('\n').length.import-parser.ts— MutablewalkNode(node, filePath, symbols)replaced by pure recursivecollectSymbols(node, filePath): ReadonlyArray<ImportedSymbol>. Static imports extracted separately viaextractStaticImports. Require calls extracted viaextractRequireCall. Dynamic imports (ImportExpression) still handled via early return incollectSymbols.export-graph.ts— 150-line imperativeanalyzeSyncwith 6 mutable collections decomposed into 5 composed pure pipeline stages:resolveEntryPoints,buildFileToPackageMap,buildConsumedSets,findDeadExports,countTotalExports.HashSetreplacesSetfor structural equality. Also addsBUILD_DIR_MAPPINGSfor build-output→source-path resolution (new behavior).reporter.ts— Mutablelines[]push andMapgrouping replaced withArr.groupBy,Order.mapInput(Order.number, ...)for line-number sorting, andArr.join('\n'). Service interface was alreadyReadonlyMap— unchanged.file-scanner.ts— Sequentialig.add()mutations inEffect.genreplaced with composed stages:loadGitignorePatterns→buildIgnorePatterns→discoverFiles→filterWithIgnore. Pure helpers extracted from the imperative scan body.workspace-detector.ts— Deeply nested if/else in a singleEffect.genreplaced with chained detection strategies viaEffect.catchTag: pnpm → npm → nx → turbo → single. Each strategy is a standalone function returningEffect<WorkspaceResult, WorkspaceNotFoundError>.index.ts— 200-line monolithic command handler decomposed into three named pipeline stages:scanWorkspace→parseAllFiles→analyzeAndReport. Warning concatenation usesArr.appendAll.
BUILD_DIR_MAPPINGS — new behavior beyond stated scope
The resolveEntryPointToSource function in export-graph.ts gained a third fallback that maps build-output directories (/dist/, /build/, /out/) to their source equivalents (/src/) via the BUILD_DIR_MAPPINGS array. This is a net improvement — it reduces false-positive dead exports for packages that export from build output directories — but goes beyond the stated "zero mutation" scope. All 42 tests pass, confirming no regressions.
readPackageInfo — additional outer catchAll
The readPackageInfo function in workspace-detector.ts now has an outer Effect.catchAll(() => Effect.succeed(null)) wrapping the entire pipe, which wasn't present in the original. This catches errors from fs.exists itself (in addition to fs.readFileString). This is more defensive and consistent with the tool's error-tolerant design, but it's a behavioral change — the old code would let fs.exists failures propagate.
Summary | 8 files | 1 commit | base: main ← refactor/dead-export-finder-pure-functional

Summary
dead-export-finderinternals — nopush, no mutableMap/Set, noletreassignmentArray,HashSet,Option, andOrdermodulesReadonlyMap/ReadonlyArray(existingMapcallers still assignable)File-by-file
export-parser.tssymbols.push()in switch/caseArr.flatMap(extractExportsFromNode)with per-type extractorsimport-parser.tswalkNodemutates passed-in arraycollectSymbolsreturnsReadonlyArrayrecursivelyexport-graph.tsanalyzeSyncwith 6 mutable collectionsresolveEntryPoints,buildFileToPackageMap,buildConsumedSets,findDeadExports,countTotalExportsreporter.tslines[]+MapgroupingArr.groupBy+Order.string+Arr.joinfile-scanner.tsig.add()mutationsloadGitignorePatterns→buildIgnorePatterns→ single constructionworkspace-detector.tsEffect.catchTagindex.tsscanWorkspace→parseAllFiles→analyzeAndReportpipelineTest plan
tsc -p tsconfig.lib.json)🤖 Generated with Claude Code