@teamscale/javascript-instrumenter 0.0.1-beta.5 → 0.0.1-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -18,14 +22,22 @@ var __importStar = (this && this.__importStar) || function (mod) {
18
22
  __setModuleDefault(result, mod);
19
23
  return result;
20
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
21
28
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.IstanbulInstrumenter = exports.IS_INSTRUMENTED_TOKEN = void 0;
29
+ exports.sourceMapFromMapFile = exports.sourceMapFromCodeComment = exports.loadInputSourceMap = exports.loadSourceMap = exports.IstanbulInstrumenter = exports.IS_INSTRUMENTED_TOKEN = void 0;
23
30
  const Task_1 = require("./Task");
24
31
  const commons_1 = require("@cqse/commons");
32
+ const source_map_1 = require("source-map");
25
33
  const istanbul = __importStar(require("istanbul-lib-instrument"));
26
34
  const fs = __importStar(require("fs"));
35
+ const mkdirp = __importStar(require("mkdirp"));
27
36
  const path = __importStar(require("path"));
28
37
  const convertSourceMap = __importStar(require("convert-source-map"));
38
+ const Postprocessor_1 = require("./Postprocessor");
39
+ const typescript_optional_1 = require("typescript-optional");
40
+ const async_1 = __importDefault(require("async"));
29
41
  exports.IS_INSTRUMENTED_TOKEN = '/** $IS_JS_PROFILER_INSTRUMENTED=true **/';
30
42
  /**
31
43
  * An instrumenter based on the IstanbulJs instrumentation and coverage framework.
@@ -39,14 +51,19 @@ class IstanbulInstrumenter {
39
51
  /**
40
52
  * {@inheritDoc #IInstrumenter.instrument}
41
53
  */
42
- instrument(task) {
43
- fs.existsSync(this.vaccineFilePath);
44
- // ATTENTION: Here is potential for parallelization. Maybe we can
45
- // run several instrumentation workers in parallel?
46
- const result = task.elements
47
- .map(e => this.instrumentOne(task.collector, e, task.originSourcePattern))
48
- .reduce((prev, current) => current.withIncrement(prev), Task_1.TaskResult.neutral());
49
- return Promise.resolve(result);
54
+ async instrument(task) {
55
+ this.clearDumpOriginsFileIfNeeded(task.dumpOriginsFile);
56
+ // We limit the number of instrumentations in parallel to one to
57
+ // not overuse memory (NodeJS has only limited mem to use).
58
+ return async_1.default
59
+ .mapLimit(task.elements, 1, async (taskElement) => {
60
+ return await this.instrumentOne(task.collector, taskElement, task.excludeFilesPattern, task.originSourcePattern, task.dumpOriginsFile);
61
+ })
62
+ .then(results => {
63
+ return results.reduce((prev, curr) => {
64
+ return prev.withIncrement(curr);
65
+ }, Task_1.TaskResult.neutral());
66
+ });
50
67
  }
51
68
  /**
52
69
  * Perform the instrumentation for one given task element (file to instrument).
@@ -54,20 +71,28 @@ class IstanbulInstrumenter {
54
71
  * @param collector - The collector to send the coverage information to.
55
72
  * @param taskElement - The task element to perform the instrumentation for.
56
73
  * @param sourcePattern - A pattern to restrict the instrumentation to only a fraction of the task element.
74
+ * @param dumpOriginsFile - A file path where all origins from the source map should be dumped in json format, or undefined if no origins should be dumped
57
75
  */
58
- instrumentOne(collector, taskElement, sourcePattern) {
59
- var _a, _b, _c;
76
+ async instrumentOne(collector, taskElement, excludeBundles, sourcePattern, dumpOriginsFile) {
77
+ var _a, _b;
60
78
  const inputFileSource = fs.readFileSync(taskElement.fromFile, 'utf8');
61
79
  // We skip files that we have already instrumented
62
80
  if (inputFileSource.startsWith(exports.IS_INSTRUMENTED_TOKEN)) {
63
81
  if (!taskElement.isInPlace()) {
64
- fs.writeFileSync(taskElement.toFile, inputFileSource);
82
+ writeToFile(taskElement.toFile, inputFileSource);
65
83
  }
66
- return new Task_1.TaskResult(0, 0, 1, 0, 0, 0);
84
+ return new Task_1.TaskResult(0, 0, 0, 1, 0, 0, 0);
67
85
  }
68
86
  // Not all file types are supported by the instrumenter
69
87
  if (!this.isFileTypeSupported(taskElement.fromFile)) {
70
- return new Task_1.TaskResult(0, 0, 0, 1, 0, 0);
88
+ return new Task_1.TaskResult(0, 0, 0, 0, 1, 0, 0);
89
+ }
90
+ // We might want to skip the instrumentation of the file
91
+ if (excludeBundles.isExcluded(taskElement.fromFile)) {
92
+ if (!taskElement.isInPlace()) {
93
+ writeToFile(taskElement.toFile, inputFileSource);
94
+ }
95
+ return new Task_1.TaskResult(0, 1, 0, 0, 0, 0, 0);
71
96
  }
72
97
  // Report progress
73
98
  this.logger.info(`Instrumenting "${path.basename(taskElement.fromFile)}"`);
@@ -77,29 +102,37 @@ class IstanbulInstrumenter {
77
102
  // alternative configurations of the instrumenter.
78
103
  const configurationAlternatives = this.configurationAlternativesFor(taskElement);
79
104
  for (let i = 0; i < configurationAlternatives.length; i++) {
105
+ const configurationAlternative = configurationAlternatives[i];
80
106
  let inputSourceMap;
81
107
  try {
82
- const instrumenter = istanbul.createInstrumenter(configurationAlternatives[i]);
83
- inputSourceMap = this.loadInputSourceMap(inputFileSource, taskElement);
108
+ const instrumenter = istanbul.createInstrumenter(configurationAlternative);
109
+ inputSourceMap = loadInputSourceMap(inputFileSource, taskElement.fromFile, taskElement.externalSourceMapFile);
84
110
  // Based on the source maps of the file to instrument, we can now
85
111
  // decide if we should NOT write an instrumented version of it
86
112
  // and use the original code instead and write it to the target path.
87
113
  //
88
- if (this.shouldExcludeFromInstrumentation(sourcePattern, taskElement.fromFile, (_b = (_a = instrumenter.lastSourceMap()) === null || _a === void 0 ? void 0 : _a.sources) !== null && _b !== void 0 ? _b : [])) {
89
- fs.writeFileSync(taskElement.toFile, inputFileSource);
90
- return new Task_1.TaskResult(1, 0, 0, 0, 0, 0);
114
+ const originSourceFiles = (_a = inputSourceMap === null || inputSourceMap === void 0 ? void 0 : inputSourceMap.sources) !== null && _a !== void 0 ? _a : [];
115
+ if (dumpOriginsFile) {
116
+ this.dumpOrigins(dumpOriginsFile, originSourceFiles);
91
117
  }
92
118
  // The main instrumentation (adding coverage statements) is performed now:
93
- instrumentedSource = instrumenter
94
- .instrumentSync(inputFileSource, taskElement.fromFile, inputSourceMap)
95
- .replace(/return actualCoverage/g, 'return makeCoverageInterceptor(actualCoverage)')
119
+ instrumentedSource = instrumenter.instrumentSync(inputFileSource, taskElement.fromFile, inputSourceMap);
120
+ this.logger.debug('Instrumentation source maps to:', (_b = instrumenter.lastSourceMap()) === null || _b === void 0 ? void 0 : _b.sources);
121
+ // In case of a bundle, the initial instrumentation step might have added
122
+ // too much and undesired instrumentations. Remove them now.
123
+ const instrumentedSourcemap = instrumenter.lastSourceMap();
124
+ let instrumentedAndCleanedSource = await this.removeUnwantedInstrumentation(taskElement, instrumentedSource, configurationAlternative, sourcePattern, instrumentedSourcemap);
125
+ instrumentedAndCleanedSource = instrumentedAndCleanedSource
126
+ .replace(/actualCoverage\s*=\s*coverage\[path\]/g, 'actualCoverage=_$registerCoverageObject(coverage[path])')
96
127
  .replace(/new Function\("return this"\)\(\)/g, "typeof window === 'object' ? window : this");
97
- this.logger.debug('Instrumentation source maps to:', (_c = instrumenter.lastSourceMap()) === null || _c === void 0 ? void 0 : _c.sources);
98
128
  // The process also can result in a new source map that we will append in the result.
99
129
  //
100
130
  // `lastSourceMap` === Sourcemap for the last file that was instrumented.
101
131
  finalSourceMap = convertSourceMap.fromObject(instrumenter.lastSourceMap()).toComment();
102
- break;
132
+ // We now can glue together the final version of the instrumented file.
133
+ const vaccineSource = this.loadVaccine(collector);
134
+ writeToFile(taskElement.toFile, `${exports.IS_INSTRUMENTED_TOKEN} ${vaccineSource} ${instrumentedAndCleanedSource} \n${finalSourceMap}`);
135
+ return new Task_1.TaskResult(1, 0, 0, 0, 0, 0, 0);
103
136
  }
104
137
  catch (e) {
105
138
  // If also the last configuration alternative failed,
@@ -108,16 +141,42 @@ class IstanbulInstrumenter {
108
141
  if (!inputSourceMap) {
109
142
  return Task_1.TaskResult.warning(`Failed loading input source map for ${taskElement.fromFile}: ${e.message}`);
110
143
  }
111
- fs.writeFileSync(taskElement.toFile, inputFileSource);
144
+ writeToFile(taskElement.toFile, inputFileSource);
112
145
  return Task_1.TaskResult.error(e);
113
146
  }
114
147
  }
115
148
  }
116
- // We now can glue together the final version of the instrumented file.
117
- //
118
- const vaccineSource = this.loadVaccine(collector);
119
- fs.writeFileSync(taskElement.toFile, `${exports.IS_INSTRUMENTED_TOKEN} ${vaccineSource} ${instrumentedSource} \n${finalSourceMap}`);
120
- return new Task_1.TaskResult(1, 0, 0, 0, 0, 0);
149
+ return new Task_1.TaskResult(0, 0, 0, 0, 0, 1, 0);
150
+ }
151
+ async removeUnwantedInstrumentation(taskElement, instrumentedSource, configurationAlternative, sourcePattern, instrumentedSourcemap) {
152
+ const instrumentedSourceMapConsumer = await new source_map_1.SourceMapConsumer(instrumentedSourcemap);
153
+ // Without a source map, excludes/includes do not work.
154
+ if (!instrumentedSourceMapConsumer) {
155
+ return instrumentedSource;
156
+ }
157
+ const removedInstrumentationFor = new Set();
158
+ // Remove the unwanted instrumentation
159
+ const cleaned = (0, Postprocessor_1.cleanSourceCode)(instrumentedSource, configurationAlternative.esModules, location => {
160
+ const originalPosition = instrumentedSourceMapConsumer.originalPositionFor({
161
+ line: location.start.line,
162
+ column: location.start.column
163
+ });
164
+ if (!originalPosition.source) {
165
+ return false;
166
+ }
167
+ const isToCover = sourcePattern.isAnyIncluded([originalPosition.source]);
168
+ if (!isToCover) {
169
+ removedInstrumentationFor.add(originalPosition.source);
170
+ }
171
+ return isToCover;
172
+ });
173
+ if (removedInstrumentationFor.size) {
174
+ this.logger.info(`Removed from ${taskElement.toFile} instrumentation for:`);
175
+ removedInstrumentationFor.forEach(entry => this.logger.info(entry));
176
+ }
177
+ // Explicitly free the source map to avoid memory leaks
178
+ instrumentedSourceMapConsumer.destroy();
179
+ return cleaned;
121
180
  }
122
181
  /**
123
182
  * Loads the vaccine from the vaccine file and adjusts some template parameters.
@@ -125,23 +184,9 @@ class IstanbulInstrumenter {
125
184
  * @param collector - The collector to send coverage information to.
126
185
  */
127
186
  loadVaccine(collector) {
128
- // We first replace some of the parameters in the file with the
187
+ // We first replace parameters in the file with the
129
188
  // actual values, for example, the collector to send the coverage information to.
130
- return fs
131
- .readFileSync(this.vaccineFilePath, 'utf8')
132
- .replace(/\$REPORT_TO_HOST/g, collector.host)
133
- .replace(/\$REPORT_TO_PORT/g, `${collector.port}`);
134
- }
135
- /**
136
- * Should the given file be excluded from the instrumentation,
137
- * based on the source files that have been transpiled into it?
138
- *
139
- * @param pattern - The pattern to match the origin source files.
140
- * @param sourceFile - The bundle file name.
141
- * @param originSourceFiles - The list of files that were transpiled into the bundle.
142
- */
143
- shouldExcludeFromInstrumentation(pattern, sourceFile, originSourceFiles) {
144
- return !pattern.isAnyIncluded(originSourceFiles);
189
+ return fs.readFileSync(this.vaccineFilePath, 'utf8').replace(/\$REPORT_TO_URL/g, collector.url);
145
190
  }
146
191
  /**
147
192
  * @returns whether the given file is supported for instrumentation.
@@ -158,33 +203,69 @@ class IstanbulInstrumenter {
158
203
  this.logger.debug(`Determining configuration alternatives for ${taskElement.fromFile}`);
159
204
  const baseConfig = {
160
205
  coverageVariable: '__coverage__',
161
- produceSourceMap: true
206
+ produceSourceMap: 'both'
162
207
  };
163
208
  return [
164
209
  { ...baseConfig, ...{ esModules: true } },
165
210
  { ...baseConfig, ...{ esModules: false } }
166
211
  ];
167
212
  }
168
- /**
169
- * Given a source code file and the task element, load the corresponding sourcemap.
170
- *
171
- * @param inputSource - The source code that might contain sourcemap comments.
172
- * @param taskElement - The task element that can have a reference to an external sourcemap.
173
- */
174
- loadInputSourceMap(inputSource, taskElement) {
175
- if (taskElement.externalSourceMapFile.isPresent()) {
176
- const sourceMapOrigin = taskElement.externalSourceMapFile.get();
177
- if (!(sourceMapOrigin instanceof Task_1.SourceMapFileReference)) {
178
- throw new commons_1.IllegalArgumentException('Type of source map not yet supported!');
213
+ /** Appends all origins from the source map to a given file. Creates the file if it does not exist yet. */
214
+ dumpOrigins(dumpOriginsFile, originSourceFiles) {
215
+ const jsonContent = JSON.stringify(originSourceFiles, null, 2);
216
+ fs.writeFile(dumpOriginsFile, jsonContent + '\n', { flag: 'a' }, error => {
217
+ if (error) {
218
+ this.logger.warn('Could not dump origins file');
219
+ }
220
+ });
221
+ }
222
+ /** Clears the dump origins file if it exists, such that it is now ready to be appended for every instrumented file. */
223
+ clearDumpOriginsFileIfNeeded(dumpOriginsFile) {
224
+ if (dumpOriginsFile && fs.existsSync(dumpOriginsFile)) {
225
+ try {
226
+ fs.unlinkSync(dumpOriginsFile);
227
+ }
228
+ catch (err) {
229
+ this.logger.warn('Could not clear origins file: ' + err);
179
230
  }
180
- return sourceMapFromMapFile(sourceMapOrigin.sourceMapFilePath);
181
- }
182
- else {
183
- return sourceMapFromCodeComment(inputSource, taskElement.fromFile);
184
231
  }
185
232
  }
186
233
  }
187
234
  exports.IstanbulInstrumenter = IstanbulInstrumenter;
235
+ /**
236
+ * Extract the sourcemap from the given source code.
237
+ *
238
+ * @param instrumentedSource - The source code.
239
+ * @param instrumentedSourceFileName - The file name to assume for the file name.
240
+ */
241
+ async function loadSourceMap(instrumentedSource, instrumentedSourceFileName) {
242
+ const instrumentedSourceMap = loadInputSourceMap(instrumentedSource, instrumentedSourceFileName, typescript_optional_1.Optional.empty());
243
+ if (instrumentedSourceMap) {
244
+ return await new source_map_1.SourceMapConsumer(instrumentedSourceMap);
245
+ }
246
+ return undefined;
247
+ }
248
+ exports.loadSourceMap = loadSourceMap;
249
+ /**
250
+ * Given a source code file, load the corresponding sourcemap.
251
+ *
252
+ * @param inputSource - The source code that might contain sourcemap comments.
253
+ * @param taskFile - The name of the file the `inputSource` is from.
254
+ * @param externalSourceMapFile - An external source map file to consider.
255
+ */
256
+ function loadInputSourceMap(inputSource, taskFile, externalSourceMapFile) {
257
+ if (externalSourceMapFile.isPresent()) {
258
+ const sourceMapOrigin = externalSourceMapFile.get();
259
+ if (!(sourceMapOrigin instanceof Task_1.SourceMapFileReference)) {
260
+ throw new commons_1.IllegalArgumentException('Type of source map not yet supported!');
261
+ }
262
+ return sourceMapFromMapFile(sourceMapOrigin.sourceMapFilePath);
263
+ }
264
+ else {
265
+ return sourceMapFromCodeComment(inputSource, taskFile);
266
+ }
267
+ }
268
+ exports.loadInputSourceMap = loadInputSourceMap;
188
269
  /**
189
270
  * Extract a sourcemap for a given code comment.
190
271
  *
@@ -218,11 +299,11 @@ function sourceMapFromCodeComment(sourcecode, sourceFilePath) {
218
299
  // One JS file can refer to several source map files in its comments.
219
300
  failedLoading++;
220
301
  }
221
- if (result) {
222
- return result;
223
- }
224
302
  }
225
303
  } while (matched);
304
+ if (result) {
305
+ return result;
306
+ }
226
307
  if (failedLoading > 0) {
227
308
  throw new commons_1.IllegalArgumentException('None of the referenced source map files loaded!');
228
309
  }
@@ -230,6 +311,7 @@ function sourceMapFromCodeComment(sourcecode, sourceFilePath) {
230
311
  return undefined;
231
312
  }
232
313
  }
314
+ exports.sourceMapFromCodeComment = sourceMapFromCodeComment;
233
315
  /**
234
316
  * Read a source map from a source map file.
235
317
  *
@@ -239,3 +321,8 @@ function sourceMapFromMapFile(mapFilePath) {
239
321
  const content = fs.readFileSync(mapFilePath, 'utf8');
240
322
  return JSON.parse(content);
241
323
  }
324
+ exports.sourceMapFromMapFile = sourceMapFromMapFile;
325
+ function writeToFile(filePath, fileContent) {
326
+ mkdirp.sync(path.dirname(filePath));
327
+ fs.writeFileSync(filePath, fileContent);
328
+ }
@@ -0,0 +1,9 @@
1
+ import { SourceLocation } from '@babel/types';
2
+ /**
3
+ * Remove IstanbulJs instrumentations based on the given
4
+ * hook `makeCoverable`.
5
+ *
6
+ * An instrumentation is removed if the hook `makeCoverable` returns `false`.
7
+ */
8
+ export declare function cleanSourceCode(code: string, esModules: boolean, makeCoverable: (location: SourceLocation) => boolean): string;
9
+ //# sourceMappingURL=Postprocessor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Postprocessor.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/Postprocessor.ts"],"names":[],"mappings":"AAGA,OAAO,EAQN,cAAc,EASd,MAAM,cAAc,CAAC;AAqKtB;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,OAAO,EAClB,aAAa,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,OAAO,GAClD,MAAM,CAaR"}
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cleanSourceCode = void 0;
7
+ const parser_1 = require("@babel/parser");
8
+ const generator_1 = __importDefault(require("@babel/generator"));
9
+ const traverse_1 = __importDefault(require("@babel/traverse"));
10
+ const types_1 = require("@babel/types");
11
+ const commons_1 = require("@cqse/commons");
12
+ const COVERAGE_OBJ_FUNCTION_NAME_PREFIX = 'cov_';
13
+ /**
14
+ * Generator for identifiers that are unique across files to instrument.
15
+ * Relevant in case no Ecmascript modules are used.
16
+ *
17
+ * We assume that the files to be executed in a browser can
18
+ * stem from different runs of the instrumenter. We have to decrease
19
+ * the probability of colliding identifiers.
20
+ */
21
+ const fileIdSeqGenerator = (() => {
22
+ const instrumenterRunId = process.pid;
23
+ let fileIdSeq = 0;
24
+ return {
25
+ next: () => {
26
+ fileIdSeq++;
27
+ let num;
28
+ if (fileIdSeq < 10000) {
29
+ num = instrumenterRunId * 10000 + fileIdSeq;
30
+ }
31
+ else if (fileIdSeq < 100000) {
32
+ num = instrumenterRunId * 100000 + fileIdSeq;
33
+ }
34
+ else {
35
+ throw new Error(`Not more that 100k files supported to be instrumented in one run.`);
36
+ }
37
+ return num.toString(36);
38
+ }
39
+ };
40
+ })();
41
+ function getIstanbulCoverageFunctionDeclarationName(node) {
42
+ var _a;
43
+ if (!(0, types_1.isFunctionDeclaration)(node)) {
44
+ return undefined;
45
+ }
46
+ const functionName = (_a = node.id) === null || _a === void 0 ? void 0 : _a.name;
47
+ if (functionName === null || functionName === void 0 ? void 0 : functionName.startsWith(COVERAGE_OBJ_FUNCTION_NAME_PREFIX)) {
48
+ return functionName;
49
+ }
50
+ else {
51
+ return undefined;
52
+ }
53
+ }
54
+ function createFileIdMappingHandler() {
55
+ const fileIdMap = new Map();
56
+ return {
57
+ enterPath(path) {
58
+ var _a;
59
+ if (!(0, types_1.isVariableDeclaration)(path.node)) {
60
+ return;
61
+ }
62
+ const grandParentPath = (_a = path.parentPath) === null || _a === void 0 ? void 0 : _a.parentPath;
63
+ const coverageFunctionName = getIstanbulCoverageFunctionDeclarationName(grandParentPath === null || grandParentPath === void 0 ? void 0 : grandParentPath.node);
64
+ if (grandParentPath && coverageFunctionName) {
65
+ const declaration = path.node;
66
+ if (declaration.declarations.length === 1) {
67
+ const declarator = declaration.declarations[0];
68
+ if ((0, types_1.isIdentifier)(declarator.id) && declarator.id.name === 'hash') {
69
+ // We take note of the hash that is stored within the `cov_*' function.
70
+ const fileIdVarName = `_$f${fileIdSeqGenerator.next()}`;
71
+ const fileId = declarator.init.value;
72
+ fileIdMap.set(coverageFunctionName, fileIdVarName);
73
+ grandParentPath.insertBefore(newStringConstDeclarationNode(fileIdVarName, fileId));
74
+ }
75
+ }
76
+ }
77
+ },
78
+ getFileHashForCoverageObjectId(coverageObjectId) {
79
+ return fileIdMap.get(coverageObjectId);
80
+ }
81
+ };
82
+ }
83
+ function createPartialInstrumentationHandler(fileIdMappingHandler) {
84
+ return {
85
+ enterPath(path, makeCoverable) {
86
+ if (!(0, types_1.isUpdateExpression)(path.node)) {
87
+ return;
88
+ }
89
+ const increment = extractCoverageIncrement(path.node);
90
+ if (!increment) {
91
+ return;
92
+ }
93
+ const wantCoverageIncrement = path.node.loc && makeCoverable(path.node.loc) && increment.type !== 'function';
94
+ // Add a new coverage instrument if desired
95
+ if (wantCoverageIncrement) {
96
+ const fileIdVarName = fileIdMappingHandler.getFileHashForCoverageObjectId(increment.coverageObjectId);
97
+ if (!fileIdVarName) {
98
+ throw new commons_1.IllegalStateException(`File ID variable for coverage object with ID ${increment.coverageObjectId} not found!`);
99
+ }
100
+ const insertAsExpression = (0, types_1.isSequenceExpression)(path.parent);
101
+ insertNodeBefore(path, newCoverageIncrementNode(fileIdVarName, increment, insertAsExpression));
102
+ }
103
+ // Remove the existing coverage increment node
104
+ path.remove();
105
+ }
106
+ };
107
+ }
108
+ /**
109
+ * Remove IstanbulJs instrumentations based on the given
110
+ * hook `makeCoverable`.
111
+ *
112
+ * An instrumentation is removed if the hook `makeCoverable` returns `false`.
113
+ */
114
+ function cleanSourceCode(code, esModules, makeCoverable) {
115
+ const ast = (0, parser_1.parse)(code, { sourceType: esModules ? 'module' : 'script' });
116
+ const fileIdMappingHandler = createFileIdMappingHandler();
117
+ const partialInstrumentationHandler = createPartialInstrumentationHandler(fileIdMappingHandler);
118
+ (0, traverse_1.default)(ast, {
119
+ enter(path) {
120
+ fileIdMappingHandler.enterPath(path);
121
+ partialInstrumentationHandler.enterPath(path, makeCoverable);
122
+ }
123
+ });
124
+ return (0, generator_1.default)(ast, {}, code).code;
125
+ }
126
+ exports.cleanSourceCode = cleanSourceCode;
127
+ /**
128
+ * We cannot just run `path.insertBefore` to add a new element to an AST.
129
+ * See https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-inserting-a-sibling-node .
130
+ *
131
+ * Special handling for some container nodes is needed.
132
+ */
133
+ function insertNodeBefore(path, toInsert) {
134
+ if ((0, types_1.isSequenceExpression)(path.parent)) {
135
+ path.parentPath.unshiftContainer('expressions', [toInsert]);
136
+ }
137
+ else {
138
+ path.insertBefore(toInsert);
139
+ }
140
+ }
141
+ /**
142
+ * Creates a new string constant AST node.
143
+ */
144
+ function newStringConstDeclarationNode(name, value) {
145
+ return {
146
+ type: 'VariableDeclaration',
147
+ kind: 'const',
148
+ declarations: [
149
+ {
150
+ type: 'VariableDeclarator',
151
+ id: {
152
+ type: 'Identifier',
153
+ name
154
+ },
155
+ init: {
156
+ type: 'StringLiteral',
157
+ value
158
+ }
159
+ }
160
+ ]
161
+ };
162
+ }
163
+ /**
164
+ * Creates a new coverage increment statement.
165
+ */
166
+ function newCoverageIncrementNode(fileIdVarName, increment, asExpression) {
167
+ let expression;
168
+ if (increment.type === 'branch') {
169
+ expression = newBranchCoverageIncrementExpression(fileIdVarName, increment);
170
+ }
171
+ else if (increment.type === 'statement') {
172
+ expression = newStatementCoverageIncrementExpression(fileIdVarName, increment);
173
+ }
174
+ else {
175
+ throw new Error(`Unexpected coverage increment type: ${increment.type}`);
176
+ }
177
+ if (asExpression) {
178
+ return expression;
179
+ }
180
+ return {
181
+ type: 'ExpressionStatement',
182
+ expression
183
+ };
184
+ }
185
+ /**
186
+ * Create a branch coverage increment node.
187
+ */
188
+ function newBranchCoverageIncrementExpression(fileIdVarName, increment) {
189
+ return {
190
+ type: 'CallExpression',
191
+ callee: { type: 'Identifier', name: '_$brCov' },
192
+ arguments: [
193
+ { type: 'Identifier', name: fileIdVarName },
194
+ { type: 'NumericLiteral', value: increment.branchId },
195
+ { type: 'NumericLiteral', value: increment.locationId }
196
+ ]
197
+ };
198
+ }
199
+ /**
200
+ * Create a statement coverage increment node.
201
+ */
202
+ function newStatementCoverageIncrementExpression(fileIdVarName, increment) {
203
+ return {
204
+ type: 'CallExpression',
205
+ callee: { type: 'Identifier', name: '_$stmtCov' },
206
+ arguments: [
207
+ { type: 'Identifier', name: fileIdVarName },
208
+ { type: 'NumericLiteral', value: increment.statementId }
209
+ ]
210
+ };
211
+ }
212
+ /**
213
+ * Returns the call expression from `cov_2pvvu1hl8v().b[2][0]++;` if
214
+ * the given UpdateExpression is a branch coverage update expression.
215
+ */
216
+ function extractBranchCoverageIncrement(expr) {
217
+ if (expr.operator === '++' &&
218
+ (0, types_1.isMemberExpression)(expr.argument) &&
219
+ (0, types_1.isMemberExpression)(expr.argument.object) &&
220
+ (0, types_1.isMemberExpression)(expr.argument.object.object) &&
221
+ (0, types_1.isCallExpression)(expr.argument.object.object.object) &&
222
+ isCoverageObjectCall(expr.argument.object.object.object)) {
223
+ const coverageObjectId = expr.argument.object.object.object.callee.name;
224
+ const branchId = expr.argument.object.property.value;
225
+ const locationId = expr.argument.property.value;
226
+ return { type: 'branch', branchId, locationId, coverageObjectId };
227
+ }
228
+ return null;
229
+ }
230
+ /**
231
+ * Returns the call expression from `cov_104fq7oo4i().s[0]++;` if
232
+ * the given UpdateExpression is a statement coverage update expression.
233
+ */
234
+ function extractStatementCoverageIncrement(expr) {
235
+ if (expr.operator === '++' &&
236
+ (0, types_1.isMemberExpression)(expr.argument) &&
237
+ (0, types_1.isMemberExpression)(expr.argument.object) &&
238
+ (0, types_1.isCallExpression)(expr.argument.object.object) &&
239
+ (0, types_1.isIdentifier)(expr.argument.object.property) &&
240
+ expr.argument.object.property.name === 's' &&
241
+ isCoverageObjectCall(expr.argument.object.object)) {
242
+ const coverageObjectId = expr.argument.object.object.callee.name;
243
+ const statementId = expr.argument.property.value;
244
+ return { type: 'statement', statementId, coverageObjectId };
245
+ }
246
+ return null;
247
+ }
248
+ /**
249
+ * Returns the call expression from `cov_104fq7oo4i().f[0]++;` if
250
+ * the given UpdateExpression is a function coverage update expression.
251
+ */
252
+ function extractFunctionCoverageIncrement(expr) {
253
+ if (expr.operator === '++' &&
254
+ (0, types_1.isMemberExpression)(expr.argument) &&
255
+ (0, types_1.isMemberExpression)(expr.argument.object) &&
256
+ (0, types_1.isCallExpression)(expr.argument.object.object) &&
257
+ (0, types_1.isIdentifier)(expr.argument.object.property) &&
258
+ expr.argument.object.property.name === 'f' &&
259
+ isCoverageObjectCall(expr.argument.object.object)) {
260
+ const coverageObjectId = expr.argument.object.object.callee.name;
261
+ const functionId = expr.argument.property.value;
262
+ return { type: 'function', functionId, coverageObjectId };
263
+ }
264
+ return null;
265
+ }
266
+ /**
267
+ * Given an `UpdateExpression` extract the call expression returning the coverage object.
268
+ */
269
+ function extractCoverageIncrement(expr) {
270
+ var _a, _b;
271
+ return ((_b = (_a = extractBranchCoverageIncrement(expr)) !== null && _a !== void 0 ? _a : extractStatementCoverageIncrement(expr)) !== null && _b !== void 0 ? _b : extractFunctionCoverageIncrement(expr));
272
+ }
273
+ /**
274
+ * Check if the given call expression is a coverage call expression.
275
+ * If this is not the case return `undefined`, and the call expression itself otherwise.
276
+ */
277
+ function isCoverageObjectCall(callExpression) {
278
+ return (callExpression !== undefined &&
279
+ (0, types_1.isIdentifier)(callExpression.callee) &&
280
+ callExpression.callee.name.startsWith(COVERAGE_OBJ_FUNCTION_NAME_PREFIX));
281
+ }