@@ -38,6 +38,10 @@ const {
3838} = require ( 'internal/errors' ) ;
3939const { matchGlobPattern } = require ( 'internal/fs/glob' ) ;
4040const { constants : { kMockSearchParam } } = require ( 'internal/test_runner/mock/loader' ) ;
41+ const { Parser : AcornParser } =
42+ require ( 'internal/deps/acorn/acorn/dist/acorn' ) ;
43+ const { simple : acornWalkSimple } =
44+ require ( 'internal/deps/acorn/acorn-walk/dist/walk' ) ;
4145
4246const kCoverageFileRegex = / ^ c o v e r a g e - ( \d + ) - ( \d { 13 } ) - ( \d + ) \. j s o n $ / ;
4347const kIgnoreRegex = / \/ \* n o d e : c o v e r a g e i g n o r e n e x t (?< count > \d + ) ? \* \/ / ;
@@ -69,6 +73,52 @@ class TestCoverage {
6973 }
7074
7175 #sourceLines = new SafeMap ( ) ;
76+ #sourceStatements = new SafeMap ( ) ;
77+
78+ getStatements ( fileUrl , source ) {
79+ if ( this . #sourceStatements. has ( fileUrl ) ) {
80+ return this . #sourceStatements. get ( fileUrl ) ;
81+ }
82+
83+ try {
84+ source ??= readFileSync ( fileURLToPath ( fileUrl ) , 'utf8' ) ;
85+ } catch {
86+ this . #sourceStatements. set ( fileUrl , null ) ;
87+ return null ;
88+ }
89+
90+ const statements = [ ] ;
91+ try {
92+ const ast = AcornParser . parse ( source , {
93+ __proto__ : null ,
94+ ecmaVersion : 'latest' ,
95+ sourceType : 'module' ,
96+ allowReturnOutsideFunction : true ,
97+ allowImportExportEverywhere : true ,
98+ allowAwaitOutsideFunction : true ,
99+ } ) ;
100+
101+ acornWalkSimple ( ast , {
102+ Statement ( node ) {
103+ if ( node . type === 'BlockStatement' ) {
104+ return ;
105+ }
106+ ArrayPrototypePush ( statements , {
107+ __proto__ : null ,
108+ startOffset : node . start ,
109+ endOffset : node . end ,
110+ count : 0 ,
111+ } ) ;
112+ } ,
113+ } ) ;
114+ } catch {
115+ this . #sourceStatements. set ( fileUrl , null ) ;
116+ return null ;
117+ }
118+
119+ this . #sourceStatements. set ( fileUrl , statements ) ;
120+ return statements ;
121+ }
72122
73123 getLines ( fileUrl , source ) {
74124 // Split the file source into lines. Make sure the lines maintain their
@@ -145,18 +195,22 @@ class TestCoverage {
145195 totalLineCount : 0 ,
146196 totalBranchCount : 0 ,
147197 totalFunctionCount : 0 ,
198+ totalStatementCount : 0 ,
148199 coveredLineCount : 0 ,
149200 coveredBranchCount : 0 ,
150201 coveredFunctionCount : 0 ,
202+ coveredStatementCount : 0 ,
151203 coveredLinePercent : 0 ,
152204 coveredBranchPercent : 0 ,
153205 coveredFunctionPercent : 0 ,
206+ coveredStatementPercent : 0 ,
154207 } ,
155208 thresholds : {
156209 __proto__ : null ,
157210 line : this . options . lineCoverage ,
158211 branch : this . options . branchCoverage ,
159212 function : this . options . functionCoverage ,
213+ statement : this . options . statementCoverage ,
160214 } ,
161215 } ;
162216
@@ -243,29 +297,83 @@ class TestCoverage {
243297 }
244298 }
245299
300+ // Compute statement coverage by mapping V8 ranges to AST statements.
301+ const statements = this . getStatements ( url ) ;
302+ let totalStatements = 0 ;
303+ let statementsCovered = 0 ;
304+ const statementReports = [ ] ;
305+
306+ if ( statements ) {
307+ for ( let j = 0 ; j < statements . length ; ++ j ) {
308+ const stmt = statements [ j ] ;
309+ let bestCount = 0 ;
310+ let bestSize = Infinity ;
311+ let found = false ;
312+
313+ for ( let fi = 0 ; fi < functions . length ; ++ fi ) {
314+ const { ranges } = functions [ fi ] ;
315+ for ( let ri = 0 ; ri < ranges . length ; ++ ri ) {
316+ const range = ranges [ ri ] ;
317+ if ( range . startOffset <= stmt . startOffset &&
318+ range . endOffset >= stmt . endOffset ) {
319+ const size = range . endOffset - range . startOffset ;
320+ if ( ! found || size < bestSize ) {
321+ bestCount = range . count ;
322+ bestSize = size ;
323+ }
324+ found = true ;
325+ }
326+ }
327+ }
328+
329+ stmt . count = found ? bestCount : 0 ;
330+
331+ const stmtLine = findLineForOffset ( stmt . startOffset , lines ) ;
332+ const isIgnored = stmtLine != null && stmtLine . ignore ;
333+
334+ if ( ! isIgnored ) {
335+ totalStatements ++ ;
336+ ArrayPrototypePush ( statementReports , {
337+ __proto__ : null ,
338+ line : stmtLine ?. line ,
339+ count : stmt . count ,
340+ } ) ;
341+ if ( stmt . count > 0 ) {
342+ statementsCovered ++ ;
343+ }
344+ }
345+ }
346+ }
347+
246348 ArrayPrototypePush ( coverageSummary . files , {
247349 __proto__ : null ,
248350 path : fileURLToPath ( url ) ,
249351 totalLineCount : lines . length ,
250352 totalBranchCount : totalBranches ,
251353 totalFunctionCount : totalFunctions ,
354+ totalStatementCount : totalStatements ,
252355 coveredLineCount : coveredCnt ,
253356 coveredBranchCount : branchesCovered ,
254357 coveredFunctionCount : functionsCovered ,
358+ coveredStatementCount : statementsCovered ,
255359 coveredLinePercent : toPercentage ( coveredCnt , lines . length ) ,
256360 coveredBranchPercent : toPercentage ( branchesCovered , totalBranches ) ,
257361 coveredFunctionPercent : toPercentage ( functionsCovered , totalFunctions ) ,
362+ coveredStatementPercent : toPercentage ( statementsCovered , totalStatements ) ,
258363 functions : functionReports ,
259364 branches : branchReports ,
260365 lines : lineReports ,
366+ statements : statementReports ,
261367 } ) ;
262368
263369 coverageSummary . totals . totalLineCount += lines . length ;
264370 coverageSummary . totals . totalBranchCount += totalBranches ;
265371 coverageSummary . totals . totalFunctionCount += totalFunctions ;
372+ coverageSummary . totals . totalStatementCount += totalStatements ;
266373 coverageSummary . totals . coveredLineCount += coveredCnt ;
267374 coverageSummary . totals . coveredBranchCount += branchesCovered ;
268375 coverageSummary . totals . coveredFunctionCount += functionsCovered ;
376+ coverageSummary . totals . coveredStatementCount += statementsCovered ;
269377 }
270378
271379 coverageSummary . totals . coveredLinePercent = toPercentage (
@@ -280,6 +388,10 @@ class TestCoverage {
280388 coverageSummary . totals . coveredFunctionCount ,
281389 coverageSummary . totals . totalFunctionCount ,
282390 ) ;
391+ coverageSummary . totals . coveredStatementPercent = toPercentage (
392+ coverageSummary . totals . coveredStatementCount ,
393+ coverageSummary . totals . totalStatementCount ,
394+ ) ;
283395 coverageSummary . files . sort ( sortCoverageFiles ) ;
284396
285397 return coverageSummary ;
@@ -695,4 +807,24 @@ function doesRangeContainOtherRange(range, otherRange) {
695807 range . endOffset >= otherRange . endOffset ;
696808}
697809
810+ function findLineForOffset ( offset , lines ) {
811+ let start = 0 ;
812+ let end = lines . length - 1 ;
813+
814+ while ( start <= end ) {
815+ const mid = MathFloor ( ( start + end ) / 2 ) ;
816+ const line = lines [ mid ] ;
817+
818+ if ( offset >= line . startOffset && offset <= line . endOffset ) {
819+ return line ;
820+ } else if ( offset > line . endOffset ) {
821+ start = mid + 1 ;
822+ } else {
823+ end = mid - 1 ;
824+ }
825+ }
826+
827+ return null ;
828+ }
829+
698830module . exports = { setupCoverage, TestCoverage } ;
0 commit comments