Skip to content

Angular component complexity rollup: combine class + <template> into single component-level finding (follow-up to #183) #234

@BartWaardenburg

Description

@BartWaardenburg

Background

#183 listed three reporting models for Angular template complexity and ranked option 1 (combined component metric: sum template + class complexity into a single component-level finding) as the most actionable, on the grounds that "a component is complex regardless of whether the complexity is in the class or the template."

v2.51.0 shipped option 3 (synthetic <template> finding alongside the class's existing function findings) because it was the smallest change and landed cleanly through every output format. The close comment on #183 explicitly flagged option 1 as a future follow-up:

shipped option 3 (synthetic <template> finding in the existing findings list) rather than your preferred option 1 (combined component metric). Smaller change; lands cleanly through every output format. A future PR can roll up to option 1 by post-processing findings without breaking the per-template entries.

#186 (CRAP scoring) explicitly carries the rollup forward as out-of-scope:

Rolling up class + template complexity into a single component-level metric (#183 reporting option 1; current implementation uses option 3 — synthetic <template> finding).

Current behaviour

For an Angular component with both class logic and template control flow, fallow health --complexity emits two (or more) separate findings:

  • One per JS/TS function/method on the component class (HostGameComponent.ngOnInit, HostGameComponent.handleClick, etc.).
  • One synthetic <template> finding rooted at the inline template literal (or the external .html file).

Sorting by complexity, ranking, and --targets selection treat these as independent. A component whose class scores 4/4 and whose template scores 6/8 looks like two medium findings instead of one heavy one.

Proposed solution

Add an optional component-level rollup that sums the template's cyclomatic and cognitive complexity into the owning class's worst function (or into a new synthetic component-level finding) so that --targets and the headline ranking surface the component as a single unit.

Implementation sketch

  1. After dead-code analysis, walk every <template> finding and resolve its owning Angular component:
  2. Sum the template's (cyclomatic, cognitive) into a new ComponentComplexity aggregate that also references the class's worst function. Three candidate shapes for the aggregate:
    • a. Decorate the worst class function with template_cyclomatic/template_cognitive fields and surface the sum in --targets ranking.
    • b. Emit a synthetic <component> finding at the class declaration line whose totals = max(class function totals) + template totals.
    • c. Keep per-finding output as-is and only roll up at the ranking layer (--targets, headline counts) without changing the JSON shape.
  3. Whichever shape lands, the per-template and per-function entries continue to exist so suppressions and links keep working; the rollup is additive.

Reporting requirements

  • The rollup must propagate through every output format (human, json, markdown, sarif, compact, codeclimate, badge, MCP) without breaking back-compat for tools that already consume the per-template entries.
  • human output groups class function + template under a single component header with both numbers visible.
  • json either gains a component block per finding or grows a top-level componentRollups array; either way the per-finding entries stay where they are today.
  • --targets and the headline complexity rank uses the rolled-up totals so that template-heavy components are not hidden by their thin classes.

Acceptance criteria

  • Component rollup primitive in core. Pick one of the three shapes above (recommend (a) — decorate the worst class function — for minimum schema churn) and implement it in crates/core/src/analyze/. Add unit tests under crates/core/tests/ covering: external templateUrl, inline template: literal, components with no template (rollup is no-op), components with multiple template literals (very rare, mostly defensive).
  • --targets and headline rank use rolled-up totals. Add an integration test in crates/cli/tests/health_tests.rs with a fixture component whose class is below threshold (e.g. cyclomatic 3) and whose template pushes the rollup above (e.g. template adds 6 → rolled-up cyclomatic 9). Without the rollup the component is not in --targets; with the rollup it is. Reverting the rollup logic should make the test fail.
  • All output formats render the rollup. Snapshot tests for human, json, markdown, sarif, compact, codeclimate, badge exercise a rolled-up component and assert the per-template entries still exist alongside the rollup.
  • MCP tool returns rolled-up data. The health MCP tool's response carries the same rollup field(s).
  • Doc note on the rollup model. Add a section to docs/explanations/health.mdx explaining (a) what the rollup represents, (b) why a thin class + heavy template still ranks high, (c) how this differs from the per-template synthetic finding model in v2.51.0.

Out of scope

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions