Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/guide/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ Comments which are considered as [legal comments](https://esbuild.github.io/api/
You can include a `@preserve` keyword in the ignore hint.
Beware that these ignore hints may now be included in final production build as well.

::: tip
Follow https://github.com/vitest-dev/vitest/issues/2021 for updates about `@preserve` usage.
:::

```diff
-/* istanbul ignore if */
+/* istanbul ignore if -- @preserve */
Expand All @@ -364,6 +368,30 @@ if (condition) {

::: code-group

```ts [lines: start/stop]
/* istanbul ignore start -- @preserve */
if (parameter) { // [!code error]
console.log('Ignored') // [!code error]
} // [!code error]
else { // [!code error]
console.log('Ignored') // [!code error]
} // [!code error]
/* istanbul ignore stop -- @preserve */

console.log('Included')

/* v8 ignore start -- @preserve */
if (parameter) { // [!code error]
console.log('Ignored') // [!code error]
} // [!code error]
else { // [!code error]
console.log('Ignored') // [!code error]
} // [!code error]
/* v8 ignore stop -- @preserve */

console.log('Included')
```

```ts [if else]
/* v8 ignore if -- @preserve */
if (parameter) { // [!code error]
Expand Down
3 changes: 2 additions & 1 deletion packages/coverage-istanbul/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
"vitest": "workspace:*"
},
"dependencies": {
"@babel/core": "^7.23.9",
"@istanbuljs/schema": "^0.1.3",
"@jridgewell/gen-mapping": "^0.3.13",
"@jridgewell/trace-mapping": "catalog:",
"istanbul-lib-coverage": "catalog:",
"istanbul-lib-instrument": "^6.0.3",
"istanbul-lib-report": "catalog:",
"istanbul-reports": "catalog:",
"magicast": "catalog:",
Expand All @@ -61,6 +61,7 @@
"@types/istanbul-lib-report": "catalog:",
"@types/istanbul-lib-source-maps": "catalog:",
"@types/istanbul-reports": "catalog:",
"istanbul-lib-instrument": "^6.0.3",
"istanbul-lib-source-maps": "catalog:",
"pathe": "catalog:",
"vitest": "workspace:*"
Expand Down
4 changes: 4 additions & 0 deletions packages/coverage-istanbul/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const external = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
/^@?vitest(\/|$)/,

// We bundle istanbul-lib-instrument but don't want to bundle its babel dependency
'@babel/core',
Comment on lines +23 to +24
Copy link
Copy Markdown
Member Author

@AriPerkkio AriPerkkio Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundling babel seemed tricky. Let's leave it external as we only care about istanbul-lib-instrument.

]

const dtsUtils = createDtsUtils()
Expand All @@ -29,6 +32,7 @@ const plugins = [
json(),
commonjs({
// "istanbul-lib-source-maps > @jridgewell/trace-mapping" is not CJS
// "istanbul-lib-instrument > @jridgewell/trace-mapping" is not CJS
Comment thread
AriPerkkio marked this conversation as resolved.
esmExternals: ['@jridgewell/trace-mapping'],
}),
oxc({
Expand Down
3 changes: 3 additions & 0 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
// @ts-expect-error missing type
importAttributesKeyword: 'with',
},

// Custom option from the patched istanbul-lib-instrument: https://github.com/istanbuljs/istanbuljs/pull/835
ignoreLines: true,
})
}

Expand Down
240 changes: 240 additions & 0 deletions patches/istanbul-lib-instrument.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 3c2c978099ce42ef26b9cd86d5772cff79e329af..0000000000000000000000000000000000000000
diff --git a/package.json b/package.json
index 5e3c704a13d642e07c6d8eb655cb052b69a99e45..743037fa385ac381b193ac7b975e5f548e064727 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@babel/core": "^7.23.9",
"@babel/parser": "^7.23.9",
+ "@jridgewell/trace-mapping": "^0.3.23",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-coverage": "^3.2.0",
"semver": "^7.5.4"
diff --git a/src/ignored-lines.js b/src/ignored-lines.js
new file mode 100644
index 0000000000000000000000000000000000000000..5392b2b17165b3bbd52871bb931be1306f7fabe1
--- /dev/null
+++ b/src/ignored-lines.js
@@ -0,0 +1,61 @@
+const IGNORE_LINES_PATTERN = /\s*istanbul\s+ignore\s+(start|stop)/;
+const EOL_PATTERN = /\r?\n/g;
+
+/**
+ * Parse ignore start/stop hints from **text file** based on regular expressions
+ * - Does not understand what a comment is in Javascript (or JSX, Vue, Svelte)
+ * - Parses source code (JS, TS, Vue, Svelte, anything) based on text search by
+ * matching for `/* istanbul ignore start *\/` pattern - not by looking for real comments
+ *
+ * ```js
+ * /* istanbul ignore start *\/
+ * <!-- /* istanbul ignore start *\/ -->
+ * <SomeFrameworkComment content="/* istanbul ignore start *\/">
+ * ```
+ */
+function getIgnoredLines(text) {
+ if (!text) {
+ return new Set();
+ }
+
+ const ranges = [];
+ let lineNumber = 0;
+
+ for (const line of text.split(EOL_PATTERN)) {
+ lineNumber++;
+
+ const match = line.match(IGNORE_LINES_PATTERN);
+ if (match) {
+ const type = match[1];
+
+ if (type === 'stop') {
+ const previous = ranges.at(-1);
+
+ // Ignore whole "ignore stop" if no previous start was found
+ if (previous && previous.stop === Infinity) {
+ previous.stop = lineNumber;
+ }
+
+ continue;
+ }
+
+ ranges.push({ start: lineNumber, stop: Infinity });
+ }
+ }
+
+ const ignoredLines = new Set();
+
+ for (const range of ranges) {
+ for (let line = range.start; line <= range.stop; line++) {
+ ignoredLines.add(line);
+
+ if (line >= lineNumber) {
+ break;
+ }
+ }
+ }
+
+ return ignoredLines;
+}
+
+module.exports = { getIgnoredLines };
diff --git a/src/instrumenter.js b/src/instrumenter.js
index ffc4387b9ba9477bdce3823760400fddb021637d..39f5ee8fede4a6d724b28050e1dd1e1942bd665a 100644
--- a/src/instrumenter.js
+++ b/src/instrumenter.js
@@ -21,6 +21,7 @@ const readInitialCoverage = require('./read-coverage');
* @param {boolean} [opts.autoWrap=false] set to true to allow `return` statements outside of functions.
* @param {boolean} [opts.produceSourceMap=false] set to true to produce a source map for the instrumented code.
* @param {Array} [opts.ignoreClassMethods=[]] set to array of class method names to ignore for coverage.
+ * @param {Array} [opts.ignoreLines=false] enable ignore hints for lines (start, end).
* @param {Function} [opts.sourceMapUrlCallback=null] a callback function that is called when a source map URL
* is found in the original code. This function is called with the source file name and the source map URL.
* @param {boolean} [opts.debug=false] - turn debugging on.
@@ -83,6 +84,7 @@ class Instrumenter {
coverageGlobalScopeFunc:
opts.coverageGlobalScopeFunc,
ignoreClassMethods: opts.ignoreClassMethods,
+ ignoreLines: opts.ignoreLines,
inputSourceMap
});

