Skip to content

refactor: split cfg-visitor.ts by control-flow construct#619

Merged
carlos-alm merged 3 commits intomainfrom
refactor/titan-split-cfg-visitor
Mar 26, 2026
Merged

refactor: split cfg-visitor.ts by control-flow construct#619
carlos-alm merged 3 commits intomainfrom
refactor/titan-split-cfg-visitor

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • Splits cfg-visitor.ts (874 SLOC) into focused modules by control-flow construct
  • New modules: cfg-conditionals.ts, cfg-loops.ts, cfg-try-catch.ts, cfg-shared.ts
  • Internal refactor within ast-analysis -- only engine.ts imports cfg-visitor

Titan Audit Context

  • Phase: decomposition (Forge Phase 3)
  • Domain: ast-analysis
  • Commits: 1
  • Depends on: none

Changes

  • src/ast-analysis/visitors/cfg-visitor.ts -- reduced to orchestrator, delegates to sub-modules
  • src/ast-analysis/visitors/cfg-conditionals.ts -- if/else/switch processing
  • src/ast-analysis/visitors/cfg-loops.ts -- for/while/do-while processing
  • src/ast-analysis/visitors/cfg-try-catch.ts -- try/catch/finally processing
  • src/ast-analysis/visitors/cfg-shared.ts -- shared CFG builder utilities

Metrics Impact

  • cfg-visitor.ts SLOC: 874 -> ~300 orchestrator + 3 focused modules
  • Blast radius: 20 (gate-validated)

Test plan

  • CI passes (lint + build + tests)
  • codegraph check --cycles --boundaries passes
  • No new functions above complexity thresholds

Impact: 16 functions changed, 0 affected
@claude
Copy link

claude bot commented Mar 26, 2026

Claude finished @carlos-alm's task —— View job


PR Review: refactor: split cfg-visitor.ts by control-flow construct

Todo List:

  • Phase 1: Root Cause Analysis - Understand the problem being solved
  • Phase 2: Configuration Change Detection - Check for high-risk config changes
  • Phase 3: Test Integrity - Look for test-related red flags
  • Phase 4: Code Quality Analysis - Review the refactored code
  • Phase 5: Backlog Compliance - Grade against BACKLOG.md criteria
  • Final Assessment and Recommendation

Phase 1: Root Cause Analysis

The PR refactors a large 874 SLOC cfg-visitor.ts file by splitting it into focused modules by control-flow construct. This addresses legitimate maintainability concerns by breaking down a monolithic visitor into:

  • cfg-conditionals.ts - if/else/switch processing (221 lines)
  • cfg-loops.ts - for/while/do-while processing (138 lines)
  • cfg-try-catch.ts - try/catch/finally processing (143 lines)
  • cfg-shared.ts - shared CFG utilities and types (189 lines)
  • Main cfg-visitor.ts reduced to ~253 line orchestrator

This is a legitimate internal refactor within the ast-analysis domain with no external API changes. The approach is sound - splitting by logical concern rather than arbitrary size limits.

Phase 2: Configuration Change Detection

NO HIGH-RISK CHANGES DETECTED

  • No configuration files modified (package.json, tsconfig.json, biome.json unchanged)
  • No test files modified
  • No linting rules disabled or relaxed
  • No build configuration changes
  • Only code files in src/ast-analysis/visitors/ were modified

Phase 3: Test Integrity

NO TEST INTEGRITY ISSUES

  • No test files were modified
  • Existing import in tests/unit/cfg.test.ts remains valid: import { createCfgVisitor } from '../../src/ast-analysis/visitors/cfg-visitor.js'
  • Tests will continue to work as the public API (createCfgVisitor) is unchanged
  • Only internal implementation details were refactored

Phase 4: Code Quality Analysis

Architecture & Design

  • Clean separation of concerns by control-flow construct
  • No circular dependencies detected (only shared types imported)
  • Proper dependency flow: cfg-visitor.ts orchestrates, sub-modules handle specifics
  • Clear module boundaries with focused responsibilities

Type Safety

  • All exports properly typed with TreeSitterNode, CfgBlockInternal, etc.
  • Consistent use of ProcessStatementsFn callback type for mutual recursion
  • Proper handling of nullable parameters (CfgBlockInternal | null)

Code Patterns

  • Consistent error handling with nn() helper for null-safety
  • Proper state management through FuncState parameter threading
  • Clean recursive structure preserved (processStatement → process[Construct] → processStatements)

Minor Line Count Increase (944 vs 874) ⚠️
The +70 line increase is due to:

  • Import/export statements across 4 new files (~25 lines)
  • Function signatures that were previously internal now need full typing (~20 lines)
  • Added documentation/spacing for module boundaries (~25 lines)

