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 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
After dead-code analysis, walk every <template> finding and resolve its owning Angular component:
For inline templates: the .ts file is already the path, and the component class is the closest enclosing class declaration to the template literal's byte-span.
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.
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.
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:#186 (CRAP scoring) explicitly carries the rollup forward as out-of-scope:
Current behaviour
For an Angular component with both class logic and template control flow,
fallow health --complexityemits two (or more) separate findings:HostGameComponent.ngOnInit,HostGameComponent.handleClick, etc.).<template>finding rooted at the inline template literal (or the external.htmlfile).Sorting by complexity, ranking, and
--targetsselection 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
--targetsand the headline ranking surface the component as a single unit.Implementation sketch
<template>finding and resolve its owning Angular component:.tsfile is already the path, and the component class is the closest enclosing class declaration to the template literal's byte-span.templateUrltemplates: follow the inverse of the existingtemplateUrlgraph edge to find the owning.tsfile, then pick the class that declared the decorator. (Same lookup Angular <template> CRAP scoring: JIT inherit-from-component + AOT source-map back-mapping (follow-up to #183) #186's tier-1 work needs.)(cyclomatic, cognitive)into a newComponentComplexityaggregate that also references the class's worst function. Three candidate shapes for the aggregate:template_cyclomatic/template_cognitivefields and surface the sum in--targetsranking.<component>finding at the class declaration line whose totals = max(class function totals) + template totals.--targets, headline counts) without changing the JSON shape.Reporting requirements
human,json,markdown,sarif,compact,codeclimate,badge, MCP) without breaking back-compat for tools that already consume the per-template entries.humanoutput groups class function + template under a single component header with both numbers visible.jsoneither gains acomponentblock per finding or grows a top-levelcomponentRollupsarray; either way the per-finding entries stay where they are today.--targetsand the headline complexity rank uses the rolled-up totals so that template-heavy components are not hidden by their thin classes.Acceptance criteria
crates/core/src/analyze/. Add unit tests undercrates/core/tests/covering: externaltemplateUrl, inlinetemplate:literal, components with no template (rollup is no-op), components with multiple template literals (very rare, mostly defensive).--targetsand headline rank use rolled-up totals. Add an integration test incrates/cli/tests/health_tests.rswith 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.human,json,markdown,sarif,compact,codeclimate,badgeexercise a rolled-up component and assert the per-template entries still exist alongside the rollup.healthMCP tool's response carries the same rollup field(s).docs/explanations/health.mdxexplaining (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
<template>findings (tracked in Angular <template> CRAP scoring: JIT inherit-from-component + AOT source-map back-mapping (follow-up to #183) #186).sfc_template/angular.rsdead-code scanner with a proper Angular template parser (separate follow-up).References
<template>CRAP scoring tiers (JIT inherit-from-.ts, AOT source-map)@Component({ template: \...` })` support (closed, shipped)dad23a43