Skip to content

Commit 10b8971

Browse files
authored
feat(coverage): v8 to ignore empty lines, comments, types (#5457)
1 parent d400388 commit 10b8971

File tree

17 files changed

+1227
-156
lines changed

17 files changed

+1227
-156
lines changed

docs/config/index.md

+46
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,20 @@ List of files included in coverage as glob patterns
11031103

11041104
List of files excluded from coverage as glob patterns.
11051105

1106+
This option overrides all default options. Extend the default options when adding new patterns to ignore:
1107+
1108+
```ts
1109+
import { coverageConfigDefaults, defineConfig } from 'vitest/config'
1110+
1111+
export default defineConfig({
1112+
test: {
1113+
coverage: {
1114+
exclude: ['**/custom-pattern/**', ...coverageConfigDefaults.exclude]
1115+
},
1116+
},
1117+
})
1118+
```
1119+
11061120
#### coverage.all
11071121

11081122
- **Type:** `boolean`
@@ -1320,6 +1334,38 @@ Sets thresholds for files matching the glob pattern.
13201334
}
13211335
```
13221336

1337+
#### coverage.ignoreEmptyLines
1338+
1339+
- **Type:** `boolean`
1340+
- **Default:** `false`
1341+
- **Available for providers:** `'v8'`
1342+
- **CLI:** `--coverage.ignoreEmptyLines=<boolean>`
1343+
1344+
Ignore empty lines, comments and other non-runtime code, e.g. Typescript types.
1345+
1346+
This option works only if the used compiler removes comments and other non-runtime code from the transpiled code.
1347+
By default Vite uses ESBuild which removes comments and Typescript types from `.ts`, `.tsx` and `.jsx` files.
1348+
1349+
If you want to apply ESBuild to other files as well, define them in [`esbuild` options](https://vitejs.dev/config/shared-options.html#esbuild):
1350+
1351+
```ts
1352+
import { defineConfig } from 'vitest/config'
1353+
1354+
export default defineConfig({
1355+
esbuild: {
1356+
// Transpile all files with ESBuild to remove comments from code coverage.
1357+
// Required for `test.coverage.ignoreEmptyLines` to work:
1358+
include: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.ts', '**/*.tsx'],
1359+
},
1360+
test: {
1361+
coverage: {
1362+
provider: 'v8',
1363+
ignoreEmptyLines: true,
1364+
},
1365+
},
1366+
})
1367+
```
1368+
13231369
#### coverage.ignoreClassMethods
13241370

13251371
- **Type:** `string[]`

docs/guide/coverage.md

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ npm i -D @vitest/coverage-istanbul
4343

4444
## Coverage Setup
4545

46+
:::tip
47+
It's recommended to always define [`coverage.include`](https://vitest.dev/config/#coverage-include) in your configuration file.
48+
This helps Vitest to reduce the amount of files picked by [`coverage.all`](https://vitest.dev/config/#coverage-all).
49+
:::
50+
4651
To test with coverage enabled, you can pass the `--coverage` flag in CLI.
4752
By default, reporter `['text', 'html', 'clover', 'json']` will be used.
4853

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@
8484
"@types/[email protected]": "patches/@[email protected]",
8585
"@sinonjs/[email protected]": "patches/@[email protected]",
8686
87-
87+
"@types/[email protected]": "patches/@[email protected]",
88+
8889
}
8990
},
9091
"simple-git-hooks": {

packages/coverage-v8/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@
5656
"picocolors": "^1.0.0",
5757
"std-env": "^3.5.0",
5858
"strip-literal": "^2.0.0",
59-
"test-exclude": "^6.0.0",
60-
"v8-to-istanbul": "^9.2.0"
59+
"test-exclude": "^6.0.0"
6160
},
6261
"devDependencies": {
6362
"@types/debug": "^4.1.12",
@@ -66,6 +65,7 @@
6665
"@types/istanbul-lib-source-maps": "^4.0.4",
6766
"@types/istanbul-reports": "^3.0.4",
6867
"pathe": "^1.1.1",
68+
"v8-to-istanbul": "^9.2.0",
6969
"vite-node": "workspace:*",
7070
"vitest": "workspace:*"
7171
}

packages/coverage-v8/src/provider.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,12 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
266266
}
267267

268268
const coverages = await Promise.all(chunk.map(async (filename) => {
269-
const transformResult = await this.ctx.vitenode.transformRequest(filename.pathname).catch(() => {})
269+
const { originalSource, source } = await this.getSources(filename.href, transformResults)
270270

271271
// Ignore empty files, e.g. files that contain only typescript types and no runtime code
272-
if (transformResult && stripLiteral(transformResult.code).trim() === '')
272+
if (source && stripLiteral(source).trim() === '')
273273
return null
274274

275-
const { originalSource } = await this.getSources(filename.href, transformResults)
276-
277275
const coverage = {
278276
url: filename.href,
279277
scriptId: '0',
@@ -309,9 +307,9 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
309307
}> {
310308
const filePath = normalize(fileURLToPath(url))
311309

312-
const transformResult = transformResults.get(filePath)
310+
const transformResult = transformResults.get(filePath) || await this.ctx.vitenode.transformRequest(filePath).catch(() => {})
313311

314-
const map = transformResult?.map
312+
const map = transformResult?.map as (EncodedSourceMap | undefined)
315313
const code = transformResult?.code
316314
const sourcesContent = map?.sourcesContent?.[0] || await fs.readFile(filePath, 'utf-8').catch(() => {
317315
// If file does not exist construct a dummy source for it.
@@ -367,7 +365,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
367365
// If no source map was found from vite-node we can assume this file was not run in the wrapper
368366
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0
369367

370-
const converter = v8ToIstanbul(url, wrapperLength, sources)
368+
const converter = v8ToIstanbul(url, wrapperLength, sources, undefined, this.options.ignoreEmptyLines)
371369
await converter.load()
372370

373371
converter.applyCoverage(functions)

packages/vitest/src/defaults.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
4343
reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]],
4444
extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte', '.marko'],
4545
allowExternal: false,
46+
ignoreEmptyLines: false,
4647
processingConcurrency: Math.min(20, os.availableParallelism?.() ?? os.cpus().length),
4748
}
4849

packages/vitest/src/types/coverage.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,12 @@ export interface CoverageIstanbulOptions extends BaseCoverageOptions {
233233
ignoreClassMethods?: string[]
234234
}
235235

236-
export interface CoverageV8Options extends BaseCoverageOptions {}
236+
export interface CoverageV8Options extends BaseCoverageOptions {
237+
/**
238+
* Ignore empty lines, comments and other non-runtime code, e.g. Typescript types
239+
*/
240+
ignoreEmptyLines?: boolean
241+
}
237242

238243
export interface CustomProviderOptions extends Pick<BaseCoverageOptions, FieldsWithDefaultValues> {
239244
/** Name of the module or path to a file to load the custom provider from */

patches/[email protected]

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
diff --git a/CHANGELOG.md b/CHANGELOG.md
2+
deleted file mode 100644
3+
index 4f7e3bc8d1bba4feb51044ff9eb77b41f972f957..0000000000000000000000000000000000000000
4+
diff --git a/index.d.ts b/index.d.ts
5+
index ee7b286844f2bf96357218166e26e1c338f774cf..657531b7c75f43e9a4e957dd1f10797e44da5bb1 100644
6+
--- a/index.d.ts
7+
+++ b/index.d.ts
8+
@@ -1,5 +1,7 @@
9+
/// <reference types="node" />
10+
11+
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
12+
+
13+
import { Profiler } from 'inspector'
14+
import { CoverageMapData } from 'istanbul-lib-coverage'
15+
import { SourceMapInput } from '@jridgewell/trace-mapping'
16+
@@ -20,6 +22,6 @@ declare class V8ToIstanbul {
17+
toIstanbul(): CoverageMapData
18+
}
19+
20+
-declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean): V8ToIstanbul
21+
+declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean, excludeEmptyLines?: boolean): V8ToIstanbul
22+
23+
export = v8ToIstanbul
24+
diff --git a/index.js b/index.js
25+
index 4db27a7d84324d0e6605c5506e3eee5665ddfeb0..7bfb839634b1e3c54efedc3c270d82edc4167a64 100644
26+
--- a/index.js
27+
+++ b/index.js
28+
@@ -1,5 +1,6 @@
29+
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
30+
const V8ToIstanbul = require('./lib/v8-to-istanbul')
31+
32+
-module.exports = function (path, wrapperLength, sources, excludePath) {
33+
- return new V8ToIstanbul(path, wrapperLength, sources, excludePath)
34+
+module.exports = function (path, wrapperLength, sources, excludePath, excludeEmptyLines) {
35+
+ return new V8ToIstanbul(path, wrapperLength, sources, excludePath, excludeEmptyLines)
36+
}
37+
diff --git a/lib/source.js b/lib/source.js
38+
index d8ebc215f6ad83d472abafe976935acfe5c61b04..021fd2aed1f73ebb4adc449ce6e96f2d89c295a5 100644
39+
--- a/lib/source.js
40+
+++ b/lib/source.js
41+
@@ -1,23 +1,32 @@
42+
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
43+
const CovLine = require('./line')
44+
const { sliceRange } = require('./range')
45+
-const { originalPositionFor, generatedPositionFor, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping')
46+
+const { originalPositionFor, generatedPositionFor, eachMapping, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping')
47+
48+
module.exports = class CovSource {
49+
- constructor (sourceRaw, wrapperLength) {
50+
+ constructor (sourceRaw, wrapperLength, traceMap) {
51+
sourceRaw = sourceRaw ? sourceRaw.trimEnd() : ''
52+
this.lines = []
53+
this.eof = sourceRaw.length
54+
this.shebangLength = getShebangLength(sourceRaw)
55+
this.wrapperLength = wrapperLength - this.shebangLength
56+
- this._buildLines(sourceRaw)
57+
+ this._buildLines(sourceRaw, traceMap)
58+
}
59+
60+
- _buildLines (source) {
61+
+ _buildLines (source, traceMap) {
62+
let position = 0
63+
let ignoreCount = 0
64+
let ignoreAll = false
65+
+ const linesToCover = traceMap && this._parseLinesToCover(traceMap)
66+
+
67+
for (const [i, lineStr] of source.split(/(?<=\r?\n)/u).entries()) {
68+
- const line = new CovLine(i + 1, position, lineStr)
69+
+ const lineNumber = i + 1
70+
+ const line = new CovLine(lineNumber, position, lineStr)
71+
+
72+
+ if (linesToCover && !linesToCover.has(lineNumber)) {
73+
+ line.ignore = true
74+
+ }
75+
+
76+
if (ignoreCount > 0) {
77+
line.ignore = true
78+
ignoreCount--
79+
@@ -125,6 +134,18 @@ module.exports = class CovSource {
80+
if (this.lines[line - 1] === undefined) return this.eof
81+
return Math.min(this.lines[line - 1].startCol + relCol, this.lines[line - 1].endCol)
82+
}
83+
+
84+
+ _parseLinesToCover (traceMap) {
85+
+ const linesToCover = new Set()
86+
+
87+
+ eachMapping(traceMap, (mapping) => {
88+
+ if (mapping.originalLine !== null) {
89+
+ linesToCover.add(mapping.originalLine)
90+
+ }
91+
+ })
92+
+
93+
+ return linesToCover
94+
+ }
95+
}
96+
97+
// this implementation is pulled over from istanbul-lib-sourcemap:
98+
diff --git a/lib/v8-to-istanbul.js b/lib/v8-to-istanbul.js
99+
index 3616437b00658861dc5a8910c64d1449e9fdf467..c1e0c0ae19984480e408713d1691fa174a7c4c1f 100644
100+
--- a/lib/v8-to-istanbul.js
101+
+++ b/lib/v8-to-istanbul.js
102+
@@ -1,3 +1,4 @@
103+
+// Patch applied: https://github.com/istanbuljs/v8-to-istanbul/pull/244
104+
const assert = require('assert')
105+
const convertSourceMap = require('convert-source-map')
106+
const util = require('util')
107+
@@ -25,12 +26,13 @@ const isNode8 = /^v8\./.test(process.version)
108+
const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0
109+
110+
module.exports = class V8ToIstanbul {
111+
- constructor (scriptPath, wrapperLength, sources, excludePath) {
112+
+ constructor (scriptPath, wrapperLength, sources, excludePath, excludeEmptyLines) {
113+
assert(typeof scriptPath === 'string', 'scriptPath must be a string')
114+
assert(!isNode8, 'This module does not support node 8 or lower, please upgrade to node 10')
115+
this.path = parsePath(scriptPath)
116+
this.wrapperLength = wrapperLength === undefined ? cjsWrapperLength : wrapperLength
117+
this.excludePath = excludePath || (() => false)
118+
+ this.excludeEmptyLines = excludeEmptyLines === true
119+
this.sources = sources || {}
120+
this.generatedLines = []
121+
this.branches = {}
122+
@@ -58,8 +60,8 @@ module.exports = class V8ToIstanbul {
123+
if (!this.sourceMap.sourcesContent) {
124+
this.sourceMap.sourcesContent = await this.sourcesContentFromSources()
125+
}
126+
- this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength), path: this.sourceMap.sources[i] }))
127+
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
128+
+ this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.sourceMap.sources[i] }))
129+
+ this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null)
130+
} else {
131+
const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1 ? this.rawSourceMap.sourcemap.sources[0] : this.rawSourceMap.sourcemap.file
132+
this.path = this._resolveSource(this.rawSourceMap, candidatePath || this.path)
133+
@@ -82,8 +84,8 @@ module.exports = class V8ToIstanbul {
134+
// We fallback to reading the original source from disk.
135+
originalRawSource = await readFile(this.path, 'utf8')
136+
}
137+
- this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength), path: this.path }]
138+
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
139+
+ this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.path }]
140+
+ this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null)
141+
}
142+
} else {
143+
this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }]
144+
@@ -281,8 +283,10 @@ module.exports = class V8ToIstanbul {
145+
s: {}
146+
}
147+
source.lines.forEach((line, index) => {
148+
- statements.statementMap[`${index}`] = line.toIstanbul()
149+
- statements.s[`${index}`] = line.ignore ? 1 : line.count
150+
+ if (!line.ignore) {
151+
+ statements.statementMap[`${index}`] = line.toIstanbul()
152+
+ statements.s[`${index}`] = line.count
153+
+ }
154+
})
155+
return statements
156+
}

pnpm-lock.yaml

+12-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)