@venizia/dev-configs 0.4.8-0 → 0.4.8-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/README.md +60 -13
- package/dist/checks/catalog.d.ts +15 -2
- package/dist/checks/catalog.js +1 -0
- package/dist/checks/catalog.js.map +1 -1
- package/dist/checks/module-cycles.d.ts +2 -0
- package/dist/checks/module-cycles.js +2 -0
- package/dist/checks/module-cycles.js.map +1 -1
- package/dist/checks/purity/manifest.d.ts +6 -1
- package/dist/checks/purity/manifest.js.map +1 -1
- package/dist/checks/purity/probe.d.ts +4 -1
- package/dist/checks/purity/probe.js +1 -0
- package/dist/checks/purity/probe.js.map +1 -1
- package/dist/checks/purity/run.d.ts +3 -0
- package/dist/checks/purity/run.js +1 -0
- package/dist/checks/purity/run.js.map +1 -1
- package/dist/checks/split-report.d.ts +1 -0
- package/dist/checks/split-report.js +1 -0
- package/dist/checks/split-report.js.map +1 -1
- package/dist/checks/test-controls.d.ts +11 -2
- package/dist/checks/test-controls.js +1 -0
- package/dist/checks/test-controls.js.map +1 -1
- package/dist/checks/test-dead-code.d.ts +1 -0
- package/dist/checks/test-dead-code.js +1 -0
- package/dist/checks/test-dead-code.js.map +1 -1
- package/dist/checks/test-mutate.d.ts +14 -2
- package/dist/checks/test-mutate.js +1 -0
- package/dist/checks/test-mutate.js.map +1 -1
- package/dist/checks/test-weight.d.ts +3 -0
- package/dist/checks/test-weight.js +1 -0
- package/dist/checks/test-weight.js.map +1 -1
- package/dist/linters/house/max-params-own.rule.d.ts +3 -1
- package/dist/linters/house/max-params-own.rule.js +1 -1
- package/dist/linters/house/max-params-own.rule.js.map +1 -1
- package/dist/linters/oxlint.d.ts +6 -1
- package/dist/linters/oxlint.js.map +1 -1
- package/dist/linters/rules/no-unused-vars.rule.d.ts +6 -1
- package/dist/linters/rules/no-unused-vars.rule.js +1 -1
- package/dist/linters/rules/no-unused-vars.rule.js.map +1 -1
- package/dist/testing/rejection.helper.d.ts +1 -0
- package/dist/testing/rejection.helper.js +1 -0
- package/dist/testing/rejection.helper.js.map +1 -1
- package/dist/testing/repo-tmp.d.ts +1 -0
- package/dist/testing/repo-tmp.js +1 -0
- package/dist/testing/repo-tmp.js.map +1 -1
- package/dist/testing/spawn.helper.d.ts +1 -0
- package/dist/testing/spawn.helper.js +1 -0
- package/dist/testing/spawn.helper.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export declare class PurityVerdicts {
|
|
|
9
9
|
static readonly SCHEME_SET: Set<string>;
|
|
10
10
|
static isValid(value: string): value is TPurityVerdict;
|
|
11
11
|
}
|
|
12
|
+
/** One of the `PurityVerdicts` values: `pass`, `fail`, `waived`, `missing` or `stale-waiver`. */
|
|
12
13
|
export type TPurityVerdict = Extract<(typeof PurityVerdicts)[keyof typeof PurityVerdicts], string>;
|
|
13
14
|
/** One row of the `--json` verdict: nothing machine- or time-specific, so two runs on one commit diff empty. */
|
|
14
15
|
export interface IPurityRowVerdict {
|
|
@@ -22,12 +23,14 @@ export interface IPurityRowVerdict {
|
|
|
22
23
|
external?: string[];
|
|
23
24
|
buildError?: string;
|
|
24
25
|
}
|
|
26
|
+
/** Result of `PurityCheck.run`: the verdict rows and how many were checked, failed and waived. */
|
|
25
27
|
export interface IPurityReport {
|
|
26
28
|
rows: IPurityRowVerdict[];
|
|
27
29
|
failed: number;
|
|
28
30
|
waived: number;
|
|
29
31
|
checked: number;
|
|
30
32
|
}
|
|
33
|
+
/** The `devz check purity` command: probes every published entry of the claimed packages and reports a verdict per row. */
|
|
31
34
|
export declare class PurityCheck {
|
|
32
35
|
/**
|
|
33
36
|
* Probes every row of the claims and returns the report. `print` receives the human report line by
|
|
@@ -25,6 +25,7 @@ class PurityVerdicts {
|
|
|
25
25
|
}
|
|
26
26
|
exports.PurityVerdicts = PurityVerdicts;
|
|
27
27
|
const formatList = (values) => values.join(', ') || 'none';
|
|
28
|
+
/** The `devz check purity` command: probes every published entry of the claimed packages and reports a verdict per row. */
|
|
28
29
|
class PurityCheck {
|
|
29
30
|
/**
|
|
30
31
|
* Probes every row of the claims and returns the report. `print` receives the human report line by
|
|
@@ -1 +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 { PurityManifest } from './manifest';\nimport type { IPurityClaim, IPurityEntry, IPurityOptions } from './manifest';\nimport { PurityProbe } 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\nexport class PurityCheck {\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 */\n static async run(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 = PurityManifest.build({ claims, root, options });\n const rows: IPurityEntry[] = filter\n ? manifest.filter((row) => row.package === filter)\n : 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\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 PurityProbe.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 =\n 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(\n ` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`,\n );\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\n if (result.buildError) {\n print(` ${result.buildError.split('\\n')[0]}`);\n }\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}\n"]}
|
|
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 { PurityManifest } from './manifest';\nimport type { IPurityClaim, IPurityEntry, IPurityOptions } from './manifest';\nimport { PurityProbe } 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\n/** One of the `PurityVerdicts` values: `pass`, `fail`, `waived`, `missing` or `stale-waiver`. */\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\n/** Result of `PurityCheck.run`: the verdict rows and how many were checked, failed and waived. */\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/** The `devz check purity` command: probes every published entry of the claimed packages and reports a verdict per row. */\nexport class PurityCheck {\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 */\n static async run(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 = PurityManifest.build({ claims, root, options });\n const rows: IPurityEntry[] = filter\n ? manifest.filter((row) => row.package === filter)\n : 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\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 PurityProbe.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 =\n 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(\n ` ✗ ${row.label.padEnd(labelWidth)} STALE WAIVER - this entry is browser-pure now`,\n );\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\n if (result.buildError) {\n print(` ${result.buildError.split('\\n')[0]}`);\n }\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}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** The `devz check split-report` command: prints the file-splitting report for one package, or every package that has a `src/` folder. */
|
|
1
2
|
export declare class SplitReport {
|
|
2
3
|
/** `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
4
|
static run(opts: {
|
|
@@ -16,6 +16,7 @@ const HUB_EXPORT_MARK = 10;
|
|
|
16
16
|
const DECLARATION_KEYWORDS = ['class', 'const', 'type', 'interface', 'function', 'enum', 'let'];
|
|
17
17
|
const DECLARATION = new RegExp(`^export\\s+(?:default\\s+)?(?:abstract\\s+)?(${DECLARATION_KEYWORDS.join('|')})\\s+(\\w+)`, 'gm');
|
|
18
18
|
const EXPORTED_CLASS = /^export\s+(?:abstract\s+)?class\s+(\w+)/gm;
|
|
19
|
+
/** The `devz check split-report` command: prints the file-splitting report for one package, or every package that has a `src/` folder. */
|
|
19
20
|
class SplitReport {
|
|
20
21
|
/** `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
22
|
static run(opts) {
|
|
@@ -1 +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\n if (!existsSync(src)) {\n return;\n }\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\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\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\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\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\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\n return out;\n }\n\n private static foldersWithoutIndex(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\n if (!statSync(full).isDirectory()) {\n continue;\n }\n\n const hasTs = readdirSync(full).some(\n (entry) => entry.endsWith('.ts') && entry !== 'index.ts',\n );\n\n if (hasTs && !existsSync(join(full, 'index.ts'))) {\n out.push(full);\n }\n\n out.push(...SplitReport.foldersWithoutIndex({ dir: full }));\n }\n\n return out;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"split-report.js","sourceRoot":"","sources":["../../src/checks/split-report.ts"],"names":[],"mappings":";;;AAAA;;;;;GAKG;AACH,qCAA0E;AAC1E,yCAA8D;AAC9D,mDAA8C;AAE9C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;AACnF,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAChG,MAAM,WAAW,GAAG,IAAI,MAAM,CAC5B,gDAAgD,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAC3F,IAAI,CACL,CAAC;AACF,MAAM,cAAc,GAAG,2CAA2C,CAAC;AASnE,0IAA0I;AAC1I,MAAa,WAAW;IACtB,sJAAsJ;IACtJ,MAAM,CAAC,GAAG,CAAC,IAA8E;QACvF,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACvD,MAAM,GAAG,GAAG,IAAA,gBAAI,EAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAEzC,IAAI,CAAC,IAAA,oBAAU,EAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAA,oBAAQ,EAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAE9D,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CACvB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,QAAQ,CAAC,IAAA,oBAAQ,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,eAAe,CAC9F,CAAC;QACF,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAC9E,CAAC;QACF,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,QAAQ,CAAC,IAAA,oBAAQ,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAC1F,CAAC;QACF,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,cAAc,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,WAAW,CAAC,mBAAmB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,IAAA,gBAAI,EAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAA,oBAAU,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,2BAAW,CAAC,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9F,KAAK,CAAC,QAAQ,IAAA,oBAAQ,EAAC,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;QACnE,MAAM,QAAQ,GAAG,qBAAqB,eAAe,WAAW,CAAC;QACjE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QACjE,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,gCAAgC,CAAC;QACpD,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpF,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,MAAM,UAAU,GAAG,iCAAiC,CAAC;QACrD,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,MAAM,WAAW,GAAG,gCAAgC,CAAC;QACrD,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,oBAAQ,EAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;QACrE,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,MAAM,SAAS,GAAG,QAAQ,cAAc,4CAA4C,CAAC;QACrF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;QAChE,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACjD,KAAK,CAAC,sBAAsB,OAAO,KAAK,QAAQ,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,wEAAwE;IACxE,MAAM,CAAC,cAAc,CAAC,OAA0B,EAAE;QAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QACxC,MAAM,UAAU,GAAG,CAAC,MAAc,EAAY,EAAE,CAC9C,IAAA,oBAAU,EAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC/B,CAAC,CAAC,IAAA,qBAAW,EAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC;iBAC/B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;iBAC1C,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAA,oBAAU,EAAC,IAAA,gBAAI,EAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;YAClD,CAAC,CAAC,EAAE,CAAC;QAET,OAAO,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;IAChE,CAAC;IAEO,MAAM,CAAC,OAAO,CAAC,IAItB;QACC,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QAEnD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAEO,MAAM,CAAC,KAAK,CAAC,IAAsB;QACzC,MAAM,IAAI,GAAG,IAAA,sBAAY,EAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAE7C,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM;YAC9B,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM;YAC/C,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SACrE,CAAC;IACJ,CAAC;IAED,sFAAsF;IAC9E,MAAM,CAAC,kBAAkB,CAAC,IAA2B;QAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;QAE3C,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1F,CAAC;IAEO,MAAM,CAAC,IAAI,CAAC,IAAqB;QACvC,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAA,qBAAW,EAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnB,SAAS;YACX,CAAC;YAED,MAAM,IAAI,GAAG,IAAA,gBAAI,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAElC,IAAI,IAAA,kBAAQ,EAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACjC,GAAG,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YAC/C,CAAC;iBAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC;IAEO,MAAM,CAAC,mBAAmB,CAAC,IAAqB;QACtD,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAA,qBAAW,EAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnB,SAAS;YACX,CAAC;YAED,MAAM,IAAI,GAAG,IAAA,gBAAI,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAElC,IAAI,CAAC,IAAA,kBAAQ,EAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBAClC,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,IAAA,qBAAW,EAAC,IAAI,CAAC,CAAC,IAAI,CAClC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,UAAU,CACzD,CAAC;YAEF,IAAI,KAAK,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAA,gBAAI,EAAC,IAAI,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;YAED,GAAG,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,mBAAmB,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,GAAG,CAAC;IACb,CAAC;CACF;AA5ID,kCA4IC","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\n/** The `devz check split-report` command: prints the file-splitting report for one package, or every package that has a `src/` folder. */\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\n if (!existsSync(src)) {\n return;\n }\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\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\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\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\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\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\n return out;\n }\n\n private static foldersWithoutIndex(opts: { dir: string }): string[] {\n const out: string[] = [];\n const dirEntries = readdirSync(opts.dir);\n\n for (const name of dirEntries) {\n if (SKIP.has(name)) {\n continue;\n }\n\n const full = join(opts.dir, name);\n\n if (!statSync(full).isDirectory()) {\n continue;\n }\n\n const hasTs = readdirSync(full).some(\n (entry) => entry.endsWith('.ts') && entry !== 'index.ts',\n );\n\n if (hasTs && !existsSync(join(full, 'index.ts'))) {\n out.push(full);\n }\n\n out.push(...SplitReport.foldersWithoutIndex({ dir: full }));\n }\n\n return out;\n }\n}\n"]}
|
|
@@ -9,25 +9,34 @@ export interface IControlEntry {
|
|
|
9
9
|
/** Repository-relative path of the test file. */
|
|
10
10
|
file: string;
|
|
11
11
|
/**
|
|
12
|
-
* `test
|
|
12
|
+
* `test`: `file` is a test file and `titlePattern` is matched against its titles.
|
|
13
13
|
* `file`: `file` is a NEGATIVE FIXTURE, a file a test needs to be absent from the result (a class the
|
|
14
14
|
* scanner must ignore). It only has to exist: without it the test's "not found" assertion passes for the
|
|
15
15
|
* wrong reason.
|
|
16
|
+
*
|
|
17
|
+
* @default 'test'
|
|
16
18
|
*/
|
|
17
19
|
kind?: 'test' | 'file';
|
|
18
20
|
/** A regular expression matched against the TITLES (suite and case) in that file. Not used by `file` entries. */
|
|
19
21
|
titlePattern?: string;
|
|
20
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* At least this many titles must match.
|
|
24
|
+
*
|
|
25
|
+
* @default 1
|
|
26
|
+
*/
|
|
21
27
|
min?: number;
|
|
22
28
|
}
|
|
29
|
+
/** One registry entry that failed its check, with the reason. */
|
|
23
30
|
export interface IControlProblem {
|
|
24
31
|
entry: IControlEntry;
|
|
25
32
|
message: string;
|
|
26
33
|
}
|
|
34
|
+
/** Result of `TestControlsCheck.run`: how many registry entries were checked and which failed. */
|
|
27
35
|
export interface IControlsReport {
|
|
28
36
|
checked: number;
|
|
29
37
|
problems: IControlProblem[];
|
|
30
38
|
}
|
|
39
|
+
/** Verifies that every positive control in the registry still exists: its file is present and, for a test entry, enough test titles match its pattern. */
|
|
31
40
|
export declare class TestControlsCheck {
|
|
32
41
|
/**
|
|
33
42
|
* The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TestControlsCheck = void 0;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
|
+
/** Verifies that every positive control in the registry still exists: its file is present and, for a test entry, enough test titles match its pattern. */
|
|
6
7
|
class TestControlsCheck {
|
|
7
8
|
/**
|
|
8
9
|
* The titles in a test file: the first string argument of `test`, `it`, `describe` and of the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-controls.js","sourceRoot":"","sources":["../../src/checks/test-controls.ts"],"names":[],"mappings":";;;AAAA,qCAAmD;AACnD,yCAAiC;
|
|
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`: `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 * @default 'test'\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 /**\n * At least this many titles must match.\n *\n * @default 1\n */\n min?: number;\n}\n\n/** One registry entry that failed its check, with the reason. */\nexport interface IControlProblem {\n entry: IControlEntry;\n message: string;\n}\n\n/** Result of `TestControlsCheck.run`: how many registry entries were checked and which failed. */\nexport interface IControlsReport {\n checked: number;\n problems: IControlProblem[];\n}\n\n/** Verifies that every positive control in the registry still exists: its file is present and, for a test entry, enough test titles match its pattern. */\nexport class TestControlsCheck {\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 */\n static async listTitles(opts: { source: string; fileName?: string }): Promise<string[]> {\n const result = await import('typescript');\n const typescript: typeof ts = result.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. */\n static async run(opts: { root: string; controls: IControlEntry[] }): Promise<IControlsReport> {\n const problems: IControlProblem[] = [];\n\n for (const entry of opts.controls) {\n const path = join(opts.root, entry.file);\n\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 TestControlsCheck.listTitles({\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}\n"]}
|
|
@@ -12,6 +12,7 @@ export interface IDeadTestCodeCandidate {
|
|
|
12
12
|
*/
|
|
13
13
|
kind: 'unreferenced' | 'folder-referenced';
|
|
14
14
|
}
|
|
15
|
+
/** Finds support files under `__tests__` (fixtures, helpers) that no tracked file refers to, using `git grep` in the given repository. */
|
|
15
16
|
export declare class DeadTestCodeCheck {
|
|
16
17
|
static find(opts: {
|
|
17
18
|
root: string;
|
|
@@ -25,6 +25,7 @@ const nameReferencedElsewhere = (opts) => {
|
|
|
25
25
|
const result = git({ root: opts.root, args: ['grep', '-l', '-I', '-E', '-e', pattern] });
|
|
26
26
|
return result.stdout.split('\n').some((line) => line !== '' && line !== opts.self);
|
|
27
27
|
};
|
|
28
|
+
/** Finds support files under `__tests__` (fixtures, helpers) that no tracked file refers to, using `git grep` in the given repository. */
|
|
28
29
|
class DeadTestCodeCheck {
|
|
29
30
|
static find(opts) {
|
|
30
31
|
const tracked = git({ root: opts.root, args: ['ls-files'] }).stdout.split('\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"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","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\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\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\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\nexport class DeadTestCodeCheck {\n static find(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}\n"]}
|
|
1
|
+
{"version":3,"file":"test-dead-code.js","sourceRoot":"","sources":["../../src/checks/test-dead-code.ts"],"names":[],"mappings":"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","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\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\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\n return result.stdout.split('\\n').some((line) => line !== '' && line !== opts.self);\n};\n\n/** Finds support files under `__tests__` (fixtures, helpers) that no tracked file refers to, using `git grep` in the given repository. */\nexport class DeadTestCodeCheck {\n static find(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}\n"]}
|
|
@@ -16,11 +16,16 @@ export interface IMutant {
|
|
|
16
16
|
find: string;
|
|
17
17
|
replace: string;
|
|
18
18
|
}
|
|
19
|
+
/** One ledger line: a candidate test that the survivor test makes redundant, or one that proves nothing (`vacuous`). */
|
|
19
20
|
export interface ILedgerLine {
|
|
20
21
|
id: string;
|
|
21
22
|
/** Repository-relative test file that is deleted. */
|
|
22
23
|
candidate: string;
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Run only the cases whose name matches (`bun test -t`).
|
|
26
|
+
*
|
|
27
|
+
* @default the whole file
|
|
28
|
+
*/
|
|
24
29
|
candidateName?: string;
|
|
25
30
|
/** Repository-relative test file that stays. */
|
|
26
31
|
survivor: string;
|
|
@@ -32,13 +37,19 @@ export interface ILedgerLine {
|
|
|
32
37
|
mutant: IMutant | IMutant[];
|
|
33
38
|
/** The source line the mutant stands for, `path:line`; recorded, not used by the replay. */
|
|
34
39
|
srcLine?: string;
|
|
35
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* `red`: the candidate must fail. `vacuous`: it must stay green under `mutant` AND `secondMutant`.
|
|
42
|
+
*
|
|
43
|
+
* @default 'red'
|
|
44
|
+
*/
|
|
36
45
|
expectCandidate?: 'red' | 'vacuous';
|
|
37
46
|
secondMutant?: IMutant | IMutant[];
|
|
38
47
|
}
|
|
48
|
+
/** The mutation ledger: every line to replay. */
|
|
39
49
|
export interface ILedger {
|
|
40
50
|
mutants: ILedgerLine[];
|
|
41
51
|
}
|
|
52
|
+
/** Result of replaying one ledger line; `ok` is false when the claim does not hold, with the reason in `problem`. */
|
|
42
53
|
export interface ILedgerResult {
|
|
43
54
|
id: string;
|
|
44
55
|
survivorRed: boolean;
|
|
@@ -48,6 +59,7 @@ export interface ILedgerResult {
|
|
|
48
59
|
ok: boolean;
|
|
49
60
|
problem?: string;
|
|
50
61
|
}
|
|
62
|
+
/** Replays the mutation ledger of the trim-tests method: applies each break, runs the survivor and candidate tests, and restores the file. */
|
|
51
63
|
export declare class TestMutateCheck {
|
|
52
64
|
/** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */
|
|
53
65
|
static replayLedger(opts: {
|
|
@@ -95,6 +95,7 @@ const withMutants = (opts) => {
|
|
|
95
95
|
});
|
|
96
96
|
return apply(0);
|
|
97
97
|
};
|
|
98
|
+
/** Replays the mutation ledger of the trim-tests method: applies each break, runs the survivor and candidate tests, and restores the file. */
|
|
98
99
|
class TestMutateCheck {
|
|
99
100
|
/** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */
|
|
100
101
|
static replayLedger(opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-mutate.js","sourceRoot":"","sources":["../../src/checks/test-mutate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\n/**\n * Replays the mutation proof of the trim-tests method. A ledger line says: \"this test (the\n * candidate) is redundant because that test (the survivor) already catches the same break\". The\n * replay applies the break (the mutant) to a file, runs the survivor and the candidate, and puts the\n * file back. The survivor must go red. The candidate must go red too, unless the line says it is\n * vacuous and a second mutant leaves it green as well.\n *\n * The mutant edits the file the test really loads: the package's own `src` for a test that imports\n * through the `@/` alias, or the `dist` of the dependency for a test that reaches another package\n * through its `exports`. A rebuild is never needed.\n */\nexport interface IMutant {\n /** Repository-relative file to edit (a `src` file, or a `dist` file of a dependency). */\n file: string;\n /** Text that must occur exactly once in the file. */\n find: string;\n replace: string;\n}\n\nexport interface ILedgerLine {\n id: string;\n /** Repository-relative test file that is deleted. */\n candidate: string;\n /** Run only the cases whose name matches (`bun test -t`). Default: the whole file. */\n candidateName?: string;\n /** Repository-relative test file that stays. */\n survivor: string;\n survivorName?: string;\n /**\n * The break. An array applies every edit together: a survivor in the package that owns the code\n * loads its `src`, a candidate in another package loads that package's `dist`, so one break is two edits.\n */\n mutant: IMutant | IMutant[];\n /** The source line the mutant stands for, `path:line`; recorded, not used by the replay. */\n srcLine?: string;\n /** `red` (default): the candidate must fail. `vacuous`: it must stay green under `mutant` AND `secondMutant`. */\n expectCandidate?: 'red' | 'vacuous';\n secondMutant?: IMutant | IMutant[];\n}\n\nexport interface ILedger {\n mutants: ILedgerLine[];\n}\n\nexport interface ILedgerResult {\n id: string;\n survivorRed: boolean;\n candidateRed: boolean;\n /** Only for a `vacuous` line: whether the candidate also stayed green under the second mutant. */\n candidateGreenUnderSecond?: boolean;\n ok: boolean;\n problem?: string;\n}\n\nconst TEST_TIMEOUT_MILLISECONDS = 300_000;\n\n/** The one place this tool builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */\n// eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle\nconst replayError = (message: string): Error => new Error(message);\n\n/** The nearest folder at or above the test file that holds a `package.json`: where `bun test` runs. */\nconst packageDirectoryOf = (opts: { root: string; file: string }): string => {\n let current = dirname(join(opts.root, opts.file));\n\n for (;;) {\n if (existsSync(join(current, 'package.json'))) {\n return current;\n }\n\n const parent = dirname(current);\n\n if (parent === current || parent.length < opts.root.length) {\n return opts.root;\n }\n\n current = parent;\n }\n};\n\n/** Runs one test file (or the cases matching `name`) and reports whether it failed. */\nconst runIsRed = (opts: { root: string; file: string; name?: string }): boolean => {\n const directory = packageDirectoryOf({ root: opts.root, file: opts.file });\n const target = join(opts.root, opts.file).slice(directory.length + 1);\n const args = ['test', target, ...(opts.name ? ['-t', opts.name] : [])];\n const result = spawnSync('bun', args, {\n cwd: directory,\n encoding: 'utf8',\n timeout: TEST_TIMEOUT_MILLISECONDS,\n killSignal: 'SIGKILL',\n env: { ...process.env, NODE_ENV: 'test' },\n });\n\n if (result.error) {\n throw replayError(`[test-mutate] could not run bun test ${target}: ${result.error.message}`);\n }\n\n // A name filter that matches nothing runs no case and exits 0: that would read as \"green\" and could\n // hide a typo as a vacuous test, so a run that executed nothing is an unusable ledger line.\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;\n const passed = Number(/^\\s*(\\d+) pass/m.exec(output)?.[1] ?? 0);\n const failed = Number(/^\\s*(\\d+) fail/m.exec(output)?.[1] ?? 0);\n\n if (passed + failed === 0) {\n throw replayError(\n `[test-mutate] bun test ${target}${opts.name ? ` -t ${opts.name}` : ''} ran no case | check the file and the name`,\n );\n }\n\n return result.status !== 0 || failed > 0;\n};\n\n/** Applies `mutant`, runs `action`, and puts the file back byte for byte, verified byte for byte. */\nconst withMutant = <T>(opts: {\n root: string;\n mutant: IMutant;\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const path = join(opts.root, opts.mutant.file);\n const original = readFileSync(path);\n const text = original.toString('utf8');\n const occurrences = text.split(opts.mutant.find).length - 1;\n\n if (occurrences !== 1) {\n throw replayError(\n `[test-mutate][${opts.id}] \\`find\\` occurs ${occurrences} times in ${opts.mutant.file}, it must occur exactly once`,\n );\n }\n\n mkdirSync(opts.scratch, { recursive: true });\n const backup = join(opts.scratch, `test-mutate-${opts.id.replace(/[^\\w.-]/g, '_')}.orig`);\n\n if (existsSync(backup)) {\n throw replayError(\n `[test-mutate][${opts.id}] a backup from an interrupted run exists: ${backup} | restore ${opts.mutant.file} from it, then delete it`,\n );\n }\n\n writeFileSync(backup, original);\n\n let outcome: { value: T } | { error: unknown };\n\n try {\n writeFileSync(\n path,\n text.replace(opts.mutant.find, () => opts.mutant.replace),\n );\n outcome = { value: opts.action() };\n } catch (error) {\n outcome = { error };\n }\n\n // Restored whatever the action did, and checked, before its result or error is passed on.\n writeFileSync(path, original);\n\n if (!readFileSync(path).equals(original)) {\n throw replayError(\n `[test-mutate][${opts.id}] ${opts.mutant.file} was not restored | the backup is ${backup}`,\n );\n }\n\n rmSync(backup, { force: true });\n\n if ('error' in outcome) {\n throw outcome.error;\n }\n\n return outcome.value;\n};\n\n/** Applies every edit of `mutant`, runs `action`, and puts each file back byte for byte, verified byte for byte. */\nconst withMutants = <T>(opts: {\n root: string;\n mutant: IMutant | IMutant[];\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const edits = Array.isArray(opts.mutant) ? opts.mutant : [opts.mutant];\n\n const apply = (index: number): T =>\n index === edits.length\n ? opts.action()\n : withMutant({\n root: opts.root,\n mutant: edits[index],\n scratch: opts.scratch,\n id: `${opts.id}-${index}`,\n action: () => apply(index + 1),\n });\n\n return apply(0);\n};\n\nexport class TestMutateCheck {\n /** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */\n static replayLedger(opts: { root: string; ledger: ILedger; scratch?: string }): ILedgerResult[] {\n const root = resolve(opts.root);\n const scratch = opts.scratch ?? join(root, '.agents/local/tmp');\n const results: ILedgerResult[] = [];\n\n for (const line of opts.ledger.mutants) {\n const { survivorRed, candidateRed } = withMutants({\n root,\n mutant: line.mutant,\n scratch,\n id: line.id,\n action: () => ({\n survivorRed: runIsRed({ root, file: line.survivor, name: line.survivorName }),\n candidateRed: runIsRed({ root, file: line.candidate, name: line.candidateName }),\n }),\n });\n const result: ILedgerResult = { id: line.id, survivorRed, candidateRed, ok: true };\n const expectation = line.expectCandidate ?? 'red';\n\n if (!survivorRed) {\n result.ok = false;\n result.problem =\n 'the survivor stayed green under the mutant: it does not cover the behaviour';\n } else if (expectation === 'red' && !candidateRed) {\n result.ok = false;\n result.problem =\n 'the candidate stayed green under the mutant: record it as vacuous with a second mutant, or pick a mutant it checks';\n } else if (expectation === 'vacuous') {\n if (!line.secondMutant) {\n result.ok = false;\n result.problem = 'a vacuous line needs a secondMutant';\n } else if (candidateRed) {\n result.ok = false;\n result.problem = 'the candidate went red under the mutant: it is not vacuous';\n } else {\n const greenUnderSecond = !withMutants({\n root,\n mutant: line.secondMutant,\n scratch,\n id: `${line.id}-second`,\n action: () => runIsRed({ root, file: line.candidate, name: line.candidateName }),\n });\n result.candidateGreenUnderSecond = greenUnderSecond;\n\n if (!greenUnderSecond) {\n result.ok = false;\n result.problem = 'the candidate went red under the second mutant: it is not vacuous';\n }\n }\n }\n\n results.push(result);\n }\n\n return results;\n }\n\n /** The report lines of a replay, one per ledger line and a closing tally. */\n static formatLedgerResults(opts: { results: ILedgerResult[] }): string[] {\n const { results } = opts;\n const lines = results.map(\n (result) =>\n `${result.ok ? 'ok ' : 'FAIL'} ${result.id}: survivor ${result.survivorRed ? 'red' : 'GREEN'}, candidate ${result.candidateRed ? 'red' : 'green'}${result.problem ? ` | ${result.problem}` : ''}`,\n );\n const failed = results.filter((result) => !result.ok).length;\n lines.push(\n `\\n[test-mutate] ${results.length - failed}/${results.length} ledger lines hold${failed ? `, ${failed} do not` : ''}.`,\n );\n\n return lines;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test-mutate.js","sourceRoot":"","sources":["../../src/checks/test-mutate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\n/**\n * Replays the mutation proof of the trim-tests method. A ledger line says: \"this test (the\n * candidate) is redundant because that test (the survivor) already catches the same break\". The\n * replay applies the break (the mutant) to a file, runs the survivor and the candidate, and puts the\n * file back. The survivor must go red. The candidate must go red too, unless the line says it is\n * vacuous and a second mutant leaves it green as well.\n *\n * The mutant edits the file the test really loads: the package's own `src` for a test that imports\n * through the `@/` alias, or the `dist` of the dependency for a test that reaches another package\n * through its `exports`. A rebuild is never needed.\n */\nexport interface IMutant {\n /** Repository-relative file to edit (a `src` file, or a `dist` file of a dependency). */\n file: string;\n /** Text that must occur exactly once in the file. */\n find: string;\n replace: string;\n}\n\n/** One ledger line: a candidate test that the survivor test makes redundant, or one that proves nothing (`vacuous`). */\nexport interface ILedgerLine {\n id: string;\n /** Repository-relative test file that is deleted. */\n candidate: string;\n /**\n * Run only the cases whose name matches (`bun test -t`).\n *\n * @default the whole file\n */\n candidateName?: string;\n /** Repository-relative test file that stays. */\n survivor: string;\n survivorName?: string;\n /**\n * The break. An array applies every edit together: a survivor in the package that owns the code\n * loads its `src`, a candidate in another package loads that package's `dist`, so one break is two edits.\n */\n mutant: IMutant | IMutant[];\n /** The source line the mutant stands for, `path:line`; recorded, not used by the replay. */\n srcLine?: string;\n /**\n * `red`: the candidate must fail. `vacuous`: it must stay green under `mutant` AND `secondMutant`.\n *\n * @default 'red'\n */\n expectCandidate?: 'red' | 'vacuous';\n secondMutant?: IMutant | IMutant[];\n}\n\n/** The mutation ledger: every line to replay. */\nexport interface ILedger {\n mutants: ILedgerLine[];\n}\n\n/** Result of replaying one ledger line; `ok` is false when the claim does not hold, with the reason in `problem`. */\nexport interface ILedgerResult {\n id: string;\n survivorRed: boolean;\n candidateRed: boolean;\n /** Only for a `vacuous` line: whether the candidate also stayed green under the second mutant. */\n candidateGreenUnderSecond?: boolean;\n ok: boolean;\n problem?: string;\n}\n\nconst TEST_TIMEOUT_MILLISECONDS = 300_000;\n\n/** The one place this tool builds an `Error`: `getError` would make dev-configs depend on inversion, which depends on dev-configs. */\n// eslint-disable-next-line venizia/no-new-error -- getError would be a dev-configs -> inversion cycle\nconst replayError = (message: string): Error => new Error(message);\n\n/** The nearest folder at or above the test file that holds a `package.json`: where `bun test` runs. */\nconst packageDirectoryOf = (opts: { root: string; file: string }): string => {\n let current = dirname(join(opts.root, opts.file));\n\n for (;;) {\n if (existsSync(join(current, 'package.json'))) {\n return current;\n }\n\n const parent = dirname(current);\n\n if (parent === current || parent.length < opts.root.length) {\n return opts.root;\n }\n\n current = parent;\n }\n};\n\n/** Runs one test file (or the cases matching `name`) and reports whether it failed. */\nconst runIsRed = (opts: { root: string; file: string; name?: string }): boolean => {\n const directory = packageDirectoryOf({ root: opts.root, file: opts.file });\n const target = join(opts.root, opts.file).slice(directory.length + 1);\n const args = ['test', target, ...(opts.name ? ['-t', opts.name] : [])];\n const result = spawnSync('bun', args, {\n cwd: directory,\n encoding: 'utf8',\n timeout: TEST_TIMEOUT_MILLISECONDS,\n killSignal: 'SIGKILL',\n env: { ...process.env, NODE_ENV: 'test' },\n });\n\n if (result.error) {\n throw replayError(`[test-mutate] could not run bun test ${target}: ${result.error.message}`);\n }\n\n // A name filter that matches nothing runs no case and exits 0: that would read as \"green\" and could\n // hide a typo as a vacuous test, so a run that executed nothing is an unusable ledger line.\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;\n const passed = Number(/^\\s*(\\d+) pass/m.exec(output)?.[1] ?? 0);\n const failed = Number(/^\\s*(\\d+) fail/m.exec(output)?.[1] ?? 0);\n\n if (passed + failed === 0) {\n throw replayError(\n `[test-mutate] bun test ${target}${opts.name ? ` -t ${opts.name}` : ''} ran no case | check the file and the name`,\n );\n }\n\n return result.status !== 0 || failed > 0;\n};\n\n/** Applies `mutant`, runs `action`, and puts the file back byte for byte, verified byte for byte. */\nconst withMutant = <T>(opts: {\n root: string;\n mutant: IMutant;\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const path = join(opts.root, opts.mutant.file);\n const original = readFileSync(path);\n const text = original.toString('utf8');\n const occurrences = text.split(opts.mutant.find).length - 1;\n\n if (occurrences !== 1) {\n throw replayError(\n `[test-mutate][${opts.id}] \\`find\\` occurs ${occurrences} times in ${opts.mutant.file}, it must occur exactly once`,\n );\n }\n\n mkdirSync(opts.scratch, { recursive: true });\n const backup = join(opts.scratch, `test-mutate-${opts.id.replace(/[^\\w.-]/g, '_')}.orig`);\n\n if (existsSync(backup)) {\n throw replayError(\n `[test-mutate][${opts.id}] a backup from an interrupted run exists: ${backup} | restore ${opts.mutant.file} from it, then delete it`,\n );\n }\n\n writeFileSync(backup, original);\n\n let outcome: { value: T } | { error: unknown };\n\n try {\n writeFileSync(\n path,\n text.replace(opts.mutant.find, () => opts.mutant.replace),\n );\n outcome = { value: opts.action() };\n } catch (error) {\n outcome = { error };\n }\n\n // Restored whatever the action did, and checked, before its result or error is passed on.\n writeFileSync(path, original);\n\n if (!readFileSync(path).equals(original)) {\n throw replayError(\n `[test-mutate][${opts.id}] ${opts.mutant.file} was not restored | the backup is ${backup}`,\n );\n }\n\n rmSync(backup, { force: true });\n\n if ('error' in outcome) {\n throw outcome.error;\n }\n\n return outcome.value;\n};\n\n/** Applies every edit of `mutant`, runs `action`, and puts each file back byte for byte, verified byte for byte. */\nconst withMutants = <T>(opts: {\n root: string;\n mutant: IMutant | IMutant[];\n scratch: string;\n id: string;\n action: () => T;\n}): T => {\n const edits = Array.isArray(opts.mutant) ? opts.mutant : [opts.mutant];\n\n const apply = (index: number): T =>\n index === edits.length\n ? opts.action()\n : withMutant({\n root: opts.root,\n mutant: edits[index],\n scratch: opts.scratch,\n id: `${opts.id}-${index}`,\n action: () => apply(index + 1),\n });\n\n return apply(0);\n};\n\n/** Replays the mutation ledger of the trim-tests method: applies each break, runs the survivor and candidate tests, and restores the file. */\nexport class TestMutateCheck {\n /** Replays every line; never throws for a green survivor (that is a result), only for an unusable ledger. */\n static replayLedger(opts: { root: string; ledger: ILedger; scratch?: string }): ILedgerResult[] {\n const root = resolve(opts.root);\n const scratch = opts.scratch ?? join(root, '.agents/local/tmp');\n const results: ILedgerResult[] = [];\n\n for (const line of opts.ledger.mutants) {\n const { survivorRed, candidateRed } = withMutants({\n root,\n mutant: line.mutant,\n scratch,\n id: line.id,\n action: () => ({\n survivorRed: runIsRed({ root, file: line.survivor, name: line.survivorName }),\n candidateRed: runIsRed({ root, file: line.candidate, name: line.candidateName }),\n }),\n });\n const result: ILedgerResult = { id: line.id, survivorRed, candidateRed, ok: true };\n const expectation = line.expectCandidate ?? 'red';\n\n if (!survivorRed) {\n result.ok = false;\n result.problem =\n 'the survivor stayed green under the mutant: it does not cover the behaviour';\n } else if (expectation === 'red' && !candidateRed) {\n result.ok = false;\n result.problem =\n 'the candidate stayed green under the mutant: record it as vacuous with a second mutant, or pick a mutant it checks';\n } else if (expectation === 'vacuous') {\n if (!line.secondMutant) {\n result.ok = false;\n result.problem = 'a vacuous line needs a secondMutant';\n } else if (candidateRed) {\n result.ok = false;\n result.problem = 'the candidate went red under the mutant: it is not vacuous';\n } else {\n const greenUnderSecond = !withMutants({\n root,\n mutant: line.secondMutant,\n scratch,\n id: `${line.id}-second`,\n action: () => runIsRed({ root, file: line.candidate, name: line.candidateName }),\n });\n result.candidateGreenUnderSecond = greenUnderSecond;\n\n if (!greenUnderSecond) {\n result.ok = false;\n result.problem = 'the candidate went red under the second mutant: it is not vacuous';\n }\n }\n }\n\n results.push(result);\n }\n\n return results;\n }\n\n /** The report lines of a replay, one per ledger line and a closing tally. */\n static formatLedgerResults(opts: { results: ILedgerResult[] }): string[] {\n const { results } = opts;\n const lines = results.map(\n (result) =>\n `${result.ok ? 'ok ' : 'FAIL'} ${result.id}: survivor ${result.survivorRed ? 'red' : 'GREEN'}, candidate ${result.candidateRed ? 'red' : 'green'}${result.problem ? ` | ${result.problem}` : ''}`,\n );\n const failed = results.filter((result) => !result.ok).length;\n lines.push(\n `\\n[test-mutate] ${results.length - failed}/${results.length} ledger lines hold${failed ? `, ${failed} do not` : ''}.`,\n );\n\n return lines;\n }\n}\n"]}
|
|
@@ -11,6 +11,7 @@ export interface ITestWeightInput {
|
|
|
11
11
|
label: string;
|
|
12
12
|
xml: string;
|
|
13
13
|
}
|
|
14
|
+
/** Time and counts of one test file from a JUnit report. */
|
|
14
15
|
export interface ITestFileWeight {
|
|
15
16
|
label: string;
|
|
16
17
|
file: string;
|
|
@@ -21,6 +22,7 @@ export interface ITestFileWeight {
|
|
|
21
22
|
slowestCase: string;
|
|
22
23
|
slowestMilliseconds: number;
|
|
23
24
|
}
|
|
25
|
+
/** Result of `TestWeightCheck.analyseJunit`: totals, per-package sums, the heaviest files and the suspect ones. */
|
|
24
26
|
export interface ITestWeightReport {
|
|
25
27
|
files: ITestFileWeight[];
|
|
26
28
|
totalMilliseconds: number;
|
|
@@ -53,6 +55,7 @@ export interface ITestWeightReport {
|
|
|
53
55
|
skipped: number;
|
|
54
56
|
}>;
|
|
55
57
|
}
|
|
58
|
+
/** Reads JUnit reports of `bun test` and shows where the test time and the test count are. */
|
|
56
59
|
export declare class TestWeightCheck {
|
|
57
60
|
/** Reads the JUnit reports of a test run into one weight report: which files carry the time, and what looks wrong. */
|
|
58
61
|
static analyseJunit(opts: {
|
|
@@ -22,6 +22,7 @@ const FILE_SUITE = /<testsuite name="([^"]+\.(?:test|spec)\.[cm]?[jt]sx?)" file=
|
|
|
22
22
|
const CASE = /<testcase name="([^"]*)" classname="([^"]*)" time="([\d.eE+-]+)" file="([^"]+)"/g;
|
|
23
23
|
const LIGHT_FILE_MILLISECONDS = 100;
|
|
24
24
|
const HEAVY_SHARE = 0.8;
|
|
25
|
+
/** Reads JUnit reports of `bun test` and shows where the test time and the test count are. */
|
|
25
26
|
class TestWeightCheck {
|
|
26
27
|
/** Reads the JUnit reports of a test run into one weight report: which files carry the time, and what looks wrong. */
|
|
27
28
|
static analyseJunit(opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-weight.js","sourceRoot":"","sources":["../../src/checks/test-weight.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the JUnit report of `bun test --reporter=junit` and says where the test time and the test\n * count are: per file, per package, the heaviest files, duplicates by name, files that assert nothing,\n * and skips. The first step of the trim-tests method (`.agents/knowledge/process/trimming-tests.md`).\n *\n * The numbers are only as good as the run that made them: a `--parallel` run times each file under\n * contention, so a file that takes 7 s there can take 1 s alone. Re-time a suspect file alone.\n */\nexport interface ITestWeightInput {\n /** Names the report in the output: the package. */\n label: string;\n xml: string;\n}\n\nexport interface ITestFileWeight {\n label: string;\n file: string;\n milliseconds: number;\n cases: number;\n assertions: number;\n skipped: number;\n slowestCase: string;\n slowestMilliseconds: number;\n}\n\nexport interface ITestWeightReport {\n files: ITestFileWeight[];\n totalMilliseconds: number;\n totalCases: number;\n totalAssertions: number;\n totalSkipped: number;\n perLabel: Array<{ label: string; files: number; cases: number; milliseconds: number }>;\n /** Heaviest first. */\n heaviest: ITestFileWeight[];\n /** How many of the heaviest files hold 80% of the time. */\n filesFor80Percent: number;\n /** Files that take under 100 ms, and what they cost together. */\n lightFiles: { count: number; milliseconds: number };\n /** The same suite and case name in more than one file. */\n sameNameAcrossFiles: Array<{ name: string; files: string[] }>;\n zeroAssertionFiles: string[];\n skippedFiles: Array<{ file: string; skipped: number }>;\n}\n\nconst ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n};\n\nconst decodeEntities = (text: string): string =>\n text.replace(/&(#x[0-9a-fA-F]+|#\\d+|[a-z]+);/g, (whole, body: string) => {\n if (body.startsWith('#x')) {\n return String.fromCodePoint(Number.parseInt(body.slice(2), 16));\n }\n\n if (body.startsWith('#')) {\n return String.fromCodePoint(Number.parseInt(body.slice(1), 10));\n }\n\n return ENTITIES[body] ?? whole;\n });\n\n/** Bun writes a file as a top-level suite whose `name` equals its `file`. */\nconst FILE_SUITE =\n /<testsuite name=\"([^\"]+\\.(?:test|spec)\\.[cm]?[jt]sx?)\" file=\"[^\"]+\" tests=\"(\\d+)\" assertions=\"(\\d+)\" failures=\"\\d+\" skipped=\"(\\d+)\" time=\"([\\d.eE+-]+)\"/g;\nconst CASE = /<testcase name=\"([^\"]*)\" classname=\"([^\"]*)\" time=\"([\\d.eE+-]+)\" file=\"([^\"]+)\"/g;\n\nconst LIGHT_FILE_MILLISECONDS = 100;\nconst HEAVY_SHARE = 0.8;\n\nexport class TestWeightCheck {\n /** Reads the JUnit reports of a test run into one weight report: which files carry the time, and what looks wrong. */\n static analyseJunit(opts: { reports: ITestWeightInput[] }): ITestWeightReport {\n const files: ITestFileWeight[] = [];\n const names = new Map<string, Set<string>>();\n\n for (const { label, xml } of opts.reports) {\n const start = files.length;\n\n for (const match of xml.matchAll(FILE_SUITE)) {\n files.push({\n label,\n file: match[1],\n milliseconds: Number(match[5]) * 1000,\n cases: Number(match[2]),\n assertions: Number(match[3]),\n skipped: Number(match[4]),\n slowestCase: '',\n slowestMilliseconds: 0,\n });\n }\n\n const byFile = new Map(files.slice(start).map((entry) => [entry.file, entry]));\n\n for (const match of xml.matchAll(CASE)) {\n const caseName = decodeEntities(match[1]);\n const suiteName = decodeEntities(match[2]);\n const entry = byFile.get(match[4]);\n const milliseconds = Number(match[3]) * 1000;\n\n if (entry && milliseconds > entry.slowestMilliseconds) {\n entry.slowestMilliseconds = milliseconds;\n entry.slowestCase = caseName;\n }\n\n const key = `${suiteName} > ${caseName}`;\n const where = names.get(key) ?? new Set<string>();\n where.add(`${label}/${match[4]}`);\n names.set(key, where);\n }\n }\n\n const heaviest = [...files].sort((left, right) => right.milliseconds - left.milliseconds);\n const totalMilliseconds = files.reduce((sum, entry) => sum + entry.milliseconds, 0);\n\n let accumulated = 0;\n let filesFor80Percent = 0;\n\n for (const entry of heaviest) {\n accumulated += entry.milliseconds;\n filesFor80Percent += 1;\n\n if (accumulated >= totalMilliseconds * HEAVY_SHARE) {\n break;\n }\n }\n\n const light = files.filter((entry) => entry.milliseconds < LIGHT_FILE_MILLISECONDS);\n const labels = [...new Set(files.map((entry) => entry.label))];\n\n return {\n files,\n totalMilliseconds,\n totalCases: files.reduce((sum, entry) => sum + entry.cases, 0),\n totalAssertions: files.reduce((sum, entry) => sum + entry.assertions, 0),\n totalSkipped: files.reduce((sum, entry) => sum + entry.skipped, 0),\n perLabel: labels.map((label) => {\n const own = files.filter((entry) => entry.label === label);\n\n return {\n label,\n files: own.length,\n cases: own.reduce((sum, entry) => sum + entry.cases, 0),\n milliseconds: own.reduce((sum, entry) => sum + entry.milliseconds, 0),\n };\n }),\n heaviest,\n filesFor80Percent,\n lightFiles: {\n count: light.length,\n milliseconds: light.reduce((sum, entry) => sum + entry.milliseconds, 0),\n },\n sameNameAcrossFiles: [...names.entries()]\n .filter(([, where]) => where.size > 1)\n .map(([name, where]) => ({ name, files: [...where].sort() })),\n zeroAssertionFiles: files\n .filter((entry) => entry.assertions === 0)\n .map((entry) => entry.file),\n skippedFiles: files\n .filter((entry) => entry.skipped > 0)\n .map((entry) => ({ file: `${entry.label}/${entry.file}`, skipped: entry.skipped })),\n };\n }\n\n /** The report as the lines `devz check test-weight` prints. */\n static formatReport(opts: { report: ITestWeightReport; top?: number }): string[] {\n const { report, top = 25 } = opts;\n const seconds = (milliseconds: number): string => (milliseconds / 1000).toFixed(1);\n const lines: string[] = [\n `files ${report.files.length} | cases ${report.totalCases} | assertions ${report.totalAssertions} | skipped ${report.totalSkipped} | sum of file times ${seconds(report.totalMilliseconds)} s`,\n ];\n\n for (const row of report.perLabel) {\n lines.push(\n ` ${row.label.padEnd(14)} files ${String(row.files).padStart(4)} cases ${String(row.cases).padStart(5)} time ${seconds(row.milliseconds).padStart(7)} s`,\n );\n }\n\n lines.push('', `heaviest ${top} files:`);\n\n for (const entry of report.heaviest.slice(0, top)) {\n lines.push(\n `${entry.milliseconds.toFixed(0).padStart(8)} ms ${String(entry.cases).padStart(5)} cases slowest ${entry.slowestMilliseconds.toFixed(0).padStart(7)} ms ${entry.label}/${entry.file}`,\n );\n }\n\n lines.push(\n '',\n `80% of the time is in the ${report.filesFor80Percent} heaviest files of ${report.files.length}`,\n `files under ${LIGHT_FILE_MILLISECONDS} ms: ${report.lightFiles.count}, together ${seconds(report.lightFiles.milliseconds)} s`,\n `same suite and case name in more than one file: ${report.sameNameAcrossFiles.length}`,\n );\n\n for (const duplicate of report.sameNameAcrossFiles.slice(0, 10)) {\n lines.push(` ${duplicate.name.slice(0, 90)} -> ${duplicate.files.join(', ')}`);\n }\n\n lines.push(\n `files with 0 assertions: ${report.zeroAssertionFiles.length}${report.zeroAssertionFiles.length ? ` (${report.zeroAssertionFiles.slice(0, 3).join(', ')})` : ''}`,\n `files with skipped cases: ${report.skippedFiles.length} (${report.skippedFiles.reduce((sum, entry) => sum + entry.skipped, 0)} cases)`,\n );\n\n return lines;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test-weight.js","sourceRoot":"","sources":["../../src/checks/test-weight.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reads the JUnit report of `bun test --reporter=junit` and says where the test time and the test\n * count are: per file, per package, the heaviest files, duplicates by name, files that assert nothing,\n * and skips. The first step of the trim-tests method (`.agents/knowledge/process/trimming-tests.md`).\n *\n * The numbers are only as good as the run that made them: a `--parallel` run times each file under\n * contention, so a file that takes 7 s there can take 1 s alone. Re-time a suspect file alone.\n */\nexport interface ITestWeightInput {\n /** Names the report in the output: the package. */\n label: string;\n xml: string;\n}\n\n/** Time and counts of one test file from a JUnit report. */\nexport interface ITestFileWeight {\n label: string;\n file: string;\n milliseconds: number;\n cases: number;\n assertions: number;\n skipped: number;\n slowestCase: string;\n slowestMilliseconds: number;\n}\n\n/** Result of `TestWeightCheck.analyseJunit`: totals, per-package sums, the heaviest files and the suspect ones. */\nexport interface ITestWeightReport {\n files: ITestFileWeight[];\n totalMilliseconds: number;\n totalCases: number;\n totalAssertions: number;\n totalSkipped: number;\n perLabel: Array<{ label: string; files: number; cases: number; milliseconds: number }>;\n /** Heaviest first. */\n heaviest: ITestFileWeight[];\n /** How many of the heaviest files hold 80% of the time. */\n filesFor80Percent: number;\n /** Files that take under 100 ms, and what they cost together. */\n lightFiles: { count: number; milliseconds: number };\n /** The same suite and case name in more than one file. */\n sameNameAcrossFiles: Array<{ name: string; files: string[] }>;\n zeroAssertionFiles: string[];\n skippedFiles: Array<{ file: string; skipped: number }>;\n}\n\nconst ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n};\n\nconst decodeEntities = (text: string): string =>\n text.replace(/&(#x[0-9a-fA-F]+|#\\d+|[a-z]+);/g, (whole, body: string) => {\n if (body.startsWith('#x')) {\n return String.fromCodePoint(Number.parseInt(body.slice(2), 16));\n }\n\n if (body.startsWith('#')) {\n return String.fromCodePoint(Number.parseInt(body.slice(1), 10));\n }\n\n return ENTITIES[body] ?? whole;\n });\n\n/** Bun writes a file as a top-level suite whose `name` equals its `file`. */\nconst FILE_SUITE =\n /<testsuite name=\"([^\"]+\\.(?:test|spec)\\.[cm]?[jt]sx?)\" file=\"[^\"]+\" tests=\"(\\d+)\" assertions=\"(\\d+)\" failures=\"\\d+\" skipped=\"(\\d+)\" time=\"([\\d.eE+-]+)\"/g;\nconst CASE = /<testcase name=\"([^\"]*)\" classname=\"([^\"]*)\" time=\"([\\d.eE+-]+)\" file=\"([^\"]+)\"/g;\n\nconst LIGHT_FILE_MILLISECONDS = 100;\nconst HEAVY_SHARE = 0.8;\n\n/** Reads JUnit reports of `bun test` and shows where the test time and the test count are. */\nexport class TestWeightCheck {\n /** Reads the JUnit reports of a test run into one weight report: which files carry the time, and what looks wrong. */\n static analyseJunit(opts: { reports: ITestWeightInput[] }): ITestWeightReport {\n const files: ITestFileWeight[] = [];\n const names = new Map<string, Set<string>>();\n\n for (const { label, xml } of opts.reports) {\n const start = files.length;\n\n for (const match of xml.matchAll(FILE_SUITE)) {\n files.push({\n label,\n file: match[1],\n milliseconds: Number(match[5]) * 1000,\n cases: Number(match[2]),\n assertions: Number(match[3]),\n skipped: Number(match[4]),\n slowestCase: '',\n slowestMilliseconds: 0,\n });\n }\n\n const byFile = new Map(files.slice(start).map((entry) => [entry.file, entry]));\n\n for (const match of xml.matchAll(CASE)) {\n const caseName = decodeEntities(match[1]);\n const suiteName = decodeEntities(match[2]);\n const entry = byFile.get(match[4]);\n const milliseconds = Number(match[3]) * 1000;\n\n if (entry && milliseconds > entry.slowestMilliseconds) {\n entry.slowestMilliseconds = milliseconds;\n entry.slowestCase = caseName;\n }\n\n const key = `${suiteName} > ${caseName}`;\n const where = names.get(key) ?? new Set<string>();\n where.add(`${label}/${match[4]}`);\n names.set(key, where);\n }\n }\n\n const heaviest = [...files].sort((left, right) => right.milliseconds - left.milliseconds);\n const totalMilliseconds = files.reduce((sum, entry) => sum + entry.milliseconds, 0);\n\n let accumulated = 0;\n let filesFor80Percent = 0;\n\n for (const entry of heaviest) {\n accumulated += entry.milliseconds;\n filesFor80Percent += 1;\n\n if (accumulated >= totalMilliseconds * HEAVY_SHARE) {\n break;\n }\n }\n\n const light = files.filter((entry) => entry.milliseconds < LIGHT_FILE_MILLISECONDS);\n const labels = [...new Set(files.map((entry) => entry.label))];\n\n return {\n files,\n totalMilliseconds,\n totalCases: files.reduce((sum, entry) => sum + entry.cases, 0),\n totalAssertions: files.reduce((sum, entry) => sum + entry.assertions, 0),\n totalSkipped: files.reduce((sum, entry) => sum + entry.skipped, 0),\n perLabel: labels.map((label) => {\n const own = files.filter((entry) => entry.label === label);\n\n return {\n label,\n files: own.length,\n cases: own.reduce((sum, entry) => sum + entry.cases, 0),\n milliseconds: own.reduce((sum, entry) => sum + entry.milliseconds, 0),\n };\n }),\n heaviest,\n filesFor80Percent,\n lightFiles: {\n count: light.length,\n milliseconds: light.reduce((sum, entry) => sum + entry.milliseconds, 0),\n },\n sameNameAcrossFiles: [...names.entries()]\n .filter(([, where]) => where.size > 1)\n .map(([name, where]) => ({ name, files: [...where].sort() })),\n zeroAssertionFiles: files\n .filter((entry) => entry.assertions === 0)\n .map((entry) => entry.file),\n skippedFiles: files\n .filter((entry) => entry.skipped > 0)\n .map((entry) => ({ file: `${entry.label}/${entry.file}`, skipped: entry.skipped })),\n };\n }\n\n /** The report as the lines `devz check test-weight` prints. */\n static formatReport(opts: { report: ITestWeightReport; top?: number }): string[] {\n const { report, top = 25 } = opts;\n const seconds = (milliseconds: number): string => (milliseconds / 1000).toFixed(1);\n const lines: string[] = [\n `files ${report.files.length} | cases ${report.totalCases} | assertions ${report.totalAssertions} | skipped ${report.totalSkipped} | sum of file times ${seconds(report.totalMilliseconds)} s`,\n ];\n\n for (const row of report.perLabel) {\n lines.push(\n ` ${row.label.padEnd(14)} files ${String(row.files).padStart(4)} cases ${String(row.cases).padStart(5)} time ${seconds(row.milliseconds).padStart(7)} s`,\n );\n }\n\n lines.push('', `heaviest ${top} files:`);\n\n for (const entry of report.heaviest.slice(0, top)) {\n lines.push(\n `${entry.milliseconds.toFixed(0).padStart(8)} ms ${String(entry.cases).padStart(5)} cases slowest ${entry.slowestMilliseconds.toFixed(0).padStart(7)} ms ${entry.label}/${entry.file}`,\n );\n }\n\n lines.push(\n '',\n `80% of the time is in the ${report.filesFor80Percent} heaviest files of ${report.files.length}`,\n `files under ${LIGHT_FILE_MILLISECONDS} ms: ${report.lightFiles.count}, together ${seconds(report.lightFiles.milliseconds)} s`,\n `same suite and case name in more than one file: ${report.sameNameAcrossFiles.length}`,\n );\n\n for (const duplicate of report.sameNameAcrossFiles.slice(0, 10)) {\n lines.push(` ${duplicate.name.slice(0, 90)} -> ${duplicate.files.join(', ')}`);\n }\n\n lines.push(\n `files with 0 assertions: ${report.zeroAssertionFiles.length}${report.zeroAssertionFiles.length ? ` (${report.zeroAssertionFiles.slice(0, 3).join(', ')})` : ''}`,\n `files with skipped cases: ${report.skippedFiles.length} (${report.skippedFiles.reduce((sum, entry) => sum + entry.skipped, 0)} cases)`,\n );\n\n return lines;\n }\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Linter, Rule } from 'eslint';
|
|
2
2
|
import type { TRuleSeverity } from '../rules/common';
|
|
3
3
|
/**
|
|
4
|
-
* `venizia/max-params-own`. A function we write takes at most `max`
|
|
4
|
+
* `venizia/max-params-own`. A function we write takes at most `max` positional parameters;
|
|
5
5
|
* beyond that it takes one options object. A signature that belongs to someone else is not counted: an
|
|
6
6
|
* inline callback handed to a call (map, reduce, sort, a library handler) or to a JSX prop, a function a
|
|
7
7
|
* declared type fixes (`const x: TFn = (...)`, a member of a typed or `satisfies` object), and a
|
|
@@ -12,7 +12,9 @@ export declare class MaxParamsOwnRule {
|
|
|
12
12
|
static readonly DEFAULT_MAX = 5;
|
|
13
13
|
static readonly RULE: Rule.RuleModule;
|
|
14
14
|
static build(opts?: {
|
|
15
|
+
/** @default 'error' */
|
|
15
16
|
severity?: TRuleSeverity;
|
|
17
|
+
/** @default 5 */
|
|
16
18
|
max?: number;
|
|
17
19
|
}): Linter.RuleEntry;
|
|
18
20
|
}
|
|
@@ -45,7 +45,7 @@ const hasDeclaredShape = (object) => {
|
|
|
45
45
|
return false;
|
|
46
46
|
};
|
|
47
47
|
/**
|
|
48
|
-
* `venizia/max-params-own`. A function we write takes at most `max`
|
|
48
|
+
* `venizia/max-params-own`. A function we write takes at most `max` positional parameters;
|
|
49
49
|
* beyond that it takes one options object. A signature that belongs to someone else is not counted: an
|
|
50
50
|
* inline callback handed to a call (map, reduce, sort, a library handler) or to a JSX prop, a function a
|
|
51
51
|
* declared type fixes (`const x: TFn = (...)`, a member of a typed or `satisfies` object), and a
|