Skip to content

Commit 2f7190c

Browse files
committed
feat(coverage): support ignore start/end ignore hints
1 parent 5d26b87 commit 2f7190c

9 files changed

Lines changed: 309 additions & 246 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@
7979
"@sinonjs/fake-timers@14.0.0": "patches/@sinonjs__fake-timers@14.0.0.patch",
8080
"cac@6.7.14": "patches/cac@6.7.14.patch",
8181
"@types/sinonjs__fake-timers@8.1.5": "patches/@types__sinonjs__fake-timers@8.1.5.patch",
82-
"acorn@8.11.3": "patches/acorn@8.11.3.patch"
82+
"acorn@8.11.3": "patches/acorn@8.11.3.patch",
83+
"istanbul-lib-instrument": "patches/istanbul-lib-instrument.patch"
8384
},
8485
"onlyBuiltDependencies": [
8586
"@sveltejs/kit",

packages/coverage-istanbul/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444
"vitest": "workspace:*"
4545
},
4646
"dependencies": {
47+
"@babel/core": "^7.23.9",
4748
"@istanbuljs/schema": "^0.1.3",
4849
"@jridgewell/gen-mapping": "^0.3.13",
4950
"@jridgewell/trace-mapping": "catalog:",
5051
"istanbul-lib-coverage": "catalog:",
51-
"istanbul-lib-instrument": "^6.0.3",
5252
"istanbul-lib-report": "catalog:",
5353
"istanbul-lib-source-maps": "catalog:",
5454
"istanbul-reports": "catalog:",
@@ -62,6 +62,7 @@
6262
"@types/istanbul-lib-report": "catalog:",
6363
"@types/istanbul-lib-source-maps": "catalog:",
6464
"@types/istanbul-reports": "catalog:",
65+
"istanbul-lib-instrument": "^6.0.3",
6566
"pathe": "catalog:",
6667
"vitest": "workspace:*"
6768
}

packages/coverage-istanbul/rollup.config.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const external = [
1919
...Object.keys(pkg.dependencies || {}),
2020
...Object.keys(pkg.peerDependencies || {}),
2121
/^@?vitest(\/|$)/,
22+
23+
// We bundle istanbul-lib-instrument but don't want to bundle its babel dependency
24+
'@babel/core',
2225
]
2326

2427
const dtsUtils = createDtsUtils()
@@ -27,13 +30,16 @@ const plugins = [
2730
...dtsUtils.isolatedDecl(),
2831
nodeResolve(),
2932
json(),
30-
commonjs(),
33+
commonjs({
34+
// "istanbul-lib-instrument > @jridgewell/trace-mapping" is not CJS
35+
esmExternals: ['@jridgewell/trace-mapping'],
36+
}),
3137
oxc({
3238
transform: { target: 'node18' },
3339
}),
3440
]
3541

36-
export default () => [
42+
export default [
3743
{
3844
input: entries,
3945
output: {

packages/coverage-istanbul/src/provider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
4949
// @ts-expect-error missing type
5050
importAttributesKeyword: 'with',
5151
},
52+
53+
// TODO: Add link to istanbuljs PR
54+
// custom option from the patched istanbul-lib-instrument
55+
ignoreLines: true,
5256
})
5357
}
5458