This is acceptable overhead for a well-structured refactor that significantly improves maintainability.

Phase 5: Backlog Compliance

Root Cause Analysis

Problem Being Solved: Large 874 SLOC monolithic cfg-visitor.ts file was difficult to maintain and navigate. Splitting by control-flow construct improves code organization and developer productivity.

Why This Approach: Logical separation by control-flow construct (conditionals, loops, try-catch) is more maintainable than arbitrary size-based splitting. Preserves all functionality while improving structure.

Risk Assessment: Low risk - purely internal refactor with no API changes, preserved test compatibility, and clean module boundaries.

Backlog Compliance

  • Zero-dep: ✓ — No new runtime dependencies added, builds entirely on existing TypeScript/tree-sitter infrastructure
  • Foundation-aligned: ✅ — Fully aligned, improves maintainability without violating any core principles (P1-P8)
  • Problem-fit: 2 — Improves maintainability but doesn't directly address core AI agent token waste/hallucination problem
  • Breaking: No — Purely internal refactor, public API (createCfgVisitor) unchanged
  • Tier: 1 (zero-dep + foundation-aligned)

Critical Concerns

None identified. This is a well-executed internal refactor that improves code maintainability without introducing risks.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: High-quality refactoring that significantly improves maintainability with no breaking changes, proper module separation, and preserved test compatibility. Minor deduction only for the problem-fit score (doesn't directly address core AI agent issues) but this is legitimate technical debt cleanup.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR splits the monolithic cfg-visitor.ts (874 SLOC) into four focused sub-modules (cfg-shared.ts, cfg-conditionals.ts, cfg-loops.ts, cfg-try-catch.ts) while reducing the orchestrator to ~250 SLOC. The decomposition is clean and the concerns map naturally: shared types/utilities in cfg-shared.ts, control-flow handlers in their respective modules, and the recursive processStatements/processStatement loop remaining in the orchestrator.

Key points from the review:

  • All CFG logic is faithfully preserved — no behavioral changes detected.
  • ProcessStatementsFn correctly lives in cfg-shared.ts (prior thread concern resolved).
  • All internal-only helpers (processAlternative, processElifSiblings, extractCaseBody, findTryHandlers, processCatchHandler, processFinallyOnly) are correctly unexported (prior thread concern resolved).
  • The mutual-recursion pattern (sub-modules receive processStatements as a callback) cleanly avoids circular imports.
  • processInfiniteLoop intentionally omits a loop_exit edge from the header — unlike for/while, infinite loops (loop in Rust, etc.) have no conditional exit path; the exit block is only reachable via break, which is correct behaviour.
  • The re-export export type { CfgBlockInternal } from './cfg-shared.js' in cfg-visitor.ts preserves the existing public API for downstream consumers.

Confidence Score: 5/5

Safe to merge — pure structural refactoring with no behavioral changes and both prior review concerns fully resolved.

All logic is faithfully extracted from the original monolith. The two concerns raised in the prior review thread (ProcessStatementsFn placement and unnecessary exports) are confirmed fixed in HEAD. No new bugs were found during this review. The module boundaries are clean, imports are minimal and correct, and the public API surface of cfg-visitor.ts is unchanged.

No files require special attention.

Important Files Changed

Filename Overview
src/ast-analysis/visitors/cfg-shared.ts New module consolidating all shared types (FuncState, CfgBlockInternal, LoopCtx, etc.), helper predicates, and utilities (makeFuncState, getBodyStatements, registerLabelCtx); ProcessStatementsFn correctly lives here after prior-review fix.
src/ast-analysis/visitors/cfg-conditionals.ts New module for if/elif/else and switch processing; processIf and processSwitch are exported, while processAlternative, processElifSiblings, and extractCaseBody are correctly kept private (exports removed per prior review).
src/ast-analysis/visitors/cfg-loops.ts New module for for/while/do-while/infinite-loop processing; all four processors are exported and import ProcessStatementsFn cleanly from cfg-shared.js (decoupled from prior cross-dependency).
src/ast-analysis/visitors/cfg-try-catch.ts New module for try/catch/finally CFG construction; processTryCatch is the sole export, findTryHandlers/processCatchHandler/processFinallyOnly are correctly private after prior review fix.
src/ast-analysis/visitors/cfg-visitor.ts Reduced from 874 SLOC to ~250 SLOC; now acts as an orchestrator delegating to the four sub-modules; still owns processStatements, processStatement, processLabeled, processBreak, processContinue, and createCfgVisitor.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    engine["engine.ts\n(external caller)"]
    visitor["cfg-visitor.ts\norchestrator\nprocessStatements / processStatement\ncreatesCfgVisitor"]
    shared["cfg-shared.ts\nshared types & utilities\nFuncState · CfgBlockInternal\nProcessStatementsFn · makeFuncState\ngetBodyStatements · registerLabelCtx\npredicates: isIfNode / isForNode / …"]
    cond["cfg-conditionals.ts\nprocessIf\nprocessSwitch"]
    loops["cfg-loops.ts\nprocessForLoop\nprocessWhileLoop\nprocessDoWhileLoop\nprocessInfiniteLoop"]
    trycatch["cfg-try-catch.ts\nprocessTryCatch"]

    engine -->|createCfgVisitor| visitor
    visitor -->|imports| shared
    visitor -->|imports| cond
    visitor -->|imports| loops
    visitor -->|imports| trycatch
    cond -->|imports types+utils| shared
    loops -->|imports types+utils| shared
    trycatch -->|imports types+utils| shared
    visitor -->|passes processStatements callback| cond
    visitor -->|passes processStatements callback| loops
    visitor -->|passes processStatements callback| trycatch
Loading

Reviews (2): Last reviewed commit: "refactor: move ProcessStatementsFn to cf..." | Re-trigger Greptile

Comment on lines +6 to +11
export type ProcessStatementsFn = (
stmts: TreeSitterNode[],
currentBlock: CfgBlockInternal,
S: FuncState,
cfgRules: AnyRules,
) => CfgBlockInternal | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 ProcessStatementsFn belongs in cfg-shared.ts, not cfg-loops.ts

This shared callback type is imported by both cfg-conditionals.ts (line 8) and cfg-try-catch.ts (line 2) purely to access this type — not for anything loop-specific. This creates an unintended module coupling: the conditionals and try-catch modules depend on the loops module as a side-effect of a shared type definition.

Moving ProcessStatementsFn to cfg-shared.ts (alongside the other shared types like FuncState, CfgBlockInternal, etc.) would eliminate this coupling and better reflect the PR's stated intent of focused modules with minimal cross-cutting dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 00cb63d. Moved ProcessStatementsFn from cfg-loops.ts to cfg-shared.ts alongside the other shared types. All three sub-modules now import it from cfg-shared.js, eliminating the unintended coupling to the loops module.

Comment on lines +60 to +72
export function findTryHandlers(
tryStmt: TreeSitterNode,
cfgRules: AnyRules,
): { catchHandler: TreeSitterNode | null; finallyHandler: TreeSitterNode | null } {
let catchHandler: TreeSitterNode | null = null;
let finallyHandler: TreeSitterNode | null = null;
for (let i = 0; i < tryStmt.namedChildCount; i++) {
const child = nn(tryStmt.namedChild(i));
if (cfgRules.catchNode && child.type === cfgRules.catchNode) catchHandler = child;
if (cfgRules.finallyNode && child.type === cfgRules.finallyNode) finallyHandler = child;
}
return { catchHandler, finallyHandler };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Internal helper functions unnecessarily exported

findTryHandlers, processCatchHandler, and processFinallyOnly are only called within this file (all three are called exclusively from processTryCatch). Exporting them widens the public API surface beyond what this module actually requires.

The same applies to processAlternative, processElifSiblings, and extractCaseBody in cfg-conditionals.ts — each is only called within that file, yet all three are exported.

In the original monolithic cfg-visitor.ts, all of these were private functions. They should remain unexported unless needed by another module or for unit testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 00cb63d. Removed the export keyword from all six internal helpers:

  • cfg-try-catch.ts: findTryHandlers, processCatchHandler, processFinallyOnly
  • cfg-conditionals.ts: processAlternative, processElifSiblings, extractCaseBody

Confirmed none of these are imported by any other module — they are only called within their respective files, matching their original private scope in the monolith.

…helpers

Move the shared ProcessStatementsFn type from cfg-loops.ts to cfg-shared.ts
to eliminate unnecessary cross-module coupling between conditionals/try-catch
and the loops module.

Un-export internal helpers that are only called within their own files:
- cfg-try-catch.ts: findTryHandlers, processCatchHandler, processFinallyOnly
- cfg-conditionals.ts: processAlternative, processElifSiblings, extractCaseBody

Impact: 6 functions changed, 7 affected
@carlos-alm
Copy link
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit b4d062b into main Mar 26, 2026
12 checks passed
@carlos-alm carlos-alm deleted the refactor/titan-split-cfg-visitor branch March 26, 2026 06:46
@github-actions github-actions bot locked and limited conversation to collaborators Mar 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant