Skip to content

Replace hand-rolled Angular template scanner with oxc-angular-compiler (follow-up to #183) #235

Description

@BartWaardenburg

Background

#183 specifically requested oxc-angular-compiler as the parser for Angular template complexity, citing:

  • A proper HTML parser producing a typed HTML AST with first-class HtmlBlock nodes for @if, @for, @switch, @defer, @let.
  • An Angular expression parser producing a typed AngularExpression AST with Conditional (ternary), Binary (&&, ||, ??), SafePropertyRead (?.).
  • An R3 AST with explicit R3IfBlock, R3ForLoopBlock, R3SwitchBlock, R3DeferredBlock nodes.
  • An R3Visitor trait with visit_if_block, visit_for_loop_block, visit_switch_block, visit_switch_block_case, visit_deferred_block.

v2.51.0 shipped a hand-rolled scanner in crates/extract/src/template_complexity.rs instead. The close comment on #183 flagged this as a divergence and a candidate for a clean swap once the upstream crate is published:

I went with a hand-rolled scanner in crates/extract/src/template_complexity.rs rather than oxc-angular-compiler. Reason: that crate is workspace-internal in voidzero-dev/oxc-angular-compiler and not published to crates.io, so depending on it via git would have broken cargo publish -p fallow-extract (which fallow-core / fallow-cli / fallow-mcp transitively need). If voidzero-dev publishes it later, swapping the scanner internals is a clean follow-up.

#186 (CRAP scoring) explicitly defers the parser swap:

Replacing the regex-based sfc_template/angular.rs dead-code scanner with a proper parser (also called out in #183 as a side benefit; tracked separately).

This issue tracks both the complexity-side swap (crates/extract/src/template_complexity.rs) and the dead-code-side swap (crates/extract/src/sfc_template/angular.rs), since they share the same upstream dependency and the same blocker.

Blocker

oxc-angular-compiler is not on crates.io. Depending on it via a git Cargo source would break cargo publish -p fallow-extract (and transitively fallow-core / fallow-cli / fallow-mcp), which is why v2.51.0 went with the hand-rolled scanner.

This issue cannot proceed until voidzero-dev publishes oxc-angular-compiler to crates.io, or until they explicitly green-light a vendored copy.

Why it's still worth doing

Complexity side (crates/extract/src/template_complexity.rs)

The current hand-rolled scanner correctly handles every construct documented in #183, but it is brittle by construction:

  • Nested template literals inside expression bindings ([attr]=\"\\literal\\") trip the string-state machine.
  • @let with multi-line ternaries that span more than two lines need bespoke handling for each shape.
  • New Angular control-flow constructs (post-@defer additions, future template syntax) require manual scanner updates rather than picking up an upstream parser version bump.
  • The scanner cannot distinguish a comment-string @if from a real @if block in every position — it relies on heuristics.

A real R3Visitor removes all of these failure modes by construction.

Dead-code side (crates/extract/src/sfc_template/angular.rs)

This is the bigger win. The regex-based scanner that powers Angular member-reference extraction is the historical source of issues #73, #82, and #94 (all closed but pointing at the same fundamental fragility). Replacing it with oxc-angular-compiler gives us:

  • Correct handling of @for track expressions with complex member references.
  • Correct handling of @defer (when ...) conditions referencing component members.
  • Correct handling of pipes, safe navigation (?.), and nested string literals inside bindings.
  • A single source of truth for both "members referenced from this template" (dead-code) and "complexity contributed by this template" (health) — they walk the same AST.

Implementation sketch

  1. Add oxc-angular-compiler as a direct dependency of fallow-extract (only when published to crates.io; do not vendor or git-pin until Angular <template> CRAP scoring: JIT inherit-from-component + AOT source-map back-mapping (follow-up to #183) #186 lands or until there is explicit green-light).
  2. Replace the body of crates/extract/src/template_complexity.rs with an R3Visitor implementation that increments cyclomatic/cognitive counters per the tables in Angular template complexity analysis for fallow health #183 — keep the public function signature identical so callers (the extractor pipeline + cache) do not need to change.
  3. Replace the regex internals of crates/extract/src/sfc_template/angular.rs with an R3Visitor that collects member references. Keep the public surface stable so the dead-code path does not need to change.
  4. Snapshot-compare the new scanner's output against the old one on a fixture matrix:
    • All fixtures under tests/fixtures/angular-* (existing dead-code + complexity coverage).
    • Real-world projects: bazam.app (178 inline templates), one canonical Nx Angular workspace, one classic external-template workspace.
  5. Where the old scanner over- or under-counted, regenerate the snapshots and document the divergence in the PR description (especially anything that changes a user-visible finding count).

Acceptance criteria

  • Complexity scanner uses oxc-angular-compiler. crates/extract/src/template_complexity.rs no longer contains a hand-rolled state machine; the implementation is an R3Visitor plus an AngularExpression walker. All existing complexity tests pass.
  • Dead-code scanner uses oxc-angular-compiler. crates/extract/src/sfc_template/angular.rs no longer contains regexes for template parsing; it walks the same R3 AST. All existing Angular dead-code tests pass.
  • No regression on real-world fixtures. Run on bazam.app, an Nx Angular workspace, and a classic external-template workspace; finding counts match the pre-swap baseline within documented and intentional divergences.
  • No cargo publish breakage. cargo publish -p fallow-extract --dry-run (and the same for downstream fallow-core, fallow-cli, fallow-mcp) succeeds against a clean checkout.
  • Doc note on parser provenance. Add a one-paragraph note to docs/explanations/angular.mdx (or wherever the Angular plugin docs live) calling out that template parsing is handled by the upstream Oxc Angular compiler, and what that means for new Angular control-flow syntax (it picks up automatically with version bumps).

Out of scope

References

Metadata

Metadata

Labels

area:angularAngular framework analysisarea:extractExtraction and parser pipelineenhancementNew feature or requestpriority:lowDeferred cleanup, blocked dependency work, or nice-to-haverustPull requests that update rust codestatus:blockedBlocked on external dependency, ecosystem release, or precondition

Fields

No fields configured for Feature.

Projects

Status
Todo

Relationships

None yet

Development

No branches or pull requests

Issue actions