packages/coverage-v8/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"dependencies": {
5757
"@bcoe/v8-coverage": "^1.0.2",
5858
"@vitest/utils": "workspace:*",
59-
"ast-v8-to-istanbul": "^0.3.8",
59+
"ast-v8-to-istanbul": "https://pkg.pr.new/AriPerkkio/ast-v8-to-istanbul@109",
6060
"istanbul-lib-coverage": "catalog:",
6161
"istanbul-lib-report": "catalog:",
6262
"istanbul-lib-source-maps": "catalog:",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
diff --git a/CHANGELOG.md b/CHANGELOG.md
2+
deleted file mode 100644
3+
index 3c2c978099ce42ef26b9cd86d5772cff79e329af..0000000000000000000000000000000000000000
4+
diff --git a/package.json b/package.json
5+
index 5e3c704a13d642e07c6d8eb655cb052b69a99e45..743037fa385ac381b193ac7b975e5f548e064727 100644
6+
--- a/package.json
7+
+++ b/package.json
8+
@@ -13,6 +13,7 @@
9+
"dependencies": {
10+
"@babel/core": "^7.23.9",
11+
"@babel/parser": "^7.23.9",
12+
+ "@jridgewell/trace-mapping": "^0.3.23",
13+
"@istanbuljs/schema": "^0.1.3",
14+
"istanbul-lib-coverage": "^3.2.0",
15+
"semver": "^7.5.4"
16+
diff --git a/src/ignored-lines.js b/src/ignored-lines.js
17+
new file mode 100644
18+
index 0000000000000000000000000000000000000000..17f0946537835bd52e005e27c4ce5513efe3a117
19+
--- /dev/null
20+
+++ b/src/ignored-lines.js
21+
@@ -0,0 +1,62 @@
22+
+const { EOL } = require('os');
23+
+
24+
+const IGNORE_LINES_PATTERN = /\s*istanbul\s+ignore\s+(start|end)/;
25+
+
26+
+/**
27+
+ * Parse ignore start/end hints from **text file** based on regular expressions
28+
+ * - Does not understand what a comment is in Javascript (or JSX, Vue, Svelte)
29+
+ * - Parses source code (JS, TS, Vue, Svelte, anything) based on text search by
30+
+ * matching for `/* istanbul ignore start *\/` pattern - not by looking for real comments
31+
+ *
32+
+ * ```js
33+
+ * /* istanbul ignore start *\/
34+
+ * <!-- /* istanbul ignore start *\/ -->
35+
+ * <SomeFrameworkComment content="/* istanbul ignore start *\/">
36+
+ * ```
37+
+ */
38+
+function getIgnoredLines(text) {
39+
+ if (!text) {
40+
+ return new Set();
41+
+ }
42+
+
43+
+ const ranges = [];
44+
+ let lineNumber = 0;
45+
+
46+
+ for (const line of text.split(EOL)) {
47+
+ lineNumber++;
48+
+
49+
+ const match = line.match(IGNORE_LINES_PATTERN);
50+
+ if (match) {
51+
+ const type = match[1];
52+
+
53+
+ if (type === 'end') {
54+
+ const previous = ranges.at(-1);
55+
+
56+
+ // Ignore whole "ignore end" if no previous start was found
57+
+ if (previous && previous.end === Infinity) {
58+
+ previous.end = lineNumber;
59+
+ }
60+
+
61+
+ continue;
62+
+ }
63+
+
64+
+ ranges.push({ start: lineNumber, end: Infinity });
65+
+ }
66+
+ }
67+
+
68+
+ const ignoredLines = new Set();
69+
+
70+
+ for (const range of ranges) {
71+
+ for (let line = range.start; line <= range.end; line++) {
72+
+ ignoredLines.add(line);
73+
+
74+
+ if (line >= lineNumber) {
75+
+ break;
76+
+ }
77+
+ }
78+
+ }
79+
+
80+
+ return ignoredLines;
81+
+}
82+
+
83+
+module.exports = { getIgnoredLines };
84+
diff --git a/src/instrumenter.js b/src/instrumenter.js
85+
index ffc4387b9ba9477bdce3823760400fddb021637d..39f5ee8fede4a6d724b28050e1dd1e1942bd665a 100644
86+
--- a/src/instrumenter.js
87+
+++ b/src/instrumenter.js
88+
@@ -21,6 +21,7 @@ const readInitialCoverage = require('./read-coverage');
89+
* @param {boolean} [opts.autoWrap=false] set to true to allow `return` statements outside of functions.
90+
* @param {boolean} [opts.produceSourceMap=false] set to true to produce a source map for the instrumented code.
91+
* @param {Array} [opts.ignoreClassMethods=[]] set to array of class method names to ignore for coverage.
92+
+ * @param {Array} [opts.ignoreLines=false] enable ignore hints for lines (start, end).
93+
* @param {Function} [opts.sourceMapUrlCallback=null] a callback function that is called when a source map URL
94+
* is found in the original code. This function is called with the source file name and the source map URL.
95+
* @param {boolean} [opts.debug=false] - turn debugging on.
96+
@@ -83,6 +84,7 @@ class Instrumenter {
97+
coverageGlobalScopeFunc:
98+
opts.coverageGlobalScopeFunc,
99+
ignoreClassMethods: opts.ignoreClassMethods,
100+
+ ignoreLines: opts.ignoreLines,
101+
inputSourceMap
102+
});
103+
104+
diff --git a/src/visitor.js b/src/visitor.js
105+
index 04e3115f832799fad6d141e8b0aeaa61ac5c98f9..88f8d2420daabecef2ad2def18c8be245b60e253 100644
106+
--- a/src/visitor.js
107+
+++ b/src/visitor.js
108+
@@ -1,8 +1,17 @@
109+
+const { readFileSync } = require('fs');
110+
const { createHash } = require('crypto');
111+
const { template } = require('@babel/core');
112+
+const {
113+
+ originalPositionFor,
114+
+ TraceMap,
115+
+ GREATEST_LOWER_BOUND,
116+
+ LEAST_UPPER_BOUND,
117+
+ sourceContentFor
118+
+} = require('@jridgewell/trace-mapping');
119+
const { defaults } = require('@istanbuljs/schema');
120+
const { SourceCoverage } = require('./source-coverage');
121+
const { SHA, MAGIC_KEY, MAGIC_VALUE } = require('./constants');
122+
+const { getIgnoredLines } = require('./ignored-lines');
123+
124+
// pattern for istanbul to ignore a section
125+
const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
126+
@@ -26,7 +35,8 @@ class VisitState {
127+
sourceFilePath,
128+
inputSourceMap,
129+
ignoreClassMethods = [],
130+
- reportLogic = false
131+
+ reportLogic = false,
132+
+ ignoreLines = false
133+
) {
134+
this.varName = genVar(sourceFilePath);
135+
this.attrs = {};
136+
@@ -35,8 +45,13 @@ class VisitState {
137+
138+
if (typeof inputSourceMap !== 'undefined') {
139+
this.cov.inputSourceMap(inputSourceMap);
140+
+
141+
+ if (ignoreLines) {
142+
+ this.traceMap = new TraceMap(inputSourceMap);
143+
+ }
144+
}
145+
this.ignoreClassMethods = ignoreClassMethods;
146+
+ this.ignoredLines = new Map();
147+
this.types = types;
148+
this.sourceMappingURL = null;
149+
this.reportLogic = reportLogic;
150+
@@ -45,7 +60,42 @@ class VisitState {
151+
// should we ignore the node? Yes, if specifically ignoring
152+
// or if the node is generated.
153+
shouldIgnore(path) {
154+
- return this.nextIgnore || !path.node.loc;
155+
+ if (this.nextIgnore || !path.node.loc) {
156+
+ return true;
157+
+ }
158+
+
159+
+ if (!this.traceMap) {
160+
+ return false;
161+
+ }
162+
+
163+
+ // Anything that starts between the line ignore hints is ignored
164+
+ const start = originalPositionTryBoth(
165+
+ this.traceMap,
166+
+ path.node.loc.start
167+
+ );
168+
+
169+
+ // Generated code
170+
+ if (start.line == null) {
171+
+ return false;
172+
+ }
173+
+
174+
+ const filename = start.source;
175+
+ let ignoredLines = this.ignoredLines.get(filename);
176+
+
177+
+ if (!ignoredLines) {
178+
+ const sources = sourceContentFor(this.traceMap, filename);
179+
+ ignoredLines = getIgnoredLines(
180+
+ sources || tryReadFileSync(filename)
181+
+ );
182+
+
183+
+ this.ignoredLines.set(filename, ignoredLines);
184+
+ }
185+
+
186+
+ if (ignoredLines.has(start.line)) {
187+
+ return true;
188+
+ }
189+
+
190+
+ return false;
191+
}
192+
193+
// extract the ignore comment hint (next|if|else) or null
194+
@@ -742,6 +792,7 @@ function shouldIgnoreFile(programNode) {
195+
* @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope.
196+
* @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope.
197+
* @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes.
198+
+ * @param {Array} [opts.ignoreLines=false] enable ignore hints for lines (start, end).
199+
* @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the
200+
* original code.
201+
*/
202+
@@ -756,7 +807,8 @@ function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
203+
sourceFilePath,
204+
opts.inputSourceMap,
205+
opts.ignoreClassMethods,
206+
- opts.reportLogic
207+
+ opts.reportLogic,
208+
+ opts.ignoreLines
209+
);
210+
return {
211+
enter(path) {
212+
@@ -840,4 +892,29 @@ function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
213+
};
214+
}
215+
216+
+function originalPositionTryBoth(sourceMap, { line, column }) {
217+
+ const mapping = originalPositionFor(sourceMap, {
218+
+ line,
219+
+ column,
220+
+ bias: GREATEST_LOWER_BOUND
221+
+ });
222+
+ if (mapping.source === null) {
223+
+ return originalPositionFor(sourceMap, {
224+
+ line,
225+
+ column,
226+
+ bias: LEAST_UPPER_BOUND
227+
+ });
228+
+ } else {
229+
+ return mapping;
230+
+ }
231+
+}
232+
+
233+
+function tryReadFileSync(filename) {
234+
+ try {
235+
+ return readFileSync(filename, 'utf8');
236+
+ } catch (_) {
237+
+ return undefined;
238+
+ }
239+
+}
240+
+
241+
module.exports = programVisitor;

0 commit comments

Comments
 (0)