@teamscale/lib-instrument 0.1.0-beta.2
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/LICENSE +24 -0
- package/README.md +3 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +15 -0
- package/lib/instrumenter.d.ts +45 -0
- package/lib/instrumenter.d.ts.map +1 -0
- package/lib/instrumenter.js +91 -0
- package/lib/origins.d.ts +32 -0
- package/lib/origins.d.ts.map +1 -0
- package/lib/origins.js +71 -0
- package/lib/utils.d.ts +36 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +2 -0
- package/lib/visitor.d.ts +30 -0
- package/lib/visitor.d.ts.map +1 -0
- package/lib/visitor.js +533 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright 2012-2015 Yahoo! Inc.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
* Redistributions of source code must retain the above copyright
|
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
* Neither the name of the Yahoo! Inc. nor the
|
|
12
|
+
names of its contributors may be used to endorse or promote products
|
|
13
|
+
derived from this software without specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY
|
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Instrumenter, InstrumenterOptions } from './instrumenter';
|
|
2
|
+
export type { InstrumenterOptions } from './instrumenter';
|
|
3
|
+
export { programVisitor } from './visitor';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new coverage instrumenter.
|
|
6
|
+
*
|
|
7
|
+
* @param opts - instrumenter options
|
|
8
|
+
*/
|
|
9
|
+
export declare function createInstrumenter(opts: InstrumenterOptions): Instrumenter;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AAEjE,YAAY,EAAC,mBAAmB,EAAC,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAC,cAAc,EAAC,MAAM,WAAW,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAE1E"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createInstrumenter = exports.programVisitor = void 0;
|
|
4
|
+
const instrumenter_1 = require("./instrumenter");
|
|
5
|
+
var visitor_1 = require("./visitor");
|
|
6
|
+
Object.defineProperty(exports, "programVisitor", { enumerable: true, get: function () { return visitor_1.programVisitor; } });
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new coverage instrumenter.
|
|
9
|
+
*
|
|
10
|
+
* @param opts - instrumenter options
|
|
11
|
+
*/
|
|
12
|
+
function createInstrumenter(opts) {
|
|
13
|
+
return new instrumenter_1.Instrumenter(opts);
|
|
14
|
+
}
|
|
15
|
+
exports.createInstrumenter = createInstrumenter;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NodePath } from '@babel/core';
|
|
2
|
+
import { SourceLocation } from "@babel/types";
|
|
3
|
+
import { ParserPlugin as PluginConfig } from '@babel/parser';
|
|
4
|
+
import { RawSourceMap } from "source-map";
|
|
5
|
+
import { InstrumentationOptions } from "./utils";
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring the coverage instrumenter.
|
|
8
|
+
*/
|
|
9
|
+
export type InstrumenterOptions = InstrumentationOptions & Partial<{
|
|
10
|
+
/** Preserve comments in output */
|
|
11
|
+
preserveComments: boolean;
|
|
12
|
+
/** Generate compact code */
|
|
13
|
+
compact: boolean;
|
|
14
|
+
/** Set to true to instrument ES6 modules */
|
|
15
|
+
esModules: boolean;
|
|
16
|
+
/** Set to true to allow `return` statements outside of functions */
|
|
17
|
+
autoWrap: boolean;
|
|
18
|
+
/** Approach to produce a source map for the instrumented code */
|
|
19
|
+
produceSourceMap: 'none' | 'inline' | 'external';
|
|
20
|
+
/** Turn debugging on */
|
|
21
|
+
debug: boolean;
|
|
22
|
+
/** Set babel parser plugins */
|
|
23
|
+
parserPlugins: PluginConfig[];
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* The main class of the instrumenter.
|
|
27
|
+
*/
|
|
28
|
+
export declare class Instrumenter {
|
|
29
|
+
private readonly opts;
|
|
30
|
+
constructor(opts?: Partial<InstrumenterOptions>);
|
|
31
|
+
/**
|
|
32
|
+
* Instrument the supplied code with coverage statements.
|
|
33
|
+
* To instrument EcmaScript modules, make sure to set the
|
|
34
|
+
* `esModules` option to `true` when creating the instrumenter.
|
|
35
|
+
*
|
|
36
|
+
* @param code - the code to instrument
|
|
37
|
+
* @param filename - the name of the file the code stems from.
|
|
38
|
+
* @param inputSourceMap - the source map that maps the not instrumented code back to its original
|
|
39
|
+
* @param shouldInstrumentCallback - a callback to decide if a given code fragment should be instrumented
|
|
40
|
+
*
|
|
41
|
+
* @returns the instrumented code.
|
|
42
|
+
*/
|
|
43
|
+
instrument(code: string, filename: string | undefined, inputSourceMap: RawSourceMap | undefined, shouldInstrumentCallback?: (path: NodePath, location: SourceLocation) => boolean): Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=instrumenter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumenter.d.ts","sourceRoot":"","sources":["../src/instrumenter.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,QAAQ,EAAgB,MAAM,aAAa,CAAC;AACpD,OAAO,EAAC,cAAc,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,YAAY,IAAI,YAAY,EAAC,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAC,YAAY,EAAoB,MAAM,YAAY,CAAC;AAG3D,OAAO,EAAC,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAE/C;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG,OAAO,CAAC;IAC/D,kCAAkC;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAE1B,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IAEjB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;IAEnB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAElB,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;IAEjD,wBAAwB;IACxB,KAAK,EAAE,OAAO,CAAC;IAEf,+BAA+B;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;CACjC,CAAC,CAAC;AAYH;;GAEG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;gBAE/B,IAAI,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC;IAI/C;;;;;;;;;;;OAWG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,cAAc,EAAE,YAAY,GAAG,SAAS,EACpF,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;CA0DtH"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Instrumenter = void 0;
|
|
4
|
+
/*
|
|
5
|
+
Copyright 2012-2015, Yahoo Inc.
|
|
6
|
+
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
|
|
7
|
+
*/
|
|
8
|
+
const core_1 = require("@babel/core");
|
|
9
|
+
const source_map_1 = require("source-map");
|
|
10
|
+
const visitor_1 = require("./visitor");
|
|
11
|
+
function mapSourceMapsOption(produceSourceMap) {
|
|
12
|
+
if (produceSourceMap === "none") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (produceSourceMap === "external") {
|
|
16
|
+
return "both";
|
|
17
|
+
}
|
|
18
|
+
return produceSourceMap;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The main class of the instrumenter.
|
|
22
|
+
*/
|
|
23
|
+
class Instrumenter {
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.opts = { ...opts };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Instrument the supplied code with coverage statements.
|
|
29
|
+
* To instrument EcmaScript modules, make sure to set the
|
|
30
|
+
* `esModules` option to `true` when creating the instrumenter.
|
|
31
|
+
*
|
|
32
|
+
* @param code - the code to instrument
|
|
33
|
+
* @param filename - the name of the file the code stems from.
|
|
34
|
+
* @param inputSourceMap - the source map that maps the not instrumented code back to its original
|
|
35
|
+
* @param shouldInstrumentCallback - a callback to decide if a given code fragment should be instrumented
|
|
36
|
+
*
|
|
37
|
+
* @returns the instrumented code.
|
|
38
|
+
*/
|
|
39
|
+
async instrument(code, filename, inputSourceMap, shouldInstrumentCallback) {
|
|
40
|
+
filename = filename ?? String(new Date().getTime()) + '.js';
|
|
41
|
+
const { opts } = this;
|
|
42
|
+
const sourceMapToUse = inputSourceMap ?? opts.inputSourceMap;
|
|
43
|
+
let inputSourceMapConsumer = undefined;
|
|
44
|
+
if (sourceMapToUse) {
|
|
45
|
+
inputSourceMapConsumer = await new source_map_1.SourceMapConsumer(sourceMapToUse);
|
|
46
|
+
}
|
|
47
|
+
const babelOpts = {
|
|
48
|
+
configFile: false,
|
|
49
|
+
babelrc: false,
|
|
50
|
+
ast: true,
|
|
51
|
+
filename,
|
|
52
|
+
inputSourceMap,
|
|
53
|
+
sourceMaps: mapSourceMapsOption(opts.produceSourceMap),
|
|
54
|
+
compact: opts.compact,
|
|
55
|
+
comments: opts.preserveComments,
|
|
56
|
+
parserOpts: {
|
|
57
|
+
allowAwaitOutsideFunction: true,
|
|
58
|
+
allowReturnOutsideFunction: opts.autoWrap,
|
|
59
|
+
sourceType: (opts.esModules ? 'module' : 'script'),
|
|
60
|
+
plugins: opts.parserPlugins
|
|
61
|
+
},
|
|
62
|
+
plugins: [
|
|
63
|
+
[
|
|
64
|
+
({ types }) => {
|
|
65
|
+
const ee = (0, visitor_1.programVisitor)(types, inputSourceMapConsumer, {
|
|
66
|
+
reportLogic: opts.reportLogic,
|
|
67
|
+
coverageGlobalScopeFunc: opts.coverageGlobalScopeFunc,
|
|
68
|
+
ignoreClassMethods: opts.ignoreClassMethods,
|
|
69
|
+
inputSourceMap,
|
|
70
|
+
isInstrumentedToken: opts.isInstrumentedToken,
|
|
71
|
+
codeToPrepend: opts.codeToPrepend,
|
|
72
|
+
shouldInstrumentCallback: shouldInstrumentCallback ?? opts.shouldInstrumentCallback
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
visitor: {
|
|
76
|
+
Program: {
|
|
77
|
+
enter: ee.enter,
|
|
78
|
+
exit(path) {
|
|
79
|
+
ee.exit(path);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
return (0, core_1.transformSync)(code, babelOpts).code;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.Instrumenter = Instrumenter;
|
package/lib/origins.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SourceLocation } from "@babel/types";
|
|
2
|
+
import { SourceMapConsumer } from "source-map";
|
|
3
|
+
/**
|
|
4
|
+
* Generator for identifiers that are unique across files to instrument.
|
|
5
|
+
* Relevant in case no Ecmascript modules are used.
|
|
6
|
+
*
|
|
7
|
+
* We assume that the files to be executed in a browser can
|
|
8
|
+
* stem from different runs of the instrumenter. We have to decrease
|
|
9
|
+
* the probability of colliding identifiers.
|
|
10
|
+
*/
|
|
11
|
+
export declare const fileIdSeqGenerator: {
|
|
12
|
+
next: () => string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Mapping source locations to their origins, before the last transpilation,
|
|
16
|
+
* based on the source map.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SourceOrigins {
|
|
19
|
+
private readonly sourceMap?;
|
|
20
|
+
/**
|
|
21
|
+
* The mapping of file ids to the file names in the transpiler origin,
|
|
22
|
+
* that is, the file names found in the source map.
|
|
23
|
+
*/
|
|
24
|
+
readonly originToIdMap: Map<string, string>;
|
|
25
|
+
constructor(sourceMap: SourceMapConsumer | undefined);
|
|
26
|
+
/**
|
|
27
|
+
* Register source origin file and retrieve a unique identifier for it; furthermore, map
|
|
28
|
+
* the given location to the location in the origin.
|
|
29
|
+
*/
|
|
30
|
+
ensureKnownOrigin(loc: SourceLocation): [string, SourceLocation];
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=origins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../src/origins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAyB,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE;IAAE,IAAI,EAAE,MAAM,MAAM,CAAA;CAkBjD,CAAC;AAEL;;;GAGG;AACH,qBAAa,aAAa;IAEtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAoB;IAE/C;;;OAGG;IACH,SAAgB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAEvC,SAAS,EAAE,iBAAiB,GAAG,SAAS;IAKpD;;;OAGG;IACH,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC;CA6BnE"}
|
package/lib/origins.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SourceOrigins = exports.fileIdSeqGenerator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Generator for identifiers that are unique across files to instrument.
|
|
6
|
+
* Relevant in case no Ecmascript modules are used.
|
|
7
|
+
*
|
|
8
|
+
* We assume that the files to be executed in a browser can
|
|
9
|
+
* stem from different runs of the instrumenter. We have to decrease
|
|
10
|
+
* the probability of colliding identifiers.
|
|
11
|
+
*/
|
|
12
|
+
exports.fileIdSeqGenerator = (() => {
|
|
13
|
+
const instrumenterRunId = process.pid;
|
|
14
|
+
let fileIdSeq = 0;
|
|
15
|
+
return {
|
|
16
|
+
next: () => {
|
|
17
|
+
fileIdSeq++;
|
|
18
|
+
let num;
|
|
19
|
+
if (fileIdSeq < 10000) {
|
|
20
|
+
num = instrumenterRunId * 10000 + fileIdSeq;
|
|
21
|
+
}
|
|
22
|
+
else if (fileIdSeq < 100000) {
|
|
23
|
+
num = instrumenterRunId * 100000 + fileIdSeq;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(`Not more that 100k files supported to be instrumented in one run.`);
|
|
27
|
+
}
|
|
28
|
+
return num.toString(36);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
})();
|
|
32
|
+
/**
|
|
33
|
+
* Mapping source locations to their origins, before the last transpilation,
|
|
34
|
+
* based on the source map.
|
|
35
|
+
*/
|
|
36
|
+
class SourceOrigins {
|
|
37
|
+
constructor(sourceMap) {
|
|
38
|
+
this.originToIdMap = new Map();
|
|
39
|
+
this.sourceMap = sourceMap;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register source origin file and retrieve a unique identifier for it; furthermore, map
|
|
43
|
+
* the given location to the location in the origin.
|
|
44
|
+
*/
|
|
45
|
+
ensureKnownOrigin(loc) {
|
|
46
|
+
let startPos = undefined;
|
|
47
|
+
let endPos = undefined;
|
|
48
|
+
let filename = loc.filename ?? '';
|
|
49
|
+
if (this.sourceMap) {
|
|
50
|
+
startPos = this.sourceMap.originalPositionFor({ line: loc.start.line, column: loc.start.column });
|
|
51
|
+
endPos = this.sourceMap.originalPositionFor({ line: loc.end.line, column: loc.end.column });
|
|
52
|
+
filename = startPos.source ?? loc.filename;
|
|
53
|
+
}
|
|
54
|
+
if (!startPos || !endPos) {
|
|
55
|
+
startPos = { line: loc.start.line, column: loc.start.column, source: null, name: null };
|
|
56
|
+
endPos = { line: loc.end.line, column: loc.end.column, source: null, name: null };
|
|
57
|
+
}
|
|
58
|
+
let id = this.originToIdMap.get(filename);
|
|
59
|
+
if (!id) {
|
|
60
|
+
id = `_$o${exports.fileIdSeqGenerator.next()}`;
|
|
61
|
+
this.originToIdMap.set(filename, id);
|
|
62
|
+
}
|
|
63
|
+
return [id, {
|
|
64
|
+
start: { line: startPos.line, column: startPos.column, index: -1 },
|
|
65
|
+
end: { line: endPos.line, column: endPos.column, index: -1 },
|
|
66
|
+
filename,
|
|
67
|
+
identifierName: startPos.name
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.SourceOrigins = SourceOrigins;
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SourceLocation } from "@babel/types";
|
|
2
|
+
import { RawSourceMap } from "source-map";
|
|
3
|
+
import { NodePath } from "@babel/core";
|
|
4
|
+
/**
|
|
5
|
+
* Options to configure the instrumenter.
|
|
6
|
+
*/
|
|
7
|
+
export type InstrumentationOptions = Partial<{
|
|
8
|
+
/** Report boolean value of logical expressions */
|
|
9
|
+
reportLogic: boolean;
|
|
10
|
+
/** Use an evaluated function to find coverageGlobalScope */
|
|
11
|
+
coverageGlobalScopeFunc: boolean;
|
|
12
|
+
/** Names of methods to ignore by default on classes */
|
|
13
|
+
ignoreClassMethods: string[];
|
|
14
|
+
/** The input source map, that maps the uninstrumented code back to the original code */
|
|
15
|
+
inputSourceMap?: RawSourceMap;
|
|
16
|
+
/** Token to add in the very beginning to indicate that the instrumentation has been performed */
|
|
17
|
+
isInstrumentedToken?: string;
|
|
18
|
+
/** Code to add before the instrumented input code */
|
|
19
|
+
codeToPrepend?: string;
|
|
20
|
+
/** Callback for determining if a given code fragment should be instrument */
|
|
21
|
+
shouldInstrumentCallback?: (path: NodePath, loc: SourceLocation) => boolean;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Source code fragment within on file.
|
|
25
|
+
*/
|
|
26
|
+
export type CodeRange = {
|
|
27
|
+
start: {
|
|
28
|
+
line?: number;
|
|
29
|
+
column?: number;
|
|
30
|
+
};
|
|
31
|
+
end: {
|
|
32
|
+
line?: number;
|
|
33
|
+
column?: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AACxC,OAAO,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAErC;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC;IACzC,kDAAkD;IAClD,WAAW,EAAE,OAAO,CAAC;IAErB,4DAA4D;IAC5D,uBAAuB,EAAE,OAAO,CAAC;IAEjC,uDAAuD;IACvD,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAE7B,wFAAwF;IACxF,cAAc,CAAC,EAAE,YAAY,CAAC;IAE9B,iGAAiG;IACjG,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,6EAA6E;IAC7E,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC;CAC/E,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACpB,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,GAAG,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C,CAAC"}
|
package/lib/utils.js
ADDED
package/lib/visitor.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NodePath } from '@babel/core';
|
|
2
|
+
import { Program } from "@babel/types";
|
|
3
|
+
type BabelTypes = typeof import("@babel/types");
|
|
4
|
+
import { SourceMapConsumer } from "source-map";
|
|
5
|
+
import { InstrumentationOptions } from "./utils";
|
|
6
|
+
/**
|
|
7
|
+
* `programVisitor` is a `babel` adaptor for instrumentation.
|
|
8
|
+
*
|
|
9
|
+
* It returns an object with two methods `enter` and `exit`.
|
|
10
|
+
* These should be assigned to or called from `Program` entry and exit functions
|
|
11
|
+
* in a babel visitor.
|
|
12
|
+
*
|
|
13
|
+
* These functions do not make assumptions about the state set by Babel and thus
|
|
14
|
+
* can be used in a context other than a Babel plugin.
|
|
15
|
+
*
|
|
16
|
+
* The exit function returns an object that currently has the following keys:
|
|
17
|
+
*
|
|
18
|
+
* `fileCoverage` - the file coverage object created for the source file.
|
|
19
|
+
* `sourceMappingURL` - any source mapping URL found when processing the file.
|
|
20
|
+
*
|
|
21
|
+
* @param types - an instance of babel-types.
|
|
22
|
+
* @param sourceFilePath - the path to source file.
|
|
23
|
+
* @param opts - additional options.
|
|
24
|
+
*/
|
|
25
|
+
export declare function programVisitor(types: BabelTypes, inputSourceMapConsumer: SourceMapConsumer | undefined, opts: InstrumentationOptions): {
|
|
26
|
+
enter(path: NodePath<Program>): void;
|
|
27
|
+
exit(path: NodePath<Program>): void;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=visitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visitor.d.ts","sourceRoot":"","sources":["../src/visitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,QAAQ,EAAQ,MAAM,aAAa,CAAC;AAElD,OAAO,EAK8C,OAAO,EAE3D,MAAM,cAAc,CAAC;AAEtB,KAAK,UAAU,GAAG,cAAc,cAAc,CAAC,CAAA;AAE/C,OAAO,EAAC,iBAAiB,EAAC,MAAM,YAAY,CAAC;AAG7C,OAAO,EAAC,sBAAsB,EAAC,MAAM,SAAS,CAAC;AAykB/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EACjB,sBAAsB,EAAE,iBAAiB,GAAG,SAAS,EACrD,IAAI,EAAE,sBAAsB;gBAYvC,SAAS,OAAO,CAAC,GAAG,IAAI;eASzB,SAAS,OAAO,CAAC;EA+BnC"}
|
package/lib/visitor.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.programVisitor = void 0;
|
|
4
|
+
const core_1 = require("@babel/core");
|
|
5
|
+
const origins_1 = require("./origins");
|
|
6
|
+
// Pattern for istanbul to ignore a section
|
|
7
|
+
const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
|
|
8
|
+
// Pattern for istanbul to ignore the whole file
|
|
9
|
+
const COMMENT_FILE_RE = /^\s*istanbul\s+ignore\s+(file)(?=\W|$)/;
|
|
10
|
+
/**
|
|
11
|
+
* `VisitState` holds the state of the visitor, provides helper functions
|
|
12
|
+
* and is the `this` for the individual coverage visitors.
|
|
13
|
+
*/
|
|
14
|
+
class VisitState {
|
|
15
|
+
constructor(types, inputSourceMapConsumer, ignoreClassMethods = [], reportLogic = false, shouldInstrumentCallback) {
|
|
16
|
+
this.attrs = {};
|
|
17
|
+
this.nextIgnore = null;
|
|
18
|
+
this.ignoreClassMethods = ignoreClassMethods;
|
|
19
|
+
this.types = types;
|
|
20
|
+
this.reportLogic = reportLogic;
|
|
21
|
+
this.shouldInstrumentCallback = shouldInstrumentCallback;
|
|
22
|
+
this.origins = new origins_1.SourceOrigins(inputSourceMapConsumer);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Use the configured callback, if available, to check if the given source
|
|
26
|
+
* location should be instrumented.
|
|
27
|
+
*/
|
|
28
|
+
shouldInstrument(path, loc) {
|
|
29
|
+
if (this.shouldInstrumentCallback) {
|
|
30
|
+
return this.shouldInstrumentCallback(path, loc);
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** Should we ignore the node? Yes, if specifically ignoring or if the node is generated. */
|
|
35
|
+
shouldIgnore(path) {
|
|
36
|
+
return this.nextIgnore !== null || !path.node.loc;
|
|
37
|
+
}
|
|
38
|
+
/** Extract the ignore comment hint (next|if|else) or null. */
|
|
39
|
+
hintFor(node) {
|
|
40
|
+
let hint = null;
|
|
41
|
+
if (node.leadingComments) {
|
|
42
|
+
node.leadingComments.forEach(c => {
|
|
43
|
+
const v = (c.value || '').trim();
|
|
44
|
+
const groups = v.match(COMMENT_RE);
|
|
45
|
+
if (groups) {
|
|
46
|
+
hint = groups[1];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return hint;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* For these expressions the statement counter needs to be hoisted, so
|
|
54
|
+
* function name inference can be preserved.
|
|
55
|
+
*/
|
|
56
|
+
counterNeedsHoisting(path) {
|
|
57
|
+
return (path.isFunctionExpression() ||
|
|
58
|
+
path.isArrowFunctionExpression() ||
|
|
59
|
+
path.isClassExpression());
|
|
60
|
+
}
|
|
61
|
+
/** All the generic stuff that needs to be done on enter for every node. */
|
|
62
|
+
onEnter(path) {
|
|
63
|
+
const n = path.node;
|
|
64
|
+
// if already ignoring, nothing more to do
|
|
65
|
+
if (this.nextIgnore !== null) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// check hint to see if ignore should be turned on
|
|
69
|
+
const hint = this.hintFor(n);
|
|
70
|
+
if (hint === 'next') {
|
|
71
|
+
this.nextIgnore = n;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// else check custom node attribute set by a prior visitor
|
|
75
|
+
if (this.getAttr(path.node, 'skip-all') !== null) {
|
|
76
|
+
this.nextIgnore = n;
|
|
77
|
+
}
|
|
78
|
+
// else check for ignored class methods
|
|
79
|
+
if (path.isFunctionExpression() &&
|
|
80
|
+
this.ignoreClassMethods.some(name => path.node.id && name === path.node.id.name)) {
|
|
81
|
+
this.nextIgnore = n;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (path.isClassMethod() &&
|
|
85
|
+
this.ignoreClassMethods.some(name => name === path.node.key.name)) {
|
|
86
|
+
this.nextIgnore = n;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* All the generic stuff on exit of a node, including resetting ignores and custom node attrs.
|
|
92
|
+
*/
|
|
93
|
+
onExit(path) {
|
|
94
|
+
// restore ignore status, if needed
|
|
95
|
+
if (path.node === this.nextIgnore) {
|
|
96
|
+
this.nextIgnore = null;
|
|
97
|
+
}
|
|
98
|
+
// nuke all attributes for the node
|
|
99
|
+
delete path.node.__cov__;
|
|
100
|
+
}
|
|
101
|
+
/** Set a node attribute for the supplied node. */
|
|
102
|
+
setAttr(node, name, value) {
|
|
103
|
+
node.__cov__ = node.__cov__ || {};
|
|
104
|
+
node.__cov__[name] = value;
|
|
105
|
+
}
|
|
106
|
+
/** Retrieve a node attribute for the supplied node or null. */
|
|
107
|
+
getAttr(node, name) {
|
|
108
|
+
const c = node.__cov__;
|
|
109
|
+
if (!c) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return c[name];
|
|
113
|
+
}
|
|
114
|
+
insertCounter(path, increment) {
|
|
115
|
+
const T = this.types;
|
|
116
|
+
if (path.isBlockStatement()) {
|
|
117
|
+
path.node.body.unshift(T.expressionStatement(increment));
|
|
118
|
+
}
|
|
119
|
+
else if (path.isStatement()) {
|
|
120
|
+
path.insertBefore(T.expressionStatement(increment));
|
|
121
|
+
}
|
|
122
|
+
else if (this.counterNeedsHoisting(path) &&
|
|
123
|
+
T.isVariableDeclarator(path.parent)) {
|
|
124
|
+
// make an attempt to hoist the statement counter, so that
|
|
125
|
+
// function names are maintained.
|
|
126
|
+
const grandParentPath = path.parentPath?.parentPath;
|
|
127
|
+
if (grandParentPath && T.isExportNamedDeclaration(grandParentPath.parent)) {
|
|
128
|
+
grandParentPath.parentPath?.insertBefore(T.expressionStatement(increment));
|
|
129
|
+
}
|
|
130
|
+
else if (grandParentPath &&
|
|
131
|
+
(T.isProgram(grandParentPath.parent) ||
|
|
132
|
+
T.isBlockStatement(grandParentPath.parent))) {
|
|
133
|
+
grandParentPath.insertBefore(T.expressionStatement(increment));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
path.replaceWith(T.sequenceExpression([increment, path.node]));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (path.isExpression()) {
|
|
140
|
+
path.replaceWith(T.sequenceExpression([increment, path.node]));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.error('Unable to insert counter for node type:', path.node.type);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
insertFunctionCounter(path) {
|
|
147
|
+
const T = this.types;
|
|
148
|
+
if (!(path.node?.loc)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const n = path.node;
|
|
152
|
+
let declarationLocation;
|
|
153
|
+
switch (n.type) {
|
|
154
|
+
case 'FunctionDeclaration':
|
|
155
|
+
case 'FunctionExpression':
|
|
156
|
+
if (n.id) {
|
|
157
|
+
declarationLocation = n.id.loc ?? undefined;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const body = path.get('body');
|
|
162
|
+
const loc = path.node.loc ?? declarationLocation;
|
|
163
|
+
const [originFileId, originPos] = this.origins.ensureKnownOrigin(loc);
|
|
164
|
+
if (body.isBlockStatement() && this.shouldInstrument(path, originPos)) {
|
|
165
|
+
// For functions, we only cover the first line of its body.
|
|
166
|
+
originPos.end = originPos.start;
|
|
167
|
+
const increment = newLineCoverageExpression(originFileId, originPos);
|
|
168
|
+
body.node.body.unshift(T.expressionStatement(increment));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
insertStatementCounter(path) {
|
|
172
|
+
const loc = path.node?.loc;
|
|
173
|
+
if (!loc) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const [originFileId, originPos] = this.origins.ensureKnownOrigin(loc);
|
|
177
|
+
if (!this.shouldInstrument(path, originPos)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const increment = newLineCoverageExpression(originFileId, originPos);
|
|
181
|
+
this.insertCounter(path, increment);
|
|
182
|
+
}
|
|
183
|
+
insertBranchCounter(path, loc) {
|
|
184
|
+
loc = loc ?? path.node.loc;
|
|
185
|
+
if (!loc) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const [originFileId, originPos] = this.origins.ensureKnownOrigin(loc);
|
|
189
|
+
if (this.shouldInstrument(path, originPos)) {
|
|
190
|
+
const increment = newLineCoverageExpression(originFileId, originPos);
|
|
191
|
+
this.insertCounter(path, increment);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
findLeaves(node, accumulator, parent, property) {
|
|
195
|
+
if (!node) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (node.type === 'LogicalExpression') {
|
|
199
|
+
const hint = this.hintFor(node);
|
|
200
|
+
if (hint !== 'next') {
|
|
201
|
+
this.findLeaves(node.left, accumulator, node, 'left');
|
|
202
|
+
this.findLeaves(node.right, accumulator, node, 'right');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
accumulator.push({
|
|
207
|
+
node,
|
|
208
|
+
parent: parent,
|
|
209
|
+
property: property
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Create a line coverage reporting statement node.
|
|
216
|
+
*/
|
|
217
|
+
function newLineCoverageExpression(originFileId, range) {
|
|
218
|
+
return {
|
|
219
|
+
type: 'CallExpression',
|
|
220
|
+
callee: { type: 'Identifier', name: '_$l' },
|
|
221
|
+
arguments: [
|
|
222
|
+
{ type: 'Identifier', name: originFileId },
|
|
223
|
+
{ type: 'NumericLiteral', value: range.start.line },
|
|
224
|
+
{ type: 'NumericLiteral', value: range.end.line },
|
|
225
|
+
]
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Creates a new string constant AST node.
|
|
230
|
+
*/
|
|
231
|
+
function newStringConstDeclarationNode(name, value) {
|
|
232
|
+
return {
|
|
233
|
+
type: 'VariableDeclaration',
|
|
234
|
+
kind: 'const',
|
|
235
|
+
declarations: [
|
|
236
|
+
{
|
|
237
|
+
type: 'VariableDeclarator',
|
|
238
|
+
id: {
|
|
239
|
+
type: 'Identifier',
|
|
240
|
+
name
|
|
241
|
+
},
|
|
242
|
+
init: {
|
|
243
|
+
type: 'StringLiteral',
|
|
244
|
+
value
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Generic function that takes a set of visitor methods and
|
|
252
|
+
* returns a visitor object with `enter` and `exit` properties,
|
|
253
|
+
* such that:
|
|
254
|
+
*
|
|
255
|
+
* - standard entry processing is done
|
|
256
|
+
* - the supplied visitors are called only when ignore is not in effect;
|
|
257
|
+
* it reliefs them from worrying about ignore states and generated nodes.
|
|
258
|
+
* - standard exit processing is done
|
|
259
|
+
*/
|
|
260
|
+
function entries(...enter) {
|
|
261
|
+
// the enter function
|
|
262
|
+
const wrappedEntry = function (path, node) {
|
|
263
|
+
this.onEnter(path);
|
|
264
|
+
if (this.shouldIgnore(path)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
enter.forEach(e => {
|
|
268
|
+
e.call(this, path, node);
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
const exit = function (path) {
|
|
272
|
+
this.onExit(path);
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
enter: wrappedEntry,
|
|
276
|
+
exit
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function coverStatement(path) {
|
|
280
|
+
this.insertStatementCounter(path);
|
|
281
|
+
}
|
|
282
|
+
function coverAssignmentPattern(path) {
|
|
283
|
+
this.insertBranchCounter(path.get('right'), undefined);
|
|
284
|
+
}
|
|
285
|
+
function coverFunction(path) {
|
|
286
|
+
this.insertFunctionCounter(path);
|
|
287
|
+
}
|
|
288
|
+
function coverVariableDeclarator(path) {
|
|
289
|
+
this.insertStatementCounter(path.get('init'));
|
|
290
|
+
}
|
|
291
|
+
function coverClassPropDeclarator(path) {
|
|
292
|
+
this.insertStatementCounter(path.get('value'));
|
|
293
|
+
}
|
|
294
|
+
function makeBlock(path) {
|
|
295
|
+
const T = this.types;
|
|
296
|
+
if (!path.node) {
|
|
297
|
+
path.replaceWith(T.blockStatement([]));
|
|
298
|
+
}
|
|
299
|
+
if (!path.isBlockStatement()) {
|
|
300
|
+
path.replaceWith(T.blockStatement([path.node]));
|
|
301
|
+
const block = path.node;
|
|
302
|
+
path.node.loc = block.body[0].loc;
|
|
303
|
+
block.body[0].leadingComments = path.node.leadingComments;
|
|
304
|
+
path.node.leadingComments = undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function blockProp(prop) {
|
|
308
|
+
return function (path) {
|
|
309
|
+
makeBlock.call(this, path.get(prop));
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function makeParenthesizedExpressionForNonIdentifier(path) {
|
|
313
|
+
const T = this.types;
|
|
314
|
+
if (path.node && !path.isIdentifier()) {
|
|
315
|
+
path.replaceWith(T.parenthesizedExpression(path.node));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function parenthesizedExpressionProp(prop) {
|
|
319
|
+
return function (path) {
|
|
320
|
+
makeParenthesizedExpressionForNonIdentifier.call(this, path.get(prop));
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function convertArrowExpression(path) {
|
|
324
|
+
const node = path.node;
|
|
325
|
+
const T = this.types;
|
|
326
|
+
if (!T.isBlockStatement(node.body)) {
|
|
327
|
+
const bloc = node.body.loc;
|
|
328
|
+
if (node.expression === true) {
|
|
329
|
+
node.expression = false;
|
|
330
|
+
}
|
|
331
|
+
node.body = T.blockStatement([T.returnStatement(node.body)]);
|
|
332
|
+
// restore body location
|
|
333
|
+
node.body.loc = bloc;
|
|
334
|
+
// set up the location for the return statement so it gets
|
|
335
|
+
// instrumented
|
|
336
|
+
node.body.body[0].loc = bloc;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function coverIfBranches(path) {
|
|
340
|
+
const n = path.node;
|
|
341
|
+
const hint = this.hintFor(n);
|
|
342
|
+
const ignoreIf = hint === 'if';
|
|
343
|
+
const ignoreElse = hint === 'else';
|
|
344
|
+
if (ignoreIf) {
|
|
345
|
+
this.setAttr(n.consequent, 'skip-all', true);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
this.insertBranchCounter(path.get('consequent'), n.loc);
|
|
349
|
+
}
|
|
350
|
+
if (ignoreElse) {
|
|
351
|
+
this.setAttr(n.alternate, 'skip-all', true);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this.insertBranchCounter(path.get('alternate'), undefined);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function createSwitchBranch(path) {
|
|
358
|
+
// Intentionally left blank
|
|
359
|
+
}
|
|
360
|
+
function coverSwitchCase(path) {
|
|
361
|
+
const T = this.types;
|
|
362
|
+
const loc = path.node.loc;
|
|
363
|
+
if (!loc) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const [originFileId, originPos] = this.origins.ensureKnownOrigin(loc);
|
|
367
|
+
if (this.shouldInstrument(path, originPos)) {
|
|
368
|
+
const increment = newLineCoverageExpression(originFileId, originPos);
|
|
369
|
+
path.node.consequent.unshift(T.expressionStatement(increment));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function coverTernary(path) {
|
|
373
|
+
const n = path.node;
|
|
374
|
+
const cHint = this.hintFor(n.consequent);
|
|
375
|
+
const aHint = this.hintFor(n.alternate);
|
|
376
|
+
if (cHint !== 'next') {
|
|
377
|
+
this.insertBranchCounter(path.get('consequent'), undefined);
|
|
378
|
+
}
|
|
379
|
+
if (aHint !== 'next') {
|
|
380
|
+
this.insertBranchCounter(path.get('alternate'), undefined);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function coverLogicalExpression(path) {
|
|
384
|
+
const T = this.types;
|
|
385
|
+
if (path.parentPath.node.type === 'LogicalExpression') {
|
|
386
|
+
return; // already processed
|
|
387
|
+
}
|
|
388
|
+
const leaves = [];
|
|
389
|
+
this.findLeaves(path.node, leaves, undefined, undefined);
|
|
390
|
+
for (const leaf of leaves) {
|
|
391
|
+
const hint = this.hintFor(leaf.node);
|
|
392
|
+
if (hint === 'next') {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const loc = path.node.loc;
|
|
396
|
+
if (!loc) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const [originFileId, originPos] = this.origins.ensureKnownOrigin(loc);
|
|
400
|
+
if (!this.shouldInstrument(path, originPos)) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const increment = newLineCoverageExpression(originFileId, originPos);
|
|
404
|
+
if (!increment) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
leaf.parent[leaf.property] = T.sequenceExpression([
|
|
408
|
+
increment,
|
|
409
|
+
leaf.node
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const codeVisitor = {
|
|
414
|
+
ArrowFunctionExpression: entries(convertArrowExpression, coverFunction),
|
|
415
|
+
AssignmentPattern: entries(coverAssignmentPattern),
|
|
416
|
+
BlockStatement: entries(),
|
|
417
|
+
ExportDefaultDeclaration: entries(),
|
|
418
|
+
ExportNamedDeclaration: entries(),
|
|
419
|
+
ClassMethod: entries(coverFunction),
|
|
420
|
+
ClassDeclaration: entries(parenthesizedExpressionProp('superClass')),
|
|
421
|
+
ClassProperty: entries(coverClassPropDeclarator),
|
|
422
|
+
ClassPrivateProperty: entries(coverClassPropDeclarator),
|
|
423
|
+
ObjectMethod: entries(coverFunction),
|
|
424
|
+
ExpressionStatement: entries(coverStatement),
|
|
425
|
+
BreakStatement: entries(coverStatement),
|
|
426
|
+
ContinueStatement: entries(coverStatement),
|
|
427
|
+
DebuggerStatement: entries(coverStatement),
|
|
428
|
+
ReturnStatement: entries(coverStatement),
|
|
429
|
+
ThrowStatement: entries(coverStatement),
|
|
430
|
+
TryStatement: entries(coverStatement),
|
|
431
|
+
VariableDeclaration: entries(),
|
|
432
|
+
VariableDeclarator: entries(coverVariableDeclarator),
|
|
433
|
+
IfStatement: entries(blockProp('consequent'), blockProp('alternate'), coverStatement, coverIfBranches),
|
|
434
|
+
ForStatement: entries(blockProp('body'), coverStatement),
|
|
435
|
+
ForInStatement: entries(blockProp('body'), coverStatement),
|
|
436
|
+
ForOfStatement: entries(blockProp('body'), coverStatement),
|
|
437
|
+
WhileStatement: entries(blockProp('body'), coverStatement),
|
|
438
|
+
DoWhileStatement: entries(blockProp('body'), coverStatement),
|
|
439
|
+
SwitchStatement: entries(createSwitchBranch, coverStatement),
|
|
440
|
+
SwitchCase: entries(coverSwitchCase),
|
|
441
|
+
WithStatement: entries(blockProp('body'), coverStatement),
|
|
442
|
+
FunctionDeclaration: entries(coverFunction),
|
|
443
|
+
FunctionExpression: entries(coverFunction),
|
|
444
|
+
LabeledStatement: entries(coverStatement),
|
|
445
|
+
ConditionalExpression: entries(coverTernary),
|
|
446
|
+
LogicalExpression: entries(coverLogicalExpression)
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* The rewire plugin (and potentially other babel middleware)
|
|
450
|
+
* may cause files to be instrumented twice, see:
|
|
451
|
+
* https://github.com/istanbuljs/babel-plugin-istanbul/issues/94
|
|
452
|
+
* we should only instrument code for coverage the first time
|
|
453
|
+
* it's run through lib-instrument.
|
|
454
|
+
*/
|
|
455
|
+
function alreadyInstrumented(path, visitState) {
|
|
456
|
+
return path.scope.hasBinding(visitState.varName);
|
|
457
|
+
}
|
|
458
|
+
function getParentComments(path) {
|
|
459
|
+
if (!path?.parent) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
if (!('comments' in path.parent)) {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
return path.parent.comments;
|
|
466
|
+
}
|
|
467
|
+
function shouldIgnoreFile(programNodePath) {
|
|
468
|
+
if (!programNodePath) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
472
|
+
return getParentComments(programNodePath).some(c => COMMENT_FILE_RE.test(c.value));
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* `programVisitor` is a `babel` adaptor for instrumentation.
|
|
476
|
+
*
|
|
477
|
+
* It returns an object with two methods `enter` and `exit`.
|
|
478
|
+
* These should be assigned to or called from `Program` entry and exit functions
|
|
479
|
+
* in a babel visitor.
|
|
480
|
+
*
|
|
481
|
+
* These functions do not make assumptions about the state set by Babel and thus
|
|
482
|
+
* can be used in a context other than a Babel plugin.
|
|
483
|
+
*
|
|
484
|
+
* The exit function returns an object that currently has the following keys:
|
|
485
|
+
*
|
|
486
|
+
* `fileCoverage` - the file coverage object created for the source file.
|
|
487
|
+
* `sourceMappingURL` - any source mapping URL found when processing the file.
|
|
488
|
+
*
|
|
489
|
+
* @param types - an instance of babel-types.
|
|
490
|
+
* @param sourceFilePath - the path to source file.
|
|
491
|
+
* @param opts - additional options.
|
|
492
|
+
*/
|
|
493
|
+
function programVisitor(types, inputSourceMapConsumer, opts) {
|
|
494
|
+
opts = { ...opts };
|
|
495
|
+
const visitState = new VisitState(types, inputSourceMapConsumer, opts.ignoreClassMethods, opts.reportLogic, opts.shouldInstrumentCallback);
|
|
496
|
+
return {
|
|
497
|
+
enter(path) {
|
|
498
|
+
if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (alreadyInstrumented(path, visitState)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
path.traverse(codeVisitor, visitState);
|
|
505
|
+
},
|
|
506
|
+
exit(path) {
|
|
507
|
+
if (alreadyInstrumented(path, visitState)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const originData = visitState.origins;
|
|
511
|
+
if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const body = path.node.body;
|
|
515
|
+
if (opts.codeToPrepend) {
|
|
516
|
+
const codeToPrependAst = (0, core_1.parse)(opts.codeToPrepend, { sourceType: 'script' });
|
|
517
|
+
if (codeToPrependAst !== null) {
|
|
518
|
+
body.unshift(...codeToPrependAst.program.body);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Add a variable definition for each origin file on top of the file.
|
|
522
|
+
for (const [originPath, originId] of originData.originToIdMap.entries()) {
|
|
523
|
+
const declaration = newStringConstDeclarationNode(originId, originPath);
|
|
524
|
+
body.unshift(declaration);
|
|
525
|
+
}
|
|
526
|
+
// Add a token for signaling that the file has been instrumented.
|
|
527
|
+
if (opts.isInstrumentedToken) {
|
|
528
|
+
types.addComment(path.node, 'leading', opts.isInstrumentedToken, false);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
exports.programVisitor = programVisitor;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teamscale/lib-instrument",
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"description": "Library for adding coverage statements to JS code; forked from istanbul-lib-coverage",
|
|
5
|
+
"author": "Krishnan Anantheswaran <kananthmail-github@yahoo.com>",
|
|
6
|
+
"maintainers": [
|
|
7
|
+
{
|
|
8
|
+
"name": "CQSE GmbH"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"lib"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@babel/core": "^7.23.2",
|
|
18
|
+
"@babel/parser": "^7.23.0",
|
|
19
|
+
"@babel/traverse": "^7.23.2",
|
|
20
|
+
"@types/node": "^20.8.10",
|
|
21
|
+
"semver": "^7.5.4",
|
|
22
|
+
"source-map": "^0.7.4",
|
|
23
|
+
"typescript": "^5.2.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@babel/cli": "^7.23.0",
|
|
27
|
+
"@babel/types": "^7.23.0",
|
|
28
|
+
"@types/babel__core": "^7.20.3",
|
|
29
|
+
"@types/babel__parser": "^7.1.1",
|
|
30
|
+
"@types/chai": "^4.3.9",
|
|
31
|
+
"@types/clone": "^2.1.3",
|
|
32
|
+
"@types/js-yaml": "^4.0.9",
|
|
33
|
+
"@types/jest": "^29.5.6",
|
|
34
|
+
"babel-jest": "^29.7.0",
|
|
35
|
+
"chai": "^4.2.0",
|
|
36
|
+
"clone": "^2.1.2",
|
|
37
|
+
"debug": "^4.3.4",
|
|
38
|
+
"documentation": "^14.0.2",
|
|
39
|
+
"js-yaml": "^4.1.0",
|
|
40
|
+
"ts-jest": "^29.1.1",
|
|
41
|
+
"jest": "^29.7.0",
|
|
42
|
+
"nopt": "^4.0.1",
|
|
43
|
+
"nyc": "^15.1.0",
|
|
44
|
+
"rimraf": "^5.0.5"
|
|
45
|
+
},
|
|
46
|
+
"license": "BSD-3-Clause",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/cqse/teamscale-javascript-profiler.git",
|
|
50
|
+
"directory": "packages/lib-instrument"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
57
|
+
"build": "tsc",
|
|
58
|
+
"test": "pnpm build && NODE_OPTIONS='--experimental-vm-modules --max-old-space-size=8192' jest --forceExit --coverage --silent=true --detectOpenHandles"
|
|
59
|
+
}
|
|
60
|
+
}
|