Skip to content

Commit b5f7d06

Browse files
committed
feat(coverage): v8 experimental AST-aware remapping
1 parent 057c0ac commit b5f7d06

19 files changed

+203
-1161
lines changed

docs/guide/coverage.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,24 +190,21 @@ Both coverage providers have their own ways how to ignore code from coverage rep
190190

191191
- [`v8`](https://github.com/istanbuljs/v8-to-istanbul#ignoring-uncovered-lines)
192192
- [`ìstanbul`](https://github.com/istanbuljs/nyc#parsing-hints-ignoring-lines)
193+
- `v8` with `experimentalAstAwareRemapping` - TODO
193194

194195
When using TypeScript the source codes are transpiled using `esbuild`, which strips all comments from the source codes ([esbuild#516](https://github.com/evanw/esbuild/issues/516)).
195196
Comments which are considered as [legal comments](https://esbuild.github.io/api/#legal-comments) are preserved.
196197

197-
For `istanbul` provider you can include a `@preserve` keyword in the ignore hint.
198+
You can include a `@preserve` keyword in the ignore hint.
198199
Beware that these ignore hints may now be included in final production build as well.
199200

200201
```diff
201202
-/* istanbul ignore if */
202203
+/* istanbul ignore if -- @preserve */
203204
if (condition) {
204-
```
205-
206-
For `v8` this does not cause any issues. You can use `v8 ignore` comments with Typescript as usual:
207205

208-
<!-- eslint-skip -->
209-
```ts
210-
/* v8 ignore next 3 */
206+
-/* v8 ignore if */
207+
+/* v8 ignore if -- @preserve */
211208
if (condition) {
212209
```
213210

packages/coverage-v8/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"dependencies": {
5757
"@ampproject/remapping": "catalog:",
5858
"@bcoe/v8-coverage": "^1.0.2",
59+
"ast-v8-to-istanbul": "https://pkg.pr.new/AriPerkkio/ast-v8-to-istanbul@81965c4",
5960
"debug": "catalog:",
6061
"istanbul-lib-coverage": "catalog:",
6162
"istanbul-lib-report": "catalog:",

packages/coverage-v8/src/provider.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
99
import remapping from '@ampproject/remapping'
1010
// @ts-expect-error -- untyped
1111
import { mergeProcessCovs } from '@bcoe/v8-coverage'
12+
import astV8ToIstanbul from 'ast-v8-to-istanbul'
1213
import createDebug from 'debug'
1314
import libCoverage from 'istanbul-lib-coverage'
1415
import libReport from 'istanbul-lib-report'
@@ -24,6 +25,7 @@ import v8ToIstanbul from 'v8-to-istanbul'
2425
import { cleanUrl } from 'vite-node/utils'
2526

2627
import { BaseCoverageProvider } from 'vitest/coverage'
28+
import { parseAstAsync } from 'vitest/node'
2729
import { version } from '../package.json' with { type: 'json' }
2830

2931
export interface ScriptCoverageWithOffset extends Profiler.ScriptCoverage {
@@ -34,10 +36,10 @@ type TransformResults = Map<string, FetchResult>
3436
interface RawCoverage { result: ScriptCoverageWithOffset[] }
3537

3638
// Note that this needs to match the line ending as well
37-
const VITE_EXPORTS_LINE_PATTERN
38-
= /Object\.defineProperty\(__vite_ssr_exports__.*\n/g
39-
const DECORATOR_METADATA_PATTERN
40-
= /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g
39+
const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g
40+
const VITE_EXPORTS_LINE_PATTERN_2 = /^__vite_ssr_exports__\..*/gm
41+
const VITE_IMPORTS_LINE_PATTERN = /^const __vite_ssr_import_.*$/gm
42+
const DECORATOR_METADATA_PATTERN = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g
4143
const FILE_PROTOCOL = 'file://'
4244

4345
const debug = createDebug('vitest:coverage')
@@ -219,6 +221,25 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
219221
}
220222

221223
private async v8ToIstanbul(filename: string, wrapperLength: number, sources: Awaited<ReturnType<typeof this.getSources>>, functions: Profiler.FunctionCoverage[]) {
224+
if (this.options.experimentalAstAwareRemapping) {
225+
const sourceMap = sources.sourceMap?.sourcemap as any || {
226+
mappings: '',
227+
version: 3,
228+
names: [],
229+
sources: [],
230+
} satisfies FetchResult['map']
231+
232+
return await astV8ToIstanbul({
233+
code: sources.source,
234+
sourceMap,
235+
ast: parseAstAsync(sources.source) as any,
236+
coverage: { functions, url: filename },
237+
ignoreClassMethods: this.options.ignoreClassMethods,
238+
wrapperLength,
239+
},
240+
)
241+
}
242+
222243
const converter = v8ToIstanbul(
223244
filename,
224245
wrapperLength,
@@ -265,7 +286,7 @@ export class V8CoverageProvider extends BaseCoverageProvider<ResolvedCoverageOpt
265286
// If file does not exist construct a dummy source for it.
266287
// These can be files that were generated dynamically during the test run and were removed after it.
267288
const length = findLongestFunctionLength(functions)
268-
return '.'.repeat(length)
289+
return '/'.repeat(length)
269290
})
270291
}
271292

@@ -392,13 +413,17 @@ function excludeGeneratedCode(
392413

393414
if (
394415
!source.match(VITE_EXPORTS_LINE_PATTERN)
416+
&& !source.match(VITE_EXPORTS_LINE_PATTERN_2)
417+
&& !source.match(VITE_IMPORTS_LINE_PATTERN)
395418
&& !source.match(DECORATOR_METADATA_PATTERN)
396419
) {
397420
return map
398421
}
399422

400423
const trimmed = new MagicString(source)
401424
trimmed.replaceAll(VITE_EXPORTS_LINE_PATTERN, '\n')
425+
trimmed.replaceAll(VITE_IMPORTS_LINE_PATTERN, '')
426+
trimmed.replaceAll(VITE_EXPORTS_LINE_PATTERN_2, '')
402427
trimmed.replaceAll(DECORATOR_METADATA_PATTERN, match =>
403428
'\n'.repeat(match.split('\n').length - 1))
404429

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,29 @@ export interface CoverageIstanbulOptions extends BaseCoverageOptions {
270270
* @default []
271271
*/
272272
ignoreClassMethods?: string[]
273+
273274
}
274275

275276
export interface CoverageV8Options extends BaseCoverageOptions {
276277
/**
277278
* Ignore empty lines, comments and other non-runtime code, e.g. Typescript types
279+
* - Requires `experimentalAstAwareRemapping: false`
278280
*/
279281
ignoreEmptyLines?: boolean
282+
283+
/**
284+
* Remap coverage with experimental AST based analysis
285+
* - Provides more accurate results compared to default mode
286+
*/
287+
experimentalAstAwareRemapping?: boolean
288+
289+
/**
290+
* Set to array of class method names to ignore for coverage.
291+
* - Requires `experimentalAstAwareRemapping: true`
292+
*
293+
* @default []
294+
*/
295+
ignoreClassMethods?: string[]
280296
}
281297

282298
export interface CustomProviderOptions

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/coverage-test/fixtures/src/ignore-hints.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function second() {
1111
// Covered line
1212
second()
1313

14-
/* v8 ignore next -- Uncovered line v8 */
14+
/* v8 ignore next -- @preserve, Uncovered line v8 */
1515
second()
1616

1717
/* istanbul ignore next -- @preserve, Uncovered line istanbul */

test/coverage-test/test/__snapshots__/bundled-istanbul.snapshot.json

Lines changed: 0 additions & 168 deletions
This file was deleted.

0 commit comments

Comments
 (0)