Skip to content

refactor(dead-export-finder): pure functional pipes, zero mutation#46

Merged
ryanbas21 merged 1 commit into
mainfrom
refactor/dead-export-finder-pure-functional
May 13, 2026
Merged

refactor(dead-export-finder): pure functional pipes, zero mutation#46
ryanbas21 merged 1 commit into
mainfrom
refactor/dead-export-finder-pure-functional

Conversation

@ryanbas21
Copy link
Copy Markdown
Owner

Summary

  • Eliminate all mutation from dead-export-finder internals — no push, no mutable Map/Set, no let reassignment
  • Pipe syntax throughout with small, well-named composed functions using Effect's Array, HashSet, Option, and Order modules
  • Service interfaces tightened to ReadonlyMap/ReadonlyArray (existing Map callers still assignable)
  • Service topology unchanged — same 6 services, same boundaries, same public API

File-by-file

File Before After
export-parser.ts symbols.push() in switch/case Arr.flatMap(extractExportsFromNode) with per-type extractors
import-parser.ts walkNode mutates passed-in array collectSymbols returns ReadonlyArray recursively
export-graph.ts 150-line imperative analyzeSync with 6 mutable collections 5 composed stages: resolveEntryPoints, buildFileToPackageMap, buildConsumedSets, findDeadExports, countTotalExports
reporter.ts Mutable lines[] + Map grouping Arr.groupBy + Order.string + Arr.join
file-scanner.ts Sequential ig.add() mutations loadGitignorePatternsbuildIgnorePatterns → single construction
workspace-detector.ts Deeply nested if/else chain Chained detection strategies via Effect.catchTag
index.ts 200-line monolithic command handler scanWorkspaceparseAllFilesanalyzeAndReport pipeline

Test plan

  • All 42 existing tests pass unchanged
  • TypeScript build clean (tsc -p tsconfig.lib.json)
  • ESLint + Prettier pass via pre-commit hooks

🤖 Generated with Claude Code

…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>
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md documenting the plan, constraints, and file-by-file strategy.
  • export-parser.ts — Inline switch/case with symbols.push() replaced by Arr.flatMap(extractExportsFromNode) with per-type extractor functions (extractNamedDeclaration, extractDefaultDeclaration, extractAllDeclaration, extractCjsExports). lineFromOffset simplified to source.slice(0, offset).split('\n').length.
  • import-parser.ts — Mutable walkNode(node, filePath, symbols) replaced by pure recursive collectSymbols(node, filePath): ReadonlyArray<ImportedSymbol>. Static imports extracted separately via extractStaticImports. Require calls extracted via extractRequireCall. Dynamic imports (ImportExpression) still handled via early return in collectSymbols.
  • export-graph.ts — 150-line imperative analyzeSync with 6 mutable collections decomposed into 5 composed pure pipeline stages: resolveEntryPoints, buildFileToPackageMap, buildConsumedSets, findDeadExports, countTotalExports. HashSet replaces Set for structural equality. Also adds BUILD_DIR_MAPPINGS for build-output→source-path resolution (new behavior).
  • reporter.ts — Mutable lines[] push and Map grouping replaced with Arr.groupBy, Order.mapInput(Order.number, ...) for line-number sorting, and Arr.join('\n'). Service interface was already ReadonlyMap — unchanged.
  • file-scanner.ts — Sequential ig.add() mutations in Effect.gen replaced with composed stages: loadGitignorePatternsbuildIgnorePatternsdiscoverFilesfilterWithIgnore. Pure helpers extracted from the imperative scan body.
  • workspace-detector.ts — Deeply nested if/else in a single Effect.gen replaced with chained detection strategies via Effect.catchTag: pnpm → npm → nx → turbo → single. Each strategy is a standalone function returning Effect<WorkspaceResult, WorkspaceNotFoundError>.
  • index.ts — 200-line monolithic command handler decomposed into three named pipeline stages: scanWorkspaceparseAllFilesanalyzeAndReport. Warning concatenation uses Arr.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.

export-graph.ts


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.

workspace-detector.ts

Summary | 8 files | 1 commit | base: mainrefactor/dead-export-finder-pure-functional

Pullfrog  | View workflow run𝕏

@ryanbas21 ryanbas21 merged commit 40c8f67 into main May 13, 2026
1 check passed
@ryanbas21 ryanbas21 deleted the refactor/dead-export-finder-pure-functional branch May 13, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant