Skip to content

Commit e02db08

Browse files
feat(coverage): v8 to support ignoreNode option (#11688)
Co-authored-by: Ari Perkkiö <[email protected]>
1 parent 7a5c083 commit e02db08

7 files changed

Lines changed: 166 additions & 2 deletions

File tree

‎docs/config/coverage.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,35 @@ Sets thresholds to 100 for files matching the glob pattern.
407407
Set to array of class method names to ignore for coverage.
408408
See [istanbul documentation](https://github.com/istanbuljs/nyc#ignoring-methods) for more information.
409409

410+
## coverage.ignoreNode <Version>5.1.0</Version> {#coverage-ignorenode}
411+
412+
- **Type:** `(node: Node, type: 'function' | 'statement' | 'branch') => boolean | 'ignore-this-and-nested-nodes' | void`
413+
- **Available for providers:** `'v8'`
414+
415+
Filter to ignore code from coverage based on its AST node. Vitest calls it for each node of the transformed code, together with the kind of coverage the node counts for. Return `true` to ignore the node, or `'ignore-this-and-nested-nodes'` to also ignore all nodes inside it.
416+
417+
This is useful for code that doesn't exist in the source file and can't carry an [ignore hint](/guide/coverage#ignoring-code), like code that compilers or other plugins generate. Since the nodes come from the code after Vite's transforms, the filter sees exactly what the plugins generated.
418+
419+
Vitest's built-in filters run first, so nodes that Vitest already ignores, like its own transform artifacts, are never passed to this function.
420+
421+
```ts
422+
import { defineConfig } from 'vitest/config'
423+
424+
export default defineConfig({
425+
test: {
426+
coverage: {
427+
provider: 'v8',
428+
ignoreNode: (node, type) =>
429+
type === 'statement'
430+
&& node.type === 'ExpressionStatement'
431+
&& node.expression.type === 'CallExpression'
432+
&& node.expression.callee.type === 'Identifier'
433+
&& node.expression.callee.name === '__generatedHelper',
434+
},
435+
},
436+
})
437+
```
438+
410439
## coverage.watermarks
411440

412441
- **Type:**

‎packages/coverage-v8/src/provider.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
242242
return {}
243243
}
244244

245-
return (await astV8ToIstanbul({
245+
return (await astV8ToIstanbul<Vite.ESTree.Node>({
246246
code: result.code,
247247
sourceMap: result.map,
248248
ast,
@@ -372,6 +372,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
372372
) {
373373
return 'ignore-this-and-nested-nodes'
374374
}
375+
376+
return this.options.ignoreNode?.(node, type)
375377
},
376378
})) as libCoverage.CoverageMapData
377379
}

‎packages/vitest/src/node/types/coverage.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReportOptions } from '@vitest/istanbul-lib-report'
22
import type { Arrayable } from '@vitest/utils'
3-
import type { TransformResult as ViteTransformResult } from 'vite'
3+
import type { ESTree, TransformResult as ViteTransformResult } from 'vite'
44
import type { AfterSuiteRunMeta } from '../../types/general'
55
import type {
66
RuntimeCoverageModuleLoader,
@@ -269,6 +269,22 @@ export interface CoverageOptions {
269269
*/
270270
ignoreClassMethods?: string[]
271271

272+
/**
273+
* Filter to ignore code from coverage based on its AST node.
274+
* Called only for nodes that Vitest's built-in filters don't already ignore.
275+
* Return `'ignore-this-and-nested-nodes'` to also ignore all nodes inside the given node.
276+
* Supported only by `v8` provider.
277+
*
278+
* @example
279+
* ```ts
280+
* ignoreNode: (node, type) => type === 'statement' && node.type === 'DebuggerStatement'
281+
* ```
282+
*/
283+
ignoreNode?: (
284+
node: ESTree.Node,
285+
type: 'function' | 'statement' | 'branch',
286+
) => boolean | 'ignore-this-and-nested-nodes' | void
287+
272288
/**
273289
* Custom instrumenter factory to use instead of the default `@vitest/istanbul-lib-instrument`.
274290
*
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { sum } from './math'
2+
3+
export function calculate(a: number, b: number) {
4+
__generatedHelper()
5+
6+
if (a > b) {
7+
const total = sum(a, b)
8+
return total
9+
}
10+
11+
return sum(b, a)
12+
}
13+
14+
function __generatedHelper() {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { expect, test } from 'vitest'
2+
import { calculate } from '../src/ignore-node'
3+
4+
test('cover some lines', () => {
5+
expect(calculate(2, 1)).toBe(3)
6+
})

‎test/coverage-test/test/configuration-options.test-d.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,40 @@ test('custom instrumenter', () => {
328328
instrumenter: factory,
329329
})
330330
})
331+
332+
test('ignoreNode', () => {
333+
assertType<Coverage>({
334+
provider: 'v8',
335+
ignoreNode: (node, type) => type === 'branch' && node.type === 'IfStatement',
336+
})
337+
338+
assertType<Coverage>({
339+
provider: 'v8',
340+
ignoreNode: () => 'ignore-this-and-nested-nodes',
341+
})
342+
343+
assertType<Coverage>({
344+
provider: 'v8',
345+
ignoreNode: () => {},
346+
})
347+
348+
assertType<Coverage>({
349+
provider: 'v8',
350+
// @ts-expect-error -- must return a boolean, 'ignore-this-and-nested-nodes' or nothing
351+
ignoreNode: () => 'ignore-everything',
352+
})
353+
354+
assertType<Coverage>({
355+
provider: 'v8',
356+
ignoreNode: (node) => {
357+
if (node.type === 'IfStatement' && node.alternate) {
358+
return true
359+
}
360+
361+
// @ts-expect-error -- CatchClause has no alternate. Should fail if node types work correctly
362+
if (node.type === 'CatchClause' && node.alternate) {
363+
return true
364+
}
365+
},
366+
})
367+
})
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect } from 'vitest'
2+
import { isNativeRunner, readCoverageMap, runVitest, test } from '../utils'
3+
4+
/* See {@link file://./../fixtures/src/ignore-node.ts} */
5+
const filename = '<process-cwd>/fixtures/src/ignore-node.ts'
6+
7+
test('{ ignoreNode } excludes the matching nodes', async () => {
8+
await runVitest({
9+
include: ['fixtures/test/ignore-node-fixture.test.ts'],
10+
coverage: {
11+
reporter: 'json',
12+
ignoreNode: (node, type) => {
13+
if (
14+
type === 'statement' &&
15+
node.type === 'ExpressionStatement' &&
16+
node.expression.type === 'CallExpression' &&
17+
node.expression.callee.type === 'Identifier' &&
18+
node.expression.callee.name === '__generatedHelper'
19+
) {
20+
return true
21+
}
22+
23+
return type === 'branch' && node.type === 'IfStatement'
24+
},
25+
},
26+
})
27+
28+
const coverageMap = await readCoverageMap()
29+
const fileCoverage = coverageMap.fileCoverageFor(filename)
30+
const lines = fileCoverage.getLineCoverage()
31+
32+
expect(lines[4]).toBeUndefined()
33+
34+
expect(fileCoverage.getBranchCoverageByLine()[6]).toBeUndefined()
35+
36+
expect(lines[7]).toBeGreaterThanOrEqual(1)
37+
expect(lines[8]).toBeGreaterThanOrEqual(1)
38+
39+
expect(lines[11]).toBe(0)
40+
})
41+
42+
test('{ ignoreNode } is not called for nodes that Vitest already ignores', async ({ skip }) => {
43+
skip(isNativeRunner(), 'Native runner does not transform imports to __vite_ssr_import_')
44+
45+
const declarators: string[] = []
46+
47+
await runVitest({
48+
include: ['fixtures/test/ignore-node-fixture.test.ts'],
49+
coverage: {
50+
reporter: 'json',
51+
ignoreNode: (node) => {
52+
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
53+
declarators.push(node.id.name)
54+
}
55+
},
56+
},
57+
})
58+
59+
expect(declarators).toEqual(['total'])
60+
})

0 commit comments

Comments
 (0)