diff --git a/src/visitor.js b/src/visitor.js
index 04e3115f832799fad6d141e8b0aeaa61ac5c98f9..88f8d2420daabecef2ad2def18c8be245b60e253 100644
--- a/src/visitor.js
+++ b/src/visitor.js
@@ -1,8 +1,17 @@
+const { readFileSync } = require('fs');
const { createHash } = require('crypto');
const { template } = require('@babel/core');
+const {
+ originalPositionFor,
+ TraceMap,
+ GREATEST_LOWER_BOUND,
+ LEAST_UPPER_BOUND,
+ sourceContentFor
+} = require('@jridgewell/trace-mapping');
const { defaults } = require('@istanbuljs/schema');
const { SourceCoverage } = require('./source-coverage');
const { SHA, MAGIC_KEY, MAGIC_VALUE } = require('./constants');
+const { getIgnoredLines } = require('./ignored-lines');

// pattern for istanbul to ignore a section
const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
@@ -26,7 +35,8 @@ class VisitState {
sourceFilePath,
inputSourceMap,
ignoreClassMethods = [],
- reportLogic = false
+ reportLogic = false,
+ ignoreLines = false
) {
this.varName = genVar(sourceFilePath);
this.attrs = {};
@@ -35,8 +45,13 @@ class VisitState {

if (typeof inputSourceMap !== 'undefined') {
this.cov.inputSourceMap(inputSourceMap);
+
+ if (ignoreLines) {
+ this.traceMap = new TraceMap(inputSourceMap);
+ }
}
this.ignoreClassMethods = ignoreClassMethods;
+ this.ignoredLines = new Map();
this.types = types;
this.sourceMappingURL = null;
this.reportLogic = reportLogic;
@@ -45,7 +60,42 @@ class VisitState {
// should we ignore the node? Yes, if specifically ignoring
// or if the node is generated.
shouldIgnore(path) {
- return this.nextIgnore || !path.node.loc;
+ if (this.nextIgnore || !path.node.loc) {
+ return true;
+ }
+
+ if (!this.traceMap) {
+ return false;
+ }
+
+ // Anything that starts between the line ignore hints is ignored
+ const start = originalPositionTryBoth(
+ this.traceMap,
+ path.node.loc.start
+ );
+
+ // Generated code
+ if (start.line == null) {
+ return false;
+ }
+
+ const filename = start.source;
+ let ignoredLines = this.ignoredLines.get(filename);
+
+ if (!ignoredLines) {
+ const sources = sourceContentFor(this.traceMap, filename);
+ ignoredLines = getIgnoredLines(
+ sources || tryReadFileSync(filename)
+ );
+
+ this.ignoredLines.set(filename, ignoredLines);
+ }
+
+ if (ignoredLines.has(start.line)) {
+ return true;
+ }
+
+ return false;
}

// extract the ignore comment hint (next|if|else) or null
@@ -742,6 +792,7 @@ function shouldIgnoreFile(programNode) {
* @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope.
* @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope.
* @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes.
+ * @param {Array} [opts.ignoreLines=false] enable ignore hints for lines (start, end).
* @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the
* original code.
*/
@@ -756,7 +807,8 @@ function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
sourceFilePath,
opts.inputSourceMap,
opts.ignoreClassMethods,
- opts.reportLogic
+ opts.reportLogic,
+ opts.ignoreLines
);
return {
enter(path) {
@@ -840,4 +892,29 @@ function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
};
}

+function originalPositionTryBoth(sourceMap, { line, column }) {
+ const mapping = originalPositionFor(sourceMap, {
+ line,
+ column,
+ bias: GREATEST_LOWER_BOUND
+ });
+ if (mapping.source === null) {
+ return originalPositionFor(sourceMap, {
+ line,
+ column,
+ bias: LEAST_UPPER_BOUND
+ });
+ } else {
+ return mapping;
+ }
+}
+
+function tryReadFileSync(filename) {
+ try {
+ return readFileSync(filename, 'utf8');
+ } catch (_) {
+ return undefined;
+ }
+}
+
module.exports = programVisitor;
Loading
Loading