@teamscale/javascript-instrumenter 0.0.1-beta.5 → 0.0.1-beta.50
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.
- package/README.md +2 -52
- package/dist/package.json +21 -15
- package/dist/src/App.d.ts +6 -1
- package/dist/src/App.d.ts.map +1 -1
- package/dist/src/App.js +90 -23
- package/dist/src/instrumenter/FileSystem.js +5 -1
- package/dist/src/instrumenter/Instrumenter.d.ts +39 -19
- package/dist/src/instrumenter/Instrumenter.d.ts.map +1 -1
- package/dist/src/instrumenter/Instrumenter.js +153 -66
- package/dist/src/instrumenter/Postprocessor.d.ts +9 -0
- package/dist/src/instrumenter/Postprocessor.d.ts.map +1 -0
- package/dist/src/instrumenter/Postprocessor.js +254 -0
- package/dist/src/instrumenter/Task.d.ts +37 -10
- package/dist/src/instrumenter/Task.d.ts.map +1 -1
- package/dist/src/instrumenter/Task.js +91 -37
- package/dist/src/instrumenter/TaskBuilder.d.ts +18 -10
- package/dist/src/instrumenter/TaskBuilder.d.ts.map +1 -1
- package/dist/src/instrumenter/TaskBuilder.js +28 -14
- package/dist/src/main.js +1 -0
- package/dist/vaccine.js +1 -221
- package/package.json +21 -15
|
@@ -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.
|
|
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
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
83
|
-
inputSourceMap =
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
writeToFile(taskElement.toFile, inputFileSource);
|
|
112
145
|
return Task_1.TaskResult.error(e);
|
|
113
146
|
}
|
|
114
147
|
}
|
|
115
148
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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:
|
|
206
|
+
produceSourceMap: 'both'
|
|
162
207
|
};
|
|
163
208
|
return [
|
|
164
209
|
{ ...baseConfig, ...{ esModules: true } },
|
|
165
210
|
{ ...baseConfig, ...{ esModules: false } }
|
|
166
211
|
];
|
|
167
212
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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,EAWd,MAAM,cAAc,CAAC;AA0ItB;;;;;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,254 @@
|
|
|
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
|
+
function getIstanbulCoverageFunctionDeclarationName(node) {
|
|
14
|
+
var _a;
|
|
15
|
+
if (!(0, types_1.isFunctionDeclaration)(node)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const functionName = (_a = node.id) === null || _a === void 0 ? void 0 : _a.name;
|
|
19
|
+
if (functionName === null || functionName === void 0 ? void 0 : functionName.startsWith(COVERAGE_OBJ_FUNCTION_NAME_PREFIX)) {
|
|
20
|
+
return functionName;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function createFileIdMappingHandler() {
|
|
27
|
+
const fileIdMap = new Map();
|
|
28
|
+
let fileIdSeq = 0;
|
|
29
|
+
return {
|
|
30
|
+
enterPath(path) {
|
|
31
|
+
var _a;
|
|
32
|
+
if (!(0, types_1.isVariableDeclaration)(path.node)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const grandParentPath = (_a = path.parentPath) === null || _a === void 0 ? void 0 : _a.parentPath;
|
|
36
|
+
const coverageFunctionName = getIstanbulCoverageFunctionDeclarationName(grandParentPath === null || grandParentPath === void 0 ? void 0 : grandParentPath.node);
|
|
37
|
+
if (grandParentPath && coverageFunctionName) {
|
|
38
|
+
const declaration = path.node;
|
|
39
|
+
if (declaration.declarations.length === 1) {
|
|
40
|
+
const declarator = declaration.declarations[0];
|
|
41
|
+
if ((0, types_1.isIdentifier)(declarator.id) && declarator.id.name === 'hash') {
|
|
42
|
+
// We take note of the hash that is stored within the `cov_*' function.
|
|
43
|
+
const fileIdVarName = `_$fid${fileIdSeq++}`;
|
|
44
|
+
const fileId = declarator.init.value;
|
|
45
|
+
fileIdMap.set(coverageFunctionName, fileIdVarName);
|
|
46
|
+
grandParentPath.insertBefore(newStringConstDeclarationNode(fileIdVarName, fileId));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
getFileHashForCoverageObjectId(coverageObjectId) {
|
|
52
|
+
return fileIdMap.get(coverageObjectId);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createPartialInstrumentationHandler(fileIdMappingHandler) {
|
|
57
|
+
return {
|
|
58
|
+
enterPath(path, makeCoverable) {
|
|
59
|
+
if (!(0, types_1.isUpdateExpression)(path.node)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const increment = extractCoverageIncrement(path.node);
|
|
63
|
+
if (!increment) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const wantCoverageIncrement = path.node.loc && makeCoverable(path.node.loc) && increment.type !== 'function';
|
|
67
|
+
// Add a new coverage instrument if desired
|
|
68
|
+
if (wantCoverageIncrement) {
|
|
69
|
+
const fileIdVarName = fileIdMappingHandler.getFileHashForCoverageObjectId(increment.coverageObjectId);
|
|
70
|
+
if (!fileIdVarName) {
|
|
71
|
+
throw new commons_1.IllegalStateException(`File ID variable for coverage object with ID ${increment.coverageObjectId} not found!`);
|
|
72
|
+
}
|
|
73
|
+
const insertAsExpression = (0, types_1.isSequenceExpression)(path.parent);
|
|
74
|
+
insertNodeBefore(path, newCoverageIncrementNode(fileIdVarName, increment, insertAsExpression));
|
|
75
|
+
}
|
|
76
|
+
// Remove the existing coverage increment node
|
|
77
|
+
path.remove();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Remove IstanbulJs instrumentations based on the given
|
|
83
|
+
* hook `makeCoverable`.
|
|
84
|
+
*
|
|
85
|
+
* An instrumentation is removed if the hook `makeCoverable` returns `false`.
|
|
86
|
+
*/
|
|
87
|
+
function cleanSourceCode(code, esModules, makeCoverable) {
|
|
88
|
+
const ast = (0, parser_1.parse)(code, { sourceType: esModules ? 'module' : 'script' });
|
|
89
|
+
const fileIdMappingHandler = createFileIdMappingHandler();
|
|
90
|
+
const partialInstrumentationHandler = createPartialInstrumentationHandler(fileIdMappingHandler);
|
|
91
|
+
(0, traverse_1.default)(ast, {
|
|
92
|
+
enter(path) {
|
|
93
|
+
fileIdMappingHandler.enterPath(path);
|
|
94
|
+
partialInstrumentationHandler.enterPath(path, makeCoverable);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return (0, generator_1.default)(ast, {}, code).code;
|
|
98
|
+
}
|
|
99
|
+
exports.cleanSourceCode = cleanSourceCode;
|
|
100
|
+
/**
|
|
101
|
+
* We cannot just run `path.insertBefore` to add a new element to an AST.
|
|
102
|
+
* See https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-inserting-a-sibling-node .
|
|
103
|
+
*
|
|
104
|
+
* Special handling for some container nodes is needed.
|
|
105
|
+
*/
|
|
106
|
+
function insertNodeBefore(path, toInsert) {
|
|
107
|
+
if ((0, types_1.isSequenceExpression)(path.parent)) {
|
|
108
|
+
path.parentPath.unshiftContainer('expressions', [toInsert]);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
path.insertBefore(toInsert);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Creates a new string constant AST node.
|
|
116
|
+
*/
|
|
117
|
+
function newStringConstDeclarationNode(name, value) {
|
|
118
|
+
return {
|
|
119
|
+
type: 'VariableDeclaration',
|
|
120
|
+
kind: 'const',
|
|
121
|
+
declarations: [
|
|
122
|
+
{
|
|
123
|
+
type: 'VariableDeclarator',
|
|
124
|
+
id: {
|
|
125
|
+
type: 'Identifier',
|
|
126
|
+
name
|
|
127
|
+
},
|
|
128
|
+
init: {
|
|
129
|
+
type: 'StringLiteral',
|
|
130
|
+
value
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Creates a new coverage increment statement.
|
|
138
|
+
*/
|
|
139
|
+
function newCoverageIncrementNode(fileIdVarName, increment, asExpression) {
|
|
140
|
+
let expression;
|
|
141
|
+
if (increment.type === 'branch') {
|
|
142
|
+
expression = newBranchCoverageIncrementExpression(fileIdVarName, increment);
|
|
143
|
+
}
|
|
144
|
+
else if (increment.type === 'statement') {
|
|
145
|
+
expression = newStatementCoverageIncrementExpression(fileIdVarName, increment);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw new Error(`Unexpected coverage increment type: ${increment.type}`);
|
|
149
|
+
}
|
|
150
|
+
if (asExpression) {
|
|
151
|
+
return expression;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
type: 'ExpressionStatement',
|
|
155
|
+
expression
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a branch coverage increment node.
|
|
160
|
+
*/
|
|
161
|
+
function newBranchCoverageIncrementExpression(fileIdVarName, increment) {
|
|
162
|
+
return {
|
|
163
|
+
type: 'CallExpression',
|
|
164
|
+
callee: { type: 'Identifier', name: '_$brCov' },
|
|
165
|
+
arguments: [
|
|
166
|
+
{ type: 'Identifier', name: fileIdVarName },
|
|
167
|
+
{ type: 'NumericLiteral', value: increment.branchId },
|
|
168
|
+
{ type: 'NumericLiteral', value: increment.locationId }
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Create a statement coverage increment node.
|
|
174
|
+
*/
|
|
175
|
+
function newStatementCoverageIncrementExpression(fileIdVarName, increment) {
|
|
176
|
+
return {
|
|
177
|
+
type: 'CallExpression',
|
|
178
|
+
callee: { type: 'Identifier', name: '_$stmtCov' },
|
|
179
|
+
arguments: [
|
|
180
|
+
{ type: 'Identifier', name: fileIdVarName },
|
|
181
|
+
{ type: 'NumericLiteral', value: increment.statementId }
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Returns the call expression from `cov_2pvvu1hl8v().b[2][0]++;` if
|
|
187
|
+
* the given UpdateExpression is a branch coverage update expression.
|
|
188
|
+
*/
|
|
189
|
+
function extractBranchCoverageIncrement(expr) {
|
|
190
|
+
if (expr.operator === '++' &&
|
|
191
|
+
(0, types_1.isMemberExpression)(expr.argument) &&
|
|
192
|
+
(0, types_1.isMemberExpression)(expr.argument.object) &&
|
|
193
|
+
(0, types_1.isMemberExpression)(expr.argument.object.object) &&
|
|
194
|
+
(0, types_1.isCallExpression)(expr.argument.object.object.object) &&
|
|
195
|
+
isCoverageObjectCall(expr.argument.object.object.object)) {
|
|
196
|
+
const coverageObjectId = expr.argument.object.object.object.callee.name;
|
|
197
|
+
const branchId = expr.argument.object.property.value;
|
|
198
|
+
const locationId = expr.argument.property.value;
|
|
199
|
+
return { type: 'branch', branchId, locationId, coverageObjectId };
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Returns the call expression from `cov_104fq7oo4i().s[0]++;` if
|
|
205
|
+
* the given UpdateExpression is a statement coverage update expression.
|
|
206
|
+
*/
|
|
207
|
+
function extractStatementCoverageIncrement(expr) {
|
|
208
|
+
if (expr.operator === '++' &&
|
|
209
|
+
(0, types_1.isMemberExpression)(expr.argument) &&
|
|
210
|
+
(0, types_1.isMemberExpression)(expr.argument.object) &&
|
|
211
|
+
(0, types_1.isCallExpression)(expr.argument.object.object) &&
|
|
212
|
+
(0, types_1.isIdentifier)(expr.argument.object.property) &&
|
|
213
|
+
expr.argument.object.property.name === 's' &&
|
|
214
|
+
isCoverageObjectCall(expr.argument.object.object)) {
|
|
215
|
+
const coverageObjectId = expr.argument.object.object.callee.name;
|
|
216
|
+
const statementId = expr.argument.property.value;
|
|
217
|
+
return { type: 'statement', statementId, coverageObjectId };
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns the call expression from `cov_104fq7oo4i().f[0]++;` if
|
|
223
|
+
* the given UpdateExpression is a function coverage update expression.
|
|
224
|
+
*/
|
|
225
|
+
function extractFunctionCoverageIncrement(expr) {
|
|
226
|
+
if (expr.operator === '++' &&
|
|
227
|
+
(0, types_1.isMemberExpression)(expr.argument) &&
|
|
228
|
+
(0, types_1.isMemberExpression)(expr.argument.object) &&
|
|
229
|
+
(0, types_1.isCallExpression)(expr.argument.object.object) &&
|
|
230
|
+
(0, types_1.isIdentifier)(expr.argument.object.property) &&
|
|
231
|
+
expr.argument.object.property.name === 'f' &&
|
|
232
|
+
isCoverageObjectCall(expr.argument.object.object)) {
|
|
233
|
+
const coverageObjectId = expr.argument.object.object.callee.name;
|
|
234
|
+
const functionId = expr.argument.property.value;
|
|
235
|
+
return { type: 'function', functionId, coverageObjectId };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Given an `UpdateExpression` extract the call expression returning the coverage object.
|
|
241
|
+
*/
|
|
242
|
+
function extractCoverageIncrement(expr) {
|
|
243
|
+
var _a, _b;
|
|
244
|
+
return ((_b = (_a = extractBranchCoverageIncrement(expr)) !== null && _a !== void 0 ? _a : extractStatementCoverageIncrement(expr)) !== null && _b !== void 0 ? _b : extractFunctionCoverageIncrement(expr));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if the given call expression is a coverage call expression.
|
|
248
|
+
* If this is not the case return `undefined`, and the call expression itself otherwise.
|
|
249
|
+
*/
|
|
250
|
+
function isCoverageObjectCall(callExpression) {
|
|
251
|
+
return (callExpression !== undefined &&
|
|
252
|
+
(0, types_1.isIdentifier)(callExpression.callee) &&
|
|
253
|
+
callExpression.callee.name.startsWith(COVERAGE_OBJ_FUNCTION_NAME_PREFIX));
|
|
254
|
+
}
|