@venizia/dev-configs 0.4.3 → 0.4.5
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.md +2 -5
- package/README.md +15 -2
- package/dist/checks/catalog.d.ts +39 -0
- package/dist/checks/catalog.d.ts.map +1 -0
- package/dist/checks/catalog.js +94 -0
- package/dist/checks/catalog.js.map +1 -0
- package/dist/checks/index.d.ts +15 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +31 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/checks/module-cycles.d.ts +22 -0
- package/dist/checks/module-cycles.d.ts.map +1 -0
- package/dist/checks/module-cycles.js +149 -0
- package/dist/checks/module-cycles.js.map +1 -0
- package/dist/checks/purity/manifest.d.ts +94 -0
- package/dist/checks/purity/manifest.d.ts.map +1 -0
- package/dist/checks/purity/manifest.js +177 -0
- package/dist/checks/purity/manifest.js.map +1 -0
- package/dist/checks/purity/probe.d.ts +68 -0
- package/dist/checks/purity/probe.d.ts.map +1 -0
- package/dist/checks/purity/probe.js +295 -0
- package/dist/checks/purity/probe.js.map +1 -0
- package/dist/checks/purity/run.d.ts +47 -0
- package/dist/checks/purity/run.d.ts.map +1 -0
- package/dist/checks/purity/run.js +121 -0
- package/dist/checks/purity/run.js.map +1 -0
- package/dist/checks/split-report.d.ts +19 -0
- package/dist/checks/split-report.d.ts.map +1 -0
- package/dist/checks/split-report.js +123 -0
- package/dist/checks/split-report.js.map +1 -0
- package/dist/checks/test-controls.d.ts +46 -0
- package/dist/checks/test-controls.d.ts.map +1 -0
- package/dist/checks/test-controls.js +78 -0
- package/dist/checks/test-controls.js.map +1 -0
- package/dist/checks/test-dead-code.d.ts +18 -0
- package/dist/checks/test-dead-code.d.ts.map +1 -0
- package/dist/checks/test-dead-code.js +50 -0
- package/dist/checks/test-dead-code.js.map +1 -0
- package/dist/checks/test-mutate.d.ts +58 -0
- package/dist/checks/test-mutate.d.ts.map +1 -0
- package/dist/checks/test-mutate.js +165 -0
- package/dist/checks/test-mutate.js.map +1 -0
- package/dist/checks/test-weight.d.ts +64 -0
- package/dist/checks/test-weight.d.ts.map +1 -0
- package/dist/checks/test-weight.js +124 -0
- package/dist/checks/test-weight.js.map +1 -0
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +319 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/codemod/const-classes.d.ts +80 -0
- package/dist/cli/codemod/const-classes.d.ts.map +1 -0
- package/dist/cli/codemod/const-classes.js +603 -0
- package/dist/cli/codemod/const-classes.js.map +1 -0
- package/dist/cli/codemod/drop-redundant-casts.d.ts +112 -0
- package/dist/cli/codemod/drop-redundant-casts.d.ts.map +1 -0
- package/dist/cli/codemod/drop-redundant-casts.js +780 -0
- package/dist/cli/codemod/drop-redundant-casts.js.map +1 -0
- package/dist/cli/codemod/index.d.ts +2 -1
- package/dist/cli/codemod/index.d.ts.map +1 -1
- package/dist/cli/codemod/index.js +25 -7
- package/dist/cli/codemod/index.js.map +1 -1
- package/dist/cli/codemod/model-settings.d.ts.map +1 -1
- package/dist/cli/codemod/model-settings.js +39 -2
- package/dist/cli/codemod/model-settings.js.map +1 -1
- package/dist/cli/index.js +9 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/repo-tmp.d.ts +12 -0
- package/dist/testing/repo-tmp.d.ts.map +1 -0
- package/dist/testing/repo-tmp.js +67 -0
- package/dist/testing/repo-tmp.js.map +1 -0
- package/dist/testing/spawn.helper.d.ts +13 -0
- package/dist/testing/spawn.helper.d.ts.map +1 -0
- package/dist/testing/spawn.helper.js +22 -0
- package/dist/testing/spawn.helper.js.map +1 -0
- package/dist/testing/tmpdir-preload.d.ts +2 -0
- package/dist/testing/tmpdir-preload.d.ts.map +1 -0
- package/dist/testing/tmpdir-preload.js +11 -0
- package/dist/testing/tmpdir-preload.js.map +1 -0
- package/package.json +9 -1
- package/src/tsconfig/tsconfig.base.json +5 -2
- package/src/tsconfig/tsconfig.common.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { IPurityClaim, IPurityOptions } from './manifest';
|
|
2
|
+
/** The outcome of one row, as the text report and the JSON verdict both name it. */
|
|
3
|
+
export declare class PurityVerdicts {
|
|
4
|
+
static readonly PASS = "pass";
|
|
5
|
+
static readonly FAIL = "fail";
|
|
6
|
+
static readonly WAIVED = "waived";
|
|
7
|
+
static readonly MISSING = "missing";
|
|
8
|
+
static readonly STALE_WAIVER = "stale-waiver";
|
|
9
|
+
static readonly SCHEME_SET: Set<string>;
|
|
10
|
+
static isValid(value: string): value is TPurityVerdict;
|
|
11
|
+
}
|
|
12
|
+
export type TPurityVerdict = Extract<(typeof PurityVerdicts)[keyof typeof PurityVerdicts], string>;
|
|
13
|
+
/** One row of the `--json` verdict: nothing machine- or time-specific, so two runs on one commit diff empty. */
|
|
14
|
+
export interface IPurityRowVerdict {
|
|
15
|
+
label: string;
|
|
16
|
+
entry: string;
|
|
17
|
+
verdict: TPurityVerdict;
|
|
18
|
+
sizeBytes?: number;
|
|
19
|
+
builtins?: string[];
|
|
20
|
+
globals?: string[];
|
|
21
|
+
guardedGlobals?: string[];
|
|
22
|
+
external?: string[];
|
|
23
|
+
buildError?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface IPurityReport {
|
|
26
|
+
rows: IPurityRowVerdict[];
|
|
27
|
+
failed: number;
|
|
28
|
+
waived: number;
|
|
29
|
+
checked: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Probes every row of the claims and returns the report. `print` receives the human report line by
|
|
33
|
+
* line, in the exact shape the repository scripts printed before the engine moved here.
|
|
34
|
+
*/
|
|
35
|
+
export declare const checkPurity: (opts: {
|
|
36
|
+
root: string;
|
|
37
|
+
claims: IPurityClaim[];
|
|
38
|
+
options?: IPurityOptions;
|
|
39
|
+
/** Only the rows of this package; a token that matches nothing is a broken gate and returns `undefined`. */
|
|
40
|
+
filter?: string;
|
|
41
|
+
/** The claims file, repository-relative, named in the hint printed for a stale waiver. */
|
|
42
|
+
claimsFile: string;
|
|
43
|
+
print: (line: string) => void;
|
|
44
|
+
/** Where the no-match message goes (stderr for the command): it is an error, not a report line. */
|
|
45
|
+
printError: (line: string) => void;
|
|
46
|
+
}) => Promise<IPurityReport | undefined>;
|
|
47
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/checks/purity/run.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAgB,cAAc,EAAE,MAAM,YAAY,CAAC;AAI7E,oFAAoF;AACpF,qBAAa,cAAc;IACzB,MAAM,CAAC,QAAQ,CAAC,IAAI,UAAU;IAC9B,MAAM,CAAC,QAAQ,CAAC,IAAI,UAAU;IAC9B,MAAM,CAAC,QAAQ,CAAC,MAAM,YAAY;IAClC,MAAM,CAAC,QAAQ,CAAC,OAAO,aAAa;IACpC,MAAM,CAAC,QAAQ,CAAC,YAAY,kBAAkB;IAE9C,MAAM,CAAC,QAAQ,CAAC,UAAU,cAMvB;IAEH,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc;CAGvD;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC;AAEnG,gHAAgH;AAChH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAU,MAAM;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,4GAA4G;IAC5G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,mGAAmG;IACnG,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,KAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAgHpC,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkPurity = exports.PurityVerdicts = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const manifest_1 = require("./manifest");
|
|
7
|
+
const probe_1 = require("./probe");
|
|
8
|
+
/** The outcome of one row, as the text report and the JSON verdict both name it. */
|
|
9
|
+
class PurityVerdicts {
|
|
10
|
+
static { this.PASS = 'pass'; }
|
|
11
|
+
static { this.FAIL = 'fail'; }
|
|
12
|
+
static { this.WAIVED = 'waived'; }
|
|
13
|
+
static { this.MISSING = 'missing'; }
|
|
14
|
+
static { this.STALE_WAIVER = 'stale-waiver'; }
|
|
15
|
+
static { this.SCHEME_SET = new Set([
|
|
16
|
+
this.PASS,
|
|
17
|
+
this.FAIL,
|
|
18
|
+
this.WAIVED,
|
|
19
|
+
this.MISSING,
|
|
20
|
+
this.STALE_WAIVER,
|
|
21
|
+
]); }
|
|
22
|
+
static isValid(value) {
|
|
23
|
+
return this.SCHEME_SET.has(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.PurityVerdicts = PurityVerdicts;
|
|
27
|
+
const formatList = (values) => values.join(', ') || 'none';
|
|
28
|
+
/**
|
|
29
|
+
* Probes every row of the claims and returns the report. `print` receives the human report line by
|
|
30
|
+
* line, in the exact shape the repository scripts printed before the engine moved here.
|
|
31
|
+
*/
|
|
32
|
+
const checkPurity = async (opts) => {
|
|
33
|
+
const { root, claims, options, filter, claimsFile, print, printError } = opts;
|
|
34
|
+
const manifest = (0, manifest_1.buildManifest)({ claims, root, options });
|
|
35
|
+
const rows = filter ? manifest.filter((row) => row.package === filter) : manifest;
|
|
36
|
+
// Non-zero, not a friendly note: a mistyped `package` or a renamed directory would otherwise turn
|
|
37
|
+
// a release-blocking gate into a silent no-op that still reports success.
|
|
38
|
+
if (rows.length === 0) {
|
|
39
|
+
const known = [...new Set(manifest.map((row) => row.package))].sort().join(', ');
|
|
40
|
+
printError(`[purity] No manifest entry matches "${filter}" | Known packages: ${known} - a filter that ` +
|
|
41
|
+
`matches nothing is a broken gate, not an empty one.`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
45
|
+
const verdicts = [];
|
|
46
|
+
let failed = 0;
|
|
47
|
+
let waived = 0;
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const entry = (0, node_path_1.join)(root, row.entry);
|
|
50
|
+
// The `exports` map is the source of these rows, so a missing file is either an unbuilt package
|
|
51
|
+
// or a manifest pointing at something the build never emits - both fatal, and neither is worth
|
|
52
|
+
// reporting as a bundler error twenty lines long.
|
|
53
|
+
if (!(0, node_fs_1.existsSync)(entry)) {
|
|
54
|
+
failed += 1;
|
|
55
|
+
print(` ✗ ${row.label.padEnd(labelWidth)} MISSING ${row.entry}`);
|
|
56
|
+
print(` The package's \`exports\` map points here and nothing is built - run \`make ${row.package}\``);
|
|
57
|
+
verdicts.push({ label: row.label, entry: row.entry, verdict: PurityVerdicts.MISSING });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const result = await (0, probe_1.probeEntry)({
|
|
61
|
+
entry,
|
|
62
|
+
cwd: root,
|
|
63
|
+
external: row.external,
|
|
64
|
+
});
|
|
65
|
+
const detail = {
|
|
66
|
+
label: row.label,
|
|
67
|
+
entry: row.entry,
|
|
68
|
+
sizeBytes: result.sizeBytes,
|
|
69
|
+
builtins: result.builtins,
|
|
70
|
+
globals: result.globals,
|
|
71
|
+
guardedGlobals: result.guardedGlobals,
|
|
72
|
+
external: result.external,
|
|
73
|
+
// The bundler's message names absolute paths; the verdict must read the same on any machine.
|
|
74
|
+
...(result.buildError ? { buildError: result.buildError.replaceAll(root, '<root>') } : {}),
|
|
75
|
+
};
|
|
76
|
+
// Guarded reads are printed on the passing path too: they are safe, but a reviewer needs to
|
|
77
|
+
// see that the entry is clean by construction and not merely by the shape of one expression.
|
|
78
|
+
const guarded = result.guardedGlobals.length > 0 ? ` | guarded: ${result.guardedGlobals.join(', ')}` : '';
|
|
79
|
+
// Same reasoning as `guarded`, printed on both paths: an entry that externalises a dependency
|
|
80
|
+
// measures less than one that does not, and that must stay visible next to the verdict.
|
|
81
|
+
const external = result.external.length > 0 ? ` | external: ${result.external.join(', ')}` : '';
|
|
82
|
+
// Checked before the normal verdict, because for a waived row the two outcomes swap meaning.
|
|
83
|
+
if (row.expectImpure) {
|
|
84
|
+
// A waived entry that comes back pure fails: the waiver now describes something untrue, and
|
|
85
|
+
// the reader who trusts it next will be trusting it about a different entry.
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
failed += 1;
|
|
88
|
+
print(` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`);
|
|
89
|
+
print(` Drop it from \`impure\` in ${claimsFile} so the gate measures it again`);
|
|
90
|
+
verdicts.push({ ...detail, verdict: PurityVerdicts.STALE_WAIVER });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
waived += 1;
|
|
94
|
+
print(` ~ ${row.label.padEnd(labelWidth)} waived | builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${external}`);
|
|
95
|
+
verdicts.push({ ...detail, verdict: PurityVerdicts.WAIVED });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (result.ok) {
|
|
99
|
+
print(` ✓ ${row.label.padEnd(labelWidth)} ${(result.sizeBytes / 1024).toFixed(1)} KB${guarded}${external}`);
|
|
100
|
+
verdicts.push({ ...detail, verdict: PurityVerdicts.PASS });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
failed += 1;
|
|
104
|
+
print(` ✗ ${row.label.padEnd(labelWidth)} builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${guarded}${external}`);
|
|
105
|
+
if (result.buildError) {
|
|
106
|
+
print(` ${result.buildError.split('\n')[0]}`);
|
|
107
|
+
}
|
|
108
|
+
verdicts.push({ ...detail, verdict: PurityVerdicts.FAIL });
|
|
109
|
+
}
|
|
110
|
+
const note = waived > 0 ? ` (${waived} waived as known-impure)` : '';
|
|
111
|
+
if (failed > 0) {
|
|
112
|
+
print(`\n[purity] ${failed}/${rows.length} entries are not browser-pure${note}.`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const checked = rows.length - waived;
|
|
116
|
+
print(`\n[purity] ${checked}/${checked} entries are browser-pure${note}.`);
|
|
117
|
+
}
|
|
118
|
+
return { rows: verdicts, failed, waived, checked: rows.length - waived };
|
|
119
|
+
};
|
|
120
|
+
exports.checkPurity = checkPurity;
|
|
121
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/checks/purity/run.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { buildManifest } from './manifest';\nimport type { IPurityClaim, IPurityEntry, IPurityOptions } from './manifest';\nimport { probeEntry } from './probe';\nimport type { IPurityResult } from './probe';\n\n/** The outcome of one row, as the text report and the JSON verdict both name it. */\nexport class PurityVerdicts {\n static readonly PASS = 'pass';\n static readonly FAIL = 'fail';\n static readonly WAIVED = 'waived';\n static readonly MISSING = 'missing';\n static readonly STALE_WAIVER = 'stale-waiver';\n\n static readonly SCHEME_SET = new Set<string>([\n this.PASS,\n this.FAIL,\n this.WAIVED,\n this.MISSING,\n this.STALE_WAIVER,\n ]);\n\n static isValid(value: string): value is TPurityVerdict {\n return this.SCHEME_SET.has(value);\n }\n}\n\nexport type TPurityVerdict = Extract<(typeof PurityVerdicts)[keyof typeof PurityVerdicts], string>;\n\n/** One row of the `--json` verdict: nothing machine- or time-specific, so two runs on one commit diff empty. */\nexport interface IPurityRowVerdict {\n label: string;\n entry: string;\n verdict: TPurityVerdict;\n sizeBytes?: number;\n builtins?: string[];\n globals?: string[];\n guardedGlobals?: string[];\n external?: string[];\n buildError?: string;\n}\n\nexport interface IPurityReport {\n rows: IPurityRowVerdict[];\n failed: number;\n waived: number;\n checked: number;\n}\n\nconst formatList = (values: string[]): string => values.join(', ') || 'none';\n\n/**\n * Probes every row of the claims and returns the report. `print` receives the human report line by\n * line, in the exact shape the repository scripts printed before the engine moved here.\n */\nexport const checkPurity = async (opts: {\n root: string;\n claims: IPurityClaim[];\n options?: IPurityOptions;\n /** Only the rows of this package; a token that matches nothing is a broken gate and returns `undefined`. */\n filter?: string;\n /** The claims file, repository-relative, named in the hint printed for a stale waiver. */\n claimsFile: string;\n print: (line: string) => void;\n /** Where the no-match message goes (stderr for the command): it is an error, not a report line. */\n printError: (line: string) => void;\n}): Promise<IPurityReport | undefined> => {\n const { root, claims, options, filter, claimsFile, print, printError } = opts;\n const manifest = buildManifest({ claims, root, options });\n const rows: IPurityEntry[] = filter ? manifest.filter((row) => row.package === filter) : manifest;\n\n // Non-zero, not a friendly note: a mistyped `package` or a renamed directory would otherwise turn\n // a release-blocking gate into a silent no-op that still reports success.\n if (rows.length === 0) {\n const known = [...new Set(manifest.map((row) => row.package))].sort().join(', ');\n printError(\n `[purity] No manifest entry matches \"${filter}\" | Known packages: ${known} - a filter that ` +\n `matches nothing is a broken gate, not an empty one.`,\n );\n return undefined;\n }\n\n const labelWidth = Math.max(...rows.map((row) => row.label.length));\n const verdicts: IPurityRowVerdict[] = [];\n let failed = 0;\n let waived = 0;\n\n for (const row of rows) {\n const entry = join(root, row.entry);\n\n // The `exports` map is the source of these rows, so a missing file is either an unbuilt package\n // or a manifest pointing at something the build never emits - both fatal, and neither is worth\n // reporting as a bundler error twenty lines long.\n if (!existsSync(entry)) {\n failed += 1;\n print(` ✗ ${row.label.padEnd(labelWidth)} MISSING ${row.entry}`);\n print(\n ` The package's \\`exports\\` map points here and nothing is built - run \\`make ${row.package}\\``,\n );\n verdicts.push({ label: row.label, entry: row.entry, verdict: PurityVerdicts.MISSING });\n continue;\n }\n\n const result: IPurityResult = await probeEntry({\n entry,\n cwd: root,\n external: row.external,\n });\n\n const detail = {\n label: row.label,\n entry: row.entry,\n sizeBytes: result.sizeBytes,\n builtins: result.builtins,\n globals: result.globals,\n guardedGlobals: result.guardedGlobals,\n external: result.external,\n // The bundler's message names absolute paths; the verdict must read the same on any machine.\n ...(result.buildError ? { buildError: result.buildError.replaceAll(root, '<root>') } : {}),\n };\n\n // Guarded reads are printed on the passing path too: they are safe, but a reviewer needs to\n // see that the entry is clean by construction and not merely by the shape of one expression.\n const guarded =\n result.guardedGlobals.length > 0 ? ` | guarded: ${result.guardedGlobals.join(', ')}` : '';\n\n // Same reasoning as `guarded`, printed on both paths: an entry that externalises a dependency\n // measures less than one that does not, and that must stay visible next to the verdict.\n const external = result.external.length > 0 ? ` | external: ${result.external.join(', ')}` : '';\n\n // Checked before the normal verdict, because for a waived row the two outcomes swap meaning.\n if (row.expectImpure) {\n // A waived entry that comes back pure fails: the waiver now describes something untrue, and\n // the reader who trusts it next will be trusting it about a different entry.\n if (result.ok) {\n failed += 1;\n print(` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`);\n print(` Drop it from \\`impure\\` in ${claimsFile} so the gate measures it again`);\n verdicts.push({ ...detail, verdict: PurityVerdicts.STALE_WAIVER });\n continue;\n }\n\n waived += 1;\n print(\n ` ~ ${row.label.padEnd(labelWidth)} waived | builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.WAIVED });\n continue;\n }\n\n if (result.ok) {\n print(\n ` ✓ ${row.label.padEnd(labelWidth)} ${(result.sizeBytes / 1024).toFixed(1)} KB${guarded}${external}`,\n );\n verdicts.push({ ...detail, verdict: PurityVerdicts.PASS });\n continue;\n }\n\n failed += 1;\n print(\n ` ✗ ${row.label.padEnd(labelWidth)} builtins: ${formatList(result.builtins)} | globals: ${formatList(result.globals)}${guarded}${external}`,\n );\n if (result.buildError) {\n print(` ${result.buildError.split('\\n')[0]}`);\n }\n verdicts.push({ ...detail, verdict: PurityVerdicts.FAIL });\n }\n\n const note = waived > 0 ? ` (${waived} waived as known-impure)` : '';\n\n if (failed > 0) {\n print(`\\n[purity] ${failed}/${rows.length} entries are not browser-pure${note}.`);\n } else {\n const checked = rows.length - waived;\n print(`\\n[purity] ${checked}/${checked} entries are browser-pure${note}.`);\n }\n\n return { rows: verdicts, failed, waived, checked: rows.length - waived };\n};\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class SplitReport {
|
|
2
|
+
/** `distDir` is the built ESM folder inside a package (`dist/esm`, or `dist` where one build is published); `print` takes the report line by line. */
|
|
3
|
+
static run(opts: {
|
|
4
|
+
packageDir: string;
|
|
5
|
+
distDir?: string;
|
|
6
|
+
print?: (line: string) => void;
|
|
7
|
+
}): void;
|
|
8
|
+
/** Every packages/* and examples/* directory that has a src/ folder. */
|
|
9
|
+
static defaultTargets(opts?: {
|
|
10
|
+
root?: string;
|
|
11
|
+
}): string[];
|
|
12
|
+
private static section;
|
|
13
|
+
private static facts;
|
|
14
|
+
/** abstract.ts + base.ts pairs are the format; exactly one of each is one concept. */
|
|
15
|
+
private static isAbstractBasePair;
|
|
16
|
+
private static walk;
|
|
17
|
+
private static foldersWithoutIndex;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=split-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-report.d.ts","sourceRoot":"","sources":["../../src/checks/split-report.ts"],"names":[],"mappings":"AA2BA,qBAAa,WAAW;IACtB,sJAAsJ;IACtJ,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI;IA4ChG,wEAAwE;IACxE,MAAM,CAAC,cAAc,CAAC,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,MAAM,EAAE;IAY7D,OAAO,CAAC,MAAM,CAAC,OAAO;IAWtB,OAAO,CAAC,MAAM,CAAC,KAAK;IAUpB,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAKjC,OAAO,CAAC,MAAM,CAAC,IAAI;IAiBnB,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAqBnC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SplitReport = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Informational report behind the file-splitting convention (conventions/file-splitting.md):
|
|
6
|
+
* hub candidates, stray types files, scope folders without a barrel, files over the soft mark,
|
|
7
|
+
* and import cycles per package. Never a gate. Usage: devz check split-report [--dist <dir>] [packages/kernel]
|
|
8
|
+
* - with no argument, scans every packages/* and examples/* directory that has a src/.
|
|
9
|
+
*/
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const module_cycles_1 = require("./module-cycles");
|
|
13
|
+
const SKIP = new Set(['node_modules', 'dist', '__tests__', 'generated', '.turbo']);
|
|
14
|
+
const SOFT_LINE_MARK = 500;
|
|
15
|
+
const HUB_EXPORT_MARK = 10;
|
|
16
|
+
const DECLARATION_KEYWORDS = ['class', 'const', 'type', 'interface', 'function', 'enum', 'let'];
|
|
17
|
+
const DECLARATION = new RegExp(`^export\\s+(?:default\\s+)?(?:abstract\\s+)?(${DECLARATION_KEYWORDS.join('|')})\\s+(\\w+)`, 'gm');
|
|
18
|
+
const EXPORTED_CLASS = /^export\s+(?:abstract\s+)?class\s+(\w+)/gm;
|
|
19
|
+
class SplitReport {
|
|
20
|
+
/** `distDir` is the built ESM folder inside a package (`dist/esm`, or `dist` where one build is published); `print` takes the report line by line. */
|
|
21
|
+
static run(opts) {
|
|
22
|
+
const { distDir = 'dist', print = console.log } = opts;
|
|
23
|
+
const src = (0, node_path_1.join)(opts.packageDir, 'src');
|
|
24
|
+
if (!(0, node_fs_1.existsSync)(src)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const facts = SplitReport.walk({ dir: src }).map((file) => SplitReport.facts({ file }));
|
|
28
|
+
const rel = (file) => (0, node_path_1.relative)(opts.packageDir, file);
|
|
29
|
+
const hubs = facts.filter((f) => ['types.ts', 'constants.ts'].includes((0, node_path_1.basename)(f.file)) && f.exports > HUB_EXPORT_MARK);
|
|
30
|
+
const multiClass = facts.filter((f) => f.classes.length >= 2 && !SplitReport.isAbstractBasePair({ facts: f }));
|
|
31
|
+
const strayTypes = facts.filter((f) => ['types.ts', 'constants.ts'].includes((0, node_path_1.basename)(f.file)) && !f.file.includes('/common/'));
|
|
32
|
+
const long = facts.filter((f) => f.lines > SOFT_LINE_MARK);
|
|
33
|
+
const noBarrel = SplitReport.foldersWithoutIndex({ dir: src });
|
|
34
|
+
const esm = (0, node_path_1.join)(opts.packageDir, distDir);
|
|
35
|
+
const cycles = (0, node_fs_1.existsSync)(esm) ? module_cycles_1.ModuleGraph.fromDirectory({ dir: esm }).cycles().length : -1;
|
|
36
|
+
print(`\n## ${(0, node_path_1.basename)(opts.packageDir)} - ${facts.length} files`);
|
|
37
|
+
const hubTitle = `hub candidates (> ${HUB_EXPORT_MARK} exports)`;
|
|
38
|
+
const hubRows = hubs.map((f) => `${rel(f.file)} (${f.exports})`);
|
|
39
|
+
SplitReport.section({ title: hubTitle, rows: hubRows, print });
|
|
40
|
+
const classTitle = 'files with 2+ exported classes';
|
|
41
|
+
const classRows = multiClass.map((f) => `${rel(f.file)} (${f.classes.join(', ')})`);
|
|
42
|
+
SplitReport.section({ title: classTitle, rows: classRows, print });
|
|
43
|
+
const strayTitle = 'types/constants outside common/';
|
|
44
|
+
const strayRows = strayTypes.map((f) => rel(f.file));
|
|
45
|
+
SplitReport.section({ title: strayTitle, rows: strayRows, print });
|
|
46
|
+
const barrelTitle = 'scope folders without index.ts';
|
|
47
|
+
const barrelRows = noBarrel.map((d) => (0, node_path_1.relative)(opts.packageDir, d));
|
|
48
|
+
SplitReport.section({ title: barrelTitle, rows: barrelRows, print });
|
|
49
|
+
const longTitle = `over ${SOFT_LINE_MARK} lines (a prompt to explain, not a defect)`;
|
|
50
|
+
const longRows = long.map((f) => `${rel(f.file)} (${f.lines})`);
|
|
51
|
+
SplitReport.section({ title: longTitle, rows: longRows, print });
|
|
52
|
+
const cycleMsg = cycles < 0 ? 'no dist' : cycles;
|
|
53
|
+
print(`- import cycles in ${distDir}: ${cycleMsg}`);
|
|
54
|
+
}
|
|
55
|
+
/** Every packages/* and examples/* directory that has a src/ folder. */
|
|
56
|
+
static defaultTargets(opts = {}) {
|
|
57
|
+
const root = opts.root ?? process.cwd();
|
|
58
|
+
const withSource = (folder) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, folder))
|
|
59
|
+
? (0, node_fs_1.readdirSync)((0, node_path_1.resolve)(root, folder))
|
|
60
|
+
.map((name) => (0, node_path_1.resolve)(root, folder, name))
|
|
61
|
+
.filter((dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'src')))
|
|
62
|
+
: [];
|
|
63
|
+
return [...withSource('packages'), ...withSource('examples')];
|
|
64
|
+
}
|
|
65
|
+
static section(opts) {
|
|
66
|
+
opts.print(`- ${opts.title}: ${opts.rows.length}`);
|
|
67
|
+
for (const row of opts.rows) {
|
|
68
|
+
opts.print(` ${row}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
static facts(opts) {
|
|
72
|
+
const text = (0, node_fs_1.readFileSync)(opts.file, 'utf8');
|
|
73
|
+
return {
|
|
74
|
+
file: opts.file,
|
|
75
|
+
lines: text.split('\n').length,
|
|
76
|
+
exports: [...text.matchAll(DECLARATION)].length,
|
|
77
|
+
classes: [...text.matchAll(EXPORTED_CLASS)].map((match) => match[1]),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** abstract.ts + base.ts pairs are the format; exactly one of each is one concept. */
|
|
81
|
+
static isAbstractBasePair(opts) {
|
|
82
|
+
const [first, second] = opts.facts.classes;
|
|
83
|
+
return opts.facts.classes.length === 2 && /Abstract/.test(first) && /Base/.test(second);
|
|
84
|
+
}
|
|
85
|
+
static walk(opts) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const dirEntries = (0, node_fs_1.readdirSync)(opts.dir);
|
|
88
|
+
for (const name of dirEntries) {
|
|
89
|
+
if (SKIP.has(name)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const full = (0, node_path_1.join)(opts.dir, name);
|
|
93
|
+
if ((0, node_fs_1.statSync)(full).isDirectory()) {
|
|
94
|
+
out.push(...SplitReport.walk({ dir: full }));
|
|
95
|
+
}
|
|
96
|
+
else if (full.endsWith('.ts') && !full.endsWith('.d.ts')) {
|
|
97
|
+
out.push(full);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
static foldersWithoutIndex(opts) {
|
|
103
|
+
const out = [];
|
|
104
|
+
const dirEntries = (0, node_fs_1.readdirSync)(opts.dir);
|
|
105
|
+
for (const name of dirEntries) {
|
|
106
|
+
if (SKIP.has(name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const full = (0, node_path_1.join)(opts.dir, name);
|
|
110
|
+
if (!(0, node_fs_1.statSync)(full).isDirectory()) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const hasTs = (0, node_fs_1.readdirSync)(full).some((entry) => entry.endsWith('.ts') && entry !== 'index.ts');
|
|
114
|
+
if (hasTs && !(0, node_fs_1.existsSync)((0, node_path_1.join)(full, 'index.ts'))) {
|
|
115
|
+
out.push(full);
|
|
116
|
+
}
|
|
117
|
+
out.push(...SplitReport.foldersWithoutIndex({ dir: full }));
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.SplitReport = SplitReport;
|
|
123
|
+
//# sourceMappingURL=split-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"split-report.js","sourceRoot":"","sources":["../../src/checks/split-report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Informational report behind the file-splitting convention (conventions/file-splitting.md):\n * hub candidates, stray types files, scope folders without a barrel, files over the soft mark,\n * and import cycles per package. Never a gate. Usage: devz check split-report [--dist <dir>] [packages/kernel]\n * - with no argument, scans every packages/* and examples/* directory that has a src/.\n */\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { basename, join, relative, resolve } from 'node:path';\nimport { ModuleGraph } from './module-cycles';\n\nconst SKIP = new Set(['node_modules', 'dist', '__tests__', 'generated', '.turbo']);\nconst SOFT_LINE_MARK = 500;\nconst HUB_EXPORT_MARK = 10;\nconst DECLARATION_KEYWORDS = ['class', 'const', 'type', 'interface', 'function', 'enum', 'let'];\nconst DECLARATION = new RegExp(\n `^export\\\\s+(?:default\\\\s+)?(?:abstract\\\\s+)?(${DECLARATION_KEYWORDS.join('|')})\\\\s+(\\\\w+)`,\n 'gm',\n);\nconst EXPORTED_CLASS = /^export\\s+(?:abstract\\s+)?class\\s+(\\w+)/gm;\n\ninterface IFileFacts {\n file: string;\n lines: number;\n exports: number;\n classes: string[];\n}\n\nexport class SplitReport {\n /** `distDir` is the built ESM folder inside a package (`dist/esm`, or `dist` where one build is published); `print` takes the report line by line. */\n static run(opts: { packageDir: string; distDir?: string; print?: (line: string) => void }): void {\n const { distDir = 'dist', print = console.log } = opts;\n const src = join(opts.packageDir, 'src');\n if (!existsSync(src)) {\n return;\n }\n const facts = SplitReport.walk({ dir: src }).map((file) => SplitReport.facts({ file }));\n const rel = (file: string) => relative(opts.packageDir, file);\n\n const hubs = facts.filter(\n (f) => ['types.ts', 'constants.ts'].includes(basename(f.file)) && f.exports > HUB_EXPORT_MARK,\n );\n const multiClass = facts.filter(\n (f) => f.classes.length >= 2 && !SplitReport.isAbstractBasePair({ facts: f }),\n );\n const strayTypes = facts.filter(\n (f) =>\n ['types.ts', 'constants.ts'].includes(basename(f.file)) && !f.file.includes('/common/'),\n );\n const long = facts.filter((f) => f.lines > SOFT_LINE_MARK);\n const noBarrel = SplitReport.foldersWithoutIndex({ dir: src });\n const esm = join(opts.packageDir, distDir);\n const cycles = existsSync(esm) ? ModuleGraph.fromDirectory({ dir: esm }).cycles().length : -1;\n\n print(`\\n## ${basename(opts.packageDir)} - ${facts.length} files`);\n const hubTitle = `hub candidates (> ${HUB_EXPORT_MARK} exports)`;\n const hubRows = hubs.map((f) => `${rel(f.file)} (${f.exports})`);\n SplitReport.section({ title: hubTitle, rows: hubRows, print });\n const classTitle = 'files with 2+ exported classes';\n const classRows = multiClass.map((f) => `${rel(f.file)} (${f.classes.join(', ')})`);\n SplitReport.section({ title: classTitle, rows: classRows, print });\n const strayTitle = 'types/constants outside common/';\n const strayRows = strayTypes.map((f) => rel(f.file));\n SplitReport.section({ title: strayTitle, rows: strayRows, print });\n const barrelTitle = 'scope folders without index.ts';\n const barrelRows = noBarrel.map((d) => relative(opts.packageDir, d));\n SplitReport.section({ title: barrelTitle, rows: barrelRows, print });\n const longTitle = `over ${SOFT_LINE_MARK} lines (a prompt to explain, not a defect)`;\n const longRows = long.map((f) => `${rel(f.file)} (${f.lines})`);\n SplitReport.section({ title: longTitle, rows: longRows, print });\n const cycleMsg = cycles < 0 ? 'no dist' : cycles;\n print(`- import cycles in ${distDir}: ${cycleMsg}`);\n }\n\n /** Every packages/* and examples/* directory that has a src/ folder. */\n static defaultTargets(opts: { root?: string } = {}): string[] {\n const root = opts.root ?? process.cwd();\n const withSource = (folder: string): string[] =>\n existsSync(resolve(root, folder))\n ? readdirSync(resolve(root, folder))\n .map((name) => resolve(root, folder, name))\n .filter((dir) => existsSync(join(dir, 'src')))\n : [];\n\n return [...withSource('packages'), ...withSource('examples')];\n }\n\n private static section(opts: {\n title: string;\n rows: string[];\n print: (line: string) => void;\n }): void {\n opts.print(`- ${opts.title}: ${opts.rows.length}`);\n for (const row of opts.rows) {\n opts.print(` ${row}`);\n }\n }\n\n private static facts(opts: { file: string }): IFileFacts {\n const text = readFileSync(opts.file, 'utf8');\n return {\n file: opts.file,\n lines: text.split('\\n').length,\n exports: [...text.matchAll(DECLARATION)].length,\n classes: [...text.matchAll(EXPORTED_CLASS)].map((match) => match[1]),\n };\n }\n\n /** abstract.ts + base.ts pairs are the format; exactly one of each is one concept. */\n private static isAbstractBasePair(opts: { facts: IFileFacts }): boolean {\n const [first, second] = opts.facts.classes;\n return opts.facts.classes.length === 2 && /Abstract/.test(first) && /Base/.test(second);\n }\n\n private static walk(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n const full = join(opts.dir, name);\n if (statSync(full).isDirectory()) {\n out.push(...SplitReport.walk({ dir: full }));\n } else if (full.endsWith('.ts') && !full.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n }\n\n private static foldersWithoutIndex(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n const full = join(opts.dir, name);\n if (!statSync(full).isDirectory()) {\n continue;\n }\n const hasTs = readdirSync(full).some(\n (entry) => entry.endsWith('.ts') && entry !== 'index.ts',\n );\n if (hasTs && !existsSync(join(full, 'index.ts'))) {\n out.push(full);\n }\n out.push(...SplitReport.foldersWithoutIndex({ dir: full }));\n }\n return out;\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken
|
|
3
|
+
* subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests
|
|
4
|
+
* method keeps a committed registry of them and fails when one disappears.
|
|
5
|
+
*/
|
|
6
|
+
export interface IControlEntry {
|
|
7
|
+
/** The gate the control belongs to; reported with a failure. */
|
|
8
|
+
gate: string;
|
|
9
|
+
/** Repository-relative path of the test file. */
|
|
10
|
+
file: string;
|
|
11
|
+
/**
|
|
12
|
+
* `test` (default): `file` is a test file and `titlePattern` is matched against its titles.
|
|
13
|
+
* `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the
|
|
14
|
+
* scanner must ignore). It only has to exist: without it the test's "not found" assertion passes for the
|
|
15
|
+
* wrong reason.
|
|
16
|
+
*/
|
|
17
|
+
kind?: 'test' | 'file';
|
|
18
|
+
/** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */
|
|
19
|
+
titlePattern?: string;
|
|
20
|
+
/** At least this many titles must match. Default 1. */
|
|
21
|
+
min?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface IControlProblem {
|
|
24
|
+
entry: IControlEntry;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
export interface IControlsReport {
|
|
28
|
+
checked: number;
|
|
29
|
+
problems: IControlProblem[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
|
|
33
|
+
* second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.
|
|
34
|
+
* A `test.each` title is returned as written (`%s` and all): the registry patterns are regular
|
|
35
|
+
* expressions, so the expansion does not matter.
|
|
36
|
+
*/
|
|
37
|
+
export declare const listTestTitles: (opts: {
|
|
38
|
+
source: string;
|
|
39
|
+
fileName?: string;
|
|
40
|
+
}) => Promise<string[]>;
|
|
41
|
+
/** Checks every entry of the registry against the test files it names. */
|
|
42
|
+
export declare const checkControls: (opts: {
|
|
43
|
+
root: string;
|
|
44
|
+
controls: IControlEntry[];
|
|
45
|
+
}) => Promise<IControlsReport>;
|
|
46
|
+
//# sourceMappingURL=test-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-controls.d.ts","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,MAAM,EAAE,CAgDnB,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,aAAa,GAAU,MAAM;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,KAAG,OAAO,CAAC,eAAe,CAkC1B,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkControls = exports.listTestTitles = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
/**
|
|
7
|
+
* The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
|
|
8
|
+
* second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.
|
|
9
|
+
* A `test.each` title is returned as written (`%s` and all): the registry patterns are regular
|
|
10
|
+
* expressions, so the expansion does not matter.
|
|
11
|
+
*/
|
|
12
|
+
const listTestTitles = async (opts) => {
|
|
13
|
+
const typescript = (await import('typescript')).default;
|
|
14
|
+
const file = typescript.createSourceFile(opts.fileName ?? 'file.test.ts', opts.source, typescript.ScriptTarget.Latest, true);
|
|
15
|
+
const titles = [];
|
|
16
|
+
const RUNNERS = new Set(['test', 'it', 'describe']);
|
|
17
|
+
const rootName = (expression) => {
|
|
18
|
+
if (typescript.isIdentifier(expression)) {
|
|
19
|
+
return expression.text;
|
|
20
|
+
}
|
|
21
|
+
if (typescript.isPropertyAccessExpression(expression)) {
|
|
22
|
+
return rootName(expression.expression);
|
|
23
|
+
}
|
|
24
|
+
if (typescript.isCallExpression(expression)) {
|
|
25
|
+
return rootName(expression.expression);
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
};
|
|
29
|
+
const visit = (node) => {
|
|
30
|
+
if (typescript.isCallExpression(node)) {
|
|
31
|
+
const first = node.arguments[0];
|
|
32
|
+
const root = rootName(node.expression);
|
|
33
|
+
if (root !== undefined &&
|
|
34
|
+
RUNNERS.has(root) &&
|
|
35
|
+
first &&
|
|
36
|
+
(typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))) {
|
|
37
|
+
titles.push(first.text);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
typescript.forEachChild(node, visit);
|
|
41
|
+
};
|
|
42
|
+
visit(file);
|
|
43
|
+
return titles;
|
|
44
|
+
};
|
|
45
|
+
exports.listTestTitles = listTestTitles;
|
|
46
|
+
/** Checks every entry of the registry against the test files it names. */
|
|
47
|
+
const checkControls = async (opts) => {
|
|
48
|
+
const problems = [];
|
|
49
|
+
for (const entry of opts.controls) {
|
|
50
|
+
const path = (0, node_path_1.join)(opts.root, entry.file);
|
|
51
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
52
|
+
problems.push({
|
|
53
|
+
entry,
|
|
54
|
+
message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (entry.kind === 'file') {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const titles = await (0, exports.listTestTitles)({
|
|
62
|
+
source: (0, node_fs_1.readFileSync)(path, 'utf8'),
|
|
63
|
+
fileName: entry.file,
|
|
64
|
+
});
|
|
65
|
+
const pattern = new RegExp(entry.titlePattern ?? '');
|
|
66
|
+
const matched = titles.filter((title) => pattern.test(title)).length;
|
|
67
|
+
const min = entry.min ?? 1;
|
|
68
|
+
if (matched < min) {
|
|
69
|
+
problems.push({
|
|
70
|
+
entry,
|
|
71
|
+
message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { checked: opts.controls.length, problems };
|
|
76
|
+
};
|
|
77
|
+
exports.checkControls = checkControls;
|
|
78
|
+
//# sourceMappingURL=test-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-controls.js","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type ts from 'typescript';\n\n/**\n * A positive control is a test that proves a gate can fail: a fixture that must be refused, a broken\n * subject the check must catch. Deleting one makes the gate pass for the wrong reason, so the trim-tests\n * method keeps a committed registry of them and fails when one disappears.\n */\nexport interface IControlEntry {\n /** The gate the control belongs to; reported with a failure. */\n gate: string;\n /** Repository-relative path of the test file. */\n file: string;\n /**\n * `test` (default): `file` is a test file and `titlePattern` is matched against its titles.\n * `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the\n * scanner must ignore). It only has to exist: without it the test's \"not found\" assertion passes for the\n * wrong reason.\n */\n kind?: 'test' | 'file';\n /** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */\n titlePattern?: string;\n /** At least this many titles must match. Default 1. */\n min?: number;\n}\n\nexport interface IControlProblem {\n entry: IControlEntry;\n message: string;\n}\n\nexport interface IControlsReport {\n checked: number;\n problems: IControlProblem[];\n}\n\n/**\n * The titles in a test file: the first string argument of `test`, `it`, `describe` and of the\n * second call of `test.each(rows)('title', ...)`, with their `.only`/`.skip`/`.if(...)` forms.\n * A `test.each` title is returned as written (`%s` and all): the registry patterns are regular\n * expressions, so the expansion does not matter.\n */\nexport const listTestTitles = async (opts: {\n source: string;\n fileName?: string;\n}): Promise<string[]> => {\n const typescript: typeof ts = (await import('typescript')).default;\n const file = typescript.createSourceFile(\n opts.fileName ?? 'file.test.ts',\n opts.source,\n typescript.ScriptTarget.Latest,\n true,\n );\n const titles: string[] = [];\n const RUNNERS = new Set(['test', 'it', 'describe']);\n\n const rootName = (expression: ts.Expression): string | undefined => {\n if (typescript.isIdentifier(expression)) {\n return expression.text;\n }\n\n if (typescript.isPropertyAccessExpression(expression)) {\n return rootName(expression.expression);\n }\n\n if (typescript.isCallExpression(expression)) {\n return rootName(expression.expression);\n }\n\n return undefined;\n };\n\n const visit = (node: ts.Node): void => {\n if (typescript.isCallExpression(node)) {\n const first = node.arguments[0];\n const root = rootName(node.expression);\n\n if (\n root !== undefined &&\n RUNNERS.has(root) &&\n first &&\n (typescript.isStringLiteral(first) || typescript.isNoSubstitutionTemplateLiteral(first))\n ) {\n titles.push(first.text);\n }\n }\n\n typescript.forEachChild(node, visit);\n };\n\n visit(file);\n\n return titles;\n};\n\n/** Checks every entry of the registry against the test files it names. */\nexport const checkControls = async (opts: {\n root: string;\n controls: IControlEntry[];\n}): Promise<IControlsReport> => {\n const problems: IControlProblem[] = [];\n\n for (const entry of opts.controls) {\n const path = join(opts.root, entry.file);\n if (!existsSync(path)) {\n problems.push({\n entry,\n message: `the ${entry.kind === 'file' ? 'negative fixture' : 'test file'} ${entry.file} does not exist`,\n });\n continue;\n }\n\n if (entry.kind === 'file') {\n continue;\n }\n\n const titles = await listTestTitles({\n source: readFileSync(path, 'utf8'),\n fileName: entry.file,\n });\n const pattern = new RegExp(entry.titlePattern ?? '');\n const matched = titles.filter((title) => pattern.test(title)).length;\n const min = entry.min ?? 1;\n\n if (matched < min) {\n problems.push({\n entry,\n message: `${matched} title(s) match /${entry.titlePattern ?? ''}/ in ${entry.file}, ${min} required | the control of gate '${entry.gate}' is gone or renamed`,\n });\n }\n }\n\n return { checked: opts.controls.length, problems };\n};\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second
|
|
3
|
+
* step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at
|
|
4
|
+
* run time or by scanning a folder, so a candidate is deleted only after the package suite is green
|
|
5
|
+
* without it.
|
|
6
|
+
*/
|
|
7
|
+
export interface IDeadTestCodeCandidate {
|
|
8
|
+
file: string;
|
|
9
|
+
/**
|
|
10
|
+
* `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does
|
|
11
|
+
* its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.
|
|
12
|
+
*/
|
|
13
|
+
kind: 'unreferenced' | 'folder-referenced';
|
|
14
|
+
}
|
|
15
|
+
export declare const findDeadTestCode: (opts: {
|
|
16
|
+
root: string;
|
|
17
|
+
}) => IDeadTestCodeCandidate[];
|
|
18
|
+
//# sourceMappingURL=test-dead-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-dead-code.d.ts","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,EAAE,cAAc,GAAG,mBAAmB,CAAC;CAC5C;AA6BD,eAAO,MAAM,gBAAgB,GAAI,MAAM;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAG,sBAAsB,EA+B/E,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDeadTestCode = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const SUPPORT_EXTENSION = /\.(?:[cm]?[jt]sx?|json|sql|txt)$/;
|
|
7
|
+
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
8
|
+
const git = (opts) => {
|
|
9
|
+
const result = (0, node_child_process_1.spawnSync)('git', opts.args, {
|
|
10
|
+
cwd: opts.root,
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '' };
|
|
15
|
+
};
|
|
16
|
+
/** Files other than `self` whose text contains `needle` (a fixed string). */
|
|
17
|
+
const referencedElsewhere = (opts) => {
|
|
18
|
+
const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });
|
|
19
|
+
return result.stdout.split('\n').some((line) => line !== '' && line !== opts.self);
|
|
20
|
+
};
|
|
21
|
+
const escapeForRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
/** Files other than `self` that mention the name of the file as a path segment or an import. */
|
|
23
|
+
const nameReferencedElsewhere = (opts) => {
|
|
24
|
+
const pattern = `[/'"\`]${escapeForRegex(opts.stem)}(\\.|'|"|\`|$)`;
|
|
25
|
+
const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });
|
|
26
|
+
return result.stdout.split('\n').some((line) => line !== '' && line !== opts.self);
|
|
27
|
+
};
|
|
28
|
+
const findDeadTestCode = (opts) => {
|
|
29
|
+
const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\n');
|
|
30
|
+
const support = tracked.filter((file) => file.includes('/__tests__/') &&
|
|
31
|
+
SUPPORT_EXTENSION.test(file) &&
|
|
32
|
+
!TEST_FILE.test(file) &&
|
|
33
|
+
!file.endsWith('.d.ts'));
|
|
34
|
+
const candidates = [];
|
|
35
|
+
for (const file of support) {
|
|
36
|
+
const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);
|
|
37
|
+
const stem = underTests.split('/').pop().replace(SUPPORT_EXTENSION, '');
|
|
38
|
+
const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');
|
|
39
|
+
if (nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||
|
|
40
|
+
referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const folder = (0, node_path_1.dirname)(underTests);
|
|
44
|
+
const folderReferenced = folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });
|
|
45
|
+
candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });
|
|
46
|
+
}
|
|
47
|
+
return candidates;
|
|
48
|
+
};
|
|
49
|
+
exports.findDeadTestCode = findDeadTestCode;
|
|
50
|
+
//# sourceMappingURL=test-dead-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":";;;AAAA,2DAA+C;AAC/C,yCAAoC;AAiBpC,MAAM,iBAAiB,GAAG,kCAAkC,CAAC;AAC7D,MAAM,SAAS,GAAG,gCAAgC,CAAC;AAEnD,MAAM,GAAG,GAAG,CAAC,IAAsC,EAAsC,EAAE;IACzF,MAAM,MAAM,GAAG,IAAA,8BAAS,EAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE;QACzC,GAAG,EAAE,IAAI,CAAC,IAAI;QACd,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;KAC7B,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AACrE,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,mBAAmB,GAAG,CAAC,IAAoD,EAAW,EAAE;IAC5F,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7F,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAE7F,gGAAgG;AAChG,MAAM,uBAAuB,GAAG,CAAC,IAAkD,EAAW,EAAE;IAC9F,MAAM,OAAO,GAAG,UAAU,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC;IACpE,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACzF,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC,CAAC;AAEK,MAAM,gBAAgB,GAAG,CAAC,IAAsB,EAA4B,EAAE;IACnF,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChF,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAC5B,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;QAC5B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QACrB,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAC1B,CAAC;IACF,MAAM,UAAU,GAA6B,EAAE,CAAC;IAEhD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAClF,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;QACzE,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;QAEnE,IACE,uBAAuB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAC9D,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAC9E,CAAC;YACD,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,IAAA,mBAAO,EAAC,UAAU,CAAC,CAAC;QACnC,MAAM,gBAAgB,GACpB,MAAM,KAAK,GAAG,IAAI,mBAAmB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzF,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC,CAAC;AA/BW,QAAA,gBAAgB,oBA+B3B","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { dirname } from 'node:path';\n\n/**\n * Support files of the tests (fixtures, helpers, probes) that no other file refers to. The second\n * step of the trim-tests method. The output is CANDIDATES: a fixture can be loaded by a path built at\n * run time or by scanning a folder, so a candidate is deleted only after the package suite is green\n * without it.\n */\nexport interface IDeadTestCodeCandidate {\n file: string;\n /**\n * `unreferenced`: neither its name nor its path under `__tests__` appears in another file, nor does\n * its folder. `folder-referenced`: only its folder path does, so a scan of that folder may load it.\n */\n kind: 'unreferenced' | 'folder-referenced';\n}\n\nconst SUPPORT_EXTENSION = /\\.(?:[cm]?[jt]sx?|json|sql|txt)$/;\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/;\n\nconst git = (opts: { root: string; args: string[] }): { status: number; stdout: string } => {\n const result = spawnSync('git', opts.args, {\n cwd: opts.root,\n encoding: 'utf8',\n maxBuffer: 256 * 1024 * 1024,\n });\n return { status: result.status ?? 1, stdout: result.stdout ?? '' };\n};\n\n/** Files other than `self` whose text contains `needle` (a fixed string). */\nconst referencedElsewhere = (opts: { root: string; needle: string; self: string }): boolean => {\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-F', '-e', opts.needle] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nconst escapeForRegex = (text: string): string => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/** Files other than `self` that mention the name of the file as a path segment or an import. */\nconst nameReferencedElsewhere = (opts: { root: string; stem: string; self: string }): boolean => {\n const pattern = `[/'\"\\`]${escapeForRegex(opts.stem)}(\\\\.|'|\"|\\`|$)`;\n const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nexport const findDeadTestCode = (opts: { root: string }): IDeadTestCodeCandidate[] => {\n const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\\n');\n const support = tracked.filter(\n (file) =>\n file.includes('/__tests__/') &&\n SUPPORT_EXTENSION.test(file) &&\n !TEST_FILE.test(file) &&\n !file.endsWith('.d.ts'),\n );\n const candidates: IDeadTestCodeCandidate[] = [];\n\n for (const file of support) {\n const underTests = file.slice(file.indexOf('/__tests__/') + '/__tests__/'.length);\n const stem = underTests.split('/').pop()!.replace(SUPPORT_EXTENSION, '');\n const withoutExtension = underTests.replace(SUPPORT_EXTENSION, '');\n\n if (\n nameReferencedElsewhere({ root: opts.root, stem, self: file }) ||\n referencedElsewhere({ root: opts.root, needle: withoutExtension, self: file })\n ) {\n continue;\n }\n\n const folder = dirname(underTests);\n const folderReferenced =\n folder !== '.' && referencedElsewhere({ root: opts.root, needle: folder, self: file });\n\n candidates.push({ file, kind: folderReferenced ? 'folder-referenced' : 'unreferenced' });\n }\n\n return candidates;\n};\n"]}
|