@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 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
@@ -0,0 +1,3 @@
1
+ ## Lib Instrument
2
+
3
+ Forked and evolved version of [Istanbul Lib Instrument](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-lib-instrument).
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;
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }