You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#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:
@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.
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.
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.
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.
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).
Background
#183 specifically requested
oxc-angular-compileras the parser for Angular template complexity, citing:HtmlBlocknodes for@if,@for,@switch,@defer,@let.AngularExpressionAST withConditional(ternary),Binary(&&,||,??),SafePropertyRead(?.).R3IfBlock,R3ForLoopBlock,R3SwitchBlock,R3DeferredBlocknodes.R3Visitortrait withvisit_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.rsinstead. The close comment on #183 flagged this as a divergence and a candidate for a clean swap once the upstream crate is published:#186 (CRAP scoring) explicitly defers the parser swap:
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-compileris not on crates.io. Depending on it via agitCargo source would breakcargo 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-compilerto 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:
[attr]=\"\\literal\\") trip the string-state machine.@letwith multi-line ternaries that span more than two lines need bespoke handling for each shape.@deferadditions, future template syntax) require manual scanner updates rather than picking up an upstream parser version bump.@iffrom a real@ifblock in every position — it relies on heuristics.A real
R3Visitorremoves 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-compilergives us:@fortrack expressions with complex member references.@defer (when ...)conditions referencing component members.?.), and nested string literals inside bindings.Implementation sketch
oxc-angular-compileras a direct dependency offallow-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).crates/extract/src/template_complexity.rswith anR3Visitorimplementation 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.crates/extract/src/sfc_template/angular.rswith anR3Visitorthat collects member references. Keep the public surface stable so the dead-code path does not need to change.tests/fixtures/angular-*(existing dead-code + complexity coverage).Acceptance criteria
oxc-angular-compiler.crates/extract/src/template_complexity.rsno longer contains a hand-rolled state machine; the implementation is anR3Visitorplus anAngularExpressionwalker. All existing complexity tests pass.oxc-angular-compiler.crates/extract/src/sfc_template/angular.rsno longer contains regexes for template parsing; it walks the same R3 AST. All existing Angular dead-code tests pass.cargo publishbreakage.cargo publish -p fallow-extract --dry-run(and the same for downstreamfallow-core,fallow-cli,fallow-mcp) succeeds against a clean checkout.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
<template>findings (Angular <template> CRAP scoring: JIT inherit-from-component + AOT source-map back-mapping (follow-up to #183) #186)....}) decorators (follow-up to #183) #187, already shipped).References
<template>CRAP scoring tiers (defers parser swap as out-of-scope)@Component({ template: \...` })` support (closed, shipped)dad23a43