@@ -6,15 +6,25 @@ import type {
66import { splitFilePath } from './file-system.js' ;
77import { formatGitPath } from './git/git.js' ;
88
9- type FileCoverage = {
9+ export type FileCoverage = {
1010 path : string ;
1111 total : number ;
12- hits : number ;
12+ covered : number ;
1313 missing : CoverageTreeMissingLOC [ ] ;
1414} ;
1515
16- // TODO: calculate folder coverage
17- const COVERAGE_PLACEHOLDER = - 1 ;
16+ type CoverageStats = Pick < FileCoverage , 'covered' | 'total' > ;
17+
18+ type FileTree = FolderNode | FileNode ;
19+
20+ type FileNode = FileCoverage & {
21+ name : string ;
22+ } ;
23+
24+ type FolderNode = {
25+ name : string ;
26+ children : FileTree [ ] ;
27+ } ;
1828
1929export function filesCoverageToTree (
2030 files : FileCoverage [ ] ,
@@ -26,14 +36,16 @@ export function filesCoverageToTree(
2636 path : formatGitPath ( file . path , gitRoot ) ,
2737 } ) ) ;
2838
29- const root = normalizedFiles . reduce < CoverageTreeNode > (
30- ( acc : CoverageTreeNode , { path : filePath , ... coverage } ) => {
31- const { folders, file } = splitFilePath ( filePath ) ;
39+ const tree = normalizedFiles . reduce < FileTree > (
40+ ( acc , coverage ) => {
41+ const { folders, file } = splitFilePath ( coverage . path ) ;
3242 return addNode ( acc , folders , file , coverage ) ;
3343 } ,
34- { name : '.' , values : { coverage : COVERAGE_PLACEHOLDER } } ,
44+ { name : '.' , children : [ ] } ,
3545 ) ;
3646
47+ const root = calculateTreeCoverage ( tree ) ;
48+
3749 return {
3850 type : 'coverage' ,
3951 ...( title && { title } ) ,
@@ -42,18 +54,19 @@ export function filesCoverageToTree(
4254}
4355
4456function addNode (
45- root : CoverageTreeNode ,
57+ root : FileTree ,
4658 folders : string [ ] ,
4759 file : string ,
48- coverage : Omit < FileCoverage , 'path' > ,
49- ) : CoverageTreeNode {
60+ coverage : FileCoverage ,
61+ ) : FileTree {
5062 const folder = folders [ 0 ] ;
63+ const rootChildren = 'children' in root ? root . children : [ ] ;
5164
5265 if ( folder ) {
53- if ( root . children ? .some ( ( { name } ) => name === folder ) ) {
66+ if ( rootChildren . some ( ( { name } ) => name === folder ) ) {
5467 return {
5568 ...root ,
56- children : root . children . map ( node =>
69+ children : rootChildren . map ( node =>
5770 node . name === folder
5871 ? addNode ( node , folders . slice ( 1 ) , file , coverage )
5972 : node ,
@@ -63,9 +76,9 @@ function addNode(
6376 return {
6477 ...root ,
6578 children : [
66- ...( root . children ?? [ ] ) ,
79+ ...rootChildren ,
6780 addNode (
68- { name : folder , values : { coverage : COVERAGE_PLACEHOLDER } } ,
81+ { name : folder , children : [ ] } ,
6982 folders . slice ( 1 ) ,
7083 file ,
7184 coverage ,
@@ -76,25 +89,65 @@ function addNode(
7689
7790 return {
7891 ...root ,
79- children : [
80- ...( root . children ?? [ ] ) ,
81- {
82- name : file ,
83- values : {
84- coverage : calculateCoverage ( coverage ) ,
85- missing : coverage . missing ,
86- } ,
87- } ,
88- ] ,
92+ children : [ ...rootChildren , { ...coverage , name : file } ] ,
8993 } ;
9094}
9195
92- function calculateCoverage ( {
93- hits,
94- total,
95- } : Pick < FileCoverage , 'hits' | 'total' > ) : number {
96+ function calculateTreeCoverage ( root : FileTree ) : CoverageTreeNode {
97+ if ( 'children' in root ) {
98+ const stats = aggregateChildCoverage ( root . children ) ;
99+ const coverage = calculateCoverage ( stats ) ;
100+ return {
101+ name : root . name ,
102+ values : { coverage } ,
103+ children : root . children . map ( calculateTreeCoverage ) ,
104+ } ;
105+ }
106+
107+ return {
108+ name : root . name ,
109+ values : {
110+ coverage : calculateCoverage ( root ) ,
111+ missing : root . missing ,
112+ } ,
113+ } ;
114+ }
115+
116+ function calculateCoverage ( { covered, total } : CoverageStats ) : number {
96117 if ( total === 0 ) {
97118 return 1 ;
98119 }
99- return hits / total ;
120+ return covered / total ;
121+ }
122+
123+ function aggregateChildCoverage (
124+ nodes : FileTree [ ] ,
125+ cache = new Map < FolderNode , CoverageStats > ( ) ,
126+ ) : CoverageStats {
127+ return nodes . reduce < CoverageStats > (
128+ ( acc , node ) => {
129+ const stats = getNodeCoverageStats ( node , cache ) ;
130+ return {
131+ covered : acc . covered + stats . covered ,
132+ total : acc . total + stats . total ,
133+ } ;
134+ } ,
135+ { covered : 0 , total : 0 } ,
136+ ) ;
137+ }
138+
139+ function getNodeCoverageStats (
140+ node : FileTree ,
141+ cache : Map < FolderNode , CoverageStats > ,
142+ ) : CoverageStats {
143+ if ( ! ( 'children' in node ) ) {
144+ return node ;
145+ }
146+ const cached = cache . get ( node ) ;
147+ if ( cached ) {
148+ return cached ;
149+ }
150+ const stats = aggregateChildCoverage ( node . children , cache ) ;
151+ cache . set ( node , stats ) ;
152+ return stats ;
100153}
0 commit